Wzorzec Circuit Breaker w Go: Zatrzymuj kaskadowe awarie

Zatrzym kaskadowe awarie w usłudach mikroserwisowych w Go.

Page content

Przekaźnik obwodu (circuit breaker) zapobiega nadmiernemu obciążaniu zawiodłego komponentu przez usługę napisaną w Go, eliminując kaskadowe awarie, które zużywają gorutyny, sockety i pamięć, aż do całkowitego załamania systemu.

Najtrudniejsza część to nie maszyna stanów. Decyzja, gdzie umieścić przekaźnik, co uznać za awarię, jak współdziała on z limitami czasu i ponowieniami, oraz co usługa powinna robić, gdy obwód jest otwarty.

circuit breaker

W języku Go wzorzec przekaźnika obwodu jest szczególnie przydatny w przypadku zapytań wychodzących: interfejsy API HTTP, bramki płatności, usługi wyszukiwania, dostawcy e-mail, bramki LLM, wewnętrzne mikrousługi oraz inne zależności, które mogą stać się wolne, przeciążone lub częściowo niedostępne. Przy prawidłowym zastosowaniu przekaźnik obwodu redukuje awarie kaskadowe. Przy błędnym zastosowaniu staje się kolejnym niejasnym trybem awarii.

Jakiego problemu rozwiązuje przekaźnik obwodu?

Systemy rozproszone rzadko awariują w sposób czysty.

Zależność może nie być całkowicie niedostępna. Może:

  • zwracać błędy 500
  • zwracać odpowiedzi z limitowaniem szybkości 429
  • akceptować połączenia TCP, ale nigdy nie odpowiadać
  • odpowiadać po 30 sekundach zamiast 300 milisekund
  • awariować tylko przy niektórych zapytaniach
  • być przeciążona, ponieważ wszyscy klienci jednocześnie próbują ponowić zapytanie

Najgorszym przypadkiem często nie jest twarda awaria. To wolna zależność.

Wolne zapytania zużywają gorutyny, sockety, połączenia z bazą danych, pamięć i pojemność wątków roboczych. Jeśli Twoja usługa będzie czekać na zależność, która jest już niesprawna, Twoja usługa może również stać się niesprawna.

Przekaźnik obwodu zapobiega temu, powodując szybką awarię po przekroczeniu progu awarii przez zależność.

Zamiast robić to w nieskończoność:

zapytanie -> wywołaj zależność -> czekaj -> przekroczenie limitu czasu -> ponów próbę -> czekaj -> awaria

usługa ostatecznie robi to:

zapytanie -> obwód otwarty -> zwróć wartość zastępczą lub błąd natychmiastowo

Ta szybka awaria nie zawsze jest mile widziana, ale jest przewidywalna. Przewidywalna awaria jest łatwiejsza w obsłudze niż powolne załamanie.

Trzy stany przekaźnika obwodu

Większość przekaźników obwodu wykorzystuje trzy stany.

Zamknięty

Obwód jest zamknięty podczas normalnej pracy.

Zapytania są przepuszczane. Przekaźnik rejestruje sukcesy i awarie. Jeśli liczba lub stosunek awarii przekroczy próg, przekaźnik się otwiera.

Zamknięty nie oznacza “bezpieczny na zawsze”. Oznacza to, że “ruch jest obecnie dozwolony”.

Otwarty

Obwód jest otwarty, gdy zależność jest uważana za niesprawną.

Zapytania są odrzucane natychmiastowo. Usługa powinna zwrócić wartość zastępczą, odpowiedź z pamięci podręcznej, odpowiedź o obniżonej jakości lub jasny błąd źródła upstream.

Otwarcie nie naprawia zależności. Daje zależności czas na odzyskanie i chroni wywołującego przed marnowaniem zasobów.

Półotwarty

Po okresie odzysku przekaźnik wchodzi w stan półotwarty.

Przez obwód przepuszczana jest tylko ograniczona liczba zapytań testowych. Jeśli się powiodą, przekaźnik się zamyka. Jeśli się nie powiodą, przekaźnik znów się otwiera.

Stan półotwarty jest ważny, ponieważ unika dwóch złych skrajności:

  • nigdy więcej nie próbować wywołać zależności
  • zbyt szybkie przywrócenie pełnego ruchu

Przejścia między stanami wyglądają następująco:

