> Как реализовать graceful shutdown в Go? (Go)

Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: Black Wall Group (BWG)

Стек: Go

> Пример ответа

Короткий ответ

Graceful shutdown в Go - это процесс корректного завершения работы приложения с завершением всех активных операций и освобождением ресурсов. Основной подход - использование signal.NotifyContext для получения системных сигналов (SIGINT, SIGTERM) и http.Server.Shutdown с контекстом и таймаутом. Важно дождаться завершения всех горутин через sync.WaitGroup и закрыть соединения с внешними системами. Это позволяет избежать потери данных и битых соединений при деплое.

Подробное объяснение

Graceful shutdown решает проблему внезапного завершения процесса, когда запросы обрабатываются, а соединения с БД или очередями открыты. Без него клиенты получают ошибки, данные могут быть потеряны, а внешние системы - десинхронизированы.

Механика в Go строится на трёх уровнях:

  1. Перехват сигналов ОС - signal.NotifyContext или signal.Notify ловят SIGINT (Ctrl+C) и SIGTERM (kill, docker stop, kubectl terminate). После получения сигнала запускается процедура завершения.

  2. Остановка приёма новых запросов - для HTTP-сервера это server.Shutdown(ctx), который перестаёт принимать новые соединения и ждёт завершения активных. Для gRPC - grpcServer.GracefulStop(). Для воркеров - закрытие каналов и ожидание завершения обработки.

  3. Завершение зависимых ресурсов - закрытие пулов соединений, flush буферов, завершение фоновых горутин. Здесь критичен порядок: сначала перестаём принимать новые задачи, потом ждём завершения текущих, потом закрываем ресурсы.

Ключевой trade-off - таймаут. Нельзя ждать вечно: если операция зависла, процесс должен завершиться принудительно. Обычно используют 10-30 секунд, после чего вызывают os.Exit(1) или просто возвращают управление.

Важно понимать разницу между server.Shutdown и server.Close: первый ждёт завершения активных запросов, второй - немедленно рвёт все соединения.

На практике

Для backend-сервиса типичная последовательность:

  1. Создаём корневой контекст с signal.NotifyContext.
  2. Запускаем HTTP-сервер в горутине.
  3. Ждём <-ctx.Done().
  4. Создаём контекст с таймаутом (например, 15 секунд).
  5. Вызываем server.Shutdown(shutdownCtx).
  6. Закрываем соединения с БД, Redis, Kafka.
  7. Ждём завершения воркеров через WaitGroup.
  8. Выходим.

Для воркеров (consumer) - закрываем канал с задачами, дожидаемся завершения обработки текущих сообщений, затем коммитим offset или подтверждаем обработку.

В микросервисной архитектуре важно также уведомить service discovery (Consul, etcd) о недоступности инстанса до начала shutdown, чтобы балансировщик перестал слать трафик.

Пример кода

GO
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
srv := &http.Server{Addr: ":8080"}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %v", err)
}
}()
<-ctx.Done()
log.Println("shutting down")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("http shutdown: %v", err)
}
db.Close()
log.Println("done")
}

Как отвечать на собеседовании

Начни с проблемы, которую решает graceful shutdown: потеря данных и обрыв соединений. Затем опиши три шага: перехват сигналов, остановка приёма новых задач, ожидание завершения активных. Упомяни signal.NotifyContext и http.Server.Shutdown как стандартные инструменты. Обязательно скажи про таймаут - это важный нюанс, который показывает понимание trade-off между корректностью и доступностью. Если спросят про воркеры - расскажи про WaitGroup и закрытие каналов. Для senior-уровня добавь про порядок закрытия ресурсов и взаимодействие с service discovery.

Что проверяет интервьюер

  • Понимание жизненного цикла процесса и системных сигналов.
  • Знание стандартной библиотеки: signal, context, http.Server.
  • Умение проектировать конкурентный код: горутины, каналы, WaitGroup.
  • Понимание trade-off: таймаут против бесконечного ожидания.
  • Навык работы с внешними зависимостями: БД, очереди, gRPC.

Типичные ошибки

  • Использование os.Exit без graceful shutdown - убивает процесс мгновенно.
  • Забывают про таймаут - приложение зависает навсегда при зависшем запросе.
  • Закрывают ресурсы до завершения обработки запросов - гонки и паника.
  • Не используют signal.NotifyContext, а только signal.Notify - теряют возможность отменять контекст.
  • Останавливают HTTP-сервер через server.Close() вместо Shutdown - рвут активные соединения.
  • Не дожидаются завершения воркеров - теряют сообщения из очередей.
  • Игнорируют ошибку от Shutdown - не логируют проблемы при завершении.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью