Пять минут падения S3 в AWS может парализовать тысячи приложений. Но чаще всего катастрофа начинается не с облачного провайдера, а с вашего кода. Один неотказоустойчивый вызов к внешнему API способен запустить цепную реакцию: исчерпание соединений БД, накопление задач в очередях, перегрузка памяти. Этого можно избежать, если внедрить механизм автоматической изоляции сбоев — Circuit Breaker.
Как работает Circuit Breaker: не просто «разомкнутая цепь»
Представьте автоматический выключатель, который физически разрывает цепь при перегрузке. В программировании Circuit Breaker — конечный автомат с тремя состояниями:
-
Закрыто (Closed)
Запросы проходят нормально. Считаем ошибки: если превышен порог (например, 5 ошибкок за 30 секунд) — переходим в состояние «Разомкнуто». -
Разомкнуто (Open)
Все запросы мгновенно отклоняются без реального выполнения. Через таймаут (например, 30 секунд) переходим в «Полуразомкнуто». -
Полуразомкнуто (Half-Open)
Пропускаем ограниченное количество запросов. Если они успешны — закрываем цепь, если нет — снова размыкаем.
Реализация на Node.js: не только для AWS SDK
Рассмотрим практический пример на TypeScript. Вместо готовой библиотеки создадим ядро Circuit Breaker, чтобы понимать внутреннюю механику:
type State = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
class CircuitBreaker {
private state: State = 'CLOSED';
private failureCount = 0;
private nextAttemptTime = 0;
constructor(
private readonly failureThreshold: number,
private readonly recoveryTimeout: number,
private readonly halfOpenAttempts: number
) {}
async exec<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttemptTime) {
throw new Error('Circuit open');
}
this.state = 'HALF_OPEN';
}
try {
const result = await fn();
this.#handleSuccess();
return result;
} catch (error) {
this.#handleFailure();
throw error;
}
}
#handleSuccess() {
this.failureCount = 0;
if (this.state === 'HALF_OPEN') {
this.state = 'CLOSED';
}
}
#handleFailure() {
this.failureCount++;
if (
this.state === 'CLOSED' &&
this.failureCount >= this.failureThreshold
) {
this.state = 'OPEN';
this.nextAttemptTime = Date.now() + this.recoveryTimeout;
} else if (this.state === 'HALF_OPEN') {
this.state = 'OPEN';
this.nextAttemptTime = Date.now() + this.recoveryTimeout;
}
}
}
Применение с axios:
const paymentServiceBreaker = new CircuitBreaker(5, 30_000, 3);
async function chargeUser(userId: string, amount: number) {
return paymentServiceBreaker.exec(async () => {
const response = await axios.post('https://api.payment.com/charge', {
userId,
amount,
}, { timeout: 10_000 });
return response.data;
});
}
Где разработчики ошибаются: тонкие грани
Ошибка 1: Глобальный Circuit Breaker для всех эндпоинтов
Если объединять вызовы к авторизации и генерации PDF в один контур, то сбой медленного PDF-сервиса заблокирует авторизацию. Изолируйте контуры по типам операций и URI.
Ошибка 2: Игнорирование таймаутов
Даже с Circuit Breaker запросы должны иметь разумные timeout. Не ждите 60 секунд ответа от «умершего» сервиса — освобождайте Event Loop.
// Плохо: без таймаута в вызове
axios.get('https://api.example.com/data');
// Хорошо: явное ограничение времени
axios.get('https://api.example.com/data', { timeout: 2500 });
Ошибка 3: Полуразомкнутое состояние без ограничения количества запросов
Если в HALF_OPEN пропускать все запросы, внезапная нагрузка на ещё не восстановившийся сервис вызовет повторный сбой. Используйте алгоритм «разрешить не более N запросов за период».
Интеграция с метриками: что нельзя упустить
Circuit Breaker бесполезен, если вы не видите его состояние. Экспортируйте метрики:
- Переключения состояния
- Количество отклонённых запросов
- Успешные/неуспешные проверки в HALF_OPEN
Пример с Prometheus:
import client from 'prom-client';
const stateGauge = new client.Gauge({
name: 'circuit_breaker_state',
help: 'Current state (0-Closed, 1-Open, 2-HalfOpen)',
labelNames: ['service'],
});
// В методе exec:
stateGauge.set({ service: 'payments' },
this.state === 'CLOSED' ? 0 : this.state === 'OPEN' ? 1 : 2);
Тестируйте как в бою: симулируйте реалистичные сбои
Юнит-тесты проверяют логику переходов, но реальные проблемы возникают в продакшене. Используйте Chaos Engineering:
-
Симуляция задержек
Замедлите 10% запросов к платежному сервису на 8 секунд — увидите, как Circuit Breaker предотвращает исчерпание потоков в Node.js. -
Эталонные тесты
Запустите нагрузочный тест: 1000 RPM к вашему API. Без Circuit Breaker латентность вырастет экспоненциально, с ним — запросы начнут отклоняться, сохраняя среднее время ответа.
Когда не нужен Circuit Breaker?
-
Для неключевых операций
Если сбой функции генерации аватаров пользователя не влияет на ядро приложения, возможно, достаточно Retry с экспоненциальной отсрочкой. -
Когда вызывающая сторона обрабатывает сбои
Сервис, который только возвращает статичные данные? Вероятно, Retry Pattern и кэширование предпочтительнее.
Circuit Breaker — это не серебряная пуля, а часть стратегии устойчивости. Комбинируйте его с Retry, Rate Limiting и балансировкой нагрузки, но помните: главная цель — дать системе «передохнуть», а не скрыть проблемы. Каждый переход в OPEN должен быть триггером для исследования причины, а не просто автоматическим костылём.