4. SLA, SLO, SLI и как они влияют на дизайн

Управление надежностью и влияние на дизайн систем

«Если вы не можете что-то измерить, вы не можете это улучшить. А если вы не способны определить, когда система дает сбой, вы не сможете спроектировать ее успешной».
— Свободная интерпретация классического принципа Bell Labs

В предыдущей главе мы разделили требования к программной системе на функциональные («что делает код») и нефункциональные («каковы качественные атрибуты его работы»). Теперь пришло время перейти от общих пожеланий к строгой математике надежности.

Каждый инженер слышал фразы: «Наш сервис должен работать быстро» или «Система обязана быть максимально надежной». Для системного архитектора подобные формулировки лишены смысла. Насколько быстро? Для скольких процентов пользователей? Сколько минут простоя в месяц допустимо, прежде чем бизнес начнет терять миллионы?

SLI, SLO и SLA — три фундаментальных инструмента Site Reliability Engineering (SRE), с помощью которых бизнес, продуктовые менеджеры и инженеры заключают негласный контракт. Для разработчика на Go уровня Senior+ понимание этих концепций критично: они определяют бюджет на таймауты сетевых вызовов, лимиты пулов горутин, выбор стратегий ретраев и даже конфигурацию аллокатора памяти рантайма.


Определения: SLI, SLO, SLA

Эти три понятия выстраиваются в четкую вертикальную иерархию — от низкоуровневых счетчиков ядра до юридических соглашений с клиентами:

Аббревиатура Расшифровка Суть Пример
SLI Service Level Indicator Конкретная числовая метрика, характеризующая аспект работы сервиса. Измеряем. P99 latency = 45ms, error rate = 0.05%
SLO Service Level Objective Целевое значение SLI за определённый период. Целимся. P99 latency ≤ 100ms за 28 дней
SLA Service Level Agreement Юридически обязывающий договор с клиентом, включающий SLO и компенсации при нарушении. Обещаем. Доступность 99.9% ежемесячно, иначе штраф 10% от оплаты
flowchart TD
    classDef process  fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
    classDef success  fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
    classDef error    fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
    classDef warning  fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
    classDef entry    fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
    classDef aux      fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
    classDef system   fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
    classDef data     fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
    classDef network  fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
    classDef accent   fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
    SLI["SLI (Индикатор)\nЧто измеряем в коде?\n(Метрики Prometheus, P99, Error Rate)"]
    SLO["SLO (Цель)\nК чему стремимся внутри команды?\n(99.9% запросов быстрее 100 мс)"]
    SLA["SLA (Соглашение)\nЧто юридически обещаем клиенту?\n(Штрафы и неустойки при нарушении)"]
    Budget["Error Budget\n(Бюджет ошибок = 1 - SLO)\nПраво на риск и скорость релизов"]

    SLI -->|"устанавливаем цель"| SLO
    SLO -->|"включает буфер безопасности"| SLA
    SLO -->|"определяет допустимый простой"| Budget
    SLA -->|"определяет компенсации при нарушении"| Business["Бизнес-последствия\n(Финансовые потери)"]
  

SLI для Go-сервиса: четыре золотых сигнала

В методологии Google SRE выделяют Four Golden Signals — четыре ключевых индикатора, исчерпывающе описывающих здоровье любого сетевого сервиса:

flowchart LR
    classDef process  fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
    classDef success  fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
    classDef error    fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
    classDef warning  fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
    classDef entry    fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
    classDef aux      fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
    classDef system   fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
    classDef data     fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
    classDef network  fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
    classDef accent   fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
    Signals["Четыре Золотых Сигнала (SRE)"]:::process --> Lat["1. Latency (Задержка)\nP50, P90, P99"]
    Signals --> Traf["2. Traffic (Трафик)\nRPS, пропускная способность"]
    Signals --> Err["3. Errors (Ошибки)\nДоля HTTP 5xx"]
    Signals --> Sat["4. Saturation (Насыщение)\nГорутины, CPU, GC pauses, RAM"]
  

1. Latency (Задержка)

Время обработки запроса. Для веб-сервиса на Go это время от получения HTTP-запроса до отправки ответа клиенту.

