Wzorzec Circuit Breaker w Go: Zatrzymanie lawiny awarii

Zapobiegaj kaskadowym awariom w mikroserwisach Go.

Page content

Breaker obwodowy (circuit breaker) zapobiega Twojej usłudze Go w wywieraniu presji na awaryjną zależność, co zapobiega kaskadowym awariom, które pochłaniają gorutyny, gniazda i pamięć, aż do całkowitego załamania całego systemu.

Najtrudniejszym aspektem nie jest maszyna stanów. Trudno jest zdecydować, gdzie breaker powinien być zlokalizowany, co wchodzi w skład awarii, jak oddziałuje na timeouty i ponowne próby (retries) oraz co powinna zrobić Twoja usługa, gdy obwód jest otwarty (open).

breaker obwodowy

W Go wzorzec breakera obwodowego jest szczególnie użyteczny przy połączeniach wychodzących: wywołaniach HTTP API, bramkach płatności, usługach wyszukiwania, dostawcach e-mail, bramkach LLM, wewnętrznych mikrousługach i innych zależnościach, które mogą stać się wolne, przeciążone lub częściowo niedostępne. Poprawnie użyty breaker obwodowy redukuje kaskadowe awarie. Złe użycie sprawia, że staje się on kolejnym niejasnym trybem awarii.

Jaki problem rozwiązuje Breaker Obwodowy?

Systemy rozproszone rzadko padają w sposób czysty.

Zależność może nie być całkowicie wyłączona. Może:

  • zwracać błędy 500
  • zwracać odpowiedzi o limicie częstotliwości 429
  • przyjmować połączenia TCP, ale nigdy nie odpowiadać
  • odpowiadać w ciągu 30 sekund zamiast 300 milisekund
  • ulegać awarii tylko dla niektórych żądań
  • być przeciążona, ponieważ każdy klient ponawia próby jednocześnie

Najgorszym scenariuszem często nie jest awaria całkowita. To wolna zależność.

Wolne wywołania pochłaniają gorutyny, gniazda, połączenia do baz danych, pamięć i zasoby pracowników. Jeśli Twoja usługa nadal czeka na zależność, która jest już niesprawna, Twoja usługa też może stać się niesprawna.

Breaker obwodowy zapobiega temu, powodując szybkie zwracanie błędu, gdy zależność przekroczy próg awarii.

Zamiast robić to w kółko:

żądanie -> wywołanie zależności -> oczekiwanie -> timeout -> retry -> oczekiwanie -> błąd

usługa w końcu zrobi to:

żądanie -> obwód otwarty -> natychmiastowy powrót fallbacku lub błędu

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

Trzy Stany Breakera Obwodowego

Większość breakerów obwodowych używa trzech stanów.

Zamknięty (Closed)

Obwód jest zamknięty podczas normalnej pracy.

Żądania są przepuszczane. Breaker rejestruje sukcesy i awarie. Jeśli liczba lub stosunek awarii przekroczy próg, breaker otwiera obwód.

Zamknięty nie oznacza „zawsze bezpieczny”. Oznacza „ruch jest obecnie dozwolony”.

Otwarty (Open)

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

Żądania są odrzucane natychmiastowo. Usługa powinna zwrócić fallback, odpowiedź z pamięci podręcznej, odpowiedź w trybie obniżonej wydajności lub jasny błąd upstreamu.

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

Półotwarty (Half-Open)

Po okresie schładzania (cool-down) breaker przechodzi w stan półotwarty.

Przez obwód przepuszczane są tylko ograniczone liczby żądań próbnych. Jeśli się powiedzą, breaker zamyka obwód. Jeśli się nie powiedzą, breaker ponownie otwiera obwód.

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

  • nigdy nie próbowania zależności ponownie
  • wysyłania pełnego ruchu z powrotem zbyt szybko

Przejścia stanów wyglądają tak:

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

Breaker Obwodowy vs Timeout vs Retry

