现场设备明明已经断电,后台监控大屏上那个绿点却还亮着。等离线告警终于弹出来,可能已经过去二十分钟,处理时机早就耽误了。
排查一圈下来,根因往往只有一个:后台显示的“在线”,从来不是设备的实时状态,而是一次有延迟的判定结果。把这个判定拆开看,问题就清楚了。
一、从掉电到“翻红”,中间隔着五个环节
一台设备从掉电到后台状态变红,要依次走过五个环节,任何一环卡住,离线判定都会变慢:
-
保活:客户端按周期发心跳,证明“我还活着”;
-
判死:Broker等超时窗口结束,才断开连接;
-
遗嘱:Broker代发LWT离线通知;
-
消费:后端订阅并解析遗嘱消息;
-
落地:写入数据库快照,前端读的其实是这份快照。
所以前端那个绿点,读取的是最后一环写入的值,而不是现场连接的真实状态。排查必须顺着这条链走。
二、急断电:连“再见”都来不及说
设备突然掉电、网线被拔、Wi-Fi瞬断,都发不出TCP的FIN或RST报文。此时Broker端的socket还维持着连接,完全不知道对面已经挂了——这就是半开连接。
有人会问,TCP不是自带keepalive吗?两个硬伤让它靠不住:默认不开启;即使开启,Linux默认7200秒才探一次。所以MQTT把保活放在了应用层:客户端在保活周期内至少发一个PINGREQ心跳,Broker回PINGRESP,两个报文固定头只有2字节,开销极小。
但低功耗设备为了省电,常把心跳拉得很长,NB-IoT场景十几分钟甚至半小时都很常见,搞不好直接“睡死”,连心跳都停了。
三、1.5倍窗口:设备能“诈尸”多久
心跳停了,Broker也不会立刻判死。MQTT 3.1.1规定:保活值不为0时,Broker在1.5倍保活周期内没收到任何报文,才认定连接失效。这个1.5是给网络抖动留的余量。
由此可以算出“诈尸”窗口:
| 60秒 | 90秒 | 1分半 |
| 5分钟 | 450秒 | 7分半 |
| 30分钟 | 2700秒 | 45分钟 |
| 0(关闭保活) | 永不判死 | 可能一直亮绿灯 |
关键结论:告警阈值一定要大于1.5倍保活周期。卡在1.5倍以内会造成误报。不同Broker还有自己的宽限系数,实际等待常比1.5倍略长,建议把阈值设成2倍保活周期。
四、keepalive=0:是关掉判活,不是“不用心跳”
按MQTT规范,保活值传0等于关闭超时判活,Broker不会因客户端不活跃而断开,遗嘱也就永远不会触发。有些MQTT库默认值或初始化漏传参数,会悄悄把保活设成0,而配置页上写的却是60秒。
确认实际生效值最直接的办法是抓包看心跳有没有发出来。注意一个坑:PINGREQ报文只有C0 00两个字节,里面根本不含“pingreq”这几个字母,按文本grep一条都捞不到。也别急着下结论——保活周期内发出任意MQTT控制报文都算活着,设备一直在上报数据时压根不会发心跳,要先看有没有持续的PUBLISH。
五、遗嘱发了没人接,等于白发
Broker老实发了遗嘱,但没人订阅、或主题对不上,状态照样不会更新。消费侧要保证接收、解析、翻状态、写快照整个流程走完,建议用通配符订阅全部设备,而不是逐个订阅。
还有两个易错点:主动下线要自己更新状态(正常DISCONNECT不会触发遗嘱);被撞号也算异常(同一ClientId在别处重连,Broker踢掉旧连接时会触发遗嘱),设备状态反复横跳时先查有没有重复ClientId。
若使用MQTT 5.0,要注意Will Delay Interval会让总延迟变成“1.5倍保活+遗嘱延迟”,而reason code 0x04主动下线也会发遗嘱。
六、三步排查顺序
遇到“假在线”,建议按下面的顺序排查,避免一上来就怀疑平台:
先看心跳发没发:抓包或去Broker控制台看实际生效的保活值。心跳和业务报文一条都没有,先回头修保活,后面的环节都不用查。
再算1.5倍窗口够不够:保活周期乘1.5,和能接受的告警时延对比。算出来比要求的时间长,就缩短心跳周期——省电和低延迟不可兼得。
最后确认遗嘱有没有被消费:主题逐字符对一遍,再看后端订阅列表里有没有对应主题。
三步走完,监控大屏上那个绿点就不再玄学了——它是一段可以算出来、也可以调短的延迟。

