Привет, Хабр!

Каждая секунда простоя может стоить компании целое состояние, важно иметь надежные механизмы защиты от сбоев. Здесь и приходит на помощь паттерн Circuit Breaker.

Представьте себе обычный автоматический выключатель в вашем доме. Когда происходит перегрузка, он "выбивается", предотвращая возможные повреждения. Точно так же работает и Circuit Breaker в микросервисах. Он мониторит вызовы к внешнему сервису и при обнаружении слишком большого количества неудачных попыток временно "отключает" вызов, предотвращая тем самым падение всей системы.

Этот паттерн основывается на трех основных состояниях: закрытое, открытое и полуоткрытое.

В "закрытом" состоянии запросы к сервису выполняются нормально. Если сбои начинают учащаться, состояние переключается на "открытое", и вызовы временно блокируются. После определенного времени система переходит в "полуоткрытое" состояние, где некоторые вызовы тестируются, чтобы проверить, исправился ли сбой.

Состояния Circuit Breaker

Закрытое состояние (Closed state)

В закрытом состоянии Circuit Breaker позволяет запросам проходить к защищаемому сервису. Это нормальное рабочее состояние.

В этом состоянии Circuit Breaker отслеживает количество неудачных запросов. Если число ошибок не превышает определенный порог, то Circuit Breaker продолжает оставаться в закрытом состоянии.

Обычно реализации Circuit Breaker ведут учет времени ответа и количества неудачных запросов. Это позволяет определить, когда пора переключиться в открытое состояние.

Открытое состояние (Open state)

В открытом состоянии Circuit Breaker блокирует все попытки выполнить запрос к защищаемому сервису. Это профилактическая мера, предотвращающая дальнейшее распространение ошибок.

Переход в открытое состояние происходит, когда количество неудачных запросов в закрытом состоянии превышает установленный порог. Этот порог может быть определен количеством ошибок, временем ответа или комбинацией обоих факторов.

После перехода в открытое состояние, Circuit Breaker остается в этом состоянии в течение определенного времени. Этот период называется временем ожидания (timeout) и является критическим для восстановления стабильности защищаемого сервиса.

Полуоткрытое состояние (Half-open state)

Полуоткрытое состояние - это переходное состояние, в котором Circuit Breaker начинает частично разрешать запросы к сервису для тестирования его доступности и надежности.

После истечения времени ожидания в открытом состоянии, Circuit Breaker переходит в полуоткрытое состояние. В этом состоянии он позволяет ограниченное количество запросов пройти к сервису. Если эти запросы успешно обработаны и не вызывают ошибок, Circuit Breaker возвращается в закрытое состояние, считая, что проблемы с сервисом устранены.

Если в полуоткрытом состоянии обнаруживаются ошибки, Circuit Breaker снова переходит в открытое состояние, и время ожидания начинает отсчитываться заново. Это гарантирует, что в случае повторного возникновения проблемы, нагрузка на сервис будет снижена.

Алгоритмы определения необходимости активации Circuit Breake

1. Порог ошибок (Error threshold)

Circuit Breaker переходит в открытое состояние, когда количество ошибок в определенном временном интервале превышает заданный порог.

Этот подход требует отслеживания количества неудачных запросов и их сравнения с пороговым значением. Например, если в течение минуты происходит более 50 неудачных вызовов, Circuit Breaker активируется.

Порог ошибок должен быть настроен с учетом нормальной работы сервиса и его способности к самовосстановлению.

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

class CircuitBreaker:
    def __init__(self, error_threshold):
        self.error_threshold = error_threshold
        self.errors = 0

    def record_failure(self):
        self.errors += 1
        if self.errors >= self.error_threshold:
            self.open()

    def reset(self):
        self.errors = 0

    def open(self):
        print("Circuit Breaker Opened")

# Пример использования
circuit_breaker = CircuitBreaker(error_threshold=50)
# Предположим, что следующий код вызывается в ответ на ошибки
for _ in range(60):
    circuit_breaker.record_failure()

