🔌 Circuit Breaker: Как не добить лежачего (и не умереть самому) Знакомая ситуация: внешний сервис (например, процессинг платежей или тяжелая аналитика…
Знакомая ситуация: внешний сервис (например, процессинг платежей или тяжелая аналитика) начинает тормозить. Ваши запросы к нему зависают по таймауту.
Джунское решение: "Наверное, сеть моргнула. Добавлю-ка я ретраи!"
Результат: внешний сервис и так лежит под нагрузкой, а ваши ретраи создают шторм запросов (Retry Storm), добивая его окончательно. Тем временем, в вашем сервисе копятся тысячи горутин, ожидающих ответа, исчерпываются коннекты к вашей собственной базе данных, случается OOM или паника. Поздравляю, вы получили каскадный сбой (Cascading Failure).
Чтобы этого избежать, в архитектуре микросервисов используется паттерн Circuit Breaker (Предохранитель).
Идея взята из электрики. Если в сети короткое замыкание - пробки выбивает, чтобы не сгорел весь дом.
Как это работает (Три состояния):
1. Closed (Закрыт): Всё хорошо. Запросы идут во внешний сервис как обычно. Если случаются ошибки, предохранитель увеличивает счетчик неудач.
2. Open (Открыт): Пробили лимит ошибок (например, 5 таймаутов подряд). "Пробки выбило". Теперь Circuit Breaker перехватывает все новые запросы и моментально возвращает ошибку, даже не пытаясь сходить по сети.
Профит: Мы экономим свои ресурсы (горутины не висят) и даем внешнему сервису время остыть и перезапуститься.
3. Half-Open (Полуоткрыт): Прошел таймаут (например, 10 секунд). Мы пропускаем один тестовый запрос, чтобы проверить "пульс" больного. Если запрос успешен - цепь закрывается (переходим в Closed). Если упал - снова Open.
Реализация на Go:
Не нужно писать конечные автоматы руками. В комьюнити есть стандарт де-факто - библиотека sony/gobreaker (да, от той самой Sony).
import "github.com/sony/gobreaker"
var cb *gobreaker.CircuitBreaker
func init() {
settings := gobreaker.Settings{
Name: "BillingAPI",
MaxRequests: 1, // Сколько запросов пускать в состоянии Half-Open
Timeout: 10 * time.Second, // Сколько времени висеть в состоянии Open
ReadyToTrip: func(counts gobreaker.Counts) bool {
// Открываем цепь, если было 5 ошибок подряд
return counts.ConsecutiveFailures >= 5
},
}
cb = gobreaker.NewCircuitBreaker(settings)
}
func GetBalance(userID int) (float64, error) {
// Оборачиваем опасный сетевой вызов в cb.Execute
result, err := cb.Execute(func() (interface{}, error) {
return billingAPI.Fetch(userID) // Реальный поход в сеть
})
if err != nil {
// Если цепь открыта, cb.Execute сразу вернет gobreaker.ErrOpenState.
// Идеальное место, чтобы отдать закешированное значение (Graceful Degradation)!
if errors.Is(err, gobreaker.ErrOpenState) {
return getBalanceFromCache(userID)
}
return 0, err
}
return result.(float64), nil
}
🔥 Не суйте предохранители везде
Circuit Breaker нужен исключительно для интеграций с внешними сервисами или некритичными зависимостями. Если вы попытаетесь обернуть им запросы к вашей основной базе данных (PostgreSQL), вы просто замаскируете проблему. Если лежит ваша главная БД - сервис должен лечь вместе с ней, а не пытаться делать вид, что всё нормально.
#golang #architecture #microservices #circuitbreaker #systemdesign
👉 @golang_lib