中文翻译暂不可用,显示俄语原文。
Как вы проектируете backpressure в LLM serving системе?
Краткий тезис
Backpressure — это механизм управления нагрузкой, предотвращающий перегрузку LLM serving системы. Он включает bounded очередь запросов, HTTP 429 при переполнении, breaker|circuit breaker на клиенте и автомасштабирование реплик. Ключевая идея — не дать системе упасть под лавиной запросов, а контролируемо отклонять или задерживать их, сохраняя стабильное время ответа для принятых запросов. Проектирование backpressure требует баланса между пропускной способностью (throughput), задержкой (latency) и надёжностью (reliability).
1. Термин: Backpressure (обратное давление)
Backpressure — это механизм, при котором потребитель (LLM serving) сигнализирует производителю (клиенту) о своей текущей загрузке и ограничивает поток запросов, чтобы избежать перегрузки. В контексте LLM serving это критично, потому что генерация токенов — ресурсоёмкая операция (GPU, память), и один медленный запрос может заблокировать очередь для других.
Почему backpressure важен для LLM serving:
- Длительное время генерации: LLM отвечает токен за токеном, запрос может занимать секунды.
- Высокая вариативность нагрузки: пиковые нагрузки от пользователей могут в 10–100 раз превышать средние.
- Ограниченные ресурсы: GPU-память и вычислительная мощность конечны.
- Эффект «головы в песке»: без backpressure система пытается обработать всё, latency растёт, запросы таймаутятся, и система падает.
Основные компоненты backpressure
- Bounded queue (очередь с ограничением) — буфер запросов фиксированного размера.
- Rate limiter (ограничитель скорости) — контролирует количество запросов в единицу времени.
- Circuit breaker (автоматический выключатель) — размыкает цепь при превышении порога ошибок.
- Load shedding (сброс нагрузки) — отклонение части запросов при перегрузке.
2. Компоненты системы backpressure
2.1 Bounded очередь запросов
Очередь с фиксированным размером (например, 1000 запросов) — первый уровень защиты. Когда очередь заполнена, новые запросы отклоняются.
Параметры
queue_size— максимальное количество ожидающих запросов.- timeout — максимальное время ожидания в очереди (например, 5 секунд).
rejection_policy— что делать при переполнении: HTTP 429, возврат ошибки, перенаправление.
Пример конфигурации (Python + FastAPI):
from fastapi import FastAPI, HTTPException
from collections import deque
import asyncio
app = FastAPI()
MAX_QUEUE_SIZE = 1000
request_queue = deque(maxlen=MAX_QUEUE_SIZE)
@app.post("/generate")
async def generate(prompt: str):
if len(request_queue) >= MAX_QUEUE_SIZE:
raise HTTPException(status_code=429, detail="Too Many Requests")
request_queue.append(prompt)
try:
result = await process_llm(prompt)
return result
finally:
request_queue.popleft()
2.2 HTTP 429 с заголовком Retry-After
При переполнении очереди или превышении лимита скорости сервер возвращает HTTP 429 (Too Many Requests) с заголовком Retry-After, указывающим клиенту, через сколько секунд можно повторить запрос.
Пример ответа
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 5
{"error": "Rate limit exceeded", "retry_after": 5}
Зачем Retry-After Клиент не будет бесконечно долбить сервер, а подождёт указанное время, снижая нагрузку.
2.3 Circuit breaker на клиенте
Circuit breaker — паттерн, при котором клиент отслеживает количество ошибок (например, 429 или таймауты) и при превышении порога временно перестаёт отправлять запросы к серверу, переходя в состояние open (разомкнут). Через заданное время он переходит в half-open (полуоткрыт) и пробует один запрос. Если успешно — возвращается в closed (замкнут).
Реализация на Python
import time
import requests
class CircuitBreaker:
def __init__(self, failure_threshold=5, recovery_timeout=30):
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.failure_count = 0
self.last_failure_time = None
self.state = "closed" # closed, open, half-open
def call(self, url, payload):
if self.state == "open":
if time.time() - self.last_failure_time > self.recovery_timeout:
self.state = "half-open"
else:
raise Exception("Circuit breaker is open")
try:
response = requests.post(url, json=payload, timeout=10)
if response.status_code == 429:
raise Exception("Rate limited")
self.failure_count = 0
self.state = "closed"
return response.json()
except Exception as e:
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = "open"
raise e
2.4 Мониторинг длины очереди
Длина очереди — ключевая метрика для backpressure. Её нужно мониторить в реальном времени, например, через Prometheus.
Пример метрики (Python + prometheus_client):
from prometheus_client import Gauge, start_http_server
import random
import time
queue_length = Gauge('llm_request_queue_length', 'Current queue length')
def update_queue_metric():
while True:
queue_length.set(len(request_queue))
time.sleep(1)
Алерт при превышении порога Если длина очереди > 80% от capacity, нужно отправлять алерт (например, в PagerDuty или Slack).
Пример правила алерта (PromQL):
alert: QueueHigh
expr: llm_request_queue_length / 1000 > 0.8
for: 1m
labels:
severity: warning
annotations:
summary: "Queue length > 80% capacity"
3. Стратегии backpressure
3.1 Rate limiting (ограничение скорости)
Ограничивает количество запросов от одного клиента или суммарно в единицу времени. Реализуется через token bucket или leaky bucket алгоритмы.
Сравнение алгоритмов
| Алгоритм | Описание | Плюсы | Минусы |
|---|---|---|---|
| Token bucket | Токены добавляются с постоянной скоростью, запрос потребляет токен. | Позволяет кратковременные всплески. | Сложнее реализовать. |
| Leaky bucket | Запросы поступают в очередь и обрабатываются с фиксированной скоростью. | Простота, предсказуемая задержка. | Не допускает всплесков. |
| Fixed window | Лимит на фиксированный интервал (например, 100 запросов в минуту). | Простая реализация. | Проблема «границы окна» (burst в конце окна). |
| Sliding window | Скользящее окно, учитывает запросы за последние N секунд. | Более точное ограничение. | Требует хранения истории. |
Пример token bucket на Python
import time
class TokenBucket:
def __init__(self, rate, capacity):
self.rate = rate # токенов в секунду
self.capacity = capacity
self.tokens = capacity
self.last_refill = time.time()
def consume(self, tokens=1):
now = time.time()
elapsed = now - self.last_refill
self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
self.last_refill = now
if self.tokens >= tokens:
self.tokens -= tokens
return True
return False
3.2 Load shedding (сброс нагрузки)
При перегрузке система отклоняет часть запросов, чтобы сохранить стабильность для остальных. Критерии сброса:
- Длина очереди > порога.
- Latency > порога (например, p99 > 5 секунд).
- CPU/GPU utilisation > 90%.
Приоритеты запросов Можно отклонять менее важные запросы (например, batch-задачи) в пользу интерактивных.
3.3 Adaptive concurrency (адаптивная конкурентность)
Динамически регулирует количество параллельно обрабатываемых запросов на основе метрик, таких как latency или utilisation. Алгоритм CoDel (Controlled Delay) или TCP Vegas могут быть адаптированы для LLM serving.
Идея Если latency начинает расти, уменьшаем concurrency. Если latency низкая — увеличиваем.
4. Автомасштабирование реплик
Backpressure — это не только отклонение запросов, но и автоматическое увеличение ресурсов. Horizontal Pod Autoscaler (HPA) в Kubernetes может масштабировать реплики на основе длины очереди или custom metrics.
Пример HPA на основе длины очереди (Prometheus adapter):
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: llm-serving-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: llm-serving
minReplicas: 2
maxReplicas: 20
metrics:
- type: Pods
pods:
metric:
name: llm_request_queue_length
target:
type: AverageValue
averageValue: 500
Пороги масштабирования
- Scale up: когда средняя длина очереди > 500 (50% capacity) в течение 1 минуты.
- Scale down: когда средняя длина очереди < 100 в течение 5 минут.
Важно Масштабирование не мгновенное (поды поднимаются 30–60 секунд), поэтому backpressure должен работать и во время масштабирования.
5. Пример полной архитектуры backpressure
[Client] --(запрос)--> [API Gateway] --(rate limit)--> [Load Balancer] --(queue)--> [LLM Serving Pods]
| |
v v
[Circuit Breaker] [Prometheus]
|
v
[HPA] --> [Kubernetes]
Поток
- Клиент отправляет запрос.
- API Gateway проверяет rate limit (token bucket) и возвращает 429 при превышении.
- Если лимит не превышен, запрос попадает в bounded очередь (например, Redis List).
- LLM serving pod забирает запрос из очереди и обрабатывает.
- Prometheus мониторит длину очереди, latency, utilisation.
- При превышении порогов HPA масштабирует поды.
- Если очередь переполнена, API Gateway возвращает 429 с Retry-After.
- Клиентский circuit breaker при множестве 429 перестаёт отправлять запросы на время.
6. Метрики для мониторинга backpressure
| Метрика | Описание | Порог для алерта |
|---|---|---|
queue_length | Текущая длина очереди | > 80% capacity |
queue_latency | Время ожидания в очереди | > 2 секунд |
request_rate | Количество запросов в секунду | > 90% от лимита |
error_rate_429 | Доля ответов 429 | > 5% |
p99_latency | 99-й перцентиль времени ответа | > 10 секунд |
gpu_utilisation | Загрузка GPU | > 95% |
pod_count | Количество реплик | Сравнение с ожидаемым |
7. Продвинутые техники
7.1 Backpressure на уровне GPU
LLM serving часто использует continuous batching (динамическое объединение запросов в батч). Если GPU перегружен, можно временно отключать приём новых запросов в батч, пока текущие не завершатся.
Пример: vLLM поддерживает max_num_seqs — максимальное количество последовательностей в одном батче. При превышении новые запросы ставятся в очередь.
7.2 Backpressure на основе стоимости токенов
Не все запросы одинаковы: генерация длинного ответа (например, 4096 токенов) потребляет больше ресурсов, чем короткого. Можно ввести стоимость запроса (например, количество токенов) и ограничивать суммарную стоимость в единицу времени.
Пример: Лимит 100 000 токенов в минуту на клиента.
7.3 Graceful degradation (плавная деградация)
При перегрузке система может не отклонять запросы, а возвращать упрощённый ответ (например, использовать меньшую модель, сократить max_tokens, отключить RAG). Это лучше, чем 429.
8. Пет-проект для закрепления
Задача Реализовать систему backpressure для LLM serving с очередью, rate limiter, circuit breaker и мониторингом.
Инструменты
- Python + FastAPI (сервер)
- Redis (очередь)
- Prometheus + Grafana (мониторинг)
- Docker Compose (локальный запуск)
Шаги:
- Создайте FastAPI-сервер с эндпоинтом
/generate, который принимает запросы и ставит их в Redis List. - Реализуйте bounded очередь: если длина списка > 1000, возвращайте 429.
- Добавьте rate limiter на основе token bucket (например, 10 запросов в секунду на клиента).
- Реализуйте circuit breaker на клиенте (отдельный скрипт), который при 5 ошибках 429 перестаёт отправлять запросы на 30 секунд.
- Экспортируйте метрики (длина очереди, количество 429, latency) в Prometheus.
- Настройте алерт в Prometheus при длине очереди > 800.
- Запустите нагрузочное тестирование (например, с помощью locust) и убедитесь, что система не падает, а возвращает 429.
Ожидаемый результат
- При низкой нагрузке все запросы обрабатываются.
- При пиковой нагрузке часть запросов получает 429, но система остаётся стабильной.
- Circuit breaker на клиенте снижает количество повторных запросов.
- В Grafana видно, как длина очереди растёт и падает, а количество реплик масштабируется.
Связь с другими вопросами
| Вопрос | Тема |
|---|---|
| 410 | Как вы проектируете LLM serving систему? |
| 412 | Как вы управляете очередями запросов в LLM serving? |
| 413 | Как вы реализуете rate limiting для LLM API? |
| 414 | Как вы мониторите LLM serving систему? |
| 415 | Как вы масштабируете LLM serving горизонтально? |
| 416 | Как вы обрабатываете ошибки в LLM serving? |
Навигация
- Предыдущий: 410
- Следующий: 412
- Индекс: 00. Индекс разборов