单卡爆改生产级网关:vLLM + LiteLLM 搭建高并发流式代理与动态负载实战

4次阅读
没有评论

共计 4816 个字符,预计需要花费 13 分钟才能阅读完成。

上个月帮朋友的小团队改造内部 AI 知识库,他们拉了一台单张 RTX 4090 的工作站,跑着默认的 Ollama 提供 OpenAI 兼容接口。前端 4 个研发同事同时点“生成代码”,后台直接排队,首字延迟(TTFT)飙到 8 秒开外,偶发直接抛 504 Gateway Timeout。很多人把本地模型跑通就以为万事大吉,但 “能跑”和“生产可用”之间,差了一整套推理优化与流量路由架构 。

本文不谈空洞概念,直接拆解我目前在用的生产级单卡 / 双卡 LLM 落地架构: 底层用 vLLM 接管模型推理以榨干 PagedAttention 显存并发能力,上层挂载 LiteLLM Proxy 作为企业级鉴权、分流与流式网关 。整套方案跑在 Docker 环境下,兼顾极低延迟与动态高可用。

一、架构设计:为什么舍弃纯原生 Ollama?

Ollama 适合个人开发尝鲜,但面对多客户端高并发时,其显存管理与 Batch 调度相对保守。vLLM 的核心优势在于利用类似操作系统的分页算法(PagedAttention),彻底解决了 KV Cache 的内存碎片化问题,在相同显存下能容纳 3~5 倍的并发 Request。

而直接将 vLLM 暴露给前端业务同样危险——缺少 Token 级速率限制、无法做多模型 Fallback、缺少细粒度的 API Key 审计。LiteLLM Proxy 则是最轻量的反代层,几乎零损耗透传 SSE(Server-Sent Events)流式响应,同时还能在本地小模型爆显存或超时时,无缝降级到公网商业 API。

二、底层引擎:vLLM 生产级启动调优与避坑

以单张 24GB 显存的显卡(如 RTX 4090 / RTX 3090 / A5000)部署 Qwen2.5-14B-Instruct-AWQ(4bit 量化)为例,显存占用与并发调优需要经过精细计算,盲目拉满默认参数大概率遭遇 OOM(Out Of Memory)。

1. 关键启动脚本 (docker-compose.yml 片段)

version: '3.8'

services:
  vllm-qwen:
    image: vllm/vllm-openai:latest
    container_name: vllm-engine
    runtime: nvidia
    environment:
      - HUGGING_FACE_HUB_TOKEN=your_token_here
    volumes:
      - /data/huggingface/hub:/root/.cache/huggingface/hub
    ports:
      - "8001:8000"
    ipc: host
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]
    command: >
      --model Qwen/Qwen2.5-14B-Instruct-AWQ
      --quantization awq
      --dtype half
      --gpu-memory-utilization 0.90
      --max-model-len 8192
      --max-num-seqs 64
      --enforce-eager
      --trust-remote-code

2. 踩坑参数深度解析:

  • --gpu-memory-utilization 0.90:我在测试时踩过大坑,如果直接给 0.95,在超长 Prompt 瞬间分配临时 Activation 显存时,PyTorch 会直接 CUDA OOM 崩溃退出。保留 10% 的显存缓冲至关重要。
  • --max-model-len 8192:官方模型可能支持 32k 甚至 128k 上下文,但在并发场景下,单请求保留过大上下文窗口会吃光 KV Cache,导致能容纳的并发数骤降。根据业务真实场景限制在 8k,性价比最高。
  • --enforce-eager:消费级显卡(如 4090)在首次启动编译 CUDA Graph 时经常耗时 5 分钟甚至报驱动段错误,加上这个参数关闭 CUDA Graph 捕获,虽然牺牲约 3% 的吞吐,但启动速度提升 10 倍且极其稳定。
  • ipc: host:必须配置!PyTorch 多进程通信依赖共享内存,Docker 默认的 64MB shm 会直接导致高并发时报 Bus error (core dumped)。

三、中间网关:LiteLLM 动态路由与流式容灾配置

vLLM 服务只监听内网 8001 端口,我们用 LiteLLM 作为流量主入口。创建配置文件 litellm_config.yaml:

model_list:
  # 主力模型:本地 vLLM 实例
  - model_name: internal-chat
    litellm_params:
      model: openai/Qwen/Qwen2.5-14B-Instruct-AWQ
      api_base: http://vllm-qwen:8000/v1
      api_key: "EMPTY"
      timeout: 30
      max_retries: 1

  # 容灾备份模型:公网 DeepSeek-V3 或公网 API
  - model_name: internal-chat-fallback
    litellm_params:
      model: deepseek/deepseek-chat
      api_key: os.environ/DEEPSEEK_API_KEY
      timeout: 15

router_settings:
  routing_strategy: "latency-based-routing"
  redis_host: "redis"
  redis_port: 6379

