Головна / Статті / 20 шаблонів Node.js, які запобігають зупинці серверів у продакшені

20 шаблонів Node.js, які запобігають зупинці серверів у продакшені

Дізнайтеся 20 практичних шаблонів Node.js — від обробки помилок до плавного завершення роботи та кеширування з’єднань — які запобігають збоям, перш ніж стане необхідним перезапуск.

4280 слів

Перерва у роботі сервера майже ніколи не починається з однієї катастрофічної події.

Зазвичай причиною є щось непомітне та безособливе.

З’єднання з базою даних ніколи не повертається до пулу.

Використання пам’яті поступово зростає, доки процес не досягне свого ліміту.

Одна маршрутка, яка раніше відповідала за 200 мілісекунд, починає виконуватися протягом п’яти секунд.

Процес розгортання перериває з’єднання, які все ще були активними.

Асинхронне завдання зазнає невдачі, і ніхто не помічає помилки.

Раптовий сплеск трафіку змушує кожен надходжуючий запит виконувати ще один дорогий виклик до бази даних.

Спочатку все це не виглядає тривожним.

Потім час відповіді починає збільшуватися.

Потім запити починають тайм-аутувати.

Потім процес повністю зупиняється.

І саме тоді починаються запитання:

„Чому сервер не працює?“

Те, що робить ситуацію такою прикрою, — це те, що більшість з цих проблем можна було уникнути.

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

У цій статті розглядаються найважливіші з цих рішень.

1. Перестаньте дозволяти одному поганому запиту зупиняти весь процес

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

Розгляньмо такий приклад:

app.get("/users/:id", async (req, res) => {
  const user = await User.findByPk(req.params.id);

  res.json(user);
});

На перший погляд усе здається нормальним.

Але що станеться, якщо з’єднання з базою даних втратиться під час обробки запиту?

Або якщо ORM видасть помилку?

Або якщо несподіване значення вхідних даних порушить логіку всередині обробника?

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

Більш стійкий підхід — це пряме спрямування помилок замість того, щоб дозволяти їм вислизнути:

app.get("/users/:id", async (req, res, next) => {
  try {
    const user = await User.findByPk(req.params.id);

    res.json(user);
  } catch (error) {
    next(error);
  }
});

Звідти все направляється до одного централізованого обробника:

app.use((err, req, res, next) => {
  console.error(err);

  res.status(500).json({
    error: "Internal server error"
  });
});

Йдеться не про приховування проблем.

Йдеться про те, щоб визначити саме де будуть знаходитися помилки.

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

2. Не вважайте перезапуск процесу рішенням

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

npm start

Або, для команд, які використовують PM2:

pm2 restart app

Це дозволяє швидко повернути сервіс у робочий стан.

Воно не вирішує проблеми, які спричинили його збої з самого початку.

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

Інструменти на кшталт PM2, Docker, Kubernetes або платформ з управлінням можуть автоматично перезапускати зупинені процеси, що справді є корисним.

Наприклад, з PM2:

pm2 start app.js --name api
pm2 save

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

Вам все одно потрібні відповіді на такі запитання:

Why did the process crash?
What request triggered it?
How much memory was being used?
What error occurred?
Was there an unhandled rejection?
Was the database unavailable?

Перезапуск відновлює роботу сервісу для користувачів.

Лише належна можливість спостереження дозволяє зрозуміти, чому сервіс взагалі припинив працювати.

Обидва елементи є необхідними.

3. Виправляйте витоки пам’яті, перш ніж вони призведуть до збоїв через нестачу пам’яті

Node.js спирається на механізм збирання сміття, але це не гарантує автоматичного належного очищення пам’яті.

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

Ось проста ілюстрація:

const cache = [];


app.get("/users", async (req, res) => {
  const users = await getUsers();


  cache.push(users);

  res.json(users);
});

Кожен надходження запиту додає ще більше даних до cache.

Оскільки додаток все ще зберігає посилання на ці записи, збирач сміття не може їх звільнити.

З часом ситуація виглядає так:

Memory usage
100 MB
200 MB
400 MB
800 MB
1.2 GB
2 GB
💥

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

