Design and implementation of circuit breaker pattern | Вопросы для собеседования | Skilio
Design and implementation of circuit breaker pattern
Вопрос:

Проектирование Circuit Breaker (Предохранитель). CB — это решение, которое предотвращает каскадные сбои и собирает метрики для мониторинга состояния внешних сервисов. То есть когда сервис от которого зависит наш клиент работает - он закрыт, если сбоит - открыт. В случае если CB открыт, то обычно отвечают либо подготовленным ответом, либо из кэша, либо идут в какую-то альтернативную реализацию.

Подсказки:

  • Рассмотрите реализацию состояний, таких как CLOSED (нормальная работа), OPEN (сбой, не пытаться вызывать), и HALF-OPEN (проверка восстановления сервиса).
  • Подумайте о конфигурациях таймаутов, порогах сбоев и периодах восстановления.

Выше ожиданий:

  • Описаны переходы состояний circuit breaker с надлежащей потокобезопасностью
  • Знает, что такое джиттер
  • Реализация джиттера в механизмах повторных попыток, использование скользящего окна для расчета частоты ошибок
  • Знает, что таймауты могут быть адаптивными на основе исторической производительности.
Ответ:

Паттерн «предохранитель» (circuit breaker) — это дизайн-паттерн, который предотвращает каскадные сбои, временно останавливая вызовы к проблемным сервисам. Реализация следует этой архитектуре:

Клиентское приложение → Предохранитель → Внешний сервис
                         ↑
                    Хранилище метрик

Предохранитель отслеживает вызовы внешнего сервиса и переходит между тремя состояниями:

  1. CLOSED — Нормальная работа, запросы проходят.
  2. OPEN — Сервис сбоит, запросы блокируются.
  3. HALF-OPEN — Проверка восстановления сервиса.

Основные компоненты

1. Управление состоянием предохранителя

Предохранитель поддерживает состояние и переходы на основе пороговых значений ошибок:

    success          threshold exceeded
  ┌─────────┐                ┌─────────┐
  │         │◄───────────────┤         │
  │ CLOSED  │                │  OPEN   │
  │         ├───────────────►│         │
  └─────────┘     failure    └─────────┘
       ▲                          │
       │                          │
       │          timeout         │
       │          elapsed         │
       │        ┌───────────┐     │
       └────────┤ HALF-OPEN │◀────┘
    success     └───────────┘
                failure returns
                  to OPEN

Обзор архитектуры

  1. Ядро предохранителя — основной компонент, управляющий переходами состояний.
  2. Сбор метрик — мониторинг и запись состояния здоровья сервиса.
  3. Система конфигурации — параметры поведения предохранителя.
  4. Механизм повторных попыток (retry) — интеллектуальный подход к повторным попыткам с джиттером.
  5. Обнаружение ошибок — идентификация и категоризация ошибок.
  6. Интеграция с приложением — применение паттерна к вызовам 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) для защиты от переходов состояний.

Логика перехода состояний

Переходы состояний должны быть атомарными операциями:

  1. CLOSED → OPEN: Когда failure_count превышает failure_threshold в заданном временном окне.
  2. OPEN → HALF-OPEN: После того, как прошло время recovery_timeout.
  3. HALF-OPEN → CLOSED: Когда success_count достигает success_threshold.
  4. 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) к последним ошибкам, придавая большее значение недавним событиям.

Система сбора метрик

Ключевые метрики для сбора

  1. Количество запросов — общее количество запросов.
  2. Количество успехов/ошибок — результаты запросов.
  3. Время отклика — статистика задержек.
  4. Изменения состояния предохранителя — частота и продолжительность переходов состояний.
  5. Количество отклоненных запросов — запросы, отклоненные из-за состояния 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 секунд.
  • Системы баз данных: Более длительные периоды (минуты).

Учёт безопасности потоков

Общий доступ к предохранителям

При совместном использовании предохранителей между потоками:

  1. Используйте потоко-безопасные коллекции для отслеживания запросов.
  2. Защищайте все изменения состояния с помощью блокировок.
  3. Рассмотрите использование атомарных операций для счётчиков.

Пример реализации

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 тесты

  1. Проверка переходов состояний с имитацией временем.
  2. Проверка правильности подсчёта ошибок.
  3. Тестирование логики восстановления.
  4. Валидация сбора метрик.

Интеграционное тестирование

  1. Тестирование с имитируемыми сервисами, которые можно настроить на возврат ошибок.
  2. Проверка поведения предохранителя с реальными HTTP-запросами.
  3. Тестирование обработки конкурентности.

Распространённые ошибки, которых следует избегать

  1. Чрезмерно агрессивные таймауты — слишком короткие таймауты могут вызывать ненужные сбои.
  2. Отсутствие джиттера в повторных попытках — может привести к синхронным штормам запросов и сбое в следствии перегзрузки.
  3. Недостаточное обнаружение ошибок — не все ошибки учитываются.
  4. Небезопасная реализация для потоков — гонки при переходах состояний.
  5. Общие предохранители — использование одного предохранителя для несвязанных сервисов.

Нюансы использования в реальном мире

  • Разным сервисам могут потребоваться разные конфигурации предохранителей.
  • Рассмотрите использование фабрики предохранителей, которая предоставляет предварительно настроенные экземпляры.
  • Интегрируйте с service discovery для динамической обработки эндпоинтов.
  • Предоставьте возможность ручного сброса для операторов или службы поддержки.
  • Сделайте панели мониторинга состояний предохранителей в Grafana для визуального контроля.
0
Python Старший Опубликовано
© Skilio, 2025
Условия использования
Политика конфиденциальности
Мы используем файлы cookie, для персонализации сервисов и повышения удобства пользования сайтом. Если вы не согласны на их использование, поменяйте настройки браузера.