- HTTP 紧挨着 TCP,位于其上层,所以 HTTP 事务的性能在很大程度上取决于底层 TCP 通道的性能。
- TCP 是个很复杂的话题, 所以这里我们只能提供对 TCP 性能的简要概述。
1. HTTP 事务的时延
- HTTP 事务主要的连接、传输以及处理时延:
- 注意,与建立 TCP 连接,以及传输请求和响应报文的时间相比,事务处理时间可能是很短的。除非客户端或服务器超载,或正在处理复杂的动态资源,否则 HTTP 时延就是由 TCP 网络时延构成的。
- HTTP 事务的时延主要原因:
- 客户端首先需要根据 URI 确定 Web 服务器的 IP 地址和端口号。如果最近没有对 URI 中的主机名进行访问,通过 DNS 解析系统将 URI 中的主机名转换成一个 IP 地址可能要花费数十秒的时间。(幸运的是,大多数 HTTP 客户端都有一个小的 DNS 缓存,用来保存近期所访问站点的 IP 地址。如果已经在本地“缓存”(记录)了 IP 地址,查询就可以立即完成。因为大多数 Web 浏览器浏览的都是少数常用站点,所以通常都可以很快地将主机名解析出来。)
- 接下来,客户端会向服务器发送一条 TCP 连接请求,并等待服务器回送一个请求接受应答。每条新的 TCP 连接都会有连接建立时延。这个值通常最多只有一两秒钟,但如果有数百个 HTTP 事务的话,这个值会快速地叠加上去。
- 一旦连接建立起来了,客户端就会通过新建立的 TCP 管道来发送 HTTP 请求。数据到达时,Web 服务器会从 TCP 连接中读取请求报文,并对请求进行处理。因特网传输请求报文,以及服务器处理请求报文都需要时间。
- 然后,Web 服务器会回送 HTTP 响应,这也需要花费时间。
- 这些 TCP 网络时延的大小取决于硬件速度、网络和服务器的负载,请求和响应报文的尺寸,以及客户端和服务器之间的距离。TCP 协议的技术复杂性也会对时延产生巨大的影响。
2. 性能聚焦区域
- 对 HTTP 程序员产生影响的、最常见的 TCP 相关时延,其中包括:
- TCP 连接建立握手;
- 用于捎带确认的 TCP 延迟确认算法;
- TCP 慢启动拥塞控制;
- 数据聚集的 Nagle 算法;
- TIME_WAIT 时延和端口耗尽。
- 如果要编写高性能的 HTTP 软件,就应该理解上面的每一个因素。如果不需要进行这个级别的性能优化,可以跳过下面的内容。
3. TCP 连接的握手时延
- 建立一条新的 TCP 连接时,甚至是在发送任意数据之前,TCP 软件之间会交换一系列的 IP 分组,对连接的有关参数进行沟通。如果连接只用来传送少量数据,这些交换过程就会严重降低 HTTP 的性能。
- TCP 连接握手需要经过以下 3 个步骤:
- 请求新的 TCP 连接时,客户端要向服务器发送一个小的 TCP 分组(通常是 40 ~ 60 个字节)。这个分组中设置了一个特殊的 SYN 标记,说明这是一个连接请求。(参见下图a)。
- 如果服务器接受了连接,就会对一些连接参数进行计算,并向客户端回送一个 TCP 分组,这个分组中的 SYN 和 ACK 标记都被置位,说明连接请求已被接受(参见下图b)。
- 最后,客户端向服务器回送一条确认信息,通知它连接已成功建立(参见下图c)。现代的 TCP 栈都允许客户端在这个确认分组中发送数据。
- HTTP 程序员永远不会看到这些分组——这些分组都由 TCP/IP 软件管理,对其是不可见的。HTTP 程序员看到的只是创建 TCP 连接时存在的时延。
- 通常 HTTP 事务都不会交换太多数据,此时,SYN/SYN+ACK 握手(参见上图a和上图b)会产生一个可测量的时延。TCP 连接的 ACK 分组(参见上图c)通常都足够大,可以承载整个 HTTP 请求报文,而且很多 HTTP 服务器响应报文都可 以放入一个 IP 分组中去。
- 最后的结果是,小的 HTTP 事务可能会在 TCP 建立上花费 50%,或更多的时间。后面会讨论 HTTP 是如何通过重用现存连接,来减小这种 TCP 建立时延所造成的影响的。
4. 延迟确认
- 由于因特网自身无法确保可靠的分组传输(因特网路由器超负荷的话,可以随意丢弃分组),所以 TCP 实现了自己的确认机制来确保数据的成功传输。
- 确认分组:每个 TCP 段都有一个序列号和数据完整性校验和,每个段的接收者收到完好的段时,都会向发送者回送小的确认分组。如果发送者没有在指定的窗口时间内收到确认信息,发送者就认为分组已被破坏或损毁,并重发数据。
- 由于确认报文很小,所以 TCP 允许在发往相同方向的输出数据分组中对其进行“捎带”。TCP 将确认信息与将要输出的数据分组结合在一起,可以更有效地利用网络。
- 延迟确认算法:为了增加确认报文找到同向传输数据分组的可能性,很多 TCP 栈都实现了一种“延迟确认”算法。延迟确认算法会在一个特定的窗口时间(通常是 100 ~ 200 毫秒)内将输出确认存放在缓冲区中,以寻找能够捎带它的输出数据分组。如果在那个时间段内没有输出数据分组,就将确认信息放在单独的分组中传送。
- 但是,HTTP 具有双峰特征的请求 - 应答行为降低了捎带信息的可能。当希望有相同方向的输出数据分组的时候,偏偏没有那么多。通常,延迟确认算法会引入相当大的时延。根据所使用操作系统的不同,可以调整或禁止延迟确认算法。
- 注意:在对 TCP 栈的任何参数进行修改之前,一定要对自己在做什么有清醒的认识。TCP 中引入这些算法的目的是防止设计欠佳的应用程序对因特网造成破坏。对 TCP 配置进行的任意修改,都要绝对确保应用程序不会引发这些算法所要避免的问题。
5. TCP 慢启动
- TCP 慢启动(slow start):TCP 数据传输的性能还取决于 TCP 连接的使用期(age)。TCP 连接会随着时间进行自我“调谐”,起初会限制连接的最大速度,如果数据成功传输,会随着时间的推移提高传输的速度。这种调谐被称为 TCP 慢启动(slow start),用于防止因特网的突然过载和拥塞。
- 打开拥塞窗口:TCP 慢启动限制了一个 TCP 端点在任意时刻可以传输的分组数。简单来说,每成功接收一个分组,发送端就有了发送另外两个分组的权限。如果某个 HTTP 事务有大量数据要发送,是不能一次将所有分组都发送出去的。必须发送一个分组,等待确认;然后可以发送两个分组,每个分组都必须被确认,这样就可以发送四个分组了,以此类推。这种方式被称为“打开拥塞窗口”。
- 由于存在这种拥塞控制特性,所以新连接的传输速度会比已经交换过一定量数据的、“已调谐”连接慢一些。由于已调谐连接要更快一些,所以 HTTP 中有一些可以重用现存连接的工具。
6. Nagle 算法与 TCP_NODELAY
- TCP 有一个数据流接口,应用程序可以通过它将任意尺寸的数据放入 TCP 栈中——即使一次只放一个字节也可以!但是,每个 TCP 段中都至少装载了 40 个字节的标记和首部,所以如果 TCP 发送了大量包含少量数据的分组,网络的性能就会严重下降。
- 发送端傻窗口综合症:即发送大量单字节分组的行为。这种行为效率很低、违反社会道德,而且可能会影响其他的因特网流量。
- Nagle 算法:根据其发明者 John Nagle 命名,试图在发送一个分组之前,将大量 TCP 数据绑定在一起,以提高网络效率。RFC 896“IP/TCP 互连网络中的拥塞控制”对此算法进行了描述。
- Nagle 算法鼓励发送全尺寸的段(LAN 上最大尺寸的分组大约是 1500 字节,在因特网上是几百字节)。
- 只有当所有其他分组都被确认之后,Nagle 算法才允许发送非全尺寸的分组。如果其他分组仍然在传输过程中,就将那部分数据缓存起来。只有当挂起分组被确认,或者缓存中积累了足够发送一个全尺寸分组的数据时,才会将缓存的数据发送出去。这个算法有几种变体,包括对超时和确认逻辑的修改,但基本算法会使数据的缓存比一个 TCP 段小一些。
- Nagle 算法会引发几种 HTTP 性能问题:
- 首先,小的 HTTP 报文可能无法填满一个分组,可能会因为等待那些永远不会到来的额外数据而产生时延。
- 其次,Nagle 算法与延迟确认之间的交互存在问题——Nagle 算法会阻止数据的发送,直到有确认分组抵达为止,但确认分组自身会被延迟确认算法延迟 100 ~ 200 毫秒。
- 使用管道化连接(稍后介绍)时这些问题可能会更加严重,因为客户端可能会有多条报文要发送给同一个服务器,而且不希望有时延存在。
- TCP_NODELAY 参数:HTTP 应用程序常常会在自己的栈中设置参数 TCP_NODELAY,禁用 Nagle 算法,提高性能。如果要这么做的话,一定要确保会向 TCP 写入大块的数据,这样就不会产生一堆小分组了。
7. TIME_WAIT 累积与端口耗尽
- TIME_WAIT 端口耗尽是很严重的性能问题,会影响到性能基准,但在现实中相对较少出现。大多数遇到性能基准问题的人最终都会碰到这个问题,而且性能都会变得出乎意料地差,所以这个问题值得特别关注。
- 当某个 TCP 端点关闭 TCP 连接时,会在内存中维护一个小的控制块,用来记录最近所关闭连接的 IP 地址和端口号。这类信息只会维持一小段时间,通常是所估计的最大分段使用期的两倍(称为2MSL,通常为 2 分钟)左右,以确保在这段时间内不会创建具有相同地址和端口号的新连接。实际上,这个算法可以防止在两分钟内创建、关闭并重新创建两个具有相同 IP 地址和端口号的连接。(注:将 2MSL 的值取为 2 分钟是有历史原因的。很早以前,路由器的速度还很慢,人们估计,在将一个分组的复制副本丢弃之前,它可以在因特网队列中保留最多一分钟的时间。现在,最大分段生存期要小得多了。)
- 现在高速路由器的使用,使得重复分组几乎不可能在连接关闭的几分钟之后,出现在服务器上。有些操作系统会将 2MSL 设置为一个较小的值,但修改此值时要特别小心。分组确实会被复制,如果来自之前连接的复制分组插入了具有相同连接值的新 TCP 流,会破坏 TCP 数据。
- 2MSL 的连接关闭延迟通常不是什么问题,但在性能基准环境下就可能会成为一个问题。进行性能基准测试时,通常只有一台或几台用来产生流量的计算机连接到某系统中去,这样就限制了连接到服务器的客户端 IP 地址数。而且,服务器通常会在 HTTP 的默认 TCP 端口 80 上进行监听。用 TIME_WAIT 防止端口号重用时,这些情况也限制了可用的连接值组合。
- 在只有一个客户端和一台 Web 服务器的异常情况下,构建一条 TCP 连接的 4 个值:< 源 IP 地址、源端口号、目的 IP 地址、目的端口号 >其中的 3 个都是固定的,只有源端口号可以随意改变。
- 客户端每次连接到服务器上去时,都会获得一个新的源端口,以实现连接的唯一性。但由于可用源端口的数量有限(比如,60 000 个),而且在 2MSL 秒(比如,120秒)内连接是无法重用的,连接率就被限制在了 60 000/120=500 次 / 秒。如果再不断进行优化,并且服务器的连接率不高于 500 次/秒,就可确保不会遇到TIME_ WAIT端口耗尽问题。要修正这个问题,可以增加客户端负载生成机器的数量,或者确保客户端和服务器在循环使用几个虚拟 IP 地址以增加更多的连接组合。
- 即使没有遇到端口耗尽问题,也要特别小心有大量连接处于打开状态的情况,或为处于等待状态的连接分配了大量控制块的情况。在有大量打开连接或控制块的情况下,有些操作系统的速度会严重减缓。