Linux 网络编程随笔

TCP 连接,一端断电和进程崩溃有什么区别

问题剖析

TCP 连接,没有打开 Keepalive 选项,没有数据交互,现在一端突然断电或者一端的进程崩溃了,这两种情况有什么区别呢?这是腾讯的一道面试题,其中有几个关键词:

  • TCP 连接没有开启 Keepalive;
  • 一直没有数据交互;
  • 进程崩溃;
  • 主机宕机;

什么是 TCP Keepalive

TCP Keepalive 其实就是 TCP 的保活机制,它的工作原理如下:

如果两端的 TCP 连接一直没有数据交互,达到了触发 TCP 保活机制的条件,那么系统内核里的 TCP 协议栈就会发送探测报文。

  • 如果对端程序是正常工作的。当 TCP 保活的探测报文发送给对端,对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。
  • 如果对端主机宕机,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经消亡

所以,TCP 保活机制可以在双方没有数据交互的情况,通过探测报文,来确定对方的 TCP 连接是否存活。
注意,应用程序若想使用 TCP 保活机制需要通过 Socket 接口设置 SO_KEEPALIVE 选项才能够生效,如果没有设置,那么就无法使用 TCP 保活机制。

主机宕机

在没有开启 TCP Keepalive,且双方一直没有数据交互的情况下,如果客户端的「主机宕机」了,会发生什么?

客户端主机宕机了,服务端是无法感知到的,再加上服务端没有开启 TCP Keepalive,又没有数据交互的情况下,服务端的 TCP 连接将会一直处于 ESTABLISHED 连接状态,直到服务端重启进程

所以,这里可以得知一个点,在没有使用 TCP 保活机制且双方不传输数据的情况下,一方的 TCP 连接处在 ESTABLISHED 状态,并不代表另一方的连接还一定正常。

进程崩溃

在没有开启 TCP Keepalive,且双方一直没有数据交互的情况下,如果服务端的「进程崩溃」了,会发生什么?

TCP 的连接信息是由系统内核维护的,所以当服务端的进程崩溃后,系统内核需要回收该进程的所有 TCP 连接资源,于是系统内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在系统内核完成,并不需要进程参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP 四次挥手的过程。

这里可以尝试做实验,使用 kill -9 来模拟服务端进程崩溃的情况,发现在 kill 掉进程后,服务端会发送 FIN 报文,与客户端进行四次挥手。

所以,即使没有开启 TCP Keepalive,且双方也没有数据交互的情况下,如果任意一方的进程发生了崩溃,这个过程系统内核是可以感知的到的,于是系统内核就会发送 FIN 报文给对方,然后与对方进行 TCP 四次挥手。

有数据传输的场景

以上就是对这道面试题的回答,接下来我们看看在「有数据传输」的场景下的一些异常情况:

第一种,客户端主机宕机,又迅速重启,会发生什么?
第二种,客户端主机宕机,一直没有重启,会发生什么?


假设客户端主机宕机,又迅速重启,会发生什么?

在客户端主机宕机后,服务端向客户端发送的报文会得不到任何的响应,在一定时长后,服务端就会触发超时重传机制,重传未得到响应的报文给客户端。

在服务端重传报文的过程中,客户端主机重启完成后,客户端的内核就会接收重传的报文,然后根据报文的信息传递给对应的进程:

  • 如果客户端主机上没有进程绑定该 TCP 报文的目标端口号,那么客户端内核就会回复 RST 报文,重置该 TCP 连接
  • 如果客户端主机上有进程绑定该 TCP 报文的目标端口号,由于客户端主机重启后,之前的 TCP 连接的数据结构已经丢失了,客户端内核里协议栈会发现找不到该 TCP 连接的 Socket 结构体,于是就会回复 RST 报文,重置该 TCP 连接

所以,只要有任意一方的主机重启完成后,收到之前 TCP 连接的报文,都会回复 RST 报文给对端,以断开连接


假设客户端主机宕机,一直没有重启,会发生什么?

这种情况,服务端超时重传报文的次数达到一定阈值后,系统内核就会判定出该 TCP 有问题,然后通过 Socket 接口告诉应用程序该 TCP 连接出问题了,于是服务端的 TCP 连接就会断开。

