Как вы проектируете backpressure в LLM serving системе?
Краткий тезис
Backpressure — это механизм управления нагрузкой, который защищает LLM serving систему от перегрузки, когда входящий поток запросов превышает пропускную способность инференса. Проектирование включает bounded очередь запросов с явной политикой отказа (HTTP 429), breaker|circuit breaker на стороне клиента, мониторинг длины очереди и времени ожидания, а также graceful degradation для приоритетных запросов. Без backpressure сервер может упасть из-за OOM или бесконечно расти latency, делая систему непригодной для production.
1. Термин: Backpressure (обратное давление)
Backpressure — это принцип управления потоком данных, при котором потребитель (сервер инференса) сигнализирует производителю (клиенту) о своей текущей загрузке и ограничивает приём новых запросов, если не может их обработать. В контексте LLM serving backpressure реализуется на уровне очереди запросов, rate limiter и circuit breaker.
Зачем нужен backpressure:
- Предотвращение OOM (Out of Memory) — LLM модели потребляют много GPU памяти, одновременный запуск десятков запросов может исчерпать VRAM.
- Контроль latency — без backpressure время ответа растёт нелинейно при перегрузке, пользователи получают таймауты.
- Обеспечение fairness — равномерное распределение ресурсов между клиентами.
- Graceful degradation — система отказывает явно (429), а не падает с 500 или зависает.
2. Проблема перегрузки LLM сервера
LLM serving (например, на базе vLLM, TensorRT-LLM, TGI) имеет фиксированную пропускную способность, определяемую:
- Размером модели (количество параметров)
- Типом GPU (H100, A100, L40S)
- Batch size (динамический батчинг)
- Длиной контекста (max tokens)
Если входящий RPS (requests per second) превышает capacity, возникают:
| Проблема | Последствия |
|---|---|
| Рост очереди | Latency растёт линейно с длиной очереди, пользователи получают таймауты |
| OOM | GPU memory исчерпывается, сервер падает с segfault |
| Деградация качества | При forced batching модель может выдавать мусор из-за превышения max context |
| Неравномерность | Одни клиенты «забивают» очередь, другие голодают |
Backpressure решает эти проблемы, ограничивая входной поток до уровня, который сервер может обработать с приемлемой latency.
3. Компоненты проектирования backpressure
3.1 Bounded очередь запросов
Очередь с фиксированным максимальным размером (bounded queue). Когда очередь заполнена, новые запросы отклоняются.
Выбор размера очереди
- Слишком маленькая → много отказов даже при кратковременных пиках.
- Слишком большая → latency растёт, пользователи ждут.
Формула: queue_size = max_latency_ms / avg_inference_time_ms * expected_concurrency
Пример: допустимая задержка 5 с, среднее время инференса 500 мс, ожидаемая конкуренция 10 → 5000/500*10 = 100 мест.
Политики при переполнении
- Drop — отбрасывать запрос (возвращать 429).
- Reject — возвращать ошибку с Retry-After.
- Delay — блокировать клиента (не рекомендуется в HTTP).
3.2 Rate Limiting (ограничение скорости)
Ограничение количества запросов в единицу времени на клиента или на API ключ. Реализуется через token bucket или leaky bucket.
Пример: 10 RPS на пользователя, 100 RPS на сервис.
3.3 Circuit Breaker (автоматический выключатель)
Паттерн, при котором клиент перестаёт отправлять запросы на сервер, если тот начал отвечать ошибками (429, 503). После таймаута (например, 30 с) клиент пробует снова.
Состояния circuit breaker
- Closed — запросы идут нормально.
- Open — запросы сразу отклоняются (fallback).
- Half-Open — пробный запрос для проверки восстановления.
3.4 Load Shedding (сброс нагрузки)
Приоритизация запросов: низкоприоритетные (batch-задачи, фоновые) отбрасываются первыми, высокоприоритетные (интерактивные) обслуживаются.
4. Проектирование очереди запросов
В production LLM serving обычно используется асинхронная очередь (например, asyncio.Queue в Python или BlockingQueue в Java). Сервер запускает фиксированное количество воркеров (worker), каждый воркер берёт запрос из очереди, выполняет инференс и возвращает результат.
import asyncio
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
MAX_QUEUE_SIZE = 100
queue = asyncio.Queue(maxsize=MAX_QUEUE_SIZE)
class InferenceRequest(BaseModel):
prompt: str
max_tokens: int = 256
class InferenceResponse(BaseModel):
result: str
async def worker():
while True:
request, future = await queue.get()
try:
# Симуляция инференса
await asyncio.sleep(0.5)
result = f"Processed: {request.prompt}"
future.set_result(result)
except Exception as e:
future.set_exception(e)
finally:
queue.task_done()
# Запуск воркеров при старте
@app.on_event("startup")
async def startup():
for _ in range(4): # 4 воркера
asyncio.create_task(worker())
@app.post("/infer", response_model=InferenceResponse)
async def infer(request: InferenceRequest):
if queue.full():
raise HTTPException(status_code=429, detail="Too Many Requests")
loop = asyncio.get_event_loop()
future = loop.create_future()
await queue.put((request, future))
result = await future
return InferenceResponse(result=result)
Ключевые моменты
maxsizeочереди — bounded.- При переполнении — HTTP 429.
- Future позволяет воркеру вернуть результат асинхронно.
5. HTTP 429 и Retry-After
Когда сервер отклоняет запрос из-за перегрузки, он должен вернуть статус 429 Too Many Requests и заголовок Retry-After (в секундах).
Пример ответа:
HTTP/1.1 429 Too Many Requests
Retry-After: 5
Content-Type: application/json
{"error": "Server overloaded, try again later"}
Клиент (или API gateway) должен прочитать Retry-After и подождать перед повторной попыткой. Это предотвращает «шторм повторных запросов» (retry storm).
6. Circuit Breaker на клиенте
Клиентская сторона также должна реализовать backpressure, чтобы не заваливать сервер повторными запросами после 429.
Реализация на Python с pybreaker
import pybreaker
import requests
breaker = pybreaker.CircuitBreaker(fail_max=5, reset_timeout=30)
@breaker
def call_llm(prompt: str) -> str:
resp = requests.post("http://llm-server/infer", json={"prompt": prompt})
if resp.status_code == 429:
raise pybreaker.CircuitBreakerError("Server overloaded")
return resp.json()["result"]
def fallback(prompt: str) -> str:
return "Сервис временно недоступен, попробуйте позже."
# Использование
try:
result = call_llm("Привет")
except pybreaker.CircuitBreakerError:
result = fallback("Привет")
Параметры circuit breaker
fail_max— количество ошибок до размыкания.reset_timeout— время в секундах до перехода в half-open.expected_exceptions— какие ошибки считать сбоями (429, 503, таймауты).
7. Мониторинг и алертинг
Без мониторинга backpressure слеп. Ключевые метрики:
| Метрика | Источник | Порог алерта |
|---|---|---|
| Queue length | Очередь сервера | > 80% от maxsize |
| Average queue wait time | Время в очереди | > 1 с |
| HTTP 429 rate | Ответы сервера | > 5% от всех запросов |
| Circuit breaker state | Клиент | Open > 10% времени |
| P50/P99 latency | Инференс | P99 > 2x от baseline |
Инструменты Prometheus + Grafana, Datadog, New Relic.
Пример метрики queue length в Prometheus:
from prometheus_client import Gauge
queue_length = Gauge('llm_queue_length', 'Current queue size')
# Обновлять при каждом put/get
queue_length.set(queue.qsize())
8. Graceful degradation и приоритизация
В production часто нужно различать типы запросов:
- Интерактивные (чат, API от пользователя) — высокая приоритет, низкая latency.
- Batch (фоновые задачи, анализ логов) — низкая приоритет, можно ждать.
Реализация: несколько очередей с разными приоритетами (priority queue) или отдельные пулы воркеров.
import asyncio
from dataclasses import dataclass, field
from typing import Optional
import heapq
@dataclass(order=True)
class PrioritizedItem:
priority: int
request: object = field(compare=False)
future: asyncio.Future = field(compare=False)
priority_queue = [] # min-heap
async def priority_worker():
while True:
item = heapq.heappop(priority_queue)
# обработать item.request
При перегрузке низкоприоритетные запросы могут быть отклонены раньше (load shedding).
9. Сравнение стратегий backpressure
| Стратегия | Описание | Плюсы | Минусы |
|---|---|---|---|
| Bounded queue + reject | Очередь фикс. размера, при переполнении 429 | Простота, предсказуемость | Потеря запросов при пиках |
| Rate limiting | Ограничение RPS на клиента | Защита от «шумных соседей» | Не защищает от общего роста нагрузки |
| Circuit breaker | Автоматическое отключение клиента | Предотвращает retry storm | Может быть слишком агрессивным |
| Load shedding | Приоритизация и сброс низкоприоритетных | Сохраняет качество для важных запросов | Сложность реализации |
| Adaptive concurrency | Динамическое ограничение на основе latency | Оптимальное использование ресурсов | Требует хорошего мониторинга |
В production обычно комбинируют все: bounded queue + rate limiting на gateway + circuit breaker на клиенте + load shedding для batch.
10. Пример полной архитектуры backpressure
[Client] --(circuit breaker)--> [API Gateway] --(rate limiter)--> [LLM Server]
|
[Bounded Queue]
|
[Worker Pool]
|
[GPU Inference]
|
[Metrics (Prometheus)]
API Gateway (например, Kong, Envoy) реализует rate limiting и возвращает 429 раньше, чем запрос дойдёт до LLM сервера. LLM сервер имеет bounded очередь и возвращает 429 при переполнении. Клиент использует circuit breaker, чтобы не слать запросы при постоянных 429.
Пет-проект для закрепления
Задача Реализовать минимальный LLM serving сервер с backpressure и клиентом с circuit breaker.
Инструменты Python, FastAPI, asyncio, pybreaker, requests.
Шаги:
- Создай FastAPI сервер с bounded очередью (
asyncio.Queue(maxsize=10)) и 2 воркерами, которые симулируют инференс (time.sleep(1)). - Добавь эндпоинт
/infer, который при переполнении очереди возвращает 429 с Retry-After. - Напиши клиент, который использует
pybreaker.CircuitBreakerс fail_max=3 и reset_timeout=10. - Запусти сервер и отправь 20 параллельных запросов. Наблюдай, как часть получает 429, а circuit breaker на клиенте перестаёт слать запросы после 3 ошибок.
- Добавь Prometheus метрику длины очереди и latency.
Ожидаемый результат Система не падает при перегрузке, клиенты получают явные ошибки, а не таймауты, и после восстановления сервера клиенты автоматически возобновляют запросы.
Связь с другими вопросами
| Вопрос | Тема |
|---|---|
| 243 | Как вы обеспечиваете низкую latency в LLM serving? |
| 245 | Как вы реализуете rate limiting для LLM API? |
| 246 | Как вы проектируете очередь запросов для batch inference? |
| 247 | Как вы мониторите LLM serving систему? |
| 248 | Какие метрики качества обслуживания (SLO) вы задаёте? |
| 249 | Как вы обрабатываете длинные контексты в serving? |
Навигация
- Предыдущий: 243
- Следующий: 245
- Индекс: 00. Индекс разборов