Powszechnym błędem jest traktowanie breakerów obwodowych, ponownych prób (retries) i timeoutów jako wzajemnie zastępowalnych. Są powiązane, ale rozwiązują różne problemy.

Timeout

Timeout ogranicza czas, przez jaki jedna operacja może trwać.

W Go zwykle oznacza to przekazanie context.Context z terminem końcowym lub timeoutem do wywołania wychodzącego.

Timeout odpowiada na to pytanie:

Jak długo jestem gotów czekać na to jedno wywołanie?

Retry

Retry powtarza operację, gdy awaria może być tymczasowa.

Retry są użyteczne przy krótkich zakłóceniach sieciowych, tymczasowych odpowiedziach 503, resetach połączeń i innych przejściowych awariach.

Retry odpowiada na to pytanie:

Czy powinienem spróbować ponownie tego wywołania?

Breaker Obwodowy

Breaker obwodowy zatrzymuje wywołania, gdy zależność prawdopodobnie jest niesprawna.

Odpowiada na to pytanie:

Czy w ogóle powinienem wywołać tę zależność teraz?

Rate Limiter

Rate limiter kontroluje, ile ruchu jest dozwolony w czasie.

Odpowiada na to pytanie:

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

Bulkhead

Bulkhead izoluje zasoby, tak aby jedna zależność nie mogła pochłonąć wszystkiego.

Odpowiada na to pytanie:

Ile mojej usługi ta zależność może uszkodzić?

Te wzorce są najsilniejsze, gdy są używane razem. Breaker obwodowy bez timeoutów jest słaby. Retry bez jitteru mogą stworzyć burze ponownych prób (retry storms). Fallback bez metryk może ukryć awarię.

Kiedy użyć Breakera Obwodowego w Go?

Użyj breakera obwodowego, gdy Twoja usługa wywołuje zależność, która może ulec awarii niezależnie od Twojej usługi.

Dobrymi kandydatami są:

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

Breakery obwodowe są szczególnie użyteczne, gdy wywołujący potrafi degradować się w sposób łagodny.

Na przykład:

  • zwróć dane produktu z pamięci podręcznej
  • pomiń blok rekomendacji
  • oznacz dostawcę płatności jako tymczasowo niedostępny
  • zaplanuj pracę na później, z kolejką wiadomości martwych jako zabezpieczeniem dla wiadomości, które nigdy się nie powiedzą, nawet po odzyskaniu zależności
  • zwróć częściową odpowiedź
  • szybko zwróć błąd z jasnym komunikatem o awarii tymczasowej

Ważnym pytaniem nie jest „czy to wywołanie może się nie powieść?”. Wszystko może się nie powieść. Lepszym pytaniem jest:

Jeśli ta zależność ulega awarii, czy powinniśmy kontynuować wysyłanie do niej pełnego ruchu?

Jeśli odpowiedź brzmi „nie”, breaker obwodowy może pomóc.

Kiedy NIE używać Breakera Obwodowego

Nie dodawaj breakera obwodowego do każdej funkcji tylko dlatego, że ten wzorzec brzmi odpowiedzialnie.

Breaker obwodowy zazwyczaj nie jest użyteczny dla:

  • lokalnych wywołań funkcji wewnątrz procesu
  • prostego CRUD w monolicie
  • logiki walidacji
  • deterministycznych reguł biznesowych
  • lokalnych operacji tylko CPU
  • ścieżek kodu, w których nie istnieje żaden użyteczny fallback
  • operacji zapisu, które nie są idempotentne
  • zależności chronionych już przez silniejszą warstwę przepływu pracy

Breaker obwodowy nie zastępuje również podstawowej higieny kodu:

  • ustawiaj timeouty
  • propaguj kontekst
  • poprawnie używaj puli połączeń
  • jawnie obsługuj błędy
  • rób bezpieczne retry
  • obserwuj wskaźniki awarii

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

Nieco stanowcza zasada jest prosta:

Dodawaj breakery obwodowe na granicach zależności, nie wszędzie.

Wybór Biblioteki Breakera Obwodowego w Go

Możesz zaimplementować podstawowy breaker obwodowy samodzielnie, ale większość produkcyjnych usług Go powinna używać biblioteki.

Najczęstszym prostym wyborem jest sony/gobreaker.

Daje Ci:

  • stany zamknięty, otwarty i półotwarty
  • konfigurowalne progi awarii
  • konfigurowalny timeout stanu otwartego
  • wywołania zwrotne zmiany stanu
  • liczniki żądań
  • ogólne wsparcie w wersji 2
  • mały interfejs API

Dla większych pipeline’ów odporności możesz również spojrzeć na biblioteki, które składają wiele polityk, takich jak retry, timeout, fallback, rate limiting, izolacja bulkhead i breaker obwodowy. Może to być użyteczne, gdy chcesz mieć jedną warstwę odporności wokół operacji.

Dla wielu usług Go jednak gobreaker wystarczy.

Porównanie Pakietów Breakera Obwodowego w Go

Go nie zawiera wbudowanego breakera obwodowego w standardowej bibliotece. W praktyce wybierasz zazwyczaj między małą biblioteką breakera obwodowego, większym frameworkiem odporności lub starszym pakietem w stylu Hystrix.

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

  • użyj sony/gobreaker, jeśli chcesz małego, skoncentrowanego breakera obwodowego
  • użyj failsafe-go, jeśli chcesz skomponować breakery obwodowe z retry, timeoutami, fallbackami, bulkheadami, limitami częstotliwości i innymi politykami odporności
  • unikaj rozpoczynania nowych projektów na bazie hystrix-go, chyba że masz już kod dziedziczny z niego korzystający
Package Najlepsze dla Mocne strony Kompromisy
sony/gobreaker/v2 Proste breakery obwodowe wokół klientów HTTP/RPC Mały interfejs API, wsparcie generyczne v2, klarowny model stanów, łatwe do owinięcia wokół klientów zależności Rozwiązuje tylko łamanie obwodu; retry, timeouty i fallbacki muszą być komponowane osobno
failsafe-go Pełne komponowanie polityk odporności Retry, fallback, breaker obwodowy, timeout, bulkhead, rate limiter, cache, hedge, adaptive limiter i adaptive throttler Więcej koncepcji do nauki; cięższy niż potrzebny, jeśli chcesz tylko podstawowego breakera
afex/hystrix-go Systemy w stylu Hystrix dziedziczne Znajome koncepcje Hystrix, wykonanie w stylu komend, historyczne użycie Starszy design; nie jest najlepszym domyślnym wyborem dla nowych usług Go
go-kit/kit/circuitbreaker Usługi oparte na endpointach Go kit Pasuje do stylu middleware Go kit i architektury endpointów Przydatne głównie, jeśli Twoja usługa już używa Go kit
cep21/circuit Zachowanie breakera obwodowego w stylu Hystrix Bardziej rozbudowane podejście w stylu Hystrix Mniej powszechne jako prosty domyślny wybór; może być więcej niż potrzebne dla małych usług

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

Ten podział utrzymuje architekturę czystą. Mały klient usługi nie potrzebuje pełnego frameworka odporności tylko po to, by przestać wywoływać awaryjną zależność. Ale bramka, agregator, SDK klienta API lub warstwa integracji o dużym ruchu mogą skorzystać z komponowanych polityk.

Instalacja gobreaker

Użyj pakietu v2 dla nowego kodu:

go get github.com/sony/gobreaker/v2

Następnie zaimportuj go:

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

Podstawowy Breaker Obwodowy w Go

Oto mały przykład wokół wywołania 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:

  • breaker owija wywołanie wychodzące
  • żądanie HTTP otrzymuje kontekst
  • klient HTTP ma timeout
  • awarie po stronie serwera liczą się jako awarie breakera
  • błędy otwartego obwodu są mapowane na błąd aplikacji

