TCP 超时与重传详解

简介


由于 下层网络层( IP )可能出现丢失、重复或包乱序的情况,因此必须在 TCP 协议中提供可靠数据传输服务。 TCP 根据接收端返回至发送端的一系列确认信息来判断是否出现丢包。当数据段或 ACK 信息丢失,TCP 将启动重传操作。

TCP 拥有联滔独立机制来完成重传,通常第二种方式会比第一种方式更高效:

  • 基于时间
  • 基于确认信息的构成

什么是RTO

TCP 在发送数据时会设置一个计数器,若计时器超时仍未收到数据确认信息,则会引发相应的超时或基于计时器的重传操作,计时器超时被称为重传超时( RTO Retransmission Timeout )。

一个简单的超时和重传例子


step 1:部署两台虚拟机 10.211.55.6 和 10.211.55.8 。
step 2:在 10.211.55.8 使用 iperf 启动一个 server,端口为 5001 。

1
2
3
4
iperf -s
------------------------------------------------------------
Server listening on TCP port 5001
TCP window size: 85.3 KByte (default)

step 3:在 10.211.55.6 上启用 telnet 连接 10.211.55.8 服务器上的 iperf 服务。

1
2
3
4
telnet 10.211.55.8 5001
Trying 10.211.55.8...
Connected to 10.211.55.8.
Escape character is '^]'.

NOTE: ipef 的安装和使用可以参考这里

step 4:在 10.211.55.6 服务器上使用 tcpdump 抓取 5001 端口上的数据包

1
tcpdump -n -i eth0 port 5001

step 5:当 10.211.55.6 telnet 已经成功连接上了 10.211.55.8 服务器之后,开启 10.211.55.8 的防火墙设置,这意味着使 10.211.55.8 上的 5001 端口无法被外部 IP 访问,以模拟断网的情况下,TCP 超时重传的机制。

1
systemctl start firewalld.service

以下的信息是 tcpdump 抓包的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
16:14:44.946872 IP 10.211.55.6.58229 > 10.211.55.8.commplex-link: Flags [S], seq 1301976868, win 29200, options [mss 1460,sackOK,TS val 4261517 ecr 0,nop,wscale 7], length 0
16:14:44.947092 IP 10.211.55.8.commplex-link > 10.211.55.6.58229: Flags [S.], seq 2629201553, ack 1301976869, win 28960, options [mss 1460,sackOK,TS val 4057742 ecr 4261517,nop,wscale 7], length 0
16:14:44.947112 IP 10.211.55.6.58229 > 10.211.55.8.commplex-link: Flags [.], ack 1, win 229, options [nop,nop,TS val 4261517 ecr 4057742], length 0
16:15:00.583709 IP 10.211.55.6.58229 > 10.211.55.8.commplex-link: Flags [P.], seq 1:5, ack 1, win 229, options [nop,nop,TS val 4277154 ecr 4057742], length 4
16:15:00.583859 IP 10.211.55.8.commplex-link > 10.211.55.6.58229: Flags [.], ack 5, win 227, options [nop,nop,TS val 4073379 ecr 4277154], length 0
16:15:16.584421 IP 10.211.55.6.58229 > 10.211.55.8.commplex-link: Flags [P.], seq 5:9, ack 1, win 229, options [nop,nop,TS val 4293155 ecr 4073379], length 4
16:15:16.785486 IP 10.211.55.6.58229 > 10.211.55.8.commplex-link: Flags [P.], seq 5:9, ack 1, win 229, options [nop,nop,TS val 4293356 ecr 4073379], length 4
16:15:16.987438 IP 10.211.55.6.58229 > 10.211.55.8.commplex-link: Flags [P.], seq 5:9, ack 1, win 229, options [nop,nop,TS val 4293558 ecr 4073379], length 4
16:15:17.391487 IP 10.211.55.6.58229 > 10.211.55.8.commplex-link: Flags [P.], seq 5:9, ack 1, win 229, options [nop,nop,TS val 4293962 ecr 4073379], length 4
16:15:18.198427 IP 10.211.55.6.58229 > 10.211.55.8.commplex-link: Flags [P.], seq 5:9, ack 1, win 229, options [nop,nop,TS val 4294769 ecr 4073379], length 4
16:15:19.810469 IP 10.211.55.6.58229 > 10.211.55.8.commplex-link: Flags [P.], seq 5:9, ack 1, win 229, options [nop,nop,TS val 4296381 ecr 4073379], length 4
16:15:23.029878 IP 10.211.55.6.58229 > 10.211.55.8.commplex-link: Flags [P.], seq 5:9, ack 1, win 229, options [nop,nop,TS val 4299600 ecr 4073379], length 4
16:15:29.477411 IP 10.211.55.6.58229 > 10.211.55.8.commplex-link: Flags [P.], seq 5:9, ack 1, win 229, options [nop,nop,TS val 4306048 ecr 4073379], length 4
16:15:42.373401 IP 10.211.55.6.58229 > 10.211.55.8.commplex-link: Flags [P.], seq 5:9, ack 1, win 229, options [nop,nop,TS val 4318944 ecr 4073379], length 4
16:16:08.133915 IP 10.211.55.6.58229 > 10.211.55.8.commplex-link: Flags [P.], seq 5:9, ack 1, win 229, options [nop,nop,TS val 4344704 ecr 4073379], length 4
16:16:59.718410 IP 10.211.55.6.58229 > 10.211.55.8.commplex-link: Flags [P.], seq 5:9, ack 1, win 229, options [nop,nop,TS val 4396289 ecr 4073379], length 4
16:18:42.886431 IP 10.211.55.6.58229 > 10.211.55.8.commplex-link: Flags [P.], seq 5:9, ack 1, win 229, options [nop,nop,TS val 4499457 ecr 4073379], length 4
16:20:43.206406 IP 10.211.55.6.58229 > 10.211.55.8.commplex-link: Flags [P.], seq 5:9, ack 1, win 229, options [nop,nop,TS val 4619776 ecr 4073379], length 4
16:22:43.525434 IP 10.211.55.6.58229 > 10.211.55.8.commplex-link: Flags [P.], seq 5:9, ack 1, win 229, options [nop,nop,TS val 4740096 ecr 4073379], length 4
16:24:43.845966 IP 10.211.55.6.58229 > 10.211.55.8.commplex-link: Flags [P.], seq 5:9, ack 1, win 229, options [nop,nop,TS val 4860416 ecr 4073379], length 4
16:26:44.165596 IP 10.211.55.6.58229 > 10.211.55.8.commplex-link: Flags [P.], seq 5:9, ack 1, win 229, options [nop,nop,TS val 4980736 ecr 4073379], length 4
16:28:44.486398 IP 10.211.55.6.58229 > 10.211.55.8.commplex-link: Flags [P.], seq 5:9, ack 1, win 229, options [nop,nop,TS val 5101056 ecr 4073379], length 4

