Паттерн Circuit Breaker в Go: предотвращение каскадных сбоев
Предотвращайте каскадные отказы в микросервисах на Go.
Предохранитель (circuit breaker) предотвращает перегрузку вашего Go-сервиса при обращении к неустойчивой зависимости, избегая каскадных сбоев, которые потребляют горутины, сокеты и память, пока вся система не рухнет.
Самая сложная задача — не машина состояний. Решение о том, где должен располагаться предохранитель, что считается ошибкой, как он взаимодействует с таймаутами и повторными попытками, и что должен делать ваш сервис, когда цепь разомкнута.

В Go паттерн предохранителя особенно полезен для исходящих вызовов: HTTP API, платежные шлюзы, поисковые сервисы, провайдеры электронной почты, шлюзы LLM, внутренние микросервисы и другие зависимости, которые могут стать медленными, перегруженными или частично недоступными. При правильном использовании предохранитель снижает каскадные сбои. При неправильном — становится еще одним неочевидным режимом отказа.
Какую проблему решает предохранитель?
Распределенные системы редко выходят из строя чисто.
Зависимость может быть не полностью выключена. Она может:
- возвращать ошибки 500
- возвращать ответы 429 о превышении лимита
- принимать TCP-соединения, но никогда не отвечать
- отвечать за 30 секунд вместо 300 миллисекунд
- терпеть неудачу только для некоторых запросов
- быть перегруженной, потому что все клиенты одновременно пытаются повторить запрос
Худший случай — это часто не жесткий сбой. Это медленная зависимость.
Медленные вызовы потребляют горутины, сокеты, соединения с базой данных, память и пропускную способность воркеров. Если ваш сервис продолжает ждать зависимость, которая уже находится в нездоровом состоянии, ваш сервис тоже может стать нездоровым.
Предохранитель предотвращает это, быстро завершая работу после того, как зависимость пересекает порог ошибок.
Вместо того чтобы делать это бесконечно:
запрос -> вызов зависимости -> ожидание -> таймаут -> повторная попытка -> ожидание -> сбой
сервис в конечном итоге делает следующее:
запрос -> цепь разомкнута -> возврат резервного варианта или ошибки немедленно
Этот быстрый сбой не всегда приятен, но он предсказуем. Предсказуемый сбой легче эксплуатировать, чем медленное разрушение.
Три состояния предохранителя
Большинство предохранителей используют три состояния.
Закрыто (Closed)
Цепь закрыта во время нормальной работы.
Запросы пропускаются. Предохранитель фиксирует успехи и неудачи. Если количество или соотношение неудач пересекает порог, предохранитель открывается.
Закрытое состояние не означает «безопасно навсегда». Это означает «трафик в настоящее время разрешен».
Разомкнуто (Open)
Цепь разомкнута, когда зависимость считается нездоровой.
Запросы отклоняются немедленно. Сервис должен вернуть резервный вариант, кэшированный ответ, деградированный ответ или четкую ошибку upstream.
Разомкнутое состояние не исправляет зависимость. Оно дает зависимости время на восстановление и защищает вызывающего от расходования ресурсов.
Полуразомкнуто (Half-Open)
После периода охлаждения предохранитель переходит в полуразомкнутое состояние.
Разрешается ограниченное количество пробных запросов. Если они успешны, предохранитель закрывается. Если они терпят неудачу, предохранитель снова открывается.
Полуразомкнутое состояние важно, потому что оно избегает двух плохих крайностей:
- никогда больше не пытаться обратиться к зависимости
- слишком быстро возвращать полный трафик
Переходы состояний выглядят следующим образом:
Предохранитель против Таймаута против Повторных попыток
Распространенная ошибка — считать предохранители, повторные попытки и таймауты взаимозаменяемыми. Они связаны, но решают разные проблемы.
Таймаут
Таймаут ограничивает время выполнения одной операции.
В Go это обычно означает передачу context.Context с дедлайном или таймаутом в исходящий вызов.
Таймаут отвечает на этот вопрос:
Как долго я готов ждать этого одного вызова?
Повторная попытка
Повторная попытка повторяет операцию, если сбой может быть временным.
Повторные попытки полезны для кратковременных сетевых сбоев, временных ответов 503, сбросов соединений и других транзитных ошибок.
Повторная попытка отвечает на этот вопрос:
Следует ли мне попробовать этот вызов снова?
Предохранитель
Предохранитель останавливает вызовы, когда зависимость, вероятно, нездорова.
Он отвечает на этот вопрос:
Следует ли мне вызывать эту зависимость прямо сейчас вообще?
Ограничитель скорости (Rate Limiter)
Ограничитель скорости контролирует объем трафика, разрешенного за единицу времени.
Он отвечает на этот вопрос:
Сколько трафика должен отправлять этот вызывающий?
Бакхед (Bulkhead)
Бакхед изолирует ресурсы, чтобы одна зависимость не могла потребить все.
Он отвечает на этот вопрос:
Насколько сильно эта зависимость может повредить моему сервису?
Эти паттерны наиболее эффективны при совместном использовании. Предохранитель без таймаутов слаб. Повторные попытки без джиттера могут создать шторм повторных попыток. Резервный вариант без метрик может скрыть сбой.
Когда использовать предохранитель в Go
Используйте предохранитель, когда ваш сервис обращается к зависимости, которая может выйти из строя независимо от вашего сервиса.
Хорошие кандидаты включают:
- внешние HTTP API
- платежные процессоры
- провайдеры электронной почты и SMS
- поисковые сервисы
- сервисы рекомендаций
- шлюзы инференса LLM
- конечные точки внутренних микросервисов
- сторонние API SaaS
- медленные или перегруженные сервисы чтения
Предохранители особенно полезны, когда вызывающая сторона может деградировать gracefully (безопасно).
Например:
- вернуть кэшированные данные о продукте
- пропустить блок рекомендаций
- пометить платежного провайдера как временно недоступный
- поставить работу в очередь на более позднее время
- вернуть частичный ответ
- быстро завершить работу с четкой временной ошибкой
Важный вопрос не в том «может ли этот вызов потерпеть неудачу?». Всё может потерпеть неудачу. Лучше спросить:
Если эта зависимость выходит из строя, следует ли нам продолжать отправлять на нее полный трафик?
Если ответ — нет, предохранитель может помочь.
Когда не следует использовать предохранитель
Не добавляйте предохранитель в каждую функцию только потому, что паттерн звучит ответственно.
Предохранитель обычно не полезен для:
- локальных вызовов функций внутри процесса
- простых CRUD-операций внутри монолита
- логики валидации
- детерминированных бизнес-правил
- локальных операций только на CPU
- путей кода, где нет полезного резервного варианта
- операций записи, которые не являются идемпотентными
- зависимостей, уже защищенных более сильным слоем рабочего процесса
Предохранитель также не заменяет базовую гигиену:
- устанавливать таймауты
- передавать контекст
- правильно использовать пулы соединений
- явно обрабатывать ошибки
- делать повторные попытки безопасными
- отслеживать частоту сбоев
Плохой предохранитель может сделать систему сложнее для понимания. Он может скрыть реальную проблему, слишком агрессивно отклонять трафик или создавать запутанное поведение во время восстановления.
Правило с небольшой долей мнения простое:
Добавляйте предохранители на границах зависимостей, а не везде.
Выбор библиотеки предохранителя для Go
Вы можете реализовать базовый предохранитель самостоятельно, но большинство производственных Go-сервисов должны использовать библиотеку.
Самым распространенным простым выбором является sony/gobreaker.
Он предоставляет:
- состояния закрыто, разомкнуто и полуразомкнуто
- настраиваемые пороги ошибок
- настраиваемый таймаут состояния разомкнуто
- обратные вызовы при изменении состояния
- счетчики запросов
- поддержку generics в v2
- небольшой интерфейс API
Для более крупных конвейеров отказоустойчивости вы также можете рассмотреть библиотеки, которые объединяют несколько политик, такие как повторные попытки, таймауты, резервные варианты, ограничение скорости, изоляция бакхедом и предохранители. Это может быть полезно, когда вы хотите единый слой отказоустойчивости вокруг операции.
Однако для многих Go-сервисов gobreaker достаточно.
Сравнение пакетов предохранителей для Go
В стандартной библиотеке Go нет встроенного предохранителя. На практике вы обычно выбираете между маленькой библиотекой предохранителя, более крупной платформой отказоустойчивости или более старой пакетом в стиле Hystrix.
Для большинства новых Go-сервисов решение простое:
- используйте
sony/gobreaker, если вам нужен маленький, сфокусированный предохранитель - используйте
failsafe-go, если вы хотите объединить предохранители с повторными попытками, таймаутами, резервными вариантами, бакхедами, ограничением скорости и другими политиками отказоустойчивости - избегайте начала новых проектов на
hystrix-go, если у вас нет устаревшего кода, использующего его
| Пакет | Лучше всего для | Сильные стороны | Компромиссы |
|---|---|---|---|
sony/gobreaker/v2 |
Простые предохранители вокруг HTTP/RPC клиентов | Маленький API, поддержка generics v2, четкая модель состояния, легко обернуть клиентов зависимостей | Решает только предохранение; повторные попытки, таймауты и резервные варианты должны быть объединены отдельно |
failsafe-go |
Полное объединение политик отказоустойчивости | Повторные попытки, резервные варианты, предохранитель, таймаут, бакхед, ограничитель скорости, кэш, хедж, адаптивный ограничитель и адаптивный дроссель | Больше концепций для изучения; тяжелее, чем необходимо, если вам нужен только базовый предохранитель |
afex/hystrix-go |
Устаревшие системы в стиле Hystrix | Знакомые концепции Hystrix, выполнение в стиле команд, историческое использование | Устаревший дизайн; не лучший выбор по умолчанию для новых Go-сервисов |
go-kit/kit/circuitbreaker |
Сервисы на базе конечных точек Go kit | Подходит для стиля middleware Go kit и архитектуры конечных точек | Полезен в основном, если ваш сервис уже использует Go kit |
cep21/circuit |
Поведение предохранителя, похожее на Hystrix | Более функциональный подход в стиле Hystrix | Менее распространен как простой вариант по умолчанию; может быть избыточным для небольших сервисов |
Моя рекомендация по умолчанию намеренно скучна: начните с sony/gobreaker/v2, когда вам нужен только предохранитель. Обратитесь к failsafe-go, когда вы хотите выразить полную политику отказоустойчивости в одном месте.
Это разделение сохраняет архитектуру чистой. Маленькому клиенту сервиса не нужна полная платформа отказоустойчивости, просто чтобы перестать вызывать зависящую зависимость. Но шлюз, агрегатор, SDK клиента API или слой интеграции с высоким трафиком могут выиграть от объединенных политик.
Установка gobreaker
Используйте пакет v2 для нового кода:
go get github.com/sony/gobreaker/v2
Затем импортируйте его:
import "github.com/sony/gobreaker/v2"
Базовый предохранитель в Go
Вот небольшой пример вокруг HTTP-вызова.
package main
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"time"
"github.com/sony/gobreaker/v2"
)
var ErrTemporaryUnavailable = errors.New("dependency temporarily unavailable")
type UserClient struct {
baseURL string
http *http.Client
cb *gobreaker.CircuitBreaker[[]byte]
}
func NewUserClient(baseURL string) *UserClient {
settings := gobreaker.Settings{
Name: "user-service",
MaxRequests: 3,
Interval: 30 * time.Second,
Timeout: 10 * time.Second,
ReadyToTrip: func(counts gobreaker.Counts) bool {
return counts.ConsecutiveFailures >= 5
},
OnStateChange: func(name string, from gobreaker.State, to gobreaker.State) {
fmt.Printf("circuit breaker %s changed from %s to %s\n", name, from, to)
},
}
return &UserClient{
baseURL: baseURL,
http: &http.Client{
Timeout: 3 * time.Second,
},
cb: gobreaker.NewCircuitBreaker[[]byte](settings),
}
}
func (c *UserClient) GetUser(ctx context.Context, userID string) ([]byte, error) {
result, err := c.cb.Execute(func() ([]byte, error) {
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
c.baseURL+"/users/"+userID,
nil,
)
if err != nil {
return nil, err
}
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 500 {
return nil, fmt.Errorf("user service returned %d", resp.StatusCode)
}
if resp.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("user not found")
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("user service client error: %d", resp.StatusCode)
}
return io.ReadAll(resp.Body)
})
if errors.Is(err, gobreaker.ErrOpenState) {
return nil, ErrTemporaryUnavailable
}
if errors.Is(err, gobreaker.ErrTooManyRequests) {
return nil, ErrTemporaryUnavailable
}
return result, err
}
Это не полный клиент для производства, но он показывает структуру:
- предохранитель оборачивает исходящий вызов
- HTTP-запрос получает контекст
- HTTP-клиент имеет таймаут
- сбои на стороне сервера считаются сбоями предохранителя
- ошибки разомкнутой цепи отображаются в ошибку приложения
Настройка параметров gobreaker
Ключевые параметры стоит понять.
Name
Name идентифицирует предохранитель.
Используйте стабильное, конкретное имя:
payment-api
search-service
llm-gateway
user-service
Избегайте расплывчатых имен, таких как:
http-client
external-call
default
Вам понадобится это имя в журналах и метриках.
MaxRequests
MaxRequests контролирует, сколько запросов разрешено, когда предохранитель полуразомкнут.
Маленькое число обычно безопаснее. Цель полуразомкнутого состояния — проверить восстановление, а не сразу отправлять полный трафик.
Interval
Interval контролирует, когда внутренние счетчики очищаются, когда предохранитель закрыт.
Если он равен нулю, счетчики не очищаются автоматически. Ненулевой интервал дает предохранителю окно памяти, похожее на скользящее, хотя это не то же самое, что полная реализация скользящего окна.
Timeout
Timeout контролирует, как долго предохранитель остается разомкнутым перед переходом в полуразомкнутое состояние.
Если таймаут слишком короткий, ваш сервис будет продолжать проверять зависимость, которая не восстановилась. Если он слишком длинный, восстановление будет отложено.
Начните с чего-то консервативного, например, от 10 до 30 секунд, а затем настройте на основе метрик производства.
ReadyToTrip
ReadyToTrip решает, когда предохранитель должен открыться.
Простое правило — последовательные неудачи:
ReadyToTrip: func(counts gobreaker.Counts) bool {
return counts.ConsecutiveFailures >= 5
}
Это легко понять, но это может не подходить для сервисов с высоким объемом.
Другой вариант — соотношение ошибок после минимального числа запросов:
ReadyToTrip: func(counts gobreaker.Counts) bool {
total := counts.Requests
failures := counts.TotalFailures
if total < 20 {
return false
}
return float64(failures)/float64(total) >= 0.5
}
Это избегает открытия цепи после крошечного размера выборки.
OnStateChange
OnStateChange — это место, где вы должны генерировать журналы или метрики.
Как минимум, записывайте:
- имя предохранителя
- старое состояние
- новое состояние
- временную метку
Для производственных систем экспонируйте состояние предохранителя как метрику. Журналы полезны для отладки, но метрики лучше для оповещений и дашбордов.
IsSuccessful
IsSuccessful позволяет вам решить, какие ошибки считаются неудачами.
Это важно.
Не каждая ошибка должна открывать предохранитель. Например, 404 Not Found от пользовательского сервиса может быть валидным бизнес-результатом. 400 Bad Request может быть ошибкой вызывающего, а не зависимости.
503 Service Unavailable, таймаут, сброс соединения или 429 Too Many Requests могут быть реальным сигналом здоровья зависимости.
Будьте осторожны здесь. Подсчет неправильных ошибок — один из самых простых способов создать шумный предохранитель.
Что должно считаться ошибкой?
Здесь важно инженерное суждение.
Обычно считайте эти ошибки неудачами:
- сетевые таймауты
- соединение отклонено
- соединение сброшено
- HTTP 500
- HTTP 502
- HTTP 503
- HTTP 504
- повторяющиеся ответы 429
- некорректные ответы от зависимости
- превышение дедлайна контекста во время исходящего вызова
Обычно не считайте эти ошибки сбоями зависимости:
- ошибки валидации
- локальные ошибки сериализации
- ожидаемые ответы 404
- сбои авторизации на стороне вызывающего
- отклонения бизнес-правил
- ошибки пользовательского ввода
Предохранитель должен представлять здоровье зависимости, а не общий сбой приложения.
Предохранители и context.Context
В Go предохранители не должны заменять context.Context.
Предохранитель решает, следует ли пытаться выполнить вызов. Контекст контролирует, как долго этот вызов может выполняться и должен ли он остановиться, когда вызывающий исчез.
Хороший исходящий вызов обычно должен иметь оба:
ctx, cancel := context.WithTimeout(parentCtx, 2*time.Second)
defer cancel()
data, err := client.GetUser(ctx, userID)
Контекст должен проходить через цепочку вызовов:
входящий контекст запроса
-> метод сервиса
-> метод клиента
-> HTTP-запрос
-> зависимость
Избегайте создания отсоединенных фоновых контекстов внутри кода, ограниченного областью запроса. Если пользовательский запрос отменен, работа downstream обычно также должна остановиться.
Спокойное правило:
Предохранитель защищает систему. Контекст защищает запрос.
Обычно вам нужны оба.
Предохранители и повторные попытки
Повторные попытки и предохранители могут хорошо работать вместе, но порядок имеет значение.
Самый безопасный вариант по умолчанию:
таймаут на попытку
повторная попытка с отсрочкой и джиттером
предохранитель вокруг вызова зависимости
Но универсального ответа нет. Подумайте, что вы хотите учитывать.
Если каждая попытка повторения проходит через предохранитель, один пользовательский запрос может внести несколько неудач. Это может быстрее открыть предохранитель, что может быть хорошо или плохо.
Если предохранитель оборачивает всю операцию повторной попытки, предохранитель видит один финальный успех или неудачу на пользовательский запрос. Это спокойнее, но это может скрыть количество неудачных попыток.
Для многих сервисов приложений эта структура разумна:
пользовательский запрос
-> предохранитель
-> политика повторных попыток
-> один HTTP-запрос с таймаутом
Это означает, что предохранитель отслеживает, сработала ли операция зависимости в конечном итоге для вызывающего.
Для клиентов более низкого уровня эта структура также может иметь смысл:
пользовательский запрос
-> политика повторных попыток
-> предохранитель
-> один HTTP-запрос с таймаутом
Это означает, что предохранитель защищает каждую попытку.
Более важное правило следующее:
Не повторяйте попытки слепо.
Используйте:
- небольшое максимальное количество повторных попыток
- экспоненциальную отсрочку
- джиттер
- таймауты на попытку
- общий дедлайн запроса
- идемпотентность для записей
- метрики для попыток повторения
Без этого повторные попытки могут превратить небольшой сбой в больший. Для более глубокого рассмотрения безопасности повторных попыток см. Идемпотентность в распределенных системах, которая действительно работает.
Предохранители и идемпотентность
Предохранители часто появляются рядом с повторными попытками, и повторные попытки поднимают вопрос об идемпотентности.
Для операций чтения повторные попытки обычно безопасны.
Для операций записи повторные попытки могут быть опасны.
Рассмотрите этот вызов платежа:
POST /charge
Если запрос истекает, платеж потерпел неудачу? Возможно. Успешно ли он прошел, но ответ был потерян? Также возможно.
Если вы повторите попытку без ключа идемпотентности, вы можете зарядить дважды.
Для операций записи используйте одно или несколько из следующих:
- ключи идемпотентности
- идентификаторы запросов
- идентификаторы операций
- уникальные ограничения
- транзакционный outbox
- оркестрацию рабочих процессов
- явную согласование
Предохранитель может остановить вас от продолжения вызова вышедшего из строя платежного провайдера, но он не может сделать небезопасные повторные попытки безопасными.
Предохранители и резервные варианты
Когда цепь разомкнута, вашему сервису нужен план.
Возможные стратегии резервных вариантов включают:
- возврат кэшированных данных
- возврат устаревших данных с предупреждением
- пропуск некритичного раздела
- постановка работы в очередь на более позднее время
- переключение на другого провайдера
- возврат временной ошибки
- показ деградированного функционала
- быстрый отказ от запроса
Резервный вариант должен быть честным.
Например, это обычно хорошо:
{
"status": "temporary_unavailable",
"message": "Recommendations are temporarily unavailable"
}
Это рискованно:
{
"recommendations": []
}
Пустой список может выглядеть как валидный результат. Он может скрыть сбой, запутать пользователей и усложнить отладку.
Тихие резервные варианты соблазнительны. Они также опасны.
Предохранители и наблюдаемость
Предохранитель без наблюдаемости в основном генератор сюрпризов.
Отслеживайте как минимум эти метрики:
- текущее состояние предохранителя
- изменения состояния
- разрешенные вызовы
- отклоненные вызовы
- успехи
- неудачи
- таймауты
- ответы резервных вариантов
- попытки повторения
- задержка downstream
- коды состояния downstream
Полезные метки включают:
- имя предохранителя
- имя зависимости
- имя операции
- класс статуса
- категория ошибки
Избегайте меток с высокой кардинальностью, таких как идентификатор пользователя, полный URL, идентификатор запроса или необработанные сообщения об ошибках.
Вы должны иметь возможность ответить на эти вопросы из дашбордов:
- Какие предохранители сейчас разомкнуты?
- Как часто они открываются?
- Какая зависимость вызвала открытие?
- Видят ли пользователи ответы резервных вариантов?
- Улучшилась ли задержка после открытия предохранителя?
- Вырос ли объем повторных попыток перед открытием предохранителя?
- Восстановилась ли зависимость?
Если вы не можете наблюдать за предохранителем, вы не можете его настроить. Для структурированного логирования, которое хорошо сочетается с метриками, см. Структурированное логирование в Go с slog.
Более дружественный к производству вид HTTP-клиента
Для реальных сервисов избегайте рассеивания логики предохранителя по обработчикам.
Создайте небольшой пакет клиента вокруг зависимости.
Пример структуры:
internal/
userservice/
client.go
errors.go
metrics.go
Обработчик не должен знать деталей gobreaker. Он должен зависеть от метода клиента уровня домена:
type UserService interface {
GetUser(ctx context.Context, userID string) (*User, error)
}
Затем реализация может содержать:
- создание HTTP-запроса
- передачу контекста
- выполнение предохранителя
- обработку кодов состояния
- декодирование ответа
- метрики
- отображение ошибок
Это сохраняет политику отказоустойчивости близко к границе зависимости. Для большего о классификации ошибок на границах см. Архитектура обработки ошибок в Go: границы и паттерны.
Где предохранители вписываются в архитектуру приложения
Паттерн предохранителя принадлежит границам интеграции.
В приложении Go это обычно означает:
По возможности держите предохранитель вне бизнес-логики.
Бизнес-слой должен понимать доменные ошибки, такие как:
payment provider unavailable
recommendations unavailable
profile service timeout
Ему не нужно понимать состояния gobreaker.
Это разделение сохраняет архитектуру чистой:
- транспортные проблемы остаются в клиентах
- политика отказоустойчивости остается рядом с зависимостями
- доменная логика остается читаемой
- обработчики остаются тонкими
- тесты легче писать
Эта статья является частью темы Архитектура приложений в производстве — наряду с руководствами по идемпотентности, outbox, saga и оркестрации в паттернах интеграции.
Распространенные ошибки
Ошибка 1: Отсутствие таймаута
Предохранитель не магическим образом останавливает медленные вызовы, если вызовы возвращаются.
Если исходящая операция может зависнуть навсегда, предохранитель может не увидеть сбой достаточно быстро.
Всегда используйте таймауты.
Ошибка 2: Один глобальный предохранитель для всего
Не используйте один предохранитель для всех зависимостей.
Выход из строя провайдера электронной почты не должен открывать цепь для вашего платежного провайдера. Медленный конечный пункт поиска не должен блокировать вызовы профиля пользователя.
Используйте отдельные предохранители для отдельных операций зависимостей, когда их режимы отказа различаются.
Ошибка 3: Подсчет ошибок вызывающего как сбоев зависимости
Если ваш сервис отправляет плохой ввод и получает 400 Bad Request, это обычно не сбой downstream.
Не обучайте предохранитель на собственных ошибках.
Ошибка 4: Повторные попытки неидемпотентных записей
Повторные попытки не бесплатны. Они могут дублировать записи, платежи, сообщения или побочные эффекты.
Делайте записи идемпотентными перед их повторной попыткой.
Ошибка 5: Скрытие сбоев за резервными вариантами
Резервные варианты должны деградировать безопасно, а не фальсифицировать реальность.
Если зависимость выключена, ваши метрики и журналы должны делать это очевидным.
Ошибка 6: Настройка без данных производства
Пороги, скопированные из примеров, — это только отправные точки.
Настройте на основе:
- объема запросов
- нормальной частоты ошибок
- задержки зависимости
- влияния на пользователя
- времени восстановления
- качества резервных вариантов
Ошибка 7: Использование предохранителей вместо управления мощностью
Предохранитель не является заменой для:
- сброса нагрузки
- ограничения скорости
- лимитов очереди
- автоскейлинга
- настройки базы данных
- лимитов пула соединений
- квот upstream
Это одна часть стратегии отказоустойчивости.
Практические значения по умолчанию
Для типичного Go-сервиса, обращающегося к внутренней HTTP-зависимости, разумной отправной точкой может быть:
Таймаут HTTP-клиента: 2 до 5 секунд
Таймаут контекста на запрос: на основе SLA вызывающего
Правило сбоя предохранителя: 5 последовательных неудач или 50 процентов ошибок после 20 запросов
Таймаут разомкнутого состояния: 10 до 30 секунд
Запросы полуразомкнутого состояния: 1 до 5
Количество повторных попыток: 1 до 3 попыток
Отсрочка повторных попыток: экспоненциальная с джиттером
Это не универсальные значения. Это безопасные отправные точки.
Для пользовательских API сохраняйте общие бюджеты задержки тугими. Для фоновых задач вы можете терпеть более длинные ожидания. Для платежных провайдеров будьте гораздо осторожнее с повторными попытками и идемпотентностью.
Чек-лист предохранителя
Прежде чем добавить предохранитель, ответьте на эти вопросы:
- Какая зависимость защищается?
- Какая операция защищается?
- Какие ошибки считаются сбоем зависимости?
- Какие ошибки должны игнорироваться предохранителем?
- Какой таймаут применяется к каждому вызову?
- Разрешены ли повторные попытки?
- Являются ли записи идемпотентными?
- Что происходит, когда цепь разомкнута?
- Есть ли резервный вариант?
- Виден ли резервный вариант в метриках?
- Кто получает оповещение, если цепь продолжает открываться?
- Как предохранитель будет настроен после развертывания?
Если вы не можете ответить на эти вопросы, добавление предохранителя может создать больше путаницы, чем отказоустойчивости.
Тестирование предохранителей в Go
Тестируйте поведение, а не внутреннюю машину состояний библиотеки.
Полезные тесты включают:
- зависимость успешна, и ответ возвращен
- зависимость терпит неудачу многократно, и цепь открывается
- разомкнутая цепь возвращает временную ошибку
- ошибки валидации на стороне клиента не срабатывают предохранитель
- таймаут контекста соблюдается
- ответ резервного варианта возвращается, когда ожидается
- метрики генерируются при изменениях состояния
Используйте фейковые HTTP-серверы для тестов интеграционного стиля:
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "unavailable", http.StatusServiceUnavailable)
}))
defer server.Close()
Для модульных тестов скрывайте зависимость за интерфейсом и внедряйте фейковую реализацию.
Держите тесты детерминированными. Избегайте сна на длительные реальные периоды. Настройте короткие таймауты предохранителя в тестах. Для большего о тестировании параллельного кода Go с фейковым временем и изолированными пузырями см. Тестирование параллельного кода Go с testing/synctest.
Следует ли создавать собственный предохранитель?
Создание маленького предохранителя — хорошее упражнение для обучения. Это помогает вам понять машину состояний.
Для производственного кода предпочитайте поддерживаемую библиотеку, если ваши потребности не очень специфичны.
Производственный предохранитель должен обрабатывать:
- параллелизм
- переходы состояний
- счетчики
- пробные запросы полуразомкнутого состояния
- обратные вызовы
- пользовательскую классификацию сбоев
- поведение без гонок
- предсказуемую обработку ошибок
Это не невозможно, но легко сделать тонко неправильно.
Скучная библиотека обычно является лучшим выбором.
Заключение
Паттерн предохранителя — не магическая пыль надежности.
В Go он работает лучше всего, когда является частью небольшого, явного стека отказоустойчивости:
таймаут контекста
+ повторная попытка с отсрочкой и джиттером
+ предохранитель
+ резервный вариант
+ метрики
Паттерн наиболее полезен на границах зависимостей, особенно вокруг удаленных сервисов, которые могут стать медленными или частично недоступными.
Используйте его, чтобы остановить каскадные сбои. Используйте его, чтобы быстро завершить работу, когда зависимость явно нездорова. Используйте его, чтобы дать перегруженным системам возможность восстановиться.
Но не используйте его как оправдание игнорировать таймауты, идемпотентность, наблюдаемость или чистую архитектуру.
Хороший предохранитель делает сбой яснее и дешевле. Плохой просто делает сбой более загадочным.
Ссылки
- Go Microservices for AI/ML Orchestration — более широкий контекст оркестрации, где вписываются предохранители
- Паттерн Saga в распределенных транзакциях — паттерны распределенных транзакций, которые сочетаются с предохранителями
- Идемпотентность в распределенных системах — безопасность повторных попыток и идемпотентные операции
- Паттерн транзакционного outbox в Go — надежная доставка событий наряду с паттернами отказоустойчивости
- Архитектура обработки ошибок в Go — классификация ошибок на границах зависимостей
- Тестирование параллельного кода Go с synctest — тестирование асинхронного поведения с предохранителями
- Структурированное логирование в Go с slog — наблюдаемость наряду с предохранителями
github.com/sony/gobreaker/v2— официальный пакет gobreaker v2- Отмена контекста и таймауты в Go — паттерны контекста, которые сочетаются с предохранителями