Деплой и масштабирование: Rate limits, квоты и биллинг
Урок 5.3: Деплой и масштабирование: Rate limits, квоты и биллинг
Добро пожаловать на этап, который отделяет любительские прототипы от профессиональных архитектурных решений. Мы прошли путь от промпт-инжиниринга до тонкой настройки (fine-tuning). Ваша модель работает идеально в песочнице. Но что произойдет, когда вы откроете шлюзы для тысяч пользователей?
В этом уроке мы не будем говорить о качестве ответов модели. Мы будем говорить о том, как сделать так, чтобы ваше приложение не упало под нагрузкой и не разорило компанию. Архитектор ИИ-решений обязан понимать механику квот Google Cloud, стратегию обработки ошибок 429 Too Many Requests и прогнозирование затрат.
Три столпа продакшна Gemini:
- Rate Limits (Ограничения частоты): RPM (запросы в минуту) и TPM (токены в минуту). Почему один длинный запрос может быть дороже ста коротких.
- Управление ошибками и очередями: Как правильно реализовывать стратегии повторных попыток (Retry) и Exponential Backoff.
- Биллинг и мониторинг: Как считать деньги до того, как они списаны, и как предотвратить бюджетный крах.
Анатомия квот: RPM, TPM и RPD
Когда вы работаете с Gemini API (через Google AI Studio или Vertex AI), вы сталкиваетесь с жесткими ограничениями. Понимание разницы между метриками критически важно для планирования архитектуры.
1. RPM (Requests Per Minute)
Это количество HTTP-запросов, которое вы отправляете к API за 60 секунд. Это самая простая метрика, но она обманчива. Вы можете укладываться в RPM, но всё равно получать отказы. Почему? Из-за TPM.
2. TPM (Tokens Per Minute) — Скрытый убийца
Для LLM, таких как Gemini 1.5 Pro, TPM часто является более строгим ограничителем. TPM учитывает сумму входных (input) и выходных (output) токенов.
Сценарий из жизни:
У вас лимит 60 RPM и 32,000 TPM.
Вы отправляете 10 запросов в минуту (легко укладываетесь в RPM).
Но каждый запрос содержит огромный контекст на 4,000 токенов.
10 * 4,000 = 40,000 токенов.
Результат: Ошибка 429 Resource Exhausted, хотя по количеству запросов вы даже не приблизились к лимиту.
3. RPD (Requests Per Day)
Обычно применяется на бесплатных тарифах (Free Tier). Для продакшн-решений (Pay-as-you-go) RPD обычно не лимитируется, но важно следить за квотами проекта в Google Cloud Console.
import time
import google.generativeai as genai
from google.api_core import exceptions
# Базовая настройка (предполагается, что API ключ уже задан)
model = genai.GenerativeModel('gemini-1.5-pro')
def naive_request(prompt):
"""
Пример того, как НЕ надо делать в продакшне.
Этот код упадет при первой же ошибке лимитов.
"""
try:
response = model.generate_content(prompt)
return response.text
except exceptions.ResourceExhausted:
print("Критическая ошибка: Квота исчерпана! Приложение падает.")
raise
# В реальной жизни мы столкнемся с ошибкой 429 очень быстро.
# Ниже мы рассмотрим, как это исправить с помощью паттерна Retry.
Стратегия выживания: Exponential Backoff и Jitter
Когда API возвращает ошибку 429 Too Many Requests или Resource Exhausted, инстинктивная реакция новичка — повторить запрос немедленно. Если 1000 пользователей одновременно получат ошибку и мгновенно попытаются повторить запрос, API «ляжет» окончательно, а ваши клиенты попадут в бесконечный цикл ошибок. Это называется «Thundering Herd Problem» (Проблема грочущего стада).
Решение: Экспоненциальная задержка (Exponential Backoff)
Алгоритм прост: если запрос не прошел, подожди 1 секунду. Не прошел снова? Подожди 2 секунды. Потом 4, 8, 16...
Добавление шума (Jitter)
Чтобы избежать синхронных повторов от множества клиентов (когда все ждут ровно 2 секунды и снова бьют в API одновременно), мы добавляем случайный «шум» (Jitter). Например, ждать не ровно 2 секунды, а 2 + random(0, 0.5).
В Python стандартом индустрии для реализации этой логики является библиотека tenacity.
import logging
import random
from tenacity import (
retry,
stop_after_attempt,
wait_random_exponential,
retry_if_exception_type
)
from google.api_core import exceptions
# Настройка логгера
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Конфигурация стратегии повторов
# wait_random_exponential: ждем 1с, 2с, 4с... до макс 60с + добавляем случайность
# stop_after_attempt: сдаемся после 6 попыток
# retry_if_exception_type: повторяем ТОЛЬКО при ошибке 429 (ResourceExhausted) или 503 (ServiceUnavailable)
@retry(
wait=wait_random_exponential(multiplier=1, max=60),
stop=stop_after_attempt(6),
retry=retry_if_exception_type((
exceptions.ResourceExhausted,
exceptions.ServiceUnavailable
))
)
def generate_content_safe(model, prompt):
try:
logger.info(f"Отправка запроса... Промпт: {prompt[:20]}...")
response = model.generate_content(prompt)
return response.text
except Exception as e:
# Логируем ошибку, чтобы видеть, когда срабатывает retry
logger.warning(f"Поймана ошибка: {e}. Tenacity запустит повторную попытку...")
raise e
# Использование
# Если квота превышена, функция будет "висеть" и пытаться снова,
# прозрачно для вызывающего кода, пока не получит результат или не истекут попытки.
if __name__ == "__main__":
try:
result = generate_content_safe(model, "Объясни квантовую физику за 5 секунд.")
print(f"Успех: {result}")
except Exception as e:
print(f"Не удалось получить ответ даже после всех попыток: {e}")
Архитектурные паттерны масштабирования
Retry — это тактика. А нам нужна стратегия. Если ваше приложение предполагает высокий трафик, прямые синхронные вызовы API (как в примере выше) — это тупик. Пользователь не должен ждать 30 секунд, глядя на крутящийся лоадер, пока ваш бэкенд борется с Rate Limits.
Асинхронные очереди (Message Queues)
Архитектурный стандарт для LLM-приложений:
- Frontend отправляет задачу на бэкенд.
- Backend кладет задачу в очередь (RabbitMQ, Redis, Google Pub/Sub) и сразу возвращает пользователю ID задачи (статус "Processing").
- Worker (отдельный процесс) забирает задачу из очереди и обращается к Gemini API.
- Если Worker ловит
RateLimitError, он возвращает задачу в очередь с задержкой, не блокируя основной сервер. - Frontend опрашивает статус задачи (polling) или получает уведомление через WebSocket.
Это позволяет сглаживать пики нагрузки (bursts). Очередь выступает буфером.
Семантическое кэширование (Semantic Caching)
Самый дешевый запрос — тот, который вы не сделали. Часто пользователи задают похожие вопросы. Традиционный кэш (по точному совпадению текста) здесь работает плохо, так как "Привет" и "Здравствуй" — разные строки.
Семантический кэш:
- Преобразуйте запрос пользователя в вектор (embedding).
- Поищите в векторной базе (Vector DB) похожие запросы, на которые вы уже отвечали.
- Если сходство > 0.95 — верните сохраненный ответ.
- Это экономит деньги и снижает TPM до нуля для частых вопросов.
Биллинг: Экономика токенов
Переход в продакшн означает переход на платный тариф (Pay-as-you-go). Цены Gemini 1.5 Pro и Flash существенно различаются. Как архитектор, вы должны уметь прогнозировать расходы.
Цены (ориентировочные, проверьте актуальные в Google Cloud):
- Input Tokens: Дешевле. Это ваш контекст, документы, системные инструкции.
- Output Tokens: Дороже. Это то, что генерирует модель.
- Context Caching: Если вы используете один и тот же огромный контекст (например, книгу) много раз, Gemini позволяет кэшировать его на стороне сервера за отдельную (меньшую) плату.
Как считать токены *до* отправки?
Нельзя полагаться на количество слов (word count). 1000 слов может быть 1300 токенов или 2000 токенов в зависимости от языка и сложности текста. Используйте метод count_tokens.
# Пример калькулятора стоимости (цены условные, для примера логики)
PRICE_PER_1M_INPUT_USD = 3.50 # Цена за миллион входных токенов (Pro)
PRICE_PER_1M_OUTPUT_USD = 10.50 # Цена за миллион выходных токенов (Pro)
def estimate_cost(model, prompt_text, estimated_output_tokens=500):
"""
Оценивает стоимость одного запроса.
Примечание: мы можем точно посчитать вход, но выход можем только предсказать.
"""
# 1. Считаем входные токены точно
input_count = model.count_tokens(prompt_text).total_tokens
# 2. Рассчитываем стоимость входа
input_cost = (input_count / 1_000_000) * PRICE_PER_1M_INPUT_USD
# 3. Рассчитываем примерную стоимость выхода
output_cost = (estimated_output_tokens / 1_000_000) * PRICE_PER_1M_OUTPUT_USD
total_cost = input_cost + output_cost
return {
"input_tokens": input_count,
"estimated_output": estimated_output_tokens,
"total_cost_usd": round(total_cost, 6)
}
prompt = "Проанализируй этот финансовый отчет и выдели ключевые риски... " * 100
cost_analysis = estimate_cost(model, prompt)
print(f"Предварительный расчет стоимости запроса:")
print(f"Входные токены: {cost_analysis['input_tokens']}")
print(f"Примерная цена: ${cost_analysis['total_cost_usd']}")
Напишите класс `BudgetAwareModel`, который оборачивает вызов `generate_content`. Класс должен принимать лимит бюджета в USD (например, $0.05) и накапливать потраченные средства. Перед каждым запросом он должен проверять, не превышен ли бюджет (на основе подсчета input-токенов и пессимистичной оценки output-токенов). Если бюджет превышен, метод должен выбрасывать исключение `BudgetExceededError`, не совершая реального вызова к API.
Ваше приложение обрабатывает большие тексты. Вы заметили, что часто получаете ошибку 429 Resource Exhausted, хотя отправляете всего 5 запросов в минуту (при лимите 60 RPM). В чем наиболее вероятная причина?
Заключение
Деплой ИИ-приложения — это баланс между производительностью и стоимостью. Мы выяснили, что простого try-except недостаточно. Вам нужны надежные механизмы повторных попыток (backoff), понимание токеномики и, в идеале, архитектура на основе очередей. В следующем модуле мы перейдем к темам безопасности и защиты от Prompt Injection, что является финальным штрихом перед запуском.