Circuit Breaker Pattern
Circuit Breaker (Предохранитель) — паттерн устойчивости, предотвращающий каскадные отказы в распределённых системах: при превышении порога ошибок «размыкает» цепь и возвращает быстрый ответ вместо ожидания таймаута.
Зачем нужно
В микросервисной архитектуре один отказавший сервис может «положить» всю систему: запросы накапливаются, потоки блокируются, память кончается. Circuit Breaker разрывает цепочку: после N ошибок он перестаёт пробрасывать запросы к сломанному сервису и сразу возвращает fallback-ответ. Через некоторое время он осторожно «полуоткрывается», проверяя, восстановился ли сервис.
Где используется
- Вызовы внешних API: платёжный шлюз, SMS-провайдер, карты
- Межсервисное взаимодействие в микросервисах
- Запросы к базам данных при деградации производительности
- Интеграции с ненадёжными legacy-системами
- Node.js BFF (Backend for Frontend): защита от падения upstream-сервисов
Основной контент
Три состояния Circuit Breaker
CLOSED (замкнут) → OPEN (разомкнут) → HALF-OPEN (полуоткрыт) → CLOSED
↘ OPEN
- CLOSED: нормальная работа, запросы проходят, считаются ошибки
- OPEN: ошибок слишком много, все запросы сразу отклоняются
- HALF-OPEN: пробный период, пропускается один запрос для проверки
Реализация Circuit Breaker
class CircuitBreaker {
constructor(request, options = {}) {
this.request = request;
this.state = 'CLOSED';
this.failureCount = 0;
this.successCount = 0;
this.nextAttempt = Date.now();
// Конфигурация
this.failureThreshold = options.failureThreshold || 5;
this.successThreshold = options.successThreshold || 2;
this.timeout = options.timeout || 5000; // мс до HALF-OPEN
}
async call(...args) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error('Circuit Breaker: цепь разомкнута, сервис недоступен');
}
this.state = 'HALF-OPEN';
}
try {
const response = await this.request(...args);
return this.onSuccess(response);
} catch (err) {
return this.onFailure(err);
}
}
onSuccess(response) {
this.failureCount = 0;
if (this.state === 'HALF-OPEN') {
this.successCount++;
if (this.successCount >= this.successThreshold) {
this.state = 'CLOSED';
this.successCount = 0;
console.log('Circuit Breaker: цепь замкнута снова');
}
}
return response;
}
onFailure(err) {
this.failureCount++;
if (this.state === 'HALF-OPEN' || this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.timeout;
this.failureCount = 0;
this.successCount = 0;
console.log(`Circuit Breaker: цепь разомкнута до ${new Date(this.nextAttempt).toISOString()}`);
}
throw err;
}
}
// Использование
async function fetchUser(id) {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
const breaker = new CircuitBreaker(fetchUser, {
failureThreshold: 3,
timeout: 10000
});
// С fallback
async function getUser(id) {
try {
return await breaker.call(id);
} catch (err) {
console.warn('Fallback: возвращаем кэшированные данные');
return cache.get(`user:${id}`) || null;
}
}
Частые ошибки
- Слишком низкий порог ошибок:
failureThreshold: 1приведёт к постоянному срабатыванию при единичных сетевых сбоях. - Слишком короткий timeout: если OPEN-период слишком короткий, Circuit Breaker не даёт сервису восстановиться.
- Нет fallback: разомкнутая цепь должна возвращать что-то полезное — кэш, дефолтное значение, деградированный ответ.
- Один breaker на всё: для разных внешних сервисов нужны отдельные экземпляры Circuit Breaker — иначе сбой одного блокирует все.