Как измерять в Go:
Middleware, замеряющий длительность с помощью time.Since() и экспортирующий гистограмму в Prometheus:

Go
package middleware

import (
    "net/http"
    "time"

    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promauto"
)

var httpRequestDuration = promauto.NewHistogramVec(
    prometheus.HistogramOpts{
        Name:    "http_request_duration_seconds",
        Help:    "Длительность обработки HTTP запросов",
        Buckets: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5},
    },
    []string{"method", "path"},
)

func MetricsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        defer func() {
            duration := time.Since(start).Seconds()
            httpRequestDuration.WithLabelValues(r.Method, r.URL.Path).Observe(duration)
        }()
        next.ServeHTTP(w, r)
    })
}

2. Traffic (Трафик)

Количество входящих запросов к сервису. Измеряется в RPS (Requests Per Second) или RPM.

Как измерять:
Счетчик (Counter), инкрементируемый в том же Middleware:

Go
var httpRequestsTotal = promauto.NewCounterVec(
    prometheus.CounterOpts{
        Name: "http_requests_total",
        Help: "Общее число входящих запросов",
    },
    []string{"method", "path"},
)

// Внутри обработчика:
httpRequestsTotal.WithLabelValues(r.Method, r.URL.Path).Inc()

3. Errors (Ошибки)

Доля запросов, завершившихся сбоем. Критически важно разделять клиентские ошибки (4xx, например 400 Bad Request или 404 Not Found) и системные сбои бэкенда (5xx). В расчете SLO сервиса учитываются исключительно ошибки 5xx.

Как измерять:
Условный инкремент счетчика ошибок при statusCode >= 500:

Go
var httpRequestErrors = promauto.NewCounterVec(
    prometheus.CounterOpts{
        Name: "http_request_errors_total",
        Help: "Количество ошибок сервера 5xx",
    },
    []string{"method", "path"},
)

// При фиксации статуса ошибки:
if statusCode >= 500 {
    httpRequestErrors.WithLabelValues(r.Method, r.URL.Path).Inc()
}

4. Saturation (Насыщение)

Степень загруженности системных ресурсов. Для Go-сервиса ключевыми показателями насыщения служат:

  • Количество активных горутин (runtime.NumGoroutine()).
  • Утилизация ядер процессора относительно GOMAXPROCS.
  • Частота и длительность фаз сборки мусора (GC Pause Duration).
  • Заполненность внутренних очередей и буферизованных каналов.

Как измерять:
Экспорт runtime-метрик через prometheus.NewGoCollector() или сбор детальных кастомных метрик из структуры runtime.MemStats:

Go
// Регистрация стандартных метрик Go runtime
prometheus.MustRegister(prometheus.NewGoCollector())

SLO и Error Budget

SLO — это целевой уровень надежности, выраженный через SLI.
Пример: «99.9% запросов к эндпоинту /api/order за скользящее окно в 30 дней должны завершаться со статусом 200 OK и выполняться быстрее 200 мс».

Из значения SLO напрямую рассчитывается Error Budget (Бюджет ошибок):

$$\text{Error Budget} = 1 - \text{SLO}$$

Если внутренний SLO по доступности равен 99.9%, то допустимый объем сбоев за 30-дневный период составляет:
$$(100\% - 99.9\%) \times 30 \times 24 \times 60 \approx 43.2 \text{ минуты}$$

Бюджет ошибок — это законное право инженерной команды идти на оправданный риск:

  • Выкатывать сложные архитектурные релизы.
  • Проводить канареечные развертывания и A/B-тесты.
  • Экспериментировать с новыми версиями компилятора Go.

Пока бюджет ошибок не исчерпан, команда может сохранять высокую скорость разработки (Feature Velocity). Но если произошла серия инцидентов и бюджет сгорел до нуля, релизы нового функционала автоматически замораживаются: все силы команды переключаются на укрепление отказоустойчивости и устранение корневых причин сбоев.

