Архитектура бэкенда
Зачем graceful shutdown: резкое убийство теряет работу в полёте
Ты катишь деплой. Оркестратор делает очевидное: поднимает новые поды и убивает старые. Убить под — значит послать его процессу сигнал и вскоре дожать его принудительно. Но твой старый под был в середине дел — запрос на оплату наполовину списал карту, три API-вызова ждали базу, дюжина вкладок браузера держала открытые keep-alive соединения. В тот миг, когда процесс умирает, всё это рвётся: недоделанные запросы не возвращают ничего, открытые сокеты сбрасываются, пользователь видит 502. С кодом всё было в порядке; проблема в том, что ты задеплоил. А деплоишь ты много раз в день. Умножь горстку потерянных запросов на каждый под, заменённый на каждом релизе, и «теряем пару запросов на каждом деплое» становится стабильным самоинфликтным процентом ошибок, который не скрывает полностью никакая логика ретраев ниже по стеку. Graceful shutdown — это дисциплина дать процессу умереть намеренно, по порядку — закончив начатое, прежде чем уйти.
Процесс не выбирает, когда умереть
В долгоживущем сервере можно вообразить, что процесс работает, пока сам не решит остановиться. В контейнерной платформе всё наоборот: решает платформа, и решает часто. Rolling-деплой заменяет каждый инстанс. Автоскейлер убирает мощность, когда трафик падает. Ноду дренируют на обслуживание, spot-инстанс отзывают, падающий по кругу сосед вынуждает перепланировать. Каждый из этих случаев заканчивается одной механикой — оркестратор говорит твоему процессу остановиться, а затем, если он не останавливается, убивает его наотмашь.
Наивный провал — считать эту остановку мгновенной. Если процесс просто выходит в тот миг, когда ему сказали, каждый обслуживаемый запрос умирает на полпути. Клиент не получает чистой ошибки, которую можно осмыслить; он получает connection reset или 502 Bad Gateway от прокси спереди, потому что апстрим исчез, пока ответ был ещё должен. Работа, что была в процессе — запись в базу, вызов оплаты, загрузка файла — остаётся в неизвестном состоянии.
Работа в полёте — это то, что ты защищаешь
Фраза, за которую держаться, — запрос в полёте (in-flight request): запрос, который сервер принял, но ещё не дозавершил отвечать. В любой загруженный момент их много. Graceful shutdown существует, чтобы дать этим запросам в полёте шанс завершиться, а не быть оборванными. Форма всегда одна — три хода:
- Перестать принимать новую работу. Закрыть дверь, чтобы свежий запрос не стартовал на процессе, который вот-вот умрёт.
- Дренировать то, что уже выполняется. Дать запросам в полёте закончиться и отправить ответы.
- Закрыть ресурсы и выйти. Когда работа сделана, освободить соединения по порядку и завершиться.
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 Оркестратор шлёт процессу сигнал остановиться (деплой, scale-down или дренаж ноды)
- 2 Процесс выходит немедленно, всё ещё держа запросы в полёте
- 3 Эти запросы оборваны посреди ответа; сокеты сброшены
- 4 Клиенты видят 502 и connection reset, коррелирующие с каждым релизом
- 01Почему резкий выход процесса вызывает потерю запросов и когда это происходит?
- 02Какие три хода graceful shutdown и почему это код приложения, а не только дело платформы?
Долгоживущий сервер не выбирает момент смерти; выбирает оркестратор, и делает это на каждом деплое, scale-down, дренаже ноды и отзыве spot — заканчивая каждый раз сигналом процессу, а затем принудительным kill’ом. Наивный баг — считать остановку мгновенной: выйти немедленно, и каждый запрос в полёте, уже принятый, но не отвеченный, оборван, так что клиент получает connection reset или 502, а любая запись или оплата в процессе остаётся в неизвестном состоянии. Поскольку эта потеря коррелирует с деплоями, а команды деплоят много раз в день, она становится стабильным самоинфликтным процентом ошибок, ограничивающим надёжность частотой релизов. Graceful shutdown чинит это тремя упорядоченными ходами — перестать принимать новое, дренировать запросы в полёте, чтобы закончились, затем закрыть ресурсы по порядку и выйти — зеркало fast-fail, защищающее тех, кто зависит от тебя, а не зависимость, от которой зависишь ты. И это код приложения: платформа лишь открывает окно и ждёт; лишь твой процесс знает, что в полёте. Следующий урок открывает это окно точно — сигналы, что шлёт оркестратор, grace period, что он ждёт, и классический баг, где сигнал вообще не доходит до твоего кода.