awesome-everything EN
↑ Обратно к восхождению

Архитектура бэкенда

Зачем graceful shutdown: резкое убийство теряет работу в полёте

Суть Процесс редко завершается на своих условиях — оркестратор шлёт сигнал убийства на каждом деплое. Резкий выход рвёт запросы в полёте и сбрасывает живые соединения; graceful shutdown перестаёт принимать новое, дренирует текущее, затем выходит чисто.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на junior-высоте — поверхность
◷ 12 min

Ты катишь деплой. Оркестратор делает очевидное: поднимает новые поды и убивает старые. Убить под — значит послать его процессу сигнал и вскоре дожать его принудительно. Но твой старый под был в середине дел — запрос на оплату наполовину списал карту, три API-вызова ждали базу, дюжина вкладок браузера держала открытые keep-alive соединения. В тот миг, когда процесс умирает, всё это рвётся: недоделанные запросы не возвращают ничего, открытые сокеты сбрасываются, пользователь видит 502. С кодом всё было в порядке; проблема в том, что ты задеплоил. А деплоишь ты много раз в день. Умножь горстку потерянных запросов на каждый под, заменённый на каждом релизе, и «теряем пару запросов на каждом деплое» становится стабильным самоинфликтным процентом ошибок, который не скрывает полностью никакая логика ретраев ниже по стеку. Graceful shutdown — это дисциплина дать процессу умереть намеренно, по порядку — закончив начатое, прежде чем уйти.

Процесс не выбирает, когда умереть

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

Наивный провал — считать эту остановку мгновенной. Если процесс просто выходит в тот миг, когда ему сказали, каждый обслуживаемый запрос умирает на полпути. Клиент не получает чистой ошибки, которую можно осмыслить; он получает connection reset или 502 Bad Gateway от прокси спереди, потому что апстрим исчез, пока ответ был ещё должен. Работа, что была в процессе — запись в базу, вызов оплаты, загрузка файла — остаётся в неизвестном состоянии.

Работа в полёте — это то, что ты защищаешь

Фраза, за которую держаться, — запрос в полёте (in-flight request): запрос, который сервер принял, но ещё не дозавершил отвечать. В любой загруженный момент их много. Graceful shutdown существует, чтобы дать этим запросам в полёте шанс завершиться, а не быть оборванными. Форма всегда одна — три хода:

  1. Перестать принимать новую работу. Закрыть дверь, чтобы свежий запрос не стартовал на процессе, который вот-вот умрёт.
  2. Дренировать то, что уже выполняется. Дать запросам в полёте закончиться и отправить ответы.
  3. Закрыть ресурсы и выйти. Когда работа сделана, освободить соединения по порядку и завершиться.

Fast-fail из юнита про circuit breaker был про отклонение вызовов к больной зависимости. Graceful shutdown — зеркальное отражение: он про то, чтобы не бросать тех, кто зависит от тебя, пока ты уходишь. Оба — формы чистого отказа вместо громкого.

Почему это забота бэкенда, а не только ops

Соблазнительно подшить shutdown под «инфраструктуру» — мол, дело платформы. Но платформа умеет лишь послать сигнал и ждать; она не знает, какие запросы в полёте, в каком порядке должны закрываться твои ресурсы и когда уйти по-настоящему безопасно. Это знание живёт в твоём процессе. Оркестратор даёт тебе окно; что ты делаешь внутри него — это код приложения. Сервис, который игнорирует сигнал и получает kill, теряет запросы на каждом деплое, как бы хорош ни был кластер.

Почему это работает

Почему резкий выход настолько хуже, чем звучит — наверняка терять запрос-другой во время деплоя пренебрежимо? Потому что потеря — не случайный фоновый шум; она коррелирует с твоими собственными действиями и масштабируется с ними. Ты не роняешь запросы, когда система спокойна и простаивает; ты роняешь их именно когда деплоишь, а современные команды деплоят постоянно — много раз в день, часто автоматически. Каждый раскат заменяет каждый инстанс во флоте, и каждый заменённый инстанс рвёт то, что обслуживал в тот миг, так что всплеск ошибок ложится поверх момента, когда ты ещё и вводишь новый код, и становится мучительно отличать настоящую регрессию от шума, наведённого деплоем. Ошибки к тому же дорогого сорта: запрос в полёте, умерший посреди записи, может оставить оплату списанной, но заказ не записанным, или полуприменённое состояние, которое требует сверки, а не пустой страницы. И поскольку отказ — сырой connection reset, а не структурированная ошибка, клиент часто не может понять, случилась ли работа, так что ретрай может её удвоить. Кумулятивный эффект — сервис, чьё число надёжности тихо ограничено его же процессом релиза: ты никогда не можешь быть доступнее, чем позволяют твои деплои, — вот почему graceful shutdown считается обязательной базой, а не полировкой.

Резкое убийствоGraceful shutdown
Новые запросыЧасть стартует и умирает на полпутиОтклонены рано; не стартуют
Запросы в полётеОборваны, возвращают reset/502Дают закончиться и ответить
Открытые соединенияСброшены без предупрежденияЗакрыты чисто, переподключиться в другом месте
Состояние ресурсовБрошено посреди операции, неизвестноЗакрыто по порядку после дренажа
Опыт клиентаConnection reset, неоднозначный ретрайЧистый ответ или чистая ошибка
Цена деплояВсплеск ошибок на каждом релизеНевидим для пользователей
Викторина

Сервис выходит в тот же миг, как оркестратор велит остановиться. Во время деплоя пользователи видят всплеск 502 и connection reset. Почему?

Викторина

Какие три хода graceful shutdown, по порядку?

Расставь шаги по порядку

Расставь, что происходит при резком выходе процесса вместо дренажа:

  1. 1 Оркестратор шлёт процессу сигнал остановиться (деплой, scale-down или дренаж ноды)
  2. 2 Процесс выходит немедленно, всё ещё держа запросы в полёте
  3. 3 Эти запросы оборваны посреди ответа; сокеты сброшены
  4. 4 Клиенты видят 502 и connection reset, коррелирующие с каждым релизом
Вспомните перед уходом
  1. 01
    Почему резкий выход процесса вызывает потерю запросов и когда это происходит?
  2. 02
    Какие три хода graceful shutdown и почему это код приложения, а не только дело платформы?
Итог

Долгоживущий сервер не выбирает момент смерти; выбирает оркестратор, и делает это на каждом деплое, scale-down, дренаже ноды и отзыве spot — заканчивая каждый раз сигналом процессу, а затем принудительным kill’ом. Наивный баг — считать остановку мгновенной: выйти немедленно, и каждый запрос в полёте, уже принятый, но не отвеченный, оборван, так что клиент получает connection reset или 502, а любая запись или оплата в процессе остаётся в неизвестном состоянии. Поскольку эта потеря коррелирует с деплоями, а команды деплоят много раз в день, она становится стабильным самоинфликтным процентом ошибок, ограничивающим надёжность частотой релизов. Graceful shutdown чинит это тремя упорядоченными ходами — перестать принимать новое, дренировать запросы в полёте, чтобы закончились, затем закрыть ресурсы по порядку и выйти — зеркало fast-fail, защищающее тех, кто зависит от тебя, а не зависимость, от которой зависишь ты. И это код приложения: платформа лишь открывает окно и ждёт; лишь твой процесс знает, что в полёте. Следующий урок открывает это окно точно — сигналы, что шлёт оркестратор, grace period, что он ждёт, и классический баг, где сигнал вообще не доходит до твоего кода.

Связанные уроки
Продолжить восхождение ↑Сигналы и grace period: SIGTERM, SIGKILL и PID 1
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.