gantt
    title Сгорание бюджета ошибок (Error Budget Burn Rate), окно 30 дней
    dateFormat YYYY-MM-DD
    axisFormat %d

    section Бюджет доступности
    Нормальная работа (Burn Rate 1x)     :active, b1, 2026-01-01, 2026-01-16
    Инцидент в ЦОД — сгорает 60% бюджета :crit, b2, 2026-01-16, 2026-01-18
    Заморозка релизов и стабилизация     :b3, 2026-01-18, 2026-01-31
  

Как SLO и Error Budget влияют на архитектурный дизайн

SLO — это не просто цветные графики в Grafana. Они диктуют выбор конкретных технических решений в кодовой базе.

Таймауты и дедлайны

Если совокупный SLO по latency для эндпоинта равен 200 мс, то все вызовы к базам данных и микросервисам должны иметь жесткие квоты времени, укладывающиеся в этот лимит. В Go это реализуется через context.WithTimeout:

Go
package service

import (
    "context"
    "errors"
    "time"
)

var ErrTemporaryUnavailable = errors.New("сервис временно недоступен")

type StockClient interface {
    CheckAvailability(ctx context.Context, items []string) (bool, error)
}

type OrderService struct {
    stockClient StockClient
}

func (s *OrderService) CreateOrder(ctx context.Context, req []string) error {
    // Если SLO сервиса 200ms, выделяем жесткий лимит 50ms на проверку наличия
    stockCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
    defer cancel()

    _, err := s.stockClient.CheckAvailability(stockCtx, req)
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            // Таймаут превышен: аккуратно отдаем ошибку без блокировки потока
            return ErrTemporaryUnavailable
        }
        return err
    }
    return nil
}

Ретраи и Circuit Breaker

При временной недоступности зависимого сервиса бездумные ретраи (Retries) способны мгновенно испепелить весь бюджет ошибок и устроить лавинообразный отказ всей системы (Thundering Herd). Архитектура обязана предусматривать:

  • Exponential backoff со случайным смещением (jitter).
  • Строгое ограничение числа попыток.
  • Circuit Breaker (предохранитель), размыкающий цепь при превышении порогового процента сбоев (подробнее в 36. Circuit Breaker, Retry, Timeout и Backoff).

Репликация и Multi-Region

Если для системы выставлен SLO доступности 99.99% («четыре девятки»), допустимое время простоя составляет всего ~52 минуты в год. Достичь такого показателя в рамках одного дата-центра физически невозможно: любая авария на магистральной оптоволоконной линии или плановые работы энергетиков сожгут годовой бюджет.

Следовательно, архитектура обязана стать:

Управление горутинами и пулами соединений

Бесконтрольный запуск горутин или открытие тысяч одновременных сокетов к базе данных приводит к быстрому исчерпанию ресурсов (Saturation) и деградации Latency.

Ограничение параллелизма через буферизованный канал-семафор:

Go
package limit

import "context"

// Ограничение количества одновременных тяжелых операций числом 100
var sem = make(chan struct{}, 100)

