Как вы проектируете graceful shutdown для LLM serving pod в Kubernetes?
Краткий тезис
Graceful shutdown (плавное завершение) для LLM serving pod в Kubernetes — это процесс, при котором под перестаёт принимать новые запросы, завершает все уже выполняющиеся (inflight) запросы, а затем корректно завершает процесс. Основные компоненты: PreStop hook для запуска drain-логики, readiness probe для исключения пода из Service, terminationGracePeriodSeconds для задания таймаута на завершение, и обработка SIGTERM внутри приложения. Без graceful shutdown пользователи получают ошибки 5xx, а модель может оставить незавершённые запросы.
1. Термин: Graceful Shutdown
Graceful shutdown — это процедура, при которой приложение завершает работу, не теряя данные и не обрывая активные соединения. В контексте LLM serving это означает:
- Drain (слив): прекращение приёма новых запросов.
- Inflight requests (выполняющиеся запросы): завершение всех начатых генераций.
- Cleanup (очистка): освобождение ресурсов (GPU memory, соединения с БД).
- Exit (выход): завершение процесса с кодом 0.
Противоположность — hard shutdown (жёсткое завершение), когда процесс убивается принудительно (SIGKILL), что приводит к ошибкам у клиентов и возможной потере данных.
2. PreStop Hook
PreStop hook — это команда или HTTP-запрос, который Kubernetes выполняет перед отправкой SIGTERM процессу. Он задаётся в spec.containers.lifecycle.preStop.
Пример конфигурации
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- "sleep 10 && curl -X POST http://localhost:8000/drain"
sleep 10— даёт время kube-proxy обновить правила iptables, чтобы новые запросы перестали направляться на под.- curl -X POST /drain — вызывает эндпоинт приложения, который переводит его в режим «не принимать новые запросы».
Важно PreStop hook выполняется до SIGTERM. Если hook зависает или падает, Kubernetes всё равно отправляет SIGTERM после истечения terminationGracePeriodSeconds.
3. Drain Endpoint на стороне приложения
Внутри LLM сервера (например, на базе vLLM, TGI или FastAPI) нужно реализовать эндпоинт /drain, который:
- Устанавливает флаг draining = True.
- Запрещает принятие новых запросов (возвращает 503 Service Unavailable).
- Продолжает обрабатывать уже запущенные inflight запросы.
Пример на FastAPI
from fastapi import FastAPI, HTTPException
import asyncio
app = FastAPI()
draining = False
inflight = set()
@app.post("/drain")
async def drain():
global draining
draining = True
return {"status": "draining"}
@app.post("/generate")
async def generate(prompt: str):
if draining:
raise HTTPException(status_code=503, detail="Server is draining")
task = asyncio.current_task()
inflight.add(task)
try:
# генерация ответа
result = await model.generate(prompt)
return result
finally:
inflight.discard(task)
4. Обработка SIGTERM
После PreStop hook Kubernetes отправляет SIGTERM процессу. Приложение должно перехватить этот сигнал и:
- Установить флаг draining (если не сделано ранее).
- Дождаться завершения всех inflight запросов.
- Выполнить cleanup (освободить GPU память, закрыть соединения).
- Выйти с кодом 0.
Пример обработки сигнала
import signal
import sys
def handle_sigterm(signum, frame):
global draining
draining = True
# ждём завершения inflight запросов
while inflight:
time.sleep(0.1)
# cleanup
model.unload()
sys.exit(0)
signal.signal(signal.SIGTERM, handle_sigterm)
Термин: SIGTERM — сигнал завершения (номер 15), который просит процесс завершиться. Процесс может его перехватить и выполнить cleanup. Если процесс не завершается за отведённое время, Kubernetes отправляет SIGKILL (номер 9), который нельзя перехватить.
5. Readiness Probe и Graceful Shutdown
Readiness probe — проверка, которая определяет, готов ли под принимать трафик. Если probe возвращает failure, под исключается из Service (endpoints).
При graceful shutdown нужно:
- В PreStop hook или при получении SIGTERM начать возвращать failure на readiness probe.
- Это гарантирует, что новые запросы не будут направлены на под.
Пример readiness probe в deployment
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
Внутри /health:
@app.get("/health")
async def health():
if draining:
return {"status": "not ready"}, 503
return {"status": "ok"}
Термин: Liveness probe — проверка, что под жив. При graceful shutdown её тоже можно отключить, чтобы Kubernetes не перезапускал под во время завершения.
6. terminationGracePeriodSeconds
terminationGracePeriodSeconds — параметр в spec.template.spec, задающий максимальное время (в секундах) на graceful shutdown. По умолчанию 30 секунд.
Для LLM serving нужно увеличить до 60–120 секунд, так как генерация длинных ответов может занимать десятки секунд.
Пример:
spec:
terminationGracePeriodSeconds: 120
Если за это время процесс не завершился, Kubernetes отправляет SIGKILL.
7. Мониторинг Graceful Shutdown
Важно отслеживать метрики graceful shutdown:
| Метрика | Описание |
|---|---|
inflight_requests | Количество выполняющихся запросов |
shutdown_duration_seconds | Время от начала drain до завершения |
draining_mode | 1 если под в режиме drain, иначе 0 |
killed_by_sigkill | 1 если под был убит принудительно |
Пример экспорта метрик (Prometheus):
from prometheus_client import Gauge, Counter
inflight_gauge = Gauge('inflight_requests', 'Current inflight requests')
shutdown_duration = Gauge('shutdown_duration_seconds', 'Shutdown duration')
sigkill_counter = Counter('killed_by_sigkill', 'Count of SIGKILL terminations')
8. Тестирование Graceful Shutdown
Для проверки graceful shutdown можно:
- Развернуть под с LLM сервером.
- Отправить несколько долгих запросов (например, на генерацию 1000 токенов).
- Удалить под (kubectl delete pod).
- Проверить, что все запросы завершились успешно (статус 200), а не 502/503.
- Проверить логи на наличие сообщений о drain и завершении.
Скрипт для тестирования
#!/bin/bash
# Отправляем запросы в фоне
for i in {1..5}; do
curl -X POST http://<pod-ip>:8000/generate -d '{"prompt":"long text..."}' &
done
sleep 2
# Удаляем под
kubectl delete pod llm-server-xxx --grace-period=120
wait
# Проверяем, что все ответы успешны
9. Распространённые проблемы и решения
| Проблема | Причина | Решение |
|---|---|---|
| 502 Bad Gateway | Новые запросы попали на под, который уже завершается | Увеличить sleep в PreStop hook, настроить readiness probe |
| 504 Gateway Timeout | Inflight запросы не успели завершиться за terminationGracePeriodSeconds | Увеличить terminationGracePeriodSeconds, оптимизировать время генерации |
| Под зависает при shutdown | Приложение не обрабатывает SIGTERM | Добавить обработчик сигнала, проверить логи |
| GPU память не освобождается | Cleanup не выполнен | Вызывать model.unload() или torch.cuda.empty_cache() |
Пет-проект для закрепления
Задача Развернуть LLM сервер (например, vLLM) в minikube с корректным graceful shutdown и проверить его работу.
Инструменты
- Minikube (локальный Kubernetes)
- Docker
- vLLM (или любой LLM сервер с HTTP API)
- Python + FastAPI (для обёртки)
- Prometheus + Grafana (опционально, для мониторинга)
Шаги:
- Напишите Dockerfile для LLM сервера с поддержкой
/drainэндпоинта и обработкой SIGTERM. - Создайте Kubernetes deployment с:
- PreStop hook (
sleep 10 && curl -X POST localhost:8000/drain) - readiness probe (проверка
/health) terminationGracePeriodSeconds: 120
- PreStop hook (
- Создайте Service (ClusterIP) для доступа к поду.
- Напишите тестовый скрипт, который отправляет несколько долгих запросов и одновременно удаляет под.
- Проверьте, что все запросы завершились успешно (статус 200).
- Добавьте метрики и визуализируйте shutdown duration в Grafana.
Ожидаемый результат Вы увидите, что при удалении пода все inflight запросы завершаются, а новые запросы получают 503 или перенаправляются на другой под (если есть реплики). Логи покажут последовательность: drain → завершение inflight → exit 0.
Связь с другими вопросами
| Вопрос | Тема |
|---|---|
| 405 | Как деплоить LLM в production? |
| 406 | Как настроить горизонтальное масштабирование LLM serving? |
| 407 | Как настроить мониторинг LLM serving? |
| 408 | Как обрабатывать ошибки в LLM serving? |
| 410 | Как настроить autoscaling для LLM serving? |
| 411 | Как обеспечить безопасность LLM serving? |
Навигация
- Предыдущий: 408
- Следующий: 410
- Индекс: 00. Индекс разборов