Архитектура бэкенда
Зачем circuit breaker: медленная зависимость роняет вызывающего
Платёжный провайдер во время инцидента замедляется с 50 мс до 5 с — он не падает, просто тормозит. Твой обработчик checkout зовёт его на каждый заказ и ждёт. За секунды каждый рабочий поток припаркован на 5-секундном платёжном вызове, пул потоков полон, и сервис перестаёт принимать любой запрос — включая главную страницу и листинг товаров, которые платежей вообще не касаются. Ничего не упало. Один даунстрим затормозил, твой код вежливо ждал, и ожидание расползлось, пока весь сервис не лёг. Circuit breaker — это тот элемент, который заметил бы, что платежи отказывают, и начал бы отклонять эти вызовы мгновенно — освобождая потоки и сохраняя жизнь остальному приложению.
Медленная зависимость опаснее мёртвой
Зависимость, которая отказывает в соединении, падает мгновенно — твой вызов возвращает ошибку за миллисекунду, и поток идёт дальше. Зависимость, которая принимает соединение и потом 5 секунд отвечает, гораздо хуже, потому что теперь каждый вызывающий блокируется на 5 секунд, держа дефицитные ресурсы: рабочий поток, сокет, взятое из пула соединение к БД, и часто вышестоящего вызывающего, ждущего его.
Это та же проблема занятости из юнита про пулы, теперь на слой выше. Под нагрузкой математика жестока: если обработчик держит поток 5 с вместо 50 мс, тому же трафику нужно в 100 раз больше потоков, чтобы успевать. Их никогда не хватает, поэтому запросы выстраиваются в очередь, пул потоков переполняется, и сервис уже не может обслуживать работу, никак не связанную с медленной зависимостью. Один больной даунстрим становится полным отказом — каскадным сбоем.
Быстрый отказ лучше зависания
Решение контринтуитивно: когда зависимость отказывает, самое безопасное — перестать её звать и вернуть ошибку немедленно. Вернуть отказ за 1 мс строго лучше, чем таймаут за 5 с, потому что быстрый отказ освобождает ресурсы вызывающего — поток, соединение, слот вышестоящего — на что-то полезное: обслужить другие маршруты, вернуть деградированный ответ, сбросить нагрузку. Медленный успех, который так и не придёт, всё равно стоит тебе всего, что стоил бы настоящий успех; быстрый отказ не стоит почти ничего.
Circuit breaker автоматизирует ровно это. Он стоит перед зависимостью, следит за вызовами, и когда отказы пересекают порог — размыкается: на период остывания он отклоняет вызовы мгновенно, даже не пытаясь, а потом осторожно проверяет, восстановилась ли зависимость. Имя буквальное: как электрический автомат, он размыкается, чтобы прекратить ток в неисправность, защищая всё, что подключено за ним.
Почему это работает
Зачем отклонять вызовы, которые ты мог бы сделать, вместо того чтобы каждый пробовал и упирался в таймаут? Потому что каждая попытка к больной зависимости не бесплатна — она тратит поток, соединение и таймаут стенных часов ожидания, всё на вызов, который почти наверняка упадёт. Когда зависимость действительно лежит, эти попытки не дают пользы и приносят реальный вред: держат твои ресурсы припёртыми, наваливают ретраи на сервис, которому для восстановления нужно меньше нагрузки, и привязывают твою латентность к сломанной зависимости. Ставка брейкера статистическая — раз достаточно недавних вызовов упало, следующий с подавляющей вероятностью упадёт тоже, так что ожидаемая ценность попытки отрицательна. Быстрый отказ превращает медленный, жрущий ресурсы, усиливающий вред сбой в дешёвый и мгновенный, и возвращает освободившуюся ёмкость частям системы, которые ещё работают. Это та же дисциплина, что и ограниченная очередь ожидания: ты ограничиваешь ущерб, который сломанная штука может нанести, вместо того чтобы дать ей сожрать всю систему по дороге вниз.
Что брейкер тебе даёт
Брейкер превращает неограниченный системный сбой в ограниченный локальный. Вместо того чтобы каждый вызывающий узнавал о поломке зависимости медленным дорогим путём — ждя таймаут, — брейкер узнаёт об этом один раз, а потом коротко-замыкает всех остальных дёшево, пока зависимость не докажет, что восстановилась. Именно это изменение и останавливает один медленный сервис от заваливания остальных.
| Без брейкера | С брейкером | |
|---|---|---|
| Медленная зависимость | Каждый вызывающий ждёт весь таймаут | Первые несколько падают, остальные отклонены мгновенно |
| Стоимость потока/соединения | Припёрты на всё время ожидания | Освобождены сразу при быстром отказе |
| Радиус поражения | Весь сервис задыхается | Ограничен одной зависимостью |
| Нагрузка восстановления | Полный трафик продолжает долбить | Только струйка пробных вызовов |
| Латентность вызывающего | Следует за сломанным даунстримом | Остаётся быстрой (мгновенная ошибка) |
Платёжный провайдер замедляется с 50 мс до 5 с, но полностью не падает. За секунды весь сервис перестаёт обслуживать даже несвязанные маршруты. Почему медленный случай хуже прямого отказа?
Почему вернуть ошибку за 1 мс лучше, чем таймаут за 5 с, против отказывающей зависимости?
Расставь по порядку, как медленная зависимость каскадом превращается в полный отказ без брейкера:
- 1 Даунстрим-зависимость замедляется с миллисекунд до секунд
- 2 Каждый вызывающий блокируется на ней, держа рабочий поток всё время ожидания
- 3 Общий пул потоков заполняется припаркованными вызывающими
- 4 Сервис больше не может принять ни один запрос, даже несвязанные маршруты
- 01Почему медленная зависимость опаснее зависимости, которая полностью лежит?
- 02Почему быстрый отказ лучше, чем дать каждому вызову упереться в таймаут, и что брейкер на самом деле делает?
Зависимость редко умирает чисто; она замедляется, и медленная — опасный случай, потому что каждый вызывающий ждёт всё время, держа поток, сокет и взятое из пула соединение — проблема занятости из юнита про пулы на слой выше. Под нагрузкой вызов, выросший с 50 мс до 5 с, требует примерно в сто раз больше потоков, поэтому пул переполняется и сервис задыхается даже на маршрутах, которые медленной зависимости не касаются: каскадный сбой, где один больной даунстрим роняет всё. Фикс контринтуитивен — когда зависимость отказывает, перестань её звать и верни ошибку немедленно, потому что отказ за 1 мс освобождает ресурсы вызывающего на полезную работу, а таймаут за 5 с припирает их на обречённом вызове и продолжает долбить сервис, которому нужно меньше нагрузки. Circuit breaker автоматизирует это: следит за вызовами, размыкается при пересечении порога, отклоняет вызовы мгновенно во время остывания, потом проверяет восстановление. Следующий урок раскрывает брейкер изнутри — трёхсостоянийную машину closed, open, half-open и таймер остывания, решающий, как быстро он проверяет восстановление.