Нагрузочное тестирование LLM-сервиса: пик, очереди, узкие места
Нагрузочное тестирование LLM-сервиса помогает понять, где растут очереди, что ломает пик и как найти узкое место в API, сети и ретраях.

Что ломается на пике
На пике растет не только время ответа модели. Запрос часто тормозит раньше, чем вообще доходит до нее. Шлюз проверяет ключ, применяет rate limit, маскирует PII, пишет аудит-лог, выбирает маршрут до модели и только потом отправляет запрос дальше. Если любой из этих шагов замедляется на 50-100 мс, общая задержка быстро расползается.
Средняя задержка в такой ситуации почти бесполезна. Она сглаживает всплески и скрывает очередь. На графике все выглядит спокойно: в среднем 2,3 секунды. Но часть пользователей уже ждет 8-12 секунд или получает таймаут. Поэтому при нагрузочном тесте смотрят не только на average, но и на p95, p99 и долю таймаутов.
Очередь редко живет в одном месте. Она копится в API-шлюзе, в пуле соединений к базе, в очереди на запись логов и даже в воркерах постобработки. Часто упирается как раз тот этап, который все считали второстепенным. Пока растет очередь на аудит или метки контента, процессы держат память, соединения остаются занятыми, и через несколько минут уже кажется, что не справляется модель. Хотя сама модель может отвечать почти в прежнем темпе.
Ретраи ухудшают картину очень быстро. Один таймаут запускает повтор, потом еще один, и трафик за пару минут растет не на 10%, а почти вдвое. Если клиент, SDK и шлюз повторяют запросы независимо друг от друга, система сама разгоняет пик. Снаружи это выглядит просто: растет задержка, потом растет доля ошибок, а затем очередь уже не уходит даже после спада входящего потока.
Обычно весь запрос тормозит один медленный этап. Например, модель выдает токены стабильно, но сервис долго ждет свободный воркер для маскирования PII или записи аудита в локальное хранилище. Пользователь видит медленный LLM API, хотя узкое место сидит в обвязке, а не в модели.
На пике ломается не самый дорогой компонент, а самый узкий. Поэтому хороший прогон ищет момент, когда очередь начинает расти быстрее, чем система успевает ее разбирать.
Какие сценарии стоит гнать
Для нагрузочного тестирования мало одного "среднего" запроса. На пике система ломается не там, где вы ждали: короткие чаты проходят нормально, а длинные ответы забивают очередь. Streaming держится, а tool calling резко увеличивает число операций в бэкенде.
Хороший прогон повторяет живой трафик, а не красивый лабораторный случай. Если продакшен еще пустой, соберите смесь по здравому смыслу и проверьте хотя бы пять режимов.
Короткий чат с быстрым ответом нужен почти в любом сервисе: поддержка, внутренний помощник, поиск по базе знаний. Он показывает, сколько запросов система держит без лишней задержки.
Длинный контекст и большой вывод вскрывают совсем другие проблемы. Пользователь загружает договор или длинную переписку и просит сводку на 1500-2000 токенов. Такой тест быстро показывает рост времени генерации, памяти и очередей.
Один и тот же запрос полезно прогнать и в streaming, и без streaming. Для пользователя это разный опыт, а для инфраструктуры - разная нагрузка на соединения, прокси и клиентские таймауты.
Смешанный поток с обычными ответами и tool calling тоже обязателен. Когда модель обращается к поиску, CRM или калькулятору, один запрос легко превращается в цепочку из нескольких операций.
И наконец, нужен резкий всплеск. Не плавный рост, а скачок после рассылки, пуша или начала рабочего дня. Именно такой режим часто ломает rate limit, балансировщик и пул соединений.
Практичный шаблон выглядит так: 70% коротких чатов, 20% длинных запросов с большим ответом и 10% запросов с вызовом инструментов. После этого добавьте burst: в течение двух минут поднимите входящий поток в 3-5 раз и посмотрите, что деградирует первым.
Отдельно проверьте отмену запроса и повторную отправку. Пользователи часто нажимают "стоп", меняют промпт и запускают заново. Если сервис не закрывает старые соединения быстро, вы получите скрытую нагрузку даже при умеренном RPS.
Если вы работаете через шлюз вроде AI Router, не ограничивайтесь тестом только модели. Прогоните те же сценарии через streaming, через маршрутизацию к внешнему провайдеру и на собственной hosted-модели. Тогда сразу видно, где проседает система: на генерации, в прокси-слое, на аудит-логах, маскировании PII или на внешнем backend для tools.
Что измерять до первого прогона
До теста зафиксируйте не одну цифру, а набор метрик. Один только RPS почти ничего не говорит. Для LLM-сервиса важны и запросы в секунду, и число одновременных запросов. Система может спокойно держать 20 RPS при коротких ответах и падать на тех же 20 RPS при длинной генерации.
Отдельно измеряйте время до первого токена и время до конца ответа. Для пользователя это две разные задержки. Если первый токен приходит быстро, интерфейс кажется живым даже при длинном ответе. Если TTFT растет, проблема часто сидит не в модели, а раньше: в балансировщике, роутинге, проверке ключа, маскировании PII или очереди перед GPU.
Очередь тоже нужно видеть по этапам, а не одной строкой в дашборде. Обычно достаточно пяти точек: вход в API, роутинг и выбор модели, очередь на препроцессинг, очередь на инференс, постобработка и логирование. Тогда вы сразу понимаете, где копится хвост. Если очередь растет только перед инференсом, ищите предел GPU или лимит провайдера. Если запросы стоят еще до модели, смотрите CPU, сеть, rate limit, базу логов и все синхронные проверки на входе.
Ошибки полезно разложить по типам. 429, таймауты и 5xx выглядят похоже для клиента, но лечатся по-разному. 429 часто говорят о лимите на ключ, провайдера или пул моделей. Таймауты любят длинные ответы, медленные ретраи и забитую сеть. 5xx чаще всплывают, когда ломается прокси, воркер, логирование или внешняя зависимость.
Размер входа и выхода в токенах нужен почти для каждой метрики. Без него сложно понять, почему одна и та же нагрузка ведет себя по-разному. Запрос на 300 входных токенов и 150 выходных токенов - это один класс нагрузки. Запрос на 8000 входных токенов и длинную генерацию - уже другой.
Еще до старта снимите загрузку CPU, GPU, сети и диска. Смотрите не только на среднее, но и на пики. Диск часто забывают, хотя именно он тормозит аудит-логи, кэш и запись трассировки. Если у вас есть слой маршрутизации вроде AI Router, полезно делить метрики по провайдеру, по собственной GPU-инфраструктуре и по конкретной модели. Иначе вы увидите только "среднюю температуру".
Как собрать план прогона
Начинать лучше не с абстрактного RPS, а с реального трафика. Возьмите логи за обычный день и за самый плотный час. Смотрите не на среднее, а на профиль запросов: сколько у вас коротких чатов, сколько длинных диалогов, сколько вызовов с документами, сколько запросов обрываются по таймауту.
Если вы идете через единый шлюз, такой срез собирать проще. В одном месте видно, какие модели вызывают чаще, где растет длина контекста и в какие минуты очередь начинает распухать.
После этого разложите трафик на группы. Для LLM это важнее, чем общий объем. Запрос на 200 токенов и запрос на 20 000 токенов нагружают систему совершенно по-разному, хотя оба считаются одним вызовом API.
Рабочий план обычно выглядит так:
- Выберите 3-5 типов запросов из логов: короткий чат, длинный чат, RAG с большим контекстом, batch-задача, потоковая генерация.
- Для каждого типа зафиксируйте длину входа и типичный размер ответа. Не ограничивайтесь средними значениями, оставьте и тяжелый хвост.
- Задайте два уровня нагрузки: обычный и пиковый. Первый дает базу, второй показывает, где система начинает срываться.
- Вынесите прогрев в отдельный этап. Сначала прогрейте соединения, кэш, пул воркеров и модель, потом запускайте основной прогон.
- Поднимайте поток ступенями, например каждые 5-10 минут. Один резкий скачок дает много шума, но плохо показывает момент, в котором начинается очередь.
Ступени помогают увидеть переломную точку. До нее задержка растет плавно. После нее p95 и число ошибок уходят вверх почти сразу. Это и есть участок, где надо копать глубже: очередь на входе, лимит провайдера, медленный retrieval, перегретый GPU или узкий пул соединений.
Полезно добавить один сценарий, который похож на реальный пиковый час, а не на лабораторную нагрузку. Например, 70% коротких запросов поддержки, 20% RAG по документам и 10% длинных аналитических запросов. Такой микс обычно дает более честную картину, чем прогон одного "среднего" запроса.
Хороший план прогона отвечает на один вопрос: в какой момент сервис перестает держать ваш реальный трафик без очереди, резкого роста задержки и лишних ошибок.
Как считать очереди без сложной математики
Очередь растет, когда в систему приходит больше работы, чем она успевает завершить. Для первого расчета достаточно двух чисел: скорость прихода и скорость обработки. Если вы получаете 50 запросов в секунду, а сервис завершает 40, очередь растет на 10 запросов в секунду. Через минуту накопится уже 600 запросов.
Это правило работает и в тесте, и в продакшене. Средняя задержка сначала может выглядеть нормально, но хвост быстро портится. Пользователь этого не видит по графику RPS, зато замечает по ответу через 20-30 секунд вместо привычных 3-5.
Смотрите не только на запросы, но и на токены. Два потока по 20 RPS могут грузить систему совершенно по-разному. Короткие запросы на 200-300 токенов пройдут легко, а длинная генерация с большим контекстом забьет модель, даже если RPS не меняется.
Удобно считать отдельно четыре этапа: прием запроса с авторизацией и rate limit, шаги до модели вроде роутинга, маскирования PII и поиска по базе, саму модель с prompt tokens/s и completion tokens/s, а затем шаги после модели - проверку контента, логирование, упаковку и отправку ответа. Так вы быстрее увидите, где образуется пробка.
Иногда модель свободна, а очередь копится еще до нее. Например, сервис долго пишет аудит-логи или тормозит на маскировании персональных данных. Бывает и наоборот: фронт API принимает все быстро, но генерация токенов идет медленно, и очередь уже сидит внутри воркеров модели.
Смотрите, сколько запрос живет в очереди, а не только сколько запросов там лежит. Простая оценка такая: если в очереди 400 запросов, а этап обрабатывает 40 в секунду, новый запрос добавит себе около 10 секунд ожидания. Это грубая оценка, но для первого расчета ее хватает.
Не смешивайте холодный старт и стабильный режим. В первые минуты могут прогреваться воркеры, соединения, кэши и сама модель. Если тестировать оба режима вместе, цифры получаются мутными. Сначала прогрейте сервис, потом измеряйте устойчивую нагрузку. Холодный старт лучше гонять отдельным сценарием.
Где искать узкое место кроме модели
Часто задержку дает не модель, а цепочка вокруг нее. Если разложить запрос по этапам, быстро видно, где копится хвост: вход в API-шлюз, проверка ключа, поиск контекста, вызов модели, проверка ответа, логирование и отправка клиенту.
API-шлюз нередко упирается первым. На пике даже простая проверка ключа, квоты и rate limit на уровне ключа может добавить 20-50 мс на каждый запрос. По одному запросу это почти незаметно, но при сотнях одновременных вызовов такая мелочь быстро превращается в очередь перед моделью.
RAG-поиск тоже часто съедает больше времени, чем ожидают. Векторный поиск, rerank, чтение документов и сбор длинного контекста легко отнимают секунду еще до первого токена. Если модель отвечает стабильно, а p95 растет, причина может быть именно в базе, кэше или сборщике промпта.
Постобработка бьет по пропускной способности не хуже инференса. JSON-валидация, парсинг длинных ответов, проверка схемы, фильтры и даже простые regex на большом потоке нагружают CPU и память. Команда часто смотрит только на GPU, хотя один сервис с валидацией может заметно срезать RPS всему контуру.
Отдельный хвост дают аудит-логи, маскирование PII и метки контента. Если сервис пишет логи синхронно или гоняет каждый ответ через отдельный модуль проверки, задержка растет ступенями. Так бывает в банках, телекоме и госсекторе, где контроль обязателен, но сам контроль тоже нужно тестировать под пиком.
Лимиты провайдера тоже ломают картину. Ваши серверы могут держать нагрузку спокойно, а внешний провайдер упрется в RPM или TPM раньше и начнет отвечать 429 или тянуть время. В схеме с единым шлюзом полезно снимать отдельно время на локальную обработку и отдельно на upstream. Иначе легко обвинить не тот слой.
Сеть проверяют реже, чем стоило бы. DNS, TLS handshake, короткие соединения без keep-alive, лишние прыжки между регионами и медленный балансировщик дают неприятный рваный хвост. Особенно это видно на коротких запросах, где сама модель отвечает быстро, а почти все время уходит на соединение.
Как понять, где именно узко
Самый практичный способ - прогнать тот же трафик с заглушкой модели на фиксированные 100-200 мс. Если задержка почти не меняется, ищите проблему до модели или после нее.
Потом отключайте части контура по одной: RAG, логирование, маскирование PII, JSON-валидацию. Такой тест обычно дает более честный ответ, чем общий график latency.
Снимайте тайминги по этапам в одном trace: gateway, auth, retrieval, upstream, postprocess, logging, response flush. Если model latency ровная, а очередь на gateway растет с 30 до 300 запросов, узкое место уже найдено.
Пример пикового часа
В 19:00 интернет-магазин запускает акцию, и трафик меняется за пару минут. До старта сервис держал, например, 40 запросов в минуту. После пуша и рассылки поток быстро вырастает до 500-700, но запросы уже не одинаковые.
Примерно половина людей задает короткие вопросы по доставке: "Когда привезут?", "Есть ли самовывоз?", "Можно оплатить частями?" Такие ответы короткие, и модель справляется с ними легко. Другая часть пользователей просит подобрать товар по длинному описанию: "Нужен ноутбук для дизайна, до такой-то суммы, с тихим охлаждением и хорошей батареей". Здесь запрос длиннее, в промпт уходит больше контекста, а RAG чаще идет в поиск по каталогу и базе остатков.
В это же время операторы не ждут. CRM продолжает отправлять диалоги на суммаризацию, чтобы менеджеры видели короткую выжимку разговора. Для модели это обычная работа. Для всей системы - еще один поток задач, который ест те же очереди, те же воркеры и часто ту же базу для логов.
В 19:03 модель отвечает почти с прежней скоростью. В 19:05 растет очередь на поиск контекста по каталогу. В 19:07 логирование начинает писать медленнее из-за всплеска событий. К 19:10 пользователи уже видят задержку, хотя GPU еще не уперлись в потолок.
В этом и смысл нагрузочного тестирования. Если этап RAG может обработать 80 запросов в секунду, а в него приходит 100, очередь растет на 20 запросов каждую секунду. Через минуту в ожидании уже 1200 запросов. Модель при этом может оставаться "здоровой": ее p95 почти не меняется, а полная задержка ответа у пользователя растет с 2-3 секунд до 10-15.
Самый неприятный эффект в том, что короткие вопросы по доставке тоже начинают тормозить. Они дешевые, но стоят в общей очереди рядом с тяжелыми подборками товара и суммаризацией диалогов. Если логирование пишет синхронно, а маскирование персональных данных идет в том же пайплайне, задержка растет еще сильнее.
В таком пиковом часе узкое место часто сидит не в модели. Обычно оно оказывается в поиске контекста, логах, пуле соединений к базе или в общем пуле воркеров, где смешали быстрые и тяжелые задачи.
Частые ошибки в тесте
Многие команды получают аккуратные графики и все равно промахиваются с реальной нагрузкой. Причина обычно простая: тест похож на лабораторию, а не на живой трафик.
Самая частая ошибка - гнать один и тот же промпт сотни раз. Так удобно, но такой прогон почти ничего не говорит о пике. В бою запросы отличаются по длине, числу сообщений в истории, ожидаемому размеру ответа и включенным инструментам. Короткий вопрос на 30 токенов и большой диалог с длинным контекстом создают совершенно разную очередь.
Из-за этого команда видит "нормальную" среднюю задержку и успокаивается. Но пользователей бьет не среднее, а хвост: p95, p99, таймауты и всплески ошибок. Если у 90% запросов все быстро, а 10% ждут 20 секунд, сервис уже выглядит сломанным.
Еще один промах - забыть, что клиент и SDK сами делают ретраи. В графике вы видите 100 запросов в секунду, а на деле система получает 130 или 150, потому что часть вызовов пошла повторно после таймаута. Очередь растет резко, и кажется, будто причина только в модели. На практике нагрузку раздувают именно повторы.
Проблемы часто лежат вне модели. Команды тестируют только инференс, но не весь путь запроса: балансировщик, API-шлюз, авторизацию, маскирование PII, аудит-логи, rate limit, сеть до провайдера и обратную отправку стрима клиенту. Если компания работает через единый OpenAI-совместимый шлюз, тест нужен по полному маршруту, а не только по вызову модели в вакууме.
Отдельная ловушка - лимиты провайдера и сеть. Даже если ваша часть системы держит пик, внешний провайдер может резать RPS, токены в минуту или число одновременных запросов. Пара лишних миллисекунд на каждом хопе тоже складывается в очередь.
И наконец, не смешивайте кэшированный и некэшированный трафик в одну кучу. Если половина теста попадает в prompt cache, цифры выглядят слишком красиво. Потом выходит релиз, кэш остывает, и задержка внезапно удваивается. Честнее считать два профиля отдельно и смотреть, как меняется хвост в каждом.
Нагрузочный тест работает только тогда, когда он похож на обычный день вашей системы, а не на удобный синтетический прогон.
Быстрая проверка перед релизом
За час до релиза не пытайтесь "еще немного подкрутить" систему. Лучше быстро подтвердить, что сервис держит обычный день и пик, а команда понимает, где он начнет сыпаться.
Держите под рукой два профиля трафика. Первый показывает обычную нагрузку: средний размер промпта, привычную длину ответа, ровный поток запросов. Второй имитирует пик: больше одновременных сессий, резкие всплески, длинные ответы и долю тяжелых операций вроде JSON-вывода или вызова большой модели. Если есть только один усредненный сценарий, тест почти всегда рисует слишком спокойную картину.
Перед выкладкой достаточно короткого чека:
- вы видите отдельные очереди на каждом этапе: входной шлюз, маскирование PII, маршрутизация, вызов провайдера или своей модели, логирование;
- команда знает предел по одновременным запросам в цифрах, а не "на глаз";
- таймауты заданы явно на каждом шаге: для соединения, первого токена и полного ответа;
- ретраи не создают шторм, если upstream уже медленный;
- мониторинг ловит не только среднюю задержку, но и хвост, 429 и 5xx.
Очереди удобно проверять по этапам, а не по одной общей линии. Если общая latency выросла с 4 до 12 секунд, причина может быть не в модели. Часто время уходит раньше: запрос долго ждет свободный воркер, упирается в rate limit провайдера или застревает на постобработке. В схеме с LLM-шлюзом это особенно заметно: сама модель может отвечать стабильно, а узкое место прячется в роутинге, лимитах по ключу или внешнем провайдере.
Последняя проверка простая и очень полезная: повторите тот же прогон еще раз. Если второй результат близок к первому по p95, ошибкам и длине очередей, тесту можно верить. Если цифры гуляют слишком сильно, сначала разберитесь с окружением и генератором нагрузки, а уже потом выпускайте релиз.
Что делать после прогона
Смотрите не на среднюю задержку, а на участок, который встал первым. Иногда модель отвечает медленно, но чаще раньше сдаются входной лимитер, пул соединений, очередь воркеров, логирование или хранилище аудита. Зафиксируйте точку отказа в цифрах: при каком RPS или числе одновременных запросов выросла очередь, где пошли таймауты и какой компонент первым дал всплеск ошибок.
Потом уберите то, что само разгоняет аварию. Лишние ретраи часто делают только хуже: один медленный ответ превращается в три новых запроса и забивает систему быстрее любой пиковой нагрузки. Если очередь уже вышла за безопасный порог, включайте backpressure: режьте прием новых задач, отдавайте 429 или 503 раньше, ставьте короткий таймаут ожидания в очереди и не держите запрос "на удачу" по 30-60 секунд.
Полезно сразу развести трафик по разным маршрутам. Короткие запросы вроде классификации, извлечения полей или короткого чата не должны стоять за длинными суммаризациями и агентными цепочками. Если смешать их в одной очереди, пользователи коротких сценариев увидят плохую задержку даже тогда, когда общая загрузка еще терпима.
После прогона чаще всего хватает четырех решений:
- Ограничить автоматические ретраи на клиенте и на шлюзе.
- Ввести backpressure по длине очереди, а не ждать, пока все упрется в таймауты.
- Разделить маршруты для коротких и длинных запросов.
- Перепроверить fallback между моделями и лимиты на уровне ключа.
Fallback нужно тестировать отдельно. Нельзя просто считать, что он "сработает". Если основная модель замедлилась, запасной маршрут не должен удвоить цену, сломать формат ответа или отправить весь трафик в того же провайдера через другой алиас. Лимиты на уровне ключа тоже важны: один шумный клиент легко съедает емкость и портит SLA для остальных.
Небольшой пример: если 80% трафика - это короткие запросы до 300 входных токенов, а 20% - длинные задачи на несколько тысяч токенов, держать их в одном пуле обычно плохая идея. Разделение очередей часто дает заметный выигрыш даже без замены модели.
Если вам нужно сравнить один и тот же профиль нагрузки на разных маршрутах, это удобно делать через единый OpenAI-совместимый шлюз. Например, в AI Router на airouter.kz можно отдельно прогнать сценарии через внешнего провайдера и через свои open-weight модели, не меняя SDK, код и промпты. Такой тест быстро показывает, где именно сидит узкое место: в маршрутизации, в лимитах upstream, в локальной GPU-инфраструктуре или в клиентских ретраях.
Часто задаваемые вопросы
Как понять, что тормозит не модель, а обвязка?
Смотрите тайминги по этапам, а не только на общий latency. Если model latency почти ровный, а очередь растет на gateway, в RAG, логах или postprocess, проблема сидит в обвязке.
Какие метрики смотреть кроме средней задержки?
Средняя задержка скрывает хвост. Держите рядом p95, p99, долю таймаутов, TTFT, время полного ответа, число одновременных запросов и длину очередей по этапам.
Зачем измерять время до первого токена отдельно?
TTFT показывает, когда пользователь видит первый признак жизни интерфейса. Если первый токен приходит поздно, ищите причину до инференса: в auth, роутинге, PII masking, очереди или сети.
Какие сценарии нужны в хорошем нагрузочном тесте?
Берите не один "средний" запрос, а смесь живого трафика. Обычно хватает короткого чата, длинного контекста с большим выводом, streaming, запросов с tool calling и резкого burst после спокойного фона.
Как быстро прикинуть рост очереди без сложной математики?
Сравните скорость прихода и скорость обработки. Если приходит 50 запросов в секунду, а этап завершает 40, очередь растет на 10 в секунду и новый запрос начнет ждать все дольше.
Почему ретраи так опасны на пике?
Потому что повторы сами раздувают пик. Один таймаут запускает новый запрос, потом еще один, и через пару минут система тратит силы уже не на полезный трафик, а на собственный шторм.
Нужно ли тестировать streaming отдельно?
Да, это отдельный сценарий. Streaming держит соединения дольше, по-другому нагружает прокси и сильнее цепляет клиентские таймауты, поэтому цифры без streaming часто дают слишком спокойную картину.
Где чаще всего прячется узкое место кроме модели?
Часто первым сдается API-шлюз, RAG-поиск, аудит-лог, PII masking, JSON-валидация, пул соединений или сеть до провайдера. Даже лишние 20–50 мс на каждом шаге на большом потоке быстро собираются в очередь.
Как проверить fallback между моделями?
Прогоняйте fallback тем же трафиком, что и основной маршрут. Смотрите не только на ошибки, но и на цену, формат ответа, TTFT и то, не уходит ли запасной путь в того же провайдера под другим именем.
Что делать после прогона, если очередь уже растет?
Сразу включайте backpressure и режьте лишние повторы. Потом разделите короткие и длинные запросы по разным очередям, зафиксируйте точку срыва в цифрах и проверьте, какой этап встал первым.