共计 4486 个字符,预计需要花费 12 分钟才能阅读完成。
上个月帮朋友的小团队改造一个内部知识库与智能客服后台,原本他们的架构是直接用 Ollama 挂载 14B 模型对外提供接口。日常一两个人提问很流畅,但只要业务端跑批量摘要或者并发上到 15 左右,显存直接被打满,TTFT(首字延迟)直接飙升到 6 秒以上,SSE(Server-Sent Events)流式连接成片断开。换上单张 24G 显存的 RTX 4090 后,我们通过重构底层推理引擎为 vLLM 并手写高并发流式代理层,最终在保持 P95 首字延迟低于 450ms 的前提下,单卡扛住了 300+ 的流式并发压测。今天把这套已经稳定运行一个月的实战架构、配置参数和踩坑细节完整复盘出来。
一、为什么 Ollama 不适合生产级高并发?
很多开发者在本地开发时习惯用 Ollama,因为它开箱即用,通过简单的 REST API 就能调用。但在并发推理场景下,Ollama 存在几个先天致命伤:
- KV Cache 显存碎片化严重: 连续请求进来时,固定长度的显存预分配机制会导致大量未利用的显存空间被锁死,无法动态分页共享。
- 缺少成熟的连续批处理(Continuous Batching): 当一个请求已经生成完毕时,系统无法以 Token 为粒度立即插播新的请求进入计算批次,算力空转严重。
- 缺乏细粒度流式背压(Backpressure): 一旦下游客户端网络慢,长连接积压会拖死整个模型实例的输出队列。
而 vLLM 核心的 PagedAttention 机制类似虚拟内存的分页管理,几乎将 KV Cache 的显存浪费降低到了 4% 以下。配合 Continuous Batching,单卡吞吐量(Throughput)能比传统方案提升 5 到 10 倍。
二、显卡极限压榨:vLLM 核心参数精准调优
部署模型选用当前在中文和代码综合能力最均衡的 Qwen/Qwen2.5-7B-Instruct(兼顾延迟与并发)以及 Qwen2.5-14B-Instruct-GPTQ-Int4。以下是我们在生产服务器上跑了多轮基准测试后锁定的启动配置:
# 生产启动命令:针对 24GB 显存显卡的极致参数调配
python3 -m vllm.entrypoints.openai.api_server \
--model /data/models/Qwen2.5-7B-Instruct \
--served-model-name qwen2.5-7b \
--trust-remote-code \
--host 0.0.0.0 \
--port 8000 \
--tensor-parallel-size 1 \
--gpu-memory-utilization 0.92 \
--max-model-len 4096 \
--max-num-seqs 256 \
--max-num-batched-tokens 8192 \
--kv-cache-dtype auto \
--disable-log-requests
踩坑点与参数拆解解析:
--gpu-memory-utilization 0.92: 千万不要头铁设成 0.98 或 0.99!vLLM 会在启动时把剩余的显存全部分配给 KV Cache 显存池。一旦设过高,当出现大 batch 且包含较长 System Prompt 时,CUDA context 会因瞬时碎片分配失败直接抛出 OOM(Out Of Memory)错误并导致整个守护进程崩溃。0.90 ~ 0.92 是 RTX 3090/4090 的安全甜点位。--max-num-seqs 256: 这个参数控制当前正在执行的最大序列数。对于 7B 浮点模型,设置在 200~256 能充分打满 4090 的 Tensor Core,再往上加虽然并发能抗住,但每个请求的解码速度(Decoding Speed)会出现体感断崖式下跌。--max-num-batched-tokens 8192: 单个批次能处理的最大 Token 数。如果调得太小,Prefill(首字生成阶段)的并行优势就废了;太大会瞬时占用极高显存。8192 是 24G 显存下的黄金平衡点。--disable-log-requests: 压测时一定要加。高并发下 Python 自带的终端 I/O 打印会严重阻塞事件循环,实测不关日志每秒处理能力直接腰斩。
三、生产级高并发流式代理网关开发
绝不能把 vLLM 的 8000 端口直接暴露给公网。生产环境需要一个前置反向代理层,用来处理 API Key 鉴权、速率限制、异常熔断以及最核心的 异步流式转发防阻塞。下面是我用 FastAPI + httpx 构建的高性能流式网关核心实现:
# gateway.py: 高并发异步流式网关核心切片
import httpx
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import StreamingResponse
app = FastAPI()
# 维持一个长连接池,避免每次请求走三次握手
limits = httpx.Limits(max_keepalive_connections=500, max_connections=1000)
timeout = httpx.Timeout(connect=5.0, read=60.0, write=5.0, pool=10.0)
client = httpx.AsyncClient(base_url="http://127.0.0.1:8000", limits=limits, timeout=timeout)
@app.post("/v1/chat/completions")
async def chat_proxy(request: Request):
headers = dict(request.headers)
headers.pop("host", None)
headers.pop("content-length", None)
body = await request.body()
# 向 vLLM 发送流式请求
req = client.build_request(
method="POST",
url="/v1/chat/completions",
headers=headers,
content=body
)
try:
response = await client.send(req, stream=True)
except httpx.RequestError as exc:
raise HTTPException(status_code=503, detail=f"LLM Engine Unreachable: {exc}")
# 生成器:带背压保护的 Token 转发
async def token_stream_generator():
try:
async for chunk in response.aiter_bytes():
yield chunk
finally:
# 关键:当客户端主动断开(如前端关闭网页)时,必须关闭后端响应,触发 vLLM 释放序列
await response.aclose()
return StreamingResponse(token_stream_generator(),
status_code=response.status_code,
headers=dict(response.headers),
media_type="text/event-stream"
)
避坑细节: 注意 token_stream_generator 中的 finally: await response.aclose()。如果省略这行,当用户提问到一半关掉浏览器标签页时,vLLM 后端依然会固执地把剩下的 Token 生成完毕,无端浪费昂贵的 GPU 算力。显式关闭连接能让 vLLM 触发 AbortProcessing,瞬间释放显存。
四、自动化部署:Docker-Compose 架构编排
为了让整套服务具备容灾自愈能力,我们采用 Docker Compose 进行统一编排。这里直接调用 NVIDIA Container Toolkit 映射显卡:
# docker-compose.yml
version: '3.8'
services:
vllm-engine:
image: vllm/vllm-openai:v0.6.3
container_name: vllm-qwen-7b
runtime: nvidia
restart: unless-stopped
environment:
- CUDA_VISIBLE_DEVICES=0
- NCCL_IGNORE_DISABLED_P2P=1
volumes:
- /data/models/Qwen2.5-7B-Instruct:/model
ports:
- "8000:8000"
command: >
--model /model
--served-model-name qwen2.5-7b
--gpu-memory-utilization 0.92
--max-model-len 4096
--max-num-seqs 256
--disable-log-requests
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
llm-gateway:
build:
context: ./gateway
container_name: llm-fastapi-gateway
restart: unless-stopped
ports:
- "8080:8080"
environment:
- VLLM_BACKEND_URL=http://vllm-engine:8000
depends_on:
- vllm-engine
五、性能对比与选型实测数据
我们使用 locust 对这套系统进行了高强度压测,模拟用户连续向模型发送包含 500 Token 上下文的问题,并要求输出约 300 Token 的回答。对比原有的 Ollama 方案与优化后的 vLLM 方案:
| 指标 | Ollama (原方案) | vLLM + 异步网关 (新方案) | 提升幅度 |
|---|---|---|---|
| 最大稳定并发数 | 16 | 310 | 19.3x |
| 首字延迟 (TTFT P95) | 6,200 ms | 418 ms | 降低 93.2% |
| 系统吞吐量 (Tokens/s) | 85 tokens/s | 1,420 tokens/s | 16.7x |
| 高并发显存 OOM 崩溃率 | 34% (频繁崩溃) | 0% (自适应排队) | 完全消除 |
总结与避坑心得
在消费级显卡部署生产级别 LLM 服务,算力不是唯一瓶颈,显存调度与流水线管理 才是拉开差距的核心。最后整理三条关键实操法则:
- 严禁生产裸奔 Ollama: Ollama 适合做开发测试和本地单人助理,一旦进入多人协同或业务 API 级调用,必须果断切换到 vLLM、SGLang 或 TensorRT-LLM。
- 留足系统显存冗余: 24G 显存不要分配超过 92% 给模型与 KV Cache,给 PyTorch 运行期以及 CUDA Graphs 预留至少 1.5G 缓冲区,是系统常驻 30 天不挂掉的铁律。
- 流式连接必须有主动取消机制: 前端无论用 fetch 还是 EventSource,在页面切换时务必调用
AbortController.abort(),网关捕获后立即中断后端计算,否则任何高性能方案都会被无意义的后台残留任务拖垮。