Архитектура бэкенда
Машина состояний: closed, open, half-open
«Быстро отказывай, когда зависимость больна» звучит просто, пока не задашь два трудных вопроса: когда именно он начинает отклонять вызовы и как он вообще снова начинает доверять зависимости? Отклоняй слишком рьяно — и один сбой блокирует здоровый сервис; доверяй слишком рьяно — и ты долбишь ещё сломанный сервис в момент, когда остывание закончилось, опрокидывая его обратно. Брейкер отвечает на оба маленькой точной машиной состояний — три состояния и один таймер — и почти каждый продакшен-брейкер, от 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 Closed: вызовы проходят, отказы лезут выше порога
- 2 Open: каждый вызов отклонён мгновенно, пока крутится таймер остывания
- 3 Half-open: несколько пробных вызовов проверяют, восстановилась ли зависимость
- 4 Снова closed: пробы успешны, счётчики сброшены, полный трафик возобновляется
- 01Какие три состояния у circuit breaker и переходы между ними?
- 02Почему остывание в open — самая значимая настройка, и зачем существует half-open?
Circuit breaker — это маленький конечный автомат с тремя состояниями и одним таймером. Closed нормальное: вызовы проходят и отказы считаются, и пересечение условия срабатывания двигает его в open. Open сработавшее: каждый вызов отклоняется мгновенно, не трогая зависимость — быстрый отказ из прошлого урока — пока не истечёт остывание, тогда он двигается в half-open. Half-open пробует: ограниченное число пробных вызовов проверяет восстановление, все успешны — возврат в closed со сбросом счётчиков, любой падает — отскок в open и перезапуск остывания. Остывание — это циферблат восстановления: слишком коротко переопрашивает сломанный сервис и может флапать, слишком долго растягивает сбой в самонанесённый отказ, а правильное значение совпадает с реальным временем восстановления зависимости. Half-open существует, чтобы предотвратить громовое стадо на хрупком, только что восстановившемся сервисе, вкатываясь струйкой вместо полного пожарного шланга, так что провалившаяся проба дёшева, а повторный коллапс предотвращён. Состояния улажены; следующий урок задаёт более трудный вопрос — что считается достаточным отказом, чтобы сработать: доля отказов по скользящему окну, минимальный порог объёма и медленные вызовы, засчитанные как отказы.