TCP/IP 系列之 TCP 流控与拥塞控制(一)

TCP 流控(flow control)与拥塞控制(congestion control),是我个人认为每个 iOS 工程师都应该熟悉的,价值含量极高的知识点。明白了三次握手,但是不了解流控和拥塞控制背后的设计原理,是不能够在简历上写「精通 tcp/ip」的。

Flow control 和 congestion control 的学习价值高,而且学习过程也会很有趣。掌握整个过程的意义,远超过学习 TCP 协议本身。

不少工程师在工作三五年之后,会尝试提升代码抽象和设计的能力,有些会刻意去读一些架构或者设计方面的书、文章,早些年的时候,流行研究 GOF 的 Design Patterns。这是一个方向,但我个人觉得,更高效可行的方法是研究经典的计算机问题。经典问题里所包含的设计思路,模块划分方式,架构处理等等,都凝结了无数前辈的智慧,理清楚整个流程,再扒一扒设计的细节,抽象的能力会自然而然的随之提升,这是内功心法方面的积累,比记住几个设计模式如何使用,意义要大的多。

TCP 流控和拥塞控制就是这样一个经典的计算机问题。流控机制和拥塞控制机制,也是学习 TCP 协议栈的精华所在。这两个机制又各自包含一系列其他设计,比如 sliding window,stop and wait,retransmission policies 等等。

首先我们要分清楚 flow control 和 congestion control 的区别,这也是很多初学者容易混淆的概念。

Flow control 是站在单条 TCP 连接的维度,目的是让发送方发包的速度,不超过接收方收包的能力,所以 flow control 解决的问题是,如何在接收方可承受的范围内,让单条 TCP 连接的速度最大化。

Congestion control 则是站在整个互联网的维度,让网络里所有 TCP 连接最大化共享网络通道的同时,尽可能的少出现网络拥塞现象,让网络世界里的每一个参与者既公平又高效。

这就是他们的区别所在,但由于二者在学习的时候存在很多交叉的概念,极容易混淆,这也是为什么这篇文章会将这二者先放到一起讨论,明白各自所针对的问题域,再深入细化学习,这样知识掌握才会牢固。

之前的文章里提到过,所谓一个 TCP 连接可以看做是一个完整的字节流。Byte stream 就像自来水管里的水,在流量大的时候,会把网络通道充满,管道满了,有些 bit 就只能等待,等待的过长,就会超时,丢包率跟着上去,网络环境自然就变差啦。大家平时使用宽带,一到晚上就感觉网速下降,其实网速并没有变化,只不过由于人多了,流量变大,而管道容量有限,每个人能分到的流量就有限了。

也就是说,互联网发展到今天,其基础设施建设,相对于需求,仍处于供不应求的状态。这还是在 TCP 协议做了巧妙的流控和拥塞控制之后,如果没有这些机制,根本无法想象今天的互联网究竟会是怎样一种体验。

拥塞控制

我们先来看看 congestion control,里面谈到的一些概念 flow control 中也会出现。拥塞控制的目的是,让互联网世界里的每一个 TCP 连接都能高速的沟通,同时又以最文明的方式,尽可能的不去占用过多的管道资源。

如果让你来设计,你会怎么做呢?

慢启动(slow start)

congestion control 的第一步就是慢启动,通信的双方在建立连接的时候,先慢慢的发包。比如我们可以先让发送方发一个包,等这个包被 ack 之后,我们再发 2 个包,这 2 个被 ack 之后再发 4 个包,以此类推,让一次所发的包数量慢慢增加,这就是慢启动。打个比方,这个过程非常像大家使用自来水时,慢慢拧开水龙头。

为什么要这么做?为什么不在一开始就猛的拧开水龙头?一开始就大流量发包,这样对一条 TCP 连接来说是快了,但大家都这么做,整个互联网世界里的就很容易出现瞬时的暴增,带宽资源的分配就难以合理均衡,拥塞现象更容易发生。而慢启动,就是让每一个 TCP 连接做个文明的绅士,慢慢的拧开水龙头,只有在双方成功建立一个健康的连接,且存在的大流量发包需求的时候,才将流量提升上去。

和 TCP 的慢启动相比,UDP 显得非常不文明,UDP 会在连接的一开始,就将流量开至最大,这也是为什么现在有不少基于 UDP 的类 TCP 方案,可以避免 TCP 慢启动所带来的影响,这种做法虽然高效却不值得称赞,是对现有公平秩序的破坏,所以有些运营商会对大流量的 UDP 采取惩罚措施。

