Проектирование Circuit Breaker (Предохранитель). CB — это решение, которое предотвращает каскадные сбои и собирает метрики для мониторинга состояния внешних сервисов. То есть когда сервис от которого зависит наш клиент работает - он закрыт, если сбоит - открыт. В случае если CB открыт, то обычно отвечают либо подготовленным ответом, либо из кэша, либо идут в какую-то альтернативную реализацию.
Подсказки:
- Рассмотрите реализацию состояний, таких как CLOSED (нормальная работа), OPEN (сбой, не пытаться вызывать), и HALF-OPEN (проверка восстановления сервиса).
- Подумайте о конфигурациях таймаутов, порогах сбоев и периодах восстановления.
Выше ожиданий:
- Описаны переходы состояний circuit breaker с надлежащей потокобезопасностью
- Знает, что такое джиттер
- Реализация джиттера в механизмах повторных попыток, использование скользящего окна для расчета частоты ошибок
- Знает, что таймауты могут быть адаптивными на основе исторической производительности.
Паттерн «предохранитель» (circuit breaker) — это дизайн-паттерн, который предотвращает каскадные сбои, временно останавливая вызовы к проблемным сервисам. Реализация следует этой архитектуре:
Клиентское приложение → Предохранитель → Внешний сервис
↑
Хранилище метрик
Предохранитель отслеживает вызовы внешнего сервиса и переходит между тремя состояниями:
- CLOSED — Нормальная работа, запросы проходят.
- OPEN — Сервис сбоит, запросы блокируются.
- HALF-OPEN — Проверка восстановления сервиса.
Основные компоненты
1. Управление состоянием предохранителя
Предохранитель поддерживает состояние и переходы на основе пороговых значений ошибок:
success threshold exceeded
┌─────────┐ ┌─────────┐
│ │◄───────────────┤ │
│ CLOSED │ │ OPEN │
│ ├───────────────►│ │
└─────────┘ failure └─────────┘
▲ │
│ │
│ timeout │
│ elapsed │
│ ┌───────────┐ │
└────────┤ HALF-OPEN │◀────┘
success └───────────┘
failure returns
to OPEN
Обзор архитектуры
- Ядро предохранителя — основной компонент, управляющий переходами состояний.
- Сбор метрик — мониторинг и запись состояния здоровья сервиса.
- Система конфигурации — параметры поведения предохранителя.
- Механизм повторных попыток (retry) — интеллектуальный подход к повторным попыткам с джиттером.
- Обнаружение ошибок — идентификация и категоризация ошибок.
- Интеграция с приложением — применение паттерна к вызовам API.
Подход к реализации
Создайте базовый класс CircuitBreaker
:
class CircuitBreaker:
def __init__(self, failure_threshold, recovery_timeout, success_threshold):
self.state = "CLOSED"
self.failure_count = 0
self.success_count = 0
self.last_failure_time = None
self._lock = threading.RLock() # Безопасность потоков
# Параметры конфигурации
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.success_threshold = success_threshold
Предохранитель должен быть потоко-безопасным, так как несколько одновременных запросов могут пытаться его использовать. Можно использовать блокировку с повторным входом (reentrant lock) для защиты от переходов состояний.
Логика перехода состояний
Переходы состояний должны быть атомарными операциями:
- CLOSED → OPEN: Когда
failure_count
превышаетfailure_threshold
в заданном временном окне. - OPEN → HALF-OPEN: После того, как прошло время
recovery_timeout
. - HALF-OPEN → CLOSED: Когда
success_count
достигаетsuccess_threshold
. - HALF-OPEN → OPEN: При первой ошибке в состоянии HALF-OPEN.
Стратегия обнаружения ошибок
Вариант 1: Простой счётчик (OK)
Отслеживает общее количество ошибок в фиксированный период времени.
Вариант 2: Окно скользящего среднего (sliding window) (ХОРОШО)
Поддерживает окно последних запросов (например, последние 100 вызовов или последние 60 секунд) и рассчитывает процент ошибок.
def track_result(self, success, response_time=None):
with self._lock:
# Добавить в окно скользящего среднего со временем
self.request_history.append((time.time(), success, response_time))
# Удалить старые записи за пределами окна
self._prune_history()
# Рассчитать текущую частоту ошибок и обновить состояние
self._update_state()
Вариант 3: Экспоненциальное сглаживание (ХОРОШО)
Применяет Exponentially Weighted Moving Average (EWMA) к последним ошибкам, придавая большее значение недавним событиям.
Система сбора метрик
Ключевые метрики для сбора
- Количество запросов — общее количество запросов.
- Количество успехов/ошибок — результаты запросов.
- Время отклика — статистика задержек.
- Изменения состояния предохранителя — частота и продолжительность переходов состояний.
- Количество отклоненных запросов — запросы, отклоненные из-за состояния OPEN предохранителя.
Варианты хранения
Вариант 1: Сбор в памяти (OK)
Хранение метрик в памяти с периодическими снапшотами — подходит для одноинстансных приложений.
Вариант 2: Распределённая система метрик (GOOD)
Передача метрик во внешние системы, такие как Prometheus, VictoriaMetrics или DataDog, для кластеров и множества распределенных севрисов.
Дополнительные возможности
Адаптивные таймауты
Реализуйте таймауты, которые корректируются на основе исторической производительности:
def get_current_timeout(self):
with self._lock:
# Базовый таймаут из конфигурации
timeout = self.base_timeout
# Корректировка на основе перцентиля последних времен отклика
if len(self.response_times) >= 10:
p95 = calculate_percentile(self.response_times, 95)
# Установить таймаут немного выше 95-го перцентиля
timeout = min(self.max_timeout, p95 * 1.2)
return timeout
Повторные попытки с джиттером
Включение джиттера (случайной задержки) в механизмах повторных попыток предотвращает проблемы всплеска нагрузки при восстановлении сервисов:
def calculate_retry_delay(self, attempt):
# Экспоненциальное замедление с джиттером
base_delay = min(self.max_retry_delay,
self.base_retry_delay * (2 ** attempt))
# Добавление случайности (джиттера) ±25%
jitter = random.uniform(-0.25, 0.25) * base_delay
return base_delay + jitter
Практические рекомендации по конфигурации
Конфигурация порога ошибок
Пороговые значения на основе процентов - распространенный подход, при котором предохранитель срабатывает, когда частота ошибок превышает определенный процент в заданном временном окне. Типичная конфигурация - >50% ошибок за последние 10 секунд.
Пороговые значения на основе количества - предохранитель срабатывает после определенного количества последовательных ошибок (например, 5-10 последовательных ошибок).
Таймаут восстановления
- Сервисы с быстрым восстановлением: 5-10 секунд.
- Сервисы с медленным восстановлением: 30-60 секунд.
- Системы баз данных: Более длительные периоды (минуты).
Учёт безопасности потоков
Общий доступ к предохранителям
При совместном использовании предохранителей между потоками:
- Используйте потоко-безопасные коллекции для отслеживания запросов.
- Защищайте все изменения состояния с помощью блокировок.
- Рассмотрите использование атомарных операций для счётчиков.
Пример реализации
def allow_request(self):
with self._lock:
current_time = time.time()
if self.state == "CLOSED":
return True
elif self.state == "OPEN":
# Проверка, истек ли таймаут восстановления
if (self.last_failure_time and
current_time - self.last_failure_time >= self.recovery_timeout):
# Переход в HALF-OPEN
self.state = "HALF-OPEN"
self.success_count = 0
return True
return False
elif self.state == "HALF-OPEN":
# Разрешить ограниченное количество запросов для тестирования сервиса
return True
Стратегии тестирования (НЕОБЯЗАТЕЛЬНО)
Unit тесты
- Проверка переходов состояний с имитацией временем.
- Проверка правильности подсчёта ошибок.
- Тестирование логики восстановления.
- Валидация сбора метрик.
Интеграционное тестирование
- Тестирование с имитируемыми сервисами, которые можно настроить на возврат ошибок.
- Проверка поведения предохранителя с реальными HTTP-запросами.
- Тестирование обработки конкурентности.
Распространённые ошибки, которых следует избегать
- Чрезмерно агрессивные таймауты — слишком короткие таймауты могут вызывать ненужные сбои.
- Отсутствие джиттера в повторных попытках — может привести к синхронным штормам запросов и сбое в следствии перегзрузки.
- Недостаточное обнаружение ошибок — не все ошибки учитываются.
- Небезопасная реализация для потоков — гонки при переходах состояний.
- Общие предохранители — использование одного предохранителя для несвязанных сервисов.
Нюансы использования в реальном мире
- Разным сервисам могут потребоваться разные конфигурации предохранителей.
- Рассмотрите использование фабрики предохранителей, которая предоставляет предварительно настроенные экземпляры.
- Интегрируйте с service discovery для динамической обработки эндпоинтов.
- Предоставьте возможность ручного сброса для операторов или службы поддержки.
- Сделайте панели мониторинга состояний предохранителей в Grafana для визуального контроля.