TCP自时钟/拥塞控制/带宽利用之脉络半景解析

0.说明

搬家公司的人很多都穿皮鞋!Why?

这个题目不是很明确,而且这个文章比较长,也算是我的一个阶段性总结,既然是总结,就不必为题目而纠结了。在端午假期的最后来做这个总结也实属不易(假期前两天加班,没有完成预期的计划,低落),记得很早以前写那篇《TCP协议疑难杂症全景解析》的时候跟现在一个心情。翻翻以前的记录,写那个的时候是2011年的7月初,小小才刚刚半个月,如今小小已经马上5岁了,时间过得真快啊,弹指一挥间!!回首过去的五年间,最累的时候是在2011年中到2014年初这两年半的时间,有了小小之后工作更加努力,然则一个产品做到了让人感觉遥遥无期的地步,好几次想离职换个工作,我可是个研发啊,事实上我那段时间学会了机器上架,也理解了更多的网络方面的更深的东西,付出嘛,总会有回报,这是必须的!无数个夜晚漫步在寂静的陆家嘴(陆家嘴作为整个中国的CBD怎会寂静?因为夜已深!),打不到车,从冬天到夏天,再到冬天...公司年终评奖的时候,大家都是拿数据说话,谁的数据好看谁赢,豪言壮语高谈阔论无卵用,我的数据当然也比较帅,只是让我获奖的不是研发运维方面的数据(人家都很棒!),而是出勤方面的,比如xx天加班,xx'次夜间出勤,xx''次节假日出勤...等等吧,我是最讨厌加班的人,却因为这个拿了个大奖,天大的笑话啊,然而这种尴尬的场景总是一再在我身上上演,也是崩溃又欲哭无泪啊!

        也是在那段时间,我学会了随时随地支起摊子就能工作这样的本事,因为我的时间在那段时间被打得很乱,一天在公司安心研发的时间几乎为0,跑用户现场,生产环境上现场编程,这压力得有多大,而且还跟内核有关,万一一个panic...上线了又要值守,之后又是各种会议,前向总结,后向计划,晚上八点从用户现场下班准备回家却又被领导叫回公司开会,开完会大半夜的又必须完成当日的决议...为此,我蹿坏了前台旁边的木头凳子...后来我占领了公司的一间会议室,起初我是在那搭测试环境的,几十台设备摆在会议桌上同时开机压测颇为壮观,我以测试修bug为由长期呆在那里,也不管什么辐射不辐射,噪音不噪音,久而久之,那里就成了我的独立办公室了,不外出的时候,呆在那里听着机器的轰鸣声敲着代码十分之惬意,外出的时候,地铁站,公交车,用户现场,机房,家里,哪里都可以展开工作,因为有项目津贴,没网络时我就用手机开热点...于是乎我忘掉了自己的工作座位,那已经成了垃圾堆,甚至有同事以为我离职了。两年多过去了,感觉自己成长了很多,虽然没有整块的时间做研发,但确实能力有了很大的提高,所以我还是很感谢那两年时间遇到的每一件事,每一个人的。
        为什么突发感慨说往事呢?可能是因为最近的工作与TCP有关,各方面场景包括来自各方的压力与那段时间类似吧,当然好处是现在不用外出了,也不会夜间出勤之类的了,最重要的是时间,比3年前充裕了很多。不管怎么说,软件硬件都在进步嘛。爆炸!
        低落中无论如何也不能让这个假期如此假惺惺地过去,所以狠下心来再搞半个夜晚,算是《 TCP协议疑难杂症全景解析》的续吧!

让我们以一个问题开始吧!
TCP依靠ACK来维持一个时钟,该时钟驱动发送端数据的发送,问题是,发送端应该发送多少数据呢?

TCP释义

TCP是一个端到端协议,其节点分布在全世界的每一个角落,彼此不知其它连接的存在,因此无法进行全局意义上的时钟同步来驱动合作式的网络行为,与之不同的是,IP层虽然是无状态无连接的,但却可以进行这种分布式合作,比如各种动态路由协议就是合作的,依靠各种交互进行同步。这就意味着,端到端的TCP协议必须完成并处理好两件事:

a).由一个自时钟来驱动自己的行为,比如发送数据,“停-等”等等,这个是通过ACK & 超时定时器(混合型自时钟)来实现的。
b).比如依靠反馈系统自适应完成与其它连接的合作关系,且这种关系必须是合作的,不能是抢占的,这个是通过拥塞控制来实现的。
本文以下的篇幅围绕上述a)和b)逐步展开。

声明

网络分析包括很多的元素,比如协议分析,性能分析,逆向,行为分析等,协议分析比较简单,性能分析则非常复杂,涉及到比如马尔科夫过程,泊松分布这类让大多数人望而却步的东西,因此本文不包括这些,仅仅分析TCP的行为以及这行为背后的原因。

1.端到端的滑动窗口机制

TCP是一个端到端的协议,它在设计上不考虑数据经由的中间网络,你可以将中间的网络抽象成一个以太构成的管道,拥有无限的容量,拥有极限的速度,那么理论上只需要在发送端和接收端之间进行交互即可,TCP采用了滑动窗口机制来做流量控制,这个我不再解释,能看到此文并且看完的,都是懂TCP/IP的,而滑动窗口则是TCP的基础中的基础。

2.现实的网络状况

