TCP-Keepalive机制刨根问底

44次阅读

共计 5174 个字符,预计需要花费 13 分钟才能阅读完成。

TCP Keepalive 机制刨根问底!

Tcp Keepalive 的起源

TCP 协议中有长连接和短连接之分。短连接环境下,数据交互完毕后,主动释放连接;

双方建立交互的连接,但是并不是一直存在数据交互,有些连接会在数据交互完毕后,主动释放连接,而有些不会,那么在长时间无数据交互的时间段内,交互双方都有可能出现掉电、死机、异常重启,还是中间路由网络无故断开、NAT 超时等各种意外。

当这些意外发生之后,这些 TCP 连接并未来得及正常释放,那么,连接的另一方并不知道对端的情况,它会一直维护这个连接,长时间的积累会导致非常多的半打开连接,造成端系统资源的消耗和浪费,为了解决这个问题,在传输层可以利用 TCP 的保活报文来实现,这就有了 TCP 的 Keepalive(保活探测)机制。

Tcp Keepalive 存在的作用

探测连接的对端是否存活

 在应用交互的过程中,可能存在以下几种情况:

  1. 客户端或服务端意外断电,死机,崩溃,重启。
  2. 中间网络已经中断,而客户端与服务器并不知道。

           
利用保活探测功能,可以探知这种对端的意外情况,从而保证在意外发生时,可以释放半打开的 TCP、
连接。

防止中间设备因超时删除连接相关的连接表

中间设备如防火墙等,会为经过它的数据报文建立相关的连接信息表,并未其设置一个超时时间的定时器,如果超出预定时间,某连接无任何报文交互的话,中间设备会将该连接信息从表中删除,在删除后,再有应用报文过来时,中间设备将丢弃该报文,从而导致应用出现异常,这个交互的过程大致如下图所示:

这种情况在有防火墙的应用环境下非常常见 ,这会给某些长时间无数据交互但是又要长时间维持连接的应用(如数据库) 带来很大的影响,为了解决这个问题,应用本身或 TCP 可以通过保活报文来维持中间设备中该连接的信息,(也可以在中间设备上开启长连接属性或调高连接表的释放时间来解决。

常见应用故障场景:

某财务应用,在客户端需要填写大量的表单数据,在客户端与服务器端建立 TCP 连接后,客户端终端使用者将花费几分钟甚至几十分钟填写表单相关信息,终端使用者终于填好表单所需信息后,点击“提交”按钮,结果,这个时候由于中间设备早已经将这个 TCP 连接从连接表中删除了,其将直接丢弃这个报文或者给客户端发送 RST 报文,应用故障产生,这将导致客户端终端使用者所有的工作将需要重新来过,给使用者带来极大的不便和损失。

TCP 保活报文交互过程

TCP 保活的交互过程大致如下图所示:
                 

TCP 保活可能带来的问题:

  1. 中间设备因大量保活连接,导致其连接表满,网关设备由于保活问题,导致其连接表满,无法新建连接 (XX 局网闸故障案例) 或性能下降严重
  2. 正常连接被释放

      当连接一端在发送保活探测报文时,中间网络正好由于各种异常 (如链路中断、中间设备重启等) 而无法将保活探测报文正确转发至对端时,可能会导致探测的一方释放本来正常的连接,但是这种可能情况发生的概率较小,
另外,一般也可以增加保活探测报文发生的次数来减少这种情况发生的概率和影响。

TCP Keepalive 协议解读

下面协议解读,基于 RFC1122#TCP Keep-Alives(注意这是协议的解读站在协议的角度)

  1. TCP Keepalive 虽不是标准规范,但操作系统一旦实现,默认情况下须为关闭,可以被上层应用开启和关闭
  2. TCP Keepalive 必须在 没有任何数据(包括 ACK 包)接收之后的周期内才会被发送,允许配置,默认值不能够小于 2 个小时
  3. 不包含数据的 ACK 段在被 TCP 发送时没有可靠性保证,意即一旦发送,不确保一定发送成功。系统实现不能对任何特定探针包作死连接对待
  4. 规范建议 keepalive 保活包不应该包含数据,但也可以包含 1 个无意义的字节,比如 0x0。
  5. SEG.SEQ = SND.NXT-1,即 TCP 保活探测报文序列号将前一个 TCP 报文序列号减 1。SND.NXT = RCV.NXT,即下一次发送正常报文序号等于 ACK 序列号;总之保活报文不在窗口控制范围内 有一张图,可以很容易说明,但请仔细观察 Tcp Keepalive 部分:

TCP Keepalive 需要注意的点(协议层面)

  1. 不太好的 TCP 堆栈实现,可能会要求保活报文必须携带有 1 个字节的数据负载
  2. TCP Keepalive 应该在服务器端启用,客户端不做任何改动;若单独在客户端启用,若客户端异常崩溃或出现连接故障,存在服务器无限期的为已打开的但已失效的文件描述符消耗资源的严重问题。
  3. 但在特殊的 NFS 文件系统环境下,需要客户端和服务器端都要启用 Tcp Keepalive 机制。
  4. TCP Keepalive 不是 TCP 规范的一部分,有三点需要注意:

    • 在短暂的故障期间,它们可能引起一个良好连接(good connection)被释放(dropped)
    • 它们消费了不必要的宽带
    • 在以数据包计费的互联网消费(额外)花费金钱

Tcp keepalive 如何使用

以下环境是在 Linux 服务器上进行,应用程序若想使用需要设置 SO_KEEPALIVE 套接口选项 才能够生效。

系统内核参数配置

  1. tcp_keepalive_time,在 TCP 保活打开的情况下,最后一次数据交换到 TCP 发送第一个保活探测包的间隔,即允许的持续空闲时长,或者说每次正常发送心跳的周期,默认值为 7200s(2h)。
  2. tcp_keepalive_probes 在 tcp_keepalive_time 之后,没有接收到对方确认,继续发送保活探测包次数,默认值为 9(次)
  3. tcp_keepalive_intvl,在 tcp_keepalive_time 之后,没有接收到对方确认,继续发送保活探测包的发送频率,默认值为 75s。

发送频率 tcp_keepalive_intvl 乘以发送次数 tcp_keepalive_probes,就得到了从开始探测到放弃探测确定连接断开的时间;

若设置,服务器在客户端连接空闲的时候,每 90 秒发送一次保活探测包到客户端,若没有及时收到客户端的 TCP Keepalive ACK 确认,将继续等待 15 秒 *2=30 秒。总之可以在 90s+30s=120 秒(两分钟)时间内可检测到连接失效与否。

以下改动,需要写入到 /etc/sysctl.conf 文件:

net.ipv4.tcp_keepalive_time=90
net.ipv4.tcp_keepalive_intvl=15
net.ipv4.tcp_keepalive_probes=2

保存退出,然后执行 sysctl -p 生效

可通过 sysctl -a | grep keepalive 命令检测一下是否已经生效。

针对已经设置 SO_KEEPALIVE 的套接字,应用程序不用重启,内核直接生效。

Java/netty 服务器如何使用

只需要在服务器端一方设置即可,客户端完全不用设置,比如基于 netty 4 服务器程序:

ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .option(ChannelOption.SO_BACKLOG, 100)
             // 心跳监测
             .childOption(ChannelOption.SO_KEEPALIVE, true)
             .handler(new LoggingHandler(LogLevel.INFO))
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast(new EchoServerHandler());
                 }
             });

            // Start the server.
            ChannelFuture f = b.bind(port).sync();

            // Wait until the server socket is closed.
            f.channel().closeFuture().sync();

