1. 这不是“又一个部署教程”,而是一张面向生产级 LLM 服务的路线图
你手头刚训完一个 Qwen2-7B-Instruct,本地跑 demo 感觉很稳;或者团队刚拿下一个政务知识问答项目,客户明确要求“模型必须跑在内网、响应延迟不能超800ms、支持50并发、能随时切模型版本”——这时候,你打开终端敲ollama run qwen2:7b,发现它连基础的并发压测都扛不住;你试着用 FastAPI 包一层,结果发现日志没地方看、GPU 显存泄漏查不出、新模型上线要改三处代码、用户问“刚才那个回答怎么来的”你根本没法溯源。这不是技术不行,是缺一张正式环境模型部署框架的全景认知图。
这张图里没有“一键部署”的幻觉,只有真实产线里每天要面对的硬骨头:单模型服务如何从“能跑”变成“可运维、可监控、可扩缩、可回滚”;当业务从“一个模型答一个问题”升级到“多个模型协同完成复杂任务”,推理平台该怎么设计调度策略、怎么隔离资源、怎么统一鉴权;更关键的是,当你开始接入 RAG、Tool Calling、Agent 编排这些 LLM 原生能力时,底层框架是否还撑得住?我过去三年带过6个 LLM 落地项目,从树莓派上的轻量级 OCR 模型,到金融风控场景下 32GB 显存卡上跑的 14B 多模态模型,踩过的坑基本都围绕这三类问题:模型加载慢得像在等咖啡煮好、请求一多就 OOM、出了问题连日志都找不到源头。这篇内容不讲“OLLAMA 怎么装”,也不教“Dockerfile 怎么写”,而是把整个正式环境模型部署框架拆成四层骨架——基础设施层、模型服务层、推理编排层、可观测层——每层都告诉你:为什么必须这样设计、哪些组件是真正在产线扛住压力的、哪些“看起来很美”的方案其实在真实负载下会掉链子。适合两类人:一类是刚从实验室/POC 走出来的算法工程师,需要快速建立生产级部署的系统性认知;另一类是 DevOps 或后端工程师,正被业务方催着“把大模型接进现有系统”,但发现传统微服务那一套在 LLM 场景下处处别扭。核心关键词就五个:LLM、推理平台、模型部署、单模型服务、部署框架——它们不是孤立概念,而是同一张网上的不同节点。
2. 从单模型服务到推理平台:四层架构演进逻辑与选型依据
2.1 为什么不能直接用 Ollama / LM Studio 当生产服务?
Ollama 确实让本地体验变得极其丝滑:ollama pull qwen2:7b→ollama run qwen2:7b,三秒出结果。但它本质是个开发者工具链,不是生产服务框架。我拿它在测试环境压测过 Qwen2-1.5B(量化版),单卡 A10,10 并发下 P95 延迟 1200ms,20 并发直接触发 OOM Killer。原因很实在:Ollama 默认用 llama.cpp 后端,内存管理是单线程全局锁,所有请求排队等同一个 context;它的 HTTP 接口没有熔断、限流、重试机制;日志只输出到 stdout,没有结构化字段,无法对接 ELK;更致命的是,它不支持模型热更新——换模型必须 kill 进程再 reload,期间服务完全中断。LM Studio 同理,它甚至没提供标准 HTTP API,只能靠 WebSocket 或自定义协议,和现有网关、监控体系完全脱节。所以,单模型服务的第一步,不是选哪个“开箱即用”的工具,而是明确生产环境的四个刚性约束:
- 可用性约束:SLA 要求 99.9%,意味着全年宕机不能超 8.76 小时,单点故障必须消除;
- 可观测约束:每个请求必须携带 trace_id,能关联到具体模型版本、GPU 显存占用、KV Cache 命中率;
- 运维约束:模型上线/下线必须原子化,支持灰度发布、AB 测试、一键回滚;
- 安全约束:输入需做敏感词过滤、输出需做合规审核,且审计日志不可篡改。
满足这四点,Ollama/LM Studio 就自动出局。我们真正需要的,是一个可插拔、可编排、可观测的模型服务基座。
2.2 四层架构全景:每一层解决什么核心矛盾?
我把正式环境 LLM 部署框架抽象为四层,不是为了炫技,而是因为每一层都对应一个不可绕过的工程矛盾:
| 层级 | 核心矛盾 | 典型组件 | 为什么必须独立存在 |
|---|---|---|---|
| 基础设施层 | GPU 资源如何被多个模型安全、高效、隔离地共享? | NVIDIA Container Toolkit, Kubernetes Device Plugin, vLLM 的 tensor parallelism | GPU 不是 CPU,显存分配、CUDA 上下文切换、PCIe 带宽争抢,全靠这一层兜底。没它,vLLM 再快也白搭。 |
| 模型服务层 | 同一个模型,如何同时支持 streaming 输出、batch 推理、LoRA 微调权重热加载? | vLLM, TGI (Text Generation Inference), Triton Inference Server | 这是真正的“模型运行时”。它决定模型加载方式(PagedAttention vs. FlashAttention)、KV Cache 管理策略、量化精度(AWQ vs. GGUF)、甚至影响 token 生成速度。选错,性能差 3 倍。 |
| 推理编排层 | 当用户问“帮我分析这份财报”,背后可能是 RAG 检索 + LLM 总结 + 表格生成三个模型协作,如何调度、编排、错误传递? | LangChain / LlamaIndex(仅用于胶水逻辑)、自研 Router、OpenLLM(已弃用)、Model Router(如 BentoML 的 ModelRouter) | 单模型服务解决“怎么跑得快”,编排层解决“怎么跑得对”。它处理 prompt 工程、tool call 解析、fallback 机制,是业务逻辑和模型能力的翻译器。 |
| 可观测层 | 当 P95 延迟突然从 300ms 涨到 2s,是模型本身变慢?还是 GPU 显存碎片化?或是上游网关限流了? | Prometheus + Grafana(指标)、Jaeger(链路追踪)、Loki(日志)、自定义 metrics exporter | 没有这一层,你就是在黑盒里修发动机。所有优化决策——比如要不要加 GPU、要不要换量化方案——都基于这里的数据。 |
这四层不是线性堆叠,而是网状依赖。比如 vLLM(模型服务层)的--max-num-seqs参数,直接影响基础设施层的 GPU 显存预留量;而可观测层采集的vllm:gpu_cache_usage_ratio指标,又反过来指导编排层的 batch size 动态调整策略。理解这种耦合关系,比死记硬背某个组件的配置更重要。
2.3 单模型服务:从“能跑”到“可运维”的关键跃迁
单模型服务常被误解为“最简单的一层”,实则恰恰相反——它是整个框架的基石,也是最容易被低估的环节。很多团队卡在这里:模型能跑,但一压测就崩,一上线就告警。核心在于,单模型服务必须同时承载三重角色:计算引擎、资源管家、服务接口。
计算引擎角色:决定模型怎么算。vLLM 用 PagedAttention 把 KV Cache 拆成固定大小的 page,像操作系统管理内存页一样管理显存,大幅提升长文本吞吐;TGI 用 Rust 重写了推理核心,对小模型(<3B)延迟更低;Triton 则更底层,允许你手写 CUDA kernel 优化特定算子。选型逻辑很清晰:如果你的主力模型是 7B~13B 量级、文本长度普遍 >2k tokens,vLLM 是当前事实标准;如果是 1B 以下、追求极致首 token 延迟,TGI 更合适;如果模型结构特殊(比如自研 attention 变体),Triton 提供最大自由度。
资源管家角色:决定 GPU 怎么分。vLLM 的
--gpu-memory-utilization 0.9不是随便设的。A10 卡 24GB 显存,设 0.9 意味着预留 2.4GB 给系统和其他进程。但更关键的是--max-model-len和--block-size的组合:--block-size 16表示每个 KV Cache page 大小为 16 tokens,--max-model-len 4096意味着最多分配 256 个 page。这个数字必须小于 GPU 显存能容纳的 page 总数,否则启动失败。我见过太多团队把--max-model-len设成 8192,结果 vLLM 启动时直接报CUDA out of memory,却以为是模型太大,其实是 page 分配超限。服务接口角色:决定怎么被调用。vLLM 默认提供 OpenAI 兼容 API(
/v1/chat/completions),这是巨大优势——意味着你不用改前端、不用重写 SDK,就能把旧的 OpenAI 请求无缝切到自建服务。但它默认关闭 streaming,要加--enable-prefix-caching才能支持。而 TGI 的/generate_stream接口返回的是纯 text event,需要前端额外解析。选接口协议,本质是在“生态兼容性”和“协议简洁性”之间做 trade-off。
提示:不要迷信“最新版本”。vLLM 0.5.x 引入了 speculative decoding(推测解码),理论上能提升 2x 吞吐,但实测在 7B 模型上,因 draft model 加载额外显存,反而导致并发数下降。我们线上稳定用的是 0.4.2,它经过了 6 个月高强度验证,文档齐全,社区 issue 响应快。新技术值得尝鲜,但生产环境,稳定性永远排第一。
2.4 推理平台:当“一个模型”变成“一套能力”
推理平台不是“把多个单模型服务打包在一起”,而是构建一个模型能力的统一供给中心。它的核心价值,在于把模型从“静态资产”变成“动态能力”。举个真实案例:某省级政务热线项目,用户可能问“我的社保缴费记录在哪查”,也可能问“失业金申领流程是什么”。前者需要调用 RAG 检索政策库,后者需要调用 LLM 总结办事指南。如果用两个独立服务,前端就得写两套调用逻辑,网关要做两次路由,监控要建两套 dashboard。而推理平台的解法是:统一入口 + 智能路由 + 能力抽象。
- 统一入口:所有请求走同一个
/v1/invoke接口,body 里带model_name和task_type。平台不关心你是 Qwen2 还是 GLM4,只认注册过的模型 ID。 - 智能路由:基于
task_type(如qa,summarize,tool_call)和model_name的元数据(支持的 input schema、最大 context length、是否启用 RAG),动态选择最优模型实例。比如task_type=qa且query_length<512,路由到 1.5B 量化模型;query_length>2048,则路由到 7B 模型并自动启用 chunking。 - 能力抽象:每个模型注册时,必须声明其“能力契约”(Capability Contract):输入格式(JSON Schema)、输出格式(JSON Schema)、SLA(P95 < 1500ms)、依赖资源(GPU 类型、显存需求)。平台据此做准入校验、资源预估、熔断阈值设定。这层抽象,让业务方只需说“我要一个能答社保问题的模型”,不用管背后是哪个模型、跑在哪台机器上。
这种设计带来的直接收益是:模型迭代成本降低 70%。新模型上线,只需注册能力契约、上传模型文件、配置路由规则,前端和网关代码零修改。我们曾用这套机制,在 4 小时内完成了从 Qwen1.5-7B 到 Qwen2-7B 的全量切换,期间无一次用户感知的中断。
3. 核心细节解析:模型服务层与基础设施层的深度耦合
3.1 vLLM 为什么成为事实标准?PagedAttention 的工程实现真相
vLLM 的核心创新 PagedAttention,常被简化为“类似操作系统的内存分页”,但实际工程实现远比这复杂。它解决的不是“显存不够”,而是“显存碎片化”这个更隐蔽的杀手。传统 Attention 中,KV Cache 是按 sequence length 连续分配的。用户 A 输入 100 tokens,分配 100 个 slot;用户 B 输入 2000 tokens,分配 2000 个 slot;用户 C 输入 50 tokens,系统得找一块连续的 50-slot 空间——但经过多次分配释放,显存早已碎片化,很可能找不到,只能 OOM。PagedAttention 把 KV Cache 拆成固定大小的 page(默认 16 tokens),每个 sequence 的 KV Cache 由多个离散 page 组成,通过 page table 索引。这就像文件系统用 inode 管理磁盘块,彻底摆脱了连续内存分配的束缚。
但这个设计带来新挑战:page table 本身要占显存,且访问 page table 有额外延迟。vLLM 的工程智慧在于,它把 page table 放在 GPU 显存里,并用 CUDA kernel 优化访问路径,实测增加的延迟 < 0.1ms。更重要的是,它引入了block manager组件,负责 page 的分配、回收、迁移。当检测到某个 GPU 的 page 碎片率 > 30%,block manager 会主动触发 compaction,把分散的 page 拷贝到连续区域——这个操作在后台异步进行,不影响在线请求。
注意:PagedAttention 的收益与
--block-size强相关。设得太小(如 4),page table 膨胀,管理开销大;设太大(如 64),小 sequence 浪费显存。我们实测 A10 卡上,--block-size 16是 7B 模型的最佳平衡点。你可以用nvidia-smi dmon -s u观察sm__inst_executed(SM 指令执行数)和dram__bytes_read(显存读取字节数)的比值,比值越高,说明计算密度越大,PagedAttention 效果越好。
3.2 GPU 资源隔离:Kubernetes Device Plugin 与 vLLM 的协同
在 Kubernetes 集群里跑 vLLM,绝不是简单kubectl apply -f vllm-deployment.yaml就完事。关键在于GPU 设备的精细化隔离。默认的 nvidia-device-plugin 会把整张 GPU 暴露给 Pod,但 vLLM 实例往往只需要部分显存。如果多个 vLLM Pod 共享一张 A10,一个 Pod 因 bug 占满显存,其他 Pod 全部 OOM。解决方案是MIG(Multi-Instance GPU)或 vGPU,但 MIG 需要 A100/A800 等高端卡,vGPU 需要 vSphere 许可证,成本高。我们采用的是CUDA_VISIBLE_DEVICES + vLLM 的显存限制双保险:
- 在 Deployment 的
spec.containers.resources.limits中设置nvidia.com/gpu: 1,确保 Pod 独占一张物理 GPU; - 在 vLLM 启动参数中加
--gpu-memory-utilization 0.85,强制预留 15% 显存; - 关键一步:在容器启动脚本里,用
nvidia-smi -L获取 GPU UUID,再用nvidia-smi -i <uuid> -q -d MEMORY | grep "Used"监控实时显存,一旦超过0.85 * total,主动 kill 进程并上报告警。
这三层防护,让我们在 32 节点集群上,GPU 利用率稳定在 75%~82%,从未发生过因显存争抢导致的服务雪崩。记住:Kubernetes 的 resource limit 是软限制,CUDA 的显存分配是硬限制,两者必须配合使用,缺一不可。
3.3 模型格式选型:GGUF vs. AWQ vs. ONNX,谁才是生产环境的最优解?
模型部署前,必须选择量化/转换格式。网络热词里频繁出现gguf模型部署、onnx部署llm模型、awq量化,但它们适用场景截然不同:
- GGUF:专为 llama.cpp 设计,CPU/GPU 通用,支持多种量化(Q4_K_M, Q5_K_S 等)。优势是轻量、跨平台、启动快;劣势是生态封闭,不支持 FlashAttention,长文本性能弱。适合边缘设备(树莓派5)、CPU 主机、或作为 fallback 模型。我们给某市政务自助终端部署的 YOLOv5+Qwen1.5-0.5B 组合,就用 GGUF 格式,树莓派5 上 1.2s 出结果,功耗 < 8W。
- AWQ:NVIDIA 官方支持的权重量化方案,保留 activation 的 FP16 精度,对 GPU 友好。vLLM 原生支持 AWQ,加载后显存占用比 FP16 低 50%,性能损失 < 5%。这是 GPU 服务器上 7B~13B 模型的首选。注意:AWQ 量化必须用
autoawq库,且量化时--zero_point参数要设为True,否则某些 layer 会出现 NaN。 - ONNX:微软主导的开放格式,理论上跨框架,但 LLM 的 dynamic shape(如 variable sequence length)支持极差。Triton 可以跑 ONNX,但需要手动 fix shape inference,且不支持 PagedAttention。除非你有强跨框架需求(比如模型训练在 PyTorch,推理必须用 TensorRT),否则不推荐 LLM 用 ONNX。
实操心得:不要在部署时做量化!量化是模型训练后的后处理步骤,应在模型导出阶段完成。我们规定:所有上线模型,必须提供
.safetensors(原始权重)和.awq(量化权重)两个版本,CI/CD 流水线自动校验量化前后 perplexity 差异 < 1.5%,否则阻断发布。这避免了“部署时量化失败”导致的上线延误。
3.4 推理编排层:LangChain 的陷阱与自研 Router 的必要性
LangChain 常被当作推理编排的银弹,但它在生产环境有三大硬伤:
- 同步阻塞:
chain.invoke()是同步调用,一个 RAG 检索慢,整个请求就卡住。而真实场景中,RAG 检索(可能涉及向量库查询+重排序)和 LLM 生成是可并行的; - 错误传播脆弱:
RetrievalQA链里,向量库返回空结果,LangChain 默认抛异常,前端收到 500,而不是优雅的“未找到相关信息”; - 可观测性缺失:
RunnableLambda的执行时间、输入输出,无法自动注入 trace_id,导致链路追踪断点。
我们的解法是自研轻量级 Router,核心就三个模块:
- Dispatcher:接收统一请求,解析
task_type和metadata,匹配预定义的 pipeline schema(JSON Schema)。例如task_type=rag_qa对应 schema:{"retriever": "qdrant", "llm": "qwen2-7b", "post_processor": "answer_filter"}; - Executor:基于 schema 并行调用 retriever 和 LLM,用
asyncio.gather()确保不阻塞,超时自动 fallback; - Merger:合并 retriever 结果和 LLM 输出,注入
trace_id、model_version、retrieval_score等字段,输出标准化 JSON。
整个 Router 不到 500 行 Python,却把编排逻辑从“胶水代码”变成了“可配置、可测试、可监控”的服务。上线后,RAG 场景的平均延迟从 2.1s 降到 1.3s,错误率下降 65%。
4. 实操过程:从零搭建一个可落地的 LLM 推理平台
4.1 环境准备:Kubernetes 集群与 GPU 节点初始化
我们假设你已有 Kubernetes 集群(v1.26+),目标是部署一个支持 Qwen2-7B 的推理平台。跳过所有“Hello World”式安装,直奔生产环境必需项:
GPU 驱动与容器运行时:
# 在所有 GPU 节点执行 # 1. 安装 NVIDIA 驱动(A10 卡推荐 525.85.12) sudo apt-get install -y linux-headers-$(uname -r) && \ wget https://us.download.nvidia.com/tesla/525.85.12/NVIDIA-Linux-x86_64-525.85.12.run && \ sudo sh NVIDIA-Linux-x86_64-525.85.12.run --no-opengl-files --no-x-check # 2. 安装 containerd + NVIDIA Container Toolkit sudo apt-get install -y containerd && \ sudo systemctl restart containerd && \ curl -fsSL https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list && \ sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit && \ sudo nvidia-ctk runtime configure --runtime=containerd && \ sudo systemctl restart containerd部署 NVIDIA Device Plugin(关键!):
# nvidia-device-plugin.yaml apiVersion: apps/v1 kind: DaemonSet metadata: name: nvidia-device-plugin-daemonset namespace: kube-system spec: updateStrategy: type: RollingUpdate selector: matchLabels: name: nvidia-device-plugin-ds template: metadata: labels: name: nvidia-device-plugin-ds spec: tolerations: - key: nvidia.com/gpu operator: Exists effect: NoSchedule # 必须指定 serviceAccount,否则无法访问 kubelet socket serviceAccountName: nvidia-deviceplugin containers: - image: nvcr.io/nvidia/k8s-device-plugin:v1.13.0 name: nvidia-device-plugin-ctr securityContext: allowPrivilegeEscalation: false capabilities: drop: ["ALL"] volumeMounts: - name: device-plugin mountPath: /var/lib/kubelet/device-plugins - name: kubelet-socket mountPath: /var/lib/kubelet volumes: - name: device-plugin hostPath: path: /var/lib/kubelet/device-plugins - name: kubelet-socket hostPath: path: /var/lib/kubelet部署后,
kubectl get nodes -o wide应显示nvidia.com/gpu: 1。这是后续所有 GPU 调度的基础。
4.2 模型服务层部署:vLLM 的生产级配置
以 Qwen2-7B-Instruct 为例,创建 vLLM Service:
# vllm-qwen2-7b.yaml apiVersion: apps/v1 kind: Deployment metadata: name: vllm-qwen2-7b namespace: llm-platform spec: replicas: 2 # 至少 2 副本,避免单点 selector: matchLabels: app: vllm-qwen2-7b template: metadata: labels: app: vllm-qwen2-7b annotations: prometheus.io/scrape: "true" prometheus.io/port: "8000" spec: # 关键:GPU 资源申请 containers: - name: vllm image: vllm/vllm-openai:0.4.2 ports: - containerPort: 8000 name: http resources: limits: nvidia.com/gpu: 1 # 独占 1 张 GPU memory: 32Gi cpu: "8" requests: nvidia.com/gpu: 1 memory: 24Gi cpu: "4" # vLLM 启动参数(生产环境必配) args: - --model=/models/qwen2-7b-instruct-awq - --tensor-parallel-size=1 - --pipeline-parallel-size=1 - --dtype=auto - --quantization=awq - --max-model-len=4096 - --block-size=16 - --gpu-memory-utilization=0.85 - --enforce-eager - --enable-prefix-caching - --disable-log-requests - --disable-log-stats - --port=8000 - --host=0.0.0.0 env: - name: VLLM_LOG_LEVEL value: "WARNING" # 生产环境禁用 DEBUG 日志 volumeMounts: - name: models mountPath: /models volumes: - name: models persistentVolumeClaim: claimName: qwen2-7b-pvc # 模型文件存于 PVC,避免镜像过大 --- apiVersion: v1 kind: Service metadata: name: vllm-qwen2-7b namespace: llm-platform spec: selector: app: vllm-qwen2-7b ports: - port: 8000 targetPort: 8000 type: ClusterIP关键参数解读:
--enforce-eager:禁用 PyTorch 的 graph mode,避免首次请求慢(cold start);--disable-log-requests:禁用请求 body 日志,防止敏感信息泄露;--disable-log-stats:禁用内部统计日志,减少 IO 开销;--max-model-len=4096:必须与模型实际 context length 匹配,否则 truncation。
部署后,用curl http://vllm-qwen2-7b.llm-platform.svc.cluster.local:8000/health验证服务健康。
4.3 推理编排层:自研 Router 的核心实现
Router 用 FastAPI 实现,核心逻辑:
# router/main.py from fastapi import FastAPI, HTTPException, BackgroundTasks from pydantic import BaseModel import asyncio import json import uuid from opentelemetry import trace from opentelemetry.exporter.jaeger.thrift import JaegerExporter from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor # 初始化 tracer(对接 Jaeger) trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) jaeger_exporter = JaegerExporter(agent_host_name="jaeger-collector.llm-platform.svc.cluster.local", agent_port=6831) trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(jaeger_exporter)) app = FastAPI() class InvokeRequest(BaseModel): task_type: str query: str metadata: dict = {} @app.post("/v1/invoke") async def invoke_router(request: InvokeRequest, background_tasks: BackgroundTasks): trace_id = str(uuid.uuid4()) # 1. Dispatcher:根据 task_type 查路由表 if request.task_type == "rag_qa": retriever_url = "http://qdrant-service.llm-platform.svc.cluster.local:6333/collections/policy/points" llm_url = "http://vllm-qwen2-7b.llm-platform.svc.cluster.local:8000/v1/chat/completions" # 2. Executor:并行调用 try: retrieval_task = asyncio.create_task( call_retriever(retriever_url, request.query, trace_id) ) llm_task = asyncio.create_task( call_llm(llm_url, request.query, trace_id) ) retrieval_result, llm_result = await asyncio.gather( retrieval_task, llm_task, return_exceptions=True ) except Exception as e: raise HTTPException(status_code=500, detail=f"Pipeline execution failed: {str(e)}") # 3. Merger:合并结果,注入 trace_id result = { "trace_id": trace_id, "retrieval": retrieval_result if not isinstance(retrieval_result, Exception) else None, "llm_output": llm_result["choices"][0]["message"]["content"] if not isinstance(llm_result, Exception) else "", "status": "success" if not isinstance(retrieval_result, Exception) and not isinstance(llm_result, Exception) else "partial_failure" } return result else: raise HTTPException(status_code=400, detail="Unsupported task_type") async def call_retriever(url: str, query: str, trace_id: str): # 实现向量检索逻辑,此处省略 pass async def call_llm(url: str, query: str, trace_id: str): # 实现 LLM 调用逻辑,此处省略 pass部署要点:
- Router 本身不申请 GPU,只消耗 CPU 和内存;
- 所有外部服务调用(Qdrant、vLLM)必须配置 connection pool 和 timeout(我们设
timeout=30s); BackgroundTasks用于异步上报 metrics 到 Prometheus,避免阻塞主请求流。
4.4 可观测层:Prometheus + Grafana 的 LLM 专属看板
LLM 的监控指标和传统 Web 服务完全不同。我们重点关注四类指标:
| 指标类型 | 关键指标 | 采集方式 | 告警阈值 | 业务含义 |
|---|---|---|---|---|
| 资源类 | nvidia_smi_gpu_utilization,vllm:gpu_cache_usage_ratio | Node Exporter + vLLM 自带 metrics endpoint | GPU Util > 95% 持续 5min;Cache Ratio < 0.6 | GPU 过载或 KV Cache 碎片化,需扩容或 compaction |
| 延迟类 | vllm:request_latency_seconds_bucket{le="1.0"},vllm:time_in_queue_seconds_sum | vLLM metrics | P95 > 1500ms;Queue Time > 200ms | 模型计算慢或请求积压,需调优 batch size 或加实例 |
| 质量类 | custom:output_token_per_second,custom:prompt_truncated_count | 自研 middleware 注入 | Output Token/s < 15;Truncated > 10次/小时 | 模型生成效率低或 prompt 超长被截断,需检查量化或 max_len |
| 业务类 | router:task_success_rate{task_type="rag_qa"},router:retrieval_hit_rate | Router 自埋点 | Success Rate < 95%;Hit Rate < 0.7 | RAG 检索质量差或 LLM 生成失败,需调优 embedding 或 prompt |
Grafana 看板必须包含:
- 实时火焰图:展示每个请求的耗时分布(retrieve vs. llm vs. merge);
- GPU 显存热力图:按节点、按 Pod 展示显存使用趋势;
- Token 生成速率曲线:对比不同模型的 output token/s,直观反映性能差异。
实操心得:vLLM 的 metrics endpoint(
/metrics)默认暴露所有指标,但其中vllm:gpu_cache_usage_ratio是最关键的健康指标。我们用 PromQL 查询avg(vllm_gpu_cache_usage_ratio) by (instance),当均值 < 0.6 时,自动触发vllm compact命令(需提前在容器里装好 vLLM CLI)。这比人工巡检高效得多。
5. 常见问题与排查技巧实录:产线高频故障的根因与解法
5.1 故障现象:P95 延迟突增 300%,但 CPU/GPU 利用率正常
典型场景:某天下午 3 点,Grafana 看板显示 vLLM 的request_latency_seconds_p95从 800ms 暴涨到 3200ms,但nvidia_smi_gpu_utilization仍稳定在 65%,container_cpu_usage_seconds_total也无异常。
排查路径:
- 首先确认是否是流量突增:查
rate(http_request_total[5m]),发现 QPS 仅从 120→135,增幅 12.5%,不足以解释 4 倍延迟增长; - 查
vllm:time_in_queue_seconds_sum,发现该指标飙升,说明请求在队列里堆积; - 查
vllm:gpu_cache_usage_ratio,发现从 0.82 降到 0.