这里说的现实指的是两种现实,一种是历史的现实,一种是当前的现实,或者还有未来的现实。在TCP/IP早期,带宽几乎是独享的,除了有些链路噪声之外,你可以将其想象成完全的以太。因此网络中不会存在拥塞,因此TCP作为一个纯粹的端到端协议,工作的非常好。
        时间进入到了20世纪80年代末,网络出现了拥塞,或者仅仅是有先见之明的猛人意识到了互联网将爆发,网络将遭遇拥塞,因此TCP不能再保持端到端的纯粹性了,它必须处理拥塞,即它必须可以意识到拥塞,并对拥塞做出反应,好吧,就是在这个时候,TCP引入了拥塞控制机制,并且全世界对这一机制的研究持续了将近30年。
        进入21世纪,几乎所有的大城市都遭遇了大城市病,其中最令人不爽的就是道路拥堵导致的各种限,限外牌,限号,...伴随各种限的就是人们的各种抢,抢什么呢?当然是资源!实际上,此时的互联网上遭遇了同样的情形,我记得很早之前在一本杂志上看到过有人预测互联网即将崩溃的文章,但事实上它依然在如此拥堵的环境下工作的很好。原因就在于TCP的拥塞控制机制是自适应收敛的,不需要任何机构的管理,就可以保持互联网的恒稳。总之,目前,TCP一切安好!
        时间即将进入未来,TCP在中国获取不再那么令人乐观,伴随运营商的限限限,以及BAT等巨头互联网厂商的各种垄断,人们连抢抢抢的权利都没有了,事实上,带宽都被巨头们抢光了。
        在我们的土地上,任何美好的约定都可以不被遵守,任何契约都可以打破。TCP在中国也同样。
        人们不必再遵守TCP的公平收敛约定,于是出现了超级多的暴力方案,这就好像在高速公路上我发现有人比我跑得快或者拥堵的时候,我任意变道并直接开车把前车撞了,然后打个电话再弄来一辆车就好了,因为我有钱,我有很多车,我不在乎成本,然而被撞的车可能是人家几年的积蓄...这难道不是一个道理吗?什么一个数据包发两遍甚至三遍,发生拥塞时保持大窗口,甚至忽略拥塞控制机制直接暴力发送...因为这是在中国,所以这种军备竞赛如火如荼的进行着,人们不需要懂什么TCP协议,甚至都不需要懂网络的基本概念,完全可以把这事做的很黄很暴力,因此,美国的作者所谓的互联网崩溃,应该说的就是我们国家吧...

3.平稳的ACK时钟

我们知道,TCP是一个持续的数据流,就像是水流一样,起码在设计上,理想情况下应该如此。如本文之首所述,TCP流的发送依靠ACK来驱动,如果TCP流的发送想保持连续,就依赖ACK会持续的到来,持续的ACK导致持续的TCP数据流,持续的数据流在接收端产生持续的ACK...
        从最简单情况开始,请忽略掉窗口,假设这个时候我只想设计一个持续的数据流发送的机制,没有端到端的流控,也没有网络拥塞控制,仅仅是一个持续的流,该是个什么样子呢?

我觉得应该是下面的样子:




我将其看作是平稳的ACK时钟。这是本质的东西。为什么我说构造这个自时钟机制是本质的呢?因为任何事都依赖一个心跳来驱动进展,这是最基本的,就连胚胎也是先有的心跳,后发育的大脑。所以,我认为要想理解TCP,首先理解这个自时钟十分必要。

        在接下来的篇幅中,我会逐步解释控制拥塞窗口的慢启动是如何加到TCP标准中的,以及它后来是如何导致平稳的ACK时钟发展成现在实际的样子的。

4.TCP慢启动与拥塞控制释义

4.1.说明

写文章之前,提纲这么列的,现在感觉没什么好说明的。

4.2.ACK时钟启动的问题

如上一节所述,如果有一个平稳的ACK时钟,TCP发送端将会由该时钟驱动将数据流源源不断地送进网络中,这些数据流在被接收端收到后会产生ACK,这些ACK作为时钟信号反馈到发送端,循环不止。
        然而,问题是,这个时钟如何启动呢?
        这是一个先有鸡还是先有蛋的难解问题,因为数据的发送依赖ACK时钟,而ACK时钟的产生又由数据的发送来触发。TCP是这么解开这个僵局的。它依赖三次握手中的获取的接收端(暂且只考虑数据单向传输,双向传输与此一致)的“通告窗口”这个字段信息定义的数据量,假设其为W,在数据开始传输的时候,首先传输W字节的数据,期望由这些数据启动ACK时钟。
        作为一个端到端的协议,这无可厚非,这也是最早期TCP版本的做法,用滑动窗口来启动数据发送,进而启动时钟,然而问题恰恰出在端到端之外,这是TCP在最初被设计的时候没有考虑到的。接收端的通告窗口仅仅受制于接收端的缓存,然而,同样对“能发多少数据”进行限制的还有网络本身。

4.3.网络容量对TCP的影响

以上的讨论当以一个端到端应用程序的视角来看的话,毫无问题。然而它忽略了TCP流经由的中间网络的状态,作为必经之路,网络状态极其重要!最初的TCP将网络看成了一个黑盒子,问题在于它不是黑盒子,它不是假想中的无限的资源,随着网络技术的发展,人们越来越认识到这一点,网络越来越被看成是一种需要争抢的稀缺资源,再也不是早期(20世纪70年代)那种充盈的资源了!
        网络有自己的容量,会有大量的数据流与你一起在网络上玩飚车(你能想象上世纪60年代的中国城市限外牌,限号,拍牌等管制吗?时代不同了),一个TCP流的数据包在网络上随时可能遭遇不测,总之,网络会影响接收端的数据接收,进而影响ACK的产生,最终ACK时钟的改变影响发送端数据的发送。因此,网络不是一个黑盒子,TCP的发送端必须有能力察觉到网络拥塞,并且对之做出必要的反应。

4.4.带宽时延乘积

带宽时延的乘积表示一个可以维持源源不断ACK流的最合适的管道容量,也就是说,只要发送端维持这个乘积这么大个窗口,就会收到源泉般的ACK流。

