接系列文章:
首先,可以在我们自己IDC内将各种路由交换设备的MSS设定小于或等于1400字节,并积极参与TCP三次握手时的MSS协商过程,期望达到自动控制服务器收发数据报文大小不超过路径最小MTU从而避免IP分片。这个方案的问题是如果路由路径上其它设备不积极参与协商活动,而它的MTU(或MSS设置值)又比较low,那就白干了。这就好比国家制定了一个高速沿途隧道限高公示通告标准,但是某些地方政府就是不告诉你,没辙。
其次,可以在业务服务中控制应用数据请求/响应的大小在1400字节以下(注:也无法根本避免前述方案中间路由MTU/MSS low的问题),在应用层数据写入时就避免往返数据包大小超过协商确定的MSS。但是,归根到底,在出发前就把数据拆分为多个数据报文,同IP分片机制本质是相同的,交互响应开销增加是必然的。考虑到人在江湖,安全第一,本方案从源头上控制,显得更实际一些。
当然,最靠谱的还是做简法,控制传输数据的欲望,用曼妙的身姿腾挪有致,相关的内容放到轻往复章节探讨。
对应到前面的快乐运猪案例,就是要么在生猪装车之前咱们按照这条路上的最低限高来装车(问题是怎么能知道整个路上的最低限高是多少),要么按照国家标准规定允许的最小限高来装车,到这里,肥猪们终于可以愉快的上路了,风和日丽,通行无阻,嗯,真的吗?
② 放大TCP拥塞窗口
把TCP拥塞窗口(cwnd)初始值设为10,这也是目前Linux Kernel中TCP/IP协议栈的缺省值。放大TCP拥塞窗口是一项有理有据的重要优化措施,对移动网络尤其重要,我们同样从一些基本理论开始逐步深入理解它。
TCP是个传输控制协议,体现控制的两个关键机制分别是基于滑动窗口的端到端之间的流量控制和基于RTT/RTO测算的端到网络之间的拥塞控制。
流量控制目标是为了避免数据发送太快对端应用层处理不过来造成SOCKET缓存溢出,就像一次发了N车肥猪,买家那边来不及处理,然后临时囤货的猪圈又已客满,只好拒收/抛弃,相关概念和细节我们不展开了,有兴趣可以研读《TCP/IP详解 卷一:协议》。
拥塞控制目标是在拥塞发生时能及时发现并通过减少数据报文进入网络的速率和数量,达到防止网络拥塞的目的,这种机制可以确保网络大部分时间是可用的。拥塞控制的前提在于能发现有网络拥塞的迹象,TCP/IP协议栈的算法是通过分组丢失来判断网络上某处可能有拥塞情况发生,评判的具体指标为分组发送超时和收到对端对某个分组的重复ACK。在有线网络时代,丢包发生确实能比较确定的表明网络中某个交换设备故障或因为网络端口流量过大,路由设备转发处理不及时造成本地缓存溢出而丢弃数据报文,但在移动网络中,丢包的情况就变得非常复杂,其它因素影响和干扰造成丢包的概率远远大于中间路由交换设备的故障或过载。比如短时间的信号干扰、进入一个信号屏蔽的区域、从空闲基站切换到繁忙基站或者移动网络类型切换等等。网络中增加了这么多不确定的影响因素,这在TCP拥塞控制算法最初设计时,是无法预见的,同时,我们也确信未来会有更完善的解决方案。这是题外话,如有兴趣可以找些资料深入研究。
拥塞控制是TCP/IP协议栈最经典的和最复杂的设计之一,互联网自我牺牲的利他精神表露无遗,设计者认为,在拥塞发生时,我们应该减少数据报文进入网络的速率和数量,主动让出道路,令网络能尽快调整恢复至正常水平。
拥塞控制机制包括四个部分:
a. 慢启动;
b. 拥塞避免;
c. 拥塞发生时的快速重传;
d. 快速恢复;
话题太大,我们聚焦到与本主题相关的【慢启动】上。
慢启动这项措施的缘起是,当新链接上的数据报文进入一个拥塞状况不可预知的网络时,贸然过快的数据发送可能会加重网络负担,就像养猪场每天都会向很多买家发车送肥猪,但是出发前并不了解各条高速路上的拥堵情况,如果按照订单一口气全部发出去,会遇到两种情况,一是高速很顺畅,很快到达(此时流量控制可能要干预了);二是高速本身就有些拥堵,大批卡车上路加剧了拥堵,并且肥猪们堵在路上,缺衣少食饿瘦了买家不干,风餐露宿冻死了卖家吃亏,重新发货还耽误时间,并且,用于重新发货的货车加入高速则进一步加重了拥堵的情况。作为一个充满社(wei)会(li)良(shi)知(tu)精神的养猪场,我们肯定不愿意贸然增加高(zi)速(ji)的负担。
下面进入简单的理论知识介绍部分,如觉枯燥,敬请谅解。
TCP是一个可靠传输协议,基础是发送-应答(ACK)式确认机制,就好比肥猪运到目的地买家签收以后,要给卡车司机一个回执带回去交差,猪场老板一看回执,大喜过望,马上继续装车发运,如此往复。如【图九 TCP链接建立、传输和关闭示意】,可以了解这种发送-应答式工作的基本流程,如果再结合流量控制、拥塞控制和超时重传等机制,会有很多变种case,整个协议栈因而显得比较复杂。
但,万变不离其宗,老子说“是以圣人抱一为天下式”,真经典。
【图九 TCP链接建立、传输和关闭示意】
慢启动顾名思义,就是把(网络链路数据报文传输)启动的速度放慢一些。方法其实也挺简单,TCP发送方维护了两个参数用于控制这个过程,它们分别是拥塞窗口(cwnd,Congestion Window)和慢启动门限(ssthresh,Slow Start Threashold),具体算法如下:
1) TCP链接建立好以后,cwnd初始化1,单位是链接建立过程中协商好的对端MSS,1代表一次可以发送1 * MSS个字节。ssthresh初始化为65535,单位是字节;
2) 每当收到一个ACK,cwnd ++,cwnd呈线性上升,发送方此时输出数据量不能超过cwnd和接收方通告的TCP窗口(这个概念我们在后面的章节中会介绍)大小;
3) 每当经过一个RTT(Round Trip Time,网络往返时间),cwnd = cwnd * 2,cwnd呈指数让升,同样发送方此时输出数据量不能超过cwnd和接收方通告的TCP窗口大小;
4) ssthresh(slow start threshold)是一个上限,当cwnd >= ssthresh时,就进入“拥塞避免”算法;
广告时间,插播简单介绍一下RTT,它是Round Trip Time(网络往返时间)的简写,简单的理解就是一个数据报文从发送出去到接收到对端ACK确认的时间(这样描述其实不够严谨,因为我们没有展开数据报文发送和对端ACK确认的各种复杂case)。RTT是TCP超时重传机制的基础,也是拥塞控制的关键参数,准确的估算出RTT具有伟大的现实意义,同时也是一项相当艰巨复杂的任务。计算机科学先辈们在持续完善RTT的计算方法,从最初RFC793中描述的经典算法,到Karn / Partridge算法,最后发展到今天在使用的Jacobson / Karels算法,如有兴趣可自行以深入研究。
通过【图十 慢启动过程示意】,可以更直观的理解慢启动的过程,经过两个RTT,cwnd已经由初始值1演化为4:即在接收方通告窗口大小允许的情况下,可以连续发送4个数据报文,然后继续指数增长,这么看来,慢启动一点都不慢。
【图十 慢启动过程示意】
注:示意图中三个RTT大括弧逐渐变大不是因为RTT数值变大,而是要 示意包含的数据报文变多;
猪场老板来解读一下这个算法,我们对一个买家同时维护两个账单数字,一是起运数量设为n,单位是卡车,二是最大同时发货数量设为m,以肥猪头数为单位,描述如下:
1) 同买家订单协商确定后,n初始化1,把符合通往买家的高速路上限高要求的一辆卡车最大装载肥猪头数设为h,1代表一次可以发送1 * h头肥猪。m初始化为65535,单位是头;
2) 每当收到一个买家回执,n ++,n呈线性上升,猪场老板此时发货数量不能超过n和买家通告的临时囤货的猪圈大小;
3) 每当经过一个送货往返,n = n * 2,n呈指数让升,同样猪场老板此时发货数量不能超过n和买家通告的临时囤货的猪圈大小;
4) m是一个上限,当n >= m时,为了避免可能带来的高速拥堵,就要进入“拥塞避免”算法;
这里,需要提到Google的一篇论文《An Argument for Increasing TCP’s Initial Congestion Window》暨RFC6928。Linux Kernel从3.0开始采用了这篇论文的建议---把cwnd 初始化为10个MSS,而在此之前,Linux Kernel采用了RFC3390的规定,cwnd是根据MSS的值来动态变化的。Google的这篇论文值得研究一下,理论分析和实践检验都有。
简单来说,cwnd初始化为10,就是为了允许在慢启动通过往复RTT“慢慢”提升拥塞窗口前,可以在第一个网络传输回合中就发送或接收14.2KB(1460 10 vs 5.7KB 1460 4)的数据。这对于HTTP和SSL来讲是非常重要的,因为它给了更多的空间在网络交互初始阶段的数据报文中填充应用协议数据。
对于移动APP,大部分网络交互都是HTTP并发短链接小数据量传输的形式,如果服务器端有10KB +的数据返回,采用过去的慢启动机制时,效率会低一些,大概需要2~3个RTT才能完成数据传输,反应到用户体验层面就是慢,而把拥塞窗口cwnd初始值提升到10后,在大多数情况下都能在1个RTT的周期内完成应用数据的传输,这在移动网络这样的高时延、不稳定、易丢包的场景下,显得尤其意义重大。
一次就发10卡车肥猪,让慢启动歇一会,别问为什么,有钱,任性。
③ 调大SOCKET读写缓冲区
把SOCKET的读缓冲区(亦可称为发送缓冲区)和写缓冲区(亦可称为接收缓冲区)大小设置为64KB。在Linux平台上,可以通过 setsockopt 函数设置SO_RCVBUF和SO_SNDBUF选项来分别调整SOCKET读缓冲区和写缓冲区的大小。
这两个缓冲区跟我们的TCP/IP协议栈到底有怎么样的关联呢。我们回忆一下【图六 TCP数据报格式及首部中的各字段】,里面有个16位窗口大小,还有我们前面提到的流量控制机制和滑动窗口的概念,大幕徐徐拉开,主角纷纷粉墨登场。在正式详细介绍之前,按照传统,我们还是先站在猪场老板的角度看一下,读缓冲区就好比买家用来囤货的临时猪圈,如果货到了买家使用部门来不及处理,就先在这里临时囤着,写缓冲区就好比养猪场根据订单装好车准备发货,如果买家说我现在可以收货便可速度发出,有点明白了吧。下面详细展开探讨:
a. 【TCP窗口】
整个TCP/IP协议体系是经典的分层设计,TCP层与应用层之间衔接的部分,就是操作系统内核为每个TCP链路维护的两个缓冲区,一个是读缓冲一个是写缓冲。从数据结构角度讲,这两个缓冲区是环形缓冲区。
读缓冲肩负的使命是把接收到并已ACK(确认)过的TCP报文中的数据缓存下来,由应用层通过系统接口读取消费。就好比买家内部会分原料采购部门和产品加工部门,采购部门收到肥猪后先送到临时猪圈好吃好喝供着,加工部门需要的时候就会拎着屠刀过来提猪。
写缓冲肩负的重任是缓存应用层通过系统接口写入的要发送的数据,然后由TCP/IP协议栈根据cwnd、ssthresh、MSS和对端通告的TCP窗口等参数,择机把数据分报文段发往对端读缓冲。想要在拥塞控制等相关参数都允许的条件下连续发送数据报文,尚需对端通告的TCP窗口大小能够容纳它们。就好比猪场老板根据买家订单发货,先调配若干辆卡车,根据高速的限高要求装上肥猪,然后再考虑高速的顺畅情况来分批发货,货可以陆续上路,但还有一个重要前提是发货前买家通告的临时猪圈空间是足够容纳这些肥猪的。
TCP窗口是用于在接收端和发送端之间动态反映接收端读缓冲大小的变化,它的初始值就是读缓冲区设定的值,单位是字节,这个数字在TCP包头的16位窗口大小字段中传递,最大65535字节,如果嫌不够大,在TCP选项中还有一个窗口扩大的选项可供选择。
为什么叫窗口,一窗一风景,英文世界很现实,境界也就到Window级了,这与中华文明一沙一世界,一花一天堂的差距甚大。再直观一些的类比就是你拿着一个放大镜,在1:10000的军用地图上顺着一条路苦苦寻找东莞某镇,放大镜的范围就是我们说的窗口。
概括而言,TCP窗口的作用是量化接收端的处理能力,调控发送端的传输节奏,通过窗口的伸缩,可以自如的调节发送端的数据发送速率,从而达到对接收端流量控制的目的。
师傅三藏曾经对悟空说:你想要啊?你想要说清楚不就行了吗?你想要的话我会给你的,你想要我当然不会不给你啦!不可能你说要我不给你,你说不要我却偏要给你,大家讲道理嘛!现在我数三下,你要说清楚你要不要......,嗯,说清楚最重要。
b. 滑动窗口
客户端和服务器在TCP链接建立的三次握手过程中,会根据各自接收缓冲区大小通告对方TCP窗口大小,接收方根据自己接收缓冲区大小初始自己的“接收窗口”,发送方根据对端通告的TCP窗口值初始化一个对应的“发送窗口”,接收窗口在此端的接收缓冲区上滑动,发送窗口在彼端的发送缓冲区上滑动。因为客户端和服务器是全双工,同时可收可发,故我们有两对这样的窗口在同时工作。
既然是滑动窗口,就意味着可以滑动、伸缩,【图十一 TCP窗口边沿移动】展示了这些情况,注意TCP/IP协议栈规定TCP窗口左边沿只能向右滑动,且TCP的ACK确认模式也在机制上禁止了TCP窗口左边沿向左移动。与窗口滑动相关术语有三个:
1) TCP窗口左边沿向右边沿靠近称为窗口合拢,发生在数据被发送和确认 时。如果左右边沿重合时,则形成一个零窗口,此时发送方不能再发送任何数据;
2) TCP窗口右边沿向右移动称为窗口张开,也有点类似窗口向右侧横向滑动。这种现象发生在接收方应用层已经读取了已确认过的数据并释放了TCP接收缓冲区时;
3) TCP窗口右边沿向左移动称为窗口收缩,RFC强烈建议避免使用这种方式;
【图十一 TCP窗口边沿移动】
我们再来看看滑动窗口与SOCKET缓冲区如何结合使用。假设一个客户端设置了16个单位的读缓冲区,编号是0 ~ 15,服务器也相应的设置了16个单位的写缓冲区,编号是0 ~ 15。在TCP链接建立的时候,客户端会把自己的读缓冲大小16通告给服务器,此时在客户端和服务器就维护了一对收发窗口。在【图十二 服务器TCP发送窗口示意】展示了服务端发送缓冲区和其上的滑动窗口,其中大的黑色边框就是著名的滑动窗口。
【图十二 服务器TCP发送窗口示意】
发送缓冲和发送窗口一共区隔出四个部分:
1) 已发送并收到ACK确认的数据(即已成功到达客户端),单元格边框以粉色标识;
2) 已发送还未收到ACK确认的数据(即发送但尚未能确认已被客户端成功收到),单元格边框以蓝色标识;
3) 处于发送窗口中还未发出的数据(即对端接收窗口通告还可容纳的部分),单元格边框以绿色标识;
4) 处于发送窗口以外还未发出的数据(即对端接收窗口通告无法容纳的部分),单元格边框以黄色标识;
为了更好的理解滑动窗口的变化过程,可以观察【图十三 TCP滑动窗口变迁示例】,它向我们展示了一个服务器向客户端发送数据时读写窗口的变化过程:
【图十三 TCP滑动窗口变迁示例】
1) 客户端通告了一个360字节的TCP窗口并在自己的读缓冲区初始化该窗口,服务器在它的写缓冲区初始化了这个窗口;
2) 服务器发送120字节到客户端,服务器发送窗口此时包括了两部分,120字节为等待ACK确认的数据、240字节为等待发送的数据,窗口大小为360字节不变;
3) 客户端收到120字节数据,放入接收缓冲区,此时应用层马上读取了头40字节,接收窗口因此调整为280(360 - 120 + 40)字节,接收窗口先合拢,然后张开。客户端回复ACK确认收到120字节数据,并且通告接收窗口调整为280字节;
4) 服务器收到客户端的ACK确认,发送窗口也先发生合拢,随后根据客户端通告的新接收窗口大小,重新调整发送窗口,此时发送窗口又张开至280字节;
5) 服务器发送240字节到客户端,服务器发送窗口此时包括了两部分,240字节为等待ACK确认和40字节等待发送的数据,窗口大小为280字节不变;
6) 客户端收到240字节数据,放入接收缓冲区,此时应用层又读取了头80字节,接收窗口因此调整为120(280 - 240 + 80),接收窗口先合拢,然后张开。客户端回复ACK确认收到240字节数据,并且通告接收窗口调整为120字节;
7) 服务器收到客户端的ACK确认,发送窗口也先发生合拢,随后根据客户端通告的新接收窗口大小,重新调整发送窗口,此时发送窗口又张开至120字节;
8) 服务器发送120字节到客户端,服务器发送窗口此时仅包括一部分,即120字节等待ACK确认的数据;
9) 客户端收到120字节数据,放入接收缓冲区,接收窗口因此调整为0(120 - 120),接收窗口合拢为0。客户端回复ACK确认收到120字节数据,并且通告接收窗口调整为0字节;
10) 服务器收到客户端的ACK确认,发送窗口也发生合拢,随后根据客户端通告的新接收窗口大小,重新调整发送窗口,此时因为接收窗口为0,发送窗口保持合拢状态;
提升TCP吞吐量,最佳状态是在流量控制机制的调控下,使得发送端总是能发送足够的数据报文填满发送端和接收端之间的逻辑管道和缓冲区。其中逻辑管道的容量有专门的学名叫BDP(Bandwidth Delay Product,带宽时延乘积,BDP = 链路带宽 * RTT),在一个高带宽低时延的网络中,TCP包头中的16位窗口大小可能就不够用了,需要用到TCP窗口缩放选项,在RFC1323中定义,有兴趣可以研究一下。
猪场老板解读:滑动窗口是从养猪场到买家临时猪圈的出入闸门,猪场养殖场这道出闸门叫发送窗口,买家临时猪圈那道入闸门叫接收窗口,为了不让买家的临时猪圈爆满溢出无法签收新来的肥猪们,进而导致猪场白送一趟货,猪场老板必须要等买家通告自己空闲槽位数量后才可进行生猪发货操作,这个槽位数量就是窗口大小,槽位减少或增加,受到猪场发货速率和买家屠宰部门提货速率的共同影响,表现出类似窗口合拢或张开的滑动状态。我们期待的最佳状态就是高速路上跑满欢快的车队,临时猪圈住满幸福的肥猪。
三藏对小牛精说:所以说做妖就像做人,要有仁慈的心,有了仁慈的心,就不再是妖,是人妖。哎,他明白了,你明白了没有?
④ 调大RTO(Retransmission TimeOut)初始值
将RTO(Retransmission TimeOut)初始值设为3s。
TCP为每一个报文段都设定了一个定时器,称为重传定时器(RTO),当RTO超时且该报文段还没有收到接收端的ACK确认,此时TCP就会对该报文段进行重传。当TCP链路发生超时时,意味着很可能某个报文段在网络路由路径的某处丢失了,也因此判断此时网络出现拥塞的可能性变得很大,TCP会积极反应,马上启动拥塞控制机制。
RTO初始值设为3s,这也是目前Linux Kernel版本中TCP/IP协议栈的缺省值,在链路传输过程中,TCP协议栈会根据RTT动态重新计算RTO,以适应当前网络的状况。有很多的网络调优方案建议把这个值尽量调小,但是,我们开篇介绍移动网络的特点之一是高时延,这也意味着在一个RTT比较大的网络上传输数据时,如果RTO初始值过小,很可能发生不必要的重传,并且还会因为这个事件引起TCP协议栈的过激反应,大炮一响,拥塞控制闪亮登场。
猪场老板的态度是什么样的呢:曾经有一份按时发货的合同摆在我的面前,我没有去注意,等到重新发了货才追悔莫及,尘世间最痛苦的事莫过于此,如果上天能给我一个再来一次的机会,我希望对甲方说耐心点,如果非要给这个耐心加一个期限的话,我希望是一万年。
⑤ 禁用TCP快速回收
TCP快速回收是一种链接资源快速回收和重用的机制,当TCP链接进入到TIME_WAIT状态时,通常需要等待2MSL的时长,但是一旦启用TCP快速回收,则只需等待一个重传时间(RTO)后就能够快速的释放这个链接,以被重新使用。Linux Kernel的TCP/IP协议栈提供了一组控制参数用于配置TCP端口的快速回收重用,当把它们的值设置为1时表示启用该选项:
1) net.ipv4.tcp_tw_reuse = 1
2) net.ipv4.tcp_tw_recycle = 1
3) net.ipv4.tcp_timestamps = 1(tcp_tw_recycle启用时必须同时启用本项,反之则不然,timestamps用于RTT计算,在TCP报文头部的可选项中传输,包括两个参数,分别为发送方发送TCP报文时的时间戳和接收方收到TCP报文响应时的时间戳。Linux系统和移动设备上的Android、iOS都缺省开启了此选项,建议不要随意关闭)
以上参数中tw是TIME_WAIT的缩写,TIME_WAIT与TCP层的链接关闭状态机相关。下面我们看看TIME_WAIT是谁,从哪里来,往哪里去。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。