Famabara

Эффективность использования всех ядер процессора в NodeJS с помощью cluster

dima
2 mo. ago
Для повышения производительности приложений на 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 так и пишет:
Обратите внимание: 16 ядер и 24 треда. И нода после вызова availableParallelism() возвращает число 24, а не 16. Так что это не ядра, а треды.

Во-вторых, большинство нодовских приложений - это обычные 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.

Без кластера:
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/s
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     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/s
8 тредов из 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/s
12 тредов из 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!

Вывод: полная загрузка всех доступных тредов - это не всегда хорошо.
+3
10
2
0
2 mo. ago
Еще можно на маке проверить
0
2 mo. ago
Не думаю, что что-то сильно изменится. Да и не важно это особо, у все серваки на линуксах.