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

Каким образом механизм circuit breaker снижает риск каскадного отказа при недоступности удалённой зависимости?

Каким образом механизм circuit breaker снижает риск каскадного отказа при недоступности удалённой зависимости?

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

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

Circuit breaker после серии сбоев временно прекращает новые вызовы удалённой зависимости и быстро возвращает контролируемый отказ или fallback. Это освобождает рабочие потоки, соединения и очереди запросов, поэтому отказ одной зависимости не перегружает вызывающие сервисы и не превращается в каскадный отказ.

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

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

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

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

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

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

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

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

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

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

В Open состояние не должно немедленно пропускать весь накопившийся поток после восстановления. Half-open ограничивает пробную нагрузку и предотвращает повторную перегрузку зависимости. Порог, длительность паузы и число проб выбираются с учётом SLA, времени восстановления и стоимости операции.

Circuit breaker не делает операцию успешной и не гарантирует восстановление зависимости. Fallback допустим только там, где бизнес-смысл позволяет вернуть кэшированные данные, частичный результат или явную временную ошибку. Для операций записи нельзя бездумно подменять отказ фиктивным успехом.

Повторы должны быть ограниченными, иметь дедлайны и обычно использовать экспоненциальную задержку с jitter. Иначе несколько экземпляров сервиса будут повторять запросы синхронно и создавать retry storm даже при наличии circuit breaker.

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

Сервис оформления заказа вызывает сервис расчёта доставки. При сбое расчёта каждый запрос ждал пять секунд и повторялся трижды. Под нагрузкой пул потоков сервиса заказов быстро заполнялся, а задержки распространялись на весь API.

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

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

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

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

1. Достаточно ли circuit breaker для защиты от каскадного отказа?

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

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

**2. Почему нельзя открывать circuit breaker на любой ошибке?

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

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

**3. Чем circuit breaker отличается от rate limiter?

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

Circuit breaker реагирует на наблюдаемые сбои зависимости и временно прекращает вызовы к ней. Эти механизмы дополняют друг друга: rate limiter ограничивает нормальную и пиковую нагрузку, а circuit breaker быстро изолирует уже отказавший компонент.