Корпоративный AI-сервис отличается от обычного веб-приложения одной неприятной особенностью: каждый запрос стоит непропорционально дорого. Один HTTP-запрос к обычному REST API обрабатывается за миллисекунды, а один запрос к LLM может занимать GPU на несколько секунд и потреблять заметную долю памяти видеокарты. Это значит, что без rate limiting один недовольный сотрудник, сломанный скрипт-автоматизация или банальный цикл-баг в интеграции может положить сервис для всей компании за считанные минуты.

Разберём, как выстроить защиту от злоупотреблений на нескольких уровнях — от nginx перед сервисом до лимитов внутри самого шлюза — на примере сервера srv-ai01 в сети 192.168.10.0/24.

Почему rate limiting для LLM не похож на rate limiting для обычного API

Классический rate limiting считает количество запросов в единицу времени. Для LLM этого недостаточно, потому что стоимость запроса сильно варьируется: короткий вопрос «привет» и запрос с вложенным документом на 50 страниц потребляют совершенно разные ресурсы. Поэтому лимитировать нужно как минимум по трём осям: количество запросов, количество токенов и параллельность (сколько запросов одновременно занимают GPU).

Что именно нужно защищать от перегрузки

Прежде чем настраивать конкретные лимиты, полезно явно перечислить ресурсы, которые могут быть исчерпаны, потому что защита от каждого из них выглядит немного по-разному: GPU-память (сколько параллельных генераций физически помещается), пропускная способность сети между srv-ai01 и остальной инфраструктурой (актуально для веб-поиска и передачи больших документов), CPU для предобработки запросов и работы код-интерпретатора, и, наконец, дисковое пространство для логов и временных файлов, которое тоже конечно и может быть исчерпано при неограниченной генерации изображений или загрузке документов.

Уровень 1: nginx перед Open WebUI

Первая линия защиты — стандартный nginx rate limiting на уровне HTTP, который отсекает грубые злоупотребления вроде скриптов, долбящих API без пауз.

# /etc/nginx/conf.d/ai-gateway.conf
limit_req_zone $binary_remote_addr zone=ai_general:10m rate=20r/m;
limit_req_zone $http_x_user_id zone=ai_per_user:10m rate=30r/m;
limit_conn_zone $http_x_user_id zone=ai_per_user_conn:10m;

server {
    listen 443 ssl;
    server_name ai.itpages.lab.local;

    location /api/chat/completions {
        limit_req zone=ai_general burst=5 nodelay;
        limit_req zone=ai_per_user burst=10 nodelay;
        limit_conn ai_per_user_conn 3;
        limit_req_status 429;

        proxy_pass http://srv-ai01.itpages.lab.local:8080;
        proxy_read_timeout 300s;
    }
}

Здесь limit_conn отдельно ограничивает количество одновременных соединений на пользователя — это критично для LLM, потому что именно параллельные долгие запросы, а не их частота, чаще всего забивают очередь GPU.

Уровень 2: лимиты по токенам через LiteLLM

nginx не знает, сколько токенов «стоит» запрос, поэтому для честного контроля нагрузки нужен прокси-слой уровня приложения. LiteLLM Proxy умеет ограничивать не только бюджет в деньгах, но и rate limit в токенах и запросах в минуту (TPM/RPM) на ключ:

curl -X POST http://srv-ai01.itpages.lab.local:4000/key/generate \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
  -d '{
    "user_id": "support-team",
    "tpm_limit": 40000,
    "rpm_limit": 60,
    "max_parallel_requests": 5
  }'

Это даёт гранулярный контроль: даже если у отдела есть бюджет в токенах на месяц, в моменте он не сможет забить очередь больше, чем на пять параллельных запросов, и не сможет превысить 40 тысяч токенов в минуту, что защищает других пользователей srv-ai01 от деградации отклика.

Уровень 3: очередь и приоритизация запросов

Когда лимиты защищают от превышения, но легитимной нагрузки всё равно больше, чем может переварить один GPU-сервер, помогает очередь с приоритетами. Например, интерактивные запросы из чата должны обрабатываться быстрее, чем фоновые batch-задачи вроде массовой генерации саммари.

# упрощённая логика приоритизации на уровне очереди перед vLLM
PRIORITY_MAP = {
    "interactive_chat": 0,   # высокий приоритет
    "batch_summarize": 5,
    "background_indexing": 10,
}

