Для повышения производительности приложений на NodeJS во всех материалах рекомендуется использовать нодовский встроенный модуль cluster. Все статьи повторяют одно и то же, что и так написано в документации.
Вот официальный пример из доки:
Вот официальный пример из доки:
import cluster from 'node:cluster'; import http from 'node:http'; import { availableParallelism } from 'node:os'; import process from 'node:process'; const numCPUs = availableParallelism(); if (cluster.isPrimary) { console.log(`Primary ${process.pid} is running`); // Fork workers. for (let i = 0; i < numCPUs; i++) { cluster.fork(); } cluster.on('exit', (worker, code, signal) => { console.log(`worker ${worker.process.pid} died`); }); } else { // Workers can share any TCP connection // In this case it is an HTTP server http.createServer((req, res) => { res.writeHead(200); res.end('hello world\n'); }).listen(8000); console.log(`Worker ${process.pid} started`); }
А теперь поделюсь своими исследованиями и расскажу, что не так в этом примере и как это можно улучшить.
Во-первых, некорректен нейминг переменной numCPUs. На моём компьютере, на котором тестировал производительность, стоит процессор Intel i7-13700, в нём всего 16 ядер. Сайт Intel так и пишет:
Во-первых, некорректен нейминг переменной numCPUs. На моём компьютере, на котором тестировал производительность, стоит процессор Intel i7-13700, в нём всего 16 ядер. Сайт Intel так и пишет:
Обратите внимание: 16 ядер и 24 треда. И нода после вызова availableParallelism() возвращает число 24, а не 16. Так что это не ядра, а треды.
Во-вторых, большинство нодовских приложений - это обычные API. Какой-нибудь REST API, гоняющий туда-сюда JSON. А берётся этот JSON в большинстве случаев из базы данных вроде PostgreSQL. Многие сайты, описывающие работу с модулем cluster в качестве примера, почему-то возвращают моковые данные, захардкоженные прямо в коде, а в реальную базу данных не лезут. И потом делают нагрузочное тестирование по этим данным. Т.е. при таком подходе на каждом ядре сидит по процессу NodeJS и другие процессы не мешают их работе, не заставляют переключать на себя внимание ядра.
А теперь давайте проведём тест на реальной работе REST API. Это будет GET-запрос за сущностью по её id. Чтобы сформировать JSON этой сущности, нода совершает не один, а несколько сложных запросов в базу данных. Кэширование в глобальных переменных или в Redis не используется.
Для тестирования я использовал пакет bombardier, написанный на Golang.
Сначала провёл тест на Windows 10.
Запускаю:
Во-вторых, большинство нодовских приложений - это обычные API. Какой-нибудь REST API, гоняющий туда-сюда JSON. А берётся этот JSON в большинстве случаев из базы данных вроде PostgreSQL. Многие сайты, описывающие работу с модулем cluster в качестве примера, почему-то возвращают моковые данные, захардкоженные прямо в коде, а в реальную базу данных не лезут. И потом делают нагрузочное тестирование по этим данным. Т.е. при таком подходе на каждом ядре сидит по процессу NodeJS и другие процессы не мешают их работе, не заставляют переключать на себя внимание ядра.
А теперь давайте проведём тест на реальной работе REST API. Это будет GET-запрос за сущностью по её id. Чтобы сформировать JSON этой сущности, нода совершает не один, а несколько сложных запросов в базу данных. Кэширование в глобальных переменных или в Redis не используется.
Для тестирования я использовал пакет bombardier, написанный на Golang.
go install github.com/codesenberg/bombardier@latest
Сначала провёл тест на Windows 10.
Запускаю:
bombardier http://127.0.0.1:7100/api/some/1101Результат без cluster:
Bombarding http://127.0.0.1:7100/api/some/1101 for 10s using 125 connection(s) [=================================================================================================================] 10s Done! Statistics Avg Stdev Max Reqs/sec 2754.89 1413.90 5813.88 Latency 45.31ms 1.95ms 72.43ms HTTP codes: 1xx - 0, 2xx - 27625, 3xx - 0, 4xx - 0, 5xx - 0 others - 0 Throughput: 9.99MB/sРезультат с cluster (все 24 потока):
Bombarding http://127.0.0.1:7100/api/some/1101 for 10s using 125 connection(s) [=================================================================================================================] 10s Done! Statistics Avg Stdev Max Reqs/sec 8424.89 801.49 15177.67 Latency 14.84ms 1.69ms 72.64ms HTTP codes: 1xx - 0, 2xx - 84224, 3xx - 0, 4xx - 0, 5xx - 0 others - 0 Throughput: 30.54MB/sРазница по производительности почти в 3 раза.
Теперь протестирую на Linux. У меня Kubuntu 22.04, которая основана Ubuntu, которая в свою очередь основана на Debian.
Без кластера:
Далее решил использовать меньшее количество ядер.
4 треда из 24:
Результаты крайне интересные!
Наилучший результат дала настройка использовать только 6 тредов из 24!
Вывод: полная загрузка всех доступных тредов - это не всегда хорошо.
Без кластера:
Bombarding http://127.0.0.1:7100/api/some/1765 for 10s using 125 connection(s) [===========================================================================================================================================] 10s Done! Statistics Avg Stdev Max Reqs/sec 3658.34 468.19 4927.05 Latency 34.12ms 2.41ms 82.46ms HTTP codes: 1xx - 0, 2xx - 36686, 3xx - 0, 4xx - 0, 5xx - 0 others - 0 Throughput: 13.65MB/sС кластером, задействовав все ядра (24 треда):
Bombarding http://127.0.0.1:7100/api/some/1765 for 10s using 125 connection(s) [===========================================================================================================================================] 10s Done! Statistics Avg Stdev Max Reqs/sec 6051.28 798.68 8477.41 Latency 20.64ms 15.07ms 75.77ms HTTP codes: 1xx - 0, 2xx - 42280, 3xx - 0, 4xx - 0, 5xx - 18363 others - 0 Throughput: 16.45MB/sКак видно, прироста почти нет, да ещё 5** ошибки появились.
Далее решил использовать меньшее количество ядер.
4 треда из 24:
Bombarding http://127.0.0.1:7100/api/some/1765 for 10s using 125 connection(s) [===========================================================================================================================================] 10s Done! Statistics Avg Stdev Max Reqs/sec 8794.34 1576.80 12092.36 Latency 14.20ms 4.79ms 85.87ms HTTP codes: 1xx - 0, 2xx - 88038, 3xx - 0, 4xx - 0, 5xx - 0 others - 0 Throughput: 32.01MB/s6 тредов из 24:
Bombarding http://127.0.0.1:7100/api/some/1765 for 10s using 125 connection(s) [===========================================================================================================================================] 10s Done! Statistics Avg Stdev Max Reqs/sec 10930.22 1545.33 12906.65 Latency 11.43ms 2.61ms 68.95ms HTTP codes: 1xx - 0, 2xx - 109364, 3xx - 0, 4xx - 0, 5xx - 0 others - 0 Throughput: 39.77MB/s8 тредов из 24:
Bombarding http://127.0.0.1:7100/api/some/1765 for 10s using 125 connection(s) [===========================================================================================================================================] 10s Done! Statistics Avg Stdev Max Reqs/sec 9214.36 1776.33 13730.37 Latency 13.55ms 4.18ms 94.78ms HTTP codes: 1xx - 0, 2xx - 92211, 3xx - 0, 4xx - 0, 5xx - 0 others - 0 Throughput: 33.54MB/s12 тредов из 24:
Bombarding http://127.0.0.1:7100/api/some/1765 for 10s using 125 connection(s) [===========================================================================================================================================] 10s Done! Statistics Avg Stdev Max Reqs/sec 7738.82 1224.59 11498.82 Latency 16.13ms 4.67ms 65.34ms HTTP codes: 1xx - 0, 2xx - 62753, 3xx - 0, 4xx - 0, 5xx - 14766 others - 0 Throughput: 23.27MB/s
Результаты крайне интересные!
Наилучший результат дала настройка использовать только 6 тредов из 24!
Вывод: полная загрузка всех доступных тредов - это не всегда хорошо.