stateDiagram-v2 [*] --> Closed Closed --> Open: Failure threshold reached Open --> HalfOpen: Timeout elapsed HalfOpen --> Closed: Trial succeeds HalfOpen --> Open: Trial fails

Przekaźnik obwodu vs Limit czasu vs Ponowienie próby

Powszechnym błędem jest traktowanie przekaźników obwodu, ponowień prób i limitów czasu jako zamiennych. Są ze sobą powiązane, ale rozwiązują różne problemy.

Limit czasu

Limit czasu określa, jak długo jedna operacja może trwać.

W Go oznacza to zwykle przekazanie context.Context z terminem lub limitem czasu do zapytania wychodzącego.

Limit czasu odpowiada na to pytanie:

Jak długo jestem gotowy czekać na to jedno zapytanie?

Ponowienie próby

Ponowienie próby powtarza operację, gdy awaria może być tymczasowa.

Ponowienia są przydatne przy krótkich awariach sieci, tymczasowych odpowiedziach 503, resetach połączeń i innych awariach przejściowych.

Ponowienie próby odpowiada na to pytanie:

Czy mam ponowić to zapytanie?

Przekaźnik obwodu

Przekaźnik obwodu zatrzymuje zapytania, gdy zależność jest prawdopodobnie niesprawna.

Odpowiada na to pytanie:

Czy mam w ogóle wywołać tę zależność w tej chwili?

Ogranicznik szybkości

Ogranicznik szybkości kontroluje, ile ruchu jest dozwolone w czasie.

Odpowiada na to pytanie:

Ile ruchu powinien wysłać ten wywołujący?

Bulkhead

Bulkhead izoluje zasoby, aby jedna zależność nie mogła zużyć wszystkiego.

Odpowiada na to pytanie:

Jak bardzo ta zależność może uszkodzić moją usługę?

Te wzorce są najskuteczniejsze, gdy używane są razem. Przekaźnik obwodu bez limitów czasu jest słaby. Ponowienia prób bez jittera mogą tworzyć burze ponowień. Wartość zastępcza bez metryk może ukryć awarię.

Kiedy używać przekaźnika obwodu w Go

Używaj przekaźnika obwodu, gdy Twoja usługa wywołuje zależność, która może awariować niezależnie od Twojej usługi.

Dobrymi kandydatami są:

  • zewnętrzne interfejsy API HTTP
  • procesory płatności
  • dostawcy e-mail i SMS
  • usługi wyszukiwania
  • usługi rekomendacji
  • bramki wnioskowania LLM
  • punkty końcowe wewnętrznych mikrousług
  • interfejsy API SaaS zewnętrznych dostawców
  • wolne lub przeciążone usługi strony odczytu

Przekaźniki obwodu są szczególnie przydatne, gdy wywołujący może obsłużyć awarię w sposób elegancki (graceful degradation).

Na przykład:

  • zwróć dane produktów z pamięci podręcznej
  • pominij blok rekomendacji
  • oznacz dostawcę płatności jako tymczasowo niedostępny
  • ustaw zadanie w kolejce na później
  • zwróć częściową odpowiedź
  • awariuj szybko z jasnym, tymczasowym błędem

Ważnym pytaniem nie jest “czy to zapytanie może awariować?”. Wszystko może awariować. Lepsze pytanie to:

Jeśli ta zależność awariuje, czy powinniśmy nadal wysyłać do niej pełny ruch?

Jeśli odpowiedź brzmi nie, przekaźnik obwodu może pomóc.

Kiedy nie używać przekaźnika obwodu

Nie dodawaj przekaźnika obwodu do każdej funkcji tylko dlatego, że wzorzec brzmi odpowiedzialnie.

Przekaźnik obwodu zwykle nie jest przydatny dla:

  • lokalnych wywołań funkcji w procesie
  • prostych operacji CRUD wewnątrz monolitu
  • logiki walidacji
  • deterministycznych reguł biznesowych
  • lokalnych operacji używających tylko CPU
  • ścieżek kodu, gdzie nie istnieje przydatna wartość zastępcza
  • operacji zapisu, które nie są idempotentne
  • zależności już chronionych przez silniejszą warstwę przepływu pracy

Przekaźnik obwodu również nie zastępuje podstawowej higieny:

  • ustaw limity czasu
  • propaguj kontekst
  • poprawnie używaj puli połączeń
  • jawnie obsługuj błędy
  • zabezpiecz ponowienia prób
  • obserwuj wskaźniki awarii

