Linux 内核参数之 keepalive
简介
TCP协议中有长连接和短连接之分。短连接环境下,数据交互完毕后,主动释放连接;
长连接环境下,数据交互完毕后,并不会释放连接;当连接的双方在连接空闲状态时,如果任意一方意外崩溃、当机、网线断开或路由器故障,另一方无法得知TCP连接已经失效。
那么,连接的另一方并不知道对端的情况,它会一直维护这个连接。而作为“服务端”来说,长时间的积累会导致非常多的半打开连接,造成端系统资源的消耗和浪费,且有可能导致在一个无效的数据链路层面发送业务数据,结果就是发送失败。
所以各端要做到快速感知失败,减少无效链接操作,这就有了 TCP 的KeepAlive保活探测机制。
keepalive 的作用
探测连接的对端是否存活
在应用交互的过程中,可能存在以下几种情况:
- 客户端或服务端意外断电,死机,崩溃,重启。
- 中间网络已经中断,而客户端与服务器并不知道。
利用保活探测功能,可以探知这种对端的意外情况,从而保证在意外发生时,可以释放半打开的TCP连接。
防止中间设备因超时删除连接相关的连接表
中间设备如防火墙等,会为经过它的数据报文建立相关的连接信息表,并未其设置一个超时时间的定时器,如果超出预定时间,某连接无任何报文交互的话,中间设备会将该连接信息从表中删除,在删除后,再有应用报文过来时,中间设备将丢弃该报文,从而导致应用出现异常。
这种情况在有防火墙的应用环境下非常常见,这会给某些长时间无数据交互但是又要长时间维持连接的应用(如数据库)带来很大的影响,为了解决这个问题,应用本身或TCP可以通过保活报文来维持中间设备中该连接的信息,(也可以在中间设备上开启长连接属性或调高连接表的释放时间来解决。
常见应用故障场景:
某财务应用,在客户端需要填写大量的表单数据,在客户端与服务器端建立TCP连接后,客户端终端使用者将花费几分钟甚至几十分钟填写表单相关信息,终端使用者终于填好表单所需信息后,点击“提交”按钮,结果,这个时候由于中间设备早已经将这个TCP连接从连接表中删除了,其将直接丢弃这个报文或者给客户端发送RST报文,应用故障产生,这将导致客户端终端使用者所有的工作将需要重新来过,给使用者带来极大的不便和损失。
keepalive 可能带来的问题
- 中间设备因大量保活连接,导致其连接表满,网关设备由于保活问题,导致其连接表满,无法新建连接(XX局网闸故障案例)或性能下降严重
- 正常连接被释放:当连接一端在发送保活探测报文时,中间网络正好由于各种异常(如链路中断、中间设备重启等)而无法将保活探测报文正确转发至对端时,可能会导致探测的一方释放本来正常的连接,但是这种可能情况发生的概率较小,另外,一般也可以增加保活探测报文发生的次数来减少这种情况发生的概率和影响。
技术原理
当一个 TCP 连接建立之后,启用 TCP Keepalive 的一端便会启动一个定时器,当有数据传输时,这个定时器就会置为 tcp_keep-alive_time,这个计时器数值到达 0 之后(也就是经过tcp_keep-alive_time时间后,这个参数之后会讲到),一个 TCP 探测包便会被发出。这个 TCP 探测包是一个纯 ACK 包(RFC1122#TCP Keep-Alives规范建议:不应该包含任何数据,但也可以包含1个无意义的字节,比如0x0),其 Seq号 与上一个包是重复的,所以其实探测保活报文不在窗口控制范围内。
而接收探测包的客户主机肯定处于下列4个状态之一。
客户主机依然正常运行,并从服务器可达。客户的TCP响应正常,而服务器也知道对方是正常的,服务器在两小时后将保活定时器复位。
客户主机已经崩溃,并且关闭或者正在重新启动。在任何一种情况下,客户的TCP都没有响应。服务端将不能收到对探测的响应,并在
keepalive_intvl
秒后超时。服务器总共发送keepalive_probes
个这样的探测 ,每个间隔keepalive_intvl
秒。如果服务器没有收到一个响应,它就认为客户主机已经关闭并终止连接。客户主机崩溃并已经重新启动。服务器将收到一个对其保活探测的响应,这个响应是一个复位,使得服务器终止这个连接。
客户机正常运行,但是服务器不可达,这种情况与2类似,TCP能发现的就是没有收到探测的响应。
注意点
- 不太好的TCP堆栈实现,可能会要求保活报文必须携带有1个字节的数据负载
- TCP Keepalive应该在服务器端启用,客户端不做任何改动;若单独在客户端启用,若客户端异常崩溃或出现连接故障,存在服务器无限期的为已打开的但已失效的文件描述符消耗资源的严重问题。
- 但在特殊的NFS文件系统环境下,需要客户端和服务器端都要启用Tcp Keepalive机制。
- TCP Keepalive不是TCP规范的一部分,有三点需要注意:
- 在短暂的故障期间,它们可能引起一个良好连接(good connection)被释放(dropped)
- 它们消费了不必要的宽带
- 在以数据包计费的互联网消费(额外)花费金钱
keepalive 参数
无论 windows,还是 linux,keepalive 包括三个参数:
keepalive_probes
: 探测次数keepalive_time
: 探测的超时keepalive_intvl
: 探测间隔
对 于一个已经建立的tcp连接。如果在 keepalive_time
时间内双方没有任何的数据包传输,则开启keepalive功能的一端将发送 keepalive
数据包,若没有收到应答,则每隔 keepalive_intvl
时间再发送该数据包,发送 keepalive_probes
次。一直没有收到应答,则发送rst包关闭连接。若收到应答,则将计时器清零。
但是如果没有设置 keep alive
,可能你在你的socket(阻塞性)的上面。
接收recv会一直阻塞不能返回,除非对端主动关闭连接,因为recv不知道socket断了。
发送:取决于数据量的大小,只要底层协议站的buffer能放下你的发送数据,应用程序级别的send就会一直成功返回。 直到buffer满,甚至buffer满了还要阻塞一段时间试图等待buffer空闲。所以你对send的返回值的检查根本检测不到失败。开启了keep alive功能,你直接通过发送接收的函数返回值就可以知道网络是否异常。设置的方法(应用层):
TCP keepalive bug
tcp 的 keepalive 有这样的一个bug:
正常情况下,连接的另一端主动调用colse关闭连接,tcp会通知,我们知道了该连接已经关闭。
但是如果tcp连接的另一端突然掉线,或者重启断电,这个时候我们并不知道网络已经关闭。
而此时,如果有发送数据失败,tcp会自动进行重传。
重传包的优先级高于keepalive,那就意味着,我们的keepalive总是不能发送出去。 而此时,我们也并不知道该连接已经出错而中断。在较长时间的重传失败之后,我们才会知道。
为了避免这种情况发生,我们要在tcp上层,自行控制。
对于此消息,记录发送时间和收到回应的时间。如果长时间没有回应,就可能是网络中断。如果长时间没有发送,就是说,长时间没有进行通信,可以自行发一个包,用于keepalive,以保持该连接的存在。
在一端的socket上开启keep alive,然后阻塞在一个recv或者不停的send,这个时候拔了网线,测试从拔掉网线到recv/send返回失败的时间。
在linux kernel里头的测试发现,对于阻塞型的socket,当recv的时候,如果没有设置keep alive,即使网线拔掉或者ifdown,recv很长时间不会返回,最长达17分钟,虽然这个时间比linux的默认超时时间()短了很多。
但是如果 设置了keep alive,基本都在keepalive_time +keepalive_probes*keepalive_intvl =33秒内返回错误。
但是对于循环不停send的socket,当拔掉网线后,会持续一段时间send返 回成功(0~10秒左右,取决 于发送数据的量),然后send阻塞,因为协议层的buffer满了,在等待buffer空闲,大概90秒左右后才会返回错误。由此看来,send的时 候,keep alive似乎没有起到作用,这个原因至今也不清楚。后来通过给send之前设置timer来解决的。
配置
将以下配置写入到/etc/sysctl.conf文件:
1 |
|
保存退出,然后执行sysctl -p
生效
可通过 sysctl -a | grep keepalive
命令检测一下是否已经生效。
针对已经设置SO_KEEPALIVE的套接字,应用程序不用重启,内核直接生效。
或者直接修改内核参数,这个方式属于临时修改,重启后失效
1 |
|