def enqueue(request, request_type):
    priority = PRIORITY_MAP.get(request_type, 5)
    queue.put((priority, time.time(), request))

vLLM и другие серверы инференса поддерживают continuous batching, но при высокой нагрузке всё равно нужен внешний контроль очереди, иначе фоновая batch-задача от одного отдела может визуально «заморозить» чат для всех остальных на несколько минут.

Обнаружение аномального поведения

Rate limiting защищает от превышения порогов, но не объясняет, что происходит. Полезно вести метрики по паттернам использования и алертить на аномалии, которые лимиты пропускают, но которые всё равно выглядят подозрительно:

# псевдокод для алерта в Prometheus/Alertmanager
- alert: SuspiciousTokenBurst
  expr: rate(litellm_total_tokens[5m]) > 3 * avg_over_time(litellm_total_tokens[1h])
  for: 2m
  labels:
    severity: warning
  annotations:
    summary: "Пользователь {{ $labels.user_id }} резко увеличил потребление токенов"

Такие алерты часто ловят не злонамеренные атаки, а честные ошибки — например, интеграцию, которая из-за бага в retry-логике шлёт один и тот же запрос по кругу.

Защита от автоматизированных злоупотреблений

Помимо честных пользователей, которые случайно превышают разумную нагрузку, стоит закладывать защиту и от намеренных злоупотреблений — например, сотрудника, который решил использовать корпоративный AI-сервис как бесплатную замену внешнему API для стороннего проекта. Признаки такого поведения обычно видны по паттерну запросов: постоянная, равномерная нагрузка 24/7 без пауз, характерных для рабочего дня, или запросы, structurally похожие на программный клиент, а не на человека, печатающего в чате.

# эвристика обнаружения нечеловеческого паттерна использования
- alert: BotLikeUsagePattern
  expr: |
    stddev_over_time(litellm_request_interval_seconds[1h]) < 0.5
    and rate(litellm_requests_total[1h]) > 10
  for: 30m
  labels:
    severity: info
  annotations:
    summary: "Пользователь {{ $labels.user_id }} показывает нехарактерно равномерный паттерн запросов"

Такие алерты не должны автоматически блокировать пользователя — это повод для человека посмотреть на ситуацию и, если нужно, уточнить у сотрудника или его руководителя, что происходит. Ложных срабатываний здесь будет достаточно много (например, легитимная batch-автоматизация), поэтому это скорее сигнал для расследования, чем триггер для авто-бана.

CAPTCHA и подтверждение для аномальных запросов

Для веб-интерфейса Open WebUI, доступного за пределами внутренней сети (например, для удалённых сотрудников через VPN), полезно добавить дополнительный уровень защиты на аутентификацию — не столько rate limiting, сколько защиту от подбора паролей и автоматизированных ботов на этапе входа.

# nginx: защита эндпоинта логина отдельным, более строгим лимитом
limit_req_zone $binary_remote_addr zone=login_limit:5m rate=5r/m;

location /api/v1/auths/signin {
    limit_req zone=login_limit burst=3 nodelay;
    proxy_pass http://srv-ai01.itpages.lab.local:8080;
}

Это отдельная, более строгая зона лимитов именно для эндпоинта аутентификации — попытки подбора пароля обычно выглядят как частые запросы к одному и тому же URL, и их стоит тормозить агрессивнее, чем обычные запросы к чату.

Коммуникация лимитов пользователям

Технически грамотный rate limiting может восприниматься сотрудниками как «сервис глючит», если ошибка 429 не сопровождается понятным сообщением. Стоит настроить кастомный ответ, объясняющий причину и когда можно повторить попытку, вместо голого кода ошибки.

error_page 429 /rate_limit_exceeded.json;
location = /rate_limit_exceeded.json {
    default_type application/json;
    return 429 '{"error": "Превышен лимит запросов. Попробуйте через минуту. Если это мешает вашей работе постоянно, обратитесь в IT-отдел для увеличения лимита."}';
}

В Open WebUI это сообщение можно дополнительно перехватить на уровне интерфейса и показать пользователю не как техническую ошибку, а как понятное объяснение с рекомендацией — большая часть недовольства лимитами возникает не из-за самого факта ограничения, а из-за непонятной реакции сервиса, которая выглядит как поломка.

Лимиты по IP и по пользователю одновременно

