Как показывает статистика, 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();
Этот метод возвращает рабочий объект, который содержит некоторые методы и свойства для разветвленного рабочего. Мы увидим несколько примеров в следующем разделе.
Модуль кластера содержит несколько событий. Два общих события, связанные с моментами начала и окончания работы работников, — это события onlineexit 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.jsvar 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 секунд работник все еще работает, и он все еще существует в workerskill Вы можете найти практический пример на GitHub .
Выводы
Приложения Node.js могут распараллеливаться с использованием кластерных модулей для более эффективного использования системы. Запуск нескольких процессов одновременно можно выполнить с помощью нескольких строк кода, и это делает миграцию относительно простой, поскольку Node.js обрабатывает сложную часть.
Как я показал в сравнении производительности, существует потенциал для заметного улучшения производительности приложений за счет более эффективного использования системных ресурсов. Помимо производительности, вы можете повысить надежность и время безотказной работы вашего приложения, перезапуская работников во время его работы.
При этом вы должны быть осторожны при рассмотрении использования кластерного модуля в вашем приложении. Основное рекомендуемое использование для модулей кластера — для веб-серверов. В других случаях вам необходимо тщательно изучить, как распределять задачи между работниками и как эффективно сообщать о прогрессе между работниками и мастером. Даже для веб-серверов убедитесь, что отдельный процесс Node.js является узким местом (память или ЦП), прежде чем вносить какие-либо изменения в свое приложение, так как вы можете вносить ошибки в свои изменения.
И последнее, на сайте Node.js есть отличная документация для модуля кластера . Так что не забудьте проверить это!