Суть Читай реальный конфиг breaker'а и код места вызова, предсказывай переходы состояний и поведение срабатывания и выбирай фикс, который senior сделает первым.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min
Поведение breaker’а решается в его конфиге и в месте вызова. Прочитай каждый сниппет, предскажи, что breaker реально делает под нагрузкой, и выбери изменение, которое senior сделает первым.
Цель
Отработай цикл, который ты гоняешь на каждом ревью breaker’а: прочитай конфиг и обёртку вызова, предскажи переходы состояний и поведение срабатывания и заметь мисконфигурацию, из-за которой breaker срабатывает не так — или не срабатывает вовсе.
Breaker только что перешёл CLOSED в OPEN, и таймер cooldown идёт. Приходит следующий вызов executeSupplier. Что происходит и какова нагрузка на зависимость прямо сейчас?
Heads-up OPEN — состояние отказа: оно короткозамыкает каждый вызов, не трогая зависимость. Подсчёт идёт в CLOSED; OPEN активно блокирует.
Heads-up Half-open входит только после истечения waitDurationInOpenState. Пока cooldown не сработал, breaker остаётся в OPEN и отвергает.
Heads-up OPEN не ставит вызовы в очередь на потом — он отвергает их мгновенно, чтобы вызывающий мог сделать fallback. Очередь снова пришпилила бы ресурсы, ради освобождения которых breaker и существует.
Сниппет 2 — конфиг порога
CircuitBreakerConfig.custom() .slidingWindowType(SlidingWindowType.COUNT_BASED) .slidingWindowSize(100) .failureRateThreshold(50) // срабатывать при 50% отказов .minimumNumberOfCalls(100) // ...но только после 100 вызовов .build();// эндпоинт: /admin/report -> ~6 запросов в минуту
Викторина
Completed
Этот конфиг применён к низконагруженному admin-эндпоинту, обслуживающему примерно 6 запросов/минуту. Зависимость за ним жёстко падает. Как ведёт себя breaker и почему?
Heads-up Ниже minimumNumberOfCalls breaker остаётся закрытым независимо от failure rate. При требуемых 100 и 6/мин он не может сработать около 17 минут — порог неправильно рассчитан под этот трафик.
Heads-up Порог — это rate, а не сырой count, и его даже нельзя вычислить, пока не видно 100 вызовов. Проблема в том, что порог объёма блокирует срабатывание, а не в rate.
Heads-up Он влияет сильно: при 6/мин 100 вызовов растягиваются примерно на 17 минут, поэтому count-based окно в 100 реагирует тут очень медленно. Именно поэтому для низкого трафика часто предпочитают time-based окно.
Сниппет 3 — half-open проба
CircuitBreakerConfig.custom() .waitDurationInOpenState(Duration.ofSeconds(10)) .permittedNumberOfCallsInHalfOpenState(10) // automaticTransitionFromOpenToHalfOpenEnabled = false (default) .build();// зависимость восстановилась на секунде 4; breaker сработал на секунде 0// между секундой 0 и секундой 30 вызовы не приходят
Викторина
Completed
Зависимость восстановилась на секунде 4, а cooldown — 10 с, но между секундой 0 и секундой 30 вызовы не приходят. Когда этот breaker реально зондирует и снова открывается для трафика?
Heads-up Авто-переход отключён по умолчанию, поэтому breaker не зондирует по одному таймеру. Ему нужен входящий вызов после cooldown, чтобы перейти в half-open.
Heads-up У breaker'а нет прямого взгляда на здоровье зависимости — он узнаёт только из исходов вызовов. Он не может знать, что восстановление было на секунде 4, не отправив пробу.
Heads-up Он остаётся открытым лишь до прихода следующего вызова после cooldown; этот вызов запускает half-open. Он не застрял навсегда — он ждёт трафика, чтобы зондировать.
Сниппет 4 — взаимодействие с timeout
// breaker считает отказы, но у вызова нет бюджета времениSupplier<Resp> guarded = CircuitBreaker .decorateSupplier(cb, () -> recsClient.fetch(req)); // может висеть 30с// во время инцидента recsClient.fetch не ошибается -- он просто виситResp r = guarded.get();
Викторина
Completed
Во время инцидента recsClient.fetch перестаёт ошибаться и вместо этого висит по 30 с на вызов. Breaker подключён, но никогда не срабатывает. Какой единственный фикс с наибольшим рычагом?
Heads-up Никакие отказы вообще не записываются, поэтому любой порог не релевантен. Зависание сначала надо превратить в считаемый отказ через timeout.
Heads-up Большее окно всё равно записывает ноль отказов, потому что зависание без timeout не успех и не отказ. Окно не может захватить события, которые никогда не регистрируются.
Heads-up Ретрай 30-секундного зависания умножает стоимость ресурсов и никогда не заставит breaker его посчитать. Фикс — timeout, превращающий зависание в отказ, который breaker может видеть.
Итог
Поведение breaker’а читается в конфиге и в месте вызова. OPEN короткозамыкает каждый вызов мгновенно, так что зависимость получает нулевую нагрузку; минимальный порог объёма, рассчитанный на высокий трафик, тихо блокирует срабатывание на низконагруженном эндпоинте (используй меньший порог или time-based окно); с отключённым авто-переходом breaker зондирует только когда вызов приходит после cooldown, никогда по одному таймеру, поэтому простаивающий breaker остаётся открытым, пока не вернётся трафик; и breaker без timeout слеп к зависанию, потому что только бюджет времени превращает зависание в считаемый отказ. Диагностируй по конфигу и обёртке, затем чини ту одну настройку, что заставляет breaker срабатывать правильно.