一个故障样本:从慢请求到离线复盘#
前面的扩展与调试章节已经分别把 metrics、trace、request logger、dump、RequestStage、/v1/loads 和 schedule simulator 讲开了,但如果一本技术书停在这里,读者仍然可能只得到一组工具说明,而不是一套真正可执行的维护路径。整合样章的作用,就是把这些分散工具重新压回同一个故障样本上,让读者看到它们在现场到底怎样一起工作。
这里选一个最常见、也最容易跨层误判的场景:某一类请求突然变慢。更具体地说,外部现象是:
- 整体 QPS 没明显下降
- 只有部分请求 TTFT 明显上升
- 请求最终能返回,但尾延迟和 streaming 体感都变差
这类问题最容易把人带偏。因为它既不像“系统整体崩了”,也不像“某个接口直接报错”,而是介于调度、执行、回包和放置之间的一类复合故障。如果不能把证据顺序走对,你很容易在 Scheduler、ModelRunner、tool parser、DetokenizerManager 甚至上游客户端之间来回跳。
先把问题压成一个维护者问题#
面对这类样本,最不应该做的事情就是直接开代码。更稳的起手式是把它先压成一句明确的问题:
这是整体退化,还是一小类请求的局部退化?它是卡在请求进入、等待 batch、执行后半段,还是回包收尾?
只有这个问题先压清楚,后面的工具才会按顺序出现,而不是一股脑一起上。
第一步:先确认它是不是系统整体变慢#
这一步先看的是 metrics,而不是 trace。原因很简单,metrics 最适合回答“这是系统级趋势,还是局部样本”。如果你看到的是:
- 全体请求的 queue 相关指标都在升高
- waiting / running 相关负载都在抬升
/v1/loads也显示多个 rank 同时吃紧
那就说明问题更偏全局调度或资源压力,后续应该优先回到第 4 节和 placement 相关章节。
但如果 metrics 告诉你:
- 整体吞吐还在
- 只是部分请求的 TTFT 和完成时间拉长
- 某些 routing key 或某些
rid特别慢
那就说明这不是“整套系统都慢了”,而更像少数请求在链路某一段卡住了。只有先把这一层判断做好,trace 才不会变成无穷大的搜索空间。
第二步:再用时间线确认它到底卡在哪一段#
一旦确认问题更偏局部样本,第二步应该回到 RequestStage / ReqTimeStats。因为这层最擅长回答的不是“结果是什么”,而是“卡在流程哪一段”。
对这类慢请求,最值得先比较的是:
queue_time有没有显著高于同类请求- TTFT 是不是一起上升
response_sent_to_client_time和finished_time是不是又被进一步拉开
这三个量如果一起看,通常就能把问题先粗分成三类:
queue_time高,TTFT 也高
说明更偏 waiting queue / batch 成形 / placement 问题。queue_time不高,但 TTFT 高
更像 prefill / 执行前半段或 tokenizer 侧的问题。- TTFT 还可以,但
response_sent_to_client_time和finished_time被拉长
更像输出后半段、回包链或 request 收尾阶段的问题。
这一步的价值,不在于它能直接给出根因,而在于它先把“慢”拆成几种不一样的慢。一本成熟的系统书到这里,应该已经让读者知道:慢请求不是单一症状,而是时间线上不同切片的异常组合。
第三步:再围绕单个 rid 把首尾事实拉齐#
一旦时间线提示“这是个别请求问题”,下一步就应该锁定一个 rid,然后回到 request logger 和 exporter 看这条请求的首尾事实。这里最该确认的是:
- 这条请求的输入形态是什么
- finish reason 是什么
- 它是否带特殊 routing / priority / tool / schema 负载
- 它是不是某类请求的代表样本,而不是随机异常
这一层特别重要,因为很多看起来像执行层的问题,最后会被 request logger 揭示成“其实是某类输入组合总是触发同一条边界路径”。例如:
- 某个 routing key 特别集中到一小部分 rank
- 某类长 prompt 请求在完成时总是慢
- 某些 output 形态总是伴随更高的尾延迟
也就是说,logger 在这里不是为了替代 trace,而是为了先把样本的“外部故事线”站稳。
第四步:决定是继续追链路,还是转成离线材料#
如果到这一步你已经知道:
- 它不是全局系统退化
- 它确实卡在 queue / prefill / 回包中的某一段
- 它还具有某类稳定输入形态或 routing 特征
那么下一步要做的就不是“继续盲看更多日志”,而是决定这条样本要不要升级成离线材料。
这里有两条不同路线。
第一条路线更适合功能和故障重现:
如果你怀疑的是 crash、协议解释错误、输出异常、detokenizer 收口错误,就更适合启用 request dump / crash dump,往 replay 路线走。
第二条路线更适合调度和 placement 问题:
如果你怀疑的是 queue time 异常、routing 偏斜、batch 成形退化、某类 prompt 长度组合总是很慢,那 request logger -> schedule simulator 这条链通常更值钱。
这一步本质上是在做一个维护判断:你要的是“把功能场景重放出来”,还是“把调度场景抽出来做策略比较”。把这两种离线路线分开,是维护层最重要的工程判断之一。
第五步:把线上慢请求升级成离线调度样本#
如果判断这更像调度或放置问题,就应该顺着 request logger 和 schedule simulator 往下走。此时真正高价值的不是再多抓一轮线上数据,而是把这类 finished request 变成一个可复用样本集。
这条离线复盘链的意义在于:
- 你可以先用线上真实请求形状构造离线实验
- 再比较不同 scheduler / routing / budget 假设
- 最后再决定是否值得回线上动代码
这比“先改再看”稳得多,因为它把高风险的策略变更先挪到了低风险环境。对一本技术书来说,这样的案例也比只讲命令更有价值:它让维护者看到证据怎样真正变成决策材料。
把整个故障样本压成一条最小工作流#
如果把这个案例重新压成一条可以带去现场的工作流,可以写成这样:
1. 先看 metrics:确认是不是系统整体退化
2. 再看 RequestStage / ReqTimeStats:确认慢在 queue、TTFT 还是收尾
3. 锁定一个 rid:用 request logger / exporter 把样本首尾事实拉齐
4. 判断离线方向:
- 功能/崩溃类:走 dump / replay
- 调度/放置类:走 request logger -> schedule simulator
5. 用离线结果决定是否值得继续改 scheduler / routing / batch shaping这条工作流的价值不在于“步骤很多”,而在于每一步都只回答一个更小的问题,因此不会过早跳进不该看的实现层。
这章和前文怎样真正闭环#
这章故意不重复定义 metrics、trace、logger、dump 和 simulator,因为这些前文已经分别讲过。它真正承担的是“把前文这些能力重新组织成一个维护者能照着走的样章”。
它和整本书的回扣点也很明确:
- 回扣第 2 节:慢请求首先是一条具体请求,必须先确认主链有没有走完整。
- 回扣第 4 节:如果
queue_time高,最终大多要回到 waiting queue、placement 或 batch 成形。 - 回扣第 5 节:如果 TTFT 或输出后半段异常,再回执行模型和收尾边界。
- 回扣第 7 节:一旦需要真正进源码,优先回
scheduler.py、tokenizer_manager.py、detokenizer_manager.py、request_logger.py或 placement 相关入口。
也就是说,这章的作用不是多教一种工具,而是把“整本书怎样真正被用于一次维护动作”讲完整。
小结#
一本成熟的系统书,最后不应该只剩“有哪些工具可以用”,而应该能给出至少一个完整样章:当系统真的不按预期工作时,维护者怎样一步步从现象走到证据,再从证据走到离线复盘和下一步决策。
这章补的正是这条最后的闭环。到这里,第 8 节就不再只是很多排障专题,而开始像一本真正能带去现场的技术书后半程。
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。