Konfigurowanie Ustawień gobreaker

Kluczowe ustawienia są warte zrozumienia.

Name

Name identyfikuje breaker.

Użyj stabilnej, konkretnej nazwy:

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

Unikaj niejasnych nazw takich jak:

http-client
external-call
default

Będziesz chciał tej nazwy w logach i metrykach.

MaxRequests

MaxRequests kontroluje, ile żądań jest dozwolonych, gdy breaker jest w stanie półotwartym.

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

Interval

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

Jeśli wynosi zero, liczniki nie są czyszczone automatycznie. Niezerowy interwał daje breakerowi okno pamięci typu „rolling-ish”, choć nie jest to to samo co pełna implementacja przesuwającego się okna (sliding window).

Timeout

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

Jeśli timeout jest za krótki, Twoja usługa będzie ciągle sondować zależność, która się nie odzyskała. Jeśli jest za długi, odzyskanie zostanie opóźnione.

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

ReadyToTrip

ReadyToTrip decyduje, kiedy breaker powinien się otworzyć.

Prostą zasadą są awarie kolejne:

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

To łatwe do zrozumienia, ale może nie być właściwe dla usług o wysokim wolumenie.

Inną opcją jest stosunek awarii po minimalnej liczbie żądań:

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 bardzo małej próbce.

OnStateChange

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

Przynajmniej zarejestruj:

  • nazwę breakera
  • stary stan
  • nowy stan
  • znacznik czasu

Dla systemów produkcyjnych wystaw stan breakera jako metrykę. Logi są użyteczne do debugowania, ale metryki są lepsze do alertowania 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 otworzyć breaker. 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 winą zależności.

503 Service Unavailable, timeout, reset połączenia lub 429 Too Many Requests mogą być prawdziwymi sygnałami zdrowia zależności.

Bądź ostrożny tutaj. Liczenie niewłaściwych błędów to jeden z najłatwiejszych sposobów na zbudowanie hałasującego breakera obwodowego.

Co powinno być liczone jako awaria?

To jest miejsce, w którym liczy się osąd inżynierski.

Zazwyczaj liczyć te jako awarie:

  • timeouty sieciowe
  • połączenie odrzucone (connection refused)
  • reset połączenia (connection reset)
  • HTTP 500
  • HTTP 502
  • HTTP 503
  • HTTP 504
  • powtarzające się odpowiedzi 429
  • zepsute odpowiedzi od zależności
  • przekroczenie limitu czasu kontekstu podczas wywołania wychodzącego

Zazwyczaj NIE liczyć tych jako awarii zależności:

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

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

Breakery Obwodowe i context.Context

W Go breakery obwodowe nie powinny zastępować context.Context.

Breaker obwodowy decyduje, czy podjąć próbę wywołania. Kontekst kontroluje, jak długo to wywołanie może trwać i czy powinno się zatrzymać, gdy wywołujący zniknie.

Dobre wywołanie wychodzące zazwyczaj powinno 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 żądania
-> metoda usługi
-> metoda klienta
-> żądanie HTTP
-> zależność

Unikaj tworzenia odłączonych kontekstów tła w kodzie skojarzonym z żądaniem. Jeśli żądanie użytkownika zostanie anulowane, praca downstream powinna zazwyczaj też się zatrzymać.

Spokojna zasada brzmi:

Breaker chroni system. Kontekst chroni żądanie.

Zazwyczaj potrzebujesz obu.

Breakery Obwodowe i Retry

Retry i breakery obwodowe mogą dobrze działać razem, ale kolejność ma znaczenie.

Najbezpieczniejszą domyślną konfiguracją jest:

timeout na próbę
retry z backoff i jitterem
breaker obwodowy wokół wywołania zależności

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

Jeśli każda próba retry przechodzi przez breaker, jedno żądanie użytkownika może przyczynić się do wielu awarii. Może to szybciej otworzyć breaker, co może być dobre lub złe.

