Для проверки производительности сервера можно использовать инструмент autocannon. Пример использования:
npx autocannon -c 200 -d 10 http://localhost:8080
В приведённом выше примере будет открыто 200 параллельных соединений на 10 секунд.
Обычное, однопоточное приложение может выглядеть следующим образом:
import { createServer } from 'http';
const { pid } = process;
const server = createServer((req, res) => {
// Имитируем интенсивную нагрузку на CPU
let i = 1e7; while (i > 0) { i--; };
console.log(`Handling request from ${pid}`);
res.end(`Hello from ${pid}\n`);
});
server.listen(8080, () => console.log(`Started at ${pid}`));
Для запуска web-приложения в кластерном режиме, мы может использовать встроенный модуль cluster:
import { createServer } from 'http';
import { cpus } from 'os';
import cluster from 'cluster';
if (cluster.isMaster) {
const availableCpus = cpus();
console.log(`Clustering to ${availableCpus.length} processes`);
// Запускаем отдельные Node.js-instances по количеству ядер процессора
availableCpus.forEach(() => cluster.fork());
} else {
const { pid } = process;
const server = createServer((req, res) => {
// Имитируем интенсивную нагрузку на CPU
let i = 1e7; while (i > 0) { i--; };
console.log(`Handling request from ${pid}`);
res.end(`Hello from ${pid}\n`);
});
server.listen(8080, () => console.log(`Started at ${pid}`));
}
Под капотом, cluster.fork() используется child_process.fork() API, это позволяет нам использовать коммуникационный канал между master-ом и worker-ами. Например, мастер может отправить всем worker-ам широковещательное сообщение:
Object.values(cluster.workers).forEach(worker => worke5r.send('Hello from the master'));
Для запуска web-сервера необходимо создать package.json, командой npm init
и добавить в файл строку:
"type": "module"
Загрузить зависимости можно командой npm install
.
При запуске AutoCannon можно увидеть следующий результат:
developer@developer-HP-ENVY-15-Notebook-PC:~/projects/Node-tests$ npx autocannon -c 200 -d 10 http://localhost:8080
Running 10s test @ http://localhost:8080
200 connections
┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐
│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │
├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤
│ Latency │ 258 ms │ 1359 ms │ 1487 ms │ 1629 ms │ 1291.43 ms │ 262.13 ms │ 1720 ms │
└─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐
│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤
│ Req/Sec │ 129 │ 129 │ 147 │ 149 │ 144.4 │ 5.54 │ 129 │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤
│ Bytes/Sec │ 17.9 kB │ 17.9 kB │ 20.4 kB │ 20.7 kB │ 20.1 kB │ 771 B │ 17.9 kB │
└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘
2k requests in 10.09s, 201 kB read
При запуске теста в кластерном режиме на двухядерном Intel® Core™ i5-3230M с поддеркой многопоточности, видим, что производительность выросла на ~50% и при этом ещё уменьшилась латентность:
┌─────────┬────────┬────────┬────────┬─────────┬───────────┬───────────┬─────────┐
│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │
├─────────┼────────┼────────┼────────┼─────────┼───────────┼───────────┼─────────┤
│ Latency │ 317 ms │ 660 ms │ 897 ms │ 1108 ms │ 682.91 ms │ 185.58 ms │ 3783 ms │
└─────────┴────────┴────────┴────────┴─────────┴───────────┴───────────┴─────────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Req/Sec │ 261 │ 261 │ 285 │ 288 │ 283.11 │ 7.48 │ 261 │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Bytes/Sec │ 36.3 kB │ 36.3 kB │ 39.6 kB │ 40.1 kB │ 39.4 kB │ 1.04 kB │ 36.3 kB │
└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘
3k requests in 10.1s, 394 kB read
Это может быть полезно для проверки управляющей инфраструктуры обеспечения высокой доступности приложения:
setTimeout(() => { throw new Error('Ooops'); }, Math.ceil(Math.random() * 3) * 1000);
Для того, чтобы отследить падение worker-а, следует отслеживать событие exit. При его получении можнно запустить нового worker-а:
if (cluster.isMaster) {
// ...
cluster.on('exit', (worker, code) => {
if (code !== 0 && !worker.exitedAfterDisconnect) {
console.log(`Wordr ${worker.process.pid} crashed. Starting a new worker`);
cluster.fork();
}
});
}
В принципе, можно не писать весь этот дополнительный код, а воспользоваться утилитой pm2.
Я подробно описал использование pm2 в проекте D820 с Orange Pi.
Легко заметить, что не существует существенных проблем с масштабированием stateless-приложений на Node.js. Достаточно легко добиваться как высокой нагрузки машины с большим количеством CPU, так и создать запустить несколько разных машин и распределять нагрузку между ними, используя балансировщик нагрузки.
Однако, если мы говорим о stateful-приложениях, то даже на одной машине, разные instances web-приложений Node.js запускаются как отдельные процессы, со своей собственной изолированной памятью. Это значит, что сессионная информация не будет распределяться между этими instances по умолчанию.
Проблема решается либо использование разделяемого между всеми instances хранилища состояний, в качестве которого могут быть использованы PostgreSQL, MongoDB, CouchDB, или, что ещё лучше - in-memory stores: Redis, или Memcached. Второе решение - использование Sticky load balancing, т.е. варианта, в котором балансировщик может каким-то образом привязать конкретного пользователя к конкретному instance Node.js. Однако, и со Sticky load balancing могут быть проблемы: использовать IP-адрес клиента не лучшая идея, т.к. клиент может распологаться за маршрутизатором и множество клиентов будут иметь для сервера один и тот же IP-адрес, что приведёт к неравномерной нагрузке разных instances. К тому же, IP-адрес клиента может меняться очень часто, например, при доступе к системе с мобильного телефона, который будет систематически получать другой IP-адрес по мере перемещения владельца между сотами.
Для некоторых вариантов сетевого взаимодействия, например, при использовании Socket.io, может быть использован только sticky load balancing.
Стоит заметить, что Sticky load balancing не поддерживается в cluster module. Но можно использовать другую библиотеку - sticky-session.
Node.js-based supervisors: forever и pm2
OS-based системы мониторинга: systemd и runit
Коммерческие системы мониторинга: M/Monit и supervisord
Container-based runtimes: Kubernetes, Nomad, Docker Swarm.
Английский термин - Dynamic horizontal scaling.
Современные облачные инфраструктуры позволяют использовать т.н. автомасштабирование - дополнительные instances поднимаются, или опускаются в зависимости от реальной потребности.
Для того, чтобы реализовать подобную схему, требуется использовать service registry - компонент, который хранит информацию о работающих в данным момент серверах. Эти информацию использует балансировщик нагрузки для того, чтобы распределять запросы по активным instances, а также каждый сервер передаёт информацию о себе сразу после того, как он загрузится. В соответствии с такой схемой могут работать как nginx, так и HAProxy. Например, для того, чтобы обновить конфигурацию nginx достаточно выполнить команду: nginx -s reload
В качестве сервиса регистрации сервером часто используется Consul. Совместно с Consul, можно использовать следующие компоненты:
- http-proxy для упрощения создания reverse proxy/load balancer в Node.js
- portfinder - package который ищет свободный порт в системе (TCP-порт)
- consul - package для подключения к сервису Consul
В книге "Node.js Design Patterns" (Third Edition) by Mario Casciaro и Licuano Mammino приводится пример, как реализовать такой подход в Node.js-приложении.
Однако, использование отдельного балансировщика - не единственный способ распределения нагрузки. Если нам не обязательно иметь единственную точку входа для сервисов (обычно единая точка требуется только для публичных сервисов), то мы можем использовать peer-to-peer load balacing. В этом случае, клиент знает некоторый стартовый набор instances и может сам переключаться с одного на другой. При этом, при подключении к конкретномсу instance, он может передавать информацию о добавлении новых peer-ов (instances, nodes) клиенту.
Данная схема хороша тем, что:
- снижает сложность инфраструктуры удаляя один из node
- повышает скорость обмена, потому что из цепочки доставки сообщений исчазает лишний node
- масштабируемость работает лучше, потому что она перестаёт зависеть от производительности и загрузки балансировщика нагрузки
Peer-to-peer load balancing является шаблоном, который активно используется в библиотеке ZeroMQ.