Как я укротил шторм на квоте FastAPI и GigaChat Pro
Понедельник, 9:47. У дилера скопилось 340 заявок за ночь. Менеджер жмёт «Сгенерировать для всех» и уходит за кофе. Через пару минут сервис начинает сыпать 429-ми, очередь в ретраи, а фронт показывает вечный спиннер. Вместо кофе мне прилетает сообщение: «Опять не работает».
Так я впервые увидел GigaChat Pro под реальной нагрузкой. Расскажу, как перестал биться об лимиты и добавил нормальные ограничители прямо в FastAPI.
Наивный asyncio.gather
Когда нужно обработать пачку, первая мысль — asyncio.gather. Всё же async, пусть летят параллельно.
async def generate_for_batch(requests):
return await asyncio.gather(*[llm.generate(r) for r in requests])
Работало, пока было 15 заявок в час. Как только пришло 340 одновременных запросов, провайдер начал бить по рукам. Все корутины получали 429 почти одновременно, ретраи по умолчанию проглатывали ошибку — и в итоге «сгенерировано 0 из 340».
Семафор — первый регулятор
Решение оказалось простым: ограничить параллельность. Я поставил семафор на 8 потоков.
LLM_SEMAPHORE = asyncio.Semaphore(8)
async def generate_one(req):
async with LLM_SEMAPHORE:
return await llm.generate(req)
Восемь — не магическое число, просто при таком значении p95 держалось около 4 секунд, а вся пачка из 340 заявок укладывалась в стабильные 3:20. Плюс добавил return_exceptions=True, чтобы одна упавшая заявка не убивала весь батч.
Retry с jitter, а не дружный залп
Семафор не спасает от отдельных 429 и таймаутов. Добавил tenacity с экспоненциальным backoff и случайным jitter. Теперь запросы не бьют в одну и ту же секунду.
Ловлю только RateLimitError и TimeoutError. Всё остальное (400-е, проблемы с токеном) — сразу наверх, без попыток.
После этого доля заявок, требующих ручной правки, упала с 11 % до 0,4 %.
Circuit breaker — когда лучше сдаться
Иногда проблема не на моей стороне: лежит целый регион у провайдера. Тогда ретраи только жгут квоту. Поставил простой circuit breaker: если за минуту больше 30 % запросов падают — на 60 секунд возвращаю заглушку и не дёргаю LLM.
Два раза за полгода эта штука спасала: вместо часов разборок переключались на резервную модель за полторы минуты.
Очередь поверх процессов
Семафор внутри одного процесса не спасает, когда воркеров два или больше. Перенёс ограничение в PostgreSQL: складываю задачи в таблицу и обрабатываю фоновым воркером через SELECT ... FOR UPDATE SKIP LOCKED. Плюс можно поставить лимиты на клиента.
Что получилось
- 340 заявок: было «иногда работает» → стало 3 минуты 20 секунд стабильно.
- Ошибки генерации: 11 % → 0,4 %.
- 429-х в пике: до 80 в день → 3–5, и они нормально ретраятся.
- Инциденты у провайдера: часы на разбор → 90 секунд до переключения.
Главный вывод простой: когда говорят «LLM тормозит», в большинстве случаев проблема не в модели. Просто между FastAPI и эндпоинтом нет ни одного ограничителя. Четыре слоя (семафор, retry с jitter, breaker и очередь) стоят пару вечеров, а разговоры «опять не работает» — сильно дороже.
А у вас как сейчас регулируется параллельность при вызове LLM?
