1.1 为什么 QPS、P99、错误率在大模型场景会失灵


文档摘要

1.1 为什么 QPS、P99、错误率在大模型场景会失灵 当团队给大语言模型(LLM)推理服务做监控时,第一反应通常是把 Web 后端的面板抄一遍:QPS、P99 延迟、错误率,俗称"三板斧"。这三板斧在传统 RESTful 微服务里久经考验,几乎成了监控的肌肉记忆。但它们背后藏着三个心照不宣的假设--请求彼此独立、处理成本大致相同、失败是离散可数的。而大模型推理恰恰把这三个假设一个个掀翻。这一节我们就逐一把它们拆开,看清"失灵"到底失灵在哪,以及你该换什么来替代。 假设一:请求彼此独立 → 实际共享 KV Cache 与批处理 传统 Web 服务里,一个请求占用的 CPU、内存与它前后哪个请求进来基本无关,你能把 QPS 当成"单位时间完成的工作量"来横比。

1.1 为什么 QPS、P99、错误率在大模型场景会失灵

当团队给大语言模型(LLM)推理服务做监控时,第一反应通常是把 Web 后端的面板抄一遍:QPS、P99 延迟、错误率,俗称"三板斧"。这三板斧在传统 RESTful 微服务里久经考验,几乎成了监控的肌肉记忆。但它们背后藏着三个心照不宣的假设--请求彼此独立、处理成本大致相同、失败是离散可数的。而大模型推理恰恰把这三个假设一个个掀翻。这一节我们就逐一把它们拆开,看清"失灵"到底失灵在哪,以及你该换什么来替代。

假设一:请求彼此独立 → 实际共享 KV Cache 与批处理

传统 Web 服务里,一个请求占用的 CPU、内存与它前后哪个请求进来基本无关,你能把 QPS 当成"单位时间完成的工作量"来横比。但 LLM 推理引擎(无论是 vLLM、TGI 还是 TensorRT-LLM)普遍采用动态批处理(continuous batching)与 KV Cache 复用:多个请求在 GPU 上拼成一个 batch 一起算,它们共享同一块显存里的 KV Cache 池,Prefill 阶段算出来的 prompt 表示还会被后续 Decode 反复读取。

后果是请求之间高度耦合。一个 32k 上下文的超长请求进来,会占住大量 KV Cache 槽位,导致同池里其他短请求被迫排队甚至被抢占;反过来,一批短请求并发时又能把吞吐抬得很高。于是"QPS"这个数字变得几乎没有横向可比性--同样是 100 QPS,全是短问答和混了长文档摘要,对系统的压力可以差出一个数量级。你盯着 QPS 曲线,看到的不是工作量,而是"恰好进来的请求长什么样",这是个被请求分布绑架的噪声指标。更隐蔽的是,当系统开始恶化,它往往不是降低 QPS(请求还在源源不断进来),而是通过拉长排队和拒绝新请求来"硬撑",此时 QPS 反而可能看着平稳,欺骗性更强。

替代度量:不要把 QPS 当成负载主指标,改盯“在途请求数(in-flight)”“KV Cache 已用比例”“批内平均/最大序列长度”。在途请求数直接反映系统正在扛多少活,KV Cache 水位预告还能不能接新请求,批内最大序列长度则提前暴露“会被谁拖垮”。这三个指标合起来,比 QPS 早得多地预示拥塞。一个具体例子:某服务 QPS 稳定在 80,但某天在途请求数从常值 40 悄悄爬到 320,KV 水位从 60% 升到 97%,批内最大序列长度翻了五倍——此时 QPS 纹丝不动,用户却已经普遍多等了十几秒。如果只看 QPS,这起拥塞在面板上是完全透明的。

假设二:处理成本大致相同 → 实际由输出长度主导,差百倍

Web 接口里,处理一个请求的成本大致正比于输入大小,且输入输出长度都在可预期范围内,一个接口的平均耗时波动通常不超过两三倍。LLM 推理则完全不同:延迟和算力消耗主要由"生成的 token 数"决定,而生成长度高度不可控--同一个 prompt,模型可能回 20 个 token,也可能滔滔不绝回 2000 个,相差百倍。一次 2000 token 的生成,其 Decode 步数是 20 token 的一百倍,消耗的算力、占用的 KV Cache 时间也接近百倍。

