1. Главная
  2. Статьи
  3. Graceful shutdown в Go: как остановить сервис и не потерять запросы

Graceful shutdown в Go: как остановить сервис и не потерять запросы

Артём Романович 3 мин чтения
GocontextKubernetesbackendmiddle
Содержание
  1. Какие сигналы приходят процессу
  2. Останавливаем HTTP-сервер
  3. Дожидаемся воркеров
  4. Порядок остановки
  5. Чек-лист перед продом

Сервис получает SIGTERM при каждом деплое. Если в этот момент он просто завершает процесс, клиенты получают обрывы соединений, а воркеры бросают задачи на середине. Graceful shutdown решает это за 30 строк кода.

Разберём, как поймать сигнал, остановить HTTP-сервер, дождаться воркеров и не упереться в таймаут Kubernetes. Этот вопрос регулярно всплывает на собесах на Middle.

Какие сигналы приходят процессу

Оркестратор сначала вежливо просит процесс завершиться, а потом убивает его. Перехватить можно только первые два сигнала.

СигналКто шлётЧто делать
SIGINTCtrl+C в терминалеGraceful shutdown
SIGTERMKubernetes, DockerGraceful shutdown
SIGKILLKubernetes после таймаутаПерехватить нельзя

Останавливаем HTTP-сервер

С Go 1.16 сигнал удобно превращать в отмену контекста через signal.NotifyContext. Метод Shutdown перестаёт принимать новые соединения и ждёт, пока текущие запросы завершатся.

main.go
func main() {
	ctx, stop := signal.NotifyContext(context.Background(),
		syscall.SIGINT, syscall.SIGTERM)
	defer stop()
 
	srv := &http.Server{Addr: ":8080", Handler: router()}
 
	go func() {
		err := srv.ListenAndServe()
		if err != nil && !errors.Is(err, http.ErrServerClosed) {
			log.Fatalf("listen: %v", err)
		}
	}()
 
	<-ctx.Done() // ждём сигнал
 
	shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
 
	if err := srv.Shutdown(shutdownCtx); err != nil {
		log.Printf("shutdown: %v", err)
	}
}

Дожидаемся воркеров

HTTP-сервер — не единственное, что работает в сервисе. Консьюмеры очередей и фоновые воркеры тоже должны закончить текущую задачу. Удобнее всего собрать их в errgroup с общим контекстом.

pool.go
func (p *Pool) Run(ctx context.Context) error {
	g, ctx := errgroup.WithContext(ctx)
	for i := 0; i < p.size; i++ {
		g.Go(func() error {
			for {
				select {
				case <-ctx.Done():
					return nil // дочитали текущую задачу и вышли
				case job := <-p.jobs:
					p.handle(job)
				}
			}
		})
	}
	return g.Wait()
}

Порядок остановки

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

  1. Снимаем readiness-пробу, чтобы балансировщик перестал слать трафик.
  2. Останавливаем HTTP и gRPC-серверы через Shutdown / GracefulStop.
  3. Отменяем контекст воркеров и ждём g.Wait().
  4. Закрываем пул соединений с PostgreSQL, Redis и продюсер Kafka.

Чек-лист перед продом

  • Таймаут Shutdown меньше terminationGracePeriodSeconds.
  • Воркеры слушают ctx.Done() и не берут новые задачи после отмены.
  • Логируем начало и конец остановки, чтобы видеть в Grafana, сколько она длится.
  • Проверили локально: kill -TERM во время нагрузочного теста не даёт 5xx.

Формат работы

Выбери подходящий формат — от прожарки резюме до полного сопровождения до оффера.

Составление резюме

Адаптирую твой опыт, оформим достижения, добавлю ключевые фразы под ATS — робота, который отсеивает резюме до того, как их увидит живой рекрутер. Будем улучшать до результата.

19 000 ₽

Разовая услуга

  • Адаптация опыта под вакансии
  • ATS-оптимизация
  • Улучшаем резюме, пока HR не начинают писать каждый день
Записаться

Бесплатная консультация

Не уверен, сработаемся ли? Запишись на 20-минутный созвон — бесплатно проконсультирую и отвечу на все вопросы.

Бесплатно

20 минут созвон

  • Оценка текущего уровня
  • Рекомендации по развитию
  • Ответы на все вопросы
Записаться

Бесплатная консультация — 20 мин

Обсудим твой уровень, цели и составим предварительный план развития. Без обязательств.

1

Напиши мне

2

Созвон 20 мин

3

Получи план

Записаться в Telegram