● 时隔一年的项目刨坑解惑
缘由
因为最近工作量不是很大,所以开始对以前的项目进行优化升级,比如:「公司核心系统内存泄露排查」和一个本地备份工具(本地拖回服务器的备份进行再次备份),下图所示:
完成这些之后,又是一阵空虚。突然想起2021年春节后,公司做了一个xxx小车项目,当时遇到一个问题一直困扰着我。直到拿到公司一块RK3568开发板做测试,才解开这一年的疑惑(其实也应该怪自己抠门,贪便宜买了树莓派ZERO W的板子,没买带网口的树莓派4B)。
上述xxx小车项目的硬件设备使用移远EC200S-CN模块,在离线后会出现两种情况:
- 原先与服务器建立的TCP连接未断开的情况下,又与服务器建立了另一个TCP连接(IP相同端口不同,或者IP、端口都不相同);
- 使用原先建立的TCP连接继续通讯。
出现第1种情况的话,服务器会缓存很多未及时释放的TCP连接,为了节约TCP资源,我加了如下判断:如果设备未在300秒(5分钟)内发送心跳包,会自动断开与设备的连接
。但是设备离线后重现上线,压根不知道自己被断开了连接,继续使用原先的TCP通道发送数据,结果设备以为自己发了数据服务器却不返回。所以又改为了心跳超时判断阈值为900秒(15分钟)。
简单点说:当时的解决方案就是延长心跳时长,服务器晚点断开TCP连接,防止设备离线重新上线后找不到原先的TCP通道。
硬件测试
跟同事使用开发板,做了两个测试:
- 拔掉移动网络天线,甚至拔掉SIM卡,服务器未收到断开通知;
- 拔掉开发板供电电源,服务器未收到断开通知;
开发板如下图:
软件测试
使用TCP调试助手模拟开发板,做了三个测试:
- 调试助手关闭端口,模拟开发板主动断开TCP连接,毫无疑问服务器收到了断开通知;
- 调试助手不关闭端口,直接正常关闭软件,服务器收到了断开通知(这个测试不严谨,因为软件有可能会在关闭时做事件处理);
- 强制结束调试助手进程,服务器收到了断开通知。如下图所示:
软硬件模拟结果完全让人摸不着头脑。在未深入了解硬件及嵌入式程序源码的情况下,这事虽然通过修改心跳阈值解决,但是具体原因不明,只能搁置。
解惑准备
昨天我拿到了一块Android/Linux开发板RK3568,关闭了WIFI,插入了网线,实物图如下:
板子原载系统是Android 12,在公司挂了一夜百度网盘,刷入了Debian(没装Ubuntu的原因是因为镜像5G,而Debian只有3.5G,百度网盘拖下来能快点),如下图:
自带的python3的版本为3.7.2,足够用。服务端使用之前的项目去掉心跳包踢除限制,再写一个简单的tcpclient.py:
import socket
client = socket.socket()
client.bind(('', 1234))
client.connect(('192.168.0.2',8899))
while True:
msg = input(">>:").strip()
if len(msg) == 0 :continue
client.send(msg.encode())
data = client.recv(1024)
print("-> Server Response:",data.decode())
解惑开始
服务器IP:192.168.0.2,TCP Server端口:8899;
客户端IP:192.168.0.127,TCP Client端口:1234(随机端口号会建立新连接,可以用来模拟断电后上电)。
准备做3个测试:
- 断开网线后,开发板向服务端发送一条数据,观察多久断开连接;
- 断开网线后,开发板向服务端发送一条数据,过几秒后重新插入网线,观察是否会断开连接;
- 断开网线后,服务端和开发板不收发任何数据,观察多久断开连接;
1. 断开网线后,开发板向服务端发送一条数据,观察多久断开连接
在断开网线后,在服务器使用命令netstat -nap tcp | grep tcp4 | grep 192.168.0.127
查看,TCP连接还是处于ESTABLISHED
状态,说明断开网线,不会立即断开TCP连接。
大约15分钟,开发板TCP断开连接,如下图所示:
提示Network is unreachable
,网络无法到达。我搜了下:
stackoverflow的这个问题 聊到了TCP的重试机制:
关于超时重传次数,以下是Linux文档中的描述:
tcp_retries1:
在连接建立过程中(未激活的sock),除了上面的情况以外,内核要重试多少次后才决定放弃连接。
tcp_retries2:
在通讯过程中(已激活的sock),数据包发送失败后,内核要重试发送多少次后才决定放弃连接。
显然,我遇到的问题是tcp_retries2
的情况,我查了下系统默认的重试次数:
tcp_retries2 的默认值是 15,这个重试次数的耗时大约是13~30分钟,这只是一个大概值,最终耗时时间还要取决于RTO(retransmission timeout,重传超时时间)。
收获及结论:默认情况(未修改tcp_retries2)下,linux嵌入式设备与服务器心跳包的最大超时阈值应控制在10分钟左右。单片机另谈。
2. 断开网线后,开发板向服务端发送一条数据,过几秒后重新插入网线,观察是否会断开连接
在经历了第1个测试后,这个测试可以得出很肯定的答案,开发板会在一段间隔后,使用原tcp通道继续重传发送数据。超时时长间隔如下所示:
3. 断开网线后,服务端和开发板不收发任何数据,观察多久断开连接
没有做实验,继续搜索相关资料,在stackoverflow找到两篇文章:「When is a TCP connection considered idle?」和「timeout in a TCP connection with no exchange of data」
这两篇文章的矛头都指向了net.ipv4.tcp_keepalive_time
:
在开发板运行sysctl -a | grep net.ipv4 | grep keepalive
,可以看到keepalive参数配置:
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_time = 7200
# tcp_keepalive_time=7200:表示保活时间是7200秒(2小时),也就2小时内如果没有任何连接相关的活动,则会启动保活机制;
# tcp_keepalive_intvl=75:表示每次检测间隔75秒;
# tcp_keepalive_probes=9:表示检测9次无响应,认为对方是不可达的,从而中断本次的连接。
根据上面的参数可以算出一个TCP连接最大空闲时间 = 7200+75*9=7875秒。
收获及结论:默认情况(上图配置)下,linux嵌入式设备与服务器停止收发数据后,会在大约2小时候断开连接。如果大量「设备」(这里的「设备」可能是真实设备,也有可能是黑客伪装的连接)只连接不收发数据,会给服务器造成严重负担,所以要及时踢掉僵尸设备。单片机另谈。
测试总结
默认配置情况下,linux嵌入式设备断开网线后,如果没有任何数据传输,2小时内TCP状态并不会改变,会一直维持连接状态;如果有数据传输,应按最大13分钟断开连接处理。
最后的查根溯源
最后回到单片机硬件设备上,既然是设备离线,那就是跟keepalive有关系,继续搜索资料:
找到了Quectel论坛的一份PDF文档,里面有关于设置keepalive时长的指令:
这样服务端和单片机设备都可以设置keepalive时长。如果设置相同时长,不仅可以避免长连接浪费,也可以一定程度筛选异常连接(比如检测黑客伪造TCP连接进行DDOS攻击)。
至此,一年以来被困扰的疑问已经解决。
参考资料:
「LTE Standard TCP/IP Application Note - Quectel Forums」
「TCP-聊一聊重传次数」