Уважно стежте за такими речами:

  • Глобальні масиви, які постійно зростають
  • Кеші без обмеження за розміром
  • Великі об’єкти, які залишаються в пам’яті
  • Прослуховувачі подій, які ніколи не від’єднуються
  • Таймери, які ніколи не скасовуються
  • Стріми, які залишилися відкритими
  • Великі об’єми даних, що зберігаються всередині закритих функцій
  • Очереді без верхньої межі
  • Якщо ваше додатку кешує дані в пам’яті, цей кеш потребує чітко визначеної політики.

    Наприклад:

    Maximum entries: 10,000
    TTL: 10 minutes
    Eviction: LRU
    

    Кеш без обмежень насправді не функціонує як кеш.

    Це проблема пам’яті, яка обов’язково виникне.

    4. Встановіть таймаути для зовнішніх запитів

    Виклик іншого сервісу без встановлення таймауту — це один із найшвидших способів зупинки вашого API.

    const response = await fetch("https://external-service.com/api");
    

    Що, якщо цей зовнішній сервіс раптово почне дуже повільно відповідати?

    Ваш запит може чекати набагато довше, ніж слід.

    Раптово у вас будуть сотні чи тисячі запитів, які всі застрягли в очікуванні відповіді від залежності, яка не відповідає.

    Цей накопичений обсяг даних зрештою може також перевантажити ваш власний сервер.

    Завжди передбачайте чітке керування таймаутами.

    Наприклад, за допомогою AbortController:

    const controller = new AbortController();
    
    const timeout = setTimeout(() => {
      controller.abort();
    }, 5000);
    
    try {
      const response = await fetch(
        "https://external-service.com/api",
        {
          signal: controller.signal
        }
      );
    
      return await response.json();
    } finally {
      clearTimeout(timeout);
    }
    

    Тепер ваш додаток не буде безкінечно чекати.

    Основне правило є простим:

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

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

    5. Усунення виснаження з’єднань з базою даних

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

    500 concurrent requests
    

    І кожен з цих запитів намагається відкрити власне нове з’єднання з базою даних.

    У певний момент база даних досягає встановленої кількості з’єднань.

    Після цього нові запити починають повністю збиватися.

    API може відповідати на це помилками на кшталт:

    Too many connections
    Connection refused
    Connection timeout
    

    Пул з’єднань існує саме для того, щоб запобігти створенню кожним вхідним запитом абсолютно нового з’єднання.

    Візуально це виглядає приблизно так:

    ┌── Connection 1
                  ├── Connection 2
    API ── Pool ──┼── Connection 3 ── Database
                  ├── Connection 4
                  └── Connection 5
    

    Пул виступає у ролі контролера, обмежуючи кількість з’єднань до бази даних у будь-який момент часу.

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

    Ретельно налаштовуйте пул.

    Якщо встановити його занадто малим:

    Requests
       ↓
    Waiting for connections
       ↓
    High latency
    

    Якщо встановити його занадто великим:

    Application
       ↓
    Too many connections
       ↓
    Database overloaded
    

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

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

    6. Усунення запитів типу N+1

    Ще одним поширеним способом, яким команди випадково завдають шкоди своїй базі даних, є використання шаблону запитів N+1.

    const posts = await Post.findAll();
    
    
    for (const post of posts) {
      post.author = await User.findByPk(post.authorId);
    }
    

    Якщо в таблиці 1 000 публікацій, це перетворюється на:

    1 query → retrieve posts
    
    1,000 queries → retrieve authors
    
    Total: 1,001 queries
    

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

    Потім обсяг даних зростає.

    І той самий ендпоїнт стає надзвичайно повільним.

    Зрештою він може спожити достатньо ресурсів бази даних, щоб також уповільнити роботу не пов’язаних з ним ендпоїнтів.

    Кілька поширених рішень:

    JOINи

    SELECT
      posts.id,
      posts.title,
      users.name
    FROM posts
    JOIN users
      ON users.id = posts.author_id;
    

    Активне завантаження в ORM

    Більшість ORM надають засоби для одночасного отримання пов’язаних записів у межах однієї контрольованої операції, замість окремого запиту для кожного рядка.

    Пакетні запити

    Замість того, щоб надсилати запити по одному, наприклад:

    Get user 1
    Get user 2
    Get user 3
    ...
    

    отримайте всіх необхідних користувачів у одному об’єднаному запиті.

    Під час тестування навантаження варто поставити просте діагностичне запитання:

    Скільки окремих запитів до бази даних виконує цей ендпоінт, коли повертає 10 000 записів?

    Якщо чесна відповідь така:

    "10 001."

    це явний сигнал про необхідність виправлень.

    7. Додайте пагінацію, перш ніж база даних стане занадто великою

    Цей фрагмент коду — це бомба з відстроченим запуском:

    const users = await User.findAll();
    
    res.json(users);
    

    Він чудово працює під час розробки, коли таблиця містить:

    37 users
    

    Через шість місяців ця таблиця тепер містить:

    2,400,000 users
    

    Цей єдиний незмінний ендпоїнт тепер може:

    • Завантажувати мільйони рядків з бази даних
    • Витрачати ресурси бази даних
    • Витрачати пам’ять процесу Node.js
    • Серіалізувати величезний JSON-дані
    • Виснажувати пропускну здатність
    • Суттєво подовжувати час відповіді
    • Створювати ризик перевищення тайм-ауту

    Рішенням є пагінація.

    Наприклад, приймати запит у такому форматі:

    GET /users?page=1&limit=50
    

    і обробляти його ось так:

    const limit = Math.min(
      Number(req.query.limit) || 50,
      100
    );
    
    const page = Number(req.query.page) || 1;
    
    const offset = (page - 1) * limit;
    

    Не менш важливим є встановлення ліміту на те, що клієнти можуть запитувати.

    Не дозволяйте клієнту надсилати:

    ?limit=100000000
    

    і змушуйте ваш API намагатися виконати запит такого розміру.

    8. Припиніть виконувати незалежні операції послідовно

    Припустимо, що ендпоїнт має зібрати три дані:

    User
    Settings
    Notifications
    

    Наївна реалізація може виглядати так:

    const user = await getUser();
    const settings = await getSettings();
    const notifications = await getNotifications();
    

    Якщо кожен виклик займає 200 мс, очікування їх по черзі дає загалом приблизно:

    200 + 200 + 200 = 600 ms
    

    Якщо ці виклики не залежать один від одного, краще виконувати їх паралельно:

    const [
      user,
      settings,
      notifications
    ] = await Promise.all([
      getUser(),
      getSettings(),
      getNotifications()
    ]);
    

    Тепер усі три запити виконуються одночасно.

    Загальний час скорочується майже до рівня найповільнішого окремого виклику:

    max(200, 200, 200) ≈ 200 ms
    

    Це стосується не лише скорочення кількох мілісекунд у відповіді.

    У масштабах додаткова затримка за кожен запит означає, що ресурси сервера зайняті довше, ніж це необхідно.

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

    Проте не варто автоматично використовувати Promise.all у кожному випадку.

    Якщо один виклик справді потребує результату іншого, як у наступному випадку:

    const user = await getUser();
    const orders = await getOrders(user.id);
    

    тоді ці виклики за своєю природою мають виконуватися послідовно.

    9. Додайте плавне завершення роботи

    Один із найпростіших способів створити зайвий час припинення роботи під час розгортання — це раптове завершення процесу.

    Розгляньмо таку послідовність:

    User → POST /checkout
                  ↓
           Processing payment
                  ↓
             Deployment starts
                  ↓
              Process killed
    

    У такій ситуації користувач може побачити лише:

    Connection reset
    

    хоча базова операція вже могла частково завершитися.

    Плавне завершення роботи вирішує цю проблему шляхом припинення сервером приймання нових з’єднань, дозволяючи існуючим запитам завершитися природним чином.

    Мінімальна версія виглядає так:

    const server = app.listen(PORT);
    
    process.on("SIGTERM", () => {
      console.log("SIGTERM received");
    
    server.close(() => {
        console.log("HTTP server closed");
    
        process.exit(0);
      });
    });
    

    У продакшн-системі плавне завершення роботи часто також означає закриття:

    • Баз даних з пулами з’єднань
    • З’єднань з Redis
    • Клієнтів черги повідомлень
  • Процеси у фоновому режимі
  • Відкриті з’єднання WebSocket
  • Будь-які інші використовувані ресурси
  • Основна мета зводиться до наступного:

    Stop receiving new work
            ↓
    Finish current work
            ↓
    Close resources
            ↓
    Exit
    

    Процес розгортання не повинен нагадувати ситуацію, коли хтось вириває кабель живлення з розетки.

    10. Використовуйте перевірки стану та готовності

    Цілком можливо, що процес Node.js все ще працює, тоді як додаток, який він підтримує, фактично перестав функціонувати.

    Node.js process → Running
    Database → DOWN
    API → Practically unusable
    

    Якщо єдиним сигналом вашого балансувальника навантаження є наявність відповіді з TCP-порту, він може продовжувати направляти трафік до інстанції, яка насправді не може надати жодної корисної інформації.

    Кінцева точка готовності надає вам реальний сигнал для перевірки:

    app.get("/ready", async (req, res) => {
      try {
        await db.query("SELECT 1");
    
       res.status(200).json({
          status: "ready"
        });
      } catch (error) {
        res.status(503).json({
          status: "not_ready"
        });
      }
    });
    

    Це дозволяє вашій інфраструктурі розрізняти два абсолютно різні стани:

    Process is alive
    

    порівняно з:

    Process is ready to serve traffic
    

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

    11. Захист сервера від сплесків трафіку

    Припустимо, ваш сервіс легко справляється з:

    100 requests/second
    

    Тоді ваш продукт починають використовувати широка аудиторія.

    Раптово ви стикаєтесь з:

    2,000 requests/second
    

    Без належних заходів безпеки такий сплеск може повністю паралізувати API.

    Деякі поширені способи подолання сплесків трафіку:

    • Обмеження кількості запитів, які клієнт може надіслати протягом певного періоду
    • Використання зворотного проксі перед серверами додатків
    • Розподіл вхідного трафіку між кількома інстанціями
    • Кешування відповідей, щоб уникнути повторної обробки
    • Надання статичного контенту через CDN
    • Відкладення обробки завдань у чергу замість їх негайної обробки
  • Обмеження кількості одночасних з’єднань до бази даних
  • Автоматичне збільшення кількості інстанцій у міру зростання попиту
  • Блокування запитів, які виконуються занадто довго
  • Вам не обов’язково потрібні всі пункти з цього списку.

    Але кожна продакшн-система повинна мати чітку відповідь на запитання:

    Що станеться, якщо обсяг трафіку раптово зросте в десять разів?

    12. Виведення дорогих операцій за межі HTTP-запиту

    Розглянемо ендпоїнт, який створює користувача та надсилає листа-привітання в межах одного запиту:

    await createUser();
    await sendWelcomeEmail();
    res.json({ success: true });
    

    Якщо сервіс електронної пошти відповідає протягом трьох секунд, користувач буде чекати саме стільки.

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

    Дизайн, заснований на черзі, уникає такої взаємозалежності:

    HTTP Request
         ↓
    Create User
         ↓
    Return response
         ↓
    Queue email job
         ↓
    Background Worker
         ↓
    Send Email
    

    API більше не змушений чекати, поки завершиться повільна фонова робота.

    Ця схема особливо корисна у таких ситуаціях:

    • Надсилання підтверджувальних чи привітальних електронних листів
    • Створення звітів за запитом
    • Зміна розміру чи трансформація завантажених зображень
    • Кодування чи транскодування відео
    • Надсилання сповіщень типу push
    • Створення експортів великих обсягів даних
    • Обробка даних, надісланих через webhook
    • Виконання складних числових чи аналітичних обчислень

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

    13. Забезпечити безпечну повторну спробу виконання фонових завдань

    Відправка роботи у чергу ставить нове питання:

    Що відбувається, якщо сам працівник зазнає невдачі під час виконання?

    Розгляньмо цей процес:

    Worker receives payment notification
            ↓
    Starts processing
            ↓
    Crashes
    

    Система черг, ймовірно, автоматично спробує виконати завдання знову.

    Зазвичай це добре.

    Але якщо завдання не є ідемпотентним, його повторна спроба може викликати ті самі побічні ефекти ще раз.

    Наприклад:

    Send payment
    Send payment again
    

    або:

    Create order
    Create duplicate order
    

    Через це фонові завдання потрібно створювати з використанням:

    • Повторних спроб
    • Затримок між спробами
    • Обробки повідомлень про помилки
    • Ідемпотентності
    • Максимальної кількості спроб
    • Моніторингу

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

    14. Припиніть записувати конфіденційну інформацію

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

    Поширений приклад:

    console.log(req.body);
    

    Цей тіло запиту може легко містити такі елементи:

    Password
    Access token
    Credit-card information
    Personal information
    

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

    Безпечніший підхід — записувати лише ті поля, які вам дійсно потрібні:

    logger.info({
      userId,
      requestId,
      endpoint: req.path
    }, "Login attempt");
    

    Не піддавайтеся омані думки:

    "Це ж лише в журналах."

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

    • Системи журналізації в хмарі
    • Платформи моніторингу
    • Системи SIEM
    • Сховища резервних копій
    • Панелі керування розробників

    Виправлення проблем у продакшені не повинно призводити до виникнення окремої інциденту безпеки.

    15. Додавайте ідентифікатори запитів ще до того, як вони знадобляться

    Коли щось ламається у продакшені, несправність часто стосується кількох систем:

    Client
      ↓
    Nginx
      ↓
    Node.js API
      ↓
    Redis
      ↓
    Database
      ↓
    Payment API
    

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

    Рішення полягає у створенні ідентифікатора запиту на ранньому етапі:

    request_id = 7f4e9c21
    

    а потім додаванні його до кожної відповідної запису журналу чи відповіді:

    API log
    Database-related log
    Payment log
    Error response
    

    Наприклад:

    {
      "requestId": "7f4e9c21",
      "method": "POST",
      "path": "/orders",
      "status": 500
    }
    

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

    Тож коли клієнт повідомляє:

    "Моя замовлення не вдалося.

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

    16. Запобігання створенню дублікатів даних внаслідок дублікатних запитів

    Проблеми в мережі трапляються постійно, і клієнти повинні з ними впоратися.

    Уявіть собі таку послідовність:

    Client
      ↓
    POST /orders
      ↓
    Server creates order
      ↓
    Response lost
    

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

    Тож, природно, він намагається знову:

    POST /orders
      ↓
    Server creates another order
    

    Результат:

    Order #1001 → original request
    Order #1002 → retry
    

    Саме цю проблему призначені вирішувати ключі ідемпотентності.

    Клієнт додає заголовок на кшталт:

    Idempotency-Key: 5e7b2d91-...
    

    Сервер пам’ятає результат, пов’язаний з цим ключем. Якщо той самий ключ з’являється знову, він просто повертає початковий результат замість створення другого ресурсу.

    Це особливо важливо для операцій на кшталт:

    Payments
    Orders
    Subscriptions
    Account creation
    Resource provisioning
    

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

    17. Використовуйте транзакції, коли дані мають залишатися консистентними

    Уявіть собі таку послідовність дій:

    Create order
    Decrease inventory
    Create payment record
    

    Тепер уявіть, що один із цих кроків зазнає невдачі на певному етапі:

    Create order → SUCCESS
    Decrease inventory → SUCCESS
    Create payment record → FAIL
    

    Тепер ваша база даних містить незавершену операцію.

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

    Ідея виглядає приблизно так:

    await db.transaction(async (transaction) => {
      const order = await createOrder(transaction);
    
      await decreaseInventory(transaction);
    
      await createPaymentRecord(transaction);
    });
    

    А якщо будь-який крок викликає помилку:

    Rollback
    

    усе буде скасовано, щоб не залишити неконсистентних записів.

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

    18. Не дозволяйте одному повільному запиту зламати базу даних

    Один погано написаний запит може зайняти непропорційно багато ресурсів бази даних.

    Розгляньмо цей приклад:

    SELECT *
    FROM orders
    WHERE customer_id = ?
    ORDER BY created_at DESC;
    

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

    Використовуйте

    EXPLAIN
    

    для перевірки важливих запитів.

    Звертайте увагу на ознаки попереджень, такі як:

    Full table scan
    Huge number of rows examined
    Unexpected joins
    Poor index usage
    Expensive sorting
    

    Потім ґрунтуйте свою оптимізацію на тому, що фактично показують дані.

    Не покладайтеся на припущення.

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

    19. Встановлення обмежень ресурсів

    Продакшн-сервер потребує обмежень.

    Без них один зловмисний або пошкоджений запит, або один виходжучий з-під контролю процес, може спожити всі доступні ресурси.

    Подумайте про встановлення обмежень для таких речей, як:

    Request body size
    File upload size
    Database connections
    HTTP request duration
    Cache size
    Queue size
    Worker concurrency
    Pagination size
    Memory usage
    

    Наприклад:

    app.use(express.json({
      limit: "1mb"
    }));
    

    та:

    const limit = Math.min(
      Number(req.query.limit) || 50,
      100
    );
    

    Не існує універсального числа, яке підходило б у кожному випадку.

    Але основний принцип завжди залишається актуальним.

    Кожен ресурс має мати розумну межу.

    Залишати щось без обмежень — рідко правильний вибір у продакшені.

    20. Контролюйте ситуацію до інциденту, а не після нього

    Ви не зможете запобігти кожній перерві в роботі.

    Те, що ви можете контролювати, — це швидкість виявлення проблеми та швидкість її усунення.

    Як мінімум, стежте за:

    HTTP 5xx rate
    Request latency
    CPU
    Memory
    Database connections
    Database latency
    Event-loop health
    Queue depth
    External API failures
    Restart count
    

    Наприклад:

    09:00 → 0.2% errors
    09:10 → 0.3%
    09:20 → 0.5%
    09:30 → 4.8% 🚨
    

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

    Ефективна система сповіщень змінює ситуацію з:

    "Клієнти кажуть нам, що API не працює."

    на:

    «Наш показник 5xx перевищив поріг п’ять хвилин тому.»

    Ця різниця у часі відповіді справді має значення.

    Закономірність більшості цих виправлень

    Якщо подивитися на всі ці виправлення, стає помітною спільна тенденція.

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

    Натомість вони будуються з урахуванням того, що:

    Requests will fail.
    Databases will become slow.
    Networks will timeout.
    Clients will retry.
    Dependencies will go offline.
    Traffic will spike.
    Deployments will restart processes.
    Developers will make mistakes.
    

    і навколо кожного з цих припущень створюються захисні механізми.

    Отримана архітектура зазвичай виглядає приблизно так:

                        ┌───────────────┐
                        │     Client    │
                        └───────┬───────┘
                                │
                                ▼
                        ┌───────────────┐
                        │ Reverse Proxy │
                        │ Rate Limiting │
                        └───────┬───────┘
                                │
                                ▼
                      ┌───────────────────┐
                      │    Node.js API    │
                      │                   │
                      │ Validation        │
                      │ Timeouts          │
                      │ Error Handling    │
                      │ Request IDs       │
                      └───────┬───────────┘
                              │
                 ┌────────────┼────────────┐
                 ▼            ▼            ▼
            ┌────────┐   ┌────────┐   ┌─────────┐
            │Database│   │ Redis  │   │ Queue   │
            └────────┘   └────────┘   └────┬────┘
                                          │
                                          ▼
                                   ┌──────────────┐
                                   │Worker / Jobs │
                                   └──────────────┘
    

    Кожен шар має чітке завдання.

    Кожна зовнішня залежність має визначену стратегію у разі збою.

    Кожна дорога операція обмежена певними рамками.

    Кожна значуща дія може бути спостережена та виміряна.

    Саме ця комбінація надає системі стійкості.

    Найважливіший урок

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

    Іноді рішенням є зміна лише одного рядка коду.

    Іноді це додавання відсутнього індексу.

    Іноді це обгортання зовнішнього виклику API у механізм таймауту.

    Іноді це перетворення 1 001 запиту до бази даних на два.

    Іноді це обгортання низки операцій запису в транзакцію.

    Іноді це просто переконання у тому, що процес завершується коректно.

    Те, що відрізняє крихку API від надійної, зазвичай зводиться до низки дрібних інженерних рішень.

    Жодне з них окремо не виглядає особливо значущим.

    Але разом вони є тим, що запобігає перетворенню звичайного дня у роботі на надзвичайну ситуацію.

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

    „Чи працює це?“

    Замість цього запитайте:

    „Що відбувається, коли це не працює?“

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

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