Цели модуля

После модуля студент сможет:

  • Проектировать graceful degradation для частичных сбоев
  • Настраивать timeouts и cancellation на всех уровнях
  • Реализовывать Circuit Breaker и Bulkhead паттерны
  • Писать операционные runbooks для типовых инцидентов
  • Организовывать incident response процесс
  • Выполнять capacity planning для растущих сервисов
  • Понимать принципы chaos engineering
  • Проводить blameless post-mortems

8.1 Reliability mindset

Что такое reliability

Reliability — способность системы выполнять свои функции в заданных условиях в течение заданного времени.

Ключевые аспекты:

  • Availability (доступность): система отвечает на запросы
  • Durability (сохранность): данные не теряются
  • Correctness (корректность): система возвращает правильные результаты
  • Performance (производительность): система отвечает вовремя

Reliability vs Features

Классическая дилемма: новые фичи vs надёжность.

Google SRE вводит понятие Error Budget:

  • Если SLO не исчерпан: можно релизить фичи
  • Если SLO исчерпан: freeze фич, focus на reliability

Это превращает субъективный спор "релизить или нет" в объективное решение на основе данных.

Принципы надёжных систем

  1. Design for failure: предполагаем что всё сломается
  2. Redundancy: нет single point of failure
  3. Isolation: сбой одного компонента не роняет всю систему
  4. Automation: ручные операции — источник ошибок
  5. Observability: видим проблемы до пользователей
  6. Practice: регулярно тестируем восстановление

Виды сбоев

ТипПримерЗащита
HardwareДиск, сервер, сетьRedundancy, failover
SoftwareБаг, memory leakТесты, canary, rollback
ConfigurationНеправильный конфигValidation, staging
CapacityПерегрузкаAutoscaling, rate limiting
DependencyВнешний сервис упалCircuit breaker, fallback
HumanОшибка оператораAutomation, runbooks

8.2 Graceful degradation

Что это

Graceful degradation — способность системы продолжать работать (возможно с ограниченной функциональностью) при сбое части компонентов.

Примеры:

  • Кэш недоступен: читаем из БД (медленнее, но работает)
  • Рекомендательный сервис упал: показываем популярные товары
  • Платёжный шлюз недоступен: принимаем заказы, обрабатываем позже

Паттерны graceful degradation

Fallback

При сбое основного сервиса используем запасной вариант:

go
func (s *RecommendationService) GetRecommendations(ctx context.Context, userID string) ([]Product, error) {
    // Пробуем персональные рекомендации
    recs, err := s.mlService.GetPersonalized(ctx, userID)
    if err == nil {
        return recs, nil
    }

    s.logger.Warn("personalized recommendations failed, using fallback",
        "user_id", userID, "error", err)

    // Fallback: популярные товары
    return s.getPopularProducts(ctx)
}

Cache-aside с fallback на БД

go
func (s *RepoService) GetRepo(ctx context.Context, id string) (*models.Repo, error) {
    // Пробуем кэш
    cached, err := s.cache.Get(ctx, "repo:"+id)
    if err == nil {
        return cached, nil
    }

    // Кэш недоступен или miss — идём в БД
    repo, err := s.repo.GetByID(ctx, id)
    if err != nil {
        return nil, err
    }

    // Пытаемся сохранить в кэш (не критично если не получится)
    if err := s.cache.Set(ctx, "repo:"+id, repo, 5*time.Minute); err != nil {
        s.logger.Warn("cache set failed", "error", err)
    }

    return repo, nil
}

Feature flags для деградации

go
func (h *Handler) GetRepoDetails(w http.ResponseWriter, r *http.Request) {
    repo, err := h.repoService.GetRepo(r.Context(), id)
    if err != nil {
        h.handleError(w, err)
        return
    }

    response := RepoDetailsResponse{
        Repo: repo,
    }

    // Статистика — опциональная фича
    if h.featureFlags.IsEnabled("repo_statistics") {
        stats, err := h.statsService.GetStats(r.Context(), id)
        if err != nil {
            // Логируем, но не фейлим весь запрос
            h.logger.Warn("stats failed, degrading", "error", err)
        } else {
            response.Stats = stats
        }
    }

    h.writeJSON(w, http.StatusOK, response)
}