2. Процентный порог ошибок (Error rate threshold)

Вместо фиксированного количества ошибок, Circuit Breaker активируется, когда процент ошибок от общего числа запросов превышает заданный уровень.

Для этого метода необходимо отслеживать общее количество запросов и количество ошибок, а затем рассчитывать процент ошибок. Например, если более 30% запросов в течение определенного временного интервала завершаются ошибкой, Circuit Breaker активируется.

Этот подход более гибок, так как учитывает общий объем трафика и менее чувствителен к его колебаниям.

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

public class CircuitBreaker {
    private int requestCount;
    private int errorCount;
    private double errorRateThreshold;

    public CircuitBreaker(double errorRateThreshold) {
        this.errorRateThreshold = errorRateThreshold;
    }

    public synchronized void onRequest(boolean success) {
        requestCount++;
        if (!success) {
            errorCount++;
        }
        if ((double) errorCount / requestCount > errorRateThreshold) {
            openCircuit();
        }
    }

    private void openCircuit() {
        System.out.println("Circuit Breaker Opened");
        // дальнейшие действия для открытия цепи
    }

    // Метод для сброса состояния
    public synchronized void reset() {
        requestCount = 0;
        errorCount = 0;
    }
}

// Пример использования
CircuitBreaker circuitBreaker = new CircuitBreaker(0.30);
for (int i = 0; i < 100; i++) {
    // В качестве примера, каждый 4-й запрос неуспешен
    circuitBreaker.onRequest(i % 4 != 0);
}

3. Оценка времени ответа (Response time evaluation)

Активация Circuit Breaker происходит, если среднее время ответа сервиса превышает установленный порог.

Этот метод требует постоянного мониторинга времени отклика сервиса. Если среднее время отклика за определенный период времени превышает заданное значение, Circuit Breaker переходит в открытое состояние.

Здесь важно правильно выбрать порог времени ответа, учитывая специфику сервиса и ожидаемую производительность.

Пример реализации на си шарпе:

public class CircuitBreaker {
    private TimeSpan _responseTimeThreshold;
    private List<TimeSpan> _responseTimes;

    public CircuitBreaker(TimeSpan responseTimeThreshold) {
        _responseTimeThreshold = responseTimeThreshold;
        _responseTimes = new List<TimeSpan>();
    }

    public void RecordResponseTime(TimeSpan responseTime) {
        _responseTimes.Add(responseTime);
        if (_responseTimes.Average() > _responseTimeThreshold) {
            OpenCircuit();
        }
    }

    private void OpenCircuit() {
        Console.WriteLine("Circuit Breaker Opened");
        // Действия при открытии цепи
    }

    public void Reset() {
        _responseTimes.Clear();
    }
}

// Пример использования
CircuitBreaker circuitBreaker = new CircuitBreaker(TimeSpan.FromMilliseconds(500));
circuitBreaker.RecordResponseTime(TimeSpan.FromMilliseconds(100));
circuitBreaker.RecordResponseTime(TimeSpan.FromMilliseconds(600));

4. Гибридные методы

Часто используется комбинация нескольких методов для определения состояния Circuit Breaker. Например, можно сочетать порог ошибок с оценкой времени ответа.

Гибридные методы позволяют более точно реагировать на разнообразные ситуации, учитывая как количество ошибок, так и изменения в производительности.

Сочетаем порог ошибок и время отклика на джава скрипте:

class CircuitBreaker {
    constructor(errorThreshold, responseTimeThreshold) {
        this.errorThreshold = errorThreshold;
        this.responseTimeThreshold = responseTimeThreshold;
        this.errors = 0;
        this.totalRequests = 0;
        this.totalResponseTime = 0;
    }

    recordRequest(success, responseTime) {
        this.totalRequests++;
        this.totalResponseTime += responseTime;
        if (!success) this.errors++;

        if (this.errors / this.totalRequests > this.errorThreshold ||
            (this.totalResponseTime / this.totalRequests) > this.responseTimeThreshold) {
            this.open();
        }
    }

