Порівняння відмовостійкості: Elixir (BEAM) проти NestJS (Node.js)

Будування надзвичайно надійних додатків часто передбачає питання: «Що станеться, коли щось піде не так?» Іншими словами, як ваша середовище виконання або фреймворк реагують, коли відбувається непередбачуване — необроблене виключення, збої робочих процесів чи тимчасові мережеві помилки?

У цій статті ми порівняємо стійкість до збоїв і надійність Elixir (і віртуальної машини BEAM) з NestJS (що працює на Node.js). Ми побачимо, як філософія Elixir «дозволяти збою» робить стійкість до помилок основною особливістю мови/середовища виконання, тоді як NestJS зазвичай покладається на власні рішення та зовнішні інструменти.

Elixir та BEAM: Вбудована стійкість до збоїв

Elixir — сучасна мова, яка компілюється в байт-код для віртуальної машини Erlang, також відомої як BEAM. Erlang був створений кілька десятиліть тому для забезпечення надійності телекомунікаційних систем — середовищ, де простій та збої були абсолютно неприпустимі.
В результаті стійкість до збоїв глибоко вбудована як в Erlang, так і в Elixir.

Філософія "Let It Crash"

Замість того, щоб захищати код від кожного можливого шляху помилки, Elixir приймає менталітет “let it crash”. Це означає, що процеси, які зазнають збоїв, ізольовані і можуть бути перезапущені без необхідності зупиняти весь додаток. Збої розглядаються як звичайні події, а не катастрофічні невдачі, що підривають роботу всього середовища виконання.

Легковагі процеси

На BEAM кожна одиниця роботи, що виконується паралельно, є процесом з власним ізольованим купом пам’яті. Ці процеси:

  • Мають дуже низькі накладні витрати (значно легші за потоки ОС).
  • Керуються преемптивним планувальником, який забезпечує, що жоден процес не зазнає голоду, не дозволяючи іншим процесам блокувати його виконання.

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

Дерева нагляду

Ключовою особливістю Elixir/Erlang є дерево нагляду.
Процес нагляду стежить за одним або кількома дочірніми процесами, автоматично застосовуючи стратегію перезапуску у разі збою дочірнього процесу. Ці стратегії можуть перезапустити лише один зламаний процес або ширші групи процесів, залежно від конфігурації.
Цей вбудований механізм забезпечує автоматичне відновлення і є центральним елементом fault-tolerant (відмовостійкої) архітектури BEAM.

Розподілена BEAM

Ще однією важливою перевагою є прозора модель розподілу BEAM:

  • Кілька вузлів BEAM можуть бути об'єднані в кластер, обмінюючись повідомленнями та процесами між фізичними машинами.
  • Це дозволяє безшовно розподіляти процеси, ще більше підвищуючи стійкість та надмірність.

Інші особливості відмовостійкості

  • Гаряче оновлення коду (Hot Code Swapping): дозволяє оновлювати частини коду без зупинки системи, мінімізуючи час простою в критичних для місії середовищах.
  • Широкий набір інструментів: стандартна бібліотека Erlang включає потужні інструменти для налагодження, трасування та моніторингу під час виконання.

NestJS та Node.js: Створення відмовостійкості

NestJS — популярний фреймворк для створення бекенд-застосунків на TypeScript поверх Node.js.
Хоча NestJS надає чудовий досвід для розробника завдяки модульній архітектурі, він успадковує модель паралельності та обробки помилок Node.js, яка відрізняється від BEAM.

Однопотокова архітектура з циклом подій (з варіантами)

Node.js зазвичай працює на однопотоковій архітектурі на основі циклу подій (event-loop), обробляючи паралельність через асинхронні зворотні виклики або обіцянки (promises). Однак Node.js також пропонує додаткові механізми паралельності, такі як:

  • Worker Threads: виконують обчислювально важкі задачі в паралельних потоках.
  • Child Processes: вивантажують завдання на окремі процеси.

Проте ці функції не інтегровані настільки глибоко в основну філософію середовища виконання, як легкі процеси BEAM. Вони вимагають явного налаштування та управління.

Обробка необроблених винятків

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

  • process.on('uncaughtException', callback)
  • process.on('unhandledRejection', callback)

…але зазвичай краща практика — дозволити процесу завершитися і покладатися на зовнішнього менеджера процесів (наприклад, PM2, Kubernetes), щоб перезапустити його. Це гарантує, що додаток не залишиться в непослідовному стані.

Зовнішні наглядові процеси та бібліотеки