4.5.带宽利用的点滴

在前些天写的一篇《 TCP核心概念-慢启动,ssthresh,拥塞避免,公平性的真实含义》中,我描述了发送窗口如何逼近ssthresh的两个阶段,第一个阶段旨在填满到达接收端的整个路径,第二个阶段旨在制造一个源源不断的ACK时钟滴答,也就是说,第二阶段的目标是等待第一个阶段发送的数据的ACK到达发送端,在此期间,发送端仍然可以继续发送数据,整个第二阶段,发送端到接收端之间的路径持续满载,发送端持续发送,接收端持续消化。
我再次把图贴一下:




可以简单看到,两个阶段发送的数据是一样的,这就是ssthresh的本质。然而那篇文章的目的是阐述ssthresh的本质含义,而不是阐述TCP慢启动,拥塞控制的脉络,所以做了极大的抽象,它将发送端到接收端之间的网络看作了一个均匀的管道,而事实上,那篇文章中我也简单提到了,这个管道的不仅仅包含网络本身,还包括中间节点比如路由器的队列。这些中间节点都是存储转发的设备,即带有队列的先存储后转发的网络设备。
        如果我们仔细观察这个过程的话,就会发现,在这个模型中ssthresh根本就没有用,这个模型只是解释了ssthresh是怎么算出来的。如果我们总结一下这个简单的模型,就会发现,它讲的是,驱动TCP发送数据的力量有两种,第一种是没有收到ACK之前,TCP会依靠外部时钟持续发送数据,在收到ACK之后,由ACK驱动发送数据,总之,TCP始终可以不断发送数据。然而现实情况下,并不是这样。
        我们可以说,ssthresh表示的到达接收端的整个管道的容量由以下两部分组成:
1).带有时间延展性的网络通道本身
2).不带时间延展性的存储转发设备队列
它并不仅仅是1)所表示的那种均匀的管道容器!
那我们接下来看一下什么叫做时间延展性。

时间延展容器的解释

简单理解,只要一批数据在不停向前走,那么后面总是可以空出新的位置来容纳新数据,按照容器构造的理论,这种容器是动态容器,容器简单理解,只要一批数据在不停向前走,那么后面总是可以空出新的位置来容纳新数据,按照容器构造的理论,这种容器是动态容器,容器的容量取决于最前面数据的速度。这种容器有个特性,只要数据头的行进速度大于等于新数据注入的速度,容器永远都不会满,甚至还会逐渐被清空。然而一旦情况反过来,比如新数据注入的速度大于数据头的行进速度了,就需要一种不带时间延展性的队列来临时存放多出来的这部分数据,在计算机网络的历史上,这种容器叫做存储转发设备。这种“不带时间延展性的存储转发设备队列”不具备空间延展性,因此也叫做“空间排队容器”。

空间排队容器的解释

既然知道了时间延展性,那么网络中的每一个存储转发设备就相当于一堵时间墙了,既然不能在时间上做延展,就只能在空间上做延展了。这种容器的典型特征是它的容量永远是逐步减少的,就像一个瓶子,只要你不从瓶子里倒出一些东西来,它的容量会越来越小,因此它只能作为一种缓冲存在,应对一些突发情况。在新数据注入的速度大于数据头行进或者被消费的速度时,需要这种“空间排队容器”临时存储多出的数据,在多出来的数据被缓冲在队列容器中的时候,整体上有两种期待,首先期待发送者降低其发送数据,其次期望这次排队是一次突发事件,而不是常态。
        总结一下,我们发现,时间延展队列是一个逐步向前清空的容器,以容纳更多的数据,而空间排队容器是一个逐步向后积累的容器,逐步可容纳越来越少的数据。

道路交通的解释

在进一步将排队论的方法引入到网络拥塞情景中之前,我们来看一些我们生活中的现象,这些现象有助于我们理解下面关于TCP发送pace的细节。我想说一下这个端午节假期的最后一天下班时的两个情景。
        大多数时候我都坐6点的公司班车下班回家,假期前一天由于临时开会没有赶上班车,于是我就去坐地铁,发现高新园站的进站口排起了长队,如果你观察稍微仔细一些,就会发现,队列的头部永远都是匀速出队的,没有任何停顿,然而队列尾部的人们却几乎都是快步走几步,然后停顿十几秒,然后再快步走。这是为什么?由于我不想在一帮程序员面前过于装,我也就默默地排着队,时不时看看手机,但我不会一直盯着,我把我本要进行的测量给了第二个故事。
        到了世界之窗,我下了地铁,出站,打上了一辆的士,告诉师傅去黄贝岭,走当时拥堵的滨河大道!师傅劝我坐地铁,我心里默念,我这是花自己的钱研究排队理论,装逼任性,关你何事...一直走到皇岗立交那里,开始拥堵,我让师傅变道到最右侧,这里观测辅道比较容易,同时开始计时,并观测在辅道上的车辆,我会记住牌照和它超过我这辆的士的时间点。【 附:深圳在快速路的设计上比较特别,它的辅道一般和干道是平行的,并且跨线立交都是与之垂直的,即很多辅道上也没有红绿灯,只有单独的右转道,你可以很容易从这个出口出去,然后飞速到下个出口进去,这样就可以绕开主道上的一大段拥堵。上海那边就不是这样,上海都是高架路,你无法观测地面道路的路况,同时一旦下到地面道路,一般都会遭遇红绿灯或者斑马线,北京就更不同了,一般都是主线跨线,辅道红灯停....广州不了解,等有时间去了再研究吧】随着车流的增加,辅道开始缓慢但是匀速行进,因为很明显,主道和辅道在下一入口有个并道的过程,然而主道和辅道的行进模式却完全不同,不管是一走一停,走得快停的久,还是缓慢匀速行进,但是基本上二者的平均速率一致,这在统计学上有什么意义呢?
        这就是统计波动的影响。再比如那个“等车悖论”,你不相信也不屑于关注,但却是真的。

