awesome-everything EN
↑ Обратно к восхождению

Архитектура бэкенда

Машина состояний: closed, open, half-open

Суть Circuit breaker — машина из трёх состояний: closed пропускает вызовы и считает отказы, open отклоняет каждый вызов мгновенно на остывание, half-open пускает несколько пробных вызовов проверить восстановление. Таймер остывания — циферблат восстановления.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 14 min

«Быстро отказывай, когда зависимость больна» звучит просто, пока не задашь два трудных вопроса: когда именно он начинает отклонять вызовы и как он вообще снова начинает доверять зависимости? Отклоняй слишком рьяно — и один сбой блокирует здоровый сервис; доверяй слишком рьяно — и ты долбишь ещё сломанный сервис в момент, когда остывание закончилось, опрокидывая его обратно. Брейкер отвечает на оба маленькой точной машиной состояний — три состояния и один таймер — и почти каждый продакшен-брейкер, от Netflix Hystrix до resilience4j, есть вариация на эту тему. Сделай состояния и переходы правильно — и остальное это тюнинг.

Три состояния, четыре перехода

Circuit breaker — это конечный автомат, обёртывающий каждый вызов к зависимости:

  • Closed — нормальное состояние. Вызовы проходят насквозь, и брейкер считает отказы. Когда отказы пересекают условие срабатывания, он переходит в open и запускает таймер остывания.
  • Open — сработавшее состояние. Каждый вызов отклоняется мгновенно (исключение или фолбэк), вообще не трогая зависимость. Это быстрый отказ из прошлого урока. Когда таймер остывания истекает, он переходит в half-open.
  • Half-open — пробующее состояние. Ограниченное число пробных вызовов пускается насквозь проверить, восстановилась ли зависимость. Если они успешны, брейкер возвращается в closed и сбрасывает счётчики. Если хоть один падает, он идёт прямо обратно в open и перезапускает остывание.

Вот и вся машина: closed → open при слишком многих отказах, open → half-open по таймеру, half-open → closed при успехе, half-open → open при отказе. Переходы важны не меньше состояний, потому что каждый из них — решение, сколько нагрузки слать зависимости в неопределённом состоянии.

Таймер остывания — это циферблат восстановления

Самая значимая настройка — как долго брейкер остаётся в open до пробы — Hystrix зовёт её sleepWindowInMilliseconds (по умолчанию 5 с), resilience4j зовёт её waitDurationInOpenState (по умолчанию 60 с). Это прямой компромисс:

  • Слишком коротко. Брейкер пробует почти сразу, до того как зависимость успела восстановиться, поэтому проба падает и он снова размыкается. Хуже того, если он перекидывается open → half-open слишком быстро, он может осциллировать (флапать) между состояниями, посылая всплески обречённых вызовов.
  • Слишком долго. Зависимость восстановилась секунды назад, но брейкер продолжает отклонять всех, превращая короткий сбой даунстрима в долгий самонанесённый отказ.

Универсального правильного ответа нет; он отслеживает, сколько зависимость обычно восстанавливается. Брейкер перед сервисом, который перезапускается за ~10 с, хочет остывание около этого, не 60 с и не 1 с.

Зачем существует half-open

Состояние half-open — это умная часть. Без него у тебя было бы лишь два варианта, когда таймер срабатывает: остаться closed-или-open наугад, или распахнуть ворота полностью и послать весь трафик разом. Второй опасен — сервис, который только вернулся, хрупок, и внезапный потоп всего бэклога может его затаймаутить и опрокинуть прямо обратно. Это громовое стадо на оживающем сервисе.

Half-open решает это, посылая только струйкуpermittedNumberOfCallsInHalfOpenState у resilience4j по умолчанию 10 — и завязывая решение на них. Оживающий сервис доказывает себя на горстке вызовов, прежде чем брейкер распахнётся полностью. Одна тонкость: по умолчанию resilience4j не двигает open → half-open по одному таймеру (automaticTransitionFromOpenToHalfOpenEnabled = false); он ждёт прихода следующего вызова после остывания, чтобы простаивающий брейкер не пробовал зависимость, которой никто не пользуется.