这意味着两件事。第一,平均延迟会被请求分布严重扭曲:少数超长输出会拉爆 P99,而你的面板如果只画均值和 P50,就对这些"长尾"视而不见(这正是开篇那幕事故的根因)。更要命的是,P99 本身还不够--你还需要知道"P99 对应的输出有多长",否则无法判断这是模型问题还是监控误报。第二,"错误率"也失准了:很多退化不是返回 500,而是"答非所问""中途截断""触发长度上限被截断"--HTTP 层面是 200,业务层面却是失败。如果只监控 HTTP 状态码,这类失败在你的错误率里是 0%。我曾见过一个生产案例:模型开启了 max_tokens 上限,长回答被静默截断,前端把截断文本当完整答案展示,错误率仪表盘连续三周是 0,直到用户投诉才暴露--而真正的"截断率"指标那天之后才被加进面板。

替代度量:延迟必须按“输出长度分桶”来看,例如把请求按生成 token 数分成短(<128)、中(128–1024)、长(>1024)三档,分别画 P50/P90/P99;同时新增“截断率”“流式中断率”两个业务层失败指标,它们才是 LLM 场景真正该被钉在告警里的“错误率”。这里有个实操细节:分桶不要机械按固定阈值,而要结合你模型的典型用途——客服问答多落在短桶,长文档摘要多落在长桶,两个桶的 P99 天生差几倍,画在同一张未按桶拆分的图里会互相掩盖。正确做法是用 Grafana 的按标签拆分(by output_length_bucket),让三档的曲线并排,这样“长桶 P99 暴涨但短桶平稳”这种信号一眼可见。

假设三:失败离散可数 → 实际降级与超时隐性化

传统服务挂了就是挂了,错误率跳变、健康检查变红,一目了然,一次故障对应一段明显的错误率尖峰。LLM 推理的失败常常是"软"的:显存压力大了,引擎可能默默把批大小调小、把排队时间拉长,而不是报错;上游限流可能返回 429 但被重试逻辑吞掉,对外表现只是"偶尔慢";负载高时排队超时,客户端断开,服务端却记为"完成"。这些"软失败"不会让你的错误率仪表盘变红,但用户体验在肉眼可见地变差。

更棘手的是"部分失败"。流式输出(streaming)场景下,前 100 个 token 正常返回,第 101 个因为 OOM 被截断,客户端看到的是一段不完整的话,服务端日志可能只是打了个 warning,最终 HTTP 还可能回 200。这类问题,靠 HTTP 错误率永远发现不了,必须深入到 token 级、流级去度量--例如统计"流式连接中提前终止的比例""首 token 延迟(TTFT)超阈值的比例"。

还有一个常被忽略的软失败来源:降级。很多推理服务在过载时会自动降级,比如临时关闭流式、降低采样质量、或退回更小的模型。这些降级在指标上几乎无痕,却直接拉低了用户感知的质量。监控时若只看“有没有报错”,就会对这些静默降级完全失明。

如何把这个隐性失败“显形”?给你三把探针:第一,在请求打点时带一个 reason 字段记录终止原因(正常结束 / 客户端断开 / 服务端截断 / 超时),用 reason 维度去做失败率拆解,让“非正常的结束原因”浮出水面;第二,对流式连接单独统计“首 token 延迟(TTFT)超阈值比例”和“连接提前关闭比例”,前者暴露排队与 Prefill 压力,后者暴露 Decode 阶段的中断;第三,把“降级开关状态”也作为一个指标上报(例如 0/1 表示是否处于降级模式),一旦它跳变,立刻关联同一时段的延迟与质量变化。三把探针合起来,软失败就不再是黑洞。

三板斧之外,还漏了什么

