У Prowid Photos есть сервис уведомлений: он считает цифру на «колокольчике» в шапке сайта. В пятницу вечером он начинает отвечать не за 20 мс, а за 5 секунд — и лента, которой нужна была всего одна цифра, перестаёт открываться у всех, хотя фотографии и база в порядке. В этом уроке разберём, как мелкая поломка становится аварией и какие приёмы её останавливают: таймауты, повторы, идемпотентность, circuit breaker, rate limiting, деградация и health checks.
Всё ломается: каскадный сбой
Сеть теряет пакеты, деплой перезапускает сервер, кончается место на диске. В системе из нескольких частей что-то сломано почти всегда (доступности звеньев перемножаются, помните первый урок модуля). Надёжность — это не «ничего не ломается», а «когда часть сломалась, остальное работает».
Что делает зависший сервис? Число запросов, одновременно «висящих» в системе, равно запросов в секунду × время ответа. При 350 RPS и ответе за 50 мс это 350 × 0,05 ≈ 18 запросов. Если каждый ждёт уведомления 5 секунд, их 350 × 5 = 1750. Каждый держит сокет, память, возможно соединение с базой. Экземпляры упираются в лимиты, балансировщик видит провал health check, а пользователи жмут «обновить» и добавляют нагрузку. Это каскадный сбой (cascading failure): отказ второстепенной части тянет вниз главные. Наша цель — удержать поломку там, где она случилась.
Таймауты: у каждого вызова есть предел
Вы звоните другу, он не берёт трубку. Через полминуты вы повесите её, а не будете слушать гудки до вечера. Код должен вести себя так же. fetch в Node.js по умолчанию может ждать минутами, а у многих драйверов лимита нет вовсе. Современный способ — AbortSignal.timeout(): по истечении времени fetch завершается ошибкой TimeoutError.
export async function fetchJson(url, { timeoutMs = 2000, ...init } = {}) {
const res = await fetch(url, {
...init,
signal: AbortSignal.timeout(timeoutMs), // abort after timeoutMs
});
if (!res.ok) {
const err = new Error(`HTTP ${res.status}`);
throw Object.assign(err, { status: res.status });
}
return res.json(); // the same signal also covers reading the body
}
try {
await fetchJson('http://notifications:4001/unread/42', { timeoutMs: 300 });
} catch (err) {
if (err.name === 'TimeoutError') console.warn('notifications is too slow');
else throw err;
}Как выбрать значение? Откройте перцентили из первого урока: если p99 уведомлений — 200 мс, ставьте 300–500 мс, а не 30 секунд «на всякий случай». Для цепочки вызовов нужен бюджет времени: лента отвечает за 300 мс на p95, поэтому три последовательных вызова по 2 секунды невозможны.
⚠️ Таймаут не отменяет работу на сервере: уведомления могли выполнить запрос, просто ответ не успел. Повтор после таймаута может сделать дело дважды — об этом ниже.
Повторы: ещё раз, но с умом
Многие сбои короткие: сеть моргнула, сервер перезапустился при деплое. Повтор спасает такие случаи, но повторять можно не всё:
| Ситуация | Повторять? |
|---|---|
сетевая ошибка или таймаут у GET | да |
502, 503, 504; 429 — после паузы из Retry-After | да |
400, 401, 403, 404, 422 | нет, ответ не изменится |
POST без ключа | нет, опасно |