高性能浏览器网络(High Performance Browser Networking) 第二章

第2章 TCP篇

互联网的核心是两个协议,IP和TCP。 IP也叫Internet协议,提供主机到主机的路由和寻址;TCP,传输控制协议,在不可靠的传输通道上提供一个可靠的网络抽象。TCP / IP协议也通常被称为Internet协议套件,在1974年,它首次在一篇题为《一个用于分组网络互通的协议》的论文中被Vint Cerf和Bob Khan提出。

最初的RFC建议(RFC 675)几经修订,在1981年发表TCP / IP V4正式规范,但分为了两个独立的RFC:

  • RFC 791 - Internet协议
  • RFC 793 - 传输控制协议

从那时起,有一些增强建议补充到TCP协议中,但核心没有大的改变。TCP迅速取代了以前的协议,成为了现在许多最流行的应用程序的首选协议:万维网,电子邮件,文件传输,和其他许多应用。

TCP在不可靠的传输信道上提供了可靠传输的抽象,隐藏了我们的应用程序大部分的复杂性功能:丢包重传,按序传送,拥塞控制和避免,数据完整性,其他特性。当您使用TCP流,TCP协议保证您的所有字节发送与接收的数据是相同的,他们会以相同的顺序到达对端。TCP设计为一个顺序发送协议,而不是一个定时发送协议,这为我们Web性能优化带来了很大的挑战。

HTTP协议并没有指定TCP为唯一的传输协议。如果我们需要,我们完全可以基于数据报套接字(UDP)或任何其他传输协议实现HTTP通信,但在实际应用中,在互联网上所有HTTP通信都是通过TCP传送。

正因为如此,了解TCP的一些核心机制是Web性能优化必不可少的知识。实际情况是,你可能不会在应用中直接使用TCP套接字,但你在应用层选择的设计方案,将影响您的应用程序在TCP层和底层网络的性能。

交错的TCP和IP协议历史

我们都熟悉IPv4和IPv6,但IPV {1,2,3,5}的情况是咋样的?IPv4中的4代表了TCP / IP协议的第四个版本,这是在1981年9月发布的版本。最初的TCP / IP草案分裂成的两个草案,到了V4版本,TCP/IP正式分裂两成单独的RFC。因此,IPv4中的V4只是一个版本继承关系 - V4既不是比IPv1,IPv2,IPv3有更高的优先级,也不存在单独的IPv1, IPv2,或IPv3协议。

1994年,IETF工作组开始研究“互联网下一代协议”(IPng)时,他们需要一个新的版本号,但V5已分配给另一个实验协议:联网流协议(ST),因此他们选用了V6。事实证明,ST从未发展起来,这就是为什么很少有人听说过它。

三次握手

所有TCP连接开始前必须进行三次握手( 图2-1 )。在任何应用中,客户端或服务器在进行数据交换之前,他们必须先协商起始数据包序列号,其他一些连接参数。出于安全考虑,双方一般都是随机挑选起始序列号。

图2-1 三次握手

SYN 

客户端挑选一个随机序列号x,同时发送一个SYN包,SYN包中也可能包括额外的TCP标志和选项。 

SYN ACK 

服务端在x上增1 ,同时挑选一个服务端随机序列号y ,附加服务端的一些标志和选项,发送给客户端 

ACK 

客户端在x y基础上都增加1,并发送一个ACK包服务器,完成了握手过程。

一旦三次握手完成后,应用程序数据可以开始在客户端和服务器之间传输。客户端可以在发送完ACK包后立刻发送数据包,但服务器必须在收到ACK之后才能发送任何数据。这个握手过程适用于每一个TCP连接,并给所有使用TCP的网络应用程序性能带来了重要影响:任何应用程序在数据传输之前,新的TCP连接都将有一个固定的往返延迟。

例如,如果客户端在纽约,服务端在伦敦,我们通过光纤链路建立一个新的TCP连接,那么三次握手将至少需要56毫秒(表1-1 ):单向传播需要28ms,往返一次需要56ms(译者注:客户端只要在收到SYN ACK后就可以发送数据了)。需要注意的是,这和连接带宽没有直接关系。实际上,延迟是由客户端和服务端的信号传播延迟造成,也就是光信号的从纽约到伦敦的传播时间。

创建新的TCP连接所需要的三次握手延迟是很明显的,这也是重用TCP连接是任何应用程序的优化的关键手段的原因之一。

