День 2: Отказоустойчивость и жизненный цикл сервиса
🎯 Цели дня
- Освоить паттерн Circuit Breaker и Retry с экспоненциальной задержкой.
- Реализовать корректный Graceful Shutdown HTTP-сервера.
- Понять порядок закрытия внешних зависимостей (БД, очереди).
- Подготовить ответы на вопросы о деплое в Kubernetes.
📖 Теоретический минимум (45 мин)
Circuit Breaker
Три состояния:
- Closed (норма) — запросы проходят.
- Open (блокировка) — запросы сразу отклоняются, чтобы не усугублять сбой.
- Half-Open (проверка) — пропускается тестовый запрос, чтобы проверить, восстановился ли сервис.
Зачем: Без CB один упавший downstream тянет за собой всю цепочку (каскадный сбой).
Retry с exponential backoff + jitter
delay = base_delay * 2^attempt + random_jitter
Почему jitter обязателен: Без него все сервисы одновременно долбят восстановившийся сервис (Thundering herd problem) и снова его кладут.
Graceful Shutdown в K8s
- Pod получает SIGTERM.
- Сервис должен убрать себя из readiness-пробы (чтобы не получать новые запросы).
- Дать активным запросам доработать (обычно 10–30 сек).
- Закрыть внешние подключения (БД, очереди).
- Завершиться.
Важно: Через 30 сек после SIGTERM K8s шлёт SIGKILL (принудительное убийство). Если не успел — данные могут потеряться.
💻 Практика (2–3 часа)
Задание 1: Circuit Breaker
Реализуй простую версию или подключи sony/gobreaker:
gotype CircuitBreaker struct { name string maxFailures int timeout time.Duration failures atomic.Int32 state atomic.Int32 // 0=closed, 1=open, 2=half-open lastFailure atomic.Int64 } var ErrCircuitOpen = errors.New("circuit breaker is open") func (cb *CircuitBreaker) Execute(ctx context.Context, fn func(context.Context) error) error { state := cb.state.Load() if state == 1 { // open lastFail := time.Unix(0, cb.lastFailure.Load()) if time.Since(lastFail) > cb.timeout { cb.state.Store(2) // half-open } else { return fmt.Errorf("%s: %w", cb.name, ErrCircuitOpen) } } err := fn(ctx) if err != nil { cb.failures.Add(1) cb.lastFailure.Store(time.Now().UnixNano()) if cb.failures.Load() >= int32(cb.maxFailures) { cb.state.Store(1) // open } return err } cb.failures.Store(0) cb.state.Store(0) // closed return nil }
Тест: Внешний сервис 3 раза возвращает 500 → CB переходит в Open → следующие запросы сразу возвращают ErrCircuitOpen без реальных вызовов.
Задание 2: Graceful Shutdown
Подними HTTP-сервер с эндпоинтом /slow (спит 5 секунд):
gofunc main() { mux := http.NewServeMux() mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) fmt.Fprint(w, "ok") }) mux.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) { select { case <-time.After(5 * time.Second): fmt.Fprint(w, "done") case <-r.Context().Done(): log.Println("клиент отключился") } }) srv := &http.Server{Addr: ":8080", Handler: mux} go func() { log.Println("сервер на :8080") if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("listen: %v", err) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("получен сигнал завершения...") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { log.Printf("принудительное завершение: %v", err) } // Здесь закрываешь подключения: БД, RabbitMQ, Redis // db.Close() // rabbitConn.Close() log.Println("сервер остановлен корректно") }
Эксперимент:
- Запусти
curl http://localhost:8080/slow. - Сразу отправь
SIGTERM:kill <pid>. - Убедись, что запрос дорабатывает (или отменяется по контексту клиента).
🏗️ Архитектурное задание для AgentForge (1 час)
Добавь в свой boilerplate корректный shutdown. Опиши порядок действий в main():
- Перехват
os.Signal(SIGINT, SIGTERM). - Вызов
srv.Shutdown(ctx)с таймаутом 10 сек. - Закрытие пула соединений с БД (
db.Close()). - Закрытие коннекта к RabbitMQ/Redis.
Вопрос: Почему именно такой порядок? Ответ: Сначала останавливаем приём новых → даём доработать старым → рвём внешние связи. Если закрыть БД раньше, активные запросы упадут с ошибкой.
🗣️ Топ-5 вопросов для самопроверки
-
Почему retry без jitter опасен в распределённых системах? Ответ: Thundering herd problem — все сервисы одновременно долбят восстановившийся сервис и снова его кладут.
-
Что произойдет с активными запросами при деплое в Kubernetes без Graceful Shutdown? Ответ: K8s убивает под через 30 сек после SIGTERM. Активные запросы оборвутся, клиенты получат 502.
-
Как правильно настроить
http.Clientдля микросервисных вызовов? Ответ: Всегда задаватьTimeout, переиспользовать один инстанс клиента (для keep-alive соединений). -
В каких состояниях может быть Circuit Breaker? Ответ: Closed (норма), Open (блокировка), Half-Open (проверка).
-
Почему порядок закрытия ресурсов при shutdown важен? Ответ: Если закрыть БД раньше, чем доработают активные запросы, они упадут с ошибкой. Правильный порядок: стоп приём → доработка → закрытие внешних связей.
🧩 Проверь себя
✅ Чек-лист дня
- Реализовал Circuit Breaker (свой или через
sony/gobreaker) - Настроил Graceful Shutdown с корректным порядком закрытия ресурсов
- Понимаю, почему retry без jitter опасен
- Могу объяснить, что происходит при деплое в K8s
- Добавил shutdown в свой boilerplate