TCP 协议回顾和 BBR 算法介绍

本科的时候面向考试学习计算机网络,TCP 是重点中的重点,可惜当时不知道 TCP 在网络世界中的重要,考完试就把知识点扔了。最近在复习计算机网络,看到 TCP 这章,就像看到了一个老朋友。就想把它记录下来,便于复习。

TCP 协议非常复杂,标准也非常多,但核心内容就以下几个部分:

  • 三次握手
  • 四次挥手
  • 可靠性传输
  • 流量控制
  • 拥塞控制

TCP 协议头

要想了解 TCP 协议首先就必须了解 TCP 的协议头:

首先,源端口号和目标端口号是不可少的,这一点和 UDP 是一样的。如果没有这两个端口号。 数据就不知道应该发给哪个应用。

接下来是包的序号和确认序号。为了保证消息的顺序性到达,TCP 给每个包编了一个序号。初始序号在建立连接时指定,此后每个包的序号为上一个包的序号+上一个包的字节数。服务器会返回一个确认号,表示这个序号之前的包都收到了。

然后是一些状态位。例如 SYN 是发起一个连接,ACK 是回复,RST 是重新连接,FIN 是结束连接。TCP 是面向连接的,这些带状态位的包可以改变双方的状态。

窗口大小是跟流量控制有关。TCP 是全双工的协议,通信双方都会维护一个缓存空间。这个窗口大小就是告诉对方我还有多少剩余的缓存空间。

三次握手

TCP 是面向连接的协议,所以在建立连接前有一系列的动作,被称为三次握手:

一开始,客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端口,处于 LISTEN 状态。然后客户端主动发起连接 SYN,之后处于 SYN-SENT 状态。服务端收到发起的连接,返回 SYN,并且 ACK 客户端的 SYN,之后处于 SYN-RCVD 状态。客户端收到服务端发送的 SYN 和 ACK 之后,发送 ACK 的 ACK,之后处于 ESTABLISHED 状态,因为它一发一收成功了。服务端收到 ACK 的 ACK 之后,处于 ESTABLISHED 状态,因为它也一发一收了。

三次握手除了双方建立连接外,还要沟通一件事情,就是 TCP 包的序号的问题。

双方在发送 SYN 包的时候,各自需要指定一个针对这次连接的序号 seq。这个 seq 实质上可以看出一个 32 位的计时器,每 4ms 加一。为什么要这么做呢?主要是为了防止在网络中被延迟的分组在以后被重复传输,而导致某个连接的一端对它作错误的判断。如果序号不按这种方式分配,而是从 1 开始,则会出现这样的情况:AB 建立连接之后,A 发送了 1,2,3 三个包,然后掉线了。由于网络的原因三个包没有到达 B,在网络中游荡。然后 A 重连了,序号重新从 1 开始,他又发送了 1,2 两个包,但没有发送 3 号包。此时上一次连接发送的 3 号包却到达了 B,B 以为是 A 这次发送的,就产生了误判。为了避免这种情况的发生,TCP 协议规定了这种方式生成初始 seq。以这种方式生成的初始 seq,需要 4 个多小时才会重复,此时早已过了 3 号包的生存时间(TTL)。

为什么要三次握手而不是两次?

原因一:服务器会收到客户端很早以前发送的,但因为延迟导致现在才到达的 SYN 报文。如果不采用三次握手,则服务器会认为新的连接已经建立,会白白浪费缓存等资源。
原因二:三次握手需要交流双方的初始序号 seq,服务器发送的第二次握手是针对客户端在第一次握手中约定的客户端初始 seq 的确认,客户端的第三次握手是针对服务器在第二次握手中约定的服务器初始 seq 的确认。如果没有第三次握手,万一服务器的 SYN 包丢了,那么客户端无法得知服务器的初始序号,此时客户端就没法接收服务器的包,因为客户端没法辨别这个包是本次连接中发送的,还是上一次连接中发送的。

SYN 洪泛攻击