Jeśli breaker owija całą operację retry, breaker widzi jedno ostateczne sukces lub awarię na żądanie użytkownika. To jest spokojniejsze, ale może ukryć liczbę nieudanych prób.

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

żądanie użytkownika
-> breaker obwodowy
   -> polityka retry
      -> jedno wywołanie HTTP z timeoutem

To oznacza, że breaker śledzi, czy operacja zależności ostatecznie zadziałała dla wywołującego.

Dla klientów niższego poziomu ten kształt może też mieć sens:

żądanie użytkownika
-> polityka retry
   -> breaker obwodowy
      -> jedno wywołanie HTTP z timeoutem

To oznacza, że breaker chroni każdą próbę.

Bardziej ważną zasadą jest ta:

Nie retryuj ślepo.

Używaj:

  • małej maksymalnej liczby retry
  • wykładniczego backoff
  • jitteru
  • timeoutów na próbę
  • globalnego deadline’u żądania
  • idempotentności dla zapisów
  • metryk dla prób retry

Bez tych elementów retry mogą zamienić małą awarię w większą. Aby uzyskać głębsze opracowanie bezpieczeństwa retry, zobacz Idempotentność w Systemach Rozproszonych, która Naprawdę Działa.

Breakery Obwodowe i Idempotentność

Breakery obwodowe często pojawiają się obok retry, a retry podnoszą pytanie o idempotentność.

Dla operacji odczytu retry są zazwyczaj bezpieczne.

Dla operacji zapisu retry mogą być niebezpieczne.

Rozważ to wywołanie płatności:

POST /charge

Jeśli żądanie ma timeout, czy płatność się nie powiodła? Może tak. Czy się powiodła, ale odpowiedź zaginęła? Też może tak.

Jeśli retry bez klucza idempotentności, możesz obciążyć dwukrotnie.

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

  • klucze idempotentności
  • ID żądań
  • ID operacji
  • ograniczenia unikalności
  • transakcyjna skrzynka nadawcza (transactional outbox)
  • orkiestracja przepływu pracy
  • jawna reconciliacja

Breaker obwodowy może powstrzymać Cię przed dalszym wywoływaniem awaryjnego dostawcy płatności, ale nie może sprawić, że niebezpieczne retry staną się bezpieczne.

Breakery Obwodowe i Fallbacki

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

Możliwe strategie fallbacku obejmują:

  • zwróć dane z pamięci podręcznej
  • zwróć stare dane z ostrzeżeniem
  • pomiń sekcję niekrytyczną
  • zaplanuj pracę na później
  • przełącz się na innego dostawcę
  • zwróć błąd tymczasowy
  • pokaż obniżoną funkcjonalność
  • szybko odrzuć żądanie

Fallback powinien być uczciwy.

Na przykład, to jest zazwyczaj dobre:

{
  "status": "temporary_unavailable",
  "message": "Rekomendacje są tymczasowo niedostępne"
}

To jest ryzykowne:

{
  "recommendations": []
}

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

Ciche fallbacki są kuszące. Są też niebezpieczne.

Breakery Obwodowe i Obserwowalność (Observability)

Breaker obwodowy bez obserwowalności to głównie generator niespodzianek.

Śledź przynajmniej te metryki:

  • obecny stan breakera
  • zmiany stanu
  • żądania dozwolone
  • żądania odrzucone
  • sukcesy
  • awarie
  • timeouty
  • odpowiedzi fallbacku
  • próby retry
  • opóźnienie downstreamu
  • kody statusu downstreamu

Użyteczne etykiety (labels) obejmują:

  • nazwa breakera
  • nazwa zależności
  • nazwa operacji
  • klasa statusu
  • kategoria błędu

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

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

  • Które breakery obwodowe są otwarte teraz?
  • Jak często się otwierają?
  • Która zależność spowodowała otwarcie?
  • Czy użytkownicy widzą odpowiedzi fallbacku?
  • Czy opóźnienie poprawiło się po otwarciu breakera?
  • Czy wolumen retry wzrósł przed otwarciem breakera?
  • Czy zależność się odzyskała?

