Деплой и масштабирование: Rate limits, квоты и биллинг

40 минут Урок 25

Урок 5.3: Деплой и масштабирование: Rate limits, квоты и биллинг

Добро пожаловать на этап, который отделяет любительские прототипы от профессиональных архитектурных решений. Мы прошли путь от промпт-инжиниринга до тонкой настройки (fine-tuning). Ваша модель работает идеально в песочнице. Но что произойдет, когда вы откроете шлюзы для тысяч пользователей?

В этом уроке мы не будем говорить о качестве ответов модели. Мы будем говорить о том, как сделать так, чтобы ваше приложение не упало под нагрузкой и не разорило компанию. Архитектор ИИ-решений обязан понимать механику квот Google Cloud, стратегию обработки ошибок 429 Too Many Requests и прогнозирование затрат.

Три столпа продакшна Gemini:

  1. Rate Limits (Ограничения частоты): RPM (запросы в минуту) и TPM (токены в минуту). Почему один длинный запрос может быть дороже ста коротких.
  2. Управление ошибками и очередями: Как правильно реализовывать стратегии повторных попыток (Retry) и Exponential Backoff.
  3. Биллинг и мониторинг: Как считать деньги до того, как они списаны, и как предотвратить бюджетный крах.

Анатомия квот: 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.

Совет Архитектора: При проектировании системы всегда рассчитывайте нагрузку, отталкиваясь от TPM, а не RPM. Именно токены, а не количество вызовов API, являются «валютой» и главным узким местом в работе с LLM.

python
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.

python
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-приложений:

  1. Frontend отправляет задачу на бэкенд.
  2. Backend кладет задачу в очередь (RabbitMQ, Redis, Google Pub/Sub) и сразу возвращает пользователю ID задачи (статус "Processing").
  3. Worker (отдельный процесс) забирает задачу из очереди и обращается к Gemini API.
  4. Если Worker ловит RateLimitError, он возвращает задачу в очередь с задержкой, не блокируя основной сервер.
  5. Frontend опрашивает статус задачи (polling) или получает уведомление через WebSocket.

Это позволяет сглаживать пики нагрузки (bursts). Очередь выступает буфером.

Семантическое кэширование (Semantic Caching)

Самый дешевый запрос — тот, который вы не сделали. Часто пользователи задают похожие вопросы. Традиционный кэш (по точному совпадению текста) здесь работает плохо, так как "Привет" и "Здравствуй" — разные строки.

Семантический кэш:

  1. Преобразуйте запрос пользователя в вектор (embedding).
  2. Поищите в векторной базе (Vector DB) похожие запросы, на которые вы уже отвечали.
  3. Если сходство > 0.95 — верните сохраненный ответ.
  4. Это экономит деньги и снижает 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.

python
# Пример калькулятора стоимости (цены условные, для примера логики)
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, что является финальным штрихом перед запуском.