Комментарий пользователя
Здравствуйте! В FastAPI‑сервисе под нагрузкой начали видеть странные паузы по 2–5 секунд, хотя CPU почти не нагружен. В логах нет ошибок, иногда всплывает asyncio.exceptions.CancelledError, а инструменты мониторинга пишут, что Future висит в await. Как понять, что именно тормозит, и можно ли это как‑то «ограничить»?
Ответ специалиста
Добрый день, Илья! Так бывает почти у всех, кто впервые выходит в прод с async/await на реальной нагрузке. Вроде бы CPU‑нагрузки нет, сервис отвечает, но иногда появляются паузы, asyncio‑таски как будто «зависают».
Скорее всего, у вас одна из двух проблем:
- блокировка цикла (Event Loop) — где-то внутри async def затесалась тяжелая математика или долгая синхронная операция. Она просто «вешает» поток, и asyncio не может переключиться на другие задачи;
- «забытые» таски — внешнее API или база данных отвечают медленно, а у вас нет жесткого таймаута. Таски копятся, забивают очередь, а потом отваливаются по CancelledError, когда клиент уже устал ждать и закрыл соединение.
При этом вы не видите RuntimeError или BlockingIOError, потому что asyncio просто не успевает пробежаться по всем таскам, и часть запросов как‑то «залипает» в очереди.
Чтобы понять, что именно тормозит, можно сделать простой эксперимент: покрыть критичные места таймаутом и посмотреть, где первый раз срабатывает:
import asyncio
import httpx
async def call_external():
# Медленный внешний сервис
async with httpx.AsyncClient(timeout=5.0) as client:
r = await client.get("https://slow-api.example.com/long_task")
r.raise_for_status()
return r.json()
async def safe_call():
# Обернем таск в wait_for
try:
result = await asyncio.wait_for(call_external(), timeout=3.0)
return result
except asyncio.TimeoutError:
# Тут мы видим, что ресурс реально тормозит
print("Timeout for external call!")
return {}
Теперь, когда asyncio падает TimeoutError, вы сразу понимаете, где именно виснет задача.
Еще один сценарий — фоновый create_task без await:
async def background_task():
await asyncio.sleep(10)
# Делаем что-то долго, без await выше
async def handle_request():
asyncio.create_task(background_task()) # Создали таск и забыли
# Никакого await, поэтому не ожидаем завершения
return {"ok": True}
В проде такие таски накапливаются, линия asyncio как будто «думает», а asyncio.CancelledError появляется, когда asyncio пытается завершиться, но не может дождаться всех тасков.
Лучше:
async def handle_request():
task = asyncio.create_task(background_task())
# Явно ждем, либо ограничиваем время
try:
await asyncio.wait_for(task, timeout=3.0)
except asyncio.TimeoutError:
print("Background task took too long, skipping")
В продакшене проще всего обернуть критичные звенья:
- asyncio.wait_for вокруг вызовов внешних API;
- asyncio.timeout вокруг логики, зависящей от БД;
- asyncio.gather с return_exceptions=True для списка параллельных запросов, чтобы не завязнуть на одном медленном.
Так вы добавляете явный предел на ожидание, и если asyncio действительно «тормозит», вы уже знаете, где именно.
Надеюсь, это поможет вашему сервису не залипать. Заглядывайте в Академию Selectel, у нас тут лампово и полезно.