TCP快速连接

TCP的三次握手已被确定为Web延迟的一个重要因素,在很大程度上也是由于网页浏览时通常需要访问几十个甚至上百个来自不通过主机的内容。

TCP快速创建(TFO)是一种旨在减少新的TCP连接延迟的机制。基于在谷歌完成的对流量分析和网络模拟,研究人员已经证实通过TFO机制,允许在SYN包中携带数据,可降低HTTP网络延迟了15%,整体页面加载时间平均可以降低10%以上,并在某些高延迟的场景下,可降低高达40%。

在Linux 3.7 +内核中,客户端和服务端目前都已可支持TFO,这将成为客户端和服务端的一个新选择。尽管如此,TFO不是每一个问题的解决方案。虽然它可能有助于消除三次握手来回的延迟,但是它只能应用与特定场景:SYN数据包的有效荷载大小有限制,只有某些类型的HTTP请求可以被发送;它仅适用于重复连接请求,因为每个新的请求有加密cookie的要求。如果想对TFO的能力和限制行了详细的了解,可以查看最新的IETF草案“TCP Fast Open”。

拥塞避免和控制

1984年初,Jhon Nagle描述了“拥塞崩溃”场景,这在任何非对称网络中都有可能出现:

 

在复杂的网络中,拥塞控制是一个公认的问题。我们已经发现,国防部的互联网协议(IP)-- 数据报协议,以及传输控制协议(TCP)--- 传输层协议,当把它们一起使用时容易遭受不寻常的拥塞问题,这是由在传输层和数据报层之间的相互作用而引起的。特别的,IP网关对于被我们称为“拥塞崩溃”的现象而言是脆弱的,特别是当这种网关连到大范围的不同带宽的网络上的时候...

如果任何主机的数据包的传输往返时间超过了最大重传阀值,该主机将开始发送越来越多重传包到网络中。这时网络就处于一种麻烦状态了。最终交换路由节点的缓冲区被塞满,新的数据包必须被丢弃。数据包的往返时间超出了上限,主机上每个数据包都要发送好几次,最终这些重复包中的一部分到达了目的地。这就是拥塞崩溃。

这种情况是持续的。一旦已达到拥塞点,如果丢弃算法是无优先级的,网络将持续处于拥塞状态。

 

 

- John Nagle - RFC 896

该报告指出,拥塞崩溃在ARPANET中不会成为一个问题,因为大多数节点有统一的带宽,骨干网有很大的容量。然而,这些断言早已变成了现实。在1986年,当网络节点超过5000个时,且不同的网络节点在持续增加时,一系列的拥塞崩溃事件横扫整个网络 - 在某些情况下,网络变得基本无法使用。

为了解决这些问题,多个机制被提出来用来控制拥塞:流量控制,拥塞控制和拥塞避免机制。

高级研究计划署网络(ARPANET)是现代互联网的前身,也是世界上第一个业务分组交换网络。该项目于1969年正式推出,并在1983年TCP / IP协议作为主要的通信协议取代了早期的NCP(网络控制程序)。其余的,正如他们所说,都变成了历史。

流量控制

流量控制是一种控制发送方过量发送数据给接收端的机制,- 接收端可能因为高负荷,处于繁忙状态,也可能接收端只分配了固定大小的缓冲区。为了解决这个问题,TCP连接的每一方都会宣告(图2-2 )自己的接收窗口(rwnd值),用来知会对方自己的数据接收缓冲区大小。

图2-2 接收窗口大小(RWND)

当连接首次建立时,双方都采用系统的缺省设置值来初始化他们的rwnd,一个典型的Web页面将缓存从服务器到客户端的大部分数据流,客户端的缓冲区就有可能成为瓶颈。然而,如果客户端传输流数据到服务器,例如在一个图像或视频的情况下,大量的上传数据,则服务器接收窗口可能成为瓶颈。

如果出于任何原因,一方如果不能处理,那么它可以更改成一个较小的窗口,并通知对方。如果该窗口大小变为零,则它被视为一个信号:不要发送更多的数据,直到应用层处理完所有缓冲区中的数据。此更新过程在每个TCP连接的整个生命周期中持续:​​每个ACK报文中携带最新的rwnd值,让双方可以动态地调整数据流速和处理速度。

窗口缩放(RFC 1323)