Zły przekaźnik obwodu może sprawić, że system będzie trudniejszy do analizy. Może ukryć prawdziwy problem, zbyt agresywnie odrzucać ruch lub tworzyć mylące zachowanie podczas odzyskiwania.

Zasada jest prosta:

Dodawaj przekaźniki obwodu na granicach zależności, a nie wszędzie.

Wybór biblioteki przekaźnika obwodu w Go

Możesz zaimplementować podstawowy przekaźnik obwodu samodzielnie, ale większość produkcyjnych usług w Go powinna używać biblioteki.

Najczęstszym prostym wyborem jest sony/gobreaker.

Zapewnia ona:

  • stany zamknięty, otwarty i półotwarty
  • konfigurowalne progi awarii
  • konfigurowalny limit czasu stanu otwartego
  • wywołania zwrotne przy zmianie stanu
  • liczniki zapytań
  • obsługę generyków w wersji v2
  • małą powierzchnię API

Dla większych potoków odporności można również rozejrzeć się za bibliotekami składającymi wiele polityk, takimi jak ponowienia prób, limity czasu, wartości zastępcze, limitowanie szybkości, izolacja bulkhead i przerywanie obwodu. Może to być przydatne, gdy chcesz mieć jedną warstwę odporności wokół operacji.

Dla wielu usług w Go gobreaker jest wystarczający.

Porównanie pakietów przekaźnika obwodu w Go

Go nie zawiera wbudowanego przekaźnika obwodu w bibliotece standardowej. W praktyce zazwyczaj wybierasz między małą biblioteką przekaźnika obwodu, większym frameworkiem odporności, a starszym pakietem w stylu Hystrix.

Dla większości nowych usług w Go decyzja jest prosta:

  • użyj sony/gobreaker, jeśli chcesz mały, skupiony przekaźnik obwodu
  • użyj failsafe-go, jeśli chcesz przekaźniki obwodu skomponowane z ponowieniami prób, limitami czasu, wartościami zastępczymi, bulkheadem, limitami szybkości i innymi politykami odporności
  • unikaj zaczynania nowych projektów z hystrix-go, chyba że masz już kod legacy używający tej biblioteki
Pakiet Najlepsze do Zalety Wady
sony/gobreaker/v2 Proste przekaźniki obwodu wokół klientów HTTP/RPC Małe API, obsługa generyków v2, jasny model stanów, łatwe owijanie klientów zależności Rozwiązuje tylko przerywanie obwodu; ponowienia prób, limity czasu i wartości zastępcze muszą być skomponowane osobno
failsafe-go Pełne komponowanie polityk odporności Ponowienia prób, wartości zastępcze, przekaźnik obwodu, limity czasu, bulkhead, ogranicznik szybkości, pamięć podręczna, hedge, ogranicznik adaptacyjny i throttler adaptacyjny Więcej koncepcji do nauki; cięższy niż potrzebny, jeśli chcesz tylko podstawowy przekaźnik
afex/hystrix-go Systemy w stylu Hystrix legacy Znane koncepcje Hystrix, wykonanie w stylu komend, historyczne użycie Starszy design; nie najlepszy domyślny wybór dla nowych usług w Go
go-kit/kit/circuitbreaker Usługi oparte na punktach końcowych Go kit Pasuje do stylu middleware i architektury punktów końcowych Go kit Przydatny głównie, jeśli Twoja usługa już używa Go kit
cep21/circuit Zachowanie przekaźnika obwodu w stylu Hystrix Bardziej funkcjonalne podejście w stylu Hystrix Mniej powszechne jako prosty domyślny wybór; może być więcej niż potrzeba dla małych usług

Moja domyślna rekomendacja jest celowo nudna: zacznij od sony/gobreaker/v2, gdy potrzebujesz tylko przekaźnika obwodu. Chwyć się failsafe-go, gdy chcesz wyrazić kompletną politykę odporności w jednym miejscu.

To rozdzielenie utrzymuje czystość architektury. Mały klient usługi nie potrzebuje pełnego frameworku odporności, tylko aby przestać wywoływać awariującą zależność. Ale bramka, agregator, SDK klienta API lub warstwa integracyjna o dużym ruchu mogą korzystać z skomponowanych polityk.

