20 шаблонів Node.js, які запобігають зупинці серверів у продакшені
Дізнайтеся 20 практичних шаблонів Node.js — від обробки помилок до плавного завершення роботи та кеширування з’єднань — які запобігають збоям, перш ніж стане необхідним перезапуск.
Перерва у роботі сервера майже ніколи не починається з однієї катастрофічної події.
Зазвичай причиною є щось непомітне та безособливе.
З’єднання з базою даних ніколи не повертається до пулу.
Використання пам’яті поступово зростає, доки процес не досягне свого ліміту.
Одна маршрутка, яка раніше відповідала за 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
- Клієнтів черги повідомлень
Основна мета зводиться до наступного:
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 буде працювати саме так, як очікується під час розробки, не обмежуйтеся лише запитанням:
„Чи працює це?“
Замість цього запитайте:
„Що відбувається, коли це не працює?“
Саме це запитання є справжньою відправною точкою для забезпечення надійності у продакшені.
А найкращий варіант інциденту з перервою у роботі — це той, якого ваші користувачі навіть не помічають, адже ви виявили та виправили проблему ще до того, як вона перетворилася на повну перерву у роботі.