Паттерн Circuit Breaker (автоматический выключатель) необходим для защиты системы от каскадных сбоев. Когда внешняя зависимость (сервис, API, база данных) становится нестабильной или недоступна, без этого механизма ошибка в одном модуле может привести к падению всей системы. Circuit Breaker перехватывает запросы к проблемному ресурсу и сразу возвращает ошибку пользователю или показывает заглушку, не пытаясь снова связаться с неработающим сервисом. Это снижает нагрузку на сеть и позволяет основной системе продолжать работу.

Внедрение Circuit Breaker: зачем это нужно при нестабильных зависимостях

Основная задача внедрения — разрыв цепочки ошибок. В микросервисной архитектуре или при работе с внешними API одна зависимость часто не контролируется вашей командой. Если этот сервис отвечает медленно или возвращает 503/504, приложение без защиты будет пытаться выполнить запрос снова и снова. Это создает нагрузку на сеть и может привести к исчерпанию пулов соединений (connection pool exhaustion) в вашем приложении.

Без Circuit Breaker система работает как единый организм: если нога сломалась, человек не может ходить. С выключателем нога перевязана, и человек продолжает идти, возможно, с меньшей скоростью или используя костыль (фоллбэк). Для бизнеса это означает сохранение доступности ключевых функций даже при проблемах у партнеров или провайдеров.

Второй аспект — скорость ответа. Пытаться установить соединение с недоступным ресурсом занимает время. Если вы знаете, что сервис «мертв», лучше сразу вернуть пользователю сообщение «Сервис временно недоступен», чем заставлять его ждать таймаут соединения в 30 секунд.

Шаги по внедрению паттерна Circuit Breaker в архитектуру приложения

Первый шаг — идентификация точек риска. Определите, к каким внешним ресурсам ваше приложение обращается: сторонние API, очереди сообщений, удаленные базы данных или другие микросервисы. К внутренним базам данных (если они в одной сети) паттерн применяется реже, но к внешним сервисам — обязательно.

Второй шаг — создание абстракции доступа. Не вызывайте внешний сервис напрямую из бизнес-логики. Создайте слой адаптера или репозиторий, который оборачивает вызов внешнего API. Именно в этот слой внедряется логика Circuit Breaker. Это позволяет менять стратегию обработки ошибок без переписывания всей бизнес-логики.

Третий шаг — определение состояния выключателя. Обычно реализуются три состояния: "Закрыто" (Closed) — нормальная работа, запросы идут дальше; "Открыто" (Open) — зависимость недоступна, запросы блокируются и возвращают ошибку сразу; "Половину открыт" (Half-Open) — периодическая попытка пробить соединение для проверки восстановления сервиса.

Четвертый шаг — интеграция с обработчиком ошибок. Когда выключатель в состоянии "Открыто", код должен формировать понятное сообщение об ошибке. Не передавайте пользователю сырые ошибки сети (например, ECONNREFUSED). Используйте заглушки (mocks) или кэшированные данные, если это уместно для вашего сценария.

Критерии выбора параметров (threshold, timeout, reset interval)

Порог срабатывания (Threshold) — количество неудачных попыток подряд. Если сервис отвечает медленно, но не падает полностью, слишком низкий порог приведет к ложным срабатываниям. Слишком высокий порг позволит системе долго страдать от медленного сервиса. Рекомендация: начните с 3–5 неудачных попыток за короткий интервал.

Таймаут (Timeout) — время ожидания ответа от внешнего сервиса. Если ответ не пришел за это время, считается ошибкой. Устанавливайте таймаут меньше времени между проверками в состоянии "Half-Open". Например, если проверка раз в 30 секунд, таймаут должен быть около 25–28 секунд.

Интервал сброса (Reset Interval) — время, которое выключатель остается в состоянии "Открыто", прежде чем попытаться снова соединиться. Если сервис упал на час, не имеет смысла проверять его каждые 30 секунд. Установите интервал от 30 до 60 секунд для частых сбоев или от нескольких минут для критических систем.

Оценка параметров требует понимания SLA вашего сервиса. Если партнер гарантирует доступность 99%, можно позволить себе более агрессивный сброс. Если сервис часто падает, лучше увеличить интервал сброса, чтобы не перегружать сеть частыми попытками соединения.

