Rate Limits и стратегии повторных запросов (Exponential Backoff)
Урок 6.2: Rate Limits и стратегии повторных запросов (Exponential Backoff)
Приветствую, коллеги! Мы продолжаем погружаться в LLMOps. В прошлом уроке мы говорили о мониторинге, а сегодня коснемся темы, которая отличает «домашний» прототип от надежного продакшн-сервиса — управление лимитами (Rate Limits) и стратегии ретраев (Retries).
Представьте ситуацию: вы запускаете новую фичу на базе Gemini 3, которая анализирует документы пользователей. На тестах всё летало. Вы выкатываете обновление, приходят первые 1000 пользователей, и... сервис падает. В логах сплошная краснота: 429 Too Many Requests. Хуже того, ваши попытки быстро повторить запросы только усугубляют ситуацию, и API блокирует вас на более длительный срок.
Сегодня мы разберем, как подружиться с квотами Google Cloud, почему простой цикл while — это зло, и как математически изящно решать проблему перегрузки с помощью Exponential Backoff с добавлением Jitter.
Анатомия ограничений: RPM, TPM и RPD
Прежде чем писать код, давайте поймем правила игры. Google (как и любой поставщик LLM) вводит ограничения не из жадности, а для защиты инфраструктуры и обеспечения справедливости (fair usage).
При работе с Gemini 3 API вы столкнетесь с тремя основными метриками:
- RPM (Requests Per Minute): Количество HTTP-запросов в минуту. Это «светофор» для частоты обращений. Даже если запросы маленькие, нельзя делать их слишком часто.
- TPM (Tokens Per Minute): Количество обрабатываемых токенов (входных + выходных) в минуту. Это «весы» для объема данных. Один тяжелый промпт с контекстом в 1 миллион токенов может мгновенно съесть всю квоту, даже если RPM будет равен 1.
- RPD (Requests Per Day): Общий лимит запросов в сутки. Обычно актуален для бесплатных тарифов (Free Tier).
Важный нюанс Gemini 3: Поскольку модель мультимодальная, картинки и видео также конвертируются в токены при расчете квот. Секунда видео может «весить» как сотни текстовых токенов.
Когда вы превышаете лимит, сервер возвращает статус HTTP 429 (Too Many Requests) или, в терминах gRPC/Python клиента, исключение ResourceExhausted.
import google.generativeai as genai
from google.api_core import exceptions
import time
# Типичная ошибка новичка: "долбёжка" сервера
def naive_retry_strategy(prompt):
model = genai.GenerativeModel('gemini-1.5-pro')
while True:
try:
response = model.generate_content(prompt)
return response.text
except exceptions.ResourceExhausted:
print("Лимит исчерпан! Пробую снова немедленно...")
# ОШИБКА: Мгновенный повтор без паузы
# Это приведет к еще более быстрому исчерпанию квоты
continue
except Exception as e:
print(f"Другая ошибка: {e}")
break
Проблема «Стада» (Thundering Herd)
Код выше иллюстрирует то, что инженеры называют «наивным ретраем». Если 100 ваших микросервисов одновременно получат ошибку 429 и одновременно попытаются повторить запрос через 0.1 секунды, они создадут эффект Thundering Herd (набег стада). API, который только начал «дышать», снова захлебнется, но уже от ваших повторных запросов.
Решение: Exponential Backoff (Экспоненциальная отсрочка)
Идея проста: если сервер занят, дайте ему время. Если он всё ещё занят, дайте ему намного больше времени.
Формула задержки выглядит так:
Delay = Base * 2Attempt
- Base: Базовая задержка (например, 1 секунда).
- Attempt: Номер попытки (0, 1, 2...).
Последовательность ожидания будет: 1с, 2с, 4с, 8с, 16с... Это позволяет быстро погасить нагрузку.
Секретный ингредиент: Jitter (Дрожание)
Чистая экспонента имеет недостаток: если сервис упал глобально, все клиенты начнут ретраить синхронно (через 1с, потом все вместе через 2с). Это создает волны нагрузки.
Чтобы «размазать» запросы во времени, мы добавляем случайность — Jitter.
Delay = (Base * 2Attempt) + Random(0, 1)
Теперь один клиент подождет 1.1с, а другой — 1.9с. Синхронизация разрушена, сервер спасен.
import time
import random
from google.api_core import exceptions
# Правильная реализация вручную для понимания алгоритма
def generate_with_backoff(model, prompt, max_retries=5, base_delay=1.0):
retries = 0
while retries < max_retries:
try:
response = model.generate_content(prompt)
return response.text
except exceptions.ResourceExhausted as e:
# Вычисляем время ожидания
# 1. Экспоненциальная часть: 2 в степени retries
# 2. Джиттер: случайное число добавит непредсказуемости
sleep_time = (base_delay * (2 ** retries)) + random.uniform(0, 1)
print(f"Квота превышена (429). Попытка {retries + 1}/{max_retries}. "
f"Ждем {sleep_time:.2f} сек.")
time.sleep(sleep_time)
retries += 1
raise Exception("Не удалось получить ответ после всех попыток ретрая")
Профессиональный подход: библиотека Tenacity
Писать циклы while и высчитывать формулы вручную — полезно для учебы, но в продакшене лучше использовать проверенные инструменты. В экосистеме Python стандартом де-факто является библиотека Tenacity.
Она позволяет вынести логику повторов в декораторы, оставляя бизнес-логику чистой. Tenacity умеет:
- Повторять только при определенных исключениях.
- Использовать разные стратегии ожидания (фиксированные, экспоненциальные).
- Прекращать попытки по времени или количеству раз.
- Добавлять Jitter одной строчкой конфигурации.
Давайте напишем production-ready обертку для Gemini 3 API.
import google.generativeai as genai
from google.api_core import exceptions
from tenacity import (
retry,
stop_after_attempt,
wait_random_exponential,
retry_if_exception_type,
before_sleep_log
)
import logging
import sys
# Настройка логирования
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
logger = logging.getLogger(__name__)
# Конфигурация Gemini
genai.configure(api_key="YOUR_API_KEY")
model = genai.GenerativeModel('gemini-1.5-flash')
# Декоратор Tenacity
# 1. wait_random_exponential: реализует экспоненту + jitter (от 1с до 60с макс)
# 2. stop_after_attempt: сдаемся после 6 попыток
# 3. retry_if_exception_type: ретраим ТОЛЬКО при ошибках квоты или сервера (5xx)
# Не ретраим при ошибках валидации (400)!
@retry(
wait=wait_random_exponential(multiplier=1, max=60),
stop=stop_after_attempt(6),
retry=retry_if_exception_type((
exceptions.ResourceExhausted,
exceptions.ServiceUnavailable,
exceptions.GatewayTimeout
)),
before_sleep=before_sleep_log(logger, logging.INFO)
)
def secure_generate(prompt):
"""
Безопасная функция генерации с автоматическим управлением Rate Limits.
"""
logger.info(f"Отправка запроса: {prompt[:20]}...")
response = model.generate_content(prompt)
return response.text
# Пример использования
if __name__ == "__main__":
try:
# Эмулируем нагрузку
result = secure_generate("Расскажи про теорию струн в двух абзацах.")
print("\nУспех:", result[:100], "...")
except Exception as e:
print(f"\nФинальный сбой: {e}")
Рассчитайте стратегию ретраев. Представьте, что у вас есть скрипт пакетной обработки (batch processing), который обрабатывает 1000 файлов. Базовая задержка (Base) = 2 секунды. Максимальное время, которое мы готовы ждать одного ответа — 35 секунд. Сколько попыток (retries) максимально может сделать система, прежде чем превысит этот лимит ожидания для одного запроса, используя чистый Exponential Backoff (без учета времени выполнения самого запроса)?
Стратегия работы с «тяжелыми» запросами (TPM Limit)
Exponential Backoff отлично работает для ошибок RPM (слишком часто). Но что, если вы упираетесь в TPM (слишком много токенов)?
Например, вы отправляете в Gemini книгу целиком. Даже один запрос может вызвать ResourceExhausted, если он превышает мгновенную пропускную способность, или если вы шлете такие запросы параллельно. В этом случае просто ждать может быть недостаточно — нужно снижать конкурентность (concurrency).
Паттерн «Token Bucket» на стороне клиента:
В высоконагруженных системах (например, когда вы используете Gemini для обработки датасетов из BigQuery) ретраев мало. Нужно предотвращать отправку.
- Оцените количество токенов в промпте перед отправкой (используя
model.count_tokens()). - Используйте локальный ограничитель (Semaphore или RateLimiter), который знает ваш лимит TPM.
- Если «бюджет токенов» на текущую минуту исчерпан, скрипт засыпает до отправки запроса в API.
Это переводит подход от «Бьемся в стену и отступаем» к «Смотрим на светофор перед выездом».
Почему использование случайного разброса (Jitter) критически важно при реализации стратегии Exponential Backoff в распределенной системе?
Заключение и лучшие практики
Внедрение LLM в продакшн — это не только про качество промптов, но и про устойчивость кода. Rate Limits — это нормальная часть работы любой облачной системы.
Чек-лист для вашего продакшена:
- ✅ Никаких бесконечных циклов: Всегда ограничивайте
max_retriesилиstop_after_delay. - ✅ Идемпотентность: Убедитесь, что повтор запроса не приведет к дублированию данных в вашей БД (например, если API ответил, но сеть отвалилась на получении ответа).
- ✅ Мониторинг 429: Настройте алерты на всплески ошибок 429. Если их слишком много, возможно, пора запросить повышение квоты у Google Cloud или оптимизировать промпты.
- ✅ Graceful Degradation: Если API недоступен после всех ретраев, покажите пользователю понятное сообщение («ИИ сейчас перегружен, попробуйте через минуту»), а не 500 Server Error.
В следующем уроке мы разберем кэширование ответов (Caching), которое поможет вам не только сэкономить квоты, но и значительно ускорить работу приложения.