推理服务的可观测性:从用户体验、流式交付到 GPU 证据的完整排障闭环用户说“回答很慢”,工程师需要回答四个问题:慢在哪里,影响谁,有什么证据,怎么验证修复。本文以 Kubernetes 上的流式 LLM 推理服务为主线,结合框架指标、OpenTelemetry、自定义阶段埋点、DCGM 和 eBPF,建立从用户体验到 GPU 执行的关联分析方法。适用于 vLLM 等推理框架;具体指标与参数必须以部署版本为准。文中的阈值、案例数字和容量配置均为教学示例,不代表实测结果或通用性能承诺。这张图只展示在线请求的主路径;实际关联必须同时维护两条辅助路径:请求路径:客户端 → Ingress/网关 → 业务/RAG → 推理引擎 → GPU/rank Metrics: 各层低基数指标 ─────────────────────────→ Prometheus/Grafana/SLO Trace: trace_id/request_id ─────────────────────→ OTel Collector/Trace Store 映射: model → Pod UID → node/worker → GPU UUID/rank → batch_id(采样追踪) 下钻: 体验告警 → model/pod 看板 → 慢请求 Trace → 短窗口 eBPF/NsightPrometheus 只保存有限枚举标签;request_id、trace_id、batch_id只放日志/追踪。图或映射中任何一环不存在时,禁止把节点全部 GPU 活动强行归属给单个请求。术语与数据契约(先读本节)术语严格含义不应写成引擎 ITL引擎实际导出相邻 token/调度事件的间隔客户端 SSE 间隔chunk gap本层收到相邻有效 SSE 内容事件的间隔;一个事件可含多个 token每 token 生成时间用户可见 TTFT客户端实际展示首个有效内容减去用户发起时间首字节、role、心跳时间请求级平均 TPOT用首/末有效输出和可信N推导,且仅N1逐 token 精确时钟若只有代理/客户端视角,面板必须叫chunk_gap,而不是 ITL。输入 token 吞吐是输入工作负载代理,前缀缓存、KV 重用和分块 Prefill 存在时不等于实际 Prefill 计算量。一、用户的一句“慢”,可能是四类不同的问题上线了流式推理接口,GPU 利用率在 90% 左右,输出吞吐也很高,但用户仍在投诉。原因可能完全不同:请求发出后几秒没有内容:首个有效输出晚,优先检查网关、业务前置处理、排队和 Prefill。很快开始回答,但持续生成很慢:检查 Decode 调度、活跃序列、KV Cache、计算、访存和通信。回答一阵后突然停顿,再一次性出现大量文字:检查流式缓冲、客户端读取、批量输出、调度中断和抢占。每个 token 都正常,但整段回答耗时很长:可能只是输出长度变大,不能直接认定模型变慢。因此,第一步不是加卡,而是把“慢”转化为明确的测量口径。可观测性建设的顺序也应如此:先量化体验,再拆解阶段,最后用硬件证据解释原因。二、先统一指标口径:TTFT、TPOT、ITL 不是同一件事2.1 为每个测量位置定义时间点在客户端或网关的一次请求观测中,定义:时间点含义t0本层开始发送请求或接收请求t1本层收到第一个有效输出 token/内容事件tN本层收到最后一个有效输出tend本层收到流完成标记或请求结束N本次请求的输出 token 数,由服务端 tokenizer/usage 等可信来源统计“有效输出”要写入接口契约:角色信息、SSE 心跳、空 delta 不计入;如果产品隐藏思考内容,需要同时记录“首个模型输出”和“首个用户可见内容”。只有后者直接解释用户为什么迟迟看不到回答。同一个指标必须带测量位置:客户端 TTFT、网关 TTFT、引擎 TTFT 的起止点不同,数值不能直接比较。非流式接口收到完整响应前通常看不到首个 token,不能拿首字节时间冒充流式 TTFT。2.2 指标定义与使用场景指标推荐口径用来回答什么TTFTt1 − t0用户等多久才看到首个有效输出单请求平均 TPOT(tN − t1) / (N − 1),仅 N 1首个 token 之后,平均每 token 需要多久ITL相邻输出 token 的到达间隔;若只观测到 chunk,则记录 chunk gap生成过程中是否存在长停顿生成跨度tN − t0从请求开始到最后有效输出的时间E2Etend − t0整个请求何时结束,包括尾部收尾单请求生成速度(N − 1) / (tN − t1)一条请求的平均生成速度,token/s聚合输出吞吐窗口内实际生成的输出 token / 窗口秒数整个服务的输出能力输入吞吐窗口内处理的输入 token / 窗口秒数Prefill 侧工作量队列长度/排队时间等待请求数/请求等待调度的实际时长首字慢是否来自排队对同一测量位置、同一输出 token 口径:tN − t0 = TTFT + TPOT × (N − 1)若 E2E 定义到完成标记,还应加上tend − tN。N 为 0 或 1 时,TPOT 不定义,不能填 0 混入延迟直方图。失败、取消和没有输出的请求单独统计结果与耗时,避免只看成功样本。例如:首个输出等待 0.8 秒,200 个输出 token 的平均 TPOT 为 0.04 秒,则生成跨度为0.8 + 199 × 0.04 = 8.76 秒。其中约 25 token/s 是这条请求的速度,不能用集群的 5,000 token/s 来替代。2.3 chunk 不是 token,TPOT 分位数也不是 ITL 分位数SSE 的一个内容事件可以包含多个 token。一次网络读取还可能包含多个 SSE 事件;token 的文本片段也不能通过“字符数”准确反推 token 数。推测解码、输出合并与代理缓冲都可能进一步改变到达形态。因此建议保留两套统计:请求级平均 TPOT:每个完成请求产生一个样本,适合比较请求体验。输出事件间隔:每个间隔产生一个样本,适合发现停顿;面板必须标清 token gap 还是 chunk gap。长请求贡献更多间隔样本。ITL P99 与请求 TPOT P99 权重不同,不能互换。vLLM 当前文档也区分了输出事件间隔与请求级 TPOT,旧版指标名和语义可能不同。[1][2]2.4 分位数之前,先控制工作负载差异至少按模型版本、输入长度档、输出长度档、业务优先级和副本查看分布。常见误判是:模型没有退化,只是输入从短问答变成了长上下文 RAG。长度档使用有限枚举,例如0–1k / 1k–4k / 4k–16k / 16k;档位应与业务匹配。不要把精确 token 长度、用户 ID 或 request_id 放到 Prometheus 标签中。还要记住三条统计边界:不能平均多个副本的 P99;应合并相同桶边界的 Histogram,再计算分位数。P99(TTFT) − P99(queue)不是 Prefill P99。P99(TTFT) + P99(decode)不是 E2E P99。分位数对应的可能是不同请求。三、把请求拆开:找到首字之前和生成之中的等待3.1 建立阶段契约,而不是画一张含糊的耗时图对单个请求,建议记录以下阶段。名字可以自定义,起止事件必须固定。阶段建议起止点常见慢因网关处理网关接收 → 向后端转发鉴权、限流、连接池、路由业务前置业务服务接收 → 向引擎提交RAG 检索、工具调用、Prompt 拼接前处理引擎接收 → 请求进入调度等待分词、多模态预处理、参数校验初次排队入队 → 第一次被调度容量不足、优先级、KV 空间限制Prefill首次相关执行 → 首 token 产生输入长度、前缀缓存、执行配置Decode 生命周期首 token → 最后 token多轮调度、抢占、计算/访存/通信流式交付引擎产生事件 → 网关转发/客户端收到输出队列、代理缓冲、网络、慢客户端连续批处理、分块 Prefill、Prefill/Decode 分离和抢占会让阶段反复发生。此时记录事件与区间:入队、调度、暂停、恢复、首次输出、最后输出,而不是假定每个请求只有一次完整 Prefill 和一次连续 Decode。请求生命周期时长也不等于该请求独占的 GPU 时间。多条请求共用一个 batch,一次 GPU 执行可能同时推进多个请求。若需要归因,可在采样追踪中记录 batch_id、调度步与参与请求,使用 span link 或事件关联;不要把整个 batch 的 GPU 时长重复相加后当成集群总耗时。3.2 如何解释客户端与服务端的差值同一请求的客户端 TTFT 明显高于服务端 TTFT,说明额外耗时存在于引擎测量范围之外,可能包含连接建立、网关、业务前置、转发缓冲、网络和客户端调度。这个差值是缩小范围的线索,不能直接命名为“网络延迟”。下一步对比网关收到后端首内容、网关写出首内容、客户端读取首内容的事件,再检查连接与缓冲。用单调时钟测本进程耗时,用同步后的墙钟关联不同主机的时间线。不要直接拿不同机器的单调时间戳相减;时钟偏差也会影响跨主机事件排序。四、分层采集:每种工具负责一种证据工具/数据适合回答不能单独证明客户端与网关埋点用户看到首内容和持续输出的时间模型哪个 kernel 慢推理框架指标排队、请求延迟、吞吐、缓存状态单个慢请求的完整因果链OTel 与自定义阶段埋点请求经过哪些阶段,在哪里等待异步 GPU 的真实执行完成时间DCGM