RSS Telegram YouTube Apple Яндекс Spotify Amazon Почта

101. Graceful Shutdown

11.05.2025

Скачать

К списку выпусков

Ссылки выпуска:

Graceful Shutdown или Плавное Выключение

Ключевые меры для плавной остановки:

Как работает Docker-рантайм?

А как работают сигналы? В ядре находится очередь сигналов и за каждым процессом закреплены обработчики сигналов. Если необходимо, то сам процесс должен обратиться к ядру с запросом на регистрации обработчика того или иного сигнала ОС. Не для всех сигналов можно зарегистрировать обработчик, для SIGKILL (9) и SIGSTOP (19) этого сделать нельзя, поэтому остановка процесса будет происходить всегда. А вот сигналы SIGINT (2), когда нажимаем Ctrl+C или SIGTERM (15) (вежливое завершение). Часто мы встречаемся с сигналом SIGHUP (1), изначально посылаемый при отключение терминала, но часто используемый для перезагрузки конфигурационных файлов.

Для того чтобы поймать сигнал в Go как в примере ниже создается канал единичного размера:

func main() {
    signalChan := make(chan os.Signal, 1)
    signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)

    // Setup work here

    <-signalChan

    fmt.Println("Received termination signal, shutting down...")
}

Однако, стоит учитывать, что можно принять много сигналов одного типа и эти сигналы, как уже было сказано, выстраиваются в очередь. Ядро ждет выполнения обработчика сигнала до того как вызывать его еще раз для следующего сигнала в очереди.

А как работает рантайм? Docker шлёт SIGTERM, ждет 10 секунд (значение по-умолчанию) и посылает SIGKILL. Если это Kubernetes, то он похоже расширяет graceful period до 30 секунд или около того. В случае K8s также стоит учитывать, что есть ingress, который может продолжать слать трафик вашему сервису, поэтому нужно сообщить ему, что этого делать не нужно через readinessProbe.

Наконец, мы можем мягко положить сервер:

ctx, cancelFn := context.WithTimeout(context.Background(), timeout)
err := server.Shutdown(ctx)

Либо завершатся все рутины обработчиков запросов, либо сработает таймаут - только после этого выполнение Shutdown() завершится. Возможно, захочется, чтобы рутины тоже узнали о том, что сервер завершает свою работу, для этого есть два варианта: обогащение контекста запроса через middleware и глобальный контекст HTTP-сервера (поле BaseContext структуры http.Server). Конечно, важно, чтобы работающие рутины учитывали возможно закрытый контекст.

Последний шаг - освобождение ресурсов! Без этого СУБД или Message Broker или кеш-сервер должны полагаться на таймауты, чтобы освободить ресурсы, а это может стать причиной проблем в будущем, когда понадобится обрабатывать бОльшее количество запросов.

Ошибка в написании обработчика сообщений брокера

Я недавно осознал ошибку, допущенную при написании обарботчика сообщений от брокера сообщений. В дашборде RabbitMQ я заметил, что количество консумеров равно 0, но процессы были запущены. Оказалось, что я написал обработку таким образом, что каждое сообщение очереди я обрабатываю в бесконечном цикле с ретраями. Когда возникает проблема с обработкой сообщения, то я просто делаю ретрай с exp back-off стратегией. Через некоторое время по таймаутам брокера сообщений соединение с консумером разрывается. Правильно делать fail fast без циклов! Т.е. считали сообщение, попытались обработать. Если успех, то шлем ACK, если нет - NACK и возвращаем сообщение в очередь до следующей попытки.

Интересная идея: автодокументация Rest API сервисов