最初的TCP规范分配了16 bit来设置接收窗口大小, 这意味着可以设置的最大值为 (216, or 65,535 bytes) . 事实证明,为了获得最佳的性能,这个上限往往是不够的,尤其是具有高带宽延延迟乘积“的网络中 。

为了解决这个问题,RFC 1323制定了一个“TCP窗口缩放”选项,这使我们能够提高接收窗口大小从65,535字节到1G bits!窗口缩放选项在三次握手过程中进行交换,在这个字段中携带了一个值,它用来表示后续ACK16bit需要左移的位数。

今天,TCP窗口缩放选项在所有主要平台下默认情况下都是启用的。然而,中间节点,路由器和防火墙可以重写,或甚至清除此选项 - 如果您的服务端或者客户端连接,无法充分利用可用带宽,可以检查一下接收窗口大小,这是一个好的开始。在Linux平台下,窗口缩放设置可以通过以下命令查看或者启用:

  • $> sysctl net.ipv4.tcp_window_scaling
  • $> sysctl -w net.ipv4.tcp_window_scaling=1

慢启动

尽管TCP存在流量控制机制,网络拥塞崩溃依然在1980年代中后期成为一个严峻的问题。问题是,前面提到的流量控制机制可以防止发送端发送过量数据给接收端,但是没有一个好的机制,可以防止发送端还是接收端侵蚀底层基础网络:在TCP连接建立的时候,无论发送方还是接收方,都无法清楚知道中间网络的可用带宽,因此我们需要一种机制来评估它,并能使调整发送速度使其适应不断变化的网络条件。

我们举一个例子来说明这种调节机制是有价值的,想象一下你在家里,正在播放一个在线视频流,服务器以你家里最大的下行带宽下发数据,确保您的最佳体验。这时,家庭网络上的另一个用户打开一个新的连接,下载一些软件更新。突然之间,分配给视频流可用的下行链路带宽下降很多,在这种情况下,视频服务器必须调整其下发数据速率 - 如果它继续以之前的速率,数据将堆积在中间的某个节点上,数据包将被丢弃,网络使用效率极其低下。

1988年,Van Jacobson 和 Michael J. Karels发布了一些算法来解决这些问题:慢启动,拥塞避免,快速重传和快速恢复。这四个算法迅速成为了TCP强制性规范的一部分。事实上,人们普遍认为,正是这些更新算法使得在上世纪80年代和90年代初的流量以指数速度继续增长情况下,网络避免了崩溃。

要了解慢启动,最好是看它的实际操作。我们再来看一下我们在上一章提到的例子,假设客户端在纽约,试图从伦敦的服务器中下载一个文件。首先,客户端和服务端进行三次握手,在此过程中,双方在ACK数据包中设置他们的各自的接收窗口(rwnd值)大小(图2-2 )。一旦最后的ACK包发出,我们就可以开始交换数据了。

我们要计算底层可用网络带宽的唯一方法就是在客户端和服务端交换数据的过程中进行逐步测量,这也是慢启动算法要干的事情。开始时,服务端为每一个新的TCP连接初始化一个拥塞窗口(cwnd的)值,cwnd的初始值一般是一个系统的缺省值(在Linux下是 initcwnd变量)。

拥塞窗口大小(cwnd) 

代表服务端能够发送出去的但还没有收到ACK确认的最大数据报文段长度

cwnd变量不会在发送端和接收端之间进行交换 - 在这个例子中,cwnd是在伦敦的服务器维护一个私有变量。前面我们讨论过的rwnd,加上这里的cwnd,TCP定义了一个规则:在客户端和服务端之间未确认的数据包的最大值为min(rwnd,cwnd)。到目前为止,一切貌似都很好,但服务端和客户端如何确定拥塞窗口的最优值?毕竟,网络条件随时在变化,即使是相同网络中的两个节点之间,我们也不想去手动调节cwnd值,如果有一种自动调整的算法将非常棒!

解决的办法是先慢慢的发送,随着数据包被确认,逐步增大窗口大小 -这就是慢启动的核心思想!最初,cwnd的值设置为一个TCP Segment,1999年4月的RFC 2581更新这个值至最多四个TCP Segment,2013年4月RFC 6928将这个值更新到了最大10个TCP Segment。

