📋 Spis treści
- Wprowadzenie do scenariuszy awarii
- Podstawowy wzorzec ponawiania
- Wykładnicze opóźnienie
- Ponawianie z kontekstem i limitem czasu
- Wzorzec circuit breaker
- Jitter w systemach rozproszonych
- Najlepsze praktyki
🎯 Wprowadzenie do scenariuszy awarii
W systemach rozproszonych i programowaniu sieciowym awarie są nieuniknione. Do typowych scenariuszy należą:
- Przekroczenia limitu czasu sieci i tymczasowe problemy z łącznością
- Niedostępność usługi lub jej przeciążenie
- Ograniczanie liczby żądań przez zewnętrzne API
- Błędy połączeń z bazą danych
- Błędy przejściowe, które same się rozwiązują
Implementacja odpowiednich mechanizmów ponawiania sprawia, że aplikacje są bardziej odporne i niezawodne.
🔁 Podstawowy wzorzec ponawiania
Najprostszy mechanizm ponawiania polega na wielokrotnym wykonywaniu operacji z ustalonym opóźnieniem pomiędzy próbami.
Przykład: prosta funkcja ponawiania
package main
import (
"errors"
"fmt"
"time"
)
// Retry wykonuje przekazaną funkcję maksymalnie maxAttempts razy
func Retry(maxAttempts int, delay time.Duration, fn func() error) error {
var err error
for attempt := 1; attempt <= maxAttempts; attempt++ {
err = fn()
if err == nil {
return nil // Sukces
}
if attempt < maxAttempts {
fmt.Printf("Próba %d nie powiodła się: %v. Ponawianie za %v...\n",
attempt, err, delay)
time.Sleep(delay)
}
}
return fmt.Errorf("po %d próbach, ostatni błąd: %w", maxAttempts, err)
}
// Przykładowe użycie
func main() {
counter := 0
// Symulowana operacja, która dwa razy zawodzi, a potem się udaje
operation := func() error {
counter++
if counter < 3 {
return errors.New("tymczasowy błąd")
}
return nil
}
err := Retry(5, 1*time.Second, operation)
if err != nil {
fmt.Printf("Operacja nie powiodła się: %v\n", err)
} else {
fmt.Println("Operacja zakończona sukcesem!")
}
}
💡 Uwaga: Stałe opóźnienia mogą przeciążać systemy przy dużym ruchu.
⏱️ Wykładnicze opóźnienie
Wykładnicze opóźnienie zwiększa czas oczekiwania pomiędzy kolejnymi próbami, zmniejszając obciążenie niedostępnych usług.
// RetryWithBackoff implementuje ponawianie z wykładniczym opóźnieniem
func RetryWithBackoff(maxAttempts int, initialDelay time.Duration,
maxDelay time.Duration, fn func() error) error {
var err error
delay := initialDelay
for attempt := 1; attempt <= maxAttempts; attempt++ {
err = fn()
if err == nil {
return nil
}
if attempt < maxAttempts {
// Oblicz wykładnicze opóźnienie
delay = time.Duration(float64(initialDelay) * math.Pow(2, float64(attempt-1)))
// Ogranicz maksymalne opóźnienie
if delay > maxDelay {
delay = maxDelay
}
fmt.Printf("Próba %d nie powiodła się: %v. Ponawianie za %v...\n",
attempt, err, delay)
time.Sleep(delay)
}
}
return fmt.Errorf("niepowodzenie po %d próbach: %w", maxAttempts, err)
}
⏰ Ponawianie z kontekstem i limitem czasu
Pakiet context umożliwia ustawienie limitów czasu i anulowanie ponawiania.
// RetryWithContext respektuje anulowanie i timeout kontekstu
func RetryWithContext(ctx context.Context, maxAttempts int,
delay time.Duration, fn func(context.Context) error) error {
var err error
for attempt := 1; attempt <= maxAttempts; attempt++ {
// Sprawdź, czy kontekst nie został anulowany
select {
case <-ctx.Done():
return fmt.Errorf("operacja anulowana: %w", ctx.Err())
default:
}
err = fn(ctx)
if err == nil {
return nil
}
if attempt < maxAttempts {
fmt.Printf("Próba %d nie powiodła się: %v. Ponawianie...\n", attempt, err)
select {
case <-time.After(delay):
case <-ctx.Done():
return fmt.Errorf("ponawianie anulowane: %w", ctx.Err())
}
}
}
return fmt.Errorf("osiągnięto maksymalną liczbę prób: %w", err)
}
⚠️ Ostrzeżenie: Zawsze respektuj anulowanie kontekstu.
🔌 Wzorzec circuit breaker
Wyłącznik obwodu zapobiega wielokrotnemu wywoływaniu niedostępnej usługi.
// Call wykonuje funkcję tylko jeśli wyłącznik nie jest otwarty
func (cb *CircuitBreaker) Call(fn func() error) error {
cb.mu.Lock()
defer cb.mu.Unlock()
if cb.state == StateOpen {
if time.Since(cb.lastFailTime) > cb.resetTimeout {
cb.state = StateHalfOpen
cb.failures = 0
fmt.Println("Circuit breaker: Otwarty -> Półotwarty")
} else {
return errors.New("circuit breaker jest otwarty")
}
}
err := fn()
if err != nil {
cb.failures++
cb.lastFailTime = time.Now()
if cb.failures >= cb.maxFailures {
cb.state = StateOpen
}
return err
}
if cb.state == StateHalfOpen {
cb.state = StateClosed
}
cb.failures = 0
return nil
}
🎲 Jitter w systemach rozproszonych
Jitter wprowadza losowość opóźnień, zapobiegając efektowi thundering herd.
// RetryWithJitter dodaje losowy jitter do wykładniczego opóźnienia
func RetryWithJitter(maxAttempts int, baseDelay time.Duration,
maxDelay time.Duration, fn func() error) error {
var err error
for attempt := 1; attempt <= maxAttempts; attempt++ {
err = fn()
if err == nil {
return nil
}
if attempt < maxAttempts {
expDelay := float64(baseDelay) * math.Pow(2, float64(attempt-1))
jitter := rand.Float64() * expDelay
delay := time.Duration(jitter)
if delay > maxDelay {
delay = maxDelay
}
fmt.Printf("Próba %d nie powiodła się. Ponawianie za %v...\n", attempt, delay)
time.Sleep(delay)
}
}
return fmt.Errorf("niepowodzenie po %d próbach: %w", maxAttempts, err)
}
✅ Wskazówka: Jitter jest kluczowy w systemach rozproszonych.
✨ Najlepsze praktyki
- Stosuj wykładnicze opóźnienie
- Dodawaj jitter
- Szanuj anulowanie kontekstu
- Ustal maksymalną liczbę prób
- Nie ponawiaj błędów 4xx
- Stosuj circuit breaker