TCP正常情况下关闭连接,接收方是可以感知到的。但是如果一方由于异常(如网络异常,主机崩溃等),FIN报文还没有发出,那么如果正常的一方没有发送报文,是无法感知到连接已经异常了。由于这种情况下感知不到对端的异常,故如果不做保活,会浪费服务端的资源,更重要的是可能会导致系统异常。
Linux keepalive机制
为此linux提供了keepalive选项,默认是关闭的。如果需要再客户端保活,可以把客户端的keepalive功能打开,这样客户端就可以感知到服务器的异常;如果需要再服务端保活,可以把服务端的打开,这样服务端就可以感知到客户端的异常;如果双方都需要保活,那么可以把双方的都打开,这样双方都可以发现对端的异常。可以通过如下的代码打开keepalive功能:
int keepAlive = 1; // 开启keepalive属性
int keepIdle = 60; // 如该连接在60秒内没有任何数据往来,则进行探测
int keepInterval = 5; // 探测时发包的时间间隔为5 秒
int keepCount = 3; // 探测尝试的次数.如果第1次探测包就收到响应了,则后2次的不再发.
setsockopt(rs, SOL_SOCKET, SO_KEEPALIVE, (void *)&keepAlive, sizeof(keepAlive));
setsockopt(rs, SOL_TCP, TCP_KEEPIDLE, (void*)&keepIdle, sizeof(keepIdle));
setsockopt(rs, SOL_TCP, TCP_KEEPINTVL, (void *)&keepInterval, sizeof(keepInterval));
setsockopt(rs, SOL_TCP, TCP_KEEPCNT, (void *)&keepCount, sizeof(keepCount));
linux也提供了全局的keepalive设置,其默认值如下:
cat /proc/sys/net/ipv4/tcp_keepalive_time
7200
cat /proc/sys/net/ipv4/tcp_keepalive_intvl
75
cat /proc/sys/net/ipv4/tcp_keepalive_probes
9
其中net.ipv4.tcp_keepalive_time表示在tcp_keepalive_time内如果连接没有任何报文往来,那么就会发送探测报文;
net.ipv4.tcp_keepalive_probes表示连续探测的次数,如果连续tcp_keepalive_intvl次都没有接收到对端的应答,那么说明是失效连接,linux会关闭这样的连接。但是如果对端有响应,那么结束本轮探测;
net.ipv4.tcp_keepalive_intvl表示侦探发包时间间隔。
考虑如下的场景:
1、如果对端程序崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡;
2、对端程序崩溃并重启。当 TCP 保活的探测报文发送给对端后,对端是可以响应的,但由于没有该连接的有效信息,会产生一个 RST 报文,这样很快就会发现 TCP 连接已经被重置;
3、对端程序没有崩溃且正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来;
4、对端程序没有崩溃但是出现死锁或者死循环等异常导致程序不能正常服务,但是因为程序还在,所以linux可以正常响应keepalive报文,故linux的keepalive会认为连接正常,从而导致正常的一方误认为对端还正常工作。
应用层保活
由此可见linux的keepalive不能识别第四种情况。故我们一般在应用层保活。
我们可以设计一个 PING-PONG 的机制,需要保活的一方,在保活时间达到后,发起对连接的 PING 操作,如果服务器端对 PING 操作有回应,则重新设置保活时间,否则对探测次数进行计数,如果最终探测次数达到了保活探测次数预先设置的值之后,则认为连接已经无效。注意如果没有收到对端的回复,需要重复几次,如果一次没有收到就认为连接已经异常了,可能会出现误判。
思考题:我们一般都需要再服务端做保活,那么在高并发的场景下,单机一般都需要支持成千上万个连接,那么在如此多的连接,我们又如何高效的保活呢?我会在后面的博文中给大家分享,欢迎持续关注。