中文翻译暂不可用,显示俄语原文。

Как вы проектируете 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 растёт линейно с длиной очереди, пользователи получают таймауты
OOMGPU 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.

Шаги:

  1. Создай FastAPI сервер с bounded очередью (asyncio.Queue(maxsize=10)) и 2 воркерами, которые симулируют инференс (time.sleep(1)).
  2. Добавь эндпоинт /infer, который при переполнении очереди возвращает 429 с Retry-After.
  3. Напиши клиент, который использует pybreaker.CircuitBreaker с fail_max=3 и reset_timeout=10.
  4. Запусти сервер и отправь 20 параллельных запросов. Наблюдай, как часть получает 429, а circuit breaker на клиенте перестаёт слать запросы после 3 ошибок.
  5. Добавь Prometheus метрику длины очереди и latency.

Ожидаемый результат Система не падает при перегрузке, клиенты получают явные ошибки, а не таймауты, и после восстановления сервера клиенты автоматически возобновляют запросы.


Связь с другими вопросами

ВопросТема
243Как вы обеспечиваете низкую latency в LLM serving?
245Как вы реализуете rate limiting для LLM API?
246Как вы проектируете очередь запросов для batch inference?
247Как вы мониторите LLM serving систему?
248Какие метрики качества обслуживания (SLO) вы задаёте?
249Как вы обрабатываете длинные контексты в serving?

Навигация