general_settings:
  master_key: "sk-nassky-admin-secret-2026"
  store_model_in_db: false

启动 LiteLLM Proxy:

docker run -d \
  --name litellm-proxy \
  --network vllm-net \
  -p 4000:4000 \
  -v $(pwd)/litellm_config.yaml:/app/config.yaml \
  -e DEEPSEEK_API_KEY="sk-xxxx" \
  ghcr.io/berriai/litellm:main-latest \
  --config /app/config.yaml --port 4000

此时对外暴露的标准 OpenAI 端点为 http://your-server-ip:4000/v1。你分发给业务方统一使用 model: "internal-chat",当本地显卡突发负载过高响应超时时,网关可以在 fallbacks 规则下静默无感转移到备用链路,避免业务系统抛红。

四、高并发流式(SSE)压测实录

架构搭完不能凭感觉,必须用真实并发打压力。我们使用 Python 编写基于 httpx 的异步流式请求压测脚本,模拟 30 个并发客户端持续请求长文本生成,统计首字延迟(TTFT)与平均生成速度(Token/s)。

import asyncio
import time
import httpx

API_URL = "http://localhost:4000/v1/chat/completions"
HEADERS = {"Authorization": "Bearer sk-nassky-admin-secret-2026"}
PAYLOAD = {
    "model": "internal-chat",
    "messages": [{"role": "user", "content": "请写一份关于微服务架构的高可用设计方案,不少于 500 字。"}],
    "stream": True
}

async def send_stream_request(client_id: int):
    start_time = time.perf_counter()
    first_token_time = None
    token_count = 0

    async with httpx.AsyncClient(timeout=60.0) as client:
        async with client.stream("POST", API_URL, headers=HEADERS, json=PAYLOAD) as response:
            async for chunk in response.aiter_text():
                if chunk:
                    if first_token_time is None:
                        first_token_time = time.perf_counter()
                    token_count += 1

    total_time = time.perf_counter() - start_time
    ttft = (first_token_time - start_time) * 1000 if first_token_time else 0
    tps = token_count / total_time if total_time > 0 else 0
    print(f"Client {client_id:02d} | TTFT: {ttft:.1f}ms | Total Time: {total_time:.2f}s | Speed: {tps:.1f} tokens/s")

async def main():
    concurrency = 20
    print(f"Starting benchmark with {concurrency} concurrent streams...")
    tasks = [send_stream_request(i) for i in range(concurrency)]
    await asyncio.gather(*tasks)

if __name__ == "__main__":
    asyncio.run(main())

实测结果对照(20 并发流式测试):

  • Ollama 原生配置(默认 context & batch):平均 TTFT 为 4280ms,中后期出现严重排队,部分连接在第 18 秒才吐出第一个字,总 TPS 维持在 45 tokens/s 左右。
  • vLLM + LiteLLM 架构 :平均 TTFT 稳定在 380ms ~ 520ms,动态 Batch 机制生效,整体吞吐达到 198 tokens/s,显卡算力利用率从 40% 冲到 92%,显存占用被 PagedAttention 锁死在 21.6GB,没有任何抖动与溢出。

五、反代与跨网段实战避坑

如果你的客户端和网关服务器之间还隔了一层 Nginx(例如 NAS、云服务器公网接入),请务必检查以下两项配置,否则流式输出会直接退化为“全量缓冲”:

location /v1/ {
    proxy_pass http://127.0.0.1:4000/v1/;
    
    # 彻底关闭缓冲,保证 SSE 流式实时推送
    proxy_buffering off;
    proxy_cache off;
    
    # 避免长生成请求被反向代理提前掐断
    proxy_read_timeout 600s;
    proxy_send_timeout 600s;
    
    # 开启 HTTP/1.1 支持 keepalive
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_set_header Host $host;
}

很多开发者遇到前端打字机效果失效、文字一次性大块吐出,90% 的原因都是 Nginx 默认开启了 proxy_buffering on,反代服务器把后端的 SSE chunk 攒够一个数据包才往下发,彻底破坏了打字体验。

总结与避坑心得

搭建私有大模型服务,最忌讳“拿开发玩具当生产基础设施”。Ollama 适合做单人调试助手,但只要面临多业务接入、长文本并发或自动化 Agent 流水线,尽早切到 vLLM + LiteLLM 的分层架构是最稳妥的选择。

最后提炼三条关键心得:
1. 显存利用率参数 --gpu-memory-utilization 宁可留空 10% 也别顶格跑,这是高并发防止突发爆显存的最佳防线;
2. 务必合理裁剪 --max-model-len,把宝贵的显存留给更多的并发 Batch,而不是留给用不上的超长窗口;
3. 网关层一定要配置公网 API 做 Fallback,保证本地算力被突发流量击穿时,核心业务依然能保持 100% 可用。

===END===

正文完
 0
评论(没有评论)