Надёжность
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 при ошибке |
Конфигурация
resilience.NewCircuitBreaker(resilience.BreakerConfig{
Name: "prometheus-client",
MaxFailures: 5,
HalfOpenMaxReqs: 3,
OpenTimeout: 30 * time.Second,
})Компоненты под защитой
| Компонент | Файл | Circuit |
|---|---|---|
| Prometheus queries | internal/prometheus/client.go | prometheus-client |
Когда circuit открыт, Prometheus клиент сразу возвращает ErrBreakerOpen, не допуская каскадных запросов к БД.
Экспоненциальный Backoff с Jitter
Повторные попытки используют экспоненциальный backoff с полным jitter для равномерного распределения retry-запросов во времени.
Формула
delay = random(0, min(baseDelay × 2^attempt, maxDelay))Конфигурация retry
| Компонент | Попыток | Базовая задержка | Максимум |
|---|---|---|---|
| Prometheus запросы | 3 | 100 ms | 5 s |
| License phone-home | 3 | 1 s | 5 s |
| Доставка уведомлений | 3 (настраивается) | 5 min | 5 min |
Отмена по контексту
Все retry-циклы учитывают отмену контекста. Если родительский контекст отменён или истекло время ожидания во время backoff-паузы, цикл немедленно завершается с ctx.Err().
Реализация
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_ENDPOINT | — | Tempo OTLP HTTP endpoint (например http://tempo:4318) |
OTEL_TRACES_SAMPLER | parentbased_traceidratio | Стратегия сэмплирования |
OTEL_TRACES_SAMPLER_ARG | 0.1 | 10% запросов с трейсами |
Если 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:
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.
Связанные страницы
- Мониторинг — Prometheus метрики, health checks и Grafana дашборды