    open() {
        console.log("Circuit Breaker Opened");
    }

    reset() {
        this.errors = 0;
        this.totalRequests = 0;
        this.totalResponseTime = 0;
    }
}

// Пример использования
let circuitBreaker = new CircuitBreaker(0.30, 500);
circuitBreaker.recordRequest(false, 100);
circuitBreaker.recordRequest(true, 600);

5. Машинное обучение

Есть алгоритмы, которые могут динамически адаптировать параметры Circuit Breaker, анализируя паттерны трафика и поведение сервисов.

Эти системы способны самостоятельно настраивать пороги и параметры, обучаясь на исторических данных и текущем состоянии системы.

С использованием библиотеки scikit-learn реализуем простую модель обучения, которая предсказывает вероятность сбоя сервиса:

from sklearn.ensemble import RandomForestClassifier
import numpy as np

class AdaptiveCircuitBreaker:
    def __init__(self):
        self.model = RandomForestClassifier()
        self.data = []
        self.labels = []
        self.threshold = 0.5

    def collect_data(self, features, label):
        self.data.append(features)
        self.labels.append(label)

    def train_model(self):
        self.model.fit(self.data, self.labels)

    def predict_failure(self, current_features):
        probability = self.model.predict_proba([current_features])[0][1]
        return probability > self.threshold

    def check_and_open(self, current_features):
        if self.predict_failure(current_features):
            self.open()

    def open(self):
        print("Circuit Breaker Opened")

# приер использования
cb = AdaptiveCircuitBreaker()

# Сбор данных: функции могут быть различными метриками сервиса (например, время отклика, количество запросов в минуту и т.д.)
cb.collect_data([0.4, 0.6, 200], 0)  # 0 - сервис работает нормально
cb.collect_data([0.7, 0.8, 300], 1)  # 1 - сервис вышел из строя

# Обучение модели
cb.train_model()

# Проверка текущего состояния и активация Circuit Breaker при необходимости
cb.check_and_open([0.5, 0.7, 250])

6. Логика перехода между состояниями

Помимо определения критериев активации, важно также четко определить условия для перехода из открытого в полуоткрытое и обратно в закрытое состояние, на java это может выглядеть так:

import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;

import java.time.Duration;

public class StateTransitionCircuitBreaker {
    private CircuitBreaker circuitBreaker;

    public StateTransitionCircuitBreaker() {
        CircuitBreakerConfig config = CircuitBreakerConfig.custom()
                .failureRateThreshold(50)
                .waitDurationInOpenState(Duration.ofMillis(1000))
                .permittedNumberOfCallsInHalfOpenState(2)
                .slidingWindowSize(10)
                .build();

        this.circuitBreaker = CircuitBreaker.of("myCircuitBreaker", config);
    }

    public void useService() {
        circuitBreaker.executeSupplier(this::callService);
    }

    private String callService() {
        // Логика вызова защищаемого сервиса
        return "Service Response";
    }
}

// Пример использования
StateTransitionCircuitBreaker stcb = new StateTransitionCircuitBreaker();
stcb.useService();

Взаимодействие с другими паттернами (например, Bulkhead, Timeout)

Взаимодействие с паттерном Bulkhead

Bulkhead — это паттерн, который изолирует элементы в системе таким образом, что если один из них выходит из строя, это не влечет за собой каскадный сбой всей системы. Это достигается путем разделения элементов системы на изолированные группы.

Комбинация Bulkhead и Circuit Breaker позволяет не только предотвратить распространение сбоев, но и обеспечить более точное управление и мониторинг каждой изолированной части системы. Например, если Circuit Breaker срабатывает в одном из Bulkheads, это не влияет на другие части системы, что повышает общую устойчивость и позволяет локализовать проблемы быстрее.

Используем библиотеку Resilience4j для реализации паттернов Circuit Breaker и Bulkhead:

