MQTT Last Will 为什么能发现设备异常离线?
MQTT Last Will(遗嘱消息,LWT)是在客户端连接 Broker 时预先登记的一条消息。当客户端异常断开,比如断电、进程崩溃、网络中断或 Keep Alive 超时,Broker 会把这条消息发布到指定 Topic。它最常见的用途是设备在线状态:设备正常工作时发布 online,异常离线时由 Broker 代发 offline。它解决的是“设备突然没了,其他系统怎么知道”的问题,但不能告诉你离线原因,也不能替代完整的监控系统。在物联网项目里,LWT 更像一个兜底信号:它能尽快告诉你连接不可靠了,但后续仍要靠日志、最后上报时间和设备自检来定位原因。
它什么时候会触发?
Will 消息在 CONNECT 阶段设置,包含 Will Topic、Payload、QoS 和 Retain。客户端正常发送 DISCONNECT 时不会触发,因为 Broker 认为这是有计划的离线。真正触发的是非正常断开:TCP 连接断掉、心跳超时、客户端进程被杀、设备断电等。这里有个边界:如果连接根本没建立成功,Broker 没收到完整 CONNECT,自然也没有遗嘱可发。另一个边界是触发时间不一定等于真实掉线时间,Broker 往往要等 Keep Alive 超时后才确认连接失效。
可以用 mosquitto 做一个最小实验:
bashmosquitto_sub -h localhost -t 'device/123/status' -v mosquitto_pub -h localhost -i dev123 --will-topic device/123/status --will-payload '{"status":"offline"}' --will-retain -l
第二个命令保持标准输入不退出,直接强杀进程或断网,订阅端会看到 offline。如果你按正常方式退出并发送 DISCONNECT,就不会触发遗嘱。测试时不要只关闭终端标签页,有些客户端会优雅断开,最好用 kill -9 或断开网络来模拟异常。真实设备还要测试休眠、基站切换、路由器重启这些场景,因为它们比本地进程崩溃更接近生产问题。如果 Broker 是集群,还要确认会话迁移和节点故障时 Will 的行为,避免某个节点重启就把一批设备误判为离线。
追问
Last Will 和 retained message 是什么关系?
Last Will 决定“异常断开时发什么”,retained message 决定“这条消息要不要被 Broker 保存给后来订阅者”。两者可以单独使用,也经常组合使用。设备上线时主动向 device/123/status 发布 retained 的 online,连接时设置 retained 的 Will 为 offline,这样新订阅者随时都能看到最新在线状态。取舍是旧状态会被保存,所以设备退役、迁移 Topic 或更换设备 ID 时必须清理 retained 消息。
Keep Alive 应该设置多长?
Keep Alive 越短,Broker 越快发现设备异常,遗嘱消息越及时;但心跳更频繁,弱网和电池设备会付出额外成本。工业网关或后台服务可以设 30-60 秒,低功耗设备可能设几分钟。不要为了“秒级离线”把 Keep Alive 调得极小,网络抖动会制造误报。更稳的做法是结合业务容忍度、网络质量和告警成本来定,并在应用层记录最后一次业务上报时间。比如状态页可以 1 分钟内显示疑似离线,告警系统再等两个心跳周期确认,减少无意义的电话和短信轰炸。
Will QoS 和 Retain 怎么选?
状态看板通常选择 QoS 1 加 retain,因为离线状态值得至少送达一次,并且后来打开页面的人也需要看到。普通日志或临时告警可以不 retain,避免新订阅者反复看到旧告警。QoS 2 很少必要,它会增加握手成本,且多数离线状态处理本身就应该幂等。真正关键的是消费端按设备 ID 去重,别因为重复投递把同一台设备告警多次。
代码里怎么设置遗嘱消息?
Python paho-mqtt 必须在 connect() 之前调用 will_set(),这是很多人第一次用时会踩的坑。连接成功后再设置不会影响当前连接,只能等下次连接才生效。上线状态建议在连接成功后立刻发布,最好也 retain,避免还没连上就误以为设备在线。示例:
pythonclient = mqtt.Client(client_id='dev123') client.will_set('device/123/status', '{"status":"offline"}', qos=1, retain=True) client.connect('broker.example.com', 1883, keepalive=60) client.publish('device/123/status', '{"status":"online"}', qos=1, retain=True)
实际项目里最容易误判什么?
第一是把网络抖动当成设备故障,尤其移动网络和 Wi-Fi 漫游场景很常见。第二是只依赖 LWT 判断离线,却没有应用层最后上报时间,结果 Broker 故障、集群切换或会话迁移时不好排查。第三是多客户端共用同一个 Client ID,后上线的连接会踢掉前一个连接,可能触发前一个连接的遗嘱。生产环境里要保证 Client ID 唯一,并把 offline 消息设计成幂等事件;正常维护、主动关机这类离线则应由客户端主动发布状态再 DISCONNECT,异常离线才交给 Will。如果业务需要区分原因,可以把 payload 设计成 status、reason、ts 三个字段,但不要指望 Will 自动知道真实原因。