附:关于学习

补充一些关于学习的问题,早年对通信比较感兴趣,也看过一些关于QoS的资料,觉得数学非常有用。现在很多人对网络行为本身关注不够,只是把大把的精力用在了关于协议的定义以及代码的实现上了,个人觉得这样做虽然可以短平快的有一些产出,但是终究会遇到瓶颈。这也许跟学院派与工程派的斗争有关吧,在校园里流行“数学没有什么用”可能仅仅是站在利益角度考虑的,因为学数学的毕业没有学编程的更容易找到工作,且工资普遍不高,再加上国内BAT,中兴,华为等巨头企业几乎都是以产品和服务为导向(华为要稍微好一点),将各大985,211变成了其后备基地或者说技校,很多毕业生一进公司就开始永远的进入了工程或者产品领域,至于说数学,物理这些基础学科有什么用,也仅限于偶尔搜到一些国外的论文什么的能大致看懂。
        所以说,如果你在公司里面跟别人去讨论这些东西,基本没人理你的,他们可能更关注如何实现,所以“talk is cheap show me the code”。以TCP为例,没有人愿意花点时间去研究一下网络行为,因为这“与端到端的TCP无关”!一个写socket的只关注与其接口的协议栈,而且目前由于各种框架,中间件的出现,直接写socket的也不多了...就算有人一剪子把网线剪断,还是会有大把的人去排查协议栈,至于说扯到什么排队论,马尔科夫链什么的,无异于隔行如隔山。这就是现状。不管怎样,我个人还是觉得,如果你只是想做一个工程师,那就写一手好代码,调一手好参数,如果你想成为经理,我不知道该怎样(也许是搞一双好皮鞋吧...),也不关注,如果想成为专家,还是关注一下全局并以数学的眼光看整体更好吧。

4.6.链路排队的根源与解释

有了对上述对空间排队容器的理解,我们可以理解链路排队了,鉴于分组交换早期都是出口排队,我们基于下面的图做讨论:




在上图中,我们已经知道ssthresh的值了,那么我们可以直接发送这么多的数据吗?理论上不是可以吗?不是说ssthresh就是链路的容量吗?发送这么多不是不会发生危险吗?完全正确!
        但是要知道,理论上一次性发送ssthresh的数据是建立在链路容量完全是同等均匀的时间延展容器组成的基础之上,容量随着均匀的时间会均匀拓展,然而现实中的端到端更像是上图中的漏斗结构,漏斗两遍对于相同的流量的时间流逝速率是完全不同的,为了平滑这个差别,就需要一堵时间墙来减缓时间流逝快的一方的时间流逝速率,这堵墙就是队列!!
        在我小的时候,那时即便是城市里的酱油也不是按瓶卖的,而是由酱菜厂的工人推着车子一边叫喊一边卖,你要买酱油的话必须自备酱油瓶,然后工人会用一个漏斗一勺勺地将酱油灌入自家的酱油瓶里,我简单的画了个图:




如果你是工人,假设漏斗超级大,你会用以上哪种方式呢?即便你没有卖过酱油,我想也会用一次性倒入的方式吧。由于最终酱油充满你的酱油瓶的时间由漏斗的细颈部决定,其实两种方式倒酱油的时间是一样的。那你为什么独爱第二种呢?因为你将酱油倒入漏斗后就不用管了,你可以去收钱,找零,服务别的买醋,买咸菜的人,如果使用第一种方式,你就必须一直拿着这个大勺子,什么别的也干不了了。然而这不是事实的全部,漏斗一般都很小:




这时你还会一次性把酱油倒进去吗?...
定性分析后,我们来定量化。还是上面那个图,我们假设漏斗的细颈容量和宽部分容量的和正好等于大勺子的容量:




这不明明可以容纳那勺子酱油的啊,为何会溢出呢?答案就在于那堵时间墙!勺子倒入漏斗宽部的速度快,从细颈流出的速度慢,相同的流量二者时间是不同的,也就是说,当你一次性把一大勺子倒进去的时候,同样的流量还来不及从细颈流出,导致了溢出!
        既然漏斗的总容量和勺子的容量相等,漏斗是可以充满的。我们知道让漏斗充满可以让买酱油的腾出手去干别的,我们还知道一次性倒进去会溢出,那怎么让漏斗充满呢?答案很简单,那就是一点点慢慢地往漏斗里倒!
        由于漏斗的细颈即便再细也有时间延展性,会不断有酱油匀速滴落,而勺子的容量与整个漏斗的容量相同,这说明一勺子酱油永远也无法充满漏斗,那怎么办?很容易,继续用勺子从酱油缸里面舀酱油....
        慢启动啊慢启动,这就是TCP的慢启动!为什么要慢启动,而不是一次性充满ssthresh的原因。还用讲慢启动的原理了吗?所谓的“慢”并不是真正的慢,看跟谁比了,跟一次性发出整个ssthresh数据相比,是慢,但是增窗的速度,很快!总之,所谓的慢启动就是利用网络的时间延展性逐步打开窗口让数据包充满整个网络的过程!
        后面在4.9节中描述拥塞避免的时候,会进一步分析排队。和这里分析的存储转发缓存的视角不同,那里将从排队论的另一个侧面来分析。

4.7.慢启动与拥塞窗口

