Масштабируйте свой сервер NodeJs для использования всех ресурсов

Node.js работает в однопоточном, неблокирующем режиме, работая как единый процесс в ЦП. Независимо от того, насколько мощный сервер используется и какие ресурсы используются, возможности однопоточного процесса ограничены. Node.js предназначен для создания распределенных приложений с несколькими узлами, отсюда и название Node.js.

Рабочая нагрузка - одна из основных причин, по которой мы начинаем масштабировать наше приложение, в том числе доступность и отказоустойчивость. Масштабирование можно выполнить несколькими способами, одним из самых простых доступных решений является клонирование. Мы можем выполнить клонирование с помощью Cluster Module, предоставляемого Node.js.

Прежде чем мы начнем обрабатывать запросы с помощью нашего сервера Node.Js, использующего ресурсы, давайте рассмотрим основы работы модуля Cluster.

Как работает кластерный модуль?

В кластерном модуле есть два типа процессов: Master и Worker. Все входящие запросы обрабатываются главным процессом, и главный процесс решает, какой рабочий должен обрабатывать входящие запросы. Рабочий процесс можно рассматривать как обычный сервер с одним экземпляром Node.Js, который обслуживает запросы.

Как Мастер-процесс распределяет входящие соединения?

  1. Первый метод (и метод по умолчанию на всех платформах, кроме Windows) - это циклический подход, когда главный процесс прослушивает порт, принимает новые соединения и распределяет их между рабочими процессами в циклическом режиме, с некоторыми встроенный ум, чтобы не перегружать рабочий процесс.

2. Второй подход заключается в том, что главный процесс создает прослушивающий сокет и отправляет его заинтересованным исполнителям. Затем рабочие принимают входящие соединения напрямую.

Второй подход теоретически должен давать наилучшие результаты. Однако на практике распределение имеет тенденцию быть очень несбалансированным из-за капризов планировщика операционной системы. Наблюдались нагрузки, при которых более 70% всех подключений завершались всего двумя процессами из восьми.

Создание простого сервера Node.js

Давайте создадим базовый сервер Node.js, который обрабатывает запрос:

/*** server.js ***/
const http = require(“http”);
// get the process ID of Node Server
const processId = process.pid;
// Creating server and handling request
const server = http.createServer((req, res) => {
    // Simulate CPU Work
    for (let index = 0; index < 1e7; index++);
    
    res.end(`Process handled by pid: ${processId}`);
});
// start server and listen the request
server.listen(8080, () => {
    console.log(`Server Started in process ${processId}`);
});

Сервер дает нам следующий ответ:

Нагрузочное тестирование этого простого сервера Node.Js:

Мы собираемся использовать ApacheBench Tool, есть другие инструменты, которые проводят подобное тестирование. Их можно использовать по собственному выбору.

Мы собираемся поразить наш сервер Node.js 500 одновременными запросами в течение 10 секунд.