Async processing с очередью

Если внешняя операция может быть выполнена асинхронно:

go
func (uc *CreateOrderUseCase) Execute(ctx context.Context, req CreateOrderRequest) (*Order, error) {
    // Критичная часть: создание заказа в БД
    order, err := uc.orderService.Create(ctx, req)
    if err != nil {
        return nil, fmt.Errorf("create order: %w", err)
    }

    // Некритичная часть: отправка уведомлений через outbox
    // Если почтовый сервис упал — уведомления отправятся позже
    event := models.NewOutboxEvent("order.created", order)
    if err := uc.outboxService.Publish(ctx, event); err != nil {
        // Логируем, но не фейлим заказ
        uc.logger.Error("failed to queue notification", "error", err)
    }

    return order, nil
}

Уровни деградации

Определите заранее уровни деградации для вашего сервиса:

Уровень 0: Полная функциональность
Уровень 1: Без персонализации (fallback на популярное)
Уровень 2: Без некритичных фич (статистика, рекомендации)
Уровень 3: Read-only режим
Уровень 4: Статическая страница "мы скоро вернёмся"

Документируйте каждый уровень и условия перехода.


8.3 Timeouts и cancellation (углублённо)

Иерархия таймаутов

Каждый уровень имеет свой таймаут:

HTTP клиент (30s)
    -> HTTP сервер (25s)
        -> UseCase (20s)
            -> Service (15s)
                -> Database query (5s)
                -> External API (10s)

Правило: таймаут внутреннего уровня < таймаута внешнего.

Настройка HTTP сервера

go
srv := &http.Server{
    Addr:         ":8080",
    Handler:      router,
    ReadTimeout:  15 * time.Second,   // Чтение запроса
    WriteTimeout: 15 * time.Second,   // Запись ответа
    IdleTimeout:  60 * time.Second,   // Keep-alive
}

Настройка HTTP клиента

go
client := &http.Client{
    Timeout: 10 * time.Second,  // Общий таймаут
    Transport: &http.Transport{
        DialContext: (&net.Dialer{
            Timeout:   5 * time.Second,  // TCP connection
            KeepAlive: 30 * time.Second,
        }).DialContext,
        TLSHandshakeTimeout:   5 * time.Second,
        ResponseHeaderTimeout: 5 * time.Second,
        IdleConnTimeout:       90 * time.Second,
        MaxIdleConns:          100,
        MaxIdleConnsPerHost:   10,
    },
}

Настройка database

go
config, _ := pgxpool.ParseConfig(dsn)
config.MaxConnLifetime = time.Hour
config.MaxConnIdleTime = 30 * time.Minute
config.ConnConfig.ConnectTimeout = 5 * time.Second

// Query timeout через context
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
rows, err := pool.Query(ctx, query, args...)

Распространение cancellation

Context с cancellation должен передаваться через все слои:

go
func (h *Handler) GetRepo(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()  // Context из HTTP запроса

    // Если клиент закроет соединение, ctx отменится
    repo, err := h.usecase.Execute(ctx, id)
    if err != nil {
        if errors.Is(err, context.Canceled) {
            // Клиент закрыл соединение — не логируем как ошибку
            return
        }
        h.handleError(w, err)
        return
    }

    h.writeJSON(w, http.StatusOK, repo)
}

Проверка cancellation в долгих операциях

go
func (s *Service) ProcessItems(ctx context.Context, items []Item) error {
    for i, item := range items {
        // Проверяем cancellation на каждой итерации
        select {
        case <-ctx.Done():
            return fmt.Errorf("cancelled after %d items: %w", i, ctx.Err())
        default:
        }

        if err := s.processItem(ctx, item); err != nil {
            return fmt.Errorf("process item %d: %w", i, err)
        }
    }
    return nil
}

Deadline propagation

Дедлайн внешнего запроса должен учитываться внутренними операциями:

