30. Circuit breaker
В электрическом щитке любого дома установлены автоматические выключатели — предохранители (circuit breakers). Если на кухне закоротит проводку тостера, биметаллическая пластина в щитке мгновенно сработает с сухим щелчком, обесточив только одну проблемную линию. Это предотвратит перегрев кабелей в стенах и спасет весь дом от пожара. Никому и в голову не придет упорно включать рубильник обратно каждую миллисекунду, пока проводка плавится.
Майкл Нейгард в классической книге «Release It!» перенес эту физическую концепцию в архитектуру программного обеспечения. В распределенных системах любая внешняя зависимость (база данных, платежный шлюз, сервис рекомендаций, сторонний API) рано или поздно начнет сбоить или отвечать с чудовищной задержкой. Без автоматического выключателя входящие HTTP-горутины начинают накапливаться, блокируя пулы соединений и память процесса, пока весь ваш бэкенд не рухнет по принципу домино. Circuit Breaker реализует спасительный принцип fast-fail: мгновенно прерывает вызовы к умирающей зависимости, спасая ваш сервис и давая внешней системе время на восстановление.
Философия Circuit Breaker
В распределенных системах временный отказ внешней зависимости (БД, внешний API, кеш) не должен превращаться в полный крах вашего сервиса. Без защитных механизмов горутины накапливаются, пулы соединений исчерпываются, а латентность растет экспоненциально. Circuit Breaker (автоматический выключатель) решает эту проблему через принцип fast-fail: если зависимость стабильно отвечает ошибками, вызовы немедленно прерываются, давая инфраструктуре время на восстановление и предотвращая каскадный отказ.
В Go Circuit Breaker — это не внешний прокси или тяжелый AOP-интерцептор, а легковесная машина состояний, работающая в памяти процесса. Она использует атомарные счетчики, контексты отмены и неблокирующие переходы, что минимизирует накладные расходы на CPU и планировщик.
1. Машина состояний и переходы
Классический Circuit Breaker оперирует тремя фундаментальными состояниями:
- Closed (Замкнут): Нормальный режим работы. Электрическая цепь замкнута, запросы свободно проходят к целевой зависимости. Ошибки и задержки непрерывно учитываются статистическим скользящим окном.
- Open (Разомкнут): Защита сработала. Цепь разорвана. Все входящие запросы мгновенно прерываются локальной ошибкой без физического обращения к сети. Запускается таймер паузы восстановления.
- Half-Open (Полуразомкнут): Пробный канареечный режим. По истечении таймера выключатель осторожно пропускает ограниченное число запросов (
MaxRequests = 1..3). Если пробные запросы завершаются успехом, выключатель переходит обратно вClosed. При первой же ошибке — мгновенный возврат вOpen.
flowchart TD
A["Запрос к зависимости"] --> B["Проверка состояния"]
B -->|"Closed"| C["Вызов целевой функции"]
C -->|"Успех"| D["Сброс счетчика ошибок"]
C -->|"Ошибка"| E["Инкремент отказов"]
E --> F{"Порог превышен?"}
F -->|"Нет"| B
F -->|"Да"| G["Переход в Open"]
G --> H["Мгновенный возврат ошибки"]
H --> I{"Истек интервал восстановления?"}
I -->|"Да"| J["Переход в Half-Open"]
J --> K["Пробный запрос"]
K -->|"Успех"| D
K -->|"Ошибка"| G
classDef normal fill:#1e293b,stroke:#38bdf8,color:#f8fafc;
classDef decision fill:#334155,stroke:#f59e0b,color:#f8fafc;
classDef closed fill:#0f172a,stroke:#10b981,color:#f8fafc;
classDef open fill:#4c0519,stroke:#f43f5e,color:#ffffff;
class A,C,D,K normal;
class B,F,I decision;
class D closed;
class G,H open;
class J decision;
2. Идиоматичная интеграция в Go
Использование проверенной библиотеки github.com/sony/gobreaker предпочтительнее самописных решений, так как она корректно обрабатывает конкурентные переходы, учет скользящих окон и паники внутри вызываемого кода.
package client
import (
"context"
"fmt"
"net/http"
"time"
"github.com/sony/gobreaker"
)
type ExternalClient struct {
cb *gobreaker.CircuitBreaker
httpClient *http.Client
}
func NewExternalClient() *ExternalClient {
cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: "payment-api",
MaxRequests: 3, // Сколько запросов пропустить в Half-Open
Interval: 60 * time.Second, // Окно сбора статистики
Timeout: 10 * time.Second, // Время в Open перед переходом в Half-Open
ReadyToTrip: func(counts gobreaker.Counts) bool {
// Переход в Open при 50% ошибок (при выборке от 10 запросов)
failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
return counts.Requests >= 10 && failureRatio >= 0.5
},
})
return &ExternalClient{
cb: cb,
httpClient: &http.Client{Timeout: 5 * time.Second},
}
}
func (c *ExternalClient) Call(ctx context.Context, payload []byte) error {
// Execute оборачивает вызов и управляет состояниями
_, err := c.cb.Execute(func() (interface{}, error) {
// Контекст прокидывается внутрь для корректной отмены
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.example.com/pay", nil)
if err != nil {
return nil, err
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 500 {
return nil, fmt.Errorf("server error: %d", resp.StatusCode)
}
return nil, nil
})
if err == gobreaker.ErrOpenState {
return fmt.Errorf("circuit breaker is open: %w", err)
}
return err
}
3. Механика работы и Mechanical Sympathy
Circuit Breaker работает на границе сетевого вызова и бизнес-логики. Его производительность напрямую влияет на latency:
- Счетчики ошибок: Должны быть атомарными или шардированными. При 10 000 RPS обновление глобального счетчика через
sync.Mutexвызывает contention на кэш-линиях (cache line bouncing). Использованиеatomic.AddInt64решает проблему, но при экстремальных нагрузках применяют локальные счетчики на горутину с периодическим агрегированием. - Таймер восстановления: Переход
Open→Half-Openне должен использоватьtime.Sleepв отдельной горутине. Это расходует память стека и ресурс планировщика. Идиоматично проверятьtime.Since(lastStateChange)при входе вExecute()или использоватьsync.Timerс однократным срабатыванием. - Escape Analysis: Функция, передаваемая в
cb.Execute(), является замыканием. Компилятор часто вынужден размещать захваченные переменные (ctx,payload,c.httpClient) в куче. Для минимизации аллокаций передавайте данные через аргументы или используйтеsync.Poolдля структур запросов. - Влияние на GC: Каждый неудачный вызов создает
error, который сохраняется для статистики. При частых переходах вOpenи обратно возникает микро-волна аллокаций. НастройкаIntervalиMaxRequestsдолжна соответствовать профилю трафика.
4. Сравнение с другими экосистемами
| Аспект | Java (Resilience4j / Hystrix) | PHP (Envoy / Nginx Proxy) | Python (pybreaker) | Go (gobreaker / hystrix-go) |
|---|---|---|---|---|
| Модель выполнения | Thread-pool isolation, тяжелые прокси | Внешний sidecar, нулевой оверхед в коде | Синхронный, блокирующий | In-process, горутинный, неблокирующий |
| Управление состоянием | Атомарные счетчики + Volatile переменные | Shared memory / Redis в прокси | Локальный map + мьютекс | atomic + lock-free переходы |
| Изоляция сбоев | Отдельный пул тредов на зависимость | Нет (зависит от прокси) | Нет (блокирует процесс) | Отказ на уровне вызова, горутины не блокируются |
| Оверхед | Высокий (контекст тредов, GC pressure) | Нулевой (на уровне сети) | Низкий, но синхронный | Минимальный (атомарные операции, компиляция) |
5. Собеседование и типичные вопросы
6. Итог
- Circuit Breaker защищает от каскадных отказов через быстрое отключение неисправных зависимостей.
- Состояния
Closed→Open→Half-Openдолжны переключаться атомарно, избегая блокировок на hot-path. - Никогда не комбинируйте CB и Retry без координации: проверяйте состояние выключателя до начала цикла повторов.
- Ограничивайте
MaxRequestsвHalf-Openдо минимума (1-3), чтобы не перегрузить восстанавливающуюся систему. - Учитывайте латентность как отказ: засчитывайте таймауты контекста в счетчик ошибок выключателя.
- В Go CB реализуется через легковесные библиотеки (
gobreaker), использующие атомики и минимальные аллокации, что соответствует философии языка.
Следующая статья: 31. Таймауты и SLA