那 TCP 的数据报文具体重传几次呢?在 Linux 系统中,提供一个叫 tcp_retries2 配置项(cat /proc/sys/net/ipv4/tcp_retries2),默认值是 15;这个内核参数是控制在 TCP 连接建立的情况下,超时重传的最大次数。

不过 tcp_retries2 设置了 15 次,并不代表 TCP 超时重传了 15 次才会通知应用程序终止该 TCP 连接,内核会根据 tcp_retries2 设置的值,计算出一个 timeout(如果 tcp_retries2 = 15,那么计算得到的 timeout = 924600ms),如果重传间隔超过这个 timeout,则认为超过了阈值,就会停止重传,然后就会断开 TCP 连接。

在发生超时重传的过程中,每一轮的超时时间(RTO)都是倍数增长的,比如:如果第一轮 RTO 是 200 毫秒,那么第二轮 RTO 是 400 毫秒,第三轮 RTO 是 800 毫秒,以此类推。

而 RTO 是基于 RTT(一个包的往返时间)来计算的,如果 RTT 较大,那么计算出来的 RTO 就越大,那么经过几轮重传后,很快就达到了上面的 timeout 阀值了。

举个例子,如果 tcp_retries2 = 15,那么计算得到的 timeout = 924600ms,如果重传总间隔时长超过了 924600ms 就会停止重传,然后就会断开 TCP 连接:

  • 如果 RTT 比较小,那么 RTO 初始值就约等于下限 200ms,也就是第一轮的超时时间是 200 毫秒,由于 timeout 总时长是 924600ms,表现出来的现象刚好就是重传了 15 次,超过了 timeout 值,从而断开 TCP 连接
  • 如果 RTT 比较大,假设 RTO 初始值计算得到的是 1000ms,也就是第一轮的超时时间是 1 秒,那么根本不需要重传 15 次,重传总间隔时长就会超过 924600ms

最小 RTO 和最大 RTO 已经是在 Linux 内核中定义好了:

1
2
#define TCP_RTO_MAX ((unsigned)(120*HZ))
#define TCP_RTO_MIN ((unsigned)(HZ/5))

Linux 2.6+ 使用 1000 毫秒的 HZ,因此 TCP_RTO_MIN 约为 200 毫秒,TCP_RTO_MAX 约为 120 秒。

如果 tcp_retries 设置为 15,且 RTT 比较小,那么 RTO 初始值就约等于下限 200ms,这意味着它需要 924.6 秒才会将断开的 TCP 连接并通知给上层(即应用程序),每一轮的 RTO 增长关系如下表格:

RetransmissionRTO (ms)Time before a timeout (secs)Time before a timeout (mins)
12000.2 secs0.0 mins
24000.6 secs0.0 mins
38001.4 secs0.0 mins
416003.0 secs0.1 mins
532006.2 secs0.1 mins
6640012.6 secs0.2 mins
71280025.4 secs0.4 mins
82560051.0 secs0.9 mins
951200102.2 secs1.7 mins
10102400204.6 secs3.4 mins
11120000324.6 secs5.4 mins
12120000444.6 secs7.4 mins
13120000564.6 secs9.4 mins
14120000684.6 secs11.4 mins
15120000804.6 secs13.4 mins
16120000924.6 secs15.4 mins

总结

如果「客户端进程崩溃」,客户端的进程在发生崩溃的时候,内核会发送 FIN 报文,与服务端进行四次挥手,以此断开 TCP 连接。

如果「客户端主机宕机」,那么是不会发生四次挥手的,具体后续会发生什么,还要看服务端会不会发送数据。

  • 如果服务端会发送数据,由于客户端已经不存在,收不到数据报文的响应报文,服务端的数据报文会超时重传,当重传总间隔时长达到一定阈值(内核会根据 tcp_retries2 设置的值计算出一个阈值)后,会断开 TCP 连接;
  • 如果服务端一直不会发送数据,再看服务端有没有开启 TCP Keepalive 机制?
    • 如果服务端有开启 TCP Keepalive 机制,服务端在一段时间没有进行数据交互时,会触发 TCP Keepalive 机制,探测对方是否存在,如果探测到对方已经消亡,则会断开自身的 TCP 连接;
    • 如果服务端没有开启 TCP Keep 机制,服务端的 TCP 连接会一直存在,并且一直保持在 ESTABLISHED 状态。