go
func (s *Service) GetRepo(ctx context.Context, id string) (*Repo, error) {
    // Не создаём новый таймаут, используем deadline из ctx
    // Если внешний запрос имеет 2s, а мы поставим 5s — запрос
    // всё равно упадёт по внешнему deadline
    // ПРАВИЛЬНО: проверяем оставшееся время
    if deadline, ok := ctx.Deadline(); ok {
        remaining := time.Until(deadline)
        if remaining < 100*time.Millisecond {
            return nil, errors.New("not enough time")
        }
    }

    return s.repo.GetByID(ctx, id)
}

Антипаттерны таймаутов

  1. Слишком длинные таймауты

    // ПЛОХО: 5 минут таймаут на HTTP запрос client := &http.Client{Timeout: 5 * time.Minute}

    // ПРАВИЛЬНО: секунды для HTTP, минуты только для batch jobs

  2. Отсутствие таймаутов

    // ПЛОХО: бесконечное ожидание resp, err := http.Get(url)

    // ПРАВИЛЬНО: всегда таймаут ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel()

  3. Таймауты без retry

    Таймаут без retry = ошибка для пользователя. Для временных сбоев: retry с backoff.


8.4 Circuit Breaker и Bulkhead (углублённо)

Circuit Breaker: состояния

Closed (нормальная работа)
    |
    | failures >= threshold
    v
Open (блокируем запросы)
    |
    | timeout прошёл
    v
Half-Open (тестируем одним запросом)
    |
    +--> success --> Closed
    +--> failure --> Open

Реализация Circuit Breaker

go
type CircuitBreaker struct {
    mu              sync.Mutex
    state           State
    failures        int
    successes       int
    lastStateChange time.Time
    config          Config
    onStateChange   func(from, to State)
}

type Config struct {
    FailureThreshold  int           // failures для перехода в Open
    SuccessThreshold  int           // successes для перехода из Half-Open в Closed
    OpenTimeout       time.Duration // время в Open до Half-Open
    HalfOpenMaxCalls  int           // максимум запросов в Half-Open
}

func (cb *CircuitBreaker) Execute(ctx context.Context, fn func(ctx context.Context) error) error {
    if !cb.allowRequest() {
        metrics.CircuitBreakerRejected.Inc()
        return ErrCircuitOpen
    }

    err := fn(ctx)

    if err != nil {
        cb.recordFailure()
        return err
    }

    cb.recordSuccess()
    return nil
}

func (cb *CircuitBreaker) allowRequest() bool {
    cb.mu.Lock()
    defer cb.mu.Unlock()

    switch cb.state {
    case StateClosed:
        return true
    case StateOpen:
        if time.Since(cb.lastStateChange) > cb.config.OpenTimeout {
            cb.transitionTo(StateHalfOpen)
            return true
        }
        return false
    case StateHalfOpen:
        return cb.halfOpenCalls < cb.config.HalfOpenMaxCalls
    }
    return false
}

Использование Circuit Breaker

go
type ExternalAPIClient struct {
    httpClient *http.Client
    breaker    *CircuitBreaker
}

func (c *ExternalAPIClient) GetData(ctx context.Context, id string) (*Data, error) {
    var data *Data

    err := c.breaker.Execute(ctx, func(ctx context.Context) error {
        var err error
        data, err = c.doRequest(ctx, id)
        return err
    })

    if err != nil {
        if errors.Is(err, ErrCircuitOpen) {
            // Fallback когда circuit open
            return c.getCachedData(ctx, id)
        }
        return nil, err
    }

    return data, nil
}

Bulkhead: изоляция ресурсов

Bulkhead (переборка в корабле) изолирует ресурсы чтобы сбой одного компонента не затопил всю систему.

go
type Bulkhead struct {
    name string
    sem  chan struct{}
}

func NewBulkhead(name string, maxConcurrent int) *Bulkhead {
    return &Bulkhead{
        name: name,
        sem:  make(chan struct{}, maxConcurrent),
    }
}

