作者:engleliu,腾讯 PCG 开发工程师
本文主要介绍 TCP 拥塞控制算法,内容多来自网上各个大佬的博客及《TCP/IP 详解》一书,在此基础上进行梳理总结,与大家分享。因水平有限,内容多有不足之处, 敬请谅解。
一、TCP 首部格式
在了解 TCP 的拥塞控制之前,先来看看 TCP 的首部格式和一些基本概念。
TCP 头部标准长度是 20 字节。包含源端口、目的端口、序列号、确认号、数据偏移、保留位、控制位、窗口大小、校验和、紧急指针、选项等。
1.1 数据偏移(Data Offset)
该字段长 4 位,单位为 4 字节。表示为 TCP 首部的长度。所以 TCP 首部长度最多为 60 字节。
1.2 控制位
目前的 TCP 控制位如下,其中 CWR 和 ECE 用于拥塞控制,ACK、RST、SYN、FIN 用于连接管理及数据传输。
CWR:用于 IP 首部的 ECN 字段。ECE 为 1 时,则通知对方已将拥塞窗口缩小。
ECE:在收到数据包的 IP 首部中 ECN 为 1 时将 TCP 首部中的 ECE 设置为 1,表示从对方到这边的网络有拥塞。
URG:紧急模式
ACK:确认
PSH:推送,接收方应尽快给应用程序传送这个数据。没用到
RST:该位为 1 表示 TCP 连接中出现异常必须强制断开连接。
SYN:初始化一个连接的同步序列号
FIN:该位为 1 表示今后不会有数据发送,希望断开连接。
1.3 窗口大小(Window)
该字段长度位 16 位,即 TCP 数据包长度位 64KB。可以通过 Options 字段的 WSOPT 选项扩展到 1GB。
1.4 选项(Options)
受 Data Offset 控制,长度最大为 40 字节。一般 Option 的格式为 TLV 结构:
常见的 TCP Options 有,SACK 字段就位于该选项中。:
1.5 SACK 选项
SACK 包括了两个 TCP 选项,一个选项用于标识是否支持 SACK,是在 TCP 连接建立时发送;另一种选项则包含了具体的 SACK 信息。
SACK_Permitted 选项,该选项只允许在 TCP 连接建立时,有 SYN 标志的包中设置,也即 TCP 握手的前两个包中,分别表示通信的两方各自是否支持 SACK。
TCP SACK-Permitted Option:
Kind: 4
Length: Variable
+----------+----------+
| Kind=4 | Length=2 |
+----------+----------+
SACK(选择性确认) 选项位于 Options 中。该选项参数告诉对方已经接收到并缓存的不连续的数据块,发送方可根据此信息检查究竟是哪些块丢失,从而发送相应的数据块。受 TCP 包长度限制,TCP 包头最多包含四组 SACK 字段。
TCP SACK Option:
Kind: 5
Length: Variable
+--------+--------+
| Kind=5 | Length |
+--------+--------+--------+--------+
| Left Edge Of lst Block |
+--------+--------+--------+--------+
| Right Edge Of lst Block |
+--------+--------+--------+--------+
| . . . |
+--------+--------+--------+--------+
| Left Edge Of nth Block |
+--------+--------+--------+--------+
| Right Edge Of nth Block |
+--------+--------+--------+--------+
SACK 的工作原理
如下图所示, 接收方收到 500-699 的数据包,但没有收到 300-499 的数据包就会回 SACK(500-700) 给发送端,表示收到 500-699 的数据。
二、滑动窗口和包守恒原则
2.1 滑动窗口
为了解决可靠传输以及包乱序的问题,TCP 引入滑动窗口的概念。在传输过程中,client 和 server 协商接收窗口 rwnd,再结合拥塞控制窗口 cwnd 计算滑动窗口 swnd。在 Linux 内核实现中,滑动窗口 cwnd 是以包为单位,所以在计算 swnd 时需要乘上 mss(最大分段大小)。
如下图所示滑动窗口包含 4 部分:
已收到 ack 确认的数据;
已发还没收到 ack 的;
在窗口中还没有发出的(接收方还有空间);
窗口以外的数据(接收方没空间)。
滑动后的示意图如下(收到 36 的 ack,并发出了 46-51 的数据):
2.2 包守恒原则
TCP 维护一个发送窗口,估计当前网络链路上能容纳的数据包数量,希望在有数据可发的情况下,回来一个确认包就发出一个数据包,总是保持发送窗口那么多包在网络中流动。
传输的理想情况是要同时达到最大的吞吐量和最小的往返延迟,要达到这个目的,连接必须同时满足两个条件:
以链路瓶颈带宽 BtlBw 发包 (带宽利用率最高)
保证链路中没有缓存队列(延迟最低)
包守恒原则是拥塞控制的基础。
三、TCP 重传机制
本文重点介绍 TCP 拥塞控制相关,传输流程不在该范围之内,有兴趣的同学可以查阅相关文档。不过 TCP 重传逻辑和拥塞控制中的快速重传
有关,所以在真正介绍拥塞控制算法之前,先来了解下 TCP 重传逻辑。
3.1 超时重传 [RFC2988]
RTT(Round Trip Time)由三部分组成:链路的传播时间(propagation delay)、末端系统的处理时间、路由器缓存中的排队和处理时间(queuing delay)。TCP 发送端得到了基于时间变化的 RTT 测量值,就能据此设置 RTO。
当一个重传报文段被再次重传时,则增大 RTO 的退避因子 。通常情况下 值为 1,多次重传 加倍增长为 2,4,8 等。通常 不能超过最大退避因子,Linux 下 RTO 不能超过 TCP_RTO_MAX(默认为 120s)。一旦收到相应的 ACK, 重置为 1。
下面介绍几种常用的 RTT 算法。
3.1.1 rtt 经典算法 [RFC793]
1)首先,先采样 RTT,记下最近几次的 RTT 值。2)然后做平滑计算 SRTT( Smoothed RTT)。公式为:(其中的 α 取值在 0.8 到 0.9 之间,这个算法英文叫 Exponential weighted moving average,中文叫:加权移动平均)
3)开始计算 RTO。公式如下:
其中:
UBOUND 是最大的 timeout 时间,上限值;
LBOUND 是最小的 timeout 时间,下限值;
β 值一般在 1.3 到 2.0 之间。
该算法的问题在于重传时,是用重传的时间还是第一次发数据的时间和 ACK 回来的时间计算 RTT 样本值,另外,delay ack 的存在也让 rtt 不能精确测量。
3.1.2 rtt 标准算法(Jacobson / Karels 算法)
该算法 [RFC6298] 特点是引入了最新的 RTT 的采样
和平滑过的
的差值做参数来计算。 公式如下: 1.计算平滑 RTT
2.计算平滑 RTT 和真实的差距(加权移动平均)
3.计算 RTO