АрхитектураОблако и инфраструктураИнженер по надёжности платформы

Сервис отвечает на запросы, но зависимость стала недоступна, из за чего запросы начинают накапливаться. Как...

Сервис отвечает на запросы, но зависимость стала недоступна, из-за чего запросы начинают накапливаться. Как circuit breaker предотвращает каскадный отказ?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Circuit breaker ограничивает обращение к неисправной зависимости: после заданного числа неудач он размыкает цепь и временно отклоняет новые вызовы без ожидания ответа. Через заданный интервал механизм переводится в пробный режим, проверяет восстановление зависимости и либо возвращается в рабочее состояние, либо снова блокирует вызовы.

Исторический контекст

Подход возник как аналог автоматического электрического выключателя: при аварии цепь разрывается, чтобы неисправность не повредила остальную систему. В распределённых приложениях он решает проблему медленных или неуспешных удалённых вызовов, которые могут занять рабочие потоки, соединения и память вызывающего сервиса.

Постановка проблемы

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

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

Подробное решение

Circuit breaker обычно имеет три состояния:

  • Closed — вызовы проходят к зависимости, а ошибки и тайм-ауты учитываются.
  • Open — вызовы к зависимости сразу отклоняются или направляются в заранее определённый fallback; новые сетевые ожидания не создаются.
  • Half-open — после паузы пропускается ограниченное число пробных вызовов. Успехи возвращают механизм в Closed, повторные ошибки — в Open.

Переход в Open выполняется по политике, например по доле ошибок, числу последовательных неудач или комбинации ошибок и тайм-аутов за временное окно. Порог должен учитывать объём трафика: несколько ошибок при двух запросах не всегда означают массовый отказ, а фиксированный порог без минимального числа наблюдений может давать ложные срабатывания.

Тайм-аут и circuit breaker решают разные задачи. Тайм-аут ограничивает продолжительность отдельного вызова, а circuit breaker ограничивает число вызовов к уже подозрительно неисправной зависимости. Без тайм-аутов breaker может слишком поздно получить сведения о неудаче; без breaker множество ограниченных по времени вызовов всё равно способно исчерпать ресурсы.

Fallback допустим только тогда, когда результат можно корректно заменить: например, вернуть кэшированное значение или явно сообщить о временной недоступности. Нельзя маскировать критическую ошибку устаревшими данными без понимания их допустимой давности.

Breaker может находиться в клиентской библиотеке, API-шлюзе или сервисной mesh-инфраструктуре. Независимо от места размещения нужно определить, какие ошибки считать отказом: сетевой тайм-аут обычно учитывается, а корректный бизнес-ответ с признаком отсутствия данных — не обязательно.

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

Ситуация из практики

Сервис заказов обращается к сервису расчёта доставки. При проблеме с ним запросы к заказам зависают до тайм-аута, после чего клиенты повторяют запросы, а пул соединений сервиса заказов постепенно исчерпывается.

Рассматривались три варианта. Только увеличение тайм-аута снижало число преждевременных ошибок, но увеличивало время удержания ресурсов. Только уменьшение тайм-аута быстрее освобождало ресурсы, но не предотвращало постоянный поток новых вызовов. Полный отказ от функции расчёта доставки сохранял доступность заказов, однако требовал корректного пользовательского сценария.

Выбран вариант с коротким ограниченным тайм-аутом, circuit breaker, одной контролируемой повторной попыткой для безопасных операций и fallback в виде явно обозначенного статуса доставки. После открытия breaker новые запросы к расчёту не выполнялись, основной сервис сохранял доступность, а пробный вызов через паузу позволял автоматически проверить восстановление зависимости.

Что кандидаты часто упускают

1. Достаточно ли circuit breaker без тайм-аутов?

Нет. Breaker реагирует на зафиксированные результаты вызовов, но бесконечно зависший вызов может не перейти в состояние ошибки. Пока он ожидает ответа, он продолжает занимать поток, соединение или другой ограниченный ресурс, поэтому тайм-аут должен быть задан отдельно.

2. Почему слишком агрессивный порог breaker опасен?

Он может открыть цепь из-за кратковременного сбоя, единичной ошибки или слишком малого числа запросов. Это создаёт ложный отказ даже после восстановления зависимости. Политику нужно настраивать с учётом объёма трафика, типа ошибок, минимального числа наблюдений и отдельного времени ожидания перед пробным режимом.

3. Может ли fallback гарантировать корректность результата?

Нет. Fallback лишь меняет способ деградации, но не устраняет отсутствие зависимости. Кэш может быть устаревшим, значение по умолчанию может повлиять на бизнес-решение, а повторная постановка операции может создать дубликат. Поэтому fallback выбирают с учётом идемпотентности, допустимой устарелости данных и требований к целостности.