一个新的TCP连接建立后,每次能发送的最大未确认数据包大小是min(cwnd,rwnd),因此,服务端可以发送4个TCP Segment到客户端,然后他要等待数据包的ACK确认。每收到一个ACK,慢启动算法定义发送端的cwnd值可以增加一个TCP Segment - 这意味着每确认了一个数据包,两个新的数据包可以被发送。这个阶段通常被称为“指数增长”(图2-3 )阶段,客户端和服务端都试图快速地占用他们之间可用的带宽。

图2-3 拥塞控制和拥塞避免

那么,为什么慢启动是我们创建Web应用需要考虑的一个重要因素?因为HTTP和许多其他应用协议运行在TCP之上,不管可用带宽是多大,每一个TCP连接必须要经过慢启动阶段 - 我们不能一开始就使用全量的链路容量!

实际上,我们开始用小拥塞窗口,在发送成功后窗口大小扩展一倍 - 即指数增长。其结果是,达到特定的阀值(客户端和服务端之间的最大带宽)所需要的时间与(公式2-1 )在客户端和服务器之间的往返时间RTT,初始拥塞窗口大小的关系如下:

Equation 2-1. Time to reach the cwnd size of size N


让我们假设以下情形:

  • 客户端和服务器接收窗口(rwnd):65,535字节(64 KB)
  • 初始拥塞窗口(cwnd):4 TCP Segments(RFC 2581)
  • RTT时间为56毫秒(伦敦,纽约)

在这个例子中,我们将使用旧的(RFC 2581)4个TCP Segments为拥塞窗口的初始值,因为它仍然是目前大部分服务器最常用的值。通过下面例子的演示,你可能觉得你有必要去更新你的服务器了!

尽管接收窗口大小(rwnd)大小设置为64 KB,但是根据我们前面定义的,TCP连接建立后,初始的最大数据包大小为min(rwnd,cwnd)。因此事实上,要达到64 KB的限制,我们将需要拥塞窗口大小(cwnd)增长到45 TCP Segments,这将需要224毫秒:


需要4个往返RTT( 图2-4 ),几百毫秒的延迟,客户端和服务器之间的吞吐量才能达到64 KB!因此事实上,对于一些客户端和服务端可以达到Mbps+的连接,慢启动没有任何效果。

图2-4 拥塞窗口大小的增长

为了减少拥塞窗口增长所需要的时间,我们可以减少客户端和服务器之间的往返时间RTT - 例如在靠近客户端的地方部署服务器。或者,我们可以增加初始拥塞窗口大小到新的RFC 9828值10 TCP Segments。

慢启动对于大文件下载,流媒体下载来说不是问题,客户端和服务端消耗几百ms达到他们之间的最大速度 - 这个相对于总的下载时间来说,微不足道。

然而,对于许多HTTP连接来说,请求是短小且突发的,大部分情况下,还没有达到最大阀值时,请求已经结束了。结果是,很多web应用的性能受到负向影响:因为慢启动需要更长时间达到最大速率,尤其对小请求来说,影响更大。

慢启动重启

除了调节新TCP连接的传输速率,TCP协议还实现了慢启动的重新启动机制(SSR),当一个连接空闲了一段时间后,将重置拥塞窗口(cwnd)。理由很简单:当连接闲置的时候,网络条件可能已经改变,为避免拥塞,拥塞窗口复位到一个“安全”的默认值。

很显然,SSR对那种应对突发请求的长TCP连接有很大的性能影响 - 例如HTTP的keep-alive请求。因为一旦被重置后,遇到突发请求时,又得经历慢启动的过程,而如上面描述,性能很低。所以在这种情况下,建议在服务器上禁用SSR。在Linux平台上的SSR设置可以通过下面的命令进行查看和禁用SSR:

  • $> sysctl net.ipv4.tcp_slow_start_after_idle
  • $> sysctl -w net.ipv4.tcp_slow_start_after_idle=0

为了说明三次握手和慢启动阶段对一个简单HTTP的影响,让我们假设在纽约的客户端向在伦敦的服务端请求一个20 KB的文件( 图2-5 ),连接参数如下:

  • 往返时间(RTT):56ms
  • 客户端和服务端之间的带宽:5 Mbps
  • 客户端和服务端的接收窗口(rwnd):65,535 bytes
  • 初始拥塞窗口大小(cwnd): 4 segments (\(4 \times 1460\ \mathrm{bytes} \approx 5.7\ \mathrm{KB}\))
  • 服务端处理时间:40ms
  • 无丢包,每包都要求ACK确认,GET请求装入单个TCP Segment

