Як масштабувати ваш сервер Node.js за допомогою кластеризації

Масштабованість є гарячою темою в техніці, і кожна мова програмування або фреймворк забезпечує свій власний спосіб обробки великих навантажень.

Сьогодні ми побачимо простий і зрозумілий приклад кластеризації Node.js. Це техніка програмування, яка допоможе вам розпаралелювати ваш код і пришвидшити роботу.

“Один примірник Node.js працює в одному потоці. Щоб скористатися перевагами багатоядерних систем, користувач іноді захоче запустити кластер процесів Node.js для обробки навантаження ".

- Документація Node.js

Ми створимо простий веб-сервер, використовуючи Koa, який насправді схожий на Express із точки зору використання.

Повний приклад доступний у цьому сховищі Github.

Що ми будемо будувати

Ми створимо простий веб-сервер, який діятиме наступним чином:

  1. Наш сервер отримає POSTзапит, ми зробимо вигляд, що користувач надсилає нам зображення.
  2. Ми скопіюємо зображення з файлової системи у тимчасовий каталог.
  3. Ми перевернемо його вертикально, використовуючи Jimp, бібліотеку обробки зображень для Node.js.
  4. Ми збережемо його у файловій системі.
  5. Ми видалимо його та надішлемо відповідь користувачеві.

Звичайно, це не реальний додаток, але дуже близький до одного. Ми просто хочемо виміряти переваги використання кластеризації.

Налаштування проекту

Я буду використовувати yarnдля встановлення моїх залежностей та ініціалізації мого проекту:

Оскільки Node.js є однопотоковим, якщо наш веб-сервер вийде з ладу, він не працюватиме, доки його не перезапустить інший процес. Отже, ми встановимо назавжди, простий демон, який перезапустить наш веб-сервер, якщо він коли-небудь вийде з ладу.

Ми також встановимо Jimp, Koa та Koa Router.

Початок роботи з Коа

Це структура папок, яку нам потрібно створити:

У нас буде srcпапка, яка містить два файли JavaScript: cluster.jsі standard.js.

Першим буде файл, де ми будемо експериментувати з clusterмодулем. Другий - це простий сервер Koa, який буде працювати без будь-якої кластеризації.

У moduleкаталозі ми створимо два файли: job.jsі log.js.

job.jsбуде виконувати роботу з маніпулювання зображеннями. log.jsреєструватиме кожну подію, яка відбувається під час цього процесу.

Модуль журналу

Модуль журналу буде простою функцією, яка прийме аргумент і запише його в stdout(аналогічно console.log).

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

Модуль Job

Буду чесним, це не красивий і супер-оптимізований сценарій. Це просто легка робота, яка дозволить нам підкреслити нашу машину.

Веб-сервер Koa

Ми створимо дуже простий веб-сервер. Він відповість на два маршрути двома різними методами HTTP.

Ми зможемо виконати запит GET //localhost:3000/. Koa відповість простим текстом, який покаже нам поточний PID (ідентифікатор процесу).

Другий маршрут буде приймати лише запити POST на /flipшляху та виконуватиме роботу, яку ми щойно створили.

Ми також створимо просте проміжне програмне забезпечення, яке встановить X-Response-Timeзаголовок. Це дозволить нам виміряти ефективність.

Чудово! Тепер ми можемо розпочати введення тексту на сервері node ./src/standard.jsта протестувати наші маршрути.

Проблема

Давайте використаємо мою машину як сервер:

  • Macbook Pro 15-дюймовий 2016
  • 2,7 ГГц Intel Core i7
  • 16 ГБ оперативної пам'яті

Якщо я роблю запит POST, сценарій вище надішле мені відповідь за ~ 3800 мілісекунд. Не так вже й погано, враховуючи, що зображення, над яким я зараз працюю, становить близько 6,7 Мб.

Я можу спробувати зробити більше запитів, але час відповіді не надто зменшиться. Це пов’язано з тим, що запити виконуватимуться послідовно.

Отже, що станеться, якщо я спробую зробити 10, 100, 1000 одночасних запитів?

Я створив простий скрипт Elixir, який виконує кілька одночасних запитів HTTP:

Я вибрав Elixir, тому що створювати паралельні процеси дуже просто, але ви можете використовувати все, що вам подобається!

Тестування десяти одночасних запитів - без кластеризації

