os.Exit(0) в обработчике SIGTERM выглядит безобидно — до первого дропнутого платежа. Разберём, как завершать сервис так, чтобы ни один запрос не потерялся.
Что происходит при SIGTERM
Оркестратор посылает процессу SIGTERM и даёт конечное время до SIGKILL. По умолчанию это 30 секунд. Всё, что мы не успели за это окно, будет оборвано на середине — включая открытую транзакцию в базе.
ctx, stop := signal.NotifyContext(
context.Background(), syscall.SIGTERM, syscall.SIGINT,
)
defer stop()
<-ctx.Done()
log.Info("получен сигнал, начинаем graceful shutdown")
shutdownCtx, cancel := context.WithTimeout(
context.Background(), 25*time.Second,
)
defer cancel()Порядок остановки
Порядок важнее скорости. Сначала перестаём принимать новое, потом доделываем начатое, и только затем закрываем ресурсы — соединения с БД, продюсеры, кеши.
Снимаемся с балансировщика
Первым делом /health начинает отвечать 503, и мы честно ждём два интервала readiness-пробы. Без этой паузы балансировщик продолжит слать трафик в уже закрывающийся под.
Дожидаемся активных запросов
func (s *Server) Shutdown(ctx context.Context) error {
s.mu.Lock()
s.closing = true // /health отвечает 503
s.mu.Unlock()
done := make(chan struct{})
go func() {
s.wg.Wait() // ждём активные запросы
close(done)
}()
select {
case <-done:
return nil
case <-ctx.Done():
return ctx.Err() // дедлайн вышел
}
}WaitGroup инкрементируется в middleware на входе в хендлер и декрементируется через defer. Это единственный источник правды о том, сколько запросов сейчас в работе.
Дедлайн и жёсткий выход
Дедлайн shutdown должен быть строго меньше terminationGracePeriodSeconds. Иначе процесс убьют в момент, когда он аккуратно закрывал пул соединений.
Чек-лист
- ✓readiness отвечает 503 сразу после сигнала
- ✓пауза на два интервала пробы перед закрытием листенера
- ✓WaitGroup на in-flight запросы и фоновые воркеры
- ✓дедлайн меньше grace period, ресурсы закрываются последними
- ✓метрика длительности shutdown в проде