TCP 协议
互联网中两个关键的协议是 IP 和 TCP。IP 协议用来主机间的路由和寻址。TCP 协议提供了在不可靠信道上的可靠网络抽象。 TCP/IP 首先由 Vint Cerf 和 Bob Kahn 在 1974 年发表的名为《一个用于分组网络互通的协议》中提出,用来指代互联网协议栈。
原始的提案(RFC 675)被重新修了了多次。1981 年提出的第四版 TCP/IP 规范由两个 RFC 组成,分别是
- RFC 791 - IP
- RFC 793 - TCP
从那之后,提出了一系列针对 TCP 协议的改进方案,但是核心的操作没有明显的改变。TCP 快速取代了之前的协议,现在是许多流行应用的选择,如 万维网、电子邮件、文件传输等待。
TCP 提供了一个在不可靠信道上建立可靠网络的有效抽象,隐藏了大多数网络交流中复杂的细节,比如说数据丢失,顺序传输,控制和避免拥塞,数据完整性等待。当使用 TCP 数据流工作保证了接收方可以分辨出发送方的每一个字节,并且按照顺序接受。TCP 是针对精确传输进行优化,而不是传输的时间,这就为网络性能的优化带来了挑战。
HTTP 标准并不强制使用 TCP 作为唯一的传输协议。如果我们想要的话,我们可以通过套接字(UDP)来传输 HTTP,或者其他的传输层协议。但是实际中,由于许多开箱即用的方便特性,互联网中所有的 HTTP 流量都是通过 TCP 完成的。
正因为如此,为了提供一个高性能的网络体验,理解一些 TCP 的核心机制是非常有必要的。亦可能不会有在应用中直接使用 TCP 套接字的机会,但是你构建应用层协议的设计性能会取决于 TCP 和底层网络的表现。
TCP 和 IP 协议纠缠在一起的历史
我们对 IPv4 和 IPv6 都很了解,那么 IPv{1,2,3,5} 发生了什么?IPv4 中的 4 代表了在 1981 年 9 月发布的 TCP/IP 协议的第四版。原始的 TCP/IP 提案包莲莲两个协议,第四版草案官方地分成了两个 RFC。因此 IPv4 中的 v4 是历史遗留问题,并没有独立的 IPv1,IPv2 和 IPv3 协议。
当工作小组在 1994 年着手于“下一代互联网协议”时需要一个新的数字,但是 v5 已经在另外一个实验性的协议(互联网流式协议 ST)中使用了,ST 从来没有流行过,所以大家也几乎没有听到过。所以下一代互联网协议称为 IPv6。
三次握手
所有的 TCP 连接均由三次握手开始,如图 2-1 所示。
图 2-1 三次握手
在客户端或者服务器端可以交换应用层的数据之前,连接的两端必须在数据包的序列号上达成一致,同时这个序列号座椅连接的一个参数。由于安全因素,这个序列号是随机采用的。
SYN
客户端算则一个随机的序列数 x 并且发送一个包含其他额外 TCP 标志和选项的 SYN 包
SYN ACK
服务器端将 x 加一,自己选择一个随机的序列数 y,加上一些自己的标志和选项,发送应答
ACK
客户端将 x 和 y 均加一,通过发送 ACK 包完成握手。
一旦完成了三次握手,应用层的数据开始在客户端和服务器端传输。客户端可以在 ACK 包之后直接发送数据包,服务器端必须等到 ACK 之后才可以发送数据。这个初始化过程发生在每一个 TCP 连接,并且暗示了每一个使用 TCP 协议的性能:每一个新的连接在发送应用层数据前都会有一个往返的延迟。
举例来说,如果我们的客户端在纽约,服务器端在伦敦,我们通过光纤建立一个新的 TCP 连接,那么三次握手至少占用 56 ms,表 1-1 中展示了一个方向的传播延迟是 28 ms,之后它还要返回纽约。注意,带宽在连接中没有起到作用。实际上,延迟是由客户端和服务端之间信号传播的延迟造成,也就是从纽约到伦敦的传播时间。
由于无法避免由三次握手造成的延迟,建立新的 TCP 连接是很昂贵的,所以连接重用是运行在 TCP 上应用的重要优化方式。
TCP 快速连接
加载一个网页通常需要从十几个不同的主机获取几百个资源。换句话说,这需要浏览器建立十几个新的 TCP 连接,每一个都会因为 TCP 握手带来额外的开心。这显著增加了网络浏览的延迟,尤其是在移动网络中。
TCP 快速连接(TFO) 是一个用来通过在 SYN 包中传递数据从而降低建立 TCP 连接延迟的机制。 然而它有它自己的局限性, SYN 包的大小有限制,只有几种 HTTP 请求可以发送,由于需要加密 cookie,它只能在重复连接上使用。你可以在最新的 IEFT 草案中找到 TCP 快速连接
使用 TFO 需要服务器端,客户端以及应用的支持。最好的结果是在 linux 内核 v4.1 以上以及相应的客户端(比如 Linux, iOS9+/OSX 10.11+)并且在应用中开启了合适的套接字选项。
基于谷歌完成的流量分析和网络模拟,研究人员展示出 TFO 可以降低 15% 的 HTTP 延迟,平均减少 10% 的页面打开时间,在高网络延迟的情况下可以降低 40%。
拥塞避免和控制
早在 1984 年,John Nagle 记录了被称作“拥塞崩溃”的情况。这种情况可能影响节点间带宽不对称的网络。
John Nagle, RFC 896
在复杂网络中可以观察到拥塞控制的问题。我们已经发现了国防部的 IP 协议 - 纯的数据报协议,和 TCP 协议 - 传输层协议 一起使用的时候,传输层和数据报层的交流会造成一个不常见的拥塞问题。实际上,IP 网关很容易受到“拥塞崩溃”现象的应用,尤其是网关连接了两个带宽不一样的网络。
当主机的往返时间超过了最大传输间隔,主机会发送越来越多相同数据的拷贝到网络中去。最终,当交换节点上所有可用的缓存都被占满,数据包会被丢掉。数据包的往返时间达到最大值时,主机会多次发送相同的数据包,并且目的地也会收到每一个包的多个拷贝。这就是拥塞崩溃。
这种情况是稳定的。一但达到了网络的饱和点,如果数据包是随机丢弃的,网络会持续在这种糟糕的环境中运行。
这个报告指出了在 ARPANET 中,拥塞崩溃的现象不会发生,因为大多数的节点带宽相同,并且主干网络有潜在的额外容量。然而这些断言并没有很快就不再成立。在 1986 年,网络节点数量大量增加,一系列的拥塞崩溃问题横扫整个网络,一些情况下网络容量下降的千分之一,一些网络变得不可用。
为了解决这些问题,TCP 实现了多种机制来控制数据的发送速率:流量控制,拥塞控制和拥塞避免。
note
美国高级研究计划署(ARPANET) 是现代互联和的先驱,搭建了世界上第一个可操作的包交换网络。这个项目在 1969 年真是开展,在 1983 年 TCP/IP 作为主要的交流协议取代了更早的网络控制项目(NCP)。正如他们所说,其余的都变成了历史
流量控制
流量控制是用来阻止发送方发送接收方无法处理的数据的机制。接收方可能巨大负荷下,状态繁忙,或者是仅仅接受缓存大小决定的数据量。如图 2- 2 所示,为了解决这个问题,TCP 连接的两端都公布出它的接受窗口(rwnd),用来交流数据接收缓存的大小。
图 2-2 接受窗口(RWND)大小
当第一次建立连接的时候,两边都是用系统默认设定来初始化 rwnd 的值。一个典型的网络页面会将服务器的数据发送的客户端,这使得客户端的窗口可能会成为瓶颈。然而,如果客户端发送大量数据到服务器,那么服务器的窗口会成为主要的限制因素。
无论因为什么原因,当任何一段无法接受数据时,它会通知发送者一个更小的窗口。如果窗口大小达到 0,这会被认为是不要再发送任何数据知道应用层将缓存的数据处理完毕。这个工作流程在 TCP 协议的每一次连接中都会存在。ACK 包包含了每一端最新的 rwnd 值,允许两端根据网络容量和数据处理速度动态调整数据流的速率。
窗口缩放 RFC1323
原始的 TCP 规范分配了 16 比特给接受窗口大小,这就限制接收方和发送方直接交流的上限(2^16 比特,或者说 65,535 字节)。为了优化性能,这个上限经常不够大,尤其是具有高带宽延迟乘积的网络中。更多的信息可以在 带宽-延迟乘积中找到。
为了解决这个问题, RFC 1323 提出了 “TCP 窗口缩放” 选项,运行透明提升最大接受窗口大小从 65,535 字节到 1 GB。这个窗口缩放选项在三次握手中交换,带上了一个 16 bit 的左移大小,在之后的 ACK 中使用。
如今,主流的平台都默认启动 TCP 窗口缩放。然而,一些中间节点,路由器和防火墙可以重写甚至取消这一选项。如果你连接服务器或者客户端的时候不能充分利用带宽,检查窗口大小是一个好的开始。在 Linux 平台上,窗口缩放设置可以用一下命令开启
- $> sysctl net.ipv4.tcp_window_scaling
- $> sysctl -w net.ipv4.tcp_window_scaling=1
慢启动
尽管在 TCP 协议中有了流量控制,在 1980 年代中后期时拥塞崩溃依然是一个实际的问题。虽然流量控制防止了发送方发送过多的数据给接收方,但是仍然没有机制防止底层网络的崩溃。在连接建立的时候发送方和接收方都知道了可利用的带宽,但是仍然需要一个机制估计和适应底层网络的状况。
用一个例子来说明这样的适应是有益的,想象一下你在家中从远程服务器上下载一个很大的视频流,它可以通过占用整个下载线路来提供最高质量的用户体验,之后,家中的另一个用户建立了一个新的网络连接来下载一个网络更新。下载视频流的下行带宽数量突然减少,视频服务器必须调整它的传输速率,如果它继续按照之前的速率的话会导致网络使用效率下降,因为数据将会在堆积在中间网关,数据包会被丢掉。
在 1988 年,Van Jacobson 和 Michael J. Karels 记录了一系列算法来解决这一的问题,如慢启动,拥塞避免,快速重传和快速恢复。这四个算法都迅速的成为了 TCP 规范中必不可少的一部分。实际上,事实上,人们普遍认为因为 TCP 的这些更新避免了在上世纪80年代和90年代初流量指数增长情况下发生网络崩溃。
最好的理解慢启动的方法是了解它是怎么工作的。所有让我们回到位于纽约的客户端上,尝试从伦敦的服务器上检索一个文件。首先进行三次握手,这个过程中,如图 2-2 所示,通过 ACK 包,两端都宣告了各自的接受窗口(rwnd)大小。一旦最终的 ACK 包放入了网络中,我们可以开始交换应用数据。
估算客户端和服务器端可用容量的唯一方式是通过交换数据,这恰好是慢启动设计去做的事情。为了开始传输数据,服务器为每一个 TCP 连接初始化了一个新的拥塞窗口(cwnd),并将初始值设置成一个保守的,针对系统的值(在 Linux 上是 initcwnd)。
拥塞窗口大小(cwnd) 发送方在没有收到客户端 ACK 之前处在传输状态的最大数据量。
cwnd 不会在发送方和接收方之间相互通知或交换,在这种情况下,它会是伦敦服务器私有并且维护。一个新的规则是在客户端和服务器之间处在传输状态的数据量最大是 rwnd 和 cwnd 之间的最小值。目前为止一切顺利,但是服务器和客户端如何确定它们拥塞窗口的值呢?毕竟网络状态在一直发生变化,正如我们之前的例子中的那样,两点间的情况都在不断改变。如果能有一个算法不需要为每一个连接手动调整窗口大小,那将会是很棒的。
解决方法是随着接受数据包的过程不断增大窗口大小,这就是慢启动。最开始的时候,cwnd 的初始值被设置成 1 个数据段。 在 1999 年 4月,RFC 2581 将这个值更新成 4 个数据段。2013 年 4 月,RFC 6928 中将这个值增加到了 10 个数据段。
客户端和服务器之间处在传输状态的数据量最大是 rwnd 和 cwnd 之间的最小值,因此一个现代的服务器可以发送 10 个数据段给客户端,之后它必须停止发送,等待客户端的回复。之后,每收到一个 ACK 包,慢启动算法将 cwnd 的大小增加一个数据段,因此,每收到一个数据包可以发出去两个新的数据包,这被称作指数增长算法,如图 2-3。客户端和服务器端尝试快速收敛到它们之间可用网络带宽。
图 2-3 拥塞控制和拥塞避免
那么我们在构建网络应用的时候为什么要考虑慢启动呢?HTTP 和很多应用协议都是建立在 TCP 之上的,无论可用带宽是多少,每一个 TCP 连接都要经过慢启动的阶段,这意味着我们不能在一开始就利用线路的整个容量。
相反,我们从一个很小的拥塞窗口开始,每一个数据来回使这个窗口大小加倍(指数增长)。结论是达到一个特定的吞吐量是一个关于初始拥塞窗口大小和客户端-服务器端之间往返时间的函数。
$$Time=RTT*\lceil log_2(\frac{N}{initial cwnd}) \rceil$$
用上述情景作为一个例子来描述慢启动的影响:
- 客户端和服务器端的接受窗口大小: 65,535 字节(64 KB)
- 初始拥塞窗口大小:10 数据段(RFC 6982)
- 往返时间:56 ms (伦敦到纽约)
不需要考虑 64 KB 的接受窗口大小,新建立的 TCP 连接受限于拥塞窗口的大小。实际上,达到 64 KB 的接受窗口限制,我们需要先将滑动窗口大小增加到 45 个数据段,这会占用 大概 168 ms。
$$\frac{65,535 bytes}{1,460 bytes} \approx 45 segments$$
$$56 ms * \lceil log_2\frac{45}{10} \rceil = 168 ms$$
如图 2-4 所示,达到 64 KB 的吞吐量需要客户端和服务器端之间三个来回!实际上由于慢启动的因素,客户端和服务器端之间可能存在的数 Mbps 的容量对一个新的连接建立没有任何用处。
图 2-4. 拥塞控制窗口大小的增长
note
上述例子使用了新的(RFC 6928)的值,10个数据段作为拥塞网络的初始值。如果你使用老一点的大小,4 个数据段,重复上述计算,你会发现增加了一个往返时间,56 ms。
为了减少用来增加拥塞窗口的时间,我们可以降低往返时间。比如让服务器距客服端更近。或者我们可以根据 RFC 6928 将初始拥塞窗口大小增大到 10 个数据段。
慢启动对于大的下载流来说不是一个严重的问题,以为客户端和服务器之间会在几百毫秒之后达到最大窗口大小并且以接近最大值得速度传输数据,慢启动阶段消耗分套的整个传输周期上就变得很小。
然而大多数的 HTTP 连接是突发且短暂的,大多数的数据传输在达到窗口最大值之前就已经结束了。结果就是许多网络应用的性能受限于客户端和服务器之间的往返时间:慢启动限制了最大可用带宽,这对于小数据的传输有不利的影响。
慢启动重启
为了控制新连接的传输速率,TCP 还实现了慢启动重启(SSR)机制,在一段时间的空闲之后充值拥塞窗口的大小。简单的因为网络环境在一段时间之后很可能发生改变,安全的默认项便是重置窗口大小。
不出意外的是 SSR 对于可能空闲的 TCP 长连接有着重要影响。结果是通常建议在服务器上关闭 SSR 用于改善 HTTP 长连接的性能。在 Linux 平台上,SSR 可以用如下命令关闭
- $> sysctl net.ipv4.tcp_slow_start_after_idle
- sysctl -w net.ipv4.tcp_slow_start_after_idle=0
为了展示三次握手和慢启动阶段对于简单 HTTP 连接的影响,我们假设在纽约的客户端向伦敦的服务器通过建立一个新的 TCP 连接请求一个 64 KB 的数据,如图 2-5 所示,一下参数如下:
- 往返时间: 56 ms
- 客户端和服务器之间的带宽: 5 Mbps
- 客户端和服务器的接受窗口大小: 65,535 字节
- 初始拥塞窗口大小: 10 segments(10 * 1460 字节, 大约是 14 KB)
- 服务器生成响应的处理时间: 40ms
- 没有数据包丢失,接受每个数据包,GET 请求可以放进单一数据段中
图 2-5 通过一个新的 TCP 连接获取一个文件
0 ms
客户端通过一个 SYN 数据包开始 TCP 握手
28 ms
服务器通过 SYN-ACK 响应,并且告知 rwnd 大小
56 ms
服务器接受 SYN-ACK,告知 rwnd 大小,理解发送一个 HTTP GET
84 ms
收到 HTTP 请求
124 ms
服务器生成 64 KB 的响应数据包,在停下来等待 ACK 的时候,发送 10 TCP 数据段(cwnd 的初始大小是 10)
152 ms
客户端收到 10 个 TCP 数据段,为每一个数据段发送 ACK
180 ms
服务器增加 cwnd 大小,发送 20 个 TCP 数据段
208 ms
客户端收到 20 个 TCP 数据段,为每一个数据段发送 ACK
236 ms
服务器增加 cwnd 大小,发送 15 个 TCP 数据段
264 ms
客户端收到 15 个 TCP 数据段,为每一个数据段发送 ACK
在往返时间为 56 ms 的线路上通过新建 TCP 连接发送 64 KB 的数据需要 264 ms!让我们假设客户端可以重用相同的 TCP 连接作为对比,再次发送相同的请求(图 2-6)
图 2-6 通过已有 TCP 连接获取文件
0 ms
客户端发送 HTTP 请求
28 ms
服务器收到 HTTP 请求
68 ms
服务器生成 64 KB 的响应数据,由于 cwnd 的值已经大于 45 数据段,因此同时发送所有的数据
96 ms
客户端收到所有的 45 个数据段,为每一个数据段发送 ACK
同样的请求在相同的连接上,但是没有了三次握手和慢启动阶段的消耗,只需要 96 ms 便完成了传输,性能提升了 275%!
在两种情况下,客户端和服务器实际上的 5 Mbps 的带宽在 TCP 连接的建立阶段并没有影响。相反的,限制因素是延迟和拥塞窗口大小。
在已有连接上,如果我们增大往返时间实际上只会让第一次和第二次连接的间隙变大,你可以调整上述参数作为练习。一旦你对 TCP 拥塞控制有了直觉上的认识,一系列的优化方式像保持连接,管道,复用只需要一点动机。
增大 TCP 初始拥塞窗口
新的 RFC 6928 将初始 cwnd 大小增加到 10 个数据段对于所有用户和运行在 TCP 上面的应用是一个提升性能的简单方法。好消息是许多操作系统已经在最新的内核中增加了这个值,你可以查看响应的文档和发布日志。
对于 Linux,在 2.6.39 之后的版本中的默认值是 IW10。然而升级到 3.2 以后的版本可以得到其他重要的更新,查看按比例缩减
拥塞避免
认识到 TCP 针对数据包丢失作为一种用来提升性能的反馈机制是相当重要的。换句话说,问题并不是数据包是否会丢失,而是什么时候丢失。慢启动使用一个保守的窗口大小来初始化连接,在超过接收方的流量控制窗口-配置好的拥塞阈值(ssthresh)窗口,或者数据包丢失前的每一次往返中加倍数据传输量,之后便有拥塞避免算法控制。
拥塞避免的一个潜在假设是数据包丢失暗示了网络拥塞,我们在线路或者路由中的某一点遇到了拥塞造成了数据包丢失,因此我们需要调整我们的窗口大小来避免更多的数据包丢失或者使网络过载。
一旦设定了拥塞窗口,拥塞避免算法规定了如何增长窗口来最小化之后的丢失。在某一个时间点上,再一次丢失了数据包,便会再进行一次这样的过程。如果你曾经观察过一个 TCP 连接,会发现吞吐量呈锯齿状,现在你知道了为什么会这样:这是因为拥塞控制,拥塞避免算法在调整拥塞窗口的大小来适应网络中的丢包。
最终,改善拥塞控制和拥塞避免的算法在学术研究和商业产品中都是一个活跃的领域,不同的网络类型,数据传输方式等等有不同的提升方法。今天,取决于你的平台,你会运行下列几种算法之一,TCP Tahoe and Reno,TCP Vegas, TCP New Reno, TCP BIC, TCP CUBIC (Linux 默认), 或者 Compound TCP (Windows 默认)等等。无论如何实现,核心都是拥塞避免和拥塞控制。
TCP 的比例缩减
觉得从丢包中恢复的最佳方式并不是一个简单事情,如果你太冒进的话会产生断断续续的丢包,影响整个连接的吞吐量;如果调整的不够及时会造成更多的丢包。开始的时候,TCP 使用乘减加增(AIMD)算法:当发生丢包时,减半拥塞窗口大小,然后每次往返缓慢的增加固定值。然而在许多情况下,AIMD 太过保守,因此开发出了新的算法。
比例缩减(PRR)是 RFC 6937 中规定的新算法,它的目标是加快丢包发生时的恢复速度。根据谷歌完成的测量,在完成部署新的算法之后,大约减少了发生丢包的连接的 3-10% 的平均延迟。
PRR 现在是 Linux 3.2 以上内核的默认拥塞控制算法,这是一个更新服务器的另一个好理由。
带宽延迟乘积
TCP 中内建的拥塞控制和拥塞避免机制带来了另一个有关性能规律:发送方和接收方的最优窗口大小取决于它们之间往返时间和目标数据速率。
为了理解这个规律,首先回顾一下最大未接收的在途数据量由最小接受窗口(rwnd)和拥塞窗口(cwnd)大小决定。挡墙接受窗口大小通过每一个 ACK 通信,拥塞窗口大小由发送方通过拥塞控制和避免算法动态决定。
物理发送方和是接收方查看了这个最大未接收数据量,它必须停下来等待另一端接受一些数据包。它需要等多久呢?这取决于两者之间的往返时间。
带宽延迟成绩(BDP) 数据线路容量和点到点延迟的乘积。结果是在任意时间最大在途的未接受数据量。
无论是发送方还是接收方经常性地被迫停下来等待之前数据包的 ACK 会造成数据流的间隙(图 2-7),这会限制连接的最大吞吐量。
图 2-7 由于过小的拥塞窗口造成的传输间隙
为了解决这个问题,窗口大小通常要调的足够大,两方均可以持续接受数据直到收到之前数据包 ACK 到达,这样便没有间隙,达到了最高吞吐量。因此,最优的窗口大小取决于往返时间!选一个较小的窗口大小,你会限制连接的吞吐量,两者之间的无论可用或者建议带宽是多少。
那么 rwnd 和 cwnd 的值应该是多大呢?实际计算一个简单的例子。让我们建设最小的 cwnd 和 rwnd 大小是 16 KB,往返时间是 100 ms:
$$16 KB = (1610248) = 131,072bits$$
$$\frac{131,072bits}{0.1s}=1,310,720 bits/s$$
$$1,310,720 bits/s = \frac{1,310,720}{1,000,000}=1.31Mbps $$
无论接收方和发送方之间的可用带宽是多少,TCP 连接的吞吐量不会超过 1.31 Mbps!为了达到更高的吞吐量,我们需要提升窗口大小或者降低往返延迟。
类似的,当我们知道了往返时间和两端的可用带宽,我们可以计算最优的窗口大小。在这个场景中,让我们假设往返时间是 100 ms,但是发送方有 10 Mbps 的可用带宽,接收方有 100 Mbps 的高吞吐线路。假设两者之间没有网络拥塞,我们的目标是充分利用到客户端的 10 Mbps 的线路:
$$10 Mbps = 10 * 1,000,000 = 10,000,000 bits/s$$
$$10,000,000 bits/s = \frac{10,000,000}{8 * 1024} = 1,221 KB/s$$
$$1,221 KB/s * 0.1s = 122.1 KB$$
窗口大小最少需要到 122.1 KB 才能最大化 10 Mbps 的线路。回顾一下 64 Kb 的最大接收窗口在没有窗口缩放的情况下是不够用的,查看窗口缩放(RFC 1323),仔细检查一下的客户端和服务器的设置!
好消息是,窗口大小的协商和调整是由网络协议栈自动管理并进行相应的调整。坏消息是,有时它仍然是 TCP 性能的影响因素。如果你曾经想要弄清楚为什么你的连接只能用可用带宽的一部分传输数据,很可能是因为数据窗口过小,网络环境过差,丢包让拥塞窗口重置,或者是网络结构限制了网络连接的速率。
高速局域网的带宽延迟乘积
BDP 是往返时间和目标数据速率的成绩。尽管当往返时间通常是高延迟网络中的瓶颈,他也可以成为局域网中的瓶颈!
为了达到 1 ms 往返时间下 1 Gbit/s 的速率,我们至少需要 122 KB 的拥塞窗口。这个计算和我们之前的计算相似,我们简单的给目标速率添加了几个零并且减少了往返延迟的几个零。
线头阻塞
TCP 提供了一个在不可靠信道上建立一个可靠网络的抽象,这包含了基本的包错误检查和纠正,顺序传递,丢包重发等,同时也包括了设计用来提升网络效率的流量控制,拥塞控制和拥塞避免。将这些特点组合起来是的 TCP 成为了大多数应用的首选。
尽管 TCP 是最常用的选择,但不是唯一的选择,或者说并不是所有情况下的最优选择。具体来说,如顺序且可靠的数据包数据包传递这些特点并不总是必要的但是却造成了不必要的延迟和负面的性能影响。
为了理解这些情况,我们回顾一下每一个 TCP 数据包都有一个序列号,并且数据必须按照顺序送达接收方,如图 2-8 所示,这简化了应用层的代码。
图 2-8 TCP 中的线头阻塞
然而这是通过引入数据包到达时间的不可预测的延迟因素完成的,通常称之为抖动(Jitter),这对应用性能是负面影响。
一些应用可能并不需要可靠或者顺序传递数据包,如果每一个数据包都是单独的信息,那么顺序传递严格来说是没有必要的,如果每一个数据包都覆盖了之前的所有信息,那么就完全不需要可靠传递。不幸的是,TCP 协议并不提供这样的配置,所有的数据包是有序的,且按顺序传递。
应用层可以处理无序送达或者数据包丢失的情况,并且它们对延迟或者抖动是敏感的话,最好选择其他的协议,比如说 UDP。
丢包是正常的
实际上,数据包丢失是达到 TCP 协议最高性能的必要条件!一个丢失的数据充当了反馈机制的医院,这允许接收方或者发送方调整他们的发送速率用于避免使网络过载,并且最小化延迟,查看本地路由器的缓存过满。更重要的是一些应用容忍丢包而不受负面影响,音频,视频和游戏状态更新等常见的应用数据不需要可靠或者顺序传递的应用,这也是为什么 WebRTC 使用 UDP 作为底层协议。
如果一个数据包丢失了,那么音频编码可以简单的茶语一个短暂的间隔并且处理接下来收到的数据包。如果这个间隔很小,用户可能注意不到,而且等到丢失的数据需要承担引入多变的输出暂停的因素,这可能造成更差的用户体验。
同样的,如果我们更新游戏状态,收到 T 时刻的数据包后,等待 T-1 时刻的数据包是没有意义的。理想情况下,我们在收到每一个数据包后更新数据,但是为了避免游戏延迟,我们可以接受间隙的丢包。
TCP 协议优化
TCP 是一个设计用来适应大多数网络连接并且充分利用底层网络。因此,优化 TCP 协议最好的方式是让 TCP 感知网络条件并针对上层和下层的需求调整自身的行为,无线网络可能需要不同的拥塞控制算法,一些应用可能需要自定义服务质量(QoS)的语义来传递最好的体验。
不同的应用需求间的相互作用以及众多 TCP 算法间的相互结合是的 TCP 的调整和优化是学术界和工业界永无止境的研究领域。在这一章中,我们肤浅的了解了一下影响 TCP 性能的几个因素。附加的一些机制如选择性确认(SACK),确认延迟,快速重传等等,使得 TCP 会话变得十分复杂(或者说有趣,看你怎么理解)。
尽管算法的细节和反馈机制会不断更新,核心原则和它们的含义是不会改变的:
- TCP 三次握手引入了一整个往返时间的延迟
- TCP 慢启动应用在每一个新建立的连接上
- TCP 流和拥塞控制条件所有连接的吞吐量
- TCP 吞吐由当前拥塞窗口大小调节
结论是在现代的高速网络下, TCP 连接的速率通常有接收方和发送方直接的延迟决定的。尽管带宽在不断增长,延迟受限于光速,并且已经十分接近最大值。在大多数情况下,延迟而不是带宽是 TCP 的瓶颈,如图 2-5 所示。
调节服务器设置
在调节 TCP 中具体的缓冲区大小和超时变量之前,你最好先升级主机系统到最新的版本。TCP 最佳实践和底层算法让他的性能不断得到提升,大多数的变化只能在最新的内核中使用。简单来说,保存你的服务器最新以确保发送端和接收端的 TCP 连接的交流最优。
note
升级系统内核版本看起来是一个很简单的建议。然而实际上会遇到很多抵抗。许多现存的服务器是按照特定的内核版本设定的,一个系统管理员不愿意升级。
每一个升级带来了它的风险,但是也得到了最好的 TCP 性能,这像是一个你可以进行的投资。
在使用了最新的内核之后,最佳的实践是确保你的服务器按照如下配置:
增大初始拥塞窗口
一个更大的初始拥塞窗口运行 TCP 在第一次往返中传输更多的数据,有效加速窗口增长。
慢启动重置
禁用慢启动可以提升周期性发送数据的 TCP 长连接的性能
窗口缩放 (RFC 1323)
启用窗口缩放增大了最大接受窗口,运行高延迟网络达到更好的性能。
TCP 快速建立
允许初始 SYN 包在特定情况下携带应用数据。TFO 是一个新的优化,这需要客户端和服务器双方的支持,研究一下你的应用是否可以使用。
在最新的内核中将上述设置组合起来可以实现最好的性能,每一个独立的 TCP 连接都可以延迟低和带宽高。
note
对于 Linux 用户,ss 是一个很有用的工具,显示已经打开的套接字连接的统计信息。在命令行中运行 ss --options --extended --memory --processes --info 观察每一个连接和各自的连接设置
配置用户行为
调整 TCP 的性能可以为服务端和客户端的每一个连接提供最佳的吞吐量和延迟。然而,应用程序如何使用每一个新的或者已建立TCP连接对性能也有很大的影响:
- 没有比特比不发送更快;发送更少的比特
- 我们不能让比特传输的更快,但是我们可以让它们更近。
- TCP 连接重用对性能有关键的影响
减少不必要的数据传输当然是最简单有效的方法。例如,减少不必要的资源,或者通过适当的压缩算法减少数据量。其次,我们可以在距客户端更近的地方部署服务器,比如使用 CDN, 将减小往返延迟,并显着提高 TCP 的性能。最后,尽可能的重用现有的 TCP 连接来最大限度地减少慢启动和拥塞控制机制所带来的开销。
性能检查列表
优化 TCP 连接无论对哪种应用,哪种接入到服务器的连接来说,都是至关重要的。一个简短的检查列表:
- 升级到最新版本的服务器内核
- 确保拥塞窗口大小设置为10
- 确保窗口缩放启用
- 禁用闲置后慢启动
- 消除冗余数据传输
- 压缩传输数据
- 在离用户更近的地方部署服务器,减少往返时间
- 尽可能重用 TCP 连接
- 研究 为 HTTP 设置 TCP 的推荐