Как работает node.js. Блокирущий и неблокирущий I/O. Libuv

Что такое node js

Node.js - позволяет нам выполнять код вне браузера. Это серверная платформа для работы с javaScript через движок v8, что позволяет нам писать бэкенд. Мы можем написать и фронт и бек на одном javaScript.

  • Node.js может выполнять роль веб сервера.
  • Node.js умеет работать с внешними библиотеками, благодаря npm.
  • Node.js позволяет взаимодействовать с операционной системой и устройствами ввода/вывода.

    устройства ввода/вывода - это устройства с которых компьютер получает или передает информацию. Например компьютерная мышь и клавиатура это устройства ввода. А монитор и колонки - устройства вывода. Так же есть устройства ввода и вывода - это флешка например или жесткий диск.

  • Зная немного javaScript можно уже начинать изучение Node.js.
  • Благодаря асинхронности Node.js распределяет ресурсы грамотнее, что позволяет ему быть очень быстрым.

На чем построен node js

  • В первую очередь это движок v8 с помощью которого javaScript код преобразуется в машинный код.

  • А также libuv - это библиотека написанная на языке C, которая отвечает за асинхронный неблокирующий ввод/вывод операций в Node.js. Libuv позволяет сделать node.js кроссплатформенным. Именно libuv знает как работать на разных операционных системах. Мы используем например метод для работы с файловой системой, node.js внутри себя как раз будет использовать libuv и мы даже не будем задумываться на какой операционной системе находимся.

Модели ввода/вывода

Теперь немного поговорим про то, какие есть модели ввода/вывода. Как можно работать с компьютером на уровне операционной системы.

Блокирущий ввод и вывод(Input/Outup). Многопоточность

Мы выполняем команду за командой и если есть трудоёмкая операция, то поток будет заблокирован пока её не выполнит. То есть системный вызов(обращение к ядру операционной системы), ждет когда операция будет закончена. Для решения такой проблемы, операции выполняют в разных потоках. Один поток работает с базой данных, другой с файловой системой, третий работает с сетью и так далее. Так работают классические веб сервера на java.

blocking

У блокирующего ввода и вывода есть минусы:

  • Потребление большого количества ресурсов и сложность в управлении потоками.

  • Как видим потоки некоторое время находятся в состоянии простоя ожидая новых данных получаемых из связанных с ним соединений. При этом, в это время, они потребляют ресуры.

  • Чем больше у нас потоков, тем больше мы будем тратить времени на переключение контекста.

    Переключение контекста (context switch) - это процесс записи и восстановления состояния процесса или потока таким образом, чтобы в дальнейшем продолжить его выполнение с прерванного места.

  • Потоки занимают место в оперативной памяти.

Неблокирущий ввод и вывод

В таком режиме сервер работает с одним главным потоком. В неблокирующим режиме системные вызовы возвращают управление немедленно не дожидаясь чтения или записи данных. Основной шаблон реализации такого режима является активный опрос ресурса в цикле до тех пор, пока не будут возвращены какие либо фактические данные - это называется цикл ожидания. Поток при всем этом не блокируется.

busy waiting - Занят ожиданием или проще можно назвать как цикл ожидания — метод, при котором процесс многократно проверяет, верно ли условие, например, доступен ли ввод с клавиатуры.

С помощью такой техники уже можно обрабатывать разные ресурсы в одном потоке, но это все еще неэффективно. Хорошо, что большинство современных операционных систем предоставляют встроенный механизм для более эффективной работы неблокирующего I/O. Этот механизм называется синхронным демультиплексором событий. Он собирает и ставит в очередь события ввода-вывода, поступающие из набора отслеживаемых ресурсов, и блокирует их до тех пор, пока новые события не станут доступны для обработки.

noneBlocking

И тут мы переходим к более подробному разбору libuv.

Подробнее про libuv

Начнем с того, что node.js однопоточный. Но в основе node.js лежит libuv, который занимается операциями ввода и вывода и может управлять потоками, по умолчанию их 4. Нужно это для задач ввода/вывода, для работы с файловой системой, для шифрования и так далее. Так как javaScript по своей природе однопоточный и не готов к таким тяжелым операциям и работе с операционной системой. Все же он создавался для работы небольших скриптов на сайтах в браузере. Тут на помощь и приходит libuv.