因为服务器在收到一个 SYN 报文后,会初始化连接变量和缓存,如果攻击方会发送大量 SYN 报文,而不完成第三次握手,那么就会导致服务器的连接资源被消耗殆尽。
针对 SYN 洪泛攻击有一种有效的防御手段,称为SYN cookie:当服务器接收到一个 SYN 报文时,它不知道这是来自一个合法的请求还是 SYN 洪泛攻击的一部分。所以它不会为其分配资源,而是将该报文中的源、目的 IP 地址和端口和服务器自己的秘密数做哈希,将(秒级时间(5 位)+最长分段大小(3 位)+哈希(24 位))作为初始 seq 返回给客户端。
如果是一个合法用户,会返回一个 ACK 包,服务器可以通过将 ACK 包中的源、目的 IP 地址和端口,和 ACK-1 对比,得知这是否是一个合法的 SYN 确认包。然后可以通过秒级时间确定这是否是一个新的包。如果是新的包且合法,服务器才会分配资源。这样就有效防止了 SYN 洪泛攻击的发生。

四次挥手

有建立连接,必然也有断开连接。断开连接的动作被称为四次挥手:

区别于三次握手,四次挥手的发起方可以是客户端也可以是服务端。因此不区分客户端和服务端而是用 AB 代替。

一开始,A 和 B 都处于 ESTABLISHED 的状态。然后 A 发送 FIN 表示请求断开连接,之后 B 处于 FIN-WAIT-1 的状态。

B 在接收到 FIN 请求后,会发送一个 ACK 包表示收到了 FIN 请求,之后 B 处于 CLOSED-WAIT 状态。需要注意的是此时 B 发送的是 ACK 包而不是 FIN 包,之所以不像三次握手一样直接回应一个 FIN 包,是因为此时 B 可能还有些事情没有做完,还可能发送数据,所以称为半关闭状态。

这个时候 A 可以选择不再接收数据,也可以选择最后再接收一段数据,等待 B 也主动关闭。不论如何,A 在收到 ACK 包后都进入了 FIN-WAIT2 阶段。此时如果 B 下线,A 将永远在这个状态。TCP 协议里没有对这个状态的处理,但 Linux 有,可以调整 tcp_fin_timeout 这个参数,设置一个超时时间。

B 处理完了所有的事情,终于也准备关闭,此时会发送一个 FIN 包。之后 B 进入 LAST-ACK 状态,等待 A 的 ACK 包。

A 在收到服务器的 FIN 包后会发送一个 ACK 包表示收到了 B 的 FIN 包。按理说此时 A 就可以关闭了,但由于 A 最后的 ACK 存在丢包的可能,B 没有收到最后的 ACK 包的话,就会重发一个 FIN 包,如果这时候 A 关闭了,B 就再也收不到 ACK 了。因而 TCP 协议要求 A 最后等待一段时间 TIME_WAIT,这个时间要足够长,长到 B 没收到 ACK 的话,重发的 FIN 包还能到达 A。

A 直接关闭还有一个问题是,A 的端口就直接空出来了,但是 B 不知道,B 原来发过的很多包很可能还在路上,如果 A 的端口被一个新的应用占用了,这个新的应用会收到上个连接中 B 发过来的包,虽然序列号是重新生成的,但是这里要上一个双保险,防止产生混乱,因而也需要等足够长的时间,等到原来 B 发送的所有的包都过期了,再空出端口来。

等待的时间设为 2MSL,MSL 是 Maximum Segment Lifetime(报文最大生存时间),它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的, 而 IP 头中有一个 TTL 域,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。协议规定 MSL 为 2 分钟,实际应用中常用的是 30 秒,1 分钟和 2 分钟等。

服务器大量 CLOSE_WAIT\TIME_WAIT 状态的原因

如果服务器出现异常,百分之八九十都是下面两种情况:
1、服务器保持了大量 CLOSE_WAIT 状态。产生的原因在于:TCP Server 已经 ACK 了过来的 FIN 数据包,但是上层应用程序迟迟没有发命令关闭 Server 到 client 端的连接。所以 TCP 一直在那等啊等…..所以说如果发现自己的服务器保持了大量的 CLOSE_WAIT,问题的根源十有八九是自己的 server 端程序代码的问题。
2、服务器保持了大量 TIME_WAIT 状态。产生的原因在于:服务器处理大量高并发短连接并主动关闭连接时容易出现 TIME_WAIT 积压。这是因为关闭的发起方在 TIME_WAIT 阶段需要等待 1-4 分钟才能回收资源。如果连接过多将导致资源来不及回收。解决方案是修改 Linux 内核,允许将 TIME-WAIT sockets 重新用于新的 TCP 连接,并开启 TCP 连接中 TIME-WAIT sockets 的快速回收,这些默认都是关闭的。

tcp 的 keepalive

