Как показывает статистика, Node.js становится все более популярным в качестве среды выполнения на стороне сервера, особенно для веб-сайтов с высоким трафиком. Кроме того, наличие нескольких сред делает его хорошей средой для быстрого прототипирования. Node.js имеет управляемую событиями архитектуру, использующую неблокирующий API ввода-вывода, который позволяет обрабатывать запросы асинхронно.
Одной из важных и часто менее заметных особенностей Node.js является его масштабируемость. Фактически, это является основной причиной, по которой некоторые крупные компании с большим трафиком интегрируют Node.js в свою платформу (например, Microsoft, Yahoo, Uber и Walmart) или даже полностью переносят свои операции на стороне сервера в Node.js (например, , PayPal, eBay и Groupon).
Каждый процесс Node.js выполняется в одном потоке, и по умолчанию он имеет ограничение памяти 512 МБ в 32-разрядных системах и 1 ГБ в 64-разрядных системах. Хотя ограничение памяти может быть увеличено до ~ 1 ГБ в 32-разрядных системах и ~ 1,7 ГБ в 64-разрядных системах , объем памяти и вычислительная мощность могут стать узкими местами для различных процессов.
Элегантное решение, которое Node.js предоставляет для масштабирования приложений, состоит в том, чтобы разделить один процесс на несколько процессов или рабочих , в терминологии Node.js. Это может быть достигнуто через кластерный модуль. Модуль кластера позволяет создавать дочерние процессы (рабочие), которые совместно используют все порты сервера с основным процессом Node (master).
В этой статье вы узнаете, как создать кластер Node.js для ускорения ваших приложений.
Модуль кластера Node.js: что это такое и как оно работает
Кластер — это пул похожих работников, работающих под родительским процессом Node. Рабочие порождаются с помощью метода fork()
child_processes
Это означает, что работники могут делиться дескрипторами сервера и использовать IPC (межпроцессное взаимодействие) для связи с родительским процессом Node.
Главный процесс отвечает за инициирование работников и контроль над ними. Вы можете создать произвольное количество рабочих в вашем мастер-процессе. Кроме того, помните, что по умолчанию входящие соединения распределяются в рамках кругового подхода среди рабочих (кроме Windows). На самом деле есть другой подход для распределения входящих соединений, который я не буду здесь обсуждать, который передает назначение ОС (по умолчанию в Windows). Документация Node.js предлагает использовать стиль циклического перебора по умолчанию в качестве политики планирования.
Хотя использование кластерного модуля в теории кажется сложным, его реализовать очень просто. Чтобы начать использовать его, вы должны включить его в приложение Node.js:
var cluster = require('cluster);
Модуль кластера выполняет один и тот же процесс Node.js несколько раз. Поэтому первое, что вам нужно сделать, — это определить, какая часть кода предназначена для главного процесса, а какая — для рабочих. Модуль кластера позволяет идентифицировать основной процесс следующим образом:
if(cluster.isMaster) { ... }
Главный процесс — это инициируемый вами процесс, который, в свою очередь, инициализирует рабочих. Чтобы запустить рабочий процесс внутри мастер-процесса, мы будем использовать метод fork()
cluster.fork();
Этот метод возвращает рабочий объект, который содержит некоторые методы и свойства для разветвленного рабочего. Мы увидим несколько примеров в следующем разделе.
Модуль кластера содержит несколько событий. Два общих события, связанные с моментами начала и окончания работы работников, — это события online
exit
online
exit
Позже мы увидим, как мы можем использовать эти два события для управления временем жизни рабочих.
Давайте теперь соберем все, что мы видели до сих пор, и покажем полный рабочий пример.
Примеры
В этом разделе представлены два примера. Первое — это простое приложение, показывающее, как кластерный модуль используется в приложении Node.js. Второй — это сервер Express, использующий преимущества кластерного модуля Node.js, который является частью производственного кода, который я обычно использую в крупных проектах. Оба примера можно скачать с GitHub .
Как модуль кластера используется в приложении Node.js
В этом первом примере мы настроили простой сервер, который отвечает на все входящие запросы сообщением, содержащим идентификатор рабочего процесса, который обработал запрос. Основной процесс раздваивает четырех рабочих. В каждом из них мы начинаем прослушивать порт 8000 для входящих запросов.
Код, который реализует то, что я только что описал, показан ниже:
var cluster = require('cluster');
var http = require('http');
var numCPUs = 4;
if (cluster.isMaster) {
for (var i = 0; i < numCPUs; i++) {
cluster.fork();
}
} else {
http.createServer(function(req, res) {
res.writeHead(200);
res.end('process ' + process.pid + ' says hello!');
}).listen(8000);
}
Вы можете протестировать этот сервер на своем компьютере, запустив его (запустите командный node simple.js
var cluster = require('cluster');
if(cluster.isMaster) {
var numWorkers = require('os').cpus().length;
console.log('Master cluster setting up ' + numWorkers + ' workers...');
for(var i = 0; i < numWorkers; i++) {
cluster.fork();
}
cluster.on('online', function(worker) {
console.log('Worker ' + worker.process.pid + ' is online');
});
cluster.on('exit', function(worker, code, signal) {
console.log('Worker ' + worker.process.pid + ' died with code: ' + code + ', and signal: ' + signal);
console.log('Starting a new worker');
cluster.fork();
});
} else {
var app = require('express')();
app.all('/*', function(req, res) {res.send('process ' + process.pid + ' says hello!').end();})
var server = app.listen(8000, function() {
console.log('Process ' + process.pid + ' is listening to all incoming requests');
});
}http://127.0.0.1:8000/ . Когда запросы получены, они распределяются по одному каждому работнику. Если рабочий доступен, он немедленно начинает обработку запроса; в противном случае он будет добавлен в очередь.
Есть несколько моментов, которые не очень эффективны в приведенном выше примере. Например, представьте, если работник умер по какой-то причине. В этом случае вы потеряете одного из своих работников, и если то же самое произойдет снова, вы получите главный процесс, в котором не будет работников для обработки входящих запросов. Другая проблема связана с количеством работников. В системах, в которых вы развертываете свое приложение, имеется разное количество ядер / потоков. В упомянутом примере, чтобы использовать все ресурсы системы, вы должны вручную проверить спецификации каждого сервера развертывания, найти количество доступных потоков и обновить его в своем коде. В следующем примере мы увидим, как сделать код более эффективным с помощью сервера Express.
Как разработать масштабируемый экспресс-сервер
Express — одна из самых популярных платформ веб-приложений для Node.js (если не самая популярная). На SitePoint мы освещали это несколько раз. Если вы хотите узнать больше об этом, я предлагаю вам прочитать статьи Создание API-интерфейсов RESTful с помощью Express 4 и создание веб-приложения Chatroom на основе Node.js. Express и Azure .
Этот второй пример показывает, как мы можем разработать хорошо масштабируемый сервер Express. Также демонстрируется, как выполнить миграцию одного сервера процессов, чтобы воспользоваться модулем кластера с несколькими строками кода.
os
Первое добавление к этому примеру — получение числа ядер ЦП с помощью модуля os
Модуль cpus()
exit
Используя этот подход, мы определяем количество рабочих для динамического разветвления, основываясь на спецификациях сервера, чтобы максимизировать использование.
Вторым и более важным дополнением является обработка смерти работника. Когда рабочий умирает, модуль кластера генерирует событие cluster.on('exit', callback);
Это может быть обработано, слушая событие и выполняя функцию обратного вызова, когда это испускается. Вы можете сделать это, написав оператор типа online
, В обратном вызове мы разветвляем нового работника, чтобы поддерживать намеченное количество работников. Это позволяет нам продолжать работу приложения, даже если есть некоторые необработанные исключения.
В этом примере я также установил прослушиватель для message
Это может быть использовано для регистрации или других операций.
Сравнение производительности
Существует несколько инструментов для сравнения API, но здесь я использую инструмент Apache Benchmark для анализа того, как использование кластерного модуля может повлиять на производительность вашего приложения.
Чтобы настроить тест, я разработал сервер Express, который имеет один маршрут и один обратный вызов для маршрута. При обратном вызове выполняется фиктивная операция, а затем возвращается короткое сообщение. Существует две версии сервера: одна без рабочих, в которой все происходит в основном процессе, а другая с 8 рабочими (так как моя машина имеет 8 ядер). В таблице ниже показано, как включение модуля кластера может увеличить количество обработанных запросов в секунду.
Параллельные соединения | 1 | 2 | 4 | 8 | 16 |
---|---|---|---|---|---|
Единый процесс | 654 | 711 | +783 | +776 | 754 |
8 рабочих | 594 | 1198 | 2110 | 3010 | 3024 |
(Запросы обрабатываются в секунду)
Расширенные операции
Хотя использование кластерных модулей относительно просто, существуют другие операции, которые вы можете выполнять с помощью рабочих. Например, вы можете достичь (почти!) Нулевого времени простоя в вашем приложении, используя кластерные модули. Мы увидим, как выполнить некоторые из этих операций в ближайшее время.
Общение между мастером и работниками
Иногда вам может понадобиться отправить сообщения от мастера к работнику, чтобы назначить задачу или выполнить другие операции. Взамен работникам может потребоваться сообщить мастеру, что задание выполнено. Чтобы прослушивать сообщения, слушатель события для worker.on('message', function(message) {
console.log(message);
});
worker
fork()
process.on('message', function(message) {
Чтобы прослушать сообщения от мастера в работнике:
console.log(message);
});
worker.send('hello from the master');
Сообщения могут быть строками или объектами JSON. Чтобы отправить сообщение от мастера конкретному работнику, вы можете написать код, подобный приведенному ниже:
process.send('hello from worker with id: ' + process.pid);
Точно так же, чтобы отправить сообщение от работника мастеру, вы можете написать:
worker.send({
type: 'task 1',
from: 'master',
data: {
// the data that you want to transfer
}
});
В Node.js сообщения являются общими и не имеют определенного типа. Поэтому рекомендуется отправлять сообщения в виде объектов JSON с некоторой информацией о типе сообщения, отправителе и самом содержимом. Например:
shutdown
Здесь важно отметить, что обратные вызовы событий сообщений обрабатываются асинхронно. Не существует определенного порядка исполнения. Вы можете найти полный пример общения между мастером и рабочими на GitHub .
Нулевое время простоя
Одним из важных результатов, который может быть достигнут с помощью рабочих, является (почти) нулевое время простоя серверов. В рамках основного процесса вы можете завершать и перезапускать рабочих по одному за раз после внесения изменений в свое приложение. Это позволяет запустить более старую версию при загрузке новой.
Чтобы иметь возможность перезапустить ваше приложение во время работы, вы должны помнить о двух моментах. Во-первых, основной процесс выполняется все время, и только рабочие завершаются и перезапускаются. Поэтому важно, чтобы ваш основной процесс был коротким и отвечал только за управление работниками.
Во-вторых, вам нужно как-то уведомить главный процесс о том, что он должен перезапустить рабочих. Есть несколько способов сделать это, включая ввод данных пользователем или просмотр файлов на предмет изменений. Последнее более эффективно, но вам нужно определить файлы для просмотра в мастер-процессе.
Мое предложение для перезапуска ваших работников — сначала попытаться безопасно их отключить; затем, если они благополучно не прекратят свою деятельность, заставят их убить. Вы можете сделать первое, отправив сообщение о workers[wid].send({type: 'shutdown', from: 'master'});
process.on('message', function(message) {
if(message.type === 'shutdown') {
process.exit(0);
}
});
И запустите безопасное отключение в обработчике событий рабочего сообщения:
workers
Чтобы сделать это для всех работников, вы можете использовать свойство работников модуля кластера, которое хранит ссылку на всех работающих работников. Мы также можем заключить все задачи в функцию в основной процесс, который можно вызывать всякий раз, когда мы хотим перезапустить всех рабочих.
function restartWorkers() {
var wid, workerIds = [];
for(wid in cluster.workers) {
workerIds.push(wid);
}
workerIds.forEach(function(wid) {
cluster.workers[wid].send({
text: 'shutdown',
from: 'master'
});
setTimeout(function() {
if(cluster.workers[wid]) {
cluster.workers[wid].kill('SIGKILL');
}
}, 5000);
});
};
Мы можем получить ID всех запущенных рабочих из workers
Этот объект сохраняет ссылку на все работающие работники и динамически обновляется, когда работники завершаются и перезапускаются. Сначала мы сохраняем идентификатор всех работающих работников в массиве workerIds
Таким образом, мы избегаем перезапуска вновь разветвленных рабочих.
Затем мы просим безопасного отключения от каждого работника. Если через 5 секунд работник все еще работает, и он все еще существует в workers
kill
Вы можете найти практический пример на GitHub .
Выводы
Приложения Node.js могут распараллеливаться с использованием кластерных модулей для более эффективного использования системы. Запуск нескольких процессов одновременно можно выполнить с помощью нескольких строк кода, и это делает миграцию относительно простой, поскольку Node.js обрабатывает сложную часть.
Как я показал в сравнении производительности, существует потенциал для заметного улучшения производительности приложений за счет более эффективного использования системных ресурсов. Помимо производительности, вы можете повысить надежность и время безотказной работы вашего приложения, перезапуская работников во время его работы.
При этом вы должны быть осторожны при рассмотрении использования кластерного модуля в вашем приложении. Основное рекомендуемое использование для модулей кластера — для веб-серверов. В других случаях вам необходимо тщательно изучить, как распределять задачи между работниками и как эффективно сообщать о прогрессе между работниками и мастером. Даже для веб-серверов убедитесь, что отдельный процесс Node.js является узким местом (память или ЦП), прежде чем вносить какие-либо изменения в свое приложение, так как вы можете вносить ошибки в свои изменения.
И последнее, на сайте Node.js есть отличная документация для модуля кластера . Так что не забудьте проверить это!