Как я укротил шторм на квоте FastAPI и GigaChat Pro

Как я укротил шторм на квоте FastAPI и GigaChat Pro

Как я укротил шторм на квоте 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?