АрхитектураНадёжность и производительностьИнженер по надёжности платформы

Во время обновления экземпляра процесс получает сигнал завершения, но часть запросов ещё выполняется. Какой...

Во время обновления экземпляра процесс получает сигнал завершения, но часть запросов ещё выполняется. Какой механизм предотвращает обрыв этих запросов в следующем фрагменте и почему его отсутствие вызывает ошибки?

def terminate(server):
    server.close_listener()
    server.exit()
Проходите собеседования с ИИ помощником Hintsage

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

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

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

Долгоживущие серверные процессы часто перезапускают при выкладке, масштабировании, обновлении конфигурации или восстановлении после сбоя. Простое завершение процесса не учитывает, что запросы имеют разную длительность и могут выполнять критические операции записи.

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

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

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

Даже закрытие слушателя не гарантирует мгновенного исчезновения трафика: балансировщику и другим компонентам нужно время, чтобы узнать о смене состояния. Поэтому только закрыть порт недостаточно — экземпляр должен стать непригодным для маршрутизации и выдержать период отвода трафика.

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

Корректное завершение обычно состоит из таких этапов:

  1. Экземпляр публикует состояние не готов к приёму новых запросов.
  2. Балансировщики и сервис-дискавери исключают его из новых маршрутов; учитывается задержка распространения этого состояния.
  3. Сервер перестаёт принимать новые соединения или запросы.
  4. Экземпляр ждёт завершения активных запросов.
  5. По истечении ограниченного тайм-аута принудительно завершает оставшиеся работы.

Минимальная модель выглядит так:

def terminate(server): server.mark_not_ready() server.stop_accepting() drained = server.wait_for_inflight(timeout=30) if not drained: server.cancel_remaining() server.exit()

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

Важно различать готовность и живость. Проверка готовности отвечает на вопрос, следует ли направлять на экземпляр новый трафик; проверка живости — способен ли процесс продолжать работу. При завершении обычно снимают готовность, но не обязательно немедленно делают проверку живости неуспешной: преждевременный перезапуск может оборвать drain.

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

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

При поэтапной выкладке API часть запросов длилась дольше обычного из-за обращения к внешней системе. Экземпляры завершались сразу после команды остановки, и во время каждой волны обновления появлялись ошибки на уже принятых запросах.

Рассматривались три варианта. Немедленное завершение было простым, но сохраняло обрывы. Фиксированная задержка перед остановкой уменьшала вероятность проблемы, однако не учитывала фактическое число активных запросов и либо была слишком короткой, либо замедляла выкладку. Ожидание активных запросов с тайм-аутом давало управляемое поведение, но требовало корректного учёта запросов и обработки зависших операций.

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

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

  1. Достаточно ли закрыть слушатель, чтобы новые запросы больше не попадали на экземпляр?

Нет. Уже установленные соединения могут продолжать передавать новые запросы, а балансировщики могут использовать устаревшее состояние из-за задержки обнаружения и кэширования. Поэтому нужны явное снятие готовности, прекращение обработки новых запросов на уровне сервера и время для отвода трафика.

  1. Что произойдёт, если тайм-аут корректного завершения меньше времени распространения состояния готовности?

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

  1. Почему корректное завершение не устраняет необходимость идемпотентности?

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