Instalowanie gobreaker

Używaj pakietu v2 dla nowego kodu:

go get github.com/sony/gobreaker/v2

Następnie zaimportuj go:

import "github.com/sony/gobreaker/v2"

Podstawowy przekaźnik obwodu w Go

Oto mały przykład wokół zapytania 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
}

To nie jest kompletny klient produkcyjny, ale pokazuje kształt:

  • przekaźnik owija zapytanie wychodzące
  • zapytanie HTTP otrzymuje kontekst
  • klient HTTP ma limit czasu
  • awarie po stronie serwera są liczone jako awarie przekaźnika
  • błędy otwartego obwodu są mapowane na błąd aplikacji

Konfigurowanie ustawień gobreaker

Kluczowe ustawienia warto zrozumieć.

Name

Name identyfikuje przekaźnik.

Używaj stabilnej, konkretnej nazwy:

payment-api
search-service
llm-gateway
user-service

Unikaj niejasnych nazw takich jak:

http-client
external-call
default

Będziesz chciał tę nazwę w logach i metrykach.

MaxRequests

MaxRequests kontroluje, ile zapytań jest dozwolonych, gdy przekaźnik jest w stanie półotwartym.

Mała liczba jest zwykle bezpieczniejsza. Cel stanu półotwartego to testowanie odzyskiwania, a nie natychmiastowe wysyłanie pełnego ruchu.

Interval

Interval kontroluje, kiedy wewnętrzne liczniki są czyszczone, gdy przekaźnik jest zamknięty.

Jeśli jest zerowy, liczniki nie są czyszczone automatycznie. Niezerowy interwał daje przekaźnikowi okno pamięci zbliżone do ruchomego, choć nie jest to to samo, co pełna implementacja okna ślizgowego.

Timeout

Timeout kontroluje, jak długo przekaźnik pozostaje otwarty przed przejściem do stanu półotwartego.

Jeśli limit czasu jest zbyt krótki, Twoja usługa będzie ciągle sondować zależność, która nie odzyskała sprawności. Jeśli jest zbyt długi, odzyskiwanie zostanie opóźnione.

Zacznij od czegoś konserwatywnego, takiego jak 10 do 30 sekund, a następnie dostosuj na podstawie metryk produkcyjnych.

ReadyToTrip

ReadyToTrip decyduje, kiedy przekaźnik powinien się otworzyć.

Prosta zasada to kolejne awarie:

ReadyToTrip: func(counts gobreaker.Counts) bool {
    return counts.ConsecutiveFailures >= 5
}

To łatwe do zrozumienia, ale może nie pasować do usług o dużym ruchu.

Inną opcją jest stosunek awarii po minimalnej liczbie zapytań:

ReadyToTrip: func(counts gobreaker.Counts) bool {
    total := counts.Requests
    failures := counts.TotalFailures

    if total < 20 {
        return false
    }

    return float64(failures)/float64(total) >= 0.5
}

To unika otwierania obwodu po małej liczbie próbek.

OnStateChange

OnStateChange to miejsce, w którym powinieneś emitować logi lub metryki.

Przynajmniej rejestruj:

  • nazwę przekaźnika
  • stary stan
  • nowy stan
  • znacznik czasu

Dla systemów produkcyjnych eksponuj stan przekaźnika jako metrykę. Logi są przydatne do debugowania, ale metryki są lepsze do alertów i dashboardów.

IsSuccessful

IsSuccessful pozwala Ci zdecydować, które błędy liczą się jako awarie.

To jest ważne.

Nie każdy błąd powinien otwierać przekaźnik. Na przykład, 404 Not Found z usługi użytkownika może być prawidłowym wynikiem biznesowym. 400 Bad Request może być winą wywołującego, a nie zależności.

503 Service Unavailable, przekroczenie limitu czasu, reset połączenia lub 429 Too Many Requests mogą być prawdziwym sygnałem zdrowia zależności.

Bądź tu ostrożny. Liczenie błędów w niewłaściwy sposób to jeden z najłatwiejszych sposobów na stworzenie hałaśliwego przekaźnika obwodu.

Co powinno liczyć się jako awaria?

Tutaj przychodzi na ratunek inżynieryjna ocena.