还是用卖酱油的例子,这个例子简直太好了,足以解释一切。
        回过头来看理想中端到端意义上平稳的ACK时钟,这正是瞄准漏斗细颈倒酱油的方式,此时我们可以忽略那个大勺子,而是把大勺子想成是一个持续的流,这个流的速度和酱油滴落到瓶子里的速度相同,然而现实中,卖家不会这么卖,而是采用了把漏斗充满的方式,并且是逐步的充满,这正是TCP在现实中的工作方式,开始的时候,慢启动逐步打开窗口。工人们小心地探测倒入漏的酱油量,她不断观测瓶子里已有的量,然后在勺子里的酱油倒完后酌情的用大勺子从酱油缸里舀适当的量一次性倒入漏斗。
        我们已经知道,卖家不会按照酱油从细颈流出的速率去匀速匹配酱油倒入漏斗的速率的,而总是一次性的倒入足够的酱油,这个量其实就是窗口的概念,对应于TCP就是拥塞窗口。TCP在慢启动阶段打开窗口的方式多少有些随意,但是确实很合理。每收到一个ACK,作为一个时钟信号,表示一个数据包离开了网络,此时理所当然地可以再发一个数据包,另外作为一种ssthresh仍未达到的信号,此时可以再打开一个窗口,也就是说,每收到一个数据被ACK的信号时,发送端可以发送两个新数据。用这种方式,逐步将整个漏斗(包括宽部和细颈)填满。
        马上要结束慢启动的讨论了,作为一个和倒酱油例子的真实实例的类比,我盗大师Van Jacobson的图两张,来展示一下不使用慢启动直接放入突发数据(酱油漏斗溢出)和使用慢启动的区别:
不使用慢启动的情况




使用慢启动的情况




漏斗被填满了,慢启动也就结束了,这个时候该怎样呢?此时最好的情况就是保持漏斗的持续充盈,保持漏斗充盈的本质就是数据守恒,对端收到一个,发送端发出一个,然而明确知道对端收到一个数据的办法就是应用ACK时钟。因此现在的目标就是造一个持续的ACK时钟,该ACK时钟的滴答频率和数据发送的频率完全相等。为了达到这个目的,依然需要继续打开窗口,以加快ACK的频率,但是打开的方式要有所变化。慢启动阶段窗口打开的方式是ACK一个发送两个,这背后除了数据守恒理所当然要发一个之外,还有漏斗未满这个信号,当漏斗满了之后,则收到一个ACK仅仅表示可以利用数据守恒再发一个。此时为了保持不过载,需要保证整个窗口的数据全部被ACK了以后,才能打开一个新窗口。直到什么时候呢?TCP怎么知道网络已经过载了呢?明确地说,TCP不会知道,虽然理论上整个窗口的值应该是ssthresh的2倍,但那只是理论上的推论,事实上ssthresh本身就是推测出来的,这个将在下一节说明。如果我们明确知道了ssthresh,并且知道ACK沿着原路返回,我们就可以在窗口达到ssthresh之后再执行一次慢启动策略,即收到一个ACK打开一个窗口,这样在一个RTT内就会把窗口打满,造就一个持续的ACK时钟,此后就可以收一个ACK发一个数据去迎合数据守恒了。但是现实中,这种策略往往会带来大量丢包,其原因在于,实际的网络是一个复杂系统,没有任何的可预测性。
        到此为止,慢启动早已结束,源泉般的ACK也已经形成,说明ACK时钟可以开始工作了。此时,窗口可以固定下来了,比如此时的窗口是W,接下来的发送完全依赖ACK时钟以及数据守恒,收到一个ACK,此时Inflight为W-1,为了填满窗口,可以发送一个数据包,接着收到3个ACK,说明Inflight为W-3,此时可以再发3个数据包...数据的发送并不是整窗为单位发送,而完全是基于ACK时钟平滑发送的。

4.8.慢启动Hystart的重要性

以上的讨论,我们的前提是ssthresh是已知的,然而在现实中,这个值在TCP连接刚刚启动的时候是未知的,需要逐步地通过慢启动来探测。
        在一般的TCP实现中,总是将ssthresh初始化为一个巨大无比的数值,它基本上就是无用的。假设网络拥有一个最大容量W,只是我们不知道而已,而为了造持续的ACK流,事实上TCP的发送拥塞窗口可以达到2*W这么大,我们知道TCP慢启动阶段是按照指数规律来打开窗口的,只要不发生丢包(即没有探测到丢包信号,比如RTO超时,三次dupACK之类),会持续这个窗口打开的过程(因为当前窗口值永远小于一个巨大无比的值),假设当前窗口已经到2W-1了,即只比最大容量小一个,发送端会在接下来的一个RTT内收到2*W个ACK,这W个ACK会打开多少窗口呢?很显然会额外打开2*W个窗口,加上之前的2*W个,总的窗口大小将会飙升到4*W,这会造成一半的丢包!
        因此在慢启动阶段通过某种手段去探测当前的网络容量是十分必要的,具体采用那种方式,方法太多了。
        不过,我这里倒是有一种比ACK train更好的“教学”方案,可以最快的速度达到窗口的峰值。那就是监控ACK到达的频率,只要ACK到达的频率与数据平滑发送的频率相等了,就等于说ACK时钟已经形成了,此时已经达到最大的窗口值了。为什么叫做教学方案呢?因为这个方案并没有考虑网络拥塞,仅仅是为了帮助理解概念,完整的故事正是下一节将要讲述的。

4.9.慢启动后的拥塞避免

在文章《 TCP核心概念-慢启动,ssthresh,拥塞避免,公平性的真实含义》中,我说过慢启动后拥塞窗口是怎么逼近ssthresh的2倍的,但是没有说为什么这样,所以这个小节作为那个问题的补充。
        我们从两方面来分析这个原因,一方面是以产生平稳的ACK时钟为目的,另一方面是以避免拥塞为目的,不管怎样,大师Van Jacobson都管这个阶段叫做术语拥塞避免。

1).形成平稳的ACK时钟