图2-5 通过新的TCP连接获取一个文件

0 ms  

   客户端开始TCP握手---SYN

28 ms

   服务端响应SYN-ACK和指定其RWND的大小

56 ms

  客户端发送ACK SYN-ACK,指定其RWND的大小,并立即发送HTTP GET请求

84 ms

  服务器接收到HTTP请求

124 ms

  服务器构建20 KB的响应,发送4 TCP Segments给客户端,并暂停等待ACK确认(初始拥塞窗口大小为4 TCP Segments)

152 ms

  客户端收到4个TCP Segments,每一个TCP Segment发送一个ACK确认

180 ms

  服务端收到每个ACK,cwnd递增1,最终发送8个TCP Segments出去

208 ms

  客户端收到8个 TCP Segments并ACK确认每一个Segment

236 ms

  服务器收到每个ACK后,将CWnd增1,并发送剩余的Segments

264 ms

  客户端收到剩余的TCP Segments,并ACK确认每一个Segment

作为一个练习,我们将初始拥塞窗口设置为10 TCP Segments图2-5,我们应该可以发现可以少一个完整的往返网络延迟 - 在性能上提高了22%!

在一个RTT为56ms的TCP连接上传输一个20KB的文件,将消耗掉264ms的时间。相比之下,让我们假设客户端是能够重复使用同一个TCP连接(图2-6 ),再次发出同样的请求。

图2-6 在现有的TCP连接上获取一个文件

0 ms

  客户端发送HTTP请求

28 ms

  服务端接收HTTP请求

68 ms

  服务端完成生成20 KB响应,目前拥塞窗口值已经大于发送文件所需的15 TCP Segments,因此它会一次性发送出所有数据。

96 ms

  客户端接收所有15 TCP Segments,ACK确认每一个Segment

在同一连接上执行了同样的请求,但现在没有三次握手的成本和慢启动阶段的延迟,消耗的时间为96ms,达到了275%的性能提升!

在上面两个例子中,服务端和客户端之间的5 Mbps的上行带宽对性能没有实质性影响,相反,传播延时,和拥塞窗口大小是关键影响因素。

事实上,在同一个TCP连接上第一个请求和第二个请求之间的性能差距只与客户端和服务端之间的RTT有关,你可以尝试着调整上面的参数试试看。

增加TCP的初始拥塞窗口(cwnd)

把服务端的初始拥塞窗口更新为新的RFC 6928值(10 TCP Segments,简称IW10),是一个提升性能简单的方法。这可以让所有用户和所有基于TCP的应用受益。好消息是,许多操作系统在新的内核中已经更新了 - 你可以检查相应的文档和发行说明。

对于Linux,所有2.6.39以上内核版本的缺省值都是IW10了。但是,不要停在那里:升级到3.2 +还可以得到其他好处,比如“按比例缩减(PRR)” 。

拥塞避免

我们需要认识到,TCP设计时将丢包作为一个性能的反馈,并用其来调节网速。换言之,它不是假设要发生丢包,而是当丢包发生时,会去调整。慢启动初始化一个保守的拥塞窗口大小(cwnd),在后面,每一个成功的确认,成倍的数据被发送,直到它超过接收端的流量控制窗口大小(rwnd:系统配置的拥塞阈值窗口),或者当丢失了一个数据包,拥塞避免算法(图2-3 )将开始起作用。

拥塞避免算法隐含的假设是一旦发生了网络丢包 - 网络路由的某处发生了拥塞,被迫丢弃该数据包,因此我们需要调整我们的窗口,以避免诱发更多的网络丢包。

一旦拥塞窗口被复位后,拥塞避免算法设定拥塞窗口增长算法,以尽量避免进一步的丢包。在某一个时间点,丢包事件又发生了,这个过程会重复的执行一次。如果你曾经看过一个TCP连接的吞吐量跟踪图,你可以观察到有很多的锯齿纹,现在你应该知道它为什么看起来这样 - 这是拥塞控制和避免算法在不停的控制拥塞窗口的大小以避免网络丢包。

