读 health_generate:探活请求怎样穿过 runtime#

这章解决什么问题#

前面的章节已经分别从两个角度讲过 health check:

  • request lifecycle 里,它被归为“非生成请求、探测与控制请求分叉”的一部分
  • extension/debugging 里,它被归为 liveness 链的一部分

但如果你真的要顺着源码读进去,仍然会碰到一个具体问题:

/health / /health_generate 这类请求,究竟是普通生成请求的缩小版,还是一条被 runtime 明确特判的特殊路径?

这章的目标,就是把这条探活源码链单独讲清。

为什么这条链值得单独成章#

如果不把它单独讲出来,读者很容易形成两种都不准确的理解:

  1. 以为 health check 就是随便发一条普通请求看看能不能回
  2. 以为 health check 只是 HTTP server 自己返回个 200,不真正经过 runtime

源码实际走的是第三种更有工程意味的路径:

  • 健康检查确实会构造真实请求对象进入 runtime
  • 但 scheduler 又会在 busy 场景下对它做特殊处理,避免它被长 prefill 阻塞

这说明它是一条“部分真实、部分特判”的探活路径。这样的设计非常值得一本技术书专门解释。

一张图:health check 不是单纯 HTTP 200,也不是完全普通请求#

这张图解决的理解障碍是:很多读者会把探活链路想成普通生成请求的微缩版,但 scheduler 中间 actually 有专门的 shortcut。

flowchart LR
    HC["/health or /health_generate"] --> TM["TokenizerManager.generate_request(...)"]
    TM --> SCH["Scheduler.process_input_requests(...)"]
    SCH -->|busy| Sig["return_health_check_ipcs + HealthCheckOutput"]
    SCH -->|idle| Normal["normal request dispatch"]
    Sig --> TM2["TokenizerManager.last_receive_tstamp"]
    Normal --> DET["Detokenizer / normal return path"]
    DET --> TM2

图比纯文字多解释的一点是:health check 同时具备两种人格。

  • 空闲时,它更像一条真实最小请求
  • 忙碌时,它更像一条被 scheduler 快速确认的探活信号

入口层:http_server.py::health_generate(...) 应该怎么读#

这段逻辑其实非常值得直接记住,因为它已经把探活路径的边界写得很清楚:

  1. 如果服务正在优雅退出,直接返回 503
  2. 如果 server_status == Starting,直接返回 503
  3. 如果关闭了生成式 health endpoint,普通 /health 可以直接返回 200
  4. 否则构造一条最小请求:
    • generation 模型用 GenerateReqInput
    • 非 generation 模型用 EmbeddingReqInput
  5. 启一个内部 task 调 tokenizer_manager.generate_request(...)
  6. 然后轮询 last_receive_tstamp

这说明 HTTP 入口本身的职责非常务实:

  • 先做最外层短路判断
  • 再决定要不要真的把探活请求送进 runtime
  • 最后只根据“有没有收到 runtime 某种回复”来判定健康

为什么它要构造真实 GenerateReqInput / EmbeddingReqInput#

这非常重要。health check 不是只 ping 某个内部变量,而是真的用:

  • GenerateReqInput(rid=..., input_ids=[0], sampling_params={"max_new_tokens":1,...})
  • EmbeddingReqInput(...)

进入 TokenizerManager.generate_request(...)

这说明 health check 的目标不是确认某个 endpoint 在监听,而是确认:

  • manager 能接请求
  • scheduler 还能推进
  • 至少有一条回包链还能活着

从系统设计角度看,这是一本技术书非常应该强调的点:探活信号越贴近真实运行路径,越能减少“HTTP 活着但 runtime 其实死了”的假阳性。

为什么它又不是完全普通请求#

真正的特殊性出现在 scheduler。

scheduler.py::process_input_requests(...) 里有一条非常关键的判断:

  • 如果 is_health_check_generate_req(recv_req)
  • 并且当前 scheduler 不是 fully idle
  • 那就不把这条请求按普通路径推进,而是把 http_worker_ipc 放进 return_health_check_ipcs

这意味着:

  • 当系统空闲时,health check 可以按正常最小请求跑一遍
  • 当系统繁忙时,健康检查不应该成为新的负担