我们已经知道,TCP是个自反馈系统,依靠ACK时钟来驱动数据的发送,在此我们不考虑拥塞,仅仅从端到端加长肥以太管道的角度来分析拥塞避免状态按照加法打开窗口的原因。在慢启动阶段,再一次重复,其目标是在漏斗不溢出的前提下使得数据可以填满到达接收端之间的整个管道,假设ssthresh已知且精确,因此发送端只要收到ACK且窗口小于ssthresh,就可以打开一个窗口,然而在窗口达到ssthresh以后,其目标就变成了形成稳定的ACK时钟流,因此从本文最初的配图上可以直观的看出,目标就是让数据线和ACK反馈线首尾闭合成一个圈,要强调的是,慢启动结束时,这个圈还差一半没有闭合!因此如果继续走慢启动的流程,一轮ACK就足以闭合这个圈,然而我们知道慢启动指数增窗,一旦发生意外(任意丢包),将会可能丢失半窗的数据,考虑到现实并非理想中的端到端长肥管道,这个意外是经常发生的且代价巨大,因此采用了加法方式逐渐闭合这个圈,如下图所示:




但是从TCP的标准和实现上均可以看出,这个过程并没有在窗口达到ssthresh的2倍时停止。这是因为现实的网络中,没有任何可信的主动信号通知拥塞,连接清空等事件,而主流的方案恰恰就是通过丢包来意识到数据发多了这个事实的。更复杂的是,随着新连接的建立和清除,已有连接的可用带宽会非常波动,并非理论上的ssthresh的2倍这个关系。而这些,我将在下面说明。

2).避免拥塞-加性增,乘性减

刚刚第一个方面说的是理想和理论,这里我们回到现实!
虽然在理论上,ssthresh和剩余带宽之间有精确的2倍关系,然而实际上,很多值都是合理的,在同一个连接中,只要可用带宽是ssthresh的常数倍(为了闭合圈,常数肯定大于1)即可。
        这个我们中TCP公平性收敛图上也可以看得出来,只要是加性增(大约一个RTT增加固定的1),乘性减(系数无所谓,不一定是1/2,只要小于1即可),就可以收敛到公平。为什么要这样?
        我们不得不再次回到存储转发设备队列的敏感性脆弱性话题上,但是不再用基于统计学的排队论这个调调来阐述,而是用一种更加容易被人理解的方式来说明。

        假设一个链路上带宽被多个连接共享,且非常平稳,我们可以说这些连接的总带宽与网络的负载相等,都等于链路带宽W,它几乎是一个常数,为了描述队列是多么的容易随着时间积累,我们假设在时间点t的时候,负载为L:




如果不发生拥塞,会有下面的式子成立:




但是如果发生了拥塞,则:




其中的加号后面的部分表示上一个时刻来不及处理的剩余部分,正是它导致了拥塞,如果诸连接不采取措施的话,假定在后面的某个时刻t+i,则:




看到了吗?这可是指数级的,你来猜想 Beta有多大?如果它是一个小于1的很小的数,那么式子是可以收敛的,这也是设计存储转发队列长度设计的依据,然而如果发生了流量突发或者遇到了UDP,组播什么的,这个 Beta就不是小于1的小数了,它可能大于1,直观上看这意味着某种积累,第一个时间点剩余了a没有处理,这个剩余的a会占用第二个时间点的一个空间,在流量不变的情况下让第二个时间点来不及处理的数据数量变成2,然后就是4,8,16...猛吧!如果排队论难以理解的话,这个应该很好理解了吧。
        怎么来的就怎么滚蛋,既然流量指数级飙升,唯一可以让整个流量指数级下降的就是所有连接同时按比例缩减流量!假设都是缩减1/2,则:




我们现在已经确定了当拥塞发生的时候,TCP必须要“乘性减法”,那如果没有拥塞的时候,该怎么样增窗呢?这要扯到控制论了,又一个高大上的话题。总之,你可以将TCP/IP网络看成一个是一个反馈系统:





在控制论中,主要有三个衡量标准,分别是效率,公平,收敛速度。

效率:所有用户的总和是否完全利用了所有的资源;
公平:所有的资源是否在所有用户之间公平分配(可以加权)
收敛:当发生偶发震荡的时候,恢复到最高效率,最好公平性的速度
按照这些指标,一共有四种选择,这四种选择定义了反馈信号发生或者没有发生时,整个系统的行为模式,它们分别是:
1).加性增(无反馈信号),加性减(有反馈信号)

2).加性增(无反馈信号),乘性减(有反馈信号)

3).乘性增(无反馈信号),乘性减(有反馈信号)

4).乘性增(无反馈信号),加性减(有反馈信号)


由于我们知道了TCP必须乘性减,那么选择只剩了2)和3),如何做出选择呢?有一个图非常好地定义了选择的模型,这个图我自己也画过:




你可以很容易证明,加性增是唯一的选择!可以用数学证明,慢启动/加性增/乘性减是最优的方案,从控制论的角度,也可以分析出来慢启动阈值ssthresh的本质以及其与最终收敛带宽的关系。

        好吧,要结束本节了,总结一下,本节首先用ACK自时钟说明了如何闭合反馈线路,然后用排队论的另一个侧面阐释了必须要乘性减,最后用控制论阐释了在这个反馈系统中必然要加性增。

附:队首(HOL,Head of Line)拥塞

你知道队首拥塞吗?超时结账的时候,如果当前付款的人与收银员出现了纠纷,那么等待付款的人将会迅速排起长队,在高速公路上,如果有任意车辆发生了碰擦,后面很容易在很短的时间将队列延伸数公里远,这就是队首拥塞,就是说在一个连续的处理流中,由于前面的处理无法顺利进行,后面的流无法向前推进导致挤压排队,这也是时间墙在作祟!
        极端一点的例子,假设一个路由器以1000pps的线速运行,如果发生队首拥塞,包队列将以1000pps的速度迅速增长!早期的入口队列路由器总是面临这个问题,现在好多了。当时引入入口排队的目的是为了解决出口排队的N加速比问题的,而队首拥塞作为一个被忽略的bug被引入了,并不是作为代价引入的。这点要明白。

