Введение в концепцию предохранителей

В мире микросервисов, где несколько сервисов взаимодействуют для обработки запросов, существует риск каскадных сбоев. Представьте себе сценарий, когда один сервис не работает или отвечает медленно, вызывая цепную реакцию, которая выводит из строя всю систему. Именно здесь вступает в игру шаблон предохранителя, действуя как защитник, предотвращающий такие катастрофические сбои.

Что такое предохранитель?

Предохранитель — это шаблон проектирования, который предотвращает распространение сбоя сети или сервиса на другие сервисы. Он работает аналогично электрическому предохранителю, который срабатывает и разрывает цепь, когда ток превышает определённое значение, предотвращая дальнейшие повреждения.

Состояния предохранителя

У предохранителя есть три основных состояния:

Замкнутое

В замкнутом состоянии предохранитель пропускает все запросы к сервису. Если определённое количество последовательных запросов терпит неудачу (достигается порог сбоя), предохранитель переходит в открытое состояние.

Открытое

В открытом состоянии все запросы немедленно блокируются, и вызывающей стороне возвращается ошибка без попытки связаться с неисправным сервисом. Это даёт сервису время на восстановление и предотвращает его перегрузку.

Полуоткрытое

По истечении заданного периода восстановления предохранитель переходит в полуоткрытое состояние. Здесь он разрешает ограниченное количество тестовых запросов, чтобы проверить, восстановился ли сервис. Если эти запросы успешны, предохранитель возвращается в замкнутое состояние. Если какой-либо из этих запросов завершается неудачно, он возвращается в открытое состояние.

stateDiagram-v2 state "Замкнутое" as Замкнутое state "Открытое" as Открытое state "Полуоткрытое" as Полуоткрытое Замкнутое --> Открытое: Последовательные сбои превышают пороговое значение Открытое --> Полуоткрытое: Истекает срок восстановления Полуоткрытое --> Замкнутое: Тестовые запросы успешны Полуоткрытое --> Открытое: Тестовые запросы завершаются неудачно

Реализация предохранителя в 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
}

Преимущества и проблемы

Преимущества

  • Предотвращение каскадных сбоев: отключая цепь при сбое сервиса, вы предотвращаете перегрузку других сервисов запросами.
  • Снижение задержки: немедленный сбой в открытом состоянии снижает задержку, вызванную ожиданием ответа от неисправного сервиса.
  • Повышение устойчивости: позволяет системе быстрее восстанавливаться, давая неисправному сервису время на ремонт без дополнительной нагрузки.

Проблемы

  • Выбор порогов: может быть сложно выбрать правильные пороги сбоя и время восстановления, не вызывая ложных срабатываний или чрезмерной задержки.
  • Сложность: реализация предохранителя может усложнить вашу кодовую базу, особенно если вы создаёте его с нуля.

Пример из реальной жизни

Давайте рассмотрим пример из реальной жизни, когда у нас есть архитектура микросервиса с несколькими сервисами, взаимодей