最后,值得注意的是,改善拥塞控制和拥塞避免的算法在学术研究和商业产品中都是一个活跃的领域,不同的网络类型,不同类型的数据传输,有不同的优化算法。今天,在不同平台上,可能运行着许多变种的算法:TCP Tahoe and Reno(原始实现),TCP Vegas,TCP New Reno,TCP BIC,TCPCUBIC(Linux上的默认实现),或 Compound TCP(Windows的默认实现),还有其他很多种实现方案。然而,无论如何实现,都是为了解决拥塞控制和拥塞问题。

快速恢复算法(PRR)

一旦发生了丢包,我们需要复位拥塞窗口,但如何以最佳的方式恢复拥塞窗口是一个简单的挑战:如果你是过于激进,那么间歇性的丢包将显著影响整个连接的吞吐量,如果你调整的速度不够快,那么你可能丢失更多的包。

本来,TCP用“加增乘减”(AIMD)算法:当发生丢包,拥塞窗口大小减半,然后慢慢增加窗口大小。然而,在许多情况下,AIMD算法是过于保守,因此新的算法被开发。

PRR是RFC 6937中指定的一种新的算法,其目标是当一个数据包丢失时,提高恢复速度。他到底优化了多少呢?根据谷歌开发新算法进行测量,它可以减少3-10%的延迟。

PRR现在是Linux 3.2 +内核默认的拥塞避免算法 - 这也是一个很好的理由去升级你的服务器!

带宽延迟乘积(BDP)

内置的TCP拥塞控制和拥塞避免机制包含了另外一重要含义:最佳的发送方和接收窗口的大小(rwnd)必须根据它们之间的往返时间(RTT)和数据传输速率的不同而有所差异。

要理解为什么,我们先回忆一下前面的结论:发送端和接收端之间未确认的最大数据包(没有接收到ACK的包)大小为min(cwnd,rwnd):rwnd将在每一个ACK中更新,拥塞窗口大小(cwnd)则根据拥塞控制和避免算法的基础上由发送者进行动态调整。

如果发送端或接收端未确认的数据包超过了上限,他们必须暂停发包,等待另一端的ACK包,然后继续发送。他们必须等待多久?这取决于它们之间的往返时间!

带宽延迟乘积(BDP) 

数据链路的容量和终端到终端的延迟的乘积。其结果是在任何一个时间点接收端和发送端之间最大的未确认数据包大小值。

如果发送端或接收端经常被迫停下来,等待以前包的ACK,那么这将在数据流中形成空当( 图2-7 ),这将限制连接的最大吞吐量。为了解决这个问题,该窗口尺寸应该设计的足够大,使得另一端在前面的数据包ACK到达之前也能继续发送数据, - 无间隙,才能达到最大的吞吐量。因此,最佳的窗口大小是依赖的往返时间(RTT)!选择了一个较低的窗口大小,这将会限制连接的吞吐量,不管这两端之间可用的或者标榜的带宽有多大。

图2-7 由于低拥塞窗口导致的传输空当

那么,rwnd和cwnd的值设置为多大才是合适的呢?实际计算是很简单。首先,我们假设,最低的cwnd和RWND的窗口大小是16KB,之间的往返时间是100ms:


无论发送端或接收端之间的可用带宽是多少,这个TCP连接不会超过1.31 Mbps的数据传输速率!为了实现更高的吞吐量,我们需要提升的最小窗口大小,或者降低的往返时间(RTT)。

同样地,如果我们知道往返时间(RTT)和两端的可用带宽,我们也可以计算出最佳的窗口大小。在这种情况下,让我们假定保持不变的往返时间(100ms),而发送方的可用带宽为10 Mbps,和接收器端拥有100 Mbps+的可用带宽。假设它们之间没有任何网络拥塞,我们的目标是,充分利用这个10 Mbps的链接:


窗口的大小至少需要122.1 KB,才能充分利用这10 Mbps的链路。前面我们在流量控制章节中讨论过,最大TCP接收窗口大小为64 KB,除非“窗口缩放(RFC 1323)”是支持的-仔细检查你的客户端和服务端的设置!

好消息是,窗口大小协商和调整被网络协议栈自动管理并进行相应的调整。坏消息是,有时它仍然是TCP性能的影响因素。如果你曾经碰到过下面的情况,你的传输速度只占用了可用带宽的一小部分,甚至你知道无论是客户端和服务端能够拥有更高的速率,你需要检查一下是否是窗口设置过小:对端小的接收窗口,不稳定的网络,高丢包导致的拥塞窗口复位,或显式的流量模型都有可能影响到您的网络吞吐量。