func (b *Bulkhead) Acquire(ctx context.Context) error {
    select {
    case b.sem <- struct{}{}:
        return nil
    case <-ctx.Done():
        return ctx.Err()
    default:
        metrics.BulkheadRejected.WithLabelValues(b.name).Inc()
        return ErrBulkheadFull
    }
}

func (b *Bulkhead) Release() {
    <-b.sem
}

Применение Bulkhead

Разные bulkheads для разных внешних сервисов:

go
type ExternalClients struct {
    paymentClient      *http.Client
    paymentBulkhead    *Bulkhead
    notificationClient *http.Client
    notificationBulkhead *Bulkhead
}

func NewExternalClients() *ExternalClients {
    return &ExternalClients{
        paymentClient:      newHTTPClient(),
        paymentBulkhead:    NewBulkhead("payment", 50),
        notificationClient: newHTTPClient(),
        notificationBulkhead: NewBulkhead("notification", 20),
    }
}

func (c *ExternalClients) ProcessPayment(ctx context.Context, req PaymentRequest) error {
    if err := c.paymentBulkhead.Acquire(ctx); err != nil {
        return fmt.Errorf("payment bulkhead: %w", err)
    }
    defer c.paymentBulkhead.Release()

    return c.paymentClient.Charge(ctx, req)
}

Если notification сервис тормозит, он займёт только свои 20 слотов. Payment сервис продолжит работать со своими 50 слотами.

Комбинация паттернов

Полный resilience stack для внешнего вызова:

go
func (c *Client) Call(ctx context.Context, req Request) (*Response, error) {
    // 1. Bulkhead: ограничиваем параллелизм
    if err := c.bulkhead.Acquire(ctx); err != nil {
        return nil, ErrBulkheadFull
    }
    defer c.bulkhead.Release()

    // 2. Circuit Breaker: защищаем от каскадных сбоев
    var resp *Response
    err := c.breaker.Execute(ctx, func(ctx context.Context) error {
        var err error
        resp, err = c.doCallWithRetry(ctx, req)
        return err
    })

    if err != nil {
        if errors.Is(err, ErrCircuitOpen) {
            return c.fallback(ctx, req)
        }
        return nil, err
    }

    return resp, nil
}

func (c *Client) doCallWithRetry(ctx context.Context, req Request) (*Response, error) {
    // 3. Retry с backoff
    return Retry(ctx, DefaultRetryConfig(), "external_call",
        func(ctx context.Context) (*Response, error) {
            // 4. Timeout через context
            ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
            defer cancel()

            return c.httpClient.Do(ctx, req)
        })
}

8.5 Operational runbooks

Что такое runbook

Runbook — пошаговая инструкция для решения конкретного инцидента. Пишется заранее, используется в стрессовой ситуации.

Структура runbook

# Runbook: High Error Rate

## Метаданные
- Алерт: HighErrorRate
- Severity: Critical
- Владелец: Backend Team
- Последний review: 2026-01-15

## Симптомы
- Error rate > 5% в течение 5 минут
- Увеличение 5xx ответов
- Жалобы пользователей

## Диагностика

### Шаг 1: Проверить статус сервиса
curl https://api.example.com/healthz
kubectl get pods -n production

### Шаг 2: Проверить логи
# Grafana/Loki запрос
{app="api"} |= "ERROR" | last 15m

### Шаг 3: Проверить метрики
# Grafana дашборд: API Overview
- Error rate по эндпоинтам
- Latency p99
- DB connection pool

### Шаг 4: Проверить зависимости
curl https://db.example.com/health
curl https://cache.example.com/health

## Возможные причины и решения

### Причина 1: Баг в последнем деплое
Признаки: error rate вырос сразу после деплоя
Решение: Rollback
kubectl rollout undo deployment/api -n production

### Причина 2: Проблема с БД
Признаки: timeout errors, connection refused
Решение: Проверить БД, перезапустить если нужно
kubectl get pods -n database
kubectl logs postgres-primary -n database

### Причина 3: Внешний сервис недоступен
Признаки: errors от внешнего API
Решение: Включить fallback через feature flag
curl -X POST https://admin.example.com/flags/external_api_fallback -d '{"enabled": true}'

