中文翻译暂不可用,显示俄语原文。
Как вы проектируете graceful shutdown для LLM serving pod в Kubernetes?
Краткий тезис
Graceful shutdown — это процесс корректного завершения работы пода, при котором не теряются in-flight запросы, не нарушаются SLA и не возникают ошибки у клиентов. Для LLM serving, где инференс может длиться десятки секунд, критично правильно настроить PreStop hook, readiness probe, таймауты и обработку сигналов SIGTERM/SIGKILL. Без этого жёсткое завершение пода приводит к сбоям, повторным вычислениям и ухудшению пользовательского опыта.
1. Термин: Graceful shutdown (корректное завершение)
Graceful shutdown — это процедура, при которой приложение (в нашем случае LLM serving pod) завершает работу поэтапно:
- перестаёт принимать новые запросы;
- дожидается завершения уже выполняющихся запросов (drain);
- корректно освобождает ресурсы (GPU-память, соединения, кэш);
- только после этого завершает процесс.
В Kubernetes этот процесс управляется через Pod lifecycle: после получения команды на удаление под переходит в состояние Terminating. В этом состоянии:
- Endpoint controller удаляет под из Service (новые запросы не направляются);
- срабатывает PreStop hook (если задан);
- через terminationGracePeriodSeconds (по умолчанию 30 секунд) отправляется SIGTERM, а затем SIGKILL.
Для LLM serving, где время одного инференса может составлять 5–60 секунд, стандартных 30 секунд часто недостаточно. Необходимо увеличивать terminationGracePeriodSeconds и реализовывать собственный механизм drain.
2. Проблемы при жёстком завершении LLM serving pod
Если под завершается принудительно (SIGKILL), возникают следующие проблемы:
| Проблема | Последствия |
|---|---|
| Потеря in-flight запросов | Клиент получает 5xx (502/503), запрос нужно повторять |
| Повреждение состояния | Если модель использует кэш (KV-cache), он теряется; при следующем запуске — холодный старт |
| Нарушение SLI/SLO | Увеличивается latency, падает availability |
| Неосвобождённая GPU-память | Может привести к утечкам на узле, проблемы с другими подами |
| Ошибки в батчинге | Если модель обрабатывает батчи, часть запросов может быть потеряна |
Пример: vLLM при получении SIGKILL может не успеть завершить текущий батч, и все запросы в нём будут потеряны. Клиенты увидят таймаут.
3. Основные компоненты graceful shutdown в Kubernetes
- PreStop hook — команда или HTTP-запрос, выполняемый перед отправкой SIGTERM. Используется для инициации drain.
- Readiness probe — проверка готовности пода принимать трафик. При shutdown readiness probe должна возвращать 503, чтобы под был исключён из Service.
- terminationGracePeriodSeconds — время, которое Kubernetes ждёт после PreStop hook и SIGTERM до принудительного SIGKILL.
- Обработка сигналов — приложение должно перехватывать SIGTERM и запускать процедуру корректного завершения.
4. PreStop hook: остановка принятия новых запросов
Первый шаг graceful shutdown — перевести под в состояние «не готов» и перестать принимать новые запросы.
Как это работает
- Kubernetes вызывает PreStop hook (например, HTTP GET на
/shutdown). - Приложение в ответе на
/shutdown:- устанавливает флаг
shutdown_in_progress = True; - readiness probe начинает возвращать 503;
- сервер перестаёт принимать новые соединения (или отклоняет их с 503).
- устанавливает флаг
- Kubernetes удаляет под из endpoints Service (обычно это происходит параллельно с PreStop hook, но не мгновенно).
Пример PreStop hook в Deployment
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "curl -X POST http://localhost:8080/shutdown || true"]
Важно PreStop hook должен быть идемпотентным и не зависеть от внешних сервисов. Если hook зависнет, под будет ждать весь terminationGracePeriodSeconds.
5. Завершение in-flight запросов (drain)
После остановки приёма новых запросов необходимо дождаться завершения уже выполняющихся. Это называется drain.
Стратегии
- Timeout-based задать максимальное время ожидания (например, 60 секунд). Если запрос не завершился за это время — прервать его (отменить через контекст).
- Graceful drain дождаться завершения всех текущих батчей, но не принимать новые.
Пример на Python (FastAPI + asyncio):
import asyncio
import signal
shutdown_event = asyncio.Event()
in_flight_requests = 0
async def shutdown_handler():
shutdown_event.set()
# Ждём завершения всех текущих запросов (максимум 60 секунд)
try:
await asyncio.wait_for(
wait_for_in_flight_requests(),
timeout=60.0
)
except asyncio.TimeoutError:
log.warning("Drain timeout, forcing shutdown")
finally:
# Освобождаем ресурсы
cleanup_model()
async def wait_for_in_flight_requests():
while in_flight_requests > 0:
await asyncio.sleep(0.1)
# В обработчике запроса увеличиваем счётчик
@app.post("/generate")
async def generate(request: Request):
if shutdown_event.is_set():
raise HTTPException(status_code=503, detail="Shutting down")
in_flight_requests += 1
try:
result = await model.generate(request)
return result
finally:
in_flight_requests -= 1
Для многопоточных серверов (Gunicorn, uWSGI): используйте аналогичный механизм с threading.Event и счётчиками.
6. Обработка сигналов в приложении
Kubernetes отправляет SIGTERM после PreStop hook (или сразу, если hook не задан). Приложение должно перехватить этот сигнал и запустить процедуру shutdown.
Пример обработки SIGTERM в Python
import signal
import sys
def handle_sigterm(signum, frame):
log.info("Received SIGTERM, starting graceful shutdown")
# Запускаем асинхронный shutdown (если есть event loop)
asyncio.create_task(shutdown_handler())
# Или синхронный shutdown
# shutdown_handler_sync()
signal.signal(signal.SIGTERM, handle_sigterm)
Важно Не используйте sys.exit() сразу — это приведёт к жёсткому завершению. Вместо этого дайте приложению время на drain.
7. Graceful shutdown для GPU-памяти и моделей
LLM serving часто использует GPU. При завершении пода необходимо корректно освободить ресурсы:
- Очистка GPU-кэша
torch.cuda.empty_cache()(PyTorch),tf.keras.backend.clear_session()(TensorFlow). - Сохранение состояния если модель использует кэш эмбеддингов или KV-cache, можно сохранить его на диск (если это имеет смысл для холодного старта).
- Закрытие соединений с Triton Inference Server, vLLM, TensorFlow Serving — отправка сигнала на завершение.
Пример для vLLM (через API):
import requests
def shutdown_vllm():
try:
requests.post("http://localhost:8000/v1/shutdown", timeout=5)
except Exception:
pass
Для моделей на GPU после завершения всех запросов вызовите torch.cuda.synchronize(), чтобы дождаться завершения всех операций на GPU, затем empty_cache().
8. Интеграция с Service Mesh и Load Balancer
В продакшене часто используются Service Mesh (Istio, Linkerd) или внешние Load Balancer. Они также участвуют в graceful shutdown.
- Istio при удалении пода Istio удаляет его из своей таблицы endpoints. Но это может занимать до 5–10 секунд. PreStop hook должен подождать, чтобы убедиться, что новые запросы больше не приходят.
- Readiness probe: должна возвращать 503 при shutdown. Это гарантирует, что даже если Service Mesh не успел обновиться, под не будет получать трафик.
- Headless service если используется StatefulSet, нужно учитывать, что поды имеют стабильные имена, и drain может быть сложнее.
Рекомендация используйте preStop с задержкой 5–10 секунд перед началом drain, чтобы дать время балансировщику обновиться.
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5 && curl -X POST http://localhost:8080/shutdown"]
9. Настройка terminationGracePeriodSeconds
Этот параметр определяет общее время, которое Kubernetes ждёт завершения пода после начала terminating.
Формула расчёта
terminationGracePeriodSeconds = max_inference_time + drain_buffer + shutdown_overhead
- max_inference_time максимальное время одного инференса (например, 30 секунд для длинных генераций).
- drain_buffer время на завершение всех текущих запросов (обычно 10–20 секунд).
- shutdown_overhead время на освобождение ресурсов (2–5 секунд).
Типичные значения
- Для LLM serving: 60–120 секунд.
- Для лёгких моделей: 30–45 секунд.
Пример в Deployment
spec:
terminationGracePeriodSeconds: 90
Важно Если terminationGracePeriodSeconds меньше реального времени drain, Kubernetes отправит SIGKILL, и запросы будут потеряны. Лучше установить с запасом.
10. Мониторинг и алерты
Graceful shutdown должен быть наблюдаемым. Добавьте метрики и логи:
Метрики (Prometheus):
shutdown_in_progress(gauge, 0/1) — флаг, что под в процессе shutdown.shutdown_duration_seconds(histogram) — время от начала shutdown до полного завершения.requests_dropped_total(counter) — количество запросов, прерванных из-за shutdown.drain_timeout_total(counter) — количество случаев, когда drain не уложился в таймаут.
Логи:
- "Shutdown initiated" — при получении SIGTERM или вызове PreStop.
- "Draining connections, in-flight requests: N" — каждые 5 секунд во время drain.
- "Shutdown complete" — после освобождения ресурсов.
Алерты
- Если
shutdown_duration_seconds> 0.8 *terminationGracePeriodSeconds— предупреждение. - Если
requests_dropped_total> 0 — критический алерт (значит, drain не сработал).
11. Тестирование graceful shutdown
Graceful shutdown нужно тестировать, особенно в условиях хаоса.
Методы тестирования
- Chaos engineering используйте
kubectl delete pod --grace-period=...или инструменты вроде Chaos Mesh, Litmus. - Интеграционные тесты запустите под, отправьте долгий запрос, удалите под, проверьте, что запрос завершился успешно.
- Нагрузочное тестирование во время стресс-теста удалите под и проверьте, что клиенты не получили ошибок (или получили минимум).
Пример скрипта для теста
# Запускаем фоновый запрос
curl -X POST http://service/generate -d '{"prompt": "long text..."}' &
sleep 2
# Удаляем под
kubectl delete pod llm-serving-xxxx --grace-period=90
wait
# Проверяем, что curl завершился с кодом 200
12. Пример реализации (Python + FastAPI + Kubernetes)
Приложение (app.py):
import asyncio
import signal
from fastapi import FastAPI, HTTPException
app = FastAPI()
shutdown_event = asyncio.Event()
in_flight = 0
@app.on_event("startup")
async def startup():
signal.signal(signal.SIGTERM, handle_sigterm)
def handle_sigterm(signum, frame):
asyncio.create_task(graceful_shutdown())
async def graceful_shutdown():
shutdown_event.set()
# Ждём завершения in-flight запросов
try:
await asyncio.wait_for(wait_for_drain(), timeout=60.0)
except asyncio.TimeoutError:
pass
# Освобождаем GPU
import torch
torch.cuda.empty_cache()
# Завершаем приложение
exit(0)
async def wait_for_drain():
while in_flight > 0:
await asyncio.sleep(0.1)
@app.post("/generate")
async def generate(prompt: str):
if shutdown_event.is_set():
raise HTTPException(status_code=503, detail="Shutting down")
global in_flight
in_flight += 1
try:
result = await model.generate(prompt)
return {"result": result}
finally:
in_flight -= 1
@app.get("/health/ready")
async def readiness():
if shutdown_event.is_set():
return {"status": "not ready"}, 503
return {"status": "ready"}
Deployment (deployment.yaml):
apiVersion: apps/v1
kind: Deployment
metadata:
name: llm-serving
spec:
replicas: 3
template:
spec:
terminationGracePeriodSeconds: 90
containers:
- name: llm
image: myregistry/llm-serving:latest
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep [[5. Как вы оцениваете качество retrieval'а в RAG-системе|5]] && curl -X POST http://localhost:8080/shutdown || true"]
Пет-проект для закрепления
Задача Разработать и протестировать graceful shutdown для LLM serving пода на Kubernetes с использованием FastAPI и vLLM.
Инструменты
- Minikube или Kind (локальный Kubernetes)
- Python 3.10+, FastAPI, vLLM (или заглушка с asyncio.sleep)
- Docker
- kubectl, curl
Шаги:
- Создайте FastAPI-приложение, которое имитирует LLM инференс (asyncio.sleep(10-30 секунд)).
- Реализуйте graceful shutdown: PreStop hook, readiness probe, обработка SIGTERM, drain.
- Соберите Docker-образ и запустите в Kubernetes (Deployment с 2 репликами).
- Напишите скрипт, который отправляет долгий запрос и одновременно удаляет под (
kubectl delete pod). - Проверьте, что запрос завершился успешно (код 200), а не ошибкой.
- Добавьте метрики (Prometheus client) и логи.
- Протестируйте с разными значениями
terminationGracePeriodSeconds(30, 60, 90 секунд).
Ожидаемый результат
- При удалении пода все in-flight запросы завершаются корректно.
- Клиенты не получают 5xx.
- В логах видна последовательность: "Shutdown initiated" → "Draining..." → "Shutdown complete".
- Метрика
shutdown_duration_secondsне превышает 80% отterminationGracePeriodSeconds.
Связь с другими вопросами
| Вопрос | Тема |
|---|---|
| 240 | Деплой LLM на Kubernetes |
| 241 | Масштабирование LLM serving |
| 243 | Health probes и liveness/readiness |
| 244 | Rolling update стратегии |
| 245 | Canary deployments для LLM |
| 246 | Мониторинг LLM serving |
Навигация
- Предыдущий: 241
- Следующий: 243
- Индекс: 00. Индекс разборов