Реализация на популярных языках (примеры кода без привязки к стеку)

Логика реализации универсальна. Представьте объект CircuitBreaker с полями: state (Closed/Open), failureCount, resetTimeout. Метод call() проверяет текущее время и состояние. Если state == Open, возвращается ошибка сразу. Иначе отправляется запрос. При ошибке увеличивается failureCount. Если failureCount >= threshold, state меняется на Open.

В состоянии Half-Open сбрасывается счетчик ошибок. Следующий успешный запрос возвращает сервис в состояние Closed. Следующая ошибка возвращает его в Open и перезапускает таймер сброса.

Пример псевдокода для понимания структуры:

Ошибки при настройке и их последствия для бизнеса

Ошибка 1: Слишком короткий reset interval. Если вы проверяете сервис каждые 5 секунд, а он восстанавливается через минуту, система будет постоянно видеть его как недоступный. Это создает ложное впечатление неработоспособности сервиса.

Ошибка 2: Игнорирование Half-Open состояния. Некоторые реализации сразу возвращают ошибку в состоянии Open, не давая шанса на восстановление. Правильно: дать несколько попыток пробить соединение с малым таймаутом.

Ошибка 3: Применение к всем запросам одинаково. Если один запрос критичен (оплата), а другой второстепенен (получение рекомендаций), настройте разные выключатели или используйте фоллбэк только для некритичных операций.

Последствия неправильной настройки — потеря доверия пользователей. Если система постоянно говорит "Сервис недоступен", когда он просто медленный, пользователи уйдут к конкурентам.

Тестирование работы Circuit Breaker в продакшене

Тестировать нужно не только успешные сценарии. Используйте инструменты для эмуляции сбоев: отключите сеть, верните код ошибки 503, увеличьте задержку ответа до 10 секунд. Наблюдайте, как система переключается в режим защиты.

Проверьте логи. Убедитесь, что при срабатывании выключателя не пишутся тысячи строк об ошибках соединения. Лог должен фиксировать переход в состояние Open и последующие попытки сброса.

Мониторинг метрик. Отслеживайте частоту срабатывания выключателей. Если они срабатывают слишком часто, возможно, ваш сервис действительно нестабилен, или параметры настроены неверно. Если они не срабатывают никогда, проверьте, правильно ли фиксируются таймауты.

Вопросы и ответы

Нужен ли Circuit Breaker для внутренних сервисов в одной сети?

Да, если один внутренний сервис зависит от другого и может упасть. Внутренние сети тоже могут иметь проблемы с маршрутизацией или перегрузкой. Паттерн защищает от каскадных эффектов внутри системы.

Как отличить Circuit Breaker от кэширования?

Кэширование хранит данные для ускорения чтения, Circuit Breaker блокирует запросы при недоступности источника. Они могут работать вместе: в состоянии Open можно отдавать кэш или заглушку, но это разные механизмы.

Можно ли использовать Circuit Breaker для базы данных?

Технически да, если база находится за пределами локальной сети или подключается через прокси. Однако чаще используют пулы соединений и стратегии повторных попыток с экспоненциальной задержкой, так как падение БД обычно означает критическую ситуацию.

По теме

class CircuitBreaker {
  constructor(threshold = 5, resetTimeout = 60000) {
    this.state = 'CLOSED';
    this.failureCount = 0;
    this.threshold = threshold;
    this.resetTimeout = resetTimeout;
    this.lastFailureTime = null;
  }

  call(request) {
    if (this.state === 'OPEN') {
      return Promise.reject(new Error('Service temporarily unavailable'));
    }

    return request()
      .then(result => {
        this.failureCount = 0;
        this.state = 'CLOSED';
        return result;
      })
      .catch(error => {
        this.failureCount++;
        if (this.failureCount >= this.threshold) {
          this.state = 'OPEN';
          console.log('Circuit breaker opened');
        }
        throw error;
      });
  }

  reset() {
    const now = Date.now();
    if (now - this.lastFailureTime >= this.resetTimeout) {
      this.state = 'HALF-OPEN';
      this.failureCount = 0;
    }
  }
}

// Usage example
const breaker = new CircuitBreaker(3, 30000);

breaker.call(() => fetchExternalService()).catch(err => {
  console.error('Handled gracefully:', err.message);
});