## Эскалация
Если не решено за 30 минут:
- Уведомить team lead: @team-lead
- Создать инцидент в Jira
- Обновить status page

## Проверка восстановления
- Error rate < 1% в течение 10 минут
- Все health checks проходят
- Нет новых алертов

Правила хороших runbooks

  1. Пишутся ДО инцидента, не во время
  2. Конкретные команды, не абстракции
  3. Тестируются регулярно (game days)
  4. Обновляются после каждого инцидента
  5. Доступны всем on-call инженерам

Runbooks для шаблона проекта

Необходимые runbooks:

  1. High Error Rate: error rate > 5%
  2. High Latency: p99 > 1s
  3. Service Down: health check fails
  4. Database Connection Failed
  5. Outbox Lag: события не публикуются
  6. Disk Full: NFS заполнен
  7. Memory Leak: OOM kills
  8. Certificate Expiry: TLS сертификат истекает

8.6 Incident response

Severity levels

SeverityОписаниеПримерResponse time
SEV1Полный отказ сервисаAPI недоступен15 минут
SEV2Значительная деградацияError rate > 10%30 минут
SEV3Частичная деградацияОдна фича не работает4 часа
SEV4Незначительная проблемаCosmetic bugСледующий спринт

Incident response процесс

Фаза 1: Detection (обнаружение)

  • Алерт срабатывает
  • On-call инженер получает уведомление
  • Подтверждает что это реальный инцидент

Фаза 2: Triage (оценка)

  • Определяем severity
  • Определяем затронутых пользователей
  • Открываем инцидент в трекере

Фаза 3: Mitigation (смягчение)

  • Применяем runbook
  • Цель: восстановить сервис как можно быстрее
  • Не пытаемся понять root cause сразу

Фаза 4: Resolution (решение)

  • Сервис восстановлен
  • Проверяем что всё работает
  • Обновляем status page

Фаза 5: Post-mortem (анализ)

  • В течение 48 часов
  • Blameless: ищем системные причины, не виноватых
  • Action items с ответственными и дедлайнами

Роли в инциденте

РольОтветственность
Incident CommanderКоординация, коммуникация
Tech LeadТехническое решение
OperationsИнфраструктура, деплой
CommunicationsStatus page, уведомления

Для маленьких команд один человек может играть несколько ролей.

Коммуникация во время инцидента

Внутренняя:

  • Slack канал #incident-2026-01-15
  • Обновления каждые 15-30 минут
  • Чёткие next steps

Внешняя (если затронуты пользователи):

  • Status page обновлён
  • Email/уведомления для критичных инцидентов
  • Прозрачность: что случилось, что делаем, когда починим

Шаблоны коммуникации

Начало инцидента:

[SEV2] Инцидент: High error rate на API
Начало: 14:30 UTC
Затронуто: ~20% запросов возвращают 500
Действия: Инвестигируем, подозрение на последний деплой
Следующее обновление: 15:00 UTC

Разрешение:

[RESOLVED] Инцидент: High error rate на API
Длительность: 45 минут
Причина: Баг в последнем деплое
Решение: Rollback на предыдущую версию
Post-mortem: будет опубликован в течение 48 часов

8.7 Capacity planning

Что это

Capacity planning — прогнозирование ресурсов необходимых для текущей и будущей нагрузки.

Методы capacity planning

  1. Тренд-анализ

    • Собираем метрики за 3-6 месяцев
    • Строим тренд роста
    • Прогнозируем на 3-6 месяцев вперёд
  2. Нагрузочное тестирование

    • Определяем максимальную нагрузку
    • Тестируем с k6
    • Находим bottleneck
  3. Headroom planning

    • Держим 30-50% запас мощности
    • Для пиковых нагрузок (Black Friday, релизы)

Ключевые метрики для capacity planning

Для HTTP сервиса:

# Текущая нагрузка
rate(http_requests_total[1h])  # RPS

# Использование ресурсов
cpu_usage_percent
memory_usage_bytes / memory_limit_bytes
db_pool_used / db_pool_max

# Latency
histogram_quantile(0.99, http_request_duration_seconds)

