基本机制——seq 与 ack
在 TCP 中为了保证数据的可靠性,发送方会记录每个包的相对顺序(从连接开始到当前包开头的字节数)并在发送的 TCP 包中携带 seq 字段代表这次发送的数据的相对顺序以及 len 字段代表这次发送数据的大小(此时可以计算出 nextSeq = seq + len),当接收端收到数据包之后并且发现数据包中确实存在数据时(ack 包中没有数据,从而避免了无穷循环)需要回复一个 ack 包,其中 ack 等于发送方的 nextSeq 代表在 ack 之前的数据都已经获得了,并请求 NextSeq 及之后的数据
由于TCP 是双向的,因此客户端和服务端都既可以是发送方也可以是接收方,因此客户端和服务端都会各自维护自己的 seq 值并相互根据对方的nextSeq 回复 ack,这里需要注意的是为了节省单独发 ack 报文在传输以及封装上的浪费,往往会在 ack 报文中同时携带想要发送的数据,此时该报文既是 ack 报文,又是数据发送报文,报文对应的客户端或者服务端也既是接受方响应数据又是发送方发送数据
常用计算公式
传输速率
velocity = acked_data / ack间隔时间 = 发送窗口/ RTT(Round Trip Time)(无数据滞留情况)
对于第二个等式,我们可以想象成,在没有数据滞留的情况下,发送方一下子把所有的发送窗口都发送完,那么第二次发送只能等到这一次发送的数据都到达了接收方同时接收方发送的 ack 报文到达发送方时才可以开始
这里可以看到传输速率主要取决于发送窗口和RTT,而不是带宽多大传输速率多大,带宽是传输速率的上界而非决定性因素,因为很多时候到不了上界
发送窗口
发送窗口 = min(发送方拥塞窗口,接收方接收窗口)
在对端的接收窗口小于自身拥塞窗口的情况一般意味着传输过程中没有或者很少有“拥塞”发生,因而拥塞窗口能增长到较高的值,此时发送窗口的上限由对端接收窗口值决定的;在对端的接收窗口大于自身拥塞窗口的情况,一般意味着传输过程中遇到了“拥塞”,因而拥塞窗口进行了适配,也就是往下调整,这往往会使得拥塞窗口变得比较小
在途数据
Bytes_in_flight = latest_nextSeq - latest_ack_from_receiver
在 Wireshark 使用技巧上,当在途数据与接收方窗口相等时,wireshark会分析出TCP Window Full,这里需要注意wireshark中的calculated window 是消息发送方的接收窗口,而非消息接收方的
另外,传输速率也可以直接通过 Wireshark 的 I/O Graph 观察(1. 选中 All Bytes 指标;2. Y 轴的单位选为 Bytes。 )
拥塞控制机制
TCP 拥塞控制主要有四个重要阶段:
- 慢启动:Slow Start,慢是指 TCP 传输的开始阶段是从一个相对低的速度(初始拥塞窗口)开始的,但其实它是指数增长的因此速度很快。在这个阶段,每次 TCP 收到一个确认了数据的 ACK,拥塞窗口就增加一个 MSS,因此拥塞窗口会以翻倍的方式增长。该过程会持续直到触碰到慢启动阈值,英文简称 ssthresh。
- 拥塞避免:传输过了慢启动阈值(ssthresh)之后,就进入了拥塞避免阶段。这个阶段的特征是“线性增乘性减”,每一个 RTT 里,拥塞窗口只增长一个 MSS,直到探测到拥塞(发生重传),然后拥塞窗口直接往下减半。
- 快速重传;TCP 每发送一个报文,就启动一个超时计时器。如果在限定时间内(一般为200ms)没收到这个报文的确认,那么发送方就会认为,这个报文已经在网络上丢失了,于是需要重传这个报文,这种形式叫做超时重传。为了解决重传定时器延时过长的问题,TCP还有另外一种重传机制,就是用快速重传。在这个机制里,一旦发送方收到 3 次重复确认(加上第一次确认就一共是 4 次),就不用等超时计时器了,直接重传这个报文。另外需要注意的是,在wireshark中,被标识为 retransmission 的报文都是抓包端重发的,而被标识为 spurious retransmission(虚假重传)的报文则是另外一端重发的,且该报文是当前端在之前已经收到过。无论是哪一端重传的报文只需要看它的seq与之前哪个报文相同就可以知道重传的是谁了。进一步的,为了避免在丢包之后把丢的包以及之后的数据全部重传导致以接收数据的重复传递,TCP 增加了 SACK 这个特性,SACK 机制可以告诉发送端,虽然接收端的确认号是 n,但是接收端的 TCP Option 里面还有更详细的信息,在那里会通过 left edge 和 right edge 记录在断点后又收到了哪些数据(两个 edge 之间的数据是已经收到的数据)。
- 快速恢复:在遇到拥塞点之后,通过快速重传,就不再进入慢启动,而是从这个减半的拥塞窗口开始,保持跟拥塞避免一样的线性增长,直到遇到下一个拥塞点
基于 TCP 的拥塞控制机制,我们可以通过在传输的某个中间节点上配置当一定时间内接收到一定大小的包后便丢弃之后包,从而使得发送端发现丢包,进而触发重传和拥塞窗口的减半主动进入拥塞避免,从而达到限流的目的
当内核大于 4.9,Linux 默认带有新的拥塞控制算法 BBR,我们可以通过 sysctl 命令,修改内核参数进而使用该算法。TCP BBR 拥塞控制算法是 Google 于 2016 年提出的新的算法,它的开发背景是,当今网络设备的缓存越来越大,导致丢包这个行为不像以前缓存小的时代那么频繁,但是报文延迟的问题比以前严重了。所以,要更加准确地探测拥塞,我们应该更多地关注延迟而不是丢包,并基于延迟的变化作出拥塞窗口的调整。
另外,在 Wireshark 使用技巧上,我们可以用 TCP Stream Graphs 的 Time sequence (Stevens) 小工具,来观察慢启动和拥塞避免等现象(通过观察斜率),包括其中发生的快速重传等行为,都可以在图上看到。