概述
在前面的一系列文章中,我们讨论了关于 TCP/IP 协议族的一些基本原理,在本篇文章中,我们将重点讨论关于基于 TCP 传输层的性能优化。
延迟( latency )
延迟是指分组从信息源发送到目的地所需要的时间。影响延迟的因素包括:
- 传播延迟:消息从发送端到接收端需要的时间,是信号的传播距离和速度的函数
- 传输延迟:把消息中的所有比特转移到链路中需要的时间,是消息长度和链路速率的函数
- 处理延迟:处理分组首部、检查位错误及确定分组目标所需的时间
- 排队延迟:到来的分组排队等待处理的时间
以上延迟的时间总和,就是客户端到服务器的总延迟时间。
我们都知道,光在真空当中的传播速度大概是30万公里/每秒,但是这是最理想的情况下网络中传播速度的极限值。实际上,根据不同的传播介质,会影响到实际的传输速度。常见的介质有铜绞线,光纤等。折射率是光速与分组在介质中传播速度之比,折射率越大,光在介质中的传播速度就越慢。基本上,以目前的技术,传播分组的光纤的大多数折射率在 1.4 ~ 1.6 之间。
我们假定光通过光纤的速度约20万公里/每秒,对应的折射率约为1.5,让我们来看几个实际例子:
路线 | 距离(km) | 时间:光在真空中 | 时间:光在光纤中 | 光纤中的 RTT |
---|---|---|---|---|
纽约到旧金山 | 4148 | 14 ms | 21 ms | 42 ms |
纽约到伦敦 | 5585 | 19 ms | 28 ms | 56 ms |
纽约到悉尼 | 15993 | 53 ms | 80 ms | 160 ms |
赤道周长 | 40075 | 133.7 ms | 200 ms | 400 ms |
这里推荐一个根据目的地IP或者域名测试 RTT 的网站:http://tools.cloudxns.net/Index/Ping
此外,我们可以使用 traceroute ( windows 下是 tracert ) 命令,追踪数据包在网络上的传输时的全部路径。traceroute 通过发送小的数据包到目的设备直到其返回,来测量其需要多长时间。一条路径上的每个设备 traceroute 要测3次。输出结果中包括每次测试的时间(ms)和设备的名称(如有的话)及其ip地址。
|
|
关于 traceroute 更多内容可以参考: https://linux.die.net/man/8/traceroute
在 Mac 下,我们可以使用一个用户体验更好的工具, BestTrace,能够以地图的方式显示出每一跳的信息,非常直观。
如果不想安装该软件,也可以使用:https://www.ipip.net/traceroute.php
带宽 ( Bandwidth )
带宽是指数据的发送速度。比如我们的百兆网卡,就是指网卡的最大发送速度为100Mbps(注意 b 是指 bit 而不是 byte)。发送速度和下面几个因素有关系:
数据发送装置将二进制信号传送至线路的能力,也称之为信号传输频率,以及另一端的数据接收装置对二进制信号的接收能力,同时也包括线路对传输频率的支持程度。
数据传播介质的并行度,也可以称之为宽度,完全等价于计算机系统总线宽度的概念。比如在光纤传输中,我们可以将若干条纤细的光纤并行组成光缆,这样就可以在一个横截面上同时传输多个信号,就像在32位的计算机总线中,可以同一时刻传输32位数据。需要注意的是,要提高计算机总线的带宽,包括提高总线频率和总线宽度两种方法,比如使用64位总线系统或者使用主频更高的处理器等。这两种方法与以上数字通信带宽的两个决定因素完全相似。
如何实际的测试你的网络带宽?
虽然 ISP 或者 IDC 运营商有在你购买带宽的时候,表明提供的带宽大小,比如 100Mbps,但是那是理论值,很有可能实际的带宽达不到官方宣传的指标。
一般个人使用的 PC 上,可以使用 speedtest 来进行测试。 speedtest 能够帮助你测试上传速度和下载速度。
另外国内有一个类似的网站叫: www.speedtest.cn ,但是相对于 speedtest.net 来说,缺少切换服务器进行测试的功能。
如果你是在服务器上或者命令行想进行测试,可以选择 speedtest-cli 进行测试
|
|
可以看到,上述的测试结果还是比较准确的,Upload 显示的为:1.18 Mbit/s ,而测试的这台服务器的出口带宽为 1 Mbit/s 。
关于带宽
的更多知识,强烈建议仔细阅读《构建高性能Web站点》 第二章
总结
可以通过上述的概念的描述,我们可以发现, Latency 和 Bandwidth 决定了网络性能,但是在绝大部分场景下,影响传输性能的最关键的因素是 Latency 。因为就算带宽再高,我们也无法绕过由于物理距离带来的高延迟问题。因此,减少延迟就作为了一个非常核心的优化指标。
TCP Fast Open
设计目标
http 的 keepalive 受限于 idle 时间,据 google 的统计( chrome 浏览器),尽管 chrome 开启了 http 的 keepalive ( chrome 默认是4分钟 ),可是依然有 35% 的请求是重新发起一条连接。而三次握手会造成一个 RTT 的延迟,因此 TFO 的目标就是去除这个延迟,在三次握手期间也能交换数据。
定义
TCP 快速打开是对 TCP 连接的一种简化握手手续的拓展,用于提高两端点间连接的打开速度。它通过握手开始时的 SYN 包中的 TFO cookie (一个TCP扩展选项)来验证一个之前连接过的客户端。如果验证成功,它可以在三次握手最终的 ACK 包收到之前就开始发送数据,这样能够减少一个 RTT 的时间,从而降低了延迟。这个加密的Cookie被存储在客户端,在一开始的连接时被设定好。然后每当客户端连接时,这个Cookie被重复返回。
具体的步骤参考下图:
开启 nginx tcp fast open
在 Nginx 1.5.8 版本以及之后,listen 指令开始支持 fastopen 参数。需要注意的是:Linux 内核版本必须在 3.7.1 以及以上的版本才支持 TCP fast open 。
首先需要内核开启对 tcp fast open 的支持:
|
|
然后编译 nginx 的时候需要增加参数:
|
|
最后,修改 nginx 配置文件:
|
|
验证请求是否有使用到 TCP Fast Open 有两种方式:
- 在服务器端直接观察 TCP Fast Open 的状态,查看 TCPFastOpenPassive 字段的数字是否会随着使用而增加。
|
|
- 使用 Wireshark 抓包:
- 观察客户端出发的第一个 SYN 包,是否包含 TFO=R TCP 扩展选项
- 观察服务端回应的 SYN-ACK 包,是否包含 TFO=C TCP 扩展选项
- 观察之后发出的 SYN 包,是否包含 TFO=C 标记,同时该包有 data
- 若 1 失败,说明客户端没有发出 TFO 请求
- 若 2 失败,说明服务器端配置有误,未能正确启动 TFO 支持
tcp fast open 目前的支持情况
目前大部分客户端浏览器不支持,比如 chrome 只在 Linux,Android,Chrome OS 才支持,参考:这里;Microsoft Edge 从 Windows 10 Preview build 14352开始支持 TFO ;Mozilla Firefox 56 将支持 TFO 。
可见,在 web 浏览器端 TFO 并没有得到普及。如果 google tcp fast open ,会发现其更多的应用场景是用于优化科学上网的梯子上。例如,这里 和 这里
curl 客户端支持 tfo ,可以使用下面的命令来开启做测试:
|
|
ref:
https://zh.wikipedia.org/wiki/TCP%E5%BF%AB%E9%80%9F%E6%89%93%E5%BC%80
http://nginx.org/en/docs/http/ngx_http_core_module.html#listen
https://www.unixteacher.org/blog/linux/speed-up-web-delivery-with-nginx-and-tfo/
https://gist.github.com/denji/8359866
https://tools.ietf.org/html/rfc7413#section-6.1
http://blog.51cto.com/davidbj/1426220
基于 TCP 流量控制和拥塞控制的优化
窗口缩放选项
我们在上一篇文章中已经完整讨论了 TCP 流量控制中:滑动窗口、默认的 rwnd (接收窗口)的大小( 2 ^ 16 = 65536 = 64KB )、窗口缩放选项及其对性能的影响等知识点,这里就不再累述。我们只需要注意,Linux 服务器的内核版本选择,需要高于 2.6.8 版本,这样默认窗口扩大选项就是开启的。
|
|
初始拥塞窗口
还是在上一篇文章中,我们学习了拥塞控制相关的算法依赖于 TCP 连接初始化一个新的拥塞窗口( cwnd ),会将其设置成一个初始值,即: initcwnd 。cwnd 决定了发送端对接收端 ACK 之前,可以发送数据量的限制。
新 TCP 连接传输的最大数据量取 rwnd 和 cwnd 中的最小值。虽然服务器实际上可以向客户端发送 4个 MSS ,但是最开始的时候必须停下来等待确认。此后,每收到一个 ACK ,慢启动算法就会告诉服务器可以将它的 cwnd 窗口增加1个 MSS 。每次收到 ACK 后,都可以多发送两个新的分组。TCP连接的这个阶段通常被称为“指数增长”阶段,因为客户端和服务器都在向两者之间网络路径的有效带宽迅速靠拢。上述算法决定了,无论你的带宽有多大,都无法在一开始的时候就完全利用连接的最大带宽。
根据拥塞控制算法中的慢启动算法,在分组被确认后逐步增加 cwnd 的大小。最初的时候,初始的 cwnd 的值只有 1 个 MSS ;1999年4月,RFC 2581 将其增加到了4个 MSS 。2013年4月,RFC 6928再次将其提高到10个 MSS 。
以下的公式描述了 cwnd 大小达到 N 字节大小所需要花费的时间:
我们来看实际的示例:
- 客户端和服务器的 rwnd 为 64 KB
- initcwnd = 4 MSS ( RFC 2581 )
- RTT = 56 ms
根据上面的公式,我们可以计算出:
- 首先计算达到 N 字节的大小(这里的 N = 64KB = 65535 byte),需要多少个 MSS,即: 65535 / 1460 = 45
- 将 N 代入公式,可以计算出所需要花费的时间为 224 ms。
要达到客户端与服务器之间64 KB的吞吐量,需要4次 RTT,几百毫秒的延迟!至于客户端与服务器之间实际的连接速率是不是在 Mbps 级别,丝毫不影响这个结果。这就是慢启动。
现在,假设只修改 initcwnd = 10 MSS ,其它条件都不变,根据上述公式,我们可以计算出其花费的时间只需要 168 ms。
由此,我们得出结论,要想减少 TCP 连接中完全利用到最大带宽的所花费的时间,要么减少 RTT (减少 RTT 其实就是减少物理距离),要么增加 initcwnd (尽量使用 Linux 内核 2.6.39 之后的版本)。
关于 initcwnd 的更多信息,强烈建议阅读一下 cdnplanet 的 这篇文章,文中完整的介绍了,初始窗口对传输时间的影响、拥塞控制的慢启动算法、如何调整和查看 initcwnd 和 initrwnd 、调整了之后的测试结论等信息。以下的图例是各个操作系统,初始窗口的大小默认值(注意,是初始窗口大小,而不是初始拥塞窗口或者初始接收窗口的大小,rwin = min(initcwnd, initrwnd) )
慢启动重启( Slow-Start Restart )
慢启动重启会在连接空闲一定时间后重置连接的拥塞窗口。道理很简单,在连接空闲的同时,网络状况也可能发生了变化,为了避免拥塞,理应将拥塞窗口重置回“安全的”默认值。
因此,慢启动重启对于那些会出现突发空闲的长周期TCP连接(比如 HTTP 的 keep-alive 连接)有很大的影响。因此,我们建议在服务器上禁用慢启动重启。在Linux平台,可以通过如下命令来检查和禁用慢启动重启:
|
|
三次握手和慢启动对 HTTP 传输性能的影响
为了更好的说明三次握手以及慢启动阶段对 HTTP 传输性能的影响,让我们看一个实际的例子。我们假设涉及的相关参数信息如下:
- http 请求文件的文件大小: 64 KB
- RTT:56 ms
- rwnd : 64 KB
- bandwidth: 5 Mbps
- initcwnd : 10 ( 10 x 1460 byte = 14 KB)
- 服务器端处理请求所花费的时间: 40 ms
- 传输过程中没有发生丢包,每个数据包都要确认,GET 请求值只占 1 段。
从上述的图示中我们可以看出,传输 64 KB 的文件需要总共花费 264 ms 的时间。
我们假设 TCP 连接能够重用同一个连接,重复上述的过程重新进行传输,其过程大概如下:
- 0 ms : 客户端发起 http 请求
- 28 ms : 服务器端接收到 http 请求
- 68 ms : Server 端花费了 40 ms 的时间处理 64 KB 响应,此时的 cwnd 的大小已经超过了 45 MSS 的大小,因此,直接将整个 64 KB 的文件直接一次性的进行发送。
- 96 ms : 客户端接收到了所有的 64 KB的文件。
同样的一次请求,相比了第一个示例中的 268 ms 处理时间,第二个示例仅仅花费了 96 ms ,性能提升了 275% 。原因在于第二个示例,没有三次握手的 RTT 延迟,以及没有初期慢启动达到最佳状态时,所带来的时间消耗。
同时我们还可以看到,5 Mbps 的 bandwidth 实际上在 TCP 连接的初始阶段,对性能没有任何影响。主要影响因素还是 latency 和 cwnd 的大小。
采用更好的拥塞预防算法–PRR 算法
在上一篇文章中,我们也学习了拥塞预防的相关知识,这里就简单的复习一下,加深一下印象。
慢启动初始以 initcwnd 为大小成倍增加 cwnd 的值之后,当超过了接收端流量控制的拥塞阈值,即 ssthresh 窗口,或者在传输过程有发生丢包,此时就会采用拥塞预防算法。拥塞预防算法把丢包作为网络拥塞的标志,即路径中某个连接或路由器已经拥堵了,以至于必须采取删包措施。因此,必须调整窗口大小,以避免造成更多的包丢失,从而保证网络畅通。
确定丢包恢复的最优方式并不容易。如果太激进,那么间歇性的丢包就会对整个连接的吞吐量造成很大影响。而如果不够快,那么还会继续造成更多分组丢失。
最初,TCP 使用 AIMD( Multiplicative Decrease and Additive Increase ,倍减加增)算法,即发生丢包时,先将拥塞窗口减半,然后每次往返再缓慢地给窗口增加一个固定的值。不过,很多时候 AIMD 算法太过保守,因此又有了新的算法。
PRR(Proportional Rate Reduction,比例降速)就是RFC 6937规定的一个新算法,其目标就是改进丢包后的恢复速度。根据谷歌的测量,实现新算法后,因丢包造成的平均连接延迟减少了3%~10% 。
需要注意的是, PRR 算法从 Linux 3.2 版本才开始支持。
带宽延迟积与窗口大小的关系
关于 BDP 的概念我们已经在在上一篇文章中学习过了,现在我们知道了,发送端和接收端之间在途未确认的最大数据量,取决于拥塞窗口( cwnd )和接收窗口( rwnd )的最小值。接收窗口会随每次 ACK 一起发送,而拥塞窗口则由发送端根据拥塞控制和预防算法动态调整。
无论发送端发送的数据还是接收端接收的数据超过了未确认的最大数据量,都必须停下来等待另一方 ACK 确认某些分组才能继续。要等待多长时间呢?取决于往返时间!
BDP(Bandwidth-delay product,带宽延迟积):数据链路的容量与其端到端延迟的乘积。这个结果就是任意时刻处于在途未确认状态的最大数据量。
因此想要充分利用带宽,必须让窗口大小接近 BDP 的大小,才能确保最大吞吐量。
我们通过如下的例子来讨论一下,究竟 rwnd 和 cwnd 的值与理论最大使用带宽的关系是什么?
- min(cwnd, rwnd) = 16 KB
- RTT = 100 ms
- 16 KB = 16 X 1024 X 8 = 131072 bits
- 131072 / 0.1 = 1310720 bits/s
- 1310720 bits/s = 1310720 / 1000000 = 1.31 Mbps
因此,无论发送端和接收端的实际带宽为多大,当窗口大小为 16 KB 时,传输速率最大只能为 1.31 Mbps 。
再来看另外一个例子,假设发送端的带宽为 10 Mbps ,接收端的带宽为 100 Mbps,RTT 为 100 ms。如果我们想要充分利用带宽,也就是客户端的 10 Mbps,那么计算出的最小窗口值为:
- 10 Mbit/s = 10 X 1000000 = 10000000 bit/s
- 10000000 bit/s = 10000000 / (8 X 1024) = 1221 KB/s
- 1221 KB/s X 0.1 s = 122.1 KB
因此,我们至少需要 122.1 KB 的窗口大小才能充分利用 10 Mbps 的带宽。并且,如果要想尽量达到最大吞吐量的带宽速度,要么增加窗口大小,要么减少 RTT 。
队首阻塞 ( Head-of-line Blocking )
所谓的队首阻塞是指,由于 TCP 的可靠性和顺序到达的特性,要求所有的数据包必须按顺序传送到接收端。如果中途有任意一个数据包没能到达接收端,那么后续的数据包必须保存在接收端的 TCP 缓冲区,等待丢失的数据包重发并到达接收端。由于应用程序对 TCP 重发和缓冲区中排队的数据包一无所知,必须等待所有数据包全部到达了之后才能访问数据,因此,应用程序只能在通过套接字数据时感觉到延迟交付。
队首阻塞造成的延迟可以让我们的应用程序不用关心分组重排和重组,但是数据包到达时间会存在无法预知的延迟变化,这个时间变化通常被称为抖动,也是影响应用程序性能的一个主要因素。
结论:无需按序交付数据或能够处理分组丢失的应用程序,以及延迟或抖动要求很高应用程序,可以考虑 UDP 协议。
总结
尽管 TCP 协议相关的优化算法正在不断地发展,但是其核心原理以及它们的影响是不变的:
- TCP三次握手增加了整整一次往返时间
- TCP慢启动将被应用到每个新连接
- TCP流量及拥塞控制会影响所有连接的吞吐量
- TCP的吞吐量由当前拥塞窗口大小控制
在大多数情况下,TCP 性能的瓶颈都是延迟,而非带宽。
Check List:
- 把服务器内核升级到最新版本(Linux:3.2+)
- 确保 cwnd 大小为10 ( 通过 ip route show 进行查看 )
- 禁用空闲后的慢启动 ( tcp_slow_start_after_idle = 0 )
- 确保启动窗口缩放( net.ipv4.tcp_window_scaling = 1 )
- 减少传输冗余数据
- 压缩要传输的数
- 把服务器放到离用户近的地方以减少 RTT
- 尽最大可能重用已经建立的TCP连接