Пример capacity planning

Текущее состояние:
- RPS: 100
- CPU: 40%
- Memory: 60%
- DB connections: 15/25

Прогноз через 6 месяцев (рост 20%/месяц):
- RPS: 100 * (1.2)^6 = 300

Необходимо:
- CPU: 40% * 3 = 120% -> нужно 3x CPU или оптимизация
- Memory: 60% * 3 = 180% -> нужно 2x memory
- DB connections: 15 * 3 = 45 -> нужно увеличить pool или PgBouncer

План:
- Месяц 1-2: Вертикальное масштабирование (больше CPU/RAM)
- Месяц 3-4: Добавить read replicas
- Месяц 5-6: Горизонтальное масштабирование (больше инстансов)

Autoscaling

Для Kubernetes:

yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api
  minReplicas: 3
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80

Правила autoscaling:

  • Min replicas: минимум для доступности
  • Max replicas: ограничение стоимости
  • Target utilization: 60-80% (не 100%!)
  • Scale down delay: избегаем flapping

Load testing с k6

js
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
    stages: [
        { duration: '1m', target: 50 },   // Ramp up до 50 VU
        { duration: '3m', target: 50 },   // Держим 50 VU
        { duration: '1m', target: 200 },  // Ramp up до 200 VU
        { duration: '3m', target: 200 },  // Держим 200 VU
        { duration: '1m', target: 0 },    // Ramp down
    ],
    thresholds: {
        http_req_duration: ['p(99)<500'],  // p99 < 500ms
        http_req_failed: ['rate<0.01'],    // < 1% ошибок
    },
};

export default function () {
    const res = http.get('https://api.example.com/repos');
    check(res, {
        'status is 200': (r) => r.status === 200,
    });
    sleep(1);
}

Запуск:

bash
k6 run load-test.js

Результаты:

  • Максимальный RPS до деградации
  • Bottleneck (CPU, memory, DB, network)
  • Рекомендации по масштабированию

8.8 Chaos engineering

Что это

Chaos engineering — практика целенаправленного внесения сбоев для проверки устойчивости системы.

Принцип: лучше найти слабость контролируемо, чем в production во время реального инцидента.

Уровни chaos engineering

  1. Game days: запланированные тесты с командой
  2. Automated chaos: регулярные автоматические тесты
  3. Production chaos: сбои в production (для зрелых команд)

Примеры chaos экспериментов

  1. Убить pod/инстанс

    kubectl delete pod api-xyz -n production