可以看到数据包 1 ~ 3 是三次握手建立连接的包。数据包 4 ~ 5 是开启防火墙之前,正常传输数据的包。

数据包 6 ~ 17 是开启防火墙之后,客户端 10.211.55.6 超时重连的情况。可以看到每次重传的时间间隔是在递增的,从最开始的 200 ms,400 ms,800 ms,1.6 s …. 到最后一次超时连接约 2 min 的时间。总共重试的时间大概为:16:28:44.486398 - 16:15:16.584421 = 13 min。

每次重传间隔时间加倍被称为 binary exponential backoff (二进制指数退避)。 TCP 拥有两个阈值来决定如何重传同一个报文段:

  • R1 :表示 TCP 在向 IP 层传递前,愿意尝试重传的次数。
  • R2 :表示 TCP 放弃当前连接的时机

在 Linux 系统中,R1 对应 net.ipv4.tcp_retries1 , R2 对应 net.ipv4.tcp_retries2 设置。这两个变量的值都是次数而不是时间。其中 tcp_retries2 的默认值为 15,对应约为 13 ~ 30 分钟。

1
2
3
4
5
sysctl -n net.ipv4.tcp_retries1
3
sysctl -n net.ipv4.tcp_retries2
15

此外,对于 SYN 报文段,变量 net.ipv4.tcp_syn_retries (默认值为6) 和 net.ipv4.tcp_synack_retries (默认值为5) 限定重传次数。

设置重传超时


TCP 超时和重传的基础是如何根据 RTT 动态计算出 RTO 。如果 TCP 先于 RTT 开始重传,可能会再网络中引入不必要的重复数据。反之,若在超过 RTT 时间间隔之后发送重传数据,整体网络利用率会随之下降。

因此,如何计算 RTO 是一个非常复杂的话题,有一系列的算法来支撑。

基于 SRTT 估算法 (经典方法)

如下的公式可以计算得到 平滑的 RTT 估计值(称为 SRTT):

1
SRTT = ( α * SRTT ) + ((1- α) * RTT)

其中,SRTT 是基于现存值和新的样本值 RTT 得到的更新结果。常量 α 为平滑因子,推荐值为 0.8 ~ 0.9 。每当得到新的样本值,SRTT 就会被更新。从上面的公式可以看出,新的 SRTT 80% ~ 90% 来自于原来的值,而有 10% ~ 20% 来自于新的样本值。这种估算方法被称为:指数加权移动平均(Exponential weighted moving average)。

由于 SRTT 估计值会随 RTT 的变化而变化,RFC 0793 推荐根据如下的公式计算 RTO :

1
RTO = min [UBOUND, max[LBOUND, (β * SRTT)]]