Node.js сам по собі не надає вбудованого аналога наглядових процесів BEAM. Замість цього розробники зазвичай використовують:

  • Менеджери процесів такі як PM2 або Docker/Kubernetes для перезапуску аварійних сервісів.
  • Переривання ланцюга та логіка повторних спроб (наприклад, бібліотеки, такі як opossum або cockatiel).
  • Моніторинг і логування (Winston, Pino, Sentry, Datadog тощо).

Хоча екосистема Node.js є потужною і пропонує різноманітні інструменти, вона вимагає поетапної інтеграції та налаштування.
NestJS може використовувати ці бібліотеки для забезпечення стійкості, але він не є таким "все-в-одному" як вбудована в BEAM (віртуальну машину Erlang) відмовостійкість.

Ключові відмінності

  1. Філософський підхід
  • Elixir/BEAM: Приймає невдачі як неминучі ("нехай це впаде"), з легкими процесами, що автоматично керуються за замовчуванням.
  • NestJS/Node.js: Зазвичай покладається на зовнішні інструменти, ручне оброблення винятків та менеджери процесів для відновлення після збоїв.

2. Ізоляція

  • Elixir/BEAM: Кожен процес працює в ізольованій пам'яті; один збій не пошкоджує інші.
  • NestJS/Node.js: За замовчуванням збій в одній частині коду може призвести до падіння всього середовища виконання, хоча Worker Threads або Child Processes можуть дещо пом'якшити це.

3. Модель відновлення

  • Elixir/BEAM: Супервізори автоматично перезапускають збоєві процеси, відновлюючи їх з відомого робочого стану.
  • NestJS/Node.js: Зазвичай використовуються зовнішні менеджери (PM2, Docker, Kubernetes) або спеціальна логіка для перезапуску всього процесу.

    Розподілені можливості

  • Elixir/BEAM: Рідна кластеризація через кілька вузлів, що дозволяє безпосередньо розподіляти процеси.

  • NestJS/Node.js: Кластеризація можлива (наприклад, через модуль cluster або контейнери), але вона більш ручна і не так глибоко інтегрована в мову/середовище виконання.

5. Вбудоване проти додаткового

  • Elixir/BEAM: Обсервабельність (Observability), відмовостійкість (fault tolerance) та примітиви паралелізму (concurrency) вбудовані безпосередньо в середовище виконання.
  • NestJS/Node.js: Сильно залежить від зовнішніх бібліотек і оркестраторів для досягнення того ж рівня стійкості.

Чи роблять глобальні обробники виключень системи безпечнішими?

У NestJS ви можете реалізувати глобальний фільтр виключень (global exception filter) для послідовного перехоплення та обробки помилок. Такий підхід:

  • Сприяє послідовним відповідям на помилки для клієнтів.
  • Дозволяє централізоване логування виключень.

Однак, глобальний обробник виключень сам по собі не вирішує глибші проблеми відмовостійкості (fault tolerance) чи стійкості (resiliency).
Не запобіжить аварії програми, якщо неперехоплене виключення проб’ється через фільтр, і не перезапустить автоматично збоєвий процес. Для по-справжньому надійних систем, що працюють 24/7, все одно необхідно:

  • Підтримка інфраструктури (PM2, Kubernetes, Docker).
  • Моніторинг та сповіщення (Datadog, Prometheus, Sentry).
  • Стійкі шаблони проєктування (circuit breakers, retries, graceful degradation).

Глобальний обробник виключень — це лише частина більш широкої стратегії надійності.

Висновок

Що стосується відмовостійкості (fault tolerance), то Elixir та BEAM виділяються завдяки своїм глибоко інтегрованим функціям стійкості: легкі та ізольовані процеси, дерева супервізії, рідна кластеризація та філософія "нехай це впаде", що вбудована в середовище виконання.
Ці можливості роблять створення високодоступних, самовідновлюваних систем простим, без необхідності інтегрувати численні сторонні інструменти.

З іншого боку, NestJS (та Node.js) можуть забезпечити роботу додатків підприємницького рівня та досягти надійної відмовостійкості (fault tolerance), але зазвичай залежать від додаткових інструментів — менеджерів процесів, circuit breakers, фреймворків для логування та оркестрації контейнерів — для відтворення подібної поведінки. Такий підхід є більш гнучким, але може вимагати значно більше ручної конфігурації.

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

Перекладено з: Comparing Fault Tolerance: Elixir (BEAM) vs. NestJS (Node.js)

Leave a Reply

Your email address will not be published. Required fields are marked *