慢启动的影响确实会比较大,它不仅仅发生在建立连接的初期。一条 TCP 通道在健康通讯一段时间之后,如果丢包率突然上去,导致大量的包发送超时,Slow start 有可能会被重新启动,这也是为什么有时候明明带宽足够,如果丢包率过高(网络不稳定),网速却上不去。

我们再深入看一下 slow start 的细节和一些基础概念。

为了简化学习模型,我们假设一条 TCP 通道里,一方只发送,另一方只接收,当然一般实际场景里,任何一方既是发送者又是接受者。

Window

谈 TCP 离不开 window 的概念,有 congestion window,receive window,sliding window 等等。window 是以 tcp segment 数量为单位,我们可以说当前 window 值由几个 tcp 包构成,而当我们说 window size 的时候,又是在说一个 window 所包含的字节数。window size 除了和 tcp segment 的数量有关之外,还和单个 tcp segment 的最大 size 有关,即 MSS 值。

MSS

MSS 值,即 maximum segment size,是单个 TCP 包所包含的最大字节数。我们之所以要限制单个 TCP 包的大小,是受限于 TCP/IP 协议栈更下层的 MTU 值,至于 MTU 是啥,就留给读者自己去扒了,MTU 也是非常重要的概念。

发送方和接收方通讯的时候,会采用一个共同的 MSS 值,这个 MSS 值是取发送方 MSS 和 接收方 MSS 二者的较小值,一般是 1460,刚才提到的 MTU 值一般是 1500,大家可以扒一扒二者的关系。

SMSS

发送方的 MSS 值即为 SMSS(Sender’s MSS),这个值要单独拿出来说,是因为和下面的 CWND 相关。

CWND 和 RWND

发送方的 Window 大小称之为 CWND(congestion window),接收方的 Window 大小称之为 RWND(receiver window,或者 advertised window)。CWND 表示当前发送方可以发送多少个 TCP 包,而 RWND 表示当前接收方还能接收多少个 TCP 包。

值得注意的是,CWND 是一个发送方本地的值,并不会在网络上传输。而 RWND 则是由接收方告知发送方的,是存在于 TCP 包的协议中,会通过网络传输。大家也可以自己研究下,RWND 在 TCP header 中是哪个字段,其大小又是如何计算的。

Transmission Window

Transmission window 的大小是真正表示,发送方可以一次发送多少个尚未被 ack 的包。这个值是一般先取 CWND 和 RWND 的较小值,之后会动态的调整变化,slow start 阶段会变化,congestion avoidence 发生时也会变化。

慢启动过程

连接刚开始建立的时候,发送方有一个初识的 CWND 值,这个值在一开始是由 SMSS 的值决定的,二者之间有一个简单的公式关系,可以参考 RFC 5681。这个初始值直接决定了一开始,发送方可以发送包的数量,这个值对于初期的性能影响比较大,发送的包越多,自然请求的响应也就越快。那么 CWND 的初始值到底是多少呢?

虽然 RFC 对于 CWND 的初始值有一个公式建议,但不同的 TCP 实现,initial CWND 值并不一样。有些云服务商为了提升性能,特意设置了一个较大的 CWND 值(比如 10 或者更大),这样在三次握手之后,立马就可以一次发送 10 个以上的 TCP 包。

我们假设发送方在三次握手,成功建立连接之后,一次先发送了 2 个 TCP 包,慢启动的规则是,每一个被 ack 的包,可以将 CWND 的值 +1。即是说,2 个 TCP 包被 ack 之后,发送方继而可以发送 4 个包,这 4 个又被 ack 之后,又能发送 8 个,以此类推,逐渐成倍的增加 CWND 值。

CWND 值会不会一直增加呢?显然不会,当通道的吞吐量达到一定量之后,CWND 的值就会受其他因素影响,比如进入 congestion avoidence 阶段,不过这是另一个话题啦。

慢启动就先介绍到这里,这篇文章的信息量也有不少了。

总结

TCP flow control 和 congestion control 涉及的知识点比较多,后面还会有更多的文章来介绍。大家也可以这两个大方向为 TCP 协议的学习框架,自己多去研究两种机制下的各种细节,如果能把整个流程烂熟于心,其义自见。

欢迎关注公众号:MrPeakTech


Hosted by Coding Pages