在长连接的过程中,可能很长一段时间连接双方没有数据通信,然后这个时候如果对端挂掉,另一方如何感知呢?答案是可以通过 keepalive 机制。TCP 的 keepalive 默认是不打开的,要用 setsockopt 将 SOL_SOCKET.SO_KEEPALIVE 设置为 1 才打开,并且可以设置三个参数tcp_keepalive_time/tcp_keepalive_probes/tcp_keepalive_intvl,分别表示连接闲置多久开始发 keepalive 的 ACK 包、发几个 ACK 包不回复才认为对方死了、两个 ACK 包之间间隔多长,默认值是 7200 秒、9次、75秒。默认情况下当双方超过 7200 没有通信的时候,TCP 协议会向对方发一个带有 ACK 标志的空数据包(keepalive 探针),对方在收到 ACK 包以后,如果连接一切正常,应该回复一个 ACK;如果连接出现错误了(例如对方重启了,连接状态丢失),则应当回复一个 RST;如果对方没有回复,服务器每隔 75s 再发 ACK,如果连续 9 个包都被无视了,说明连接被断开了。

可靠性传输

TCP 协议为了保证顺序性,每一个包都有一个 ID。在建立连接的时候,会商定起始的 ID 是什么,然后按照 ID 一个个发送。为了保证不丢包,对于发送的包都要进行应答,但是这个应答也不是一个一个来的,而是会应答某个之前的 ID,表示都收到了,这种模式称为累计确认或者累计应答(cumulative acknowledgment)。

为了记录所有发送的包和接收的包,TCP 也需要发送端和接收端分别都有缓存来保存这些记录。发送端的缓存里是按照包的 ID 一个个排列,根据处理的情况分成四个部分。

  • 第一部分:发送了并且已经确认的。
  • 第二部分:发送了并且尚未确认的。
  • 第三部分:没有发送,但是已经等待发送的。
  • 第四部分:没有发送,并且暂时还不会发送的。

为什么会有三、四部分的区分呢?这是因为接受端有个处理极限,就是剩余缓冲区的大小,如果给接收端发送的包的大小超过了剩余缓冲区的大小,那么有一部分包就会被丢弃,这是不合适的。所以超出剩余缓冲区大小的包,发送端暂时不会发。

于是,发送端需要保持下面的数据结构:

对于接收端来讲,它的缓存里记录的内容要简单一些:

  • 第一部分:接收并且确认过的。
  • 第二部分:还没接收,但尚在接收能力范围之内的。
  • 第三部分:还没接收,超过能力范围的。

对应的数据结构像这样:

顺序与丢包问题

还是刚才的图,在发送端来看,1、2、3 已经发送并确认;4、5、6、7、8、9 都是发送了还没确认;10、11、12 是还没发出的;13、14、15 是接收方没有空间,不准备发的。

在接收端来看,1、2、3、4、5 是已经完成 ACK,但是没读取的;6、7 是等待接收的;8、9 是已经接收,但是没有 ACK 的。

发送端和接收端当前的状态如下:

  • 1、2、3 没有问题,双方达成了一致。
  • 4、5 接收方说 ACK 了,但是发送方还没收到,有可能丢了,有可能在路上。
  • 6、7、8、9 肯定都发了,但是 8、9 已经到了,但是 6、7 没到,出现了乱序,缓存着但是没办法 ACK。

根据这个例子,我们可以知道,顺序问题和丢包问题都有可能发生,所以我们先来看确认与重发的机制。

假设 4 的确认到了,不幸的是,5 的 ACK 丢了,6、7 的数据包丢了,这该怎么办呢?

一种方法就是超时重试,当发出去的 TCP 包超过超时时间未收到 ACK,就重新发送。这个超时时间 RTO 的大小和往返时间 RTT 相关,具体的计算公式为:

  • SRTT=0.875*SRTT+0.125*RTT
  • DevRTT=0.75*DevRTT+0.25*(|RTT-SRTT|)
  • RTO=SRTT+4*DevRTT

其中 RTT 表示一次采样的往返时间。

如果过一段时间,5、6、7 都超时了,就会重新发送。接收方发现 5 原来接收过,于是丢弃 5;6 收到了,发送 ACK,要求下一个是 7,7 不幸又丢了。当 7 再次超时的时候,有需要重传的时候,TCP 的策略是超时间隔加倍。每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。

超时触发重传存在的问题是,超时周期可能相对较长。那是不是可以有更快的方式呢?

