Короткий ответ
Bulkhead ограничивает долю ресурсов, которую может занять вызов к нестабильной зависимости, не давая ей исчерпать общий ресурс системы; circuit breaker поверх этого снижает количество бесполезных попыток к уже деградировавшей зависимости внутри изолированного пула — конкретные параметры нужно подбирать под профиль зависимости и нагрузки.
Как это работает подробнее
Bulkhead выделяет, например, 10 из 100 воркеров под вызовы к зависимости X; если X деградирует, максимум 10 воркеров будет занято, остальные 90 продолжают обслуживать остальной трафик; circuit breaker на этом пуле при превышении порога ошибок переходит в OPEN, освобождая даже эти 10 воркеров.
Чем больше ресурсов выделено под bulkhead, тем выше throughput в нормальном режиме, но тем больше ресурсов теряется при деградации зависимости; чем меньше — тем лучше изоляция, но тем ниже пропускная способность. Агрессивные пороги breaker защищают быстрее, но могут ложно открываться на нормальные всплески ошибок. Критерий выбора — исторические данные о нормальном уровне ошибок/латентности и бизнес-цена как ложного срабатывания, так и задержки в срабатывании.
Пример кода
payment_bulkhead = ThreadPoolExecutor(max_workers=10) # изолированный пул
payment_breaker = CircuitBreaker(failure_threshold=5, reset_timeout=30)
def call_payment(request):
if payment_breaker.is_open():
return fallback_response()
future = payment_bulkhead.submit(payment_client.charge, request)
try:
return future.result(timeout=3)
except Exception:
payment_breaker.record_failure()
return fallback_response()Что использовать на практике
- Практика. Размер bulkhead-пула подбирают исходя из ожидаемой доли трафика и допустимой деградации остальной системы; пороги breaker подбирают на основе наблюдаемого нормального уровня ошибок и типичного времени восстановления зависимости.
Подводные камни
- Риск. Настраивать breaker глобально на весь сервис, а не на пул конкретной зависимости; выбирать параметры один раз «по интуиции» и не пересматривать их по мере изменения профиля нагрузки.