Статьи

Как создать кластер Node.js для ускорения ваших приложений

Как показывает статистика, 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 есть отличная документация для модуля кластера . Так что не забудьте проверить это!