4.10.ACK时钟的丢失

如果ACK持续到来,会驱动数据的持续发送,我们上面已经很详细地讨论了这个ACK时钟如何启动以及如何工作,扯了很远终于把慢启动的本质揭露了,看来这个ACK时钟确实来之不易,慢启动结束后,TCP寄希望于拥塞控制可以动态适应带宽,因此ACK时钟的频率也会适时动态改变,比如收到了三次重复ACK,虽然很可能数据包已经丢了,但是既然收到了ACK,还是说明有数据包被收到了,这个ACK时钟依然存在,这只是意味着发送端需要采取应对措施了,数据还是可以发送的,毕竟时钟没有丢嘛。
        然而,ACK时钟确实可能会丢掉,比如:
1).数据包持续大量丢失,无法在接收端制造任何ACK;
2).ACK数据包大量持续丢失,无法到达发送端;
3).发送端持久不发送数据,无法在接收端制造ACK,此时网络状况可能已经异变。
...
总而言之,只要ACK收不到了,就说明ACK时钟已经丢了,此时需要另外一种机制来重新激活ACK时钟,这就是RTO超时定时器。它是一个外部的时钟,驱动ACK时钟丢失后的数据发送,因此每当发生RTO时,需要重新执行慢启动,因为慢启动是制造ACK时钟所必须的,理由不赘述。

4.11.影响拥塞窗口的因素

当然,网络拥塞可以影响拥塞窗口,除此之外,应用程序本身也会影响拥塞窗口的大小,比如发送端已经不造数据了,此时拥塞窗口变大有什么影响呢?影响就是制造突发!而网络中的存储转发节点最怕的就是突发,请参考我的一篇文章中讲的队列的脆弱性!如果已经暂时没有数据要发送,拥塞窗口却一边被接收端清空,一边又被RTT,ACK等反馈机制增大,一旦应用程序制造了大量数据,且对端也同时腾出了大量的接收缓存,这批大量的数据将会突发到网络上,造成严重的后果!
        RFC2861中有个叫做 Congestion Window Validation(CWV)的机制,具体可以参见该RFC。
        另外值得注意的是,TCP发送端当前能发多少数据并不是由发送窗口决定的,而是由窗口与“在途数据”之差决定的,这样在发送端发送数据和收到ACK反馈之间形成一个流水,发送逻辑仅仅是补充一个差值,使得最终的将要发送的数据和“在途数据”之和等于拥塞窗口的大小。在Linux的拥塞避免实现中,会check以下的逻辑:
/* RFC2861 Check whether we are limited by application or congestion window
 * This is the inverse of cwnd check in tcp_tso_should_defer
 */
bool tcp_is_cwnd_limited(const struct sock *sk, u32 in_flight)
{
    const struct tcp_sock *tp = tcp_sk(sk);
    u32 left;

    if (in_flight >= tp->snd_cwnd)
        return true;

    left = tp->snd_cwnd - in_flight;
    if (sk_can_gso(sk) &&
        left * sysctl_tcp_tso_win_divisor < tp->snd_cwnd &&
        left * tp->mss_cache < sk->sk_gso_max_size &&
        left < sk->sk_gso_max_segs)
        return true;
    // 保持突发不超越一个阈值
    return left <= tcp_max_tso_deferred_mss(tp);
}

//拥塞避免核心逻辑:
if (!tcp_is_cwnd_limited(sk, in_flight))
    return;
else
    XXX_cong_avoid(...);

上述代码描述了在带宽利用恒稳的时候数据包守恒的情况,在拥塞窗口并不是瓶颈的时候,这个守恒将持续下去,in_flight是不会变的,当收到ACK的时候,会将in_flight减少对应的被确认的数据大小,这意味着可以再发出去这个多,如果为了补充被确认的in_flight将要发送的数据小于一个突发限额,那么在可能的情形下需要增加拥塞窗口以发送更多的数据,否则就不再增加拥塞窗口。这个逻辑保证了两点:
1).维持了网络中数据包的守恒;
2).在维持守恒的前提下避免了大量的突发。
我们想象一种情景,ACK到达的速率非常快,或者路径上暂时拥堵的地方突然放空,此时ACK大量的数据会使得in_flight突然变少了,从而在拥塞窗口中空出大量的空间,如果这个时候按照窗口滑动的理论把这些空出的部分全部用来发送数据,会有可能造成突发式的拥塞,因此此时需要在维持守恒的基础上制止拥塞,此时需要暂时停止拥塞避免的增窗过程,避免突发进一步加大。由于上述代码的控制,使得突发总是被维持在一个范围内。
        这是一个典型的负反馈系统,这也是一种ACK时钟自动调速的机制,这种时钟变速理论上是无级变速,缓慢进行的,然而受到中间存储转发设备的影响,流量可能会被整形,最终的效果就不是那么平滑了。 如果我们抓包分析一下TCP在高性能网络上的表现,就会发现,事实上其拥塞窗口/时间曲线已经不再呈现那种细细的锯齿状态了。其实我们也可以预期,开始的时候,增加窗口,增加到其制造的剩余突发首次变得大的时候,说明ACK的速率已经跟不上发送速率了,这是一种自适应的调速,此时窗口将不再增大,从未维持了平衡。
        这样做调速而不是一味的执行拥塞避免增窗是合理的,因为拥塞窗口只是用来描述当前的网络拥塞情况的一种手段,并没有积累容器的作用(就像带突发的令牌桶那样),网络拥塞情况会随着时间的流逝而变化,因此前一个时刻积累的拥塞窗口的值如果没有被有效利用来发送数据,将会被废弃。拥塞窗口是时间敏感的!为了避免由于对端通告的接收窗口过小而造成拥塞窗口多余的部分被废弃而突变,RFC2861将会让拥塞窗口在小于对端通告窗口的时候--此时将由通告窗口决定发送窗口,拥塞窗口紧贴着in_flight的上沿,差不多大一个可接受的突发量的样子。这个突发量在早期被定义为3,后来被定义为网络乱序度的值,在Linux最近的版本中,又被定义为3。