高速局域网中的带宽延迟乘积

BDP是一个与往返时间(RTT)和数据速率相关的函数。虽然RTT可能是高传播延迟网络中常见的瓶颈,但是RTT也可能成为本地局域网上的瓶颈!

在1 ms RTT的局域网中,要达到1 Gbit / s速率,我们需要一个至少122 KB的拥塞窗口。计算过程和上面是完全一样的,我们简单地在目标数据传输速率上增加几个0,在往返RTT中减少同样的几个0。

线头阻塞

TCP提供了一个运行在不可靠信道上的可靠的网络抽象,其中包括基本的数据包错误校验和纠错,按顺序传输,重传丢包,以及流量控制,拥塞控制和拥塞避免等提升网络性能的特性。正是TCP拥有这些特点,使得TCP成为大多数应用程序的首选传输协议。

然而,尽管TCP是一种普遍的选择,但不是唯一的,在某些场景下,也不一定是最佳的选择。特别是,顺序发送和可靠传输这样的特性并不总是必要的,可能引起不必要的延误和负向的性能影响。

要理解为什么,我们记得每一个TCP数据包带有一个唯一的序列号,在传输时,数据必须按顺序传递给接收端( 图2-8 )。如果某个包在接收端的接收缓冲区中丢失,则所有后续数据包必须被放在接收端的缓冲区中,直到重传包到达接收端。因为这个工作在TCP层完成,我们的应用程序无法感知底层的重传,也无法直接查看底层的缓冲区,应用程只有等待所有的数据包接收完毕后才能处理数据。相反,它只是看到从socket中读不到数据或者读取数据延迟,这种效应被称为TCP的线头(HOL)阻塞。

图2-8 TCP线头阻塞

线头阻塞虽然带来了延迟,但它避免了我们的应用程序去处理重新排序包和重新组装包的工作,这使得我们的应用程序代码要简单得多。然而,这带来了数据包到达时间的不可预知性-通常称为抖动 -这对应用程序的性能产生了负向影响。

此外,一些应用程序可能根本不需要可靠传输,或者顺序传输:如果每一个数据包是一个独立的消息,则按序传输是不必要的,如果每个消息​将​覆盖所有以前的消息,可靠传传输也完全没有必要。不幸的是,TCP不提供这样的配置 - 所有的包必须按序传输。

能够容忍和自行处理包的乱序或者丢包的应用,或者对延迟和抖动敏感的应用,可以考虑另外一个选择:UDP。

丢包是允许的

事实上,以获得最佳的性能从TCP,丢包是必要的,丢弃的数据包作为一个网络状态反馈机制,接收端和发送端依据它来调整发送速率以避免网络拥塞,降低延迟-见“本地路由器的Bufferbloat” 。此外,一些应用程序容忍数据包丢失而不受影响:音频,视频和游戏状态更新等应用程序的数据不需要可靠和按序传输 - 顺便说一句,这也是为什么WebRTC使用UDP作为传输协议。

如果一个数据包丢失,音频编解码器可以简单的在音频中插入一个小暂停,并继续处理传入的数据包。如果间隙足够小,用户可能根本察觉不到,而等待丢失的包可能带来更多暂停的风险,这也将导致的更差的用户体验。

同样,如果我们在一个3D的世界去更新一个游戏状态,然后等待一个T-1时间点的数据包来描述其状态时,这时我们收到了时间T的状态,这种情况下T-1时间点的状态就没有必要了,我们收到每个消息后直接更新,可以接收间歇性的丢包,这不影响游戏的体验。

优化TCP

TCP协议作为一种自适应协议,设计的目标是所有网络节点具有同等地位,有效的利用底层网络。因此,优化TCP的最好方法就是让TCP感知网络条件和针对上层和下层的需求调整自身的行为:无线网络可能需要不同的拥塞算法,有些应用程序可能需要自定义的服务质量(QoS)的语义提供最佳体验。

不同的应用需求和众多的TCP优化算法相结合,使得TCP调整和优化算法成为学术和商业研究一个无止境的领域。在本章中,我们只是介绍了影响TCP性能的诸多因素的一些皮毛。还有一些其他机制,如选择性确认(SACK),延迟确认和快速重传以及许多其他机制,这些机制使得TCP会话理解,分析,调整起来更复杂(或有趣的,取决于你的观点)。

