Skip to content

Надёжность

SLOzy реализует несколько паттернов для обеспечения стабильной работы в неблагоприятных условиях: circuit breaker для предотвращения каскадных сбоев, экспоненциальный backoff с jitter для защиты от thundering herd и трассировка запросов для анализа задержек.

Circuit Breaker

Circuit breaker оборачивает все внешние HTTP-вызовы — запросы к Prometheus, доставку уведомлений и license phone-home — конечным автоматом, который предотвращает отправку запросов к отказавшей зависимости.

Состояния

СостояниеЗначениеПереход
ClosedНормальная работа, запросы проходят→ Open после 5 последовательных ошибок
OpenЗапросы отклоняются мгновенно→ Half-Open через 30 секунд
Half-OpenОграниченное число запросов для проверки восстановления→ Closed после 3 успехов, или обратно в Open при ошибке

Конфигурация

go
resilience.NewCircuitBreaker(resilience.BreakerConfig{
    Name:            "prometheus-client",
    MaxFailures:     5,
    HalfOpenMaxReqs: 3,
    OpenTimeout:     30 * time.Second,
})

Компоненты под защитой

КомпонентФайлCircuit
Prometheus queriesinternal/prometheus/client.goprometheus-client

Когда circuit открыт, Prometheus клиент сразу возвращает ErrBreakerOpen, не допуская каскадных запросов к БД.

Экспоненциальный Backoff с Jitter

Повторные попытки используют экспоненциальный backoff с полным jitter для равномерного распределения retry-запросов во времени.

Формула

delay = random(0, min(baseDelay × 2^attempt, maxDelay))

Конфигурация retry

КомпонентПопытокБазовая задержкаМаксимум
Prometheus запросы3100 ms5 s
License phone-home31 s5 s
Доставка уведомлений3 (настраивается)5 min5 min

Отмена по контексту

Все retry-циклы учитывают отмену контекста. Если родительский контекст отменён или истекло время ожидания во время backoff-паузы, цикл немедленно завершается с ctx.Err().

Реализация

go
func BackoffDuration(attempt int, base time.Duration, max time.Duration) time.Duration {
    exp := 1 << uint(attempt)
    d := time.Duration(float64(base) * float64(exp))
    if d > max { d = max }
    return time.Duration(rand.Int63n(int64(d)))
}

func Retry(ctx context.Context, maxAttempts int, base, max time.Duration, fn func(context.Context) error) error {
    for attempt := 0; attempt < maxAttempts; attempt++ {
        if attempt > 0 {
            delay := BackoffDuration(attempt-1, base, max)
            timer := time.NewTimer(delay)
            select {
            case <-ctx.Done():
                timer.Stop()
                return ctx.Err()
            case <-timer.C:
            }
        }
        if err := fn(ctx); err == nil {
            return nil
        }
        lastErr = err
    }
    return lastErr
}

Трассировка запросов

SLOzy использует OpenTelemetry SDK с OTLP HTTP экспортом в Grafana Tempo. Tracer инициализируется в main.go через resilience.InitTracerProvider().

Конфигурация

ПеременнаяПо умолчаниюОписание
OTEL_EXPORTER_OTLP_ENDPOINTTempo OTLP HTTP endpoint (например http://tempo:4318)
OTEL_TRACES_SAMPLERparentbased_traceidratioСтратегия сэмплирования
OTEL_TRACES_SAMPLER_ARG0.110% запросов с трейсами

Если OTEL_EXPORTER_OTLP_ENDPOINT не задан, трейсинг — no-op (ничего не отправляет).

Медленные запросы

Запросы дольше 100 ms логируются как потенциальные проблемы производительности:

[trace] slow request: GET /api/v1/slos took 1.2s (status=200)

Типы span

ТипОписание
SpanKindServerВходящий HTTP-запрос (Gin middleware через otelgin)
SpanKindClientИсходящий HTTP-запрос (Prometheus query)

Регистрация middleware

Регистрируется в cmd/slozy-web/main.go:

go
app.router.Use(resilience.TracingMiddleware("slozy-web"))

Middleware оборачивает otelgin.Middleware, который автоматически проставляет HTTP-атрибуты (method, path, status) и распространяет контекст трассировки.

Интеграция с Grafana

Трейсы доступны в Grafana через Tempo datasource. Используйте Explore → Tempo для поиска по имени сервиса (slozy-web), длительности или тегам. Span-метрики (service graphs, span metrics) генерируются metrics_generator Tempo и доступны в VictoriaMetrics.

Связанные страницы