import io.github.resilience4j.bulkhead.Bulkhead;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;

public class ServiceWithBulkheadAndCircuitBreaker {
    private final CircuitBreaker circuitBreaker;
    private final Bulkhead bulkhead;

    public ServiceWithBulkheadAndCircuitBreaker() {
        this.circuitBreaker = CircuitBreaker.ofDefaults("myCircuitBreaker");
        this.bulkhead = Bulkhead.ofDefaults("myBulkhead");
    }

    public String processRequest() {
        return Bulkhead.decorateSupplier(bulkhead, () ->
                CircuitBreaker.decorateSupplier(circuitBreaker, this::serviceCall)
        ).get();
    }

    private String serviceCall() {
        // Логика обработки запроса
        return "Service Response";
    }
}

Взаимодействие с паттерном Timeout

Timeout — это механизм, который предотвращает ожидание ответа от вызова в течение неопределенно долгого времени. Он ограничивает время ожидания ответа и, в случае его превышения, прерывает операцию, предотвращая зависание и потенциальные задержки в системе.

Circuit Breaker и Timeout дополняют друг друга. В то время как Timeout защищает от долгих задержек в ответах, Circuit Breaker защищает от повторных попыток доступа к уже недоступным сервисам. Использование Timeout вместе с Circuit Breaker позволяет избежать долгих периодов ожидания в случае недоступности сервисов, а также предотвращает ненужные нагрузки на сервис, который уже испытывает трудности.

Использование Hystrix для реализации Circuit Breaker с Timeout:

import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandProperties;

public class ServiceWithTimeout extends HystrixCommand<String> {
    protected ServiceWithTimeout() {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("MyServiceGroup"))
                    .andCommandPropertiesDefaults(
                        HystrixCommandProperties.Setter()
                            .withExecutionTimeoutInMilliseconds(1000)
                    ));
    }

    @Override
    protected String run() throws Exception {
        // Логика вызова сервиса
        return "Service Response";
    }

    @Override
    protected String getFallback() {
        return "Fallback Response";
    }
}

Дополнительные взаимодействия

  1. Retry Pattern: Этот паттерн предусматривает повторные попытки выполнения операции в случае ее сбоя. Когда используется в сочетании с Circuit Breaker, важно правильно сбалансировать логику повторных попыток, чтобы избежать излишней нагрузки на сервис.

  2. Rate Limiter: Паттерн ограничения скорости запросов помогает контролировать количество операций, отправляемых к сервису за определенный период времени. Это может предотвратить перегрузку сервисов и использоваться вместе с Circuit Breaker для обеспечения дополнительной стабильности.

  3. Fallback: Предусматривает альтернативные действия в случае сбоя основного сервиса. Это может быть простое возвращение статических данных, использование кеша или перенаправление запроса на другой сервис.

Конфигурация и настройка для разных сценариев

Определение порогов срабатывания

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

Для сервисов, где время отклика важно, порог срабатывания может быть установлен на основе максимально допустимого времени ответа.

Длительность ожидания в в открытом состоянии

Период, в течение которого Circuit Breaker остается в открытом состоянии, должен быть достаточным для того, чтобы проблемный сервис мог восстановиться. Слишком короткое время может привести к частым срабатываниям, а слишком долгое — к ненужному простою.

Настройка полуоткрытого состояния

В полуоткрытом состоянии Circuit Breaker должен позволять ограниченное количество запросов для проверки работоспособности сервиса. Количество этих запросов и условия их успешности должны быть тщательно настроены.

Адаптация к различным условиям эксплуатации

В условиях, когда нагрузка на систему меняется, полезно иметь возможность динамически изменять параметры Circuit Breaker. Это может быть реализовано через внешние конфигурационные файлы или через административный интерфейс.


Настройки Circuit Breaker должны быть протестированы в условиях, максимально приближенных к реальной эксплуатации, включая тестирование под нагрузкой и стресс-тестирование. Важно убедиться, что Circuit Breaker реагирует на сбои так, как ожидается, и что система корректно обрабатывает переходы между состояниями.