除了三板斧本身的失灵,传统监控还有一个更隐蔽的盲区:它几乎不碰“token 级成本”和“缓存命中”。LLM 推理的计费与资源消耗是按 token 计的,但多数面板只到“请求”粒度,于是你既算不清单次调用的真实成本,也无法发现“同一类 prompt 被反复计算”的浪费。Prometheus 里应该有一个 token 总量指标(prompt_tokens、completion_tokens),它既是成本归因的依据,也是容量预测的输入——因为显存与算力消耗最终都落在 token 上。

另一块是“缓存命中”:很多推理服务或网关层会做语义缓存(相似 prompt 直接返回历史结果),命中率高低直接决定真实负载。如果只盯 QPS,你会以为系统在处理 100 个请求,实际上 60 个被缓存拦截、真实落到 GPU 的只有 40 个——容量规划会因此严重误判。把缓存命中率单独成指标,才能把“表面流量”和“真实算力流量”区分开。这一点在接入网关或代理层时尤其关键,后续第 3 章会展开。

把三板斧换成三个转向

三板斧失灵,不是因为它们没用,而是因为它们度量的是"请求这个动作",而 LLM 推理真正该度量的是"资源占用"与"生成过程"。据此,监控思路要完成三个转向:

  • 从"看 QPS"转向"看显存占用率与 KV Cache 命中/淘汰"--这才是推理服务的真实负载;QPS 只作为辅助的业务量参考,绝不能作为容量告警的主力。
  • 从"看 P99 均值"转向"看分位随输出长度的拆解",并把流式中断、截断、超时单独成指标;延迟面板必须按输出长度分桶,否则长尾永远藏在均值里。
  • 从"看 HTTP 错误率"转向"看排队时长、批内等待、降级次数"这些"软失败"信号;把"软失败率"定义清楚,让它和 HTTP 错误率并列成为告警双支柱。
```mermaid graph TD T[传统三板斧] --> T1[QPS] T --> T2[P99 延迟] T --> T3[错误率] T1 --> F1[被请求分布绑架,不可比] T2 --> F2[被长尾拉爆,均值掩盖] T3 --> F3[只覆盖 HTTP 层,漏软失败] F1 --> N[看在途请求+KV Cache 水位] F2 --> N2[看分位 × 输出长度拆解] F3 --> N3[看排队/降级/流中断] ```

这张图把"失灵原因 → 替代度量"直接对应起来,建议截图存进你的监控设计文档:它几乎就是后面第 2 章指标体系的提纲。

```mermaid graph LR subgraph 传统假设 A1[请求独立] A2[成本相同] A3[失败离散] end subgraph LLM 现实 B1[共享 KV Cache 与批处理] B2[输出长度主导,差百倍] B3[软失败/部分失败] end A1 -->|推翻| B1 A2 -->|推翻| B2 A3 -->|推翻| B3 ```

把三组"假设 vs 现实"并排看,你会发现它们不是偶发 bug,而是范式不匹配--这正是为什么复制 Web 监控模板到大模型场景必然水土不服。

```mermaid graph TD P[典型生产事故] --> P1[面板全绿:QPS 稳/错误率 0/P99 平] P --> P2[客诉:变慢/截断] P1 --> R[根因:均值掩盖长尾 + HTTP 层漏软失败] R --> S[解法:切到资源与生成过程度量] S --> S1[在途请求+KV 水位] S --> S2[分位×长度] S --> S3[软失败率] ```

最后用开篇那幕事故收尾:它不是一个孤例,而是"用错度量维度"的必然结果。当你把度量维度从"请求动作"换到"资源占用 + 生成过程",这类静默劣化才第一次变得可见。

小结:这一节我们拆穿了三板斧的三个底层假设--请求并不独立(共享 KV 与批处理)、成本绝不雷同(输出长度差百倍)、失败常常隐性(软失败与部分失败),并给出了三个度量转向:看在途与 KV 水位、看分位乘输出长度、看软失败率。记住一句话--大模型推理监控要看"资源与生成",不要只看"请求"。带着这个认知,下一节我们深入它背后的物理约束:显存、算力、排队。


发布者: 作者: 分词分到自闭的小龙虾 转发
评论区 (0)
U