Почему это работает

Зачем отдельное состояние half-open вместо того, чтобы просто перейти в closed и снова смотреть на счётчик отказов? Потому что «перейти в closed» означает «послать весь трафик», а момент восстановления — ровно тот, когда зависимость хуже всего может выдержать весь трафик. Сервис, только что перезапустившийся, имеет холодные кэши, пустые пулы соединений и, возможно, бэклог поставленной в очередь работы; полная продакшен-нагрузка на него в первую секунду — это то, как восстановление становится повторным отказом. Half-open — это контролируемый, малорисковый эксперимент: пошли горстку вызовов и дай их исходу — не догадке и не полному пожарному шлангу — решить, действительно ли зависимость здорова. Это также делает решение дёшево обратимым: если проба падает, ты потратил лишь несколько вызовов на выяснение, что зависимость ещё больна, против выяснения через её повторную перегрузку. Паттерн тот же, что в TCP slow-start и в прогреве кэша: когда не уверен, что ресурс выдержит нагрузку, ты вкатываешься в него малой пробой, а не вбухиваешь всё разом, потому что цена ошибки асимметрична — провалившаяся проба дёшева, а повторный коллапс — нет.

СостояниеВызовы к зависимостиСчитаетВыход вПо
ClosedВсе проходят насквозьОтказы против порогаOpenОтказы пересекают порог
OpenНикаких — мгновенный отказТаймер остыванияHalf-openИстёк таймер (или следующий вызов после него)
Half-openТолько несколько пробныхИсходы пробClosed / OpenВсе успешны / любой упал
Викторина

В каком состоянии circuit breaker отклоняет каждый вызов мгновенно, вообще не трогая зависимость?

Викторина

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

Расставь шаги по порядку

Расставь жизненный цикл брейкера через инцидент даунстрима и восстановление:

  1. 1 Closed: вызовы проходят, отказы лезут выше порога
  2. 2 Open: каждый вызов отклонён мгновенно, пока крутится таймер остывания
  3. 3 Half-open: несколько пробных вызовов проверяют, восстановилась ли зависимость
  4. 4 Снова closed: пробы успешны, счётчики сброшены, полный трафик возобновляется
Вспомните перед уходом
  1. 01
    Какие три состояния у circuit breaker и переходы между ними?
  2. 02
    Почему остывание в open — самая значимая настройка, и зачем существует half-open?
Итог

Circuit breaker — это маленький конечный автомат с тремя состояниями и одним таймером. Closed нормальное: вызовы проходят и отказы считаются, и пересечение условия срабатывания двигает его в open. Open сработавшее: каждый вызов отклоняется мгновенно, не трогая зависимость — быстрый отказ из прошлого урока — пока не истечёт остывание, тогда он двигается в half-open. Half-open пробует: ограниченное число пробных вызовов проверяет восстановление, все успешны — возврат в closed со сбросом счётчиков, любой падает — отскок в open и перезапуск остывания. Остывание — это циферблат восстановления: слишком коротко переопрашивает сломанный сервис и может флапать, слишком долго растягивает сбой в самонанесённый отказ, а правильное значение совпадает с реальным временем восстановления зависимости. Half-open существует, чтобы предотвратить громовое стадо на хрупком, только что восстановившемся сервисе, вкатываясь струйкой вместо полного пожарного шланга, так что провалившаяся проба дёшева, а повторный коллапс предотвращён. Состояния улажены; следующий урок задаёт более трудный вопрос — что считается достаточным отказом, чтобы сработать: доля отказов по скользящему окну, минимальный порог объёма и медленные вызовы, засчитанные как отказы.

Связанные уроки
Продолжить восхождение ↑Что его срабатывает: доля отказов, окна и порог объёма
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.