有一个叫快速重传的机制:当接收方收到一个序号大于下一个所期望的报文段时,就检测到了数据流中的一个间格,于是发送三个冗余的 ACK,客户端收到后,就在定时器过期之前,重传丢失的报文段。

例如,接收方发现 6、8、9 都已经接收了,就是 7 没来,那肯定是丢了,于是发送三个 6 的 ACK,要求下一个是 7。客户端收到 3 个,就会发现 7 的确又丢了,不等超时,马上重发。

还有一种方式称为Selective Acknowledgment (SACK)。这种方式需要在 TCP 头里加一个 SACK 的东西,可以将缓存的地图发送给发送方。例如可以发送 ACK6、SACK8、SACK9,有了地图,发送方一下子就能看出来是 7 丢了。

接收端宕机

正常情况下发送端发送的报文如果没有收到 ACK 会不断重发,但如果接收端宕机了,此时发送端无法收到接收端的消息,发送端会无限重发吗?答案肯定是否。Linux 中的 tcp_retries2 参数表示重试多少次主动断开连接(默认15),如果重试 15 次后还没收到 ACK,客户端会发送一个 RST 然后主动断开连接。

流量控制

流量控制是为了平衡发送端与接收端的速度,避免出现包处理不过来的情况。在协议头里面,有一个窗口大小字段,这个就是用来进行流量控制的。

我们先假设窗口不变的情况,窗口始终为 9。4 的确认来的时候,会右移一个,这个时候第 13 个包也可以发送了。

这个时候,假设发送端发送过猛,会将第三部分的 10、11、12、13 全部发送完毕,之后就停止发送了,未发送可发送部分 0。

当对于包 5 的确认到达的时候,在客户端相当于窗口再滑动了一格,这个时候,才可以有更多的包可以发送了,例如第 14 个包才可以发送。

如果接收方实在处理的太慢,导致缓存中没有空间了,可以通过确认信息修改窗口的大小,甚至可以设置为 0,则发送方将暂时停止发送。

我们假设一个极端情况,接收端的应用一直不读取缓存中的数据,当数据包 6 确认后,窗口大小就不能再是 9 了,就要缩小一个变为 8。

这个新的窗口 8 通过 6 的确认消息到达发送端的时候,你会发现窗口没有平行右移,而是仅仅左面的边右移了,窗口的大小从 9 改成了 8。

如果接收端还是一直不处理数据,则随着确认的包越来越多,窗口越来越小,直到为 0。

当这个窗口通过包 14 的确认到达发送端的时候,发送端的窗口也调整为 0,停止发送。

如果这样的话,发送方会定时发送窗口探测数据包,看是否有机会调整窗口的大小。当接收方比较慢的时候,要防止低能窗口综合征,别空出一个字节来就赶快告诉发送方,然后马上又填满 了,可以当窗口太小的时候,不更新窗口,直到达到一定大小,或者缓冲区一半为空,才更新窗口。

拥塞控制

拥塞控制是为了避免网络中传输着太多的包导致网络拥挤。这里有一个公式,即:发送但还未确认的包要小于等于滑动窗口(rwnd)和拥塞窗口(cwnd)的最小值。前者在流量控制中已经讲过,剩下的就是后者。

拥塞控制有三个时期:慢启动、拥塞避免和快速恢复。

当开始时,cwnd 设置为一个报文段,一次只能发送一个;当收到这一个确认的时候,cwnd 加一,于是一次能发送两个;当这两个确认到来的时候,每个确认 cwnd 加一,两个确认 cwnd 加二,于是一次能发送四个。当这四个的确认到来的时候,每个确认 cwnd 加一,四个确认 cwnd 加四,于是一次能够发送八个。依次类推,此时,cwnd 的增长速度是指数型的增长。

涨到什么时候是个头呢?有一个值 ssthresh 初始为 65535 个字节,当超过这个值的时候,就进入了拥塞避免状态。此时不再是一个确认对应一个 cwnd 的增长,而是一个确认对应 1/cwnd 的增长。我们接着上面的过程来,一次发送八个,当八个确认到来的时候,每个确认增加 1/8,八个确认一共 cwnd 增加 1,于是一次能够发送九个,变成了线性增长。

但 cwnd 不可能无限增长,总有一个时候网络会拥挤,拥挤的表现形式是丢包。发送端有两种方式感知到丢包:超时和收到三个相同 ACK(快速重传)。针对这两种情况的丢包,发送端的处理方式也不一样。