β 为延迟离散因子,推荐值为 1.3 ~ 2.0 。 unbound 为 RTO 的上边界(可设定为建议值:1分钟), lbound 为 RTO 的下边界(可设定建议值:1秒)。由此我们可以得出,RTO 的值最小为 1 秒,或者约等于 2 倍 SRTT 。

经典方法适合于相对稳定的 RTT ,但是在 RTT 变化比较大的网络中,就不是一种较好的方案了。

Jacobson / Karels 算法 (标准方法)

由于经典方法无法适应于网络不稳定, RTT 变化比较大的网络中,为了解决这个问题,通过记录 RTT 测量值的变化情况以及均值来得到较为准确的估计值。

下面是该算法的具体公式:

1
2
3
4
5
6
7
# 计算平滑 RTT
SRTT = SRTT + α (RTT – SRTT)
# 计算平滑 RTT 和 真实 RTT 的差距
DevRTT = (1-β)*DevRTT + β*(|RTT-SRTT|)
RTO = µ * SRTT + ∂ * DevRTT

在 Linux 下,α = 0.125,β = 0.25,μ = 1,∂ = 4, 其中 DevRTT 表示 Deviation RTT (标准差 RTT )。

基于计时器的重传


一旦 TCP 发送端得到了基于时间变化的 RTT 测量值,就能据此设置 RTO ,发送报文段时应确保重传计时器设置合理。 TCP 将超时重传视为相当重要的事件,当发生这种情况时,它通过降低当前数据发送率来对此快速响应。两种方法实现:

  • 基于拥塞控制机制减小发送窗口大小。
  • 每当一个重传报文段被再次重传时,则增大RTO退避因子。

特别是当同一报文段出现多次重传时, RTO 值乘以值 α 来形成新的超时避让值:

1
RTO = α * RTO

在通常的情况下, α 的值为1。随着多次重传, α 呈加倍增长: 2,4,8… 。 通常 α 不能超过最大退避因子。在接收到 ack 之后, α 又会被重置为 1 。

在 Linux 下,最大退避因子由 TCP_RTO_MAX 决定, 默认为 120 s 。

1
#define TCP_RTO_MAX ((unsigned)(120*HZ))

快速重传 ( Fast Retransmit )


快速重传机制是基于接收端的反馈信息来引发的重传。与基于超时重传机制相比,快速重传能更加及时有效地修复丢包的情况。大部分操作系统同时实现了这两种重传机制。

快速重传机制不以时间驱动,而以数据驱动重传。这表示,如果包没有连续到达,就 ack 最后那个可能被丢了的包,如果发送方连续收到 3 次相同的 ack ,这时就进行重传。其好处就是不用等 timeout 了再进行重传。

下面来看一个实际的例子:

fast-retransmit

上面的图示中,客户端发送了序号为 1 的数据,服务器端收到了回复了 ack 2 ,但是由于客户端序号为 2 的数据由于网络丢包的原因,导致服务器端没有正确的接收到,于是服务器端还是回复 ack 2 。此时客户端又发送了序号为 3 的数据,服务器端还是回复的 ack 2 。直到客户端又发送了序号 4 ,5 的数据,服务器端再次回复了两次 ack 2 的数据包,此时,客户端总共收到了三次 ack 2 的确认,确认了服务器端还没有收到数据包 2 的数据,进行了重传。

快速重传的问题

快速重传虽然解决了 timeout 的问题,但是仍然有一个问题不是太好解决,即重传之前的一个数据包,还是重传所有的数据包的问题。按照上面的示例,就是是重传 2 这一个数据包,还是 2,3,4,5 这四个数据包了。因为发送端并不清楚连续的3个 ack(2) 是谁传回来的?

带选择确认的重传


带选择性的重传( Selective Acknowledgment SACK )可以很好地解决快速重传中无法标记出哪些数据到了,哪些数据没有到的问题。其实现会在 TCP Header 中增加 SACK 的选项,用于接收端标记已经收到的数据包区间

sack-example

需要注意的是,SACK 需要发送端和接收端都支持,在 Linux 下可以通过 tcp_sack 参数来进行设置。

1
2
sysctl -n net.ipv4.tcp_sack
1

重复收到数据的问题 ( Duplicate SACK )


Duplicate SACK 又被称为 D-SACK ,其主要使用了 SACK 来告诉发送方有哪些数据被重复接收了。 D-SACK 使用了 SACK 的第一个段来做标志:

  • 如果 SACK 的第一个段的范围被 ACK 所覆盖,那么就是 D-SACK 。
  • 如果SACK的第一个段的范围被 SACK 的第二个段覆盖,那么就是 D-SACK 。

引入了D-SACK,有这么几个好处:

  • 可以让发送方知道,是发出去的包丢了,还是回来的ACK包丢了。
  • 是不是自己的timeout太小了,导致重传。
  • 网络上出现了先发的包后到的情况(又称reordering)。
  • 网络上是不是把我的数据包给复制了。