那天快到晚上十点,手机响了。客户在电话里说泵站的数据大半天没动过,问是不是设备坏了。
我打开后台。设备列表里那台的灯是绿的,状态写着"在线"。
问题就出在这儿——它在线,但数据不动。
第一反应是重启,然后就没下文了
现场没人,只能远程让客户把控制柜里的 4G 路由器断电重启。等了三四分钟,数据回来了。客户挺高兴,我也以为这事就这么过去了。
第二天上午,同样的事又来了一遍。
连接还在,只是没数据
我登上服务器,查了一下我们用的那个端口(8899),连接状态是 ESTABLISHED。从服务器的角度看,这条 TCP 连接好端端地在那儿。
但翻日志,最后一次收到这台设备的数据,是前一天下午。
这就是半开连接。设备那一头早就断了——4G 信号闪断、路由器重启、或者运营商把空闲连接回收了——断的时候没来得及发 FIN 包,服务器这边就一直以为对方还在。这条连接能挂几个小时,运气好的话挂几天也没人管它。
TCP 不会主动去问一句"你还在吗"。你不发数据,它就一直等着。
心跳不是设了就完事
其实我们一开始就写了心跳,设备每 5 分钟给服务器发一次包。按理说断了应该很快能发现。
问题出在中间那层。设备的 4G 链路要过运营商的 NAT,NAT 网关维护着一张地址转换表,每一条连接对应一个表项,而空闲表项是会被回收的。各家超时时间不一样,我后来查过我们这批卡,大概 4 分钟左右。
心跳 5 分钟一次,NAT 4 分钟回收——心跳还没来得及发,链路先被掐了。
而且这个回收是静默的:设备不知道,服务器也不知道。下一次心跳发出去,石沉大海。设备这边调用 send 甚至可能是成功的,数据只是进了缓冲区,要等到下一次写入或者超时,它才反应过来不对劲。
我把心跳间隔从 300 秒改成了 60 秒。这个数字没什么理论依据,就是比 NAT 超时短一大截,又不至于太费流量——一个心跳包几十字节,一小时 60 个,一个月下来也就几兆。
改完那几天掉线确实少了,但没彻底干净。
设备的时间是错的
这个坑比较隐蔽。我们有一批门锁 DTU(RN102),服务器偶尔会报"心跳时间戳陈旧":设备明明在发心跳,服务器拿到的时间戳却是好几个小时前的。
一开始我以为是网络延迟。后来看设备日志才发现,那台设备重启过,重启之后时钟回到了模组的默认时间,NTP 又没同步成功。它自己以为发的是"现在",服务器收到的是一个过去的时间。
我们判断在线的逻辑是拿这个时间戳跟当前时间比,一比就觉得数据不对,状态跟着乱。
最后改了两处:设备上电先做一次 NTP 同步,失败就重试几次;服务器那边改成以收到心跳的服务器时间为准,不再信设备上报的时间戳。
说白了就是别信设备端的时间。这条原则我后来用在了所有地方。
重连也别太勤快
还有一个问题是压力大的时候才暴露的。有次基站侧闪断了一两分钟,一批设备同时掉线,都按"断了就重连"的逻辑跑,结果几秒钟内大量设备一起往服务器挤,连接数直接顶满,本来能连上的也被堵在外面。
后来加了退避:第一次重连等 1 秒,失败等 2 秒,再失败 4 秒、8 秒,到 30 秒封顶,往后都按 30 秒来。再随机加一点抖动,免得所有设备整整齐齐一起重试。
改动很小,效果挺明显。
还差的一件事
上面这些改完之后,这套已经稳定跑了几个月。偶尔还有单台设备掉线,但基本能在几分钟内自己恢复。
还剩一件事我一直没做:指令的应用层确认。现在服务器下发指令,只要 TCP 写成功就算发出去了,设备到底执行没执行,其实没有回执。对开关这类操作来说,我觉得还是该让设备回一个执行结果。已经排进计划了,等做完再写一篇。



评论一下?