第一种是超时丢包,这种情况下,发送端会认为当前网络非常拥挤,因此会采取激进的限制措施:将 sshresh 设为 cwnd/2,将 cwnd 设为 1,重新开始慢启动。

第二种是三个相同 ACK 丢包,发送端会认为这个丢包是个偶然事件,因此网络并不非常拥挤,采取的措施也会温和一些:sshresh 设为 cwnd/2,cwnd 设为 cwnd/2,又因为返回了三个确认包,cwnd 再加 3。之后进入快速恢复阶段,因为当前 cwnd 仍在比较高的值,这个阶段中 cwnd 也是线性增长。

两种方式的比较如下:

BBR 算法

上文所讲是 TCP 经典的拥塞控制算法(CUBIC)。而在 2016 年底,Google 提出了一个新的算法(BBR),相比 CUBIC 有很大的提升,目前已经集成到 Linux 4.9 之后内核中了。

经典算法的缺陷

经典算法(CUBIC)基于丢包进行拥塞控制,这种方式有两大缺陷:

  • 不能区分是拥塞导致的丢包还是错误丢包:基于丢包的拥塞控制方法把数据包的丢失解释为网络发生了拥塞,而假定链路错误造成的分组丢失是忽略不计的,这种情况是基于当时 V. Jacobson 的观察,认为链路错误的几率太低从而可以忽略,然而在高速网络中,这种假设是不成立的,当数据传输速率比较高时,链路错误是不能忽略的。在无线网络中,链路的误码率更高。因此,如果笼统地认为分组丢失就是拥塞所引起的,从而降低一半的速率,是对网络资源的极大浪费。
  • 引起缓冲区膨胀:我们会在网络中设置一些缓冲区,用于吸收网络中的流量波动,在连接的开始阶段,基于丢包的拥塞控制方法倾向于填满缓冲区。当瓶颈链路的缓冲区很大时,需要很长时间才能将缓冲区中的数据包排空,造成很大的网络延时,这种情况称之为缓冲区膨胀。在过去存储器昂贵,缓冲区的容量只比管道稍大,增加的时延不明显,随着内存价格的下降导致缓冲区容量远大于管道 ,增加的时延很大。因此等缓冲区满再控制流量,会造成很大的时延。

最优工作点

网络工作的目标是满足最大带宽和最小时延,而 CUBIC 这样的算法在缓冲区溢出时才开始工作,实际上已经错过了网络工作的最优点,网络工作的最优点应该是在管道刚被填满时。这是因为随着网络中数据包的增加,我们的有效带宽(BW)和往返时延(RTT)会发生如下三阶段的变化:

1)当网络中数据包不多,还没有填满链路管道时,随着网络中数据包的增加,有效带宽不断上升,往返时延不发生变化。当数据包刚好填满管道,达到网络工作的最优点(满足最大带宽 BtlBw 和最小时延 RTprop),定义带宽时延积 BDP = BtlBw × RTprop ,则在最优点网络中的数据包数量 = BDP ;2)继续增加网络中的数据包,超出 BDP 的数据包会占用缓冲区 ,有效带宽不再变化(达到带宽上限),往返时延会增加;3)继续增加数据包,缓冲区会被填满从而发生丢包。

过去基于丢包的拥塞控制算法工作在第二条蓝线处,但实际上第一条蓝线处,网络中数据包刚刚填满管道时才是最优工作点。

算法基本思想

BBR 算法的基本思想有两个:1)不考虑丢包;2)估计最优工作点(max BW , min RTT)。

上图红色圆圈所示即为网络工作的最优点,此时有效带宽 = BtlBW(链路带宽),保证了链路带宽被 100 % 利用;网络中数据包 = BDP(时延带宽积),保证未占用缓冲区 。

然而 max BW 和 min RTT 并不能被同时测准。要测量最大带宽,就要把链路管道填满,此时缓冲区中有一定量的数据包,延迟较高。要测量最低延迟,就要保证缓冲区为空,网络中数据包越少越好,但此时带宽较低。

BBR 的解决办法是:交替测量带宽和延迟,用一段时间内的带宽极大值和延迟极小值作为估计值。

如何测量有效带宽?
实际上是根据收到 ACK 包的速率来的。比如发送了 1-12 号包,2s 后返回了编号为 6 的 ACK 包,那么有效带宽就是 5*包大小 Mbps。