// Пример не мой!!!
const crypto = require('crypto');

const start = Date.now(); // время начала

// используем функцию для шифровки пароля 1000000 - это кол итераций
crypto.pbkdf2('qwerty', '5', 1000000, 64, 'sha512', () => {
    console.log('1 закончил за', Date.now() - start ); // вычитаем из времени выполнения время начала скрипта.
});
crypto.pbkdf2('qwerty', '5', 1000000, 64, 'sha512', () => {
    console.log('2 закончил за', Date.now() - start );
});
crypto.pbkdf2('qwerty', '5', 1000000, 64, 'sha512', () => {
    console.log('3 закончил за', Date.now() - start );
});
crypto.pbkdf2('qwerty', '5', 1000000, 64, 'sha512', () => {
    console.log('4 закончил за', Date.now() - start );
});

// у нас получился такой вывод в миллисекундах
/*
1 закончил за 1128
2 закончил за 1196
3 закончил за 1247
4 закончил за 1269
*/

Как видим выполнение плюс минут за одно время, но если мы добавим еще одну операцию.

crypto.pbkdf2('qwerty', '5', 1000000, 64, 'sha512', () => {
    console.log('1 закончил за', Date.now() - start );
});
crypto.pbkdf2('qwerty', '5', 1000000, 64, 'sha512', () => {
    console.log('2 закончил за', Date.now() - start );
});
crypto.pbkdf2('qwerty', '5', 1000000, 64, 'sha512', () => {
    console.log('3 закончил за', Date.now() - start );
});
crypto.pbkdf2('qwerty', '5', 1000000, 64, 'sha512', () => {
    console.log('4 закончил за', Date.now() - start );
});
crypto.pbkdf2('qwerty', '5', 1000000, 64, 'sha512', () => {
    console.log('5 закончил за', Date.now() - start );
});


/*
2 закончил за 1086
1 закончил за 1095
3 закончил за 1115
4 закончил за 1139
5 закончил за 1888
*/

Пятый запуск отработал гораздо позже. Если добавим еще, то будет так же. Первые четыре выполнения всегда будут плюс минус равны, остальные уже будут отставать. Наши первые 4 вызова занимают 4 потока, 5 функция ждет и тот поток который освободится раньше всех, забирает на себя 5 функцию, этим и объясняется разница во времени.

Из чего состоит libuv

Libuv состоит из:

  1. Демультиплексор событий (Event Demultiplexor) или интерфейс уведомоления о событиях - Он принимает запрос из приложения и в него отправляется ресурс(ссылка на файл), операция и callback.
  2. Цикл событий (Event Loop) - бесконечный цикл который распределяет задачи.
  3. Очереди событий (Event Queue) - содержит в себе события и их обработчики(callback)

Все эти компоненты вместе образуют шаблон под названием reactor pattern.

Шаблон reactor pattern

Вот как работает этот шаблон:

reactor
  1. Приложение создает новую операцию ввода/вывода отправляя запрос в демультиплексор событий. Так же приложение указывает обработчик, который будет вызываться после завершения операции. При этом управление немедленно возвращается приложению.
  2. Когда набор операций ввода/вывода завершается, демультиплексор событий помещает новые события в очередь событий.
  3. В этот момент цикл событий обходит элементы в очереди событий.
  4. Для каждого события вызывается соответствующий обработчик который был указан для события.
  5. Обработчик возвращает управление циклу событий после завершения его выполнения (5а). Однако во время выполнения обработчика (5b) могут быть запрошены новые асинхронные операции, в результате чего новые операции будут отправлены в демультиплексор событий (1) до того, как управление будет возвращено циклу событий. Вся схема повторятеся пока, не будут обработаны все элементы из очереди.
  6. Были обработаны все элементы из очереди, цикл блокируется. Процедура начнется с самого начала с новым запросом.

Каждая операционная система имеет свой собственный интерфейс для демультиплексора событий: epoll в Linux, kqueue в Mac OS X и API I/O Completion Port (IOCP) в Windows. Поэтому основная команда Node.js создала библиотеку libuv с целью сделать Node.js совместимым со всеми основными платформами.

30.12.2022