➜ test_app ab -c 500 -t 10 http://localhost:8080/
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient)
Finished 3502 requests
Server Software: 
Server Hostname: localhost
Server Port: 8080
Document Path: /
Document Length: 29 bytes
Concurrency Level: 500
Time taken for tests: 11.342 seconds
Complete requests: 3502
Failed requests: 0
Total transferred: 416104 bytes
HTML transferred: 116029 bytes
Requests per second: 308.76 [#/sec] (mean)
Time per request: 1619.385 [ms] (mean)
Time per request: 3.239 [ms] (mean, across all concurrent requests)
Transfer rate: 35.83 [Kbytes/sec] received
Connection Times (ms)
 min mean[+/-sd] median max
Connect: 0 6 3.7 5 17
Processing: 21 1411 193.9 1412 2750
Waiting: 4 742 395.9 746 1424
Total: 21 1417 192.9 1420 2750
Percentage of the requests served within a certain time (ms)
 50% 1420
 66% 1422
 75% 1438
 80% 1438
 90% 1624
 95% 1624
 98% 1624
 99% 1625
 100% 2750 (longest request)

На этом простом сервере на уровне 500 одновременных запросов было обработано всего 3502 запроса. И было выполнено 308 запросов в секунду с временем запроса 1619 (мс).

Количество обработанных запросов хорошее, и оно должно работать для большинства малых и средних приложений. Но мы не полностью использовали ресурсы, и большая часть доступных ресурсов простаивает.

Реализация кластерного модуля

Теперь, когда мы готовы к работе, давайте реализуем кластер для нашего сервера.

/** cluster.js **/
const os = require(“os”);
const cluster = require(“cluster”);
if (cluster.isMaster) {
    const number_of_cpus = os.cpus().length;
   
    console.log(`Master ${process.pid} is running`);
    console.log(`Forking Server for ${number_of_cpus} CPUs\n`);
    // Create a Worker Process for each Available CPU
    for (let index = 0; index < number_of_cpus; index++) {
        cluster.fork();
    }
    // When Worker process has died, Log the worker
    cluster.on(“exit”, (worker, code, signal) => {
        console.log(`\nWorker ${worker.process.pid} died\n`);
    });
} else {
    // if Worker process, master is false, cluster.isWorker is true
    // worker starts server for individual cpus
    // the worker created above is starting server 
    require(“./server”);
}

Мой персональный компьютер с i7 8-го поколения имеет 8 ядер процессора. Учитывая, что большинство доступных в настоящее время процессоров имеют как минимум двухъядерный процессор, ресурсы для оставшихся 7 ядер бездействовали *.

Теперь давайте запустим созданный файл cluster.js, сервер выдаст нам следующий ответ:

Если вы реализовали вышеупомянутый кластер, вы будете использовать всю мощность процессора / сервера. Запрос обрабатывается главным процессом с того же порта сервера, который будет обслуживаться любым из 8 рабочих процессов (серверов).

Выполнено нагрузочное тестирование с кластером:

➜  test_app ab -c 500 -t 10  http://localhost:8080/
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient)
Completed 5000 requests
Completed 10000 requests
Completed 15000 requests
Completed 20000 requests
Finished 20374 requests
Server Software:        
Server Hostname:        localhost
Server Port:            8080
Document Path:          /
Document Length:        29 bytes
Concurrency Level:      500
Time taken for tests:   10.000 seconds
Complete requests:      20374
Failed requests:        0
Total transferred:      2118896 bytes
HTML transferred:       590846 bytes
Requests per second:    2037.39 [#/sec] (mean)
Time per request:       245.412 [ms] (mean)
Time per request:       0.491 [ms] (mean, across all concurrent requests)
Transfer rate:          206.92 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   1.3      0      12
Processing:     6  242  15.6    241     369
Waiting:        6  242  15.5    241     368
Total:         18  242  15.5    241     371
Percentage of the requests served within a certain time (ms)
  50%    241
  66%    244
  75%    246
  80%    247
  90%    251
  95%    259
  98%    283
  99%    290
 100%    371 (longest request)

Вспомните приведенный выше простой сервер Node.js, который обрабатывал 308 запросов в секунду в нашем нагрузочном тесте, теперь это число увеличилось до 2037 запросов. В секунду - это значительный 6-кратный рост количества обработанных запросов. Кроме того, ранее Время на запрос составляло 1619 мс, теперь оно было уменьшено до 245 мс. Раньше мы обслуживали 3502 запроса, теперь оно увеличилось до всего 20374 запроса (это увеличение в 5,8 раза). Если вы посмотрите на реализацию выше, это значительное улучшение вызвано 10 строками кода. И нам также не пришлось реорганизовывать существующий код сервера.

Доступность и нулевое время простоя

В восторге от достигнутого нами прогресса, теперь он становится лучше.

Когда у нас есть один экземпляр сервера, и этот сервер дает сбой. При перезапуске сервера будет простой. Даже если процесс автоматизирован, будет задержка, и ни один запрос не может быть обработан за это время.

Имитация сбоя сервера:

/*** server.js ***/
const http = require(“http”);
// get the process ID of Node Server
const processId = process.pid;
// Creating server and handling request
const server = http.createServer((req, res) => {
    // Simulate CPU Work
    for (let index = 0; index < 1e7; index++);
    
    res.end(`Process handled by pid: ${processId}`);
});
// start server and listen the request
server.listen(8080, () => {
    console.log(`Server Started in process ${processId}`);
});
// Warning: Only For Testing and Visualization Purpose
// Don't add the code below in production
// Let's simulate Server Randomly Crashing using process.exit()
setTimeout(() => {
    process.exit(1);
}, Math.random() * 10000);

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

Теперь помните, что это тот случай, когда у нас есть 8 серверов, созданных нашим кластерным модулем. Когда у нас есть один экземпляр сервера, и он дает сбой, запросы не могут быть обслужены за это время.

➜  test_app node cluster.js
Master 63104 is running
Forking Server for 8 CPUs
Server Started in process 63111
Server Started in process 63118
Server Started in process 63112
Server Started in process 63130
Server Started in process 63119
Server Started in process 63137
Server Started in process 63142
Server Started in process 63146
Worker 63142 died
Worker 63112 died
Worker 63111 died
Worker 63146 died
Worker 63119 died
Worker 63130 died
Worker 63118 died
Worker 63137 died
➜  test_app

Минимальное время простоя

Когда у нас есть несколько экземпляров сервера, доступность сервера может быть легко увеличена.

Давайте откроем наш файл cluster.js и добавим выделенный код в cluster.js:

/** cluster.js **/
const os = require(“os”);
const cluster = require(“cluster”);
if (cluster.isMaster) {
    const number_of_cpus = os.cpus().length;
   
    console.log(`Master ${process.pid} is running`);
    console.log(`Forking Server for ${number_of_cpus} CPUs\n`);
    // Create a Worker Process for each Available CPU
    for (let index = 0; index < number_of_cpus; index++) {
        cluster.fork();
    }
    // When Worker process has died, Log the worker
    cluster.on(“exit”, (worker, code, signal) => {
        /**
        * The condition checks if worker actually crashed and
        * wasn't manually disconnected or killed by master process.
        *
        * The condition can be changed by desired error code,
        * and condition.
        */
        if (code !== 0 && !worker.exitedAfterDisconnect) {
            console.log(`Worker ${worker.process.pid} died`);
            cluster.fork();
        }
    });
} else {
    // if Worker process, master is false, cluster.isWorker is true
    // worker starts server for individual cpus
    // the worker created above is starting server
    require(“./server”);
}

Выполнено нагрузочное тестирование с перезапуском сервера:

Давайте запустим сервер с внесенными изменениями в кластер (Run: node cluster.js). Теперь давайте откроем наш инструмент тестирования и начнем тестировать наш сервер.

➜  test_app ab -c 500 -t 10 -r http://localhost:8080/
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient)
Completed 5000 requests
Completed 10000 requests
Completed 15000 requests
Completed 20000 requests
Finished 20200 requests
Server Software:        
Server Hostname:        localhost
Server Port:            8080
Document Path:          /
Document Length:        29 bytes
Concurrency Level:      500
Time taken for tests:   10.000 seconds
Complete requests:      20200
Failed requests:        12
   (Connect: 0, Receive: 4, Length: 4, Exceptions: 4)
