共计 4521 个字符,预计需要花费 12 分钟才能阅读完成。
上个月业务部门突然推来一个客服 Agent 的改造需求,预期峰值并发从原来的十几个暴涨到近两百。起初大家习惯性地接入公有云商业接口,结果跑了三天账单就直接拉响警报,而且遇到网络抖动时流式打字卡顿严重。为了彻底兜底并砍掉 80% 的 API 开销,我决定在本地工作站和单台 GPU 服务器上搭一套兼具低延迟与高吞吐的私有 LLM 代理网关。
很多朋友本地跑大模型习惯直接上 Ollama,开发调试确实爽,但一旦扔到生产环境承受几十个并发的流式请求,Ollama 的吞吐和显存复用能力很快就会见顶。最终我敲定的技术组合是 vLLM(底层推理引擎)+ LiteLLM Proxy(网关鉴权与路由)。这篇文章记录我从显存压榨、网关搭建到真实高并发压测的全套落地细节。
一、为什么放弃 Ollama,转向 vLLM + LiteLLM?
在小规模自建服务时,选型通常在 TGI、vLLM 和 Ollama 之间纠结。我们做了一轮基准测试,用一张 RTX 4090(24GB 显存)跑 Qwen2.5-14B-Instruct-AWQ,输入 512 tokens,输出 256 tokens,并发逐步从 1 压到 64:
- Ollama (llama.cpp):并发超过 16 后,首字延迟(TTFT)急剧飙升,并发 32 时直接发生排队阻塞,显存无法做到零碎片的动态按需分配。
- vLLM:得益于核心的 PagedAttention 机制,K-V Cache 被切成类似操作系统的虚拟内存页,显存几乎做到零浪费。并发飙到 48 时,系统吞吐量(Tokens/s)依然呈现线性增长,整体吞吐比 Ollama 翻了近 3.8 倍。
但 vLLM 本身只负责纯粹的推理计算,不具备生产环境必备的 API Key 轮询管理、请求速率限制(Rate Limit)、降级熔断以及跨模型路由功能。这时在最前端套一层轻量级的 LiteLLM Proxy,就能无缝提供 100% 兼容 OpenAI 格式的标准接口,两者的结合可以说是目前中小团队性价比最高的方案。
二、底层推理核心:vLLM 极限显存压榨配置
在单张 24GB 显存的卡上跑 14B 参数模型,显存分配必须精打细算。很多新手启动 vLLM 直接一行 python3 -m vllm.entrypoints.openai.api_server 就跑起来了,结果一发压测就直接爆显存(CUDA Out of Memory)。
关键参数在于显存预留比例(gpu-memory-utilization)与最大上下文长度(max-model-len)的博弈。下面是我在线上验证过最稳定的生产级启动脚本:
#!/usr/bin/env bash
# run_vllm.sh
docker run -d \
--name vllm-engine \
--runtime nvidia \
--gpus '"device=0"' \
-p 8000:8000 \
-v /root/.cache/huggingface:/root/.cache/huggingface \
-v /data/models:/models \
--ipc=host \
vllm/vllm-openai:v0.6.3 \
--model /models/Qwen2.5-14B-Instruct-AWQ \
--served-model-name qwen-14b \
--quantization awq \
--dtype half \
--gpu-memory-utilization 0.90 \
--max-model-len 4096 \
--max-num-seqs 64 \
--enforce-eager \
--trust-remote-code
这里踩过的几个深坑必须提醒:
--ipc=host必须加:如果不挂载主机的 IPC 共享内存,当并发增高、多进程数据交换变大时,PyTorch 会毫无预警地崩掉 DataLoader 或直接死锁挂起。--enforce-eager的取舍:vLLM 默认开启 CUDA Graph 优化,对于固定输入长度能提升约 10% 的推理速度。但它在服务初次启动时需要预热捕获图,如果你的显存只剩下 1~2GB 的安全边界,CUDA Graph 捕获阶段会瞬间 OOM。在 24G 显存跑 14B AWQ 的紧绷状态下,加上--enforce-eager关闭 CUDA Graph 能保住近 1.5GB 的显存用于承载更多并发请求。max-num-seqs:单批次最大处理请求数,直接控制调度池深度。设为 64 可以有效限制突发流量将显存打穿。
三、生产级网关组网:Docker Compose 编排 LiteLLM
后端推理引擎跑稳后,我们需要用 LiteLLM 作为网关层做安全拦截、鉴权、负载切分。推荐使用 Docker Compose 快速编排,将 PostgreSQL(用于保存使用量数据)和 Redis(用于高并发流控)一并带起。
新建 docker-compose.yml:
version: '3.8'
services:
litellm-proxy:
image: ghcr.io/berriai/litellm:main-v1.49.0
container_name: litellm-proxy
ports:
- "4000:4000"
volumes:
- ./litellm-config.yaml:/app/config.yaml
environment:
- DATABASE_URL=postgresql://litellm:password123@postgres:5432/litellm_db
- REDIS_URL=redis://redis:6379/0
- STORE_MODEL_IN_DB=True
command: ["--config", "/app/config.yaml", "--port", "4000", "--num_workers", "4"]
depends_on:
- postgres
- redis
restart: always
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: litellm
POSTGRES_PASSWORD: password123
POSTGRES_DB: litellm_db
volumes:
- pgdata:/var/lib/postgresql/data
restart: always
redis:
image: redis:7-alpine
restart: always
volumes:
pgdata:
编写关键网关配置 litellm-config.yaml,实现多模型路由与自动回退(Fallback):
model_list:
# 优先路由到本地 vLLM 实例
- model_name: default-chat
litellm_params:
model: openai/qwen-14b
api_base: http://host.docker.internal:8000/v1
api_key: "EMPTY"
tpm: 100000
rpm: 1200
# 当本地单卡算力打满或宕机时,静默熔断到远程云端 API
- model_name: default-chat-fallback
litellm_params:
model: deepseek/deepseek-chat
api_key: "sk-your-remote-api-key"
router_settings:
fallbacks: [{"default-chat": ["default-chat-fallback"]}]
routing_strategy: "latency-based-routing"
redis_host: "redis"
redis_port: 6379
general_settings:
master_key: "sk-nassky-super-admin-key"
这套配置最狠的地方在于:业务端只需要统一请求网关的 default-chat 模型。平日里所有流量全部白嫖本地单卡算力,当并发洪峰把本地显卡打爆触发超显存异常或超时时,LiteLLM 会在毫秒级内自动将失败请求转派到 DeepSeek 或云端接口,业务层完全无感知,既省钱又兼顾了可用性。
四、攻克 SSE 流式传输在反向代理中的三大陷阱
在将这套系统接给前端前端页面时,最常遇到的问题就是:终端打字机效果没了,页面死等 5 秒,然后一大坨字瞬间吐出来。或者在压测时发现客户端明明断开了连接,vLLM 的 GPU 却依然在疯狂计算。
1. Nginx 缓冲截断导致打字机失效
如果你在 LiteLLM 前面还挂了一层 Nginx 作为域名入口,必须在 location 块中显式关闭缓冲与代理缓存,否则 Nginx 会把分块的 Server-Sent Events (SSE) 先塞满缓冲区再下发:
location /v1/chat/completions {
proxy_pass http://127.0.0.1:4000;
proxy_http_version 1.1;
proxy_set_header Connection "";
# 彻底关闭缓冲,保障 SSE 实时推送
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
# 避免复杂思考长推理时网关超时掐断连接
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
2. 客户端取消但 GPU 继续空转(幽灵计算)
许多前端用户在看到生成的内容不合心意时会连续点击“重新生成”,这会在瞬间发送多条并发请求。如果你的中间件没有传递连接断开事件,vLLM 会老老实实把上一个请求的几百个 token 全部算完,无端消耗计算资源。
在 FastAPI 或自行编写的转发代理中,一定要监听底层 ASGI 连接状态。在 vLLM 原生接口中,确保请求头携带标准断连信号,并在网关层开启客户端取消传播机制,确保调用端 AbortController 一旦触发,推理引擎立即回收对应的 K-V Cache 页。
五、实战压测表现与收益核算
架构跑通后,我使用自定义的 Python 异步脚本对其进行压力测试。在维持首字延迟(TTFT)在 800ms 以内的前提下:
- 纯单卡 Qwen2.5-14B-AWQ:支撑稳定并发达到 42 QPS,显存占用稳定在 21.6GB 左右,没有出现任何 OOM 报错。
- 容灾表现:人工切断 vLLM 服务,LiteLLM 在第 2 次重试后成功切换至降级云端模型,降级切换耗时仅 120ms。
- 财务降本:上线运行两周,本地网关承载了总请求量的 91.4%,原本月均预估 4500 元的公有云 API 账单直接压缩到了不足 400 元(仅包含了偶尔打满溢出到云端的费用和服务器电费)。
总结与避坑心得
大模型落地的工程化不是简单地找个 WebUI 跑起来就完事。私有化网关的核心在于 分层解耦:让 vLLM 专注于极致的计算吞吐和动态显存管理,把安全、流控、鉴权和灾备彻底推给轻量级的 LiteLLM 代理层。
在自己动手部署前,务必牢记三点:显存宁可留出 10% 缓冲也别贪大、SSE 链路上所有的反代缓冲必须统统掐死、降级熔断策略是避免本地单卡被打崩的最后底牌。按照这套骨架搭建,无论是团队内部降本,还是给中小业务提供稳定靠谱的私有 LLM 基础设施,都能跑得极其扎实。
===EOF===