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 — иначе сбой одного блокирует все.

Связанные темы

Ресурсы