Як бачите, ми породжуємо 10 одночасних процесів з нашого iex (Elixir REPL).

Сервер Node.js негайно скопіює наше зображення і почне перевертати його.

Перша відповідь реєструється через 16 секунд, а остання - через 40 секунд.

Таке різке зниження продуктивності! Лише 10 одночасних запитів,ми знизили продуктивність веб-сервера на 950%!

Представляємо кластеризацію

Пам'ятаєте, що я згадав на початку статті?

Щоб скористатися перевагами багатоядерних систем, користувач іноді захоче запустити кластер процесів Node.js для обробки навантаження.

Залежно від того, на якому сервері ми будемо запускати наш додаток Koa, ми можемо мати різну кількість ядер.

Кожне ядро ​​відповідає за індивідуальне поводження з навантаженням. В основному, кожен HTTP-запит буде задоволений одним ядром.

Так, наприклад - моя машина, яка має вісім ядер, буде обробляти вісім одночасних запитів.

Тепер ми можемо порахувати, скільки центральних процесорів у нас завдяки osмодулю:

cpus()Метод повертає масив об'єктів, що описують наші процесори. Ми можемо прив’язати його довжину до константи, яка буде називатися numWorkers, тому що саме таку кількість робітників ми будемо використовувати.

Тепер ми готові вимагати clusterмодуль.

Зараз нам потрібен спосіб розділити наш основний процес на Nрізні процеси.

Ми назвемо наш основний процес masterта інші процеси workers.

clusterМодуль Node.js пропонує метод із назвою isMaster. Він поверне логічне значення, яке покаже нам, чи поточним процесом керує робітник чи майстер:

Чудово. Золоте правило тут полягає в тому, що ми не хочемо обслуговувати нашу програму Koa в рамках головного процесу.

Ми хочемо створити програму Koa для кожного працівника, тому, коли надходить запит, про це подбає перший вільний працівник.

cluster.fork()Метод буде відповідати нашої мети:

Гаразд, спочатку це може бути трохи складно.

Як ви можете бачити у сценарії вище, якщо наш сценарій був виконаний головним процесом, ми оголосимо константу, що викликається workers. Це створить працівника для кожного ядра нашого процесора і збереже всю інформацію про них.

Якщо ви не впевнені у прийнятому синтаксисі, використання […Array(x)].map()- це те саме, що:

Я просто вважаю за краще використовувати незмінні значення при розробці програми з високою одночасністю.

Додавання Коа

Як ми вже говорили раніше, ми не хочемо обслуговувати нашу заявку Koa під загальний процес.

Давайте скопіюємо нашу структуру програми Koa у elseзаяву, тому ми будемо впевнені, що її обслуговуватиме робочий:

Як бачите, ми також додали до isMasterвисловлювання пару прослуховувачів подій :

Перший скаже нам, що породили нового працівника. Другий створить нового працівника, коли інший працівник вийде з ладу.

Таким чином, головний процес буде відповідальним лише за створення нових працівників та організацію їх. Кожен працівник обслуговуватиме екземпляр Коа, який буде доступний у :3000порту.

Тестування десяти одночасних запитів - з кластеризацією

Як бачите, ми отримали першу відповідь приблизно через 10 секунд, а останню - приблизно через 14 секунд. Це дивовижне покращення за попередні 40 секунд часу відгуку!

Ми зробили десять одночасних запитів, і сервер Koa негайно прийняв вісім із них. Коли перший працівник надіслав свою відповідь клієнту, він прийняв один із решти запитів і обробив його!

Висновок

Node.js має надзвичайну здатність обробляти великі навантаження, але не було б розумно зупиняти запит, поки сервер не закінчить свій процес.

Насправді веб-сервери Node.js можуть обробляти тисячі одночасних запитів, лише якщо ви негайно надіслали відповідь клієнту.

Найкращою практикою було б додати інтерфейс обміну повідомленнями pub / sub за допомогою Redis або будь-якого іншого чудового інструменту. Коли клієнт надсилає запит, сервер починає спілкування в реальному часі з іншими службами. Це відповідає за дорогі роботи.

Балансири навантаження також допоможуть у розподілі великих навантажень.

Знову ж таки, технологія дає нам безмежні можливості, і ми обов’язково знайдемо правильне рішення для масштабування нашого застосування до нескінченності та не тільки!