Во время обновления экземпляра процесс получает сигнал завершения, но часть запросов ещё выполняется. Какой механизм предотвращает обрыв этих запросов в следующем фрагменте и почему его отсутствие вызывает ошибки?
def terminate(server):
server.close_listener()
server.exit()
Нужен механизм корректного завершения: экземпляр сначала прекращает принимать новые запросы, затем дожидается завершения уже выполняющихся и только после этого завершает процесс. В приведённом коде процесс выходит сразу после закрытия слушателя, поэтому активные запросы могут быть оборваны, а балансировщик ещё некоторое время способен направлять на экземпляр новые запросы.
Долгоживущие серверные процессы часто перезапускают при выкладке, масштабировании, обновлении конфигурации или восстановлении после сбоя. Простое завершение процесса не учитывает, что запросы имеют разную длительность и могут выполнять критические операции записи.
Подход с корректным завершением появился как практический способ отделить прекращение приёма новых работ от завершения уже начатых. Он снижает число ошибок, возникающих именно в переходных состояниях, когда экземпляр формально выводится из эксплуатации, но ещё обслуживает трафик.
После получения сигнала завершения экземпляр должен перестать получать новые запросы, но уже принятые запросы нельзя немедленно прерывать без последствий. Иначе клиент увидит сетевую ошибку или таймаут, а операция может остаться выполненной частично или завершиться повторно после ретрая.
Даже закрытие слушателя не гарантирует мгновенного исчезновения трафика: балансировщику и другим компонентам нужно время, чтобы узнать о смене состояния. Поэтому только закрыть порт недостаточно — экземпляр должен стать непригодным для маршрутизации и выдержать период отвода трафика.
Корректное завершение обычно состоит из таких этапов:
Минимальная модель выглядит так:
Тайм-аут обязательен: зависший запрос или недоступная зависимость не должны навсегда блокировать остановку экземпляра. Его выбирают с учётом максимальной ожидаемой длительности запроса, времени отвода трафика и ограничения на длительность операции.
Важно различать готовность и живость. Проверка готовности отвечает на вопрос, следует ли направлять на экземпляр новый трафик; проверка живости — способен ли процесс продолжать работу. При завершении обычно снимают готовность, но не обязательно немедленно делают проверку живости неуспешной: преждевременный перезапуск может оборвать drain.
Механизм не гарантирует сохранение каждого запроса. Принудительный останов, авария узла, сетевой разрыв или превышение тайм-аута всё равно могут привести к повтору операции. Поэтому критичные операции должны быть идемпотентными или защищёнными ключом дедупликации, а клиентские ретраи — согласованы с их семантикой.
При поэтапной выкладке API часть запросов длилась дольше обычного из-за обращения к внешней системе. Экземпляры завершались сразу после команды остановки, и во время каждой волны обновления появлялись ошибки на уже принятых запросах.
Рассматривались три варианта. Немедленное завершение было простым, но сохраняло обрывы. Фиксированная задержка перед остановкой уменьшала вероятность проблемы, однако не учитывала фактическое число активных запросов и либо была слишком короткой, либо замедляла выкладку. Ожидание активных запросов с тайм-аутом давало управляемое поведение, но требовало корректного учёта запросов и обработки зависших операций.
Выбрали третий вариант: сначала снимать экземпляр с готовности, затем прекращать приём и ждать drain с ограниченным тайм-аутом. Для операций записи добавили идемпотентные ключи. В результате ошибки, связанные именно с обновлением экземпляров, исчезли при штатном завершении, а зависшие запросы по-прежнему не могли заблокировать остановку навсегда.
Нет. Уже установленные соединения могут продолжать передавать новые запросы, а балансировщики могут использовать устаревшее состояние из-за задержки обнаружения и кэширования. Поэтому нужны явное снятие готовности, прекращение обработки новых запросов на уровне сервера и время для отвода трафика.
Часть трафика ещё будет направляться на завершаемый экземпляр после того, как он начнёт drain. Если процесс завершится раньше отвода, запросы оборвутся. Тайм-аут должен покрывать не только обычную длительность запросов, но и задержку обновления маршрутизации; при этом слишком большой тайм-аут увеличивает время выкладки и удерживает ресурсы.
Оно защищает только штатный сценарий остановки и не предотвращает аварийный отказ, потерю соединения или принудительное завершение по тайм-ауту. Клиент может не узнать, завершилась ли операция на сервере, и повторить её. Идемпотентность или дедупликация позволяют безопасно обработать такой повтор, особенно для операций создания, списания или публикации событий.