一个故障样本:从慢请求到离线复盘#

前面的扩展与调试章节已经分别把 metrics、trace、request logger、dump、RequestStage/v1/loads 和 schedule simulator 讲开了,但如果一本技术书停在这里,读者仍然可能只得到一组工具说明,而不是一套真正可执行的维护路径。整合样章的作用,就是把这些分散工具重新压回同一个故障样本上,让读者看到它们在现场到底怎样一起工作。

这里选一个最常见、也最容易跨层误判的场景:某一类请求突然变慢。更具体地说,外部现象是:

  • 整体 QPS 没明显下降
  • 只有部分请求 TTFT 明显上升
  • 请求最终能返回,但尾延迟和 streaming 体感都变差

这类问题最容易把人带偏。因为它既不像“系统整体崩了”,也不像“某个接口直接报错”,而是介于调度、执行、回包和放置之间的一类复合故障。如果不能把证据顺序走对,你很容易在 SchedulerModelRunnertool parserDetokenizerManager 甚至上游客户端之间来回跳。

先把问题压成一个维护者问题#

面对这类样本,最不应该做的事情就是直接开代码。更稳的起手式是把它先压成一句明确的问题:

这是整体退化,还是一小类请求的局部退化?它是卡在请求进入、等待 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_timefinished_time 是不是又被进一步拉开

这三个量如果一起看,通常就能把问题先粗分成三类:

  1. queue_time 高,TTFT 也高
    说明更偏 waiting queue / batch 成形 / placement 问题。
  2. queue_time 不高,但 TTFT 高
    更像 prefill / 执行前半段或 tokenizer 侧的问题。
  3. TTFT 还可以,但 response_sent_to_client_timefinished_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.pytokenizer_manager.pydetokenizer_manager.pyrequest_logger.py 或 placement 相关入口。

也就是说,这章的作用不是多教一种工具,而是把“整本书怎样真正被用于一次维护动作”讲完整。

小结#

一本成熟的系统书,最后不应该只剩“有哪些工具可以用”,而应该能给出至少一个完整样章:当系统真的不按预期工作时,维护者怎样一步步从现象走到证据,再从证据走到离线复盘和下一步决策。

这章补的正是这条最后的闭环。到这里,第 8 节就不再只是很多排障专题,而开始像一本真正能带去现场的技术书后半程。