Watchdog、health check 与 liveness 链#
扩展与调试这一节已经覆盖了 observability、profiling、dump、failure workflow 和更深层缓存故障面,但还有一层特别像生产维护现实的内容,如果不单独收束就很容易被低估:系统怎样判断自己“还活着”,以及当它可能卡住、子进程崩掉、health check 被长 prefill 阻塞时,运行时怎样把这些现象暴露出来。
这章真正讲的不是“请求慢不慢”,而是另一件更底层的事:系统有没有继续前进,卡住时谁先发现,健康检查为什么有时会和真实流量互相影响。把这一层讲清楚之后,维护层才算真正触到 inference runtime 最贴地面的部分。
先把 liveness 看成一条跨进程链,而不是几个零散功能#
如果只看配置项和路由名,watchdog、soft watchdog、health check、dummy health server 看起来像四套互不相干的东西。更稳的理解方式是:它们其实共同组成一条 liveness 观察链。
flowchart LR
HC["HTTP health check"] --> TM["TokenizerManager"]
TM --> SCH["Scheduler"]
SCH --> DET["DetokenizerManager"]
TM --> SW["SubprocessWatchdog"]
SCH --> W1["Scheduler watchdog"]
TM --> W2["Tokenizer soft watchdog"]
DET --> W3["Detokenizer soft watchdog"]这张图最重要的一点是:health check 不只是 HTTP 路由,而要穿过 manager 和 scheduler 才能真正说明系统健康;watchdog 则从不同层并行观察这条链是不是还在前进。
server_args.py 已经把 liveness 面显式暴露出来了#
ServerArgs 里至少公开了:
watchdog_timeoutsoft_watchdog_timeout- 若干和 health checks、CI、keep-alive 相关的控制项
这说明 liveness 不是藏在内部实现里的未文档化行为,而是被当成正式运行时配置面暴露出来的一层人格。技术书在这里最该做的,不是重复列 flag,而是替读者建立一个判断:这些 flag 共同在定义“系统怎样声明自己还活着、以及多久算不活了”。
WatchdogRaw 和 SubprocessWatchdog 其实在盯两类不同问题#
utils/watchdog.py 里最值得区分的是这两类 watchdog。
WatchdogRaw 更偏组件内部有没有继续前进。它依赖:
- counter 是否变化
- 当前组件是否 active
- timeout 是否超过阈值
Scheduler watchdog 就属于这一路。它的价值在于:组件卡住时,不需要等到外层 HTTP 超时,内部就能先给出 stuck 信号。
SubprocessWatchdog 则更偏另一类问题:子进程有没有崩掉、失联或完全没再喂狗。engine.py 在装配完成后会创建它,并挂回 tokenizer manager,作为整条 runtime 的子进程 liveness 观察者。
这两者一起看,才能真正理解为什么 liveness 不是单一指标,而是一条分层观察链。
tokenizer、scheduler、detokenizer 三侧其实各自都在维护前进信号#
这一层特别值得系统书点出来,因为很多人会误以为 watchdog 只属于某一个后台线程。更准确地说,三侧都在各自不同位置维护前进信号。
TokenizerManager 会:
- 初始化 metric collector watchdog
- 维护 soft watchdog
- 维护
_subprocess_watchdog
也就是说,它既负责观察自己有没有继续处理入口与回包,也负责感知更下游的子进程有没有失联。
Scheduler 会同时维护 soft watchdog 和更偏硬性的 watchdog。前者更像调试信息采集,后者更像真正的 stuck detection。
DetokenizerManager 则在自己的事件循环里围绕 recv_pyobj() 和 dispatch 做 disable() / feed()。这说明 detokenizer 卡住时,不需要等到 HTTP 层显式报错,runtime 自己就应该先看见它不再推进。
health check 为什么不能简单等于“HTTP 200”#
这也是这章最值钱的一层现实。scheduler.py 里的逻辑已经很明确:当 server busy 时,某些 health check 不会按普通请求那样走完整执行路径,而是通过 return_health_check_ipcs 和 maybe_send_health_check_signal() 处理,避免健康检查被长 prefill 完全堵死。
这说明 SGLang 对 health check 的理解非常务实:
- 不能让 health check 只验证 HTTP server 还在监听,否则说明不了 runtime 有没有继续前进
- 也不能让 health check 完全伪装成真实请求,否则它会被长上下文或拥塞一起拖垮
这也是为什么健康检查在 inference runtime 里从来不是一个“附属路由”,而是一条精心设计的折中路径。
launch_dummy_health_check_server(...) 说明启动期也需要另一种健康信号#
engine.py 和相关 common 工具里还有 dummy health check server 的路径。这一层特别像成熟系统才会有的设计:在完整 runtime 还没准备好时,外部控制平面也不能完全失明,因此需要一个比完整服务更轻量的健康信号。
它不是为了替代真实 health check,而是为了在启动、切换或某些特殊部署阶段,先提供一条“至少我还活着”的最小信号。这种分层健康信号比简单的“ready / not ready”更贴近现实工程系统。
sigterm_watchdog 把优雅退出也纳入了 liveness 语义#
这也是很容易被忽略的一点。liveness 不只关心“活着时有没有卡住”,也关心“准备退出时有没有在错误时机退出”。sigterm_watchdog(...) 的存在就说明了这一点:当 health check 已经失败时,退出策略会更激进。
这很像一本系统书应有的视角:不是把存活与退出拆成两个互不相干的话题,而是把它们看成同一条生存语义上的两端。
真正常见的三类症状,其实对应三类不同的 liveness 断裂#
第一类是:端口还在,但请求已经不再推进。
这时优先看 soft / hard watchdog 是否已经吐出 stuck 信息,再看 scheduler counter 是否还在变化。
第二类是:请求偶尔能通,但 health check 很不稳定。
这时更该怀疑的是 health check signal 本身是不是被当前负载形态影响,例如长 prefill 或 busy scheduler,而不是直接怀疑 HTTP 路由。
第三类是:主进程没死,但子进程已经挂掉。
这时优先看 SubprocessWatchdog 和 engine / bootstrap 日志,而不是先去 API surface 查 schema。
把症状先分型,和前面第 8 节一再强调的维护层方法是一样的:先决定问题属于哪条链,再决定用哪类证据。
真正排查 liveness 问题时,更稳的顺序#
更稳的顺序通常是:
- 先看进程还在不在,区分“卡住”还是“已经死了”。
- 再看
SubprocessWatchdog是否已经报告子进程异常。 - 再看 tokenizer、scheduler、detokenizer 各自的 watchdog 是否仍在
feed()。 - 再看 health check 是卡在 HTTP 表面,还是卡在 runtime 真实推进。
- 最后再决定要不要深入 trace、dump 或更具体的执行层。
这种顺序的价值在于,它先验证“系统有没有继续前进”,再去细分“为什么没前进”。如果一开始就直接钻进 trace,很容易在还没确认 liveness 断裂层级时就把自己淹没在细节里。
最容易出现的三种误判#
第一,误以为 health check 就是 HTTP 200。
更准确地说,SGLang 试图让 health check 既不只是 HTTP 活着,也不完全等于真实请求全链路。
第二,误以为 watchdog 只是超时线程。
它更像一组分层的前进信号观察器,分别覆盖组件内部、跨进程和退出边界。
第三,误以为端口还在就说明系统没问题。
主进程存活、子进程存活、请求推进、health check 可响应,这其实是四种不同层次的“活着”。
小结#
这一章真正补齐的,是维护层里最底层、也最现实的一条链:health check 不是单纯 HTTP 200,watchdog 也不是单纯超时线程,两者一起才构成 SGLang 的 liveness 观察面。
把这层读清之后,扩展与调试部分就不只是在讲“怎么找问题”,也开始讲“系统怎样证明自己还活着、还在继续前进”。
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。