Цели модуля
После модуля студент сможет:
- Проектировать 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
Это превращает субъективный спор "релизить или нет" в объективное решение на основе данных.
Принципы надёжных систем
- Design for failure: предполагаем что всё сломается
- Redundancy: нет single point of failure
- Isolation: сбой одного компонента не роняет всю систему
- Automation: ручные операции — источник ошибок
- Observability: видим проблемы до пользователей
- 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
При сбое основного сервиса используем запасной вариант:
gofunc (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 на БД
gofunc (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 для деградации
gofunc (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 с очередью
Если внешняя операция может быть выполнена асинхронно:
gofunc (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 сервера
gosrv := &http.Server{ Addr: ":8080", Handler: router, ReadTimeout: 15 * time.Second, // Чтение запроса WriteTimeout: 15 * time.Second, // Запись ответа IdleTimeout: 60 * time.Second, // Keep-alive }
Настройка HTTP клиента
goclient := &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
goconfig, _ := 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 должен передаваться через все слои:
gofunc (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 в долгих операциях
gofunc (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
Дедлайн внешнего запроса должен учитываться внутренними операциями:
gofunc (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) }
Антипаттерны таймаутов
-
Слишком длинные таймауты
// ПЛОХО: 5 минут таймаут на HTTP запрос client := &http.Client{Timeout: 5 * time.Minute}
// ПРАВИЛЬНО: секунды для HTTP, минуты только для batch jobs
-
Отсутствие таймаутов
// ПЛОХО: бесконечное ожидание resp, err := http.Get(url)
// ПРАВИЛЬНО: всегда таймаут ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel()
-
Таймауты без 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
gotype 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
gotype 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 (переборка в корабле) изолирует ресурсы чтобы сбой одного компонента не затопил всю систему.
gotype 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 для разных внешних сервисов:
gotype 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 для внешнего вызова:
gofunc (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
- Пишутся ДО инцидента, не во время
- Конкретные команды, не абстракции
- Тестируются регулярно (game days)
- Обновляются после каждого инцидента
- Доступны всем on-call инженерам
Runbooks для шаблона проекта
Необходимые runbooks:
- High Error Rate: error rate > 5%
- High Latency: p99 > 1s
- Service Down: health check fails
- Database Connection Failed
- Outbox Lag: события не публикуются
- Disk Full: NFS заполнен
- Memory Leak: OOM kills
- 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 | Инфраструктура, деплой |
| Communications | Status 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
-
Тренд-анализ
- Собираем метрики за 3-6 месяцев
- Строим тренд роста
- Прогнозируем на 3-6 месяцев вперёд
-
Нагрузочное тестирование
- Определяем максимальную нагрузку
- Тестируем с k6
- Находим bottleneck
-
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:
yamlapiVersion: 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
jsimport 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); }
Запуск:
bashk6 run load-test.js
Результаты:
- Максимальный RPS до деградации
- Bottleneck (CPU, memory, DB, network)
- Рекомендации по масштабированию
8.8 Chaos engineering
Что это
Chaos engineering — практика целенаправленного внесения сбоев для проверки устойчивости системы.
Принцип: лучше найти слабость контролируемо, чем в production во время реального инцидента.
Уровни chaos engineering
- Game days: запланированные тесты с командой
- Automated chaos: регулярные автоматические тесты
- Production chaos: сбои в production (для зрелых команд)
Примеры chaos экспериментов
-
Убить pod/инстанс
kubectl delete pod api-xyz -n production
Проверяем:
- Сервис продолжает работать (остальные pod'ы)
- Новый pod запускается
- Нет потери запросов
-
Задержка сети
tc (traffic control) для добавления задержки
tc qdisc add dev eth0 root netem delay 500ms
Проверяем:
- Таймауты срабатывают
- Circuit breaker открывается
- Fallback работает
-
Отказ внешнего сервиса
Mock внешнего сервиса возвращает 500
Проверяем:
- Circuit breaker открывается
- Fallback активируется
- Пользователи видят degraded response
-
Заполнение диска
Заполняем диск на 90%
Проверяем:
- Алерт срабатывает
- Логи не пишутся (или ротируются)
- Сервис не падает
Chaos Mesh / LitmusChaos
Для Kubernetes есть специализированные инструменты:
yamlapiVersion: chaos-mesh.org/v1alpha1 kind: PodChaos metadata: name: kill-api-pod spec: action: pod-kill mode: one selector: labelSelectors: app: api
Правила chaos engineering
- Начинаем с staging, не с production
- Определяем blast radius (что может сломаться)
- Имеем план отката
- Не проводим в пиковые часы
- Документируем результаты
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 # Бэкапы и восстановление
Что изучаем на примере шаблона
- Timeout middleware для HTTP
- Recovery middleware для panic
- DR модуль для проверки целостности
- Runbooks для типовых инцидентов
- 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 для внешнего сервиса:
gofunc (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:
gotype 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 для изоляции внешних сервисов:
gopaymentBulkhead := NewBulkhead("payment", 50) notificationBulkhead := NewBulkhead("notification", 20)
Проверьте что сбой notification не влияет на payment.
Задание 5: Runbook
Напишите runbook для инцидента "High Error Rate":
- Симптомы
- Диагностика (конкретные команды)
- Возможные причины
- Решения
- Эскалация
- Проверка восстановления
Протестируйте runbook в staging.
Задание 6: Incident response simulation
Проведите симуляцию инцидента:
- Введите искусственную ошибку в staging
- Дождитесь алерта
- Следуйте runbook
- Зафиксируйте timeline
- Устраните причину
- Проведите post-mortem
Задание 7: Capacity planning
Выполните capacity planning для вашего сервиса:
- Соберите метрики за последний месяц
- Определите текущую нагрузку (RPS, CPU, memory)
- Спрогнозируйте рост на 6 месяцев
- Определите bottleneck
- Составьте план масштабирования
Задание 8: Chaos experiment
Проведите chaos эксперимент в staging:
- Убейте один инстанс сервиса
- Проверьте что сервис продолжает работать
- Проверьте что новый инстанс запускается
- Задокументируйте результаты
Задание 9: Post-mortem
Напишите post-mortem для симулированного инцидента:
- Timeline
- Impact
- Root cause
- Что прошло хорошо
- Что можно улучшить
- Action items с ответственными
Quiz "Проверь себя"
const (
StateClosed State = iota
StateOpen
StateHalfOpen
)✅ Итоги модуля
Студент должен:
- Проектировать 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/ шаблона.