Zazwyczaj liczyć jako awarie należy:

  • przekroczenia limitu czasu sieci
  • połączenie odrzucone
  • reset połączenia
  • HTTP 500
  • HTTP 502
  • HTTP 503
  • HTTP 504
  • powtarzające się odpowiedzi 429
  • zniekształcone odpowiedzi od zależności
  • przekroczenie limitu czasu kontekstu podczas zapytania wychodzącego

Zazwyczaj nie liczyć jako awarii zależności należy:

  • błędy walidacji
  • błędy lokalnej serializacji
  • oczekiwane odpowiedzi 404
  • awarie autoryzacji po stronie wywołującego
  • odrzucenia reguł biznesowych
  • błędy danych wejściowych użytkownika

Przekaźnik powinien reprezentować zdrowie zależności, a nie ogólną awarię aplikacji.

Przekaźniki obwodu i context.Context

W Go przekaźniki obwodu nie powinny zastępować context.Context.

Przekaźnik obwodu decyduje, czy próbować wywołania. Kontekst kontroluje, jak długo to wywołanie może trwać i czy powinno się zatrzymać, gdy wywołujący zniknie.

Dobre zapytanie wychodzące powinno zwykle mieć oba:

ctx, cancel := context.WithTimeout(parentCtx, 2*time.Second)
defer cancel()

data, err := client.GetUser(ctx, userID)

Kontekst powinien przepływać przez łańcuch wywołań:

kontekst przychodzącego zapytania
-> metoda usługi
-> metoda klienta
-> zapytanie HTTP
-> zależność

Unikaj tworzenia oderwanych kontekstów tła w kodzie zakreślonym zapytaniem. Jeśli zapytanie użytkownika zostanie anulowane, praca po stronie downstream powinna zwykle również się zatrzymać.

Spokojna zasada brzmi:

Przekaźnik chroni system. Kontekst chroni zapytanie.

Normalnie potrzebujesz obu.

Przekaźniki obwodu i ponowienia prób

Ponowienia prób i przekaźniki obwodu mogą dobrze ze sobą współpracować, ale kolejność ma znaczenie.

Najbezpieczniejszym domyślnym ustawieniem jest:

limit czasu dla każdej próby
ponowienie z cofaniem wykładniczym i jitterem
przekaźnik obwodu wokół zapytania do zależności

Ale nie ma uniwersalnej odpowiedzi. Pomyśl, co chcesz liczyć.

Jeśli każda próba ponowienia przechodzi przez przekaźnik, jedno zapytanie użytkownika może przyczynić się do wielu awarii. To może szybciej otworzyć przekaźnik, co może być dobre lub złe.

Jeśli przekaźnik owija całą operację ponowienia, przekaźnik widzi jeden ostateczny sukces lub awarię na zapytanie użytkownika. To jest spokojniejsze, ale może ukryć liczbę nieudanych prób.

Dla wielu usług aplikacyjnych ten kształt jest rozsądny:

zapytanie użytkownika
-> przekaźnik obwodu
   -> polityka ponowienia prób
      -> jedno zapytanie HTTP z limitem czasu

Oznacza to, że przekaźnik śledzi, czy operacja zależności ostatecznie zadziałała dla wywołującego.

Dla klientów na niższym poziomie ten kształt może również mieć sens:

zapytanie użytkownika
-> polityka ponowienia prób
   -> przekaźnik obwodu
      -> jedno zapytanie HTTP z limitem czasu

Oznacza to, że przekaźnik chroni każdą próbę.

Bardziej ważną zasadą jest ta:

Nie powtarzaj prób ślepo.

Używaj:

  • małej maksymalnej liczby ponowień prób
  • cofania wykładniczego
  • jittera
  • limitów czasu dla każdej próby
  • całkowitego limitu czasu zapytania
  • idempotencji dla zapisów
  • metryk dla prób ponowienia

Bez tego ponowienia prób mogą przekształcić małą awarię w większą. W celu głębszego omówienia bezpieczeństwa ponowień prób zobacz Idempotencja w systemach rozproszonych, która naprawdę działa.

Przekaźniki obwodu i idempotencja

Przekaźniki obwodu często pojawiają się obok ponowień prób, a ponowienia prób rodzą pytanie o idempotencję.

Dla operacji odczytu ponawianie prób jest zwykle bezpieczne.

Dla operacji zapisu ponawianie prób może być niebezpieczne.

Rozważ to zapytanie płatności:

POST /charge