这是一个非常成熟的工程权衡。

return_health_check_ipcs 为什么是这条链的真正关键#

很多读者第一次看这段代码时,注意力会停在 is_health_check_generate_req(...) 这个 helper 上,但真正更值得记住的是:

return_health_check_ipcs

它代表的不是“某条特殊请求”,而是:

  • 某个 HTTP worker 正在等待一条探活确认
  • 这条确认不一定需要完整跑完一次 generation path

这是把探活信号从“业务请求”转成“liveness 观察事件”的关键一步。

maybe_send_health_check_signal() 真正在做什么#

这段函数非常短,但非常重要:

  • 如果 return_health_check_ipcs 非空
  • 就构造一个 HealthCheckOutput(http_worker_ipc=...)
  • 然后通过 send_to_tokenizer.send_output(...) 发回去

也就是说,scheduler 在 busy 场景下选择发送的,不是完整生成结果,而是一种“我还活着”的专用输出对象。

这再次说明 health check 不是普通生成请求的缩小版,而是 scheduler 与 tokenizer 之间的一种半旁路探活协议。

TokenizerManager.last_receive_tstamp 为什么是最终判据#

http_server.py::health_generate(...) 在入口侧不等待具体输出内容,而是轮询:

  • tokenizer_manager.last_receive_tstamp

只要它在 health check 发出之后被刷新,就认为系统健康。

这说明入口侧最终关心的是:

  • runtime 是否仍能回送某种东西

而不是:

  • 这条最小请求的业务语义是否完成得多漂亮

这种判据非常合理,因为探活的目标是证明“链路在前进”,不是证明“业务结果完全正确”。

HealthCheckOutput 为什么是正式对象,而不是裸信号#

io_struct.py 里单独定义了 HealthCheckOutput。这件事很值得技术书点出来,因为它说明:

  • health check 特判不是临时魔法分支
  • 而是被建模成正式 runtime 消息类型

从系统设计角度看,这很漂亮:

  • 生成请求有自己的对象
  • 输出有自己的对象
  • 探活信号也有自己的对象

整个 IPC 体系因此保持了一致的“消息都是对象”风格。

这条链对排障有什么直接价值#

很多现场问题其实都能通过这条链更快定位:

1. /health 失败,但普通请求偶尔还能通#

这类问题更可能和 server_statuslast_receive_tstamp 或 scheduler busy 路径有关,而不一定是业务主链完全死了。

2. HTTP 端口还在,但 /health_generate 失败#

优先看:

  • server_status 是否已经变成 UnHealthy
  • scheduler 是否还在发 HealthCheckOutput
  • tokenizer manager 的 last_receive_tstamp 是否长期不变

3. 系统很忙时,health check 波动#

优先看 return_health_check_ipcsmaybe_send_health_check_signal() 是否仍然按预期工作,而不是先去怀疑模型前向本身。

如果你要顺着源码读这条链,推荐顺序是什么#

建议按下面顺序:

  1. http_server.py::health_generate(...)
  2. scheduler.py::is_health_check_generate_req(...)
  3. scheduler.py::process_input_requests(...)
  4. scheduler.py::maybe_send_health_check_signal(...)
  5. io_struct.py::HealthCheckOutput
  6. TokenizerManagerlast_receive_tstamp 的更新位置

这样读,你会得到一条真正完整的探活链,而不是几段分散的 if 分支。

这一层最容易出现的误判#

1. 以为 health check 就是普通最小生成请求#

busy 场景下 scheduler 会明确特判。

2. 以为 health check 只是 HTTP server 自己返回 200#

生成式探活路径需要经过 manager 和 scheduler。

3. 以为探活成功意味着业务语义完全正常#

它更准确地说明“链路还在推进”,不是业务 correctness 的完整证明。

小结#

这一章真正要补齐的,是代码导读里探活链路的正式入口:

  • health_generate(...) 负责在入口层决定是否真的进 runtime
  • scheduler 用 return_health_check_ipcsHealthCheckOutput 保护 busy 场景
  • tokenizer 侧最终用 last_receive_tstamp 把探活结果折回成健康判据

到这里,request lifecycle 和 liveness 章节里反复回扣的 health check 逻辑,就终于在源码层真正闭环了。