虽然说每个算法和反馈机制的具体细节将在后续章节中详细讨论,其核心思想及其影响基本不变:

  • TCP三次握手引入了一次完整的往返延迟(RTT)
  • 每一个新的连接都有一个慢启动过程
  • TCP流量和拥塞控制调节所有连接的吞吐量
  • TCP吞吐量受当前拥塞窗口大小影响

其结果是,在现代高带宽网络下,TCP连接的速率通常受限于发送端和接收端之间的传播延迟。此外,虽然带宽的不断增加,但是传播速度基本上限制在光速的一个很小的常数因子上了。在大多数情况下,不是带宽,而是时延,成为TCP的主要瓶颈-参见图2-5 。

服务器配置调整

作为优化的一个起点,在调整缓冲区设置,几十个TCP超时变量值前,把你的服务器升级到最新版本也许是最简单有效的。TCP控制其性能的最佳实践和底层优化算法在不断发展,这些变化多数包含在最新的内核中。总之,保持你的服务器更新到最新版本,以确保发送端和接收端的TCP连接最佳性能。

从表面上看,服务器内核版本升级似乎是很小也很简单的事情。然而,在实践中,经常会碰到有重大阻力:现有的很多服务器都是基于某个内核版本开发的,许多系统管理员都不愿意冒着风险去执行升级。

一切都是公平的,每一次升级会带来风险,但是,获得更好的TCP性能,这也是一个投资。就看你的去评估了。

如果你的内核已经是最新了,你可以按照下面的最佳实践来调整你的服务器配置:

“增加TCP的初始拥塞窗口(cwnd)” 
较大的起点拥塞窗口允许TCP在第一往返传输更多的数据,并可以更快的增长窗口 - 这对那种突发和短请求居多的服务器来说,尤为关键。

“慢启动重启动”  

对于TCP长链接,禁止空闲后的慢启动,可以提升性能,特别是那种突发性请求居多的场景。

“窗口缩放”(RFC 1323)  

启用窗口缩放增加最大接收窗口大小,并允许高延迟连接,以实现更好的吞吐量。

“TCP快速打开”  

允许在某些情况下,在初始的SYN数据包发送应用程序数据。TFO是一种新的优化,这需要客户端和服务器同时支持 -如果你的应用程序有需要,可以考虑。

配合上述的配置和最新的内核可以获得最佳的性能 - 更低的延迟和更高的吞吐量。

根据您的应用程序情况,你可能还需要调整其他的TCP服务器上的设置,以优化连接速率,内存消耗,或类似的指标。然而,这些配置设置都依赖于平台,应用程序和硬件 - 需要参考平台文档。

对于Linux用户, ss是一个强大的工具,可以用来检查打开的socket各种统计信息。在命令行中,执行“ ss --options --extended --memory --processes --info看到当前的各个端口以及各自的配置。

调整应用程序的行为

调整TCP的性能可以为服务端和客户端提供最佳的吞吐量和延迟。然而,应用程序如何使用每一个新的或者已建立TCP连接,对性能也有很大的影响:

  • 发送尽量少的数据
  • 我们不能让数据跑的更快,但我们能让距离更短
  • TCP连接复用是提高性能的关键

消除不必要的数据传输当然是最简单有效的方法 - 例如,消除了不必要的资源,或者采用适当的压缩算法确保最小的数据被传输。其次,我们可以通过在离客户端更近的地方部署服务器 - 例如,使用一个CDN - 将有助于减少网络往返延迟(RTT),并显着提高TCP的性能。最后,在可能的情况下,现有的TCP连接应该被重复使用,以最大限度地减少慢启动和拥塞控制机制所带来的开销。

性能优化Checklist

高性能的TCP连接无论对哪种应用,也无论是哪一个接入到您服务器的连接来说,都是至关重要的。一个简短的checklist:

  • 升级到最新版本的服务器内核(Linux系统:3.2 +)
  • 确保拥塞窗口(cwnd)大小设置为10 TCP Segments
  • 禁用闲置后慢启动
  • 确保该窗口缩放启用
  • 消除冗余数据传输
  • 压缩传输数据
  • 在离用户更近的地方部署服务器,以减少往返时间
  • 尽可能重用建立的TCP连接

没有更多推荐了,返回首页