Одного rate limit на пользователя обычно недостаточно: если аккаунт скомпрометирован или ключ утёк, атакующий легко обойдёт лимит, просто дождавшись окна сброса. На srv-ai01 разумно комбинировать два уровня ограничений — по API-ключу/пользователю и по source IP — так, чтобы сработавший на любом из уровней лимит блокировал запрос:

limit_req_zone $http_x_api_key zone=per_user:10m rate=20r/m;
limit_req_zone $binary_remote_addr zone=per_ip:10m rate=60r/m;

location /v1/chat/completions {
    limit_req zone=per_user burst=5 nodelay;
    limit_req zone=per_ip burst=15 nodelay;
    proxy_pass http://ai_backend;
}

Такая связка ловит два разных сценария злоупотреблений: пользователя, который сам не в меру активен (первая зона), и внешнего клиента, который перебирает ключи или дергает публичный эндпоинт без ключа вовсе (вторая зона). Оба лимита логируются отдельно, поэтому по логам nginx сразу видно, что именно сработало и на каком пользователе или адресе стоит сосредоточить внимание дальше.

Частые ошибки и как их избежать

  • Rate limiting только на уровне IP-адреса. В офисе с NAT все сотрудники выходят с одного внешнего IP, и лимит на IP либо душит всех сразу, либо бесполезен. Лимитируй по идентификатору пользователя или API-ключу, а не по IP.
  • Лимиты считают запросы, но не токены и не параллельность. Пользователь легко укладывается в лимит по количеству запросов, но каждый его запрос — это документ на 100 страниц. Комбинируй минимум две метрики: запросы в минуту и токены в минуту.
  • Нет отдельного лимита на параллельность. Именно параллельные долгие генерации, а не частота запросов, чаще всего кладут инференс-сервер. Ограничивай max_parallel_requests на пользователя отдельно от RPM.
  • Лимиты настроены, но никогда не проверялись нагрузочным тестом. Конфигурация, которая никогда не подвергалась реальной нагрузке, может вести себя не так, как ожидается — например, отдавать 502 вместо аккуратного 429. Всегда прогоняй нагрузочный тест перед тем, как считать лимиты готовыми к продакшену.

Ограничение размера входных данных

Отдельная, часто забываемая ось защиты — лимит на размер самого запроса, а не только на частоту. Без него один пользователь может отправить документ на несколько сотен тысяч токенов и занять контекстное окно модели на несколько минут, независимо от того, укладывается ли он в лимиты RPM и TPM.

# nginx: ограничение размера тела запроса
client_max_body_size 10m;

location /api/chat/completions {
    client_max_body_size 5m;
    proxy_pass http://srv-ai01.itpages.lab.local:8080;
}
# дополнительная проверка на уровне приложения — лимит контекста
MAX_INPUT_TOKENS = 16000

def validate_request(messages):
    total_tokens = count_tokens(messages)
    if total_tokens > MAX_INPUT_TOKENS:
        raise RequestTooLarge(
            f"Запрос на {total_tokens} токенов превышает лимит {MAX_INPUT_TOKENS}"
        )

Лимит стоит подбирать исходя из реальных задач: для команды поддержки, где документы редко превышают пару страниц, разумно держать жёсткий лимит, а для аналитиков, работающих с длинными отчётами, — завести отдельную группу с расширенным лимитом контекста и одновременно более строгим ограничением параллельности, чтобы компенсировать возросшую стоимость каждого запроса.

Тестирование лимитов перед продакшеном

Настроенные лимиты нужно проверять нагрузочным тестированием до того, как реальные пользователи столкнутся с ними в бою. Простой инструмент вроде hey или vegeta позволяет сымитировать всплеск нагрузки и убедиться, что сервис деградирует предсказуемо — отдаёт 429, а не падает целиком или не начинает отвечать всем пользователям с многоминутной задержкой.

hey -n 200 -c 20 -m POST \
  -H "Content-Type: application/json" \
  -H "X-User-Id: load-test-user" \
  -d '{"model":"support-assistant","messages":[{"role":"user","content":"тест нагрузки"}]}' \
  http://srv-ai01.itpages.lab.local:8080/v1/chat/completions

Результаты такого теста стоит сверить с ожиданиями: если 90% запросов сверх лимита должны получать 429 за миллисекунды, а на практике они висят по 30 секунд перед отказом, значит лимиты применяются на неправильном уровне стека — скорее всего, слишком поздно, уже после того как запрос дошёл до inference-сервера.

Что дальше

Безопасность и эксплуатация AI-инфраструктуры — вот с чем ещё стоит разобраться: