Ключевые меры для плавной остановки:
Как работает 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 и возвращаем сообщение в очередь до следующей попытки.