Суть Читай реальные обработчики shutdown, манифест пода Kubernetes и последовательность drain, предсказывай сбой и выбирай самое рычажное исправление, которое senior сделает первым.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min
Баги shutdown прячутся в коде обработчика и манифесте пода, а не в прозе. Прочитай каждый сниппет, предскажи, где именно он теряет запрос, и выбери исправление, к которому senior потянется первым.
Цель
Отработай цикл, который ты проходишь в каждом ревью shutdown: проследи сигнал, порядок и дедлайн через реальный код и конфиг, затем найди единственное изменение, останавливающее потерю запросов.
Под реальной нагрузкой этот обработчик периодически зависает до SIGKILL, даже когда ни один запрос активно не выполняется. В чём дефект и исправление?
Heads-up Закрытие пула после drain HTTP-сервера — правильный порядок, а не deadlock. Зависание — это незакрытые простаивающие keep-alive сокеты, не дающие server.close() вызвать колбэк.
Heads-up process.exit выполняется только внутри колбэка close, после drain сервера — этот порядок в норме. Проблема в том, что колбэк не срабатывает из-за открытых keep-alive сокетов.
Heads-up SIGKILL нельзя поймать или обработать — обработчика SIGKILL не существует. SIGTERM корректен; баг — в drain keep-alive, а не в выборе сигнала.
Этот обработчик корректно проваливает readiness, но in-flight запросы всё равно падают с ошибками pool-closed и cache-closed во время drain. Что не так?
Heads-up Проваливать readiness первым корректно — это запускает часы deregistration. Баг — в datastores, закрывающихся до HTTP-слоя, который от них ещё зависит во время drain.
Heads-up Параллельный запуск делает хуже — он гарантирует, что пул закроется, пока запросы ещё дрейнируются. Лекарство — последовательный reverse-dependency order, HTTP первым.
Heads-up Нет фиксированного правила redis-потом-db; порядок — reverse-dependency. Оба datastore должны закрываться после того, как HTTP-сервер сдрейнировал запросы, которые их используют.
preStop sleep и grace period выглядят нормально, но во время shutdown поды иногда убиваются и перезапускаются посреди drain. Что в этом манифесте это вызывает?
Heads-up 5s preStop комфортно помещаются в бюджет 30s. Перезапуск идёт от общего эндпоинта probe, делающего liveness проваленной, а не от слишком короткого grace period.
Heads-up preStop выполняется до SIGTERM, и оркестратор на нём блокируется; SIGTERM всё равно доставляется после. Дефект — общий эндпоинт liveness/readiness.
Heads-up Проваливать readiness во время termination — это ровно то, как останавливают новый трафик; это обязательно. Баг — liveness, делящая этот эндпоинт и триггерящая перезапуск.
Сниппет 4 — guardian timeout и requeue
process.on("SIGTERM", async () => { worker.stopPulling(); // прекратить брать новые задачи const t = setTimeout(() => { log.error("drain timed out, forcing exit"); process.exit(1); }, 25_000); // guardian < grace period await worker.finishOrRequeue(); // ack завершённых, requeue остальных clearTimeout(t); process.exit(0);});
Викторина
Completed
Guardian timeout и логика requeue структурно корректны. Какое единственное оставшееся предусловие делает worker.finishOrRequeue() безопасным и что ломается без него?
Heads-up Guardian должен быть меньше grace period, чтобы force-exit на своих условиях до SIGKILL. Равенство не оставляет запаса и рискует SIGKILL посреди flush. И это не решает безопасность requeue.
Heads-up Сначала нужно прекратить брать новые задачи — иначе новая работа продолжает приходить на умирающий воркер и никогда не завершается. Порядок тут корректен; недостающая часть — идемпотентность.
Heads-up Ненулевой выход при форсированном/таймаутном drain нормален — он сигнализирует аномальный shutdown для алертов. Реальное предусловие — идемпотентность requeue-задач, независимо от кода выхода.
Итог
Каждый баг shutdown читается в обработчике и манифесте: один server.close() зависает на простаивающих keep-alive сокетах, поэтому закрывай их явно; teardown должен идти в reverse dependency order с datastores последними, иначе in-flight queries бьются в мёртвый пул; liveness и readiness не должны делить эндпоинт, иначе провал readiness для drain заодно триггерит перезапуск; а guardian timeout покупает чистый выход на своих условиях, пока requeue остаётся корректным только при идемпотентном consumer. Проследи сигнал, порядок и дедлайн через код, исправь единственную строку, теряющую запрос, и перепроверь под нагрузкой.