算法流程

BBR 算法共有 4 种状态:启动、排空、带宽探测和时延探测。其中前两种状态一般发生在连接建立初期,稳定之后会维持在带宽探测状态,当带宽探测连续十秒没有检测到更小的时延后,会进行一次短暂的时延探测。

(1)当连接建立时,BBR 采用类似标准 TCP 的慢启动 ,指数增加发送速率,目的也是尽可能快的占满管道,经过三次发现有效带宽不再增长,说明管道被填满,开始占用缓冲区 ,结束慢启动,进入排空阶段。在慢启动过程中,由于缓冲区在前期几乎没被占用,延迟的最小值就是延迟的初始估计;慢启动结束时的最大有效带宽就是带宽的初始估计。

(2)在排空阶段,指数降低发送速率,此时缓冲区慢慢排空,往返延迟慢慢降低。当往返延迟不再降低,结束排空阶段。

(3)排空阶段结束后,BBR 就进入带宽探测阶段,BBR 在绝大部分时间里都会稳定在这个阶段。这一阶段中 BBR 会通过不断改变发送速率来探测实际有效的带宽。具体地,BBR 会以至少 8 个往返延迟为周期:1)在第一个往返的时间里,BBR 尝试以估计带宽的 5/4 速度发送,如果此时接收到的有效带宽也相应增加,那么 BBR 会把新的有效带宽作为估计带宽,然后进一步增加发包速度,直到检测到的有效带宽不再增加;2)在下一个往返的时间里,为了把前面往返可能多发出来的包排空,BBR 会以估计带宽的 3/4 速度发送,这一步降低速度后如果检测到往返延迟下降了,说明之前的估计带宽偏大,BBR 会把 3/4 的估计带宽作为新的估计带宽;3)接下来 6 个往返的时间里,BBR 使用估计的带宽发包。

可以看到,BBR 在往上探测剩余可用带宽和向下收敛到实际可用带宽时的表现是不对称的:往上探测剩余可用带宽时,如果一次加速成功后,BBR 会立刻尝试再加速,直到可用带宽被占满;而向下收敛带宽时,如果一次减速成功,BBR 不会继续接着减速,而是等到下一个周期再减速。这种加减速的策略也被称为“激进加,保守减”。

(4)如果带宽探测阶段连续 10 秒估计延迟没有改变(也就是没有发现一个更低的延迟),那么就会进入延迟探测阶段。延迟探测阶段持续的时间仅为 200 毫秒(或一个往返延迟,如果后者更大),这段时间里发送窗口固定为 4 个包,也就是几乎不发包。这段时间内测得的最小延迟作为新的延迟估计。由于这段时间内发包速度几乎为 0,所以这段时间内测到的最小延迟基本等于实际的最小延迟。

算法优缺点

BBR 算法的优点前面其实已经提到很多了:1)丢包不敏感,不会因为网络中的错误报文自废武功;2)填满通道而不是填满缓冲区的方式能够让 BBR 算法获取更低的延迟和丢包率。

但是 BBR 也不是十全十美的:1)BBR 周期性的特点会让它对带宽变化不敏感,因为大多数情况下 BBR 在一个周期内只有第一阶段有一次机会去提升带宽,如果网络上同时存在 CUBIC 算法和 BBR 算法,那么当网络中有新的剩余带宽出现时,实时增加的 CUBIC 算法有更大概率抢占到这部分带宽。2)另一点是 BBR 算法的初衷是维持最优工作点,也就是避免数据包进入缓冲区,但是当网络上混杂着 BBR 算法和 CUBIC 算法时,CUBIC 算法会不可避免地填满链路上的缓冲区,这也导致 BBR 算法排空缓冲区的努力成为泡影,性能也会大打折扣。

所以,BBR 算法在纯净的环境中表现优秀,但如果是在一个丢包率不高但有 CUBIC 竞争的环境就会大打折扣。需要一个纯净的网络环境才能发挥其效果。这一点也需要我们牢记在心。

总结

TCP 协议的核心部分在于:三次握手、四次挥手、可靠性传输、流量控制、拥塞控制,掌握好这些知识点对网络编程很有帮助。

参考资料

Linux Kernel 4.9 中的 BBR 算法与之前的 TCP 拥塞控制相比有什么优势?
BBR 拥塞控制算法解析(来自 Google 的 TCP 拥塞控制算法)
令人躁动一时且令人不安的TCP BBR算法