从一份运行日志还原一次生产卡死:三个信号与一场"假卡死"
标签:日志分析、结构化日志、死循环、忙等、有限状态机
引言:设备不会说话,但它每时每刻都在往日志里写自己的呼吸。一份运行日志,就是设备写给工程师的病历。今天这台工业检测设备出了件怪事:下午三点半前后,它连续三次"卡死"------检测进度归零、CPU 飙满、整条产线走走停停,最后只能靠重启收场。这篇文章,就顺着日志里留下的三组信号,把这场卡死一步步还原出来,顺便讲清几个读日志、判性能的核心概念。
一、日志里的"括号":结构化 span 与配对
这份日志有一个非常友好的特征:每一段操作,都用一个"开始"标记和一个"结束"标记包起来,像一对括号。比如一次算法检测,会先打一行"【开始】算法检测",做完再打一行"【结束】算法检测"。
这一对括号,就是结构化日志 里的"跨度"(span)------它圈定了一个操作的起止边界。它最有价值的用法,不是"看它有没有执行",而是配对 :把所有"开始"和"结束"按操作编号配对,卡住的地方,就是那一段"只有开始、没有结束"的地方。
道理很简单:程序不会说"我卡住了",它只会停止打日志。于是"最后一行日志停在哪",就成了定位卡点的最硬线索。一个操作开了头却没结尾,意味着它进去了、再也没出来。
用这一招,一次就能锁定:全天只有三段"算法检测"没有闭合,而且它们全部指向同一个检测元件。三次卡死,同一个元凶,一目了然。
二、心跳:主从轮询的一问一答
在展开卡死之前,先看一眼日志的背景音------一份从凌晨就开始、永不停歇的问答。
日志最开头,是密密麻麻、零点几秒一次的往返:上位机发一句查询,下位机回一句应答。这就是工业设备最经典的主从轮询协议:主节点掌握主动权,周期性地发问;从节点只答、不主动开口。
细看这些查询,各有分工:一类问"你现在什么状态"(应答里一个数字代表一个状态码,比如"待机中""照射中""无法照射");一类问"你有没有故障、有没有联锁"(几个数字分别代表不同故障位,全零是健康,某位变一就是那一路出问题);一类问"你的目标电压电流是多少"(用于核对设定值是否下发成功)。
这三类问询合起来,就是一套完整的心跳 + 健康检查。它就像设备的脉搏------只要这些一问一答还在继续,就说明主从之间的通信链路还活着。这也是后面判断"是整机死了还是局部卡死"的重要参照。
三、信号一:一个"只有开始没有结束"的 span
下午三点多,日志的平静被打破。三点二十五分左右,一段"算法检测"打出了"开始",却再也没有等来"结束"。
接下来的变化,被一份逐秒采样 的性能数据记录得清清楚楚:挂死前,CPU 占用稳定在百分之五百左右(这是一台 24 核机器,多核等效值);挂死后短短六七秒,CPU 一路冲到 2400%------24 个核全部打满,然后不再回落。
更关键的是,这"六七秒后冲满"的延迟,三次卡死完全一致 。第一次、第二次、第三次,都是挂死后约六七秒,CPU 从约 500% 飙到 2400%。一次是巧合,三次一致就是确定性行为------这不是偶发的抖动,而是某个确定的逻辑在作祟。
而且,这三次卡死,全都靠"重启程序"才结束,没有一次是它自己恢复的。重启之后,CPU 立刻回落到正常区间,之后的几块板子检测全部正常。到这里,一个清晰的因果已经浮现:某次检测挂死 → CPU 被打满 → 只能重启。
四、信号二:CPU 与 GPU 的反差,区分"死循环"和"等锁"
CPU 飙满,还不足以定性。真正一锤定音的,是另一组反差数据。
同一时刻,GPU 的占用只有 3%------而正常做检测时,GPU 应该在 65% 到 85% 之间。一个做三维检测的算法,本该重度依赖 GPU 做矩阵运算,结果 GPU 几乎空闲、CPU 却全核打满。这意味着什么?
CPU 在空转。 这不是"算得慢",而是死循环------某个循环条件永远为真,代码在原地打转,疯狂消耗 CPU 时间片,却没有任何实质计算(所以 GPU 一动不动)。
这里就引出一个非常实用的判据:区分"死循环"和"等锁"。两者都是"卡住",但特征截然相反:
- 死循环 :CPU 100%、GPU 几乎为 0------它在忙,忙着一遍遍空转;
- 等锁 :CPU 几乎为 0------它在等,线程挂在等待上,几乎不占 CPU。
用生活类比:死循环是"一个人急得原地团团转",等锁是"一个人安安静静排在队伍里"。日志里还能看到对应的证据------挂死之后,后续的作业"一次算法检测都没开始",正是它们全在排队等那把被占死的锁。
五、信号三:同一秒重复打印 30 次,暴露的"忙等"
除了这次主事故,日志还顺手暴露了另一个藏在暗处的 bug------一个"忙等"(busy-wait)。
正常代码里,一个需要反复检查某个条件、又暂时没结果的循环,应该在每轮检查之间"让出 CPU"歇一下(比如延迟几十毫秒),让别的任务跑。但有一段上下料握手循环,把这个"歇一下"只写在了少数几个分支里------只要走到循环末尾的那条路径上没经过任何等待,整个循环就变成忙等:不做任何让步地狂转,CPU 占满一个核。
日志给出的铁证是:同一秒内,同一句"已写启动信号"被重复打印了 30 次。一句正常应该几秒才出现一次的话,在一秒里刷了 30 遍,就是典型的自旋特征------循环飞快空转,每次都走到打日志那一行。
忙等的危害不只是白烧 CPU:它还会让本该节制的硬件读写被打到飞起。正确的写法是协作式让步------每轮循环末尾显式地让出一次 CPU(哪怕只延迟二三十毫秒),让"等"这件事变成"每隔一小段才醒来看一眼",而不是"睁着眼死盯"。
六、把三个信号拼回一条因果链
把日志里的三组信号拼起来,一次完整的生产卡死就还原出来了:
- 某个检测元件触发了算法死循环------"算法检测"的 span 开了头不结尾,CPU 六七秒后打满 24 核,GPU 掉到 3%;
- 死循环持有了进程级唯一的检测锁------后续所有作业连"开始"都打不出来,在等锁上静默排队,检测进度归零;
- 全程不自愈,只能重启------三次挂死,三次重启,没有一次自己恢复;重启后 CPU 立刻回落,产线恢复正常。
而那个"同一秒刷 30 次"的忙等循环,则是顺带发现的另一个独立隐患------它不是这次卡死的主因,但同样是需要修掉的"烧核"问题。
七、总结
这篇文章顺着日志,用三个信号还原了一次工业设备的生产卡死,也顺带讲清了几个读日志、判性能的概念:
- 结构化 span:把操作用"开始/结束"包成括号,配对找"只有开始没有结束"的那段,是定位卡点的最快路径。
- 主从轮询:心跳式的问答(状态、故障、目标值三类查询),是判断"通信还活没活"的脉搏。
- 死循环 vs 等锁:CPU 100% + GPU 接近 0 是死循环(空转);CPU 接近 0 是等锁。两者特征截然相反,不可混为一谈。
- CPU 聚合口径:多核机器上,"2400%"这种数要除以核数理解------24 核打满,就是 2400%。
- 忙等:同一秒重复打印同一句几十次,就是自旋;循环里要显式让出 CPU,把"死盯"变成"每隔一小段醒来看一眼"。
一句话收尾:设备从不撒谎,它只是用日志说话。读懂"括号"、读懂 CPU 与 GPU 的反差、读懂那一秒 30 次的重复------你就能在它彻底停摆之前,找到病根。