共计 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===