Представим, что мы разрабатываем сервис pdf-to-text. Нам загрузили документ на десять страниц. Девять из них обработаны, осталась последняя. И тут сервис получает сигнал об остановке (SIGTERM): приехало обновление или понадобилось уменьшить количество подов.
Если завершение процесса не обработано и приложение сразу остановится, клиент получит ошибку. И совсем не важно, что мы уже почти всё сделали. В идеале нам бы хотелось получить до остановки сервиса возможность дообработать последнюю, десятую страницу и отдать ответ клиенту.
Здесь как раз и нужно корректное завершение работы, или graceful shutdown. Сервис перестаёт принимать новые запросы, а уже принятым даёт время завершиться. В нашем случае приложение дообрабатывает документ и отправляет ответ клиенту.
Если используете FastAPI с Uvicorn, базовый механизм уже встроен. Uvicorn обрабатывает SIGTERM, закрывает сокет для новых подключений и ждёт завершения текущих запросов. Затем приложение выполняет cleanup, например, закрывает соединения с внешними сервисами. В FastAPI эту логику можно разместить в lifespan:
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
print("Запуск приложения")
yield
# Закрываем соединения и освобождаем ресурсы.
print("Завершение работы приложения")
app = FastAPI(lifespan=lifespan)Таймауты
Ждать завершения запроса можно бесконечно, и по умолчанию Uvicorn именно так и делает. Поэтому ожидание стоит ограничить флагом — timeout-graceful-shutdown.
Снаружи тоже есть лимит. docker stop по умолчанию даёт контейнеру 10 секунд, Kubernetes — 30 (terminationGracePeriodSeconds). Если приложение не успеет завершить запросы и cleanup, придёт SIGKILL. Его перехватить нельзя, и процесс просто убьют. Поэтому таймаут Uvicorn вместе с cleanup должен укладываться в отведённое на остановку время.
А что с новыми запросами?
Пока один экземпляр дочитывает наш PDF, балансировщик должен направлять новые запросы на другие поды. В Kubernetes с этим есть свои нюансы: под может получить SIGTERM раньше, чем перестанет получать трафик. Обычно это решают небольшой паузой перед остановкой через preStop.
И если экземпляр всего один, на время рестарта простой неизбежен.
⚫️Graceful shutdown — лишь одна из деталей, о которых нужно помнить при деплое. На курсе DLOps проходим весь путь: от FastAPI и Docker до Kubernetes, Triton и мониторинга.
🪔 DeepSchool