func HeavyOperation(ctx context.Context) error {
    select {
    case sem <- struct{}{}:
        defer func() { <-sem }()
        // Выполнение полезной работы
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
}

Тонкая настройка пула соединений к PostgreSQL через пакет database/sql:

Go
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(5 * time.Minute)

Mechanical Sympathy: GC и хвостовые задержки

Для сервисов на Go с жесткими требованиями к хвостовой задержке (P99 < 50ms) критическое значение приобретает поведение сборщика мусора.

Хотя паузы Stop-The-World в рантайме Go составляют доли миллисекунды, при интенсивном темпе аллокаций фазы маркировки (GC Mark Phase) запускаются слишком часто. Если горутина попадает на фазу GC Assist (когда рантайм привлекает пользовательскую горутину к помощи в разметке памяти), время ее выполнения скачкообразно возрастает, что мгновенно пробивает границу P99.

Инженерный комплекс мер:

  1. Снижение аллокаций: переиспользование временных объектов через sync.Pool.
  2. Профилирование памяти: регулярный запуск go tool pprof -alloc_space.
  3. Тюнинг GC: настройка параметров GOGC и мягкого лимита GOMEMLIMIT (Go 1.19+).
  4. Предпочтение Value-типов: размещение данных на стеке без выхода в кучу (Escape Analysis).
Go
package buffer

import (
    "net/http"
    "sync"
)

type Response struct {
    Data []byte
}

// Пул переиспользуемых структур для горячего пути
var responsePool = sync.Pool{
    New: func() interface{} { 
        return &Response{Data: make([]byte, 0, 1024)} 
    },
}

func HandlerOptimized(w http.ResponseWriter, r *http.Request) {
    resp := responsePool.Get().(*Response)
    defer responsePool.Put(resp)
    resp.Data = resp.Data[:0] // Сброс длины среза без повторной аллокации памяти

    w.WriteHeader(http.StatusOK)
}

Практический пример: расчет бюджета ошибок для Go-сервиса

Спроектируем контур надежности для микросервиса обработки платежей.

SLI:

  • Availability: доля ответов 2xx/3xx к общему числу запросов.
  • P99 Latency: 99-й процентиль времени формирования ответа.

SLO:

  • Availability $\ge$ 99.95% за скользящее окно 30 дней.
  • P99 Latency $\le$ 100 мс за те же 30 дней.

Расчет бюджета ошибок:

  • Месячный интервал: $30 \times 24 \times 60 = 43\,200$ минут.
  • Допустимое время деградации: $0.05\% \times 43\,200 = \mathbf{21.6 \text{ минуты}}$ в месяц.

Архитектурные последствия:

  • Неаккуратный деплой, вызывающий 2-минутный разрыв соединений, съедает 10% месячного бюджета.
  • Инцидент с падением пода на 5 минут сжигает сразу 23% бюджета.
  • Следовательно, сервис обязан поддерживать:
    1. Корректный Graceful Shutdown с перехватом SIGTERM.
    2. Настроенные Readiness и Liveness probes в манифестах Kubernetes.
    3. Стратегию развертывания Rolling Update с параметрами maxUnavailable: 0 и maxSurge: 1.

SLA: когда метрики становятся деньгами

SLA (Service Level Agreement) — это уже юридический и финансовый документ между поставщиком и клиентом.

Классическая формулировка:

«Платформа гарантирует доступность сервиса на уровне 99.9% ежемесячно. В случае превышения суммарного времени простоя более 43.2 минут в месяц, заказчику предоставляется компенсация в размере 10% от ежемесячной абонентской платы за каждый последующий час недоступности».

Фундаментальное правило системного проектирования: внутренний SLO обязан быть жестче внешнего SLA.

flowchart LR
    classDef process  fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
    classDef success  fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
    classDef error    fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
    classDef warning  fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
    classDef entry    fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
    classDef aux      fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
    classDef system   fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
    classDef data     fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
    classDef network  fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
    classDef accent   fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
    SLA["Внешний SLA: 99.9%\n(Допустимо: 43.2 мин)"]
    SLO["Внутренний SLO: 99.95%\n(Допустимо: 21.6 мин)"]
    Buffer["Буфер безопасности:\n21.6 мин на ликвидацию аварии"]:::process

    SLO --> Buffer --> SLA
  

Итог

  1. SLI — объективные числовые измерения работы системы (Latency, Traffic, Errors, Saturation), собираемые через Prometheus middleware.
  2. SLO — амбициозная внутренняя цель надежности, порождающая Error Budget (инженерное право на риск и эксперименты).
  3. SLA — юридический договор с клиентом, нарушение которого ведет к штрафам.
  4. SLO напрямую диктуют архитектуру: таймауты сетевых контекстов (context.WithTimeout), паттерны отказоустойчивости (Circuit Breaker), размеры пулов горутин и стратегии развертывания в Kubernetes.
  5. Для систем на Go контроль хвостовых задержек P99 неразрывно связан с тюнингом Garbage Collector (GOMEMLIMIT) и переиспользованием памяти (sync.Pool).

Освоив метрики надежности, мы готовы перейти к фундаментальным законам распределенных вычислений: компромиссам между задержкой, пропускной способностью и доступностью. В следующей лекции: 5. Latency, Throughput, Availability и Trade-offs.