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

В Go паттерн тайлбрейкера особенно полезен при совершении исходящих вызовов: HTTP API, платежных шлюзах, поисковых сервисах, провайдерах электронной почты, шлюзах LLM, внутренних микросервисах и других зависимостях, которые могут стать медленными, перегруженными или частично недоступными. При грамотном использовании тайлбрейкер снижает риск каскадных сбоев. При неправильном использовании он становится еще одной загадочной точкой отказа системы.
Какую проблему решает Тайлбрейкер?
Распределенные системы редко отказывают «чистым» образом.
Зависимость может быть не полностью недоступна. Она может:
- возвращать ошибки 500
- возвращать ответы 429 (превышен лимит запросов)
- принимать TCP-соединения, но никогда не отвечать
- отвечать за 30 секунд вместо 300 миллисекунд
- отказывать только в некоторых запросах
- быть перегруженной из-за того, что все клиенты одновременно повторяют запросы
Худший сценарий — это часто не жесткий отказ, а медленная зависимость.
Медленные вызовы потребляют горутины, сокеты, соединения с базами данных, память и ресурсы воркеров. Если ваш сервис продолжает ждать от зависимой системы, которая уже нездорова, ваш сервис тоже может стать нездоровым.
Тайлбрейкер предотвращает это, быстро отказывая в обслуживании, когда зависимость пересекает порог ошибок.
Вместо бесконечного выполнения этой цепочки:
запрос -> вызов зависимости -> ожидание -> таймаут -> повтор -> ожидание -> отказ
сервис в итоге выполняет эту:
запрос -> цепь разорвана -> немедленно вернуть резервный ответ или ошибку
Этот быстрый отказ не всегда приятен, но он предсказуем. Предсказуемый отказ легче обслуживать, чем медленное угасание системы.
Три состояния Тайлбрейкера
Большинство тайлбрейкеров используют три состояния.
Closed (Закрыто)
Цепь закрыта во время нормальной работы.
Запросы проходят через тайлбрейкер. Он записывает успешные и неудачные попытки. Если количество или доля ошибок превышают пороговое значение, тайлбрейкер переходит в состояние Open.
Состояние Closed не означает «навсегда безопасно». Оно означает «трафик в данный момент разрешен».
Open (Открыто)
Цепь открыта, когда зависимость считается нездоровой.
Запросы немедленно отклоняются. Сервис должен вернуть резервный ответ, закэшированный ответ, деградированный ответ или четкую ошибку вышестоящего уровня.
Состояние Open не исправляет зависимость. Оно дает зависимости время на восстановление и защищает вызывающую сторону от浪费 ресурсов.
Half-Open (Полуоткрыто)
После периода остывания тайлбрейкер переходит в состояние Half-Open.
Через него пропускается ограниченное количество пробных запросов. Если они успешны, цепь закрывается. Если они завершаются неудачей, цепь снова открывается.
Состояние Half-Open важно, потому что оно избегает двух крайностей:
- никогда больше не пытаться обращаться к зависимости
- слишком быстро возвращать весь трафик
Переходы между состояниями выглядят так:
Тайлбрейкер против Таймаута против Повторных попыток (Retry)
Частая ошибка — рассматривать тайлбрейкеры, повторные попытки и таймауты как взаимозаменяемые. Они связаны, но решают разные проблемы.
Таймаут (Timeout)
Таймаут ограничивает время, которое одна операция может выполняться.
В Go это обычно означает передачу context.Context с дедлайном или таймаутом в исходящий вызов.
Таймаут отвечает на этот вопрос:
Как долго я готов ждать этот один вызов?
Повторная попытка (Retry)
Повторная попытка повторяет операцию, если сбой может быть временным.
Повторные попытки полезны при кратковременных сетевых сбоях, временных ответах 503, сбоях соединений и других переходных отказах.
Повторная попытка отвечает на этот вопрос:
Стоит ли мне попробовать вызвать это еще раз?
Тайлбрейкер (Circuit Breaker)
Тайлбрейкер останавливает вызовы, когда зависимость, вероятно, нездорова.
Он отвечает на этот вопрос:
Стоит ли мне вообще вызывать эту зависимость прямо сейчас?
Лимитер скорости (Rate Limiter)
Лимитер скорости контролирует объем трафика, разрешенный за определенное время.
Он отвечает на этот вопрос:
Сколько трафика этот вызывающий должен отправлять?
Мембрана (Bulkhead)
Мембрана изолирует ресурсы, чтобы одна зависимость не могла消耗ировать все.
Он отвечает на этот вопрос:
Насколько сильно эта зависимость может повредить моему сервису?
Эти паттерны наиболее эффективны при совместном использовании. Тайлбрейкер без таймаутов слаб. Повторные попытки без джиттера (jitter) могут вызвать шторм повторных попыток. Резервный ответ без метрик может скрыть аварию.
Когда использовать Тайлбрейкер в Go
Используйте тайлбрейкер, когда ваш сервис вызывает зависимость, которая может отказывать независимо от вашего сервиса.
Хорошие кандидаты включают:
- внешние HTTP API
- платежные процессоры
- провайдеры электронной почты и SMS
- поисковые сервисы
- рекомендательные сервисы
- шлюзы вывода LLM
- внутренние конечные точки микросервисов
- сторонние SaaS API
- медленные или перегруженные сервисы чтения данных
Тайлбрейкеры особенно полезны, когда вызывающая сторона может gracefully деградировать.
Например:
- вернуть закэшированные данные о продукте
- пропустить блок рекомендаций
- пометить поставщика платежей как временно недоступного
- поставить работу в очередь на позже, используя очередь мертвых сообщений в качестве страховки для сообщений, которые никогда не будут обработаны успешно даже после восстановления зависимости
- вернуть частичный ответ
- быстро завершиться с четкой временной ошибкой
Важный вопрос не в том: «Может ли этот вызов завершиться неудачей?». Все может завершиться неудачей. Более правильный вопрос:
Если эта зависимость отказывает, должны ли мы продолжать отправлять на нее полный трафик?
Если ответ «нет», тайлбрейкер может помочь.
Когда не следует использовать Тайлбрейкер
Не добавляйте тайлбрейкер к каждой функции только потому, что этот паттерн звучит ответственно.
Тайлбрейкер обычно не полезен для:
- локальных вызовов функций в процессе
- простых CRUD-операций внутри монолита
- логики валидации
- детерминированных бизнес-правил
- локальных операций, использующих только CPU
- путей кода, где не существует полезного резервного ответа
- операций записи, которые не являются идемпотентными
- зависимостей, уже защищенных более мощным уровнем рабочих процессов
Тайлбрейкер также не заменяет базовую гигиену:
- устанавливайте таймауты
- передавайте контекст
- корректно используйте пулы соединений
- явно обрабатывайте ошибки
- делайте повторные попытки безопасными
- отслеживайте частоту отказов
Плохой тайлбрейкер может сделать систему сложнее для понимания. Он может скрыть реальную проблему, слишком агрессивно отклонять трафик или создавать запутанное поведение во время восстановления.
Слегка категоричное правило просто:
Добавляйте тайлбрейкеры на границах зависимостей, а не везде.
Выбор библиотеки Тайлбрейкера для Go
Вы можете реализовать базовый тайлбрейкер самостоятельно, но большинство производственных Go-сервисов должны использовать библиотеку.
Самым распространенным простым выбором является sony/gobreaker.
Она предоставляет:
- состояния закрыто, открыто и полуоткрыто
- настраиваемые пороги ошибок
- настраиваемый тайм-аут состояния открытой цепи
- обратные вызовы при изменении состояния
- счетчики запросов
- общую поддержку в версии 2
- небольшой интерфейс API
Для более сложных конвейеров устойчивости вы также можете рассмотреть библиотеки, которые комбинируют несколько политик, таких как повторные попытки, таймауты, резервные ответы, лимитирование скорости, изоляция мембраны и разрыв цепи. Это может быть полезно, когда вы хотите единый слой устойчивости вокруг операции.
Однако для многих Go-сервисов gobreaker достаточно.
Сравнение пакетов Тайлбрейкера для Go
Go не включает встроенный тайлбрейкер в стандартную библиотеку. На практике вы обычно выбираете между небольшой библиотекой тайлбрейкера, крупной рамкой устойчивости или старым пакетом в стиле Hystrix.
Для большинства новых Go-сервисов решение просто:
- используйте
sony/gobreaker, если вам нужна небольшая, сфокусированная библиотека тайлбрейкера - используйте
failsafe-go, если вы хотите комбинировать тайлбрейкеры с повторными попытками, таймаутами, резервными ответами, мембранами, лимитами скорости и другими политиками устойчивости - избегайте начала новых проектов на базе
hystrix-go, если у вас уже нет унаследованного кода, использующего его
| Пакет | Для чего лучше всего | Преимущества | Компромиссы |
|---|---|---|---|
sony/gobreaker/v2 |
Простые тайлбрейкеры вокруг HTTP/RPC клиентов | Малый интерфейс API, поддержка дженериков в 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 контролирует, сколько запросов разрешено, пока тайлбрейкер находится в состоянии Half-Open.
Меньшее число обычно безопаснее. Цель состояния Half-Open — проверить восстановление, а не немедленно отправить полный трафик.
Interval (Интервал)
Interval контролирует, когда внутренние счетчики очищаются, пока тайлбрейкер находится в состоянии Closed.
Если он равен нулю, счетчики не очищаются автоматически. Ненулевой интервал дает тайлбрейкеру скользящее-ish окно памяти, хотя это не то же самое, что полная реализация скользящего окна.
Timeout (Таймаут)
Timeout контролирует, как долго тайлбрейкер остается в состоянии Open перед переходом в Half-Open.
Если таймаут слишком короткий, ваш сервис будет постоянно зондировать зависимость, которая не восстановилась. Если он слишком длинный, восстановление будет задержано.
Начните с консервативного значения, такого как 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-запрос
-> зависимость
Избегайте создания оторванных фоновых контекстов внутри кода, ограниченного запросом. Если пользовательский запрос отменен, фоновая работа обычно тоже должна остановиться.
Спокойное правило:
Тайлбрейкер защищает систему. Контекст защищает запрос.
Обычно вам нужны оба.
Тайлбрейкеры и Повторные попытки (Retries)
Повторные попытки и тайлбрейкеры могут хорошо работать вместе, но порядок имеет значение.
Самый безопасный вариант по умолчанию:
таймаут на каждую попытку
повтор с экспоненциальной задержкой и джиттером
тайлбрейкер вокруг вызова зависимости
Но универсального ответа нет. Подумайте о том, что вы хотите считать.
Если каждая попытка повторной попытки проходит через тайлбрейкер, один пользовательский запрос может внести несколько сбоев. Это может быстрее открыть цепь, что может быть хорошо или плохо.
Если тайлбрейкер оборачивает всю операцию повторной попытки, тайлбрейкер видит один итоговый успех или неудачу на каждый пользовательский запрос. Это спокойнее, но это может скрыть количество неудачных попыток.
Для многих приложений этот вариант разумный:
запрос пользователя
-> тайлбрейкер
-> политика повторной попытки
-> одна HTTP-попытка с таймаутом
Это означает, что тайлбрейкер отслеживает, сработала ли операция зависимости в итоге для вызывающего.
Для низкоуровневых клиентов этот вариант также имеет смысл:
запрос пользователя
-> политика повторной попытки
-> тайлбрейкер
-> одна HTTP-попытка с таймаутом
Это означает, что тайлбрейкер защищает каждую попытку.
Более важное правило таково:
Не повторяйте вслепую.
Используйте:
- небольшое максимальное количество повторных попыток
- экспоненциальную задержку
- джиттер
- таймауты на каждую попытку
- общий дедлайн запроса
- идемпотентность для операций записи
- метрики для попыток повторной попытки
Без этого повторные попытки могут превратить небольшую аварию в более крупную. Для более глубокого рассмотрения безопасности повторных попыток см. Идемпотентность в распределенных системах, которая действительно работает.
Тайлбрейкеры и Идемпотентность
Тайлбрейкеры часто появляются рядом с повторными попытками, а повторные попытки поднимают вопрос идемпотентности.
Для операций чтения повторная попытка обычно безопасна.
Для операций записи повторная попытка может быть опасной.
Рассмотрите этот вызов оплаты:
POST /charge
Если запрос завершился по таймауту, платеж провалился? Возможно. Успешно ли он прошел, но ответ был потерян? Тоже возможно.
Если вы повторите попытку без ключа идемпотентности, вы можете списать деньги дважды.
Для операций записи используйте один или несколько из следующих методов:
- ключи идемпотентности
- идентификаторы запросов
- идентификаторы операций
- уникальные ограничения
- транзакционная исходящая очередь (outbox)
- оркестрация рабочих процессов
- явное сверка (reconciliation)
Тайлбрейкер может остановить вас от продолжения вызова отказывающего платежного провайдера, но он не может сделать небезопасные повторные попытки безопасными.
Тайлбрейкеры и Резервные ответы (Fallbacks)
Когда цепь открыта, вашему сервису нужен план.
Возможные стратегии резервного ответа включают:
- вернуть закэшированные данные
- вернуть устаревшие данные с предупреждением
- опустить некритичный раздел
- поставить работу в очередь на позже
- переключиться на другого провайдера
- вернуть временную ошибку
- показать деградированный функционал
- быстро завершить запрос
Резервный ответ должен быть честным.
Например, это обычно хорошо:
{
"status": "temporary_unavailable",
"message": "Рекомендации временно недоступны"
}
Это рискованно:
{
"recommendations": []
}
Пустой список может выглядеть как допустимый результат. Он может скрыть аварию, запутать пользователей и усложнить отладку.
Тихие резервные ответы соблазнительны. Они также опасны.
Наблюдаемость (Observability) Тайлбрейкеров
Тайлбрейкер без наблюдаемости — это в основном генератор сюрпризов.
Отслеживайте как минимум эти метрики:
- текущее состояние тайлбрейкера
- изменения состояния
- разрешенные вызовы
- отклоненные вызовы
- успехи
- сбои
- таймауты
- резервные ответы
- попытки повторной попытки
- задержка на стороне зависимостей
- коды состояния на стороне зависимостей
Полезные метки (labels) включают:
- имя тайлбрейкера
- имя зависимости
- имя операции
- класс статуса
- категория ошибки
Избегайте высококардинальных меток, таких как идентификатор пользователя, полный 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 это обычно означает:
Старайтесь держать тайлбрейкер подальше от бизнес-логики.
Слой бизнеса должен понимать доменные ошибки, такие как:
поставщик платежей недоступен
рекомендации недоступны
таймаут сервиса профилей
Ему не нужно понимать состояния gobreaker.
Это разделение сохраняет архитектуру чистой:
- проблемы транспорта остаются в клиентах
- политика устойчивости остается рядом с зависимостями
- бизнес-логика остается читаемой
- обработчики остаются легкими
- тесты писать легче
Эта статья является частью темы Архитектура приложений в производстве — наряду с руководствами по идемпотентности, outbox, саге и оркестрации в Паттернах интеграции.
Частые ошибки
Ошибка 1: Отсутствие Таймаута
Тайлбрейкер не останавливает магическим образом медленные вызовы, если вызовы не возвращаются.
Если исходящая операция может зависнуть навсегда, тайлбрейкер может не увидеть отказ достаточно быстро.
Всегда используйте таймауты.
Ошибка 2: Один глобальный Тайлбрейкер для всего
Не используйте один тайлбрейкер для всех зависимостей.
Отказавший провайдер электронной почты не должен открывать цепь для вашего платежного провайдера. Медленная конечная точка поиска не должна блокировать вызовы профилей пользователей.
Используйте отдельные тайлбрейкеры для отдельных операций зависимостей, если их модели отказов различаются.
Ошибка 3: Подсчет ошибок вызывающего как сбоев зависимости
Если ваш сервис отправляет неверный ввод и получает 400 Bad Request, это обычно не является аварией на стороне зависимостей.
Не обучайте тайлбрейкер на собственных ошибках.
Ошибка 4: Повторная попытка неидемпотентных записей
Повторные попытки не бесплатны. Они могут дублировать записи, платежи, сообщения или побочные эффекты.
Сделайте записи идемпотентными перед их повторной попыткой.
Ошибка 5: Скрытие аварий за Резервными ответами
Резервные ответы должны деградировать gracefully, а не фальсифицировать реальность.
Если зависимость недоступна, ваши метрики и логи должны делать это очевидным.
Ошибка 6: Настройка без Производственных Данных
Пороги, скопированные из примеров, — это только отправные точки.
Настройте на основе:
- объема запросов
- нормальной частоты ошибок
- задержки зависимости
- влияния на пользователей
- времени восстановления
- качества резервного ответа
Ошибка 7: Использование Тайлбрейкеров вместо Управления Емкостью
Тайлбрейкер не является заменой для:
- сброса нагрузки (load shedding)
- лимитирования скорости
- ограничений очередей
- автоматического масштабирования
- настройки базы данных
- ограничений пулов соединений
- квот на стороне поставщика
Это одна часть стратегии устойчивости.
Практические значения по умолчанию
Для типичного Go-сервиса, вызывающего внутреннюю HTTP-зависимость, разумной отправной точкой может быть:
таймаут HTTP-клиента: 2–5 секунд
таймаут контекста на запрос: на основе SLA вызывающего
правило сбоя тайлбрейкера: 5 последовательных сбоев или 50 процентов ошибок после 20 запросов
таймаут открытия: 10–30 секунд
запросы в полуоткрытом состоянии: 1–5
количество повторных попыток: 1–3 попытки
задержка повторной попытки: экспоненциальная с джиттером
Это не универсальные значения. Это безопасные-ish отправные точки.
Для пользовательских 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.
Стоит ли строить собственный Тайлбрейкер?
Построение небольшого тайлбрейкера — это хорошее учебное упражнение. Это помогает понять конечный автомат.
Для производственного кода предпочитайте поддерживаемую библиотеку, если ваши потребности не очень специфичны.
Производственному тайлбрейкеру нужно обрабатывать:
- конкурентность
- переходы состояний
- счетчики
- зонды в полуоткрытом состоянии
- обратные вызовы
- настраиваемую классификацию сбоев
- поведение без гонок данных (race-free)
- предсказуемую обработку ошибок
Это не невозможно, но легко сделать ошибку тонко.
Скучная библиотека обычно является лучшим выбором.
Заключение
Паттерн тайлбрейкера — это не магическая пыль надежности.
В Go он работает лучше всего, когда является частью небольшого, явного стека устойчивости:
таймаут контекста
+ повтор с экспоненциальной задержкой и джиттером
+ тайлбрейкер
+ резервный ответ
+ метрики
Паттерн наиболее полезен на границах зависимостей, особенно вокруг удаленных сервисов, которые могут стать медленными или частично недоступными.
Используйте его, чтобы остановить каскадные сбои. Используйте его, чтобы быстро завершиться с ошибкой, когда зависимость явно нездорова. Используйте его, чтобы дать перегруженным системам возможность восстановиться.
Но не используйте его как оправдание для игнорирования таймаутов, идемпотентности, наблюдаемости или чистой архитектуры.
Хороший тайлбрейкер делает отказ понятнее и дешевле. Плохой просто делает отказ более загадочным.
Ссылки
- Микросервисы Go для оркестрации AI/ML — более широкий контекст оркестрации, в который вписываются тайлбрейкеры
- Паттерн Саги в распределенных транзакциях — паттерны распределенных транзакций, которые сочетаются с тайлбрейкерами
- Идемпотентность в распределенных системах — безопасность повторных попыток и идемпотентные операции
- Паттерн транзакционной исходящей очереди в Go — надежная доставка событий вместе с паттернами устойчивости
- Архитектура обработки ошибок Go — классификация ошибок на границах зависимостей
- Тестирование конкурентного Go-кода с synctest — тестирование асинхронного поведения с тайлбрейкерами
- Структурированное логирование в Go с помощью slog — наблюдаемость вместе с тайлбрейкерами
github.com/sony/gobreaker/v2— официальный пакет gobreaker v2- Отмена контекста и таймауты Go — паттерны контекста, которые сочетаются с тайлбрейкерами