Java 程序只能做到设置 SO_KEEPALIVE 选项,至于 TCP_KEEPCNT,TCP_KEEPIDLE,TCP_KEEPINTVL 等参数配置,只能依赖于 sysctl 配置,系统进行读取

其他语言怎么使用:链接

TcpKeepLive 常见的使用模式

默认情况下使用 keepalive 周期为 2 个小时,如不选择更改属于误用范畴,造成资源浪费:内核会为每一个连接都打开一个保活计时器,N 个连接会打开 N 个保活计时器。

优势很明显:

  1. TCP 协议层面保活探测机制,系统内核完全替上层应用自动给做好了
  2. 内核层面计时器相比上层应用,更为高效
  3. 上层应用只需要处理数据收发、连接异常通知即可
  4. 数据包将更为紧凑

关闭 TCP 的 keepalive,完全使用业务层面心跳保活机制 完全应用掌管心跳,灵活和可控,比如每一个连接心跳周期的可根据需要减少或延长

业务心跳 + TCP keepalive 一起使用,互相作为补充,但 TCP 保活探测周期和应用的心跳周期要协调,以互补方可,不能够差距过大,否则将达不到设想的效果

朋友的公司所做 IM 平台业务心跳 2 - 5 分钟智能调整 + tcp keepalive 300 秒,组合协作,据说效果也不错。

虽然说没有固定的模式可遵循,那么有以下原则可以参考

  1. 不想折腾,那就弃用 TCP Keepalive 吧,完全依赖应用层心跳机制,灵活可控性强
  2. 除非可以很好把控 TCP Keepalive 机制,那就可以根据需要自由使用吧

TcpKeepLive 注意事项

我们知道 TCP 连接关闭时,需要连接的两端中的某一方发起关闭动作,如果某一方突然断电,另外一端是无法知道的。tcp 的 keep_alive 就是用以检测异常的一种机制。

有三个参数:

  • 发送心跳消息的间隔
  • 未收到回复时,重试的时间间隔
  • 重试的次数

如果是 Linux 操作系统,这三个值分别为

huangcheng@ubuntu:~$ cat /proc/sys/net/ipv4/tcp_keepalive_time
7200
huangcheng@ubuntu:~$ cat /proc/sys/net/ipv4/tcp_keepalive_intvl
75
huangcheng@ubuntu:~$ cat /proc/sys/net/ipv4/tcp_keepalive_probes
9

       也就意味着每隔 7200s(两个小时)发起一次 keepalive 的报文,如果没有回应,75 秒后进行重试,最多重试 9 次即认为连接关闭。

       这三个选项分别对应 TCP_KEEPIDLE、TCP_KEEPINTL 和 TCP_KEEPCNT 的选项值,通过 setsockopt 进行设置。

但是,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_probeskeepalive_intvl =33 秒内返回错误。*
       
但是对于循环不停 send 的 socket,当拔掉网线后,会持续一段时间 send 返回成功(0~10 秒左右,取决 于发送数据的量),然后 send 阻塞,因为协议层的 buffer 满了,在等待 buffer 空闲,大概 90 秒左右后才会返回错误。
由此看来,send 的时候,keep alive 似乎没有起到作用,这个原因是因为重传机制
可以通过给 send 之前设置 timer 来解决的。

正文完
 0