Архитектура бэкенда
Работа в полёте: длинные запросы, фоновые джобы и дедлайн
Логика дренажа верна, порядок разбора правильный, страховочный таймаут взведён — и всё это работает прекрасно для запросов, что заканчиваются за пару сотен миллисекунд. Затем ты смотришь, что реально крутится на этом сервисе: CSV-экспорт, что стримит девяносто секунд, воркер транскода видео, что жуёт одну джобу четыре минуты, построитель отчётов на полпути записи строк. Grace period — тридцать секунд. Ни одно из этого не успеет вовремя, и «дай работе в полёте сдренироваться» — правило, что построили прошлые три урока — тихо становится «жди работу, что никогда не закончится до дедлайна, затем всё равно получи SIGKILL посреди записи». Прошлые уроки предполагали, что работа в полёте влезает в окно. Сеньорная реальность — часть не влезает, и притворяться, что влезет, — вот как ты теряешь полузаписанный отчёт и оплату, что была списана, но не записана. Так что вопрос меняет форму: не «как дренировать?», а «что делать с работой, которую дедлайн обрежет?» — и ответ зависит от того, можно ли эту работу безопасно переделать.
Grace period — это бюджет, а не гарантия
Переосмысли grace period как бюджет дедлайна, который ты тратишь, а не отрезок времени, что тебе дан. Всё окно — terminationGracePeriodSeconds, по умолчанию 30s — должно покрыть preStop-сон (распространение, ~5–15s), дренаж keep-alive, разбор ресурсов и запас, что резервирует страховочный таймаут. Что остаётся на собственно дозавершение работы в полёте — это бюджет минус всё это, часто заметно меньше двадцати секунд. Любая единица работы, чьё оставшееся время выполнения превышает этот остаточный бюджет, не закончится, точка. Ошибка — считать grace period «уймой времени» и await’ить всё; сеньорный ход — знать распределение длительности своей работы и принять, что длинный хвост структурно недренируем. Ты можешь поднять grace period для сервиса, где доминируют длинные запросы, но не можешь поднять его без предела — оркестратор, дренаж ноды, дедлайн отзыва spot всё его ограничивают — так что для по-настоящему длинной работы больше времени — не ответ. Ответ — другая диспозиция.
Два рода работы, две диспозиции
Работа, что не влезет, делится на два случая, и они обрабатываются по-разному:
- Длинные синхронные запросы (стриминговый экспорт, медленная загрузка). Ты не можешь вернуть HTTP-запрос в очередь — есть клиент, держащий сокет. Так что выбор: дать ему закончиться, если он близко, или отклонить его чисто, чтобы клиент мог переретраить на здоровый инстанс. Чистое отклонение — это
503 Service Unavailableс заголовкомRetry-After, или отказ стартовать новые длинные операции, когда выключение началось. Кардинальный грех — молча оборвать его посреди стрима, что даёт клиенту connection reset, который он не может осмыслить. - Фоновые джобы (воркер очереди посреди задачи). Здесь у тебя есть реальный вариант, которого нет у HTTP: вернуть в очередь (requeue). Перестань тянуть новые джобы в тот миг, как началось выключение, и для джобы в руках либо закончи её, если влезает, либо отпусти обратно в очередь, чтобы другой воркер подхватил. Большинство очередей делает это за тебя: если воркер умирает без подтверждения (ack), visibility timeout сообщения истекает, и брокер передоставляет его. Эта передоставка — страховочная сетка, но она же и ловушка.
Возврат — at-least-once, поэтому требует идемпотентности
Вот где этот юнит сталкивается с юнитом про идемпотентность, и столкновение — вся суть. Возврат — делаешь ли ты его явно или даёшь visibility timeout сделать — это at-least-once доставка: джоба может выполниться снова с начала. Если воркер был убит после списания карты, но до ack сообщения, передоставленная джоба списывает карту снова. Возврат безопасен, только когда консьюмер идемпотентен — когда выполнение джобы второй раз даёт то же конечное состояние, что и выполнение раз, через ключ идемпотентности, таблицу inbox/dedup или машину состояний, что становится no-op на уже применённой работе. Это не опциональная полировка; это предусловие, что делает возврат корректным, а не генератором двойного списания. Для длинных джоб чекпоинтинг смягчает цену: периодически персисти прогресс, чтобы передоставка возобновилась около места остановки, а не переделывала минуты работы — но чекпоинтинг всё равно полагается на то, что каждый шаг безопасно переприменить. Правило складывается: дренируй то, что влезает, возвращай то, что не влезает, и возвращай только работу, что доказуемо безопасна для выполнения дважды.
Почему это работает
Почему «просто закончи джобу, очередь подождёт» — неверный инстинкт, даже когда очередь и правда подождала бы? Потому что ограничение, что кусает, — не терпение очереди, а дедлайн процесса, и это двое разных часов, принадлежащих двум разным системам. Брокер рад держать сообщение, и даунстрим рад принять запись, когда бы она ни пришла; что не радо — оркестратор, который SIGKILL’ит твой воркер на границе grace period независимо от того, сколько остальная система терпела бы ожидание. Так что воркер, говорящий «я просто закончу эту четырёхминутную джобу», даёт обещание, которое не имеет полномочий сдержать: kill идёт по графику платформы, а не джобы. В тот миг, как он приземляется посреди записи, ты в худшем состоянии из всех — частичная работа применена, ack не послан, и передоставка уже в очереди за тобой. Эта передоставка тебя спасает, но только если переделка безопасна; если нет — дедлайн платформы только что произвёл дубликат из совершенно корректно выглядящего воркера. Вот почему диспозицию надо решать до дедлайна, а не обнаруживать на нём: ты выбираешь заранее «эта джоба идемпотентна и может быть передоставлена» или «этот запрос слишком длинный, отвергай новые во время выключения», и обработчик graceful shutdown обеспечивает этот выбор. Дедлайн неотменяем и внешен; единственное, что ты контролируешь, — переживёт ли работа, которую он прерывает, прерывание. Идемпотентность — то, что превращает прерванную джобу из порчи данных в безвредный ретрай — ровно поэтому два юнита соединены в бедре.
| Род работы | Влезает в бюджет? | Диспозиция | Предусловие безопасности |
|---|---|---|---|
| Короткий запрос | Да | Дренировать — дать закончиться и ответить | Нет |
| Длинный запрос | Нет | Отвергать новые: 503 + Retry-After | Клиент ретраит в другом месте |
| Фоновая джоба (влезает) | Да | Закончить, затем ack | Нет |
| Фоновая джоба (слишком длинная) | Нет | Возврат / дать visibility timeout передоставить | Консьюмер должен быть идемпотентен |
| Длинная джоба, частичный прогресс | Нет | Чекпоинт, затем возврат для возобновления | Каждый шаг безопасно переприменить |
Воркер очереди четыре минуты в джобе, когда приходит SIGTERM, а grace period — 30s. Какова верная диспозиция?
Почему возврат фоновой работы во время выключения требует, чтобы консьюмер был идемпотентен?
Расставь, как воркер обрабатывает работу в полёте, когда приходит SIGTERM:
- 1 Перестать тянуть новые джобы и отказаться стартовать новые длинные запросы (503 + Retry-After)
- 2 Дать работе, что влезает в остаточный бюджет, закончиться и подтвердить её
- 3 Сделать чекпоинт частичного прогресса на длинных джобах, затем отпустить их обратно в очередь
- 4 Полагаться на идемпотентных консьюмеров, чтобы любая передоставленная джоба была безопасна для повторного выполнения
- 01Почему grace period это бюджет, а не уйма времени, и какая работа не влезет?
- 02Какова диспозиция для длинных запросов против фоновых джоб и почему возврат требует идемпотентности?
Механика дренажа прошлых трёх уроков предполагала, что работа в полёте влезает в окно; сеньорная реальность — часть не влезает, так что grace period лучше читать как бюджет дедлайна — дефолтные 30s минус preStop-сон, дренаж keep-alive, разбор и запас страховки — оставляя часто меньше двадцати секунд, и любая джоба, чьё оставшееся время превышает это, просто не закончится. Длинные синхронные запросы нельзя вернуть в очередь, так что заканчивай их, если близко, или отвергай чисто с 503 + Retry-After, а не обрывай стрим; фоновые джобы можно вернуть, перестав тянуть новые и отпустив остальное, или дав visibility timeout передоставить. Но передоставка — at-least-once, так что джоба, убитая после частичной записи, выполняется снова с начала — что делает идемпотентность предусловием, превращающим прерванную джобу из порчи данных в безвредный ретрай, ровно дисциплину, что построил юнит об идемпотентности, с чекпоинтингом, чтобы срезать время переделки. Решай диспозицию до дедлайна, потому что kill внешен, и единственное, что ты контролируешь, — переживёт ли прерванная работа прерывание. Механика юнита теперь полна для одного инстанса; финальный урок отдаляется к флоту, где graceful shutdown становится свойством всего rolling-деплоя, а не одного процесса.