Четыре свойства LLM-трафика
- Запросы не однотипные: web ~50 мс · LLM 10 с – 10+ мин · агент ~1 мин. Длительность requests разбросана в 100× (до 100 000× с учётом prefill).
- Стоимость ∝ токенам: prefill + decode; 128-token prompt → ~10 MB KV-кэша, 32K-token prompt → ~2.5 GB. Слот занят на минуты.
- Кэш — состояние на ноде: поток запросов вытесняет KV-кэш → следующий ход сессии пересчитывает prefill.
- Bursty: агент (pi.dev, Cursor, Kilo) создаёт 3–10 параллельных запросов на человека, а не 1.
Почему это ломает round-robin
- RR распределяет число запросов, а не стоимость. Длинный запрос занимает слот на минуты, следующий по кругу падает на ту же ноду → очередь, пока другие простаивают.
- Кэш протухает под потоком коротких запросов → следующий ход сессии пересчитывает prefill.
- «Fairness by request count is not fairness by cost» (NVIDIA Dynamo / Ace The Cloud, 2026).
Цифры: token-weighted shortest-queue — P99 TTFT 4200 → 650 мс (6.5× лучше RR); llm-d (Tesla production) — до 3× throughput и TTFT в 2 раза ниже против RR (cache-aware routing).
Что видит балансер: 3 реплики, 12 секунд
R1: на реплику прилетает большой запрос (100K prefill + decode, 8 с) — слот занят весь отрезок, кэш сессии живёт — вытеснять его нечем. R2/R3: прилетает поток коротких запросов (1.5 с) — KV/prefix-кэш протухает между ними, и следующий ход сессии пересчитывает prefill заново.
Одна реплика «думает» минуты, на других кэш протухает — балансировать надо нагрузку и кэш, а не число запросов.
Сигналы балансировки
| Сигнал | Источник | Правило |
|---|---|---|
| Load | vLLM /metrics (3 с) + in-flight WLC | Сначала без очереди (no-wait), затем least-loaded |
| Session | header x-session-id | pin: TTL 60 мин, rebind после 3 spill |
| Prefix | MD5(system prompt) | pin: TTL 5 мин, отпускается при перегрузе |
| Health | scrape + 5xx | LIVE ⇄ PROBING ⇄ EJECTED, активный probe |
- Sticky: сессия привязана к реплике с тёплым кэшем — TTFT на турах 2+ падает в 4–5 раз (индустриальные замеры, не наш деплой). Session affinity экономит 70–90% prefill-вычислений на турах 2+ (10-туровый диалог, 8K контекст).
- Load: забитая vLLM-реплика (waiting > 0) — вне роутинга; запросы уходят на свободные.
- Cold start: новая сессия — на холодную реплику (least-loaded), не в чужую очередь.
Набор сигналов совпадает с индустриальной практикой — колесо не изобретали: llm-d (scoring: cache + prefill/decode + SLA + load), NVIDIA Dynamo (worker-reported KV-состояние), SGLang (router-predicted radix tree), Gateway API Inference Extension (KV cache % + queue + LoRA). «load + prefix/KV-locality + health» — де-факто стандарт 2026.
Что мы видели в референс-деплое
Наш Python-балансировщик роутит 4 реплики vLLM по этим сигналам (sticky, load, health, cold start). Подробности — в части 4 серии «Наш опыт».
Вывод
LLM-трафик — не HTTP-трафик: запросы различаются в 100× по стоимости, кэш — состояние на ноде, нагрузка bursty. Балансировать надо стоимость и локальность кэша, а не число соединений.
Смежные материалы: Балансировка LLM-трафика (часть 4 серии), Multi-tenant Serving.