Jeśli zapytanie przekroczy limit czasu, czy płatność awariowała? Może. Czy się powiodła, ale odpowiedź zaginęła? Również może.

Jeśli ponowisz próbę bez klucza idempotencji, możesz obciążyć dwukrotnie.

Dla operacji zapisu użyj jednej lub więcej z tych metod:

  • kluczy idempotencji
  • identyfikatorów zapytań
  • identyfikatorów operacji
  • ograniczeń unikalności
  • transakcyjnej skrzynki wysyłkowej (outbox)
  • orkiestracji przepływu pracy
  • jawnej rekonsyliacji

Przekaźnik obwodu może zapobiec kontynuowaniu wywoływania awariującego dostawcy płatności, ale nie może sprawić, aby niebezpieczne ponowienia prób stały się bezpieczne.

Przekaźniki obwodu i wartości zastępcze

Gdy obwód jest otwarty, Twoja usługa potrzebuje planu.

Możliwe strategie wartości zastępczych obejmują:

  • zwrócenie danych z pamięci podręcznej
  • zwrócenie przestarzałych danych z ostrzeżeniem
  • pominięcie niekrytycznej sekcji
  • ustawienie zadania w kolejce na później
  • przełączenie na innego dostawcę
  • zwrócenie tymczasowego błędu
  • pokazanie funkcjonalności o obniżonej jakości
  • szybka awaria zapytania

Wartość zastępcza powinna być szczera.

Na przykład, to jest zwykle dobre:

{
  "status": "temporary_unavailable",
  "message": "Recommendations are temporarily unavailable"
}

To jest ryzykowne:

{
  "recommendations": []
}

Pusta lista może wyglądać jak prawidłowy wynik. Może ukryć awarię, zmylić użytkowników i utrudnić debugowanie.

Cisze wartości zastępcze są kuszące. Są również niebezpieczne.

Przekaźniki obwodu i obserwowalność

Przekaźnik obwodu bez obserwowalności to głównie generator niespodzianek.

Śledź przynajmniej te metryki:

  • obecny stan przekaźnika
  • zmiany stanów
  • zezwolone zapytania
  • odrzucone zapytania
  • sukcesy
  • awarie
  • przekroczenia limitu czasu
  • odpowiedzi zastępcze
  • próby ponowienia
  • opóźnienie downstream
  • kody stanu downstream

Przydatne etykiety obejmują:

  • nazwa przekaźnika
  • nazwa zależności
  • nazwa operacji
  • klasa stanu
  • kategoria błędu

Unikaj etykiet o wysokiej kardynalności, takich jak ID użytkownika, pełny URL, ID zapytania lub surowe komunikaty błędów.

Powinieneś móc odpowiedzieć na te pytania z dashboardów:

  • Które przekaźniki obwodu są teraz otwarte?
  • Jak często się otwierają?
  • Która zależność spowodowała otwarcie?
  • Czy użytkownicy widzą odpowiedzi zastępcze?
  • Czy opóźnienie poprawiło się po otwarciu przekaźnika?
  • Czy objętość ponowień prób wzrosła przed otwarciem przekaźnika?
  • Czy zależność odzyskała sprawność?

Jeśli nie możesz obserwować przekaźnika, nie możesz go stroić. W celu strukturalnego logowania, które dobrze łączy się z metrykami, zobacz Strukturalne logowanie w Go z slog.

Bardziej przyjazny dla produkcji kształt klienta HTTP

Dla prawdziwych usług unikaj rozrzucania logiki przekaźnika obwodu po handlerach.

Stwórz mały pakiet klienta wokół zależności.

Przykładowa struktura:

internal/
  userservice/
    client.go
    errors.go
    metrics.go

Handler nie powinien znać szczegółów gobreaker. Powinien opierać się na metodzie klienta na poziomie domenowym:

type UserService interface {
    GetUser(ctx context.Context, userID string) (*User, error)
}

Następnie implementacja może zawierać:

  • tworzenie zapytań HTTP
  • propagację kontekstu
  • wykonanie przekaźnika
  • obsługę kodów stanu
  • dekodowanie odpowiedzi
  • metryki
  • mapowanie błędów

To utrzymuje politykę odporności blisko granicy zależności. W celu więcej informacji na temat klasyfikacji błędów na granicach zobacz Architektura obsługi błędów w Go: Granice i wzorce.

Gdzie przekaźniki obwodu mieszczą się w architekturze aplikacji

Wzorzec przekaźnika obwodu należy do granic integracji.

W aplikacji Go oznacza to zwykle:

graph LR A[Handler] --> B[Application Service] B --> C[Dependency Client] C --> D[Circuit Breaker] D --> E[HTTP / RPC / DB / Queue]

Unikaj przekaźnika w logice biznesowej, jeśli to możliwe.

Warstwa biznesowa powinna rozumieć błędy domenowe, takie jak:

dostawca płatności niedostępny
rekomendacje niedostępne
przekroczenie limitu czasu usługi profili

Nie powinna potrzebować rozumieć stanów gobreaker.

To rozdzielenie utrzymuje czystość architektury:

  • zagadnienia transportowe pozostają w klientach
  • polityka odporności pozostaje blisko zależności
  • logika domenowa pozostaje czytelna
  • handlerzy pozostają cienkie
  • testy są łatwiejsze do napisania

Ten artykuł jest częścią tematu Architektura aplikacji w środowisku produkcyjnym — obok przewodników po idempotencji, outbox, saga i orkiestracji w Wzorcach Integracyjnych.

Powszechne błędy

Błąd 1: Brak limitu czasu

Przekaźnik obwodu nie magicznie zatrzymuje wolnych zapytań, chyba że zapytania zwrócą wynik.

Jeśli operacja wychodząca może wisieć w nieskończoność, przekaźnik może nie dostrzec awarii wystarczająco szybko.

Zawsze używaj limitów czasu.

Błąd 2: Jeden globalny przekaźnik dla wszystkiego

Nie używaj jednego przekaźnika dla wszystkich zależności.

Awariujący dostawca e-mail nie powinien otworzyć obwodu dla Twojego dostawcy płatności. Wolny punkt końcowy wyszukiwania nie powinien blokować zapytań do profili użytkowników.

Używaj osobnych przekaźników dla osobnych operacji zależności, gdy ich tryby awarii się różnią.

Błąd 3: Liczenie błędów wywołującego jako awarii zależności

Jeśli Twoja usługa wysyła złe dane wejściowe i otrzymuje 400 Bad Request, to zwykle nie jest awaria po stronie downstream.

Nie trenuj przekaźnika na własnych błędach.

Błąd 4: Ponawianie prób nieidempotentnych zapisów

Ponowienia prób nie są darmowe. Mogą duplikować zapisy, płatności, wiadomości lub efekty uboczne.

Spraw, aby zapisy były idempotentne przed ponawianiem prób.

Błąd 5: Ukrywanie awarii za wartościami zastępczymi

Wartości zastępcze powinny obniżać jakość w sposób elegancki, a nie fałszować rzeczywistość.

Jeśli zależność jest niedostępna, Twoje metryki i logi powinny to jasno pokazywać.

Błąd 6: Strojenie bez danych produkcyjnych

Progi skopiowane z przykładów są tylko punktami wyjścia.

Strojenie na podstawie:

  • objętości zapytań
  • normalnego wskaźnika błędów
  • opóźnienia zależności
  • wpływu na użytkownika
  • czasu odzyskiwania
  • jakości wartości zastępczej

Błąd 7: Używanie przekaźników obwodu zamiast zarządzania pojemnością

Przekaźnik obwodu nie jest substytutem:

  • odrzucania obciążenia
  • limitowania szybkości
  • limitów kolejki
  • autoskalowania
  • strojenia bazy danych
  • limitów puli połączeń
  • kwot upstream

Jest to jedna część strategii odporności.

Praktyczne wartości domyślne

Dla typowej usługi Go wywołującej wewnętrzną zależność HTTP, rozsądnym punktem wyjścia może być:

Limit czasu klienta HTTP: 2 do 5 sekund
Limit czasu kontekstu na zapytanie: na podstawie SLA wywołującego
zasada awarii przekaźnika: 5 kolejnych awarii lub 50 procent awarii po 20 zapytaniach
limit czasu otwarcia: 10 do 30 sekund
zapytania półotwarte: 1 do 5
liczba ponowień prób: 1 do 3 próby
cofanie ponowienia prób: wykładnicze z jitterem

To nie są uniwersalne wartości. To raczej bezpieczne punkty wyjścia.

