Олег Ильин oo-ilin.ru
← Все статьи

Graceful shutdown сервиса на Go под нагрузкой

Олег Ильин Олег Ильин · 12 мар 2026 · 8 мин

os.Exit(0) в обработчике SIGTERM выглядит безобидно — до первого дропнутого платежа. Разберём, как завершать сервис так, чтобы ни один запрос не потерялся.

Что происходит при SIGTERM

Оркестратор посылает процессу SIGTERM и даёт конечное время до SIGKILL. По умолчанию это 30 секунд. Всё, что мы не успели за это окно, будет оборвано на середине — включая открытую транзакцию в базе.

os.Exit(0) прямо в обработчике сигнала обрывает in-flight запросы. В платёжном сервисе это потерянные транзакции и разъехавшаяся реконсиляция.
main.gogo
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-пробы. Без этой паузы балансировщик продолжит слать трафик в уже закрывающийся под.

Дожидаемся активных запросов

server.gogo
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. Иначе процесс убьют в момент, когда он аккуратно закрывал пул соединений.

Практика: grace period 30s, дедлайн shutdown 25s, из них 5s — на readiness-паузу.

Чек-лист

  • ✓readiness отвечает 503 сразу после сигнала
  • ✓пауза на два интервала пробы перед закрытием листенера
  • ✓WaitGroup на in-flight запросы и фоновые воркеры
  • ✓дедлайн меньше grace period, ресурсы закрываются последними
  • ✓метрика длительности shutdown в проде
Не бойтесь потерпеть неудачу. Бойтесь не попробовать.

Читать дальше