Рубрики
Без рубрики

Настройка кластера Node.js

Мы все знаем, что Node.js отлично справляется с асинхронной обработкой множества событий, но многие не знают, что все это делается в одном потоке. Node.js фактически не является многопоточным, поэтому все эти запросы просто обрабатываются в цикле событий одного потока. Так почему бы не получить максимальную отдачу от вашего четырехъядерного процессора, используя кластер Node.js?

Мы все знаем, что 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 (if (cluster.isMaster) {…}). Единственная цель мастера здесь – создать всех рабочих (количество создаваемых рабочих зависит от количества доступных процессоров), а рабочие отвечают за запуск отдельных экземпляров сервера Express.

Когда рабочий отделяется от основного процесса, он выполняет код с самого начала модуля. Когда рабочий доходит до оператора if, он возвращает false для cluster.isMaster, поэтому вместо этого он создает приложение Express, маршрут, а затем прослушивает порт 8080. В случае четырехъядерного процессора у нас будет четыре порожденных воркера, все они будут слушать один и тот же порт для поступления запросов.

Но как распределяются запросы между воркерами? Очевидно, что все они не могут (и не должны) слушать и отвечать на каждый запрос, который мы получаем. Чтобы справиться с этим, в кластерный модуль встроен балансировщик нагрузки, который распределяет запросы между различными рабочими. В Linux и OSX (но не в Windows) по умолчанию действует политика round-robin (cluster.SCHED_RR). Единственный другой доступный вариант планирования – это оставить все на усмотрение операционной системы (cluster.SCHED_NONE), который используется по умолчанию в Windows.

Политика планирования может быть установлена либо в cluster.schedulingPolicy, либо с помощью переменной окружения NODE_CLUSTER_SCHED_POLICY (со значениями ‘rr‘ или ‘none‘).

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

Благодаря таким функциям кластеризация становится очень простой.

cluster.fork() vs child_process.fork()

Если у вас есть опыт работы с методом fork() в child_process, вы можете подумать, что cluster.fork() в чем-то похож на него (и во многом это так), поэтому в этом разделе мы объясним некоторые ключевые различия этих двух методов форкинга.

Существует несколько основных различий между cluster.fork() и child_process.fork(). Метод child_process.fork() немного ниже по уровню и требует передачи местоположения (пути к файлу) модуля в качестве аргумента, а также других необязательных аргументов, таких как текущий рабочий каталог, пользователь, которому принадлежит процесс, переменные окружения и многое другое.

Еще одно отличие заключается в том, что кластер начинает выполнение рабочего процесса с начала того же модуля, из которого он был запущен. Так что если точка входа вашего приложения – index.js, но рабочий порожден в cluster-my-app.js, то он все равно начнет выполнение с начала в index.js. child_process отличается тем, что он порождает выполнение в любом файле, который ему передан, а не обязательно в точке входа данного приложения.

Возможно, вы уже догадались, что модуль кластера фактически использует модуль child_process для создания дочерних приложений, что делается с помощью собственного метода fork() модуля child_process, позволяя им общаться через IPC, который является способом разделения хэндлов портов между рабочими.

Чтобы было понятно, форкинг в Node сильно отличается от форкинга в POISIX тем, что он не клонирует текущий процесс, но запускает новый экземпляр V8.

Хотя это один из самых простых способов многопоточности, его следует использовать с осторожностью. То, что вы можете породить 1 000 рабочих, не означает, что вы должны это делать. Каждый рабочий занимает системные ресурсы, поэтому порождайте только тех, кто действительно необходим. В документации Node говорится, что поскольку каждый дочерний процесс – это новый экземпляр V8, вам следует ожидать 30 мс времени запуска для каждого и не менее 10 Мб памяти на экземпляр.

Обработка ошибок

Что же делать, когда один (или несколько!) из ваших рабочих умирает? Весь смысл кластеризации теряется, если вы не можете перезапустить рабочих после их падения. К счастью для вас, модуль кластера расширяет EventEmitter и предоставляет событие ‘exit‘, которое сообщает вам, когда один из ваших дочерних рабочих умирает.

Вы можете использовать его для регистрации события и перезапуска процесса:

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 route, используя модуль sleep и возвращая пользователю кучу случайных данных.

Вот то же самое веб-приложение, но с кластеризацией:

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/

После выполнения этой команды для обеих версий приложения, вот некоторые из наиболее интересных результатов:

Нет кластеризации0,84 МБ / с58.691.18 секунд3467
Кластеризация (4 процесса)2,70 МБ / сек188.720,03 секунды11146

Как видите, кластерное приложение имеет примерно 3,2-кратное улучшение по сравнению с однопроцессным приложением практически по всем перечисленным показателям, за исключением среднего времени отклика, которое имеет гораздо более значительное улучшение.

Оригинал: “https://stackabuse.com/setting-up-a-node-js-cluster/”