Dla interfejsów API skierowanych do użytkowników, utrzymuj ścisłe budżety całkowitego opóźnienia. Dla zadań tła możesz tolerować dłuższe czasy oczekiwania. Dla dostawców płatności bądź znacznie ostrożniejszy z ponowieniami prób i idempotencją.

Kontrola przekaźnika obwodu

Przed dodaniem przekaźnika obwodu odpowiedz na te pytania:

  • Którą zależność chroni się?
  • Którą operację chroni się?
  • Jakie błędy liczą się jako awaria zależności?
  • Jakie błędy powinny być ignorowane przez przekaźnik?
  • Jaki limit czasu dotyczy każdego zapytania?
  • Czy ponowienia prób są dozwolone?
  • Czy zapisy są idempotentne?
  • Co się dzieje, gdy obwód jest otwarty?
  • Czy istnieje wartość zastępcza?
  • Czy wartość zastępcza jest widoczna w metrykach?
  • Kto otrzymuje alert, jeśli obwód ciągle się otwiera?
  • Jak przekaźnik będzie strojony po wdrożeniu?

Jeśli nie możesz odpowiedzieć na te pytania, dodanie przekaźnika może stworzyć więcej zamieszania niż odporności.

Testowanie przekaźników obwodu w Go

Testuj zachowanie, a nie wewnętrzną maszynę stanów biblioteki.

Przydatne testy obejmują:

  • zależność powoduje sukces i zwraca odpowiedź
  • zależność awariuje powtarzalnie i obwód się otwiera
  • otwarty obwód zwraca tymczasowy błąd
  • błędy walidacji po stronie klienta nie uruchamiają przekaźnika
  • limit czasu kontekstu jest respektowany
  • odpowiedź zastępcza jest zwracana, gdy oczekiwano
  • metryki są emitowane przy zmianach stanów

Używaj fałszywych serwerów HTTP dla testów w stylu integracyjnym:

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    http.Error(w, "unavailable", http.StatusServiceUnavailable)
}))
defer server.Close()

Dla testów jednostkowych ukryj zależność za interfejsem i wstrzyknij fałszywą implementację.

Utrzymuj testy deterministyczne. Unikaj spania przez długie rzeczywiste okresy czasu. Konfiguruj krótkie limity czasu przekaźnika w testach. W celu więcej informacji na temat testowania konkurencyjnego kodu Go z fałszywym czasem i izolowanymi bańkami zobacz Testowanie konkurencyjnego kodu Go z testing/synctest.

Czy powinieneś budować własny przekaźnik obwodu?

Budowanie małego przekaźnika obwodu to dobra ćwiczenie naukowe. Pomaga zrozumieć maszynę stanów.

Dla kodu produkcyjnego preferuj utrzymywaną bibliotekę, chyba że Twoje potrzeby są bardzo specyficzne.

Produkcja przekaźnik musi obsługiwać:

  • konkurencję
  • przejścia stanów
  • liczniki
  • sondy półotwarte
  • wywołania zwrotne
  • niestandardową klasyfikację awarii
  • zachowanie wolne od wyścików
  • przewidywalną obsługę błędów

To nie jest niemożliwe, ale łatwo popełnić subtelne błędy.

Nudna biblioteka jest zazwyczaj lepszym wyborem.

Podsumowanie

Wzorzec przekaźnika obwodu to nie magiczny proszek do niezawodności.

W Go działa najlepiej, gdy jest częścią małego, jawnej stosu odporności:

limit czasu kontekstu
+ ponowienie prób z cofaniem wykładniczym i jitterem
+ przekaźnik obwodu
+ wartość zastępcza
+ metryki

Wzorzec jest najbardziej przydatny na granicach zależności, szczególnie wokół usług zdalnych, które mogą stać się wolne lub częściowo niedostępne.

Używaj go do zatrzymywania awarii kaskadowych. Używaj go do szybkiej awarii, gdy zależność jest wyraźnie niesprawna. Używaj go do dawania przeciążonym systemom przestrzeni do odzyskania.

Ale nie używaj go jako wymówki do ignorowania limitów czasu, idempotencji, obserwowalności lub czystej architektury.

Dobry przekaźnik obwodu sprawia, że awaria jest jasniejsza i tańsza. Zły tylko sprawia, że awaria staje się bardziej tajemnicza.

Referencje

Subskrybuj

Otrzymuj nowe wpisy o systemach, infrastrukturze i inżynierii AI.