Все мы знаем, что Node.js отлично справляется с асинхронной обработкой большого количества событий, но многие люди не знают, что все это делается в одном потоке. На самом деле Node.js не является многопоточным, поэтому все эти запросы просто обрабатываются в цикле событий одного потока.
Так почему бы не получить максимальную отдачу от четырехъядерного процессора с помощью кластера Node.js? Это запустит несколько экземпляров вашего кода для обработки еще большего количества запросов. Это может показаться немного сложным, но на самом деле это довольно просто сделать с помощью кластерного модуля, который был представлен в Node.js v0.8.
Очевидно, это полезно для любого приложения, которое может разделять работу между разными процессами, но особенно важно для приложений, которые обрабатывают множество запросов ввода-вывода, например веб-сайта.
К сожалению, из-за сложности параллельной обработки кластеризация приложения на сервере не всегда проста. Что вы делаете, когда вам нужно, чтобы несколько процессов прослушивали один и тот же порт? Напомним, что только один процесс может получить доступ к порту в любой момент времени. Наивное решение здесь - настроить каждый процесс для прослушивания на другом порту, а затем настроить Nginx для балансировки нагрузки между портами.
Это жизнеспособное решение, но оно требует гораздо больше работы по настройке и настройке каждого процесса, не говоря уже о настройке Nginx. С помощью этого решения вы просто добавляете себе больше вещей, которыми нужно управлять.
Вместо этого вы можете разделить главный процесс на несколько дочерних процессов (обычно с одним дочерним процессом на процессор). В этом случае дочерним элементам разрешено совместно использовать порт с родителем (благодаря межпроцессному взаимодействию или IPC ), поэтому нет необходимости беспокоиться об управлении несколькими портами.
Это именно то, что делает за вас cluster
Работа с кластерным модулем
Кластеризация приложения чрезвычайно проста, особенно для кода веб-сервера, такого как проекты Express. Все, что вам действительно нужно сделать, это следующее:
var cluster = require('cluster');
var express = require('express');
var numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
for (var i = 0; i < numCPUs; i++) {
// Create a worker
cluster.fork();
}
} else {
// Workers share the TCP connection in this server
var app = express();
app.get('/', function (req, res) {
res.send('Hello World!');
});
// All workers use this port
app.listen(8080);
}
Функциональные возможности кода разделены на две части: основной код и
рабочий код. Это делается в операторе if (cluster.isMaster) {...}
).
Единственная цель мастера здесь - создать всех воркеров (количество
созданных воркеров зависит от количества доступных процессоров), а
рабочие несут ответственность за запуск отдельных экземпляров сервера
Express.
Когда рабочий процесс отключается от основного процесса, он повторно
запускает код с начала модуля. cluster.isMaster
к оператору if, он
возвращает false
для cluster.isMaster, поэтому вместо этого он создает
приложение Express, маршрут, а затем прослушивает порт 8080
. В случае
с четырехъядерным процессором у нас было бы порождено четыре воркера,
каждый из которых слушал бы один и тот же порт для входящих запросов.
Но как распределяются запросы между рабочими? Очевидно, они не могут (и
не должны) все слушать и отвечать на каждый запрос, который мы получаем.
Чтобы справиться с этим, на самом деле в cluster
есть встроенный
балансировщик нагрузки, который обрабатывает распределение запросов
между различными исполнителями. В Linux и OSX (но не в Windows) по
умолчанию cluster.SCHED_RR
Единственный другой доступный вариант
планирования - оставить его на cluster.SCHED_NONE
операционной системы
(cluster.SCHED_NONE), что по умолчанию в Windows.
Политику планирования можно установить либо в cluster.schedulingPolicy
либо путем установки ее в переменной среды NODE_CLUSTER_SCHED_POLICY
(со значениями «rr» или «none»).
Вам также может быть интересно, как разные процессы могут использовать
один порт. Сложность запуска такого количества процессов, которые
обрабатывают сетевые запросы, заключается в том, что традиционно только
один может открыть порт одновременно. Большим преимуществом cluster
является то, что он обеспечивает совместное использование портов за вас,
поэтому любые открытые порты, например, для веб-сервера, будут доступны
для всех детей. Это делается через IPC, что означает, что мастер просто
отправляет дескриптор порта каждому исполнителю.
Благодаря таким функциям кластеризация становится очень простой.
cluster.fork () против child_process.fork ()
Если у вас есть предыдущий опыт работы с fork()
child_process
вы можете
подумать, что cluster.fork()
в чем-то похож (и они во многом похожи),
поэтому мы объясним некоторые ключевые различия в этих двух методах
разветвления. в этой секции.
Есть несколько основных различий между cluster.fork()
и
child_process.fork()
. Метод child_process.fork()
является немного
более низкоуровневым и требует, чтобы вы передавали местоположение (путь
к файлу) модуля в качестве аргумента, а также другие необязательные
аргументы, такие как текущий рабочий каталог, пользователь, владеющий
процессом, переменные среды , и больше.
Другое отличие состоит в том, что cluster
запускает выполнение
рабочего с начала того же модуля, из которого он запускался. Поэтому,
если точкой входа вашего приложения является index.js
, но рабочий
cluster-my-app.js
, он все равно начнет выполнение с самого начала с
index.js
. child_process
отличается тем, что запускает выполнение в
любом переданном ему файле, и не обязательно в точке входа данного
приложения.
Вы, возможно, уже догадались, что cluster
фактически использует
child_process
внизу для создания дочерних child_process
, что
делается с помощью собственного fork()
child_process, позволяющего им
общаться через IPC, что является тем, как дескрипторы портов разделяются
между рабочими.
Чтобы было ясно, форк в Node сильно отличается от форка POISIX тем, что он фактически не клонирует текущий процесс, но запускает новый экземпляр V8.
Хотя это один из самых простых способов многопоточности, его следует использовать с осторожностью. То, что вы можете создать 1000 рабочих, не означает, что вы должны это делать. Каждый воркер занимает системные ресурсы, поэтому порождайте только те, которые действительно необходимы. В документации по Node указано, что, поскольку каждый дочерний процесс является новым экземпляром V8, вам необходимо ожидать время запуска 30 мс для каждого и не менее 10 МБ памяти на каждый экземпляр.
Обработка ошибок
Итак, что вы делаете, когда один (или несколько!) Из ваших рабочих
умирает? Вся суть кластеризации теряется, если вы не можете
перезапустить воркеры после их сбоя. К счастью для вас, модуль cluster
EventEmitter
и предоставляет событие выхода, которое сообщает вам,
когда один из ваших дочерних рабочих процессов умирает.
Вы можете использовать это для регистрации события и перезапуска процесса:
cluster.on('exit', function(worker, code, signal) {
console.log('Worker %d died with code/signal %s. Restarting worker...', worker.process.pid, signal || code);
cluster.fork();
});
Теперь, после всего 4 строк кода, у вас есть собственный внутренний менеджер процессов!
Сравнение производительности
Хорошо, теперь самое интересное. Посмотрим, насколько нам действительно помогает кластеризация.
Для этого эксперимента я создал веб-приложение, подобное приведенному выше примеру кода. Но самая большая разница в том, что мы имитируем работу, выполняемую в рамках маршрута Express, используя модуль сна и возвращая пользователю кучу случайных данных.
Вот то же самое веб-приложение, но с кластеризацией:
var cluster = require('cluster');
var crypto = require('crypto');
var express = require('express');
var sleep = require('sleep');
var numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
for (var i = 0; i < numCPUs; i++) {
// Create a worker
cluster.fork();
}
} else {
// Workers share the TCP connection in this server
var app = express();
app.get('/', function (req, res) {
// Simulate route processing delay
var randSleep = Math.round(10000 + (Math.random() * 10000));
sleep.usleep(randSleep);
var numChars = Math.round(5000 + (Math.random() * 5000));
var randChars = crypto.randomBytes(numChars).toString('hex');
res.send(randChars);
});
// All workers use this port
app.listen(8080);
}
А вот «контрольный» код, по которому мы будем проводить сравнения. По
сути, это то же самое, только без cluster.fork()
:
var crypto = require('crypto');
var express = require('express');
var sleep = require('sleep');
var app = express();
app.get('/', function (req, res) {
// Simulate route processing delay
var randSleep = Math.round(10000 + (Math.random() * 10000));
sleep.usleep(randSleep);
var numChars = Math.round(5000 + (Math.random() * 5000));
var randChars = crypto.randomBytes(numChars).toString('hex');
res.send(randChars);
});
app.listen(8080);
Чтобы имитировать большую пользовательскую нагрузку, мы будем использовать инструмент командной строки под названием Siege , который мы можем использовать для выполнения нескольких одновременных запросов к URL-адресу по нашему выбору.
Siege также хорош тем, что отслеживает такие показатели производительности, как доступность, пропускная способность и скорость обработки запросов.
Вот команда Siege, которую мы будем использовать для тестов:
$ siege -c100 -t60s http://localhost:8080/
После выполнения этой команды для обеих версий приложения можно получить несколько наиболее интересных результатов:
Тип Всего обработано запросов Запросов в секунду Среднее время ответа Пропускная способность Без кластеризации 3467 58,69 1,18 с 0,84 МБ / с Кластеризация (4 процесса) 11146 188,72 0,03 секунды 2,70 МБ / с
Как видите, кластеризованное приложение примерно в 3,2 раза лучше по сравнению с однопроцессным приложением практически по всем перечисленным показателям, за исключением среднего времени отклика, которое имеет гораздо более значительное улучшение.