它没卡死,只是慢:一次深夜排障里三个反直觉的坑

它没卡死,只是慢:一次深夜排障里三个反直觉的坑

本文分享技术排查过程与纠错方法,不含商业推广。

凌晨一点多,我自托管的助手连不上了。

进程在,内存占着,端口 18789 没人监听。日志停在两行之间:已经打完 shutdown budget at startup,还没走到 [plugins] loading。

第一反应是”卡死了”。接下来几个小时证明:这个判断本身就是最大的坑。


坑一:把”慢”读成”死”——因为量错了进程

“卡死”总得有个证据。我抓到的证据看起来很硬:CPU 0.1%,日志 18–30 分钟不推进。

于是整个维修方向都建立在”它冻住了”上面。直到换了正确的采样对象再量一次:

采样对象 CPU 结论
我一开始量的那个进程 0.1% “冻住了”
真正在启动的网关主进程 58% 核在忙 它一直在干活,只是慢

前一个是什么?是数据库只读快照的辅助进程——它本来就该闲着。在启动链路里,它既不是瓶颈也不是信号。

同一个角色,两次表现完全不同(0.1% vs 58%),这本身就是结论:
该下的是”这条路径不可靠“,不是”它固定卡死在某处“。
——前者要求你换路径并量化,后者只会让你守着它等。

可迁移的动作:报”卡死”之前,先回答一句——我量的这个进程,是这条链路里的真身吗? 名字像、父子像,都不算;命令行和它在链路里的角色才算。


坑二:”急救”把病情加重了

进程不监听端口,最自然的动作是再拉一次。我们的看门狗就是这么干的:探活失败 → 重新触发计划任务。

它踩了两个坑,都写进了日志:

  1. 计划任务设了 MultipleInstancesPolicy=IgnoreNew,重复启动会被直接拒绝,返回 0x800710E0。这个码常被读成”端口被占”,其实是**”重复启动被拒”**。
  2. 就算真起了第二个网关,它会因为**”已有进程占用 state 目录”**而退出——并且把正在跑的启动时钟清零。

结果:启动迁移本来要跑 7–8 分钟,看门狗每 5 分钟催一次,每次都在它快跑完时把它拽回起点。

当”启动”本身是一个分钟级的慢过程时,探活阈值和重启策略必须一起设计。
阈值比启动时间短,监控就从”救火”变成了”纵火”。

修法(后来落进脚本):把最大等待提到 900 秒;检测到已有网关进程在启动就跳过启动、只进入等待循环;连续失败 3 次就暂停,别无限重试。

一句话判据:启动迁移跑着的时候,别重启它。


坑三:把”改了”当成”修好了”

“隐藏控制台缓冲区写满、把写日志的进程堵住”——这个假说听起来很合理。于是给启动脚本打了个补丁:换到真实工作目录,把输出重定向到文件而不是隐藏控制台。

补丁上线,脚本”跑起来了”,看起来好了一点。

但我们没有直接宣布修好,而是做了交换测试:杀掉当前实例,让计划任务用新脚本重新拉。

启动路径 结果
服务路径(计划任务 → 脚本)打补丁后 06:24:56 起,到 06:34:50 仍未绑端口
服务路径(补丁前) 同样停在两行之间
pty 路径(直接起进程) 两次都顺利绑上端口,健康检查 200

同一台机器、同一个版本、同一份数据库——换一条启动路径,结果就不同。

所以结论只能写成:补丁没有解决问题,假说被证伪。补丁留着(换工作目录 + 落盘日志本身无害,还方便取证),但不能算作修复。

“改了”和”修好了”之间,隔着一次交换测试。
没有对照的”好像好点了”,在深夜四点最容易被当成结论。


收束:两条可以搬走的判据

  1. 同一角色、两次不同表现 ⇒ 说”不可靠”,不说”固定故障”。
    “固定卡死”会把你钉在等待上;”不可靠”才会逼你换路径、做对照、量化差异。
  2. 数字必须带对象和时点。
    这一夜里,我们差点引用两个”铁证”数字——一个计数、一个日志行号。追问下去:计数是某次实测、某个时刻、某个数据库的读数,库后来被迁移过;行号只是一次检索输出的前缀,源文件早已轮转。同一个数字,在两次采样里可能是两回事。

最后那句”是不是卡死”,答案往往是:它没卡死,只是慢。 而”慢”这件事,只有量对对象、给够时间、做完对照,才看得清。



它没卡死,只是慢:一次深夜排障里三个反直觉的坑
https://genui.art/2026/10/07/2026-10-07-slow-is-not-hung/
作者
快乐
发布于
2026年10月7日
许可协议