Мониторинг и управление состоянием

Важно отслеживать изменения состояний Circuit Breaker (закрыто, открыто, полуоткрыто). Для этого могут использоваться специализированные инструменты мониторинга или встроенные функции библиотек Circuit Breaker.

Важными метриками для отслеживания являются количество успешных и неуспешных запросов, время ответа, количество переходов в открытое состояние и другие.

Настройка системы алертов на основе определенных пороговых значений метрик позволяет оперативно реагировать на изменения в работе сервисов.

Логирование событий, связанных с работой Circuit Breaker, таких как переходы между состояниями и ошибки в запросах, обеспечивает важную информацию для анализа и диагностики проблем.

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

Некоторые реализации Circuit Breaker предоставляют API, которые позволяют управлять его состоянием, включая включение/выключение или изменение параметров.

Интеграция Circuit Breaker с системами оркестрации и автоматизации позволяет автоматически принимать меры при обнаружении проблем, например, масштабирование сервисов или перенаправление трафика.

Если говорить про тестирование, то применение принципов Chaos Engineering поможет в тестирование устойчивости системы путем искусственного индуцирования сбоев и наблюдения за реакцией Circuit Breaker.

Заключение

Circuit Breaker предотвращает каскадные сбои, отключая недоступные или нестабильные сервисы. Когда система сталкивается с ошибками в одном из сервисов, Circuit Breaker переключается в открытое состояние, предотвращая дальнейшие вызовы к этому сервису, что помогает избежать распространения проблемы на другие части системы.

Введение состояния "полуоткрытого" позволяет Circuit Breaker проводить контролируемые попытки восстановления сервиса. Это обеспечивает возможность для сервиса восстановиться, не подвергая его излишней нагрузке.

Circuit Breaker эффективно изолирует проблемы в одном сервисе, не позволяя им влиять на работу всей системы. Это обеспечивает устойчивость системы в целом даже в случае сбоев в отдельных ее частях.

В средах с высокой нагрузкой Circuit Breaker помогает регулировать поток запросов к сервисам. Это важно в микросервисных архитектурах, где отдельные компоненты могут испытывать различные уровни нагрузки.

Circuit Breaker позволяет системе адаптироваться к изменяющимся условиям, автоматически регулируя доступ к сервисам в зависимости от их текущего состояния и нагрузки.

Путем предотвращения ненужных вызовов к проблемным или перегруженным сервисам, Circuit Breaker способствует более эффективному распределению и использованию ресурсов в системе. Circuit Breaker позволяет системе реагировать на сбои динамически, автоматически адаптируясь к текущему состоянию сервисов и инфраструктуры.

Поскольку Circuit Breaker изолирует и выделяет проблемные сервисы, это облегчает быстрое определение и устранение проблем.

Circuit Breaker может быть интегрирован с системами мониторинга, предоставляя важную информацию о состоянии сервисов и системы в целом. При срабатывании Circuit Breaker может инициировать оповещения, позволяющие оперативно реагировать на возникшие проблемы.

На этом все. Хочу порекомендовать вам бесплатный вебинар курса Highload Architect, на котором эксперты отрасли расскажут про проектирование баз данных в highload проектах. Регистрируйтесь, будет интересно!

Комментарии (2)


  1. Kelbon
    06.12.2023 16:30
    -2

    Здравствуйте, так как это блог компании OTUS, я здесь это напишу.

    Прекратите мне писать, звонить, спамить на электронную почту и пытаться любыми другими способами коммуницировать со мной.

    Меня не интересуют ваши продукты, я никогда не куплю курсы и никогда не стану преподавателем на некачественных курсах.

    Если я по приколу прошёл "тест на знание С++" и получил в нём максимум, это не значит что мне нужен курс профессионального С++ разработчика, это значит, что мне он нахуй не нужен. Особенно того качества, что вы способны предоставить.

    Спасибо