5.结语与布雷斯悖论

终于写完了,这篇文章可以说是不间断写完的,有感而发之后本来想写成一篇散文,可是写着写着却又成了不伦不类的半吊子技术随笔,不管怎么,终于是写完了,我是不是可以睡一觉了啊,喝瓶真露,然后做梦!做梦之前,在最后,我想再引出一个主题来,这就是布雷斯悖论。
        城市道路是一个再好不过的关于时间延展容器和空间排队容器的实例了,TCP/IP网络只是比它简单一点而已,你知道为什么吗?
因为城市交通中的汽车司机(公交车,旅游大巴,班车等除外)都是自路由的,即选择哪条路是司机自己决定的,既然由自己选择,就会涉及到协作(博弈论中名词),既然是博弈就会有稳定和不稳定均衡之说,令人遗憾的是,城市交通中司机们的协作是不稳定的,因此就会出现一个悖论,路是越修越堵,越宽越堵,指示牌越多越堵!这既是布雷斯悖论。
        在TCP/IP网络中,所有的数据包没有自路由的机制(最近开始的主动网络可能会有自由选路这方面的机制),所有路径完全按照路由表来,即便一个数据包想从某条特殊路径通过,也是无助的,这就是源路由为什么不被大多数路由器接受的原因吧,一个端到端的机制无法自适应网络拥塞,源路由很可能会产生不稳定的均衡!然而,我们想一下,路由表是怎么生成的?如果是管理员手工配置的,那不存在任何问题,但如果是类似OSPF协议生成的呢?这种协议可以自定义路径权值,如果把拥塞状态也加权进去,会不会也会将流量导向同一处呢?一定会!但是那条路瞬间权值会降低...抖动在所难免,涉及到BGP的话,情况就更复杂了。虽然跟城市交通很类似,但却更简单,因为即便是动态路由协议,它也是全局考虑的,不会出现多方博弈的场景。
        那是不是意味着可以无休止地建设网络基础设施了呢?非也!实际上,TCP/IP上的博弈也超级多,运营商之间,TCP与UDP之间,P2P与UDP之间,组播,自治域之间...太多了,博弈无处不在。

6.后面的故事

关于OSPF,精彩之处可能比TCP还要多,如果加入BGP的博弈,那更是一道佳肴了,在局域网领域,关于CSMA/CD/CA,应该跟这个差不多吧,CSMA/CD的精彩在于其闭环,而不像TCP这样必须依赖外部时钟来触发丢失的事件。但是也不能因此而贬损TCP,毕竟TCP是一个端到端的协议,其范围是全世界的所有节点,而CSMA/CD/CA之所以可以闭环是因为它处理的仅仅是有限范围内的事件,这段范围内在物理定律可以hold住的前提下不会降低人们的期望【附:所谓的物理定律可以hold住说的是完全靠物理定律就能感知事件,完全不依靠类似TCP的那种虚假的启发式...
        物理定律包括但不限于光速,波叠加,干涉,衍射等,人们可以忍受的延迟一般不能超过半秒级,而CSMA/CD/CA可以依靠光速和波的性质在这么短的时间内探知一切,这就是其闭环的原因,如果我们看一下TCP,它有一个time_wait,默认2分钟,这远远超过了人们对网络事件到达率的期望,因此维护这个状态必须依靠TCP内部的状态机以及地球的周长!】,因此它成功了,以太网的故事,后面会继续!
网络三部曲,搞定了任意一个,你就是大拿,搞定了全部,你就是神!这三个我分别简述,排序按照分层模型从低到上:

1).以太网

绝对是近30年来网络技术风头浪尖上的佼佼者,屡次完败对手秒杀群雄,靠的就是成本和收益之间完美的平衡!目前以太网技术已经渗透到了跨区域数据中心,OverLay Network,城域网,甚至广域网...以太网的CSMA/CD开创了完美的反馈控制的先河,它的设计思路和很多后来的协议完全一致,这是根本正确的做法。

2).IP协议

简单,无状态,将复杂性留给了端到端,但却为整个网络肩负!分组交换网的功臣,最佳实践者!动态路由,策略路由,流量工程大行其道,综合服务完败区分服务。IPv4的问题由IPv6修正!

3).TCP协议

在IP为其减负后,一心一意地服务端到端,其QoS目前尚佳,没有衰老的迹象,然而在中国除外,这个我就不多说了,明白人自然明白,不明白的说了也不懂。就像女人的安全裤一样,其实薄薄的一层布料没有任何安全防御功能,既不避光也不防弹,如果有人不遵守规矩的话,他就撕烂那一层象征性的布料,没有谁能阻拦,安全靠的是一种道德精神而不是强制准入控制,这就是端到端!
        屏蔽了IP层之后,你什么事情都可以默默地干!比如,把拥塞窗口保持在1亿...


这个端午假期有点假!旋转升降座椅会爆炸!菊花残,满地伤,花落人断肠!任太妹,人太美,你妹骑马跨马背,大便骑沟便沟内!

                                                                      ----------写于2016/06/11 补充于2016/06/16

相关文章

相关标签/搜索