Введение в концепцию предохранителей
В мире микросервисов, где несколько сервисов взаимодействуют для обработки запросов, существует риск каскадных сбоев. Представьте себе сценарий, когда один сервис не работает или отвечает медленно, вызывая цепную реакцию, которая выводит из строя всю систему. Именно здесь вступает в игру шаблон предохранителя, действуя как защитник, предотвращающий такие катастрофические сбои.
Что такое предохранитель?
Предохранитель — это шаблон проектирования, который предотвращает распространение сбоя сети или сервиса на другие сервисы. Он работает аналогично электрическому предохранителю, который срабатывает и разрывает цепь, когда ток превышает определённое значение, предотвращая дальнейшие повреждения.
Состояния предохранителя
У предохранителя есть три основных состояния:
Замкнутое
В замкнутом состоянии предохранитель пропускает все запросы к сервису. Если определённое количество последовательных запросов терпит неудачу (достигается порог сбоя), предохранитель переходит в открытое состояние.
Открытое
В открытом состоянии все запросы немедленно блокируются, и вызывающей стороне возвращается ошибка без попытки связаться с неисправным сервисом. Это даёт сервису время на восстановление и предотвращает его перегрузку.
Полуоткрытое
По истечении заданного периода восстановления предохранитель переходит в полуоткрытое состояние. Здесь он разрешает ограниченное количество тестовых запросов, чтобы проверить, восстановился ли сервис. Если эти запросы успешны, предохранитель возвращается в замкнутое состояние. Если какой-либо из этих запросов завершается неудачно, он возвращается в открытое состояние.
Реализация предохранителя в Go
Чтобы реализовать предохранитель в Go, вы можете использовать существующую библиотеку, например, gobreaker, или создать свой собственный с нуля.
Использование библиотеки gobreaker
Библиотека gobreaker — популярный выбор для реализации предохранителей в Go. Вот как вы можете её использовать:
package main
import (
"fmt"
"log"
"net/http"
"time"
"github.com/sony/gobreaker"
)
func main() {
var st gobreaker.Settings
st.Name = "Предохранитель PoC"
st.Timeout = time.Second * 5
st.MaxRequests = 2
st.ReadyToTrip = func(counts gobreaker.Counts) bool {
return counts.ConsecutiveFailures >= 1
}
cb := gobreaker.NewCircuitBreaker(st)
url := "http://localhost:8080/success"
cb.Execute(func() (int, error) { return Get(url) })
fmt.Println("Состояние предохранителя:", cb.State()) // замкнуто!
url = "http://localhost:8080/failure"
cb.Execute(func() (int, error) { return Get(url) })
fmt.Println("Состояние предохранителя:", cb.State()) // открыто!
time.Sleep(time.Second * 6)
url = "http://localhost:8080/success"
cb.Execute(func() (int, error) { return Get(url) })
fmt.Println("Состояние предохранителя:", cb.State()) // полуоткрыто!
url = "http://localhost:8080/success"
cb.Execute(func() (int, error) { return Get(url) })
fmt.Println("Состояние предохранителя:", cb.State()) // замкнуто!
}
func Get(url string) (int, error) {
r, _ := http.Get(url)
if r.StatusCode != http.StatusOK {
return r.StatusCode, fmt.Errorf("не удалось получить %s", url)
}
return r.StatusCode, nil
}
Создание предохранителя с нуля
Если вы предпочитаете создать свой собственный предохранитель, вот простой пример:
package main
import (
"fmt"
"log"
"net/http"
"sync"
"time"
)
const (
Замкнутое = "замкнуто"
Открыто = "открыто"
Полуоткрыто = "полуоткрыто"
)
type circuitBreaker struct {
mu sync.Mutex
state string
failureCount int
lastFailureTime time.Time
halfOpenSuccessCount int
failureThreshold int
recoveryTime time.Duration
halfOpenMaxRequests int
timeout time.Duration
}
func NewCircuitBreaker(failureThreshold int, recoveryTime time.Duration, halfOpenMaxRequests int, timeout time.Duration) *circuitBreaker {
return &circuitBreaker{
state: Замкнутое,
failureThreshold: failureThreshold,
recoveryTime: recoveryTime,
halfOpenMaxRequests: halfOpenMaxRequests,
timeout: timeout,
}
}
func (cb *circuitBreaker) Call(fn func() (any, error)) (any, error) {
cb.mu.Lock()
defer cb.mu.Unlock()
switch cb.state {
case Замкнутое:
return cb.handleClosedState(fn)
case Открыто:
return cb.handleOpenState()
case Полуоткрыто:
return cb.handleHalfOpenState(fn)
default:
return nil, fmt.Errorf("неизвестно состояние цепи")
}
}
func (cb *circuitBreaker) handleClosedState(fn func() (any, error)) (any, error) {
result, err := fn()
if err != nil {
cb.failureCount++
cb.lastFailureTime = time.Now()
if cb.failureCount >= cb.failureThreshold {
cb.state = Открыто
}
return nil, err
}
cb.failureCount = 0
return result, nil
}
func (cb *circuitBreaker) handleOpenState() (any, error) {
if time.Since(cb.lastFailureTime) > cb.recoveryTime {
cb.state = Полуоткрыто
}
return nil, fmt.Errorf("цепь разомкнута")
}
func (cb *circuitBreaker) handleHalfOpenState(fn func() (any, error)) (any, error) {
result, err := fn()
if err != nil {
cb.state = Открыто
return nil, err
}
cb.halfOpenSuccessCount++
if cb.halfOpenSuccessCount >= cb.halfOpenMaxRequests {
cb.state = Замкнутое
}
return result, nil
}
func main() {
cb := NewCircuitBreaker(3, time.Second*5, 2, time.Second*1)
url := "http://localhost:8080/success"
result, err := cb.Call(func() (any, error) { return Get(url) })
fmt.Println("Состояние предохранителя:", cb.state)
url = "http://localhost:8080/failure"
result, err = cb.Call(func() (any, error) { return Get(url) })
fmt.Println("Состояние предохранителя:", cb.state)
time.Sleep(time.Second * 6)
url = "http://localhost:8080/success"
result, err = cb.Call(func() (any, error) { return Get(url) })
fmt.Println("Состояние предохранителя:", cb.state)
}
func Get(url string) (int, error) {
r, _ := http.Get(url)
if r.StatusCode != http.StatusOK {
return r.StatusCode, fmt.Errorf("не удалось получить %s", url)
}
return r.StatusCode, nil
}
Преимущества и проблемы
Преимущества
- Предотвращение каскадных сбоев: отключая цепь при сбое сервиса, вы предотвращаете перегрузку других сервисов запросами.
- Снижение задержки: немедленный сбой в открытом состоянии снижает задержку, вызванную ожиданием ответа от неисправного сервиса.
- Повышение устойчивости: позволяет системе быстрее восстанавливаться, давая неисправному сервису время на ремонт без дополнительной нагрузки.
Проблемы
- Выбор порогов: может быть сложно выбрать правильные пороги сбоя и время восстановления, не вызывая ложных срабатываний или чрезмерной задержки.
- Сложность: реализация предохранителя может усложнить вашу кодовую базу, особенно если вы создаёте его с нуля.
Пример из реальной жизни
Давайте рассмотрим пример из реальной жизни, когда у нас есть архитектура микросервиса с несколькими сервисами, взаимодей