АрхитектураОблако и инфраструктураИнженер по платформе Kubernetes

Разберите ситуацию: при обновлении оркестратор удаляет экземпляр, но часть запросов обрывается. Как организ...

Разберите ситуацию: при обновлении оркестратор удаляет экземпляр, но часть запросов обрывается. Как организовать корректное завершение контейнера, чтобы уже принятые запросы успели завершиться?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Нужно реализовать graceful shutdown: перед остановкой экземпляр перестаёт принимать новые запросы, но продолжает обслуживать уже начатые. Оркестратор должен сначала исключить его из маршрутизации, затем отправить приложению сигнал завершения и предоставить ограниченное время на освобождение ресурсов.

Исторический контекст

Контейнеры и оркестраторы делают экземпляры заменяемыми: при обновлении, масштабировании или сбое старые экземпляры могут быть быстро удалены. Простое принудительное завершение процесса приводило к обрывам HTTP-запросов, незавершённым транзакциям и потере сообщений.

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

Постановка проблемы

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

Риск особенно высок для долгих запросов, потоковой передачи, WebSocket-соединений и обработчиков, изменяющих внешнее состояние. Увеличение времени остановки само по себе не решает проблему: пока экземпляр остаётся доступным для новых запросов, он может постоянно получать новую работу.

Подробное решение

Сначала экземпляр переводят в состояние, при котором он больше не считается готовым для новых запросов. В Kubernetes это обычно достигается изменением состояния готовности Pod и удалением его из соответствующих EndpointSlice; конкретному балансировщику нужно время, чтобы получить и применить это изменение.

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

В Kubernetes период задаётся параметром terminationGracePeriodSeconds; его значение должно учитывать максимальную ожидаемую длительность запроса и запас на закрытие ресурсов. Хук preStop может помочь начать draining, но он не заменяет обработчик сигнала и потребляет часть общего периода завершения.

Нужна защита от гонки между исключением экземпляра из маршрутизации и фактическим прекращением процесса. На практике используют небольшой запас времени после перехода в состояние неготовности, однако его нельзя считать универсальной гарантией: задержки зависят от kube-proxy, ingress-контроллера, внешнего балансировщика и клиентских соединений.

Есть компромисс: слишком короткий период ускоряет обновление, но увеличивает число оборванных запросов; слишком длинный уменьшает риск потерь, но замедляет rollout и удерживает ресурсы. Для долгих запросов дополнительно применяют ограничения времени выполнения, идемпотентность операций, повторяемость сообщений и отдельные правила закрытия WebSocket или потоковых соединений.

Ситуация из практики

Сервис обработки отчётов обновлялся в Kubernetes. Во время rollout обычные запросы проходили успешно, но пользователи иногда получали разрыв соединения при скачивании больших отчётов, потому что старый Pod завершался сразу после отправки сигнала остановки.

Рассматривались три варианта. Принудительно увеличить период завершения было просто, но не запрещало новые запросы и лишь дольше удерживало Pod. Полностью отключить автоматические обновления устраняло обрывы во время rollout, но ухудшало скорость доставки исправлений. Реализовать draining в приложении и согласовать его с readiness-пробой было сложнее, зато решало корневую проблему.

Выбрали третий вариант: при завершении сервис переводил себя в состояние неготовности, прекращал принимать новые задания, ожидал активные скачивания не дольше установленного лимита и только потом закрывал соединения. Для запросов, превысивших лимит, добавили возобновление скачивания по диапазонам. В результате обычные обновления перестали обрывать активные передачи, а зависшие операции не блокировали rollout бесконечно.

Что кандидаты часто упускают

1. Достаточно ли отправить процессу SIGTERM, чтобы запросы не обрывались?

Нет. Сигнал лишь сообщает процессу о необходимости завершиться; он не удаляет экземпляр из маршрутизации и не гарантирует, что приложение его обработает. Нужны обработчик завершения, прекращение приёма новых запросов и корректно настроенный период ожидания.

2. Гарантирует ли удаление Pod из EndpointSlice, что новый трафик больше не попадёт на него?

Нет, это уменьшает вероятность новой маршрутизации, но не устраняет её мгновенно. Компоненты сети распространяют изменения с задержкой, а уже установленные TCP-соединения могут продолжаться независимо от списка конечных точек. Поэтому необходимы draining в приложении и запас времени, а для внешних балансировщиков нужно учитывать их собственную модель удаления backend-узлов.

3. Что произойдёт, если активный запрос длится дольше terminationGracePeriodSeconds?

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