Jeśli nie możesz obserwować breakera, nie możesz go tuneować. Do strukturalnego logowania, który dobrze współgra z metrykami, zobacz Strukturalne Logowanie w Go z slog.

Bardziej Przyjazny dla Produkcji Kształt Klienta HTTP

Dla prawdziwych usług unikaj rozprzestrzeniania logiki breakera obwodowego 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 polegać na metodzie klienta na poziomie domeny:

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

Następnie implementacja może zawierać:

  • tworzenie żądania HTTP
  • propagację kontekstu
  • wykonanie breakera
  • obsługę kodów statusu
  • dekodowanie odpowiedzi
  • metryki
  • mapowanie błędów

To utrzymuje politykę odporności blisko granicy zależności. Aby uzyskać więcej na temat klasyfikacji błędów na granicach, zobacz Architektura Obsługi Błędów Go: Granice i Wzorce.

Gdzie Breakery Obwodowe Pasują w Architekturze Aplikacji?

Wzorzec breakera obwodowego należy do granic integracji.

W aplikacji Go zwykle oznacza to:

graph LR A[Handler] --> B[Usługa Aplikacji] B --> C[Klient Zależności] C --> D[Breaker Obwodowy] D --> E[HTTP / RPC / DB / Kolejka]

Trzymaj breaker z dala od logiki biznesowej, gdy to możliwe.

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

dostawca płatności niedostępny
rekomendacje niedostępne
timeout usługi profilu

Nie powinna rozumieć stanów gobreaker.

Ten podział utrzymuje architekturę czystą:

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

Ten artykuł jest częścią tematu Architektura Aplikacji w Produkcji — wraz z przewodnikami po idempotentności, outbox, saga i orkiestracji w Wzorcach Integracji.

Powszechne Błędy

Błąd 1: Brak Timeoutu

Breaker obwodowy nie zatrzymuje magicznie wolnych wywołań, chyba że wywołania zwrócą wynik lub błąd.

Jeśli operacja wychodząca może wisieć w nieskończoność, breaker może nie zobaczyć awarii wystarczająco szybko.

Zawsze używaj timeoutów.

Błąd 2: Jeden Globalny Breaker dla Wszystkiego

Nie używaj jednego breakera dla wszystkich zależności.

Awaryjny dostawca e-mail nie powinien otwierać obwodu dla Twojego dostawcy płatności. Wolny endpoint wyszukiwania nie powinien blokować wywołań profilu użytkownika.

Używaj oddzielnych breakerów dla oddzielnych 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, jest to zazwyczaj nie awaria downstreamu.

Nie trenuj breakera na własnych błędach.

Błąd 4: Retry Nieidempotentnych Zapisów

Retry nie są darmowe. Mogą duplikować zapisy, płatności, wiadomości lub efekty uboczne.

Upewnij się, że zapisy są idempotentne przed ich retry.

Błąd 5: Ukrywanie Awarii Za Fallbackami

Fallbacki powinny degradować się łagodnie, a nie fałszować rzeczywistości.

Jeśli zależność jest wyłączona, Twoje metryki i logi powinny to make obvious (czynić oczywistym).

Błąd 6: Tuneowanie Bez Danych Produkcyjnych

Progi skopiowane z przykładów to tylko punkty startowe.

Tuneuj na podstawie:

  • wolumenu żądań
  • normalnej częstości błędów
  • opóźnienia zależności
  • wpływu na użytkownika
  • czasu odzyskiwania
  • jakości fallbacku

Błąd 7: Używanie Breakerów Obwodowych Zamiar Zarządzania Pojemnością

Breaker obwodowy nie jest zamiennikiem dla:

  • shedowania obciążenia (load shedding)
  • limitowania częstotliwości (rate limiting)
  • limitów kolejek
  • autoscalingu
  • tuningu bazy danych
  • limitów puli połączeń
  • limitów upstreamu

