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

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

Зачем circuit breaker: медленная зависимость роняет вызывающего

Суть Зависимость редко падает чисто — она замедляется, и медленная зависимость опаснее мёртвой: каждый вызывающий ждёт, занимая поток, пока весь сервис не задохнётся. Circuit breaker быстро отказывает вызовам к больной зависимости, чтобы вызывающий перестал ждать.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на junior-высоте — поверхность
◷ 12 min

Платёжный провайдер во время инцидента замедляется с 50 мс до 5 с — он не падает, просто тормозит. Твой обработчик checkout зовёт его на каждый заказ и ждёт. За секунды каждый рабочий поток припаркован на 5-секундном платёжном вызове, пул потоков полон, и сервис перестаёт принимать любой запрос — включая главную страницу и листинг товаров, которые платежей вообще не касаются. Ничего не упало. Один даунстрим затормозил, твой код вежливо ждал, и ожидание расползлось, пока весь сервис не лёг. Circuit breaker — это тот элемент, который заметил бы, что платежи отказывают, и начал бы отклонять эти вызовы мгновенно — освобождая потоки и сохраняя жизнь остальному приложению.

Медленная зависимость опаснее мёртвой

Зависимость, которая отказывает в соединении, падает мгновенно — твой вызов возвращает ошибку за миллисекунду, и поток идёт дальше. Зависимость, которая принимает соединение и потом 5 секунд отвечает, гораздо хуже, потому что теперь каждый вызывающий блокируется на 5 секунд, держа дефицитные ресурсы: рабочий поток, сокет, взятое из пула соединение к БД, и часто вышестоящего вызывающего, ждущего его.

Это та же проблема занятости из юнита про пулы, теперь на слой выше. Под нагрузкой математика жестока: если обработчик держит поток 5 с вместо 50 мс, тому же трафику нужно в 100 раз больше потоков, чтобы успевать. Их никогда не хватает, поэтому запросы выстраиваются в очередь, пул потоков переполняется, и сервис уже не может обслуживать работу, никак не связанную с медленной зависимостью. Один больной даунстрим становится полным отказом — каскадным сбоем.

Быстрый отказ лучше зависания

Решение контринтуитивно: когда зависимость отказывает, самое безопасное — перестать её звать и вернуть ошибку немедленно. Вернуть отказ за 1 мс строго лучше, чем таймаут за 5 с, потому что быстрый отказ освобождает ресурсы вызывающего — поток, соединение, слот вышестоящего — на что-то полезное: обслужить другие маршруты, вернуть деградированный ответ, сбросить нагрузку. Медленный успех, который так и не придёт, всё равно стоит тебе всего, что стоил бы настоящий успех; быстрый отказ не стоит почти ничего.

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

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

Зачем отклонять вызовы, которые ты мог бы сделать, вместо того чтобы каждый пробовал и упирался в таймаут? Потому что каждая попытка к больной зависимости не бесплатна — она тратит поток, соединение и таймаут стенных часов ожидания, всё на вызов, который почти наверняка упадёт. Когда зависимость действительно лежит, эти попытки не дают пользы и приносят реальный вред: держат твои ресурсы припёртыми, наваливают ретраи на сервис, которому для восстановления нужно меньше нагрузки, и привязывают твою латентность к сломанной зависимости. Ставка брейкера статистическая — раз достаточно недавних вызовов упало, следующий с подавляющей вероятностью упадёт тоже, так что ожидаемая ценность попытки отрицательна. Быстрый отказ превращает медленный, жрущий ресурсы, усиливающий вред сбой в дешёвый и мгновенный, и возвращает освободившуюся ёмкость частям системы, которые ещё работают. Это та же дисциплина, что и ограниченная очередь ожидания: ты ограничиваешь ущерб, который сломанная штука может нанести, вместо того чтобы дать ей сожрать всю систему по дороге вниз.

Что брейкер тебе даёт

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

Без брейкераС брейкером
Медленная зависимостьКаждый вызывающий ждёт весь таймаутПервые несколько падают, остальные отклонены мгновенно
Стоимость потока/соединенияПрипёрты на всё время ожиданияОсвобождены сразу при быстром отказе
Радиус пораженияВесь сервис задыхаетсяОграничен одной зависимостью
Нагрузка восстановленияПолный трафик продолжает долбитьТолько струйка пробных вызовов
Латентность вызывающегоСледует за сломанным даунстримомОстаётся быстрой (мгновенная ошибка)
Викторина

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

Викторина

Почему вернуть ошибку за 1 мс лучше, чем таймаут за 5 с, против отказывающей зависимости?

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

Расставь по порядку, как медленная зависимость каскадом превращается в полный отказ без брейкера:

  1. 1 Даунстрим-зависимость замедляется с миллисекунд до секунд
  2. 2 Каждый вызывающий блокируется на ней, держа рабочий поток всё время ожидания
  3. 3 Общий пул потоков заполняется припаркованными вызывающими
  4. 4 Сервис больше не может принять ни один запрос, даже несвязанные маршруты
Вспомните перед уходом
  1. 01
    Почему медленная зависимость опаснее зависимости, которая полностью лежит?
  2. 02
    Почему быстрый отказ лучше, чем дать каждому вызову упереться в таймаут, и что брейкер на самом деле делает?
Итог

Зависимость редко умирает чисто; она замедляется, и медленная — опасный случай, потому что каждый вызывающий ждёт всё время, держа поток, сокет и взятое из пула соединение — проблема занятости из юнита про пулы на слой выше. Под нагрузкой вызов, выросший с 50 мс до 5 с, требует примерно в сто раз больше потоков, поэтому пул переполняется и сервис задыхается даже на маршрутах, которые медленной зависимости не касаются: каскадный сбой, где один больной даунстрим роняет всё. Фикс контринтуитивен — когда зависимость отказывает, перестань её звать и верни ошибку немедленно, потому что отказ за 1 мс освобождает ресурсы вызывающего на полезную работу, а таймаут за 5 с припирает их на обречённом вызове и продолжает долбить сервис, которому нужно меньше нагрузки. Circuit breaker автоматизирует это: следит за вызовами, размыкается при пересечении порога, отклоняет вызовы мгновенно во время остывания, потом проверяет восстановление. Следующий урок раскрывает брейкер изнутри — трёхсостоянийную машину closed, 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.