Total transferred:      2100488 bytes
HTML transferred:       585713 bytes
Requests per second:    2019.91 [#/sec] (mean)
Time per request:       247.536 [ms] (mean)
Time per request:       0.495 [ms] (mean, across all concurrent requests)
Transfer rate:          205.12 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   1.5      0      13
Processing:    13  243  15.7    241     364
Waiting:        0  243  16.0    241     363
Total:         22  243  15.5    241     370
Percentage of the requests served within a certain time (ms)
  50%    241
  66%    245
  75%    248
  80%    250
  90%    258
  95%    265
  98%    273
  99%    287
 100%    370 (longest request)
➜  test_app

В приведенном выше нагрузочном тесте на уровне 500 одновременных запросов и 2019 запросов в секунду. При общем количестве запросов 20200 только 12 запросов не удалось, что означает, что у нас было 99,941% времени безотказной работы. сервера, даже если серверы выходят из строя один за другим и перезагружаются. Это очень мило, учитывая, что мы добавили только 3 дополнительные строки кода.

Что узнать больше, ищите в моих предыдущих статьях:





Сравнение с« == и === - это всего лишь разница в проверке типов в JavaScript, подумайте еще раз?
Способ, которым JavaScript обрабатывает типы, по-видимому, является самая недооцененная и спорная часть. Люди просто считают… bhattaraib58.medium.com »



Познакомьтесь со мнойLinkedInTwitter