To jest jeden element strategii odporności.

Praktyczne Wartości Domyślne

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

timeout klienta HTTP: 2 do 5 sekund
timeout kontekstu na żądanie: na podstawie SLA wywołującego
reguła awarii breakera: 5 awarii kolejnych lub 50 procent awarii po 20 żądaniach
timeout otwarty: 10 do 30 sekund
żądania półotwarte: 1 do 5
liczba retry: 1 do 3 próby
backoff retry: wykładniczy z jitterem

To nie są uniwersalne wartości. To są bezpieczne-ish punkty startowe.

Dla interfejsów API skierowanych do użytkowników utrzymuj ścisłe budżety opóźnień całkowitych. Dla zadań w tle możesz tolerować dłuższe oczekiwania. Dla dostawców płatności bądź znacznie ostrożniejszy z retry i idempotentnością.

Checklista Breakera Obwodowego

Przed dodaniem breakera obwodowego odpowiedz na te pytania:

  • Jaka zależność jest chroniona?
  • Jaka operacja jest chroniona?
  • Jakie błędy liczą się jako awaria zależności?
  • Jakie błędy powinny być ignorowane przez breaker?
  • Jaki timeout stosuje się do każdego wywołania?
  • Czy retry są dozwolone?
  • Czy zapisy są idempotentne?
  • Co się dzieje, gdy obwód jest otwarty?
  • Czy istnieje fallback?
  • Czy fallback jest widoczny w metrykach?
  • Kto dostaje alert, jeśli obwód ciągle się otwiera?
  • Jak breaker będzie tuneowany po wdrożeniu?

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

Testowanie Breakerów Obwodowych w Go

Testuj zachowanie, nie wewnętrzny stan maszyny stanów biblioteki.

Użyteczne testy obejmują:

  • zależność się powodzi i odpowiedź jest zwracana
  • zależność powtarzająco się nie powodzi i obwód się otwiera
  • otwarty obwód zwraca błąd tymczasowy
  • błędy walidacji po stronie klienta nie wyzwalają breakera
  • timeout kontekstu jest szanowany
  • odpowiedź fallbacku jest zwracana, gdy oczekiwano
  • metryki są emitowane przy zmianach stanu

Używaj fałszywych serwerów HTTP do testów integracyjnych:

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ę.

Utrzymaj testy deterministyczne. Unikaj uśpienia na długie rzeczywiste czasy. Skonfiguruj krótkie timeouty breakera w testach. Aby uzyskać więcej na temat testowania współbieżnego kodu Go z fałszywym czasem i izolowanymi pętlami, zobacz Testowanie Współbieżnego Kodu Go z testing/synctest.

Czy Powinieneś Zbudować Własny Breaker Obwodowy?

Zbudowanie małego breakera obwodowego to dobry ćwiczenie edukacyjne. Pomaga zrozumieć maszynę stanów.

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

Produkcyjny breaker musi obsługiwać:

  • współbieżność
  • przejścia stanów
  • liczniki
  • sondy półotwarte
  • wywołania zwrotne
  • niestandardową klasyfikację awarii
  • zachowanie wolne od wyścigów (race-free)
  • przewidywalną obsługę błędów

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

Nudna biblioteka jest zazwyczaj lepszym wyborem.

Podsumowanie

Wzorzec breakera obwodowego to nie magiczny pył niezawodności.

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

timeout kontekstu
+ retry z backoff i jitterem
+ breaker obwodowy
+ fallback
+ metryki

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

Użyj go, aby powstrzymać kaskadowe awarie. Użyj go, aby szybko zwracać błąd, gdy zależność jest wyraźnie niesprawna. Użyj go, aby dać przeciążonym systemom miejsce do odzyskania.

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

Dobry breaker obwodowy czyni awarię bardziej przejrzystą i tańszą. Zły po prostu czyni awarię bardziej tajemniczą.

Referencje

Subskrybuj

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