Проверяем:

  • Сервис продолжает работать (остальные pod'ы)
  • Новый pod запускается
  • Нет потери запросов
  1. Задержка сети

    tc (traffic control) для добавления задержки

    tc qdisc add dev eth0 root netem delay 500ms

Проверяем:

  • Таймауты срабатывают
  • Circuit breaker открывается
  • Fallback работает
  1. Отказ внешнего сервиса

    Mock внешнего сервиса возвращает 500

Проверяем:

  • Circuit breaker открывается
  • Fallback активируется
  • Пользователи видят degraded response
  1. Заполнение диска

    Заполняем диск на 90%

Проверяем:

  • Алерт срабатывает
  • Логи не пишутся (или ротируются)
  • Сервис не падает

Chaos Mesh / LitmusChaos

Для Kubernetes есть специализированные инструменты:

yaml
apiVersion: chaos-mesh.org/v1alpha1
kind: PodChaos
metadata:
  name: kill-api-pod
spec:
  action: pod-kill
  mode: one
  selector:
    labelSelectors:
      app: api

Правила chaos engineering

  1. Начинаем с staging, не с production
  2. Определяем blast radius (что может сломаться)
  3. Имеем план отката
  4. Не проводим в пиковые часы
  5. Документируем результаты

8.9 Post-mortem culture

Blameless post-mortem

Цель post-mortem: улучшить систему, не наказать человека.

Фокус на вопросах:

  • Что произошло?
  • Почему это произошло?
  • Как предотвратить в будущем?

НЕ фокус на:

  • Кто виноват?
  • Кто должен быть наказан?

Шаблон post-mortem

# Post-mortem: [Название инцидента]

## Метаданные
- Дата: 2026-01-15
- Severity: SEV2
- Длительность: 45 минут
- Затронуто: ~5000 пользователей
- Автор: @engineer
- Участники: @team-lead, @ops, @engineer

## Резюме
Краткое описание инцидента (2-3 предложения).

## Timeline
- 14:30 - Деплой версии v2.5.0
- 14:32 - Алерт HighErrorRate сработал
- 14:35 - On-call подтвердил инцидент
- 14:40 - Диагностика: ошибки в новом коде
- 14:50 - Решение: rollback
- 14:55 - Rollback завершён
- 15:00 - Подтверждение восстановления

## Impact
- Длительность: 45 минут
- Затронутые пользователи: ~5000
- Ошибки: ~12000 запросов
- Финансовый impact: [если применимо]

## Root Cause
Детальное описание первопричины.

Пример: В версии v2.5.0 была добавлена новая валидация,
которая не учитывала legacy данные. Это привело к ошибкам
для 20% запросов.

## Что прошло хорошо
- Алерт сработал через 2 минуты
- Rollback занял 5 минут
- Команда скоординировалась быстро

## Что можно улучшить
- Не было тестов на legacy данные
- Canary deployment не поймал проблему
- Runbook не покрывал этот сценарий

## Action Items
| Действие | Ответственный | Дедлайн | Приоритет |
|---|---|---|---|
| Добавить тесты на legacy данные | @engineer | 2026-01-22 | High |
| Настроить canary для валидации | @ops | 2026-01-29 | Medium |
| Обновить runbook | @engineer | 2026-01-19 | High |

## Уроки
Что мы узнали как команда?

Метрики post-mortem процесса

  • Время до обнаружения (MTTD)
  • Время до восстановления (MTTR)
  • Количество action items выполненных в срок
  • Повторяющиеся инциденты (должно быть 0)

Привязка к шаблону проекта

Где reliability в шаблоне

internal/handlers/http/middleware/
├── timeout.go            # Timeout middleware
├── recovery.go           # Panic recovery
└── circuit_breaker.go    # Circuit breaker (если есть)

internal/dr/
├── fsck.go               # Проверка целостности
├── snapshot.go           # Снапшоты
└── restore.go            # Восстановление

docs/operations/
├── runbook.md            # Операционные процедуры
├── scaling.md            # Масштабирование
└── backup-restore.md     # Бэкапы и восстановление

Что изучаем на примере шаблона

  1. Timeout middleware для HTTP
  2. Recovery middleware для panic
  3. DR модуль для проверки целостности
  4. Runbooks для типовых инцидентов
  5. Graceful shutdown

Задание: изучите internal/ и docs/operations/

  • Есть ли timeout middleware?
  • Есть ли circuit breaker для внешних сервисов?
  • Какие runbooks написаны?
  • Как организован graceful shutdown?

💱 Компромиссы

  • Fallback vs честная ошибка: популярные товары при сбое ML-сервиса сохраняют доступность, но пользователь получает нерелевантный контент; возврат ошибки честнее, однако рушит пользовательский сценарий целиком.
  • Короткие vs длинные таймауты: секунды на HTTP-вызов быстро режут зависание и не копят очередь, но убивают медленные легитимные ответы; минутные ожидания терпимы к медленным зависимостям, однако съедают горутины и слоты bulkhead.
  • Чувствительный vs консервативный Circuit Breaker: низкий FailureThreshold быстро открывает цепь и спасает от каскадного отказа, но временный сбой блокирует живой сервис; высокий порог замечает реальный отказ слишком поздно.
  • Узкий vs широкий Bulkhead: жёсткий лимит слотов надёжно изолирует медленную зависимость, но часть трафика получает ErrBulkheadFull; щедрый лимит реже отклоняет запросы, однако изоляция слабеет — один сервис способен занять все ресурсы.
  • Headroom 30-50% vs работа впритык: запас мощности переживает пики (Black Friday, релизы), но постоянно платит за простаивающие инстансы; экономия дешевле ровно до первого пика.

Практическое задание

Задание 1: Graceful degradation

Реализуйте fallback для внешнего сервиса:

go
func (s *Service) GetRecommendations(ctx context.Context, userID string) ([]Product, error) {
    // Пробуем основной сервис
    recs, err := s.mlService.Get(ctx, userID)
    if err == nil {
        return recs, nil
    }

    // Fallback на популярное
    return s.getPopular(ctx)
}

Добавьте метрику fallback_used_total.

Задание 2: Timeouts

Настройте timeouts на всех уровнях:

  • HTTP сервер: ReadTimeout, WriteTimeout
  • HTTP клиент: Timeout, Transport timeouts
  • Database: query timeout через context
  • Внешние сервисы: context.WithTimeout

Проверьте что внутренний таймаут < внешнего.

Задание 3: Circuit Breaker

Реализуйте Circuit Breaker для внешнего API:

go
type CircuitBreaker struct {
    // Closed, Open, Half-Open states
}

func (cb *CircuitBreaker) Execute(ctx context.Context, fn func() error) error

Протестируйте:

  • Переход в Open после N failures
  • Переход в Half-Open после timeout
  • Переход в Closed после success

Задание 4: Bulkhead

Реализуйте Bulkhead для изоляции внешних сервисов:

go
paymentBulkhead := NewBulkhead("payment", 50)
notificationBulkhead := NewBulkhead("notification", 20)

Проверьте что сбой notification не влияет на payment.

Задание 5: Runbook

Напишите runbook для инцидента "High Error Rate":

  • Симптомы
  • Диагностика (конкретные команды)
  • Возможные причины
  • Решения
  • Эскалация
  • Проверка восстановления

Протестируйте runbook в staging.

Задание 6: Incident response simulation

Проведите симуляцию инцидента:

  1. Введите искусственную ошибку в staging
  2. Дождитесь алерта
  3. Следуйте runbook
  4. Зафиксируйте timeline
  5. Устраните причину
  6. Проведите post-mortem

Задание 7: Capacity planning

Выполните capacity planning для вашего сервиса:

  1. Соберите метрики за последний месяц
  2. Определите текущую нагрузку (RPS, CPU, memory)
  3. Спрогнозируйте рост на 6 месяцев
  4. Определите bottleneck
  5. Составьте план масштабирования

Задание 8: Chaos experiment

Проведите chaos эксперимент в staging:

  1. Убейте один инстанс сервиса
  2. Проверьте что сервис продолжает работать
  3. Проверьте что новый инстанс запускается
  4. Задокументируйте результаты

Задание 9: Post-mortem

Напишите post-mortem для симулированного инцидента:

  • Timeline
  • Impact
  • Root cause
  • Что прошло хорошо
  • Что можно улучшить
  • Action items с ответственными

Quiz "Проверь себя"

1. Как правильно построить иерархию таймаутов?
2. Какие severity levels инцидентов существуют? — выберите все подходящие
3. Что такое graceful degradation?
4. Какие состояния у Circuit Breaker?
go
const (
    StateClosed    State = iota
    StateOpen
    StateHalfOpen
)
5. Что такое capacity planning?
6. Как error budget связывает reliability и скорость разработки?
7. Зачем нужен Bulkhead?
8. Что такое chaos engineering?
9. Что такое blameless post-mortem?
10. Какие ключевые элементы у runbook? — выберите все подходящие

✅ Итоги модуля

Студент должен:

  • Проектировать graceful degradation с fallbacks
  • Настраивать timeouts на всех уровнях
  • Реализовывать Circuit Breaker и Bulkhead
  • Писать операционные runbooks
  • Организовывать incident response процесс
  • Выполнять capacity planning
  • Понимать chaos engineering
  • Проводить blameless post-mortems
  • Ответить на все вопросы Quiz

🔗 Мостик к Модулю 9

Следующий модуль: Модуль 9. Performance and Load Testing - pprof (CPU и memory profiling), go tool trace, memory leaks диагностика, k6 нагрузочное тестирование, benchmarks, optimization patterns. Привязка к k6/ шаблона.