TCP连接管理
HTTP 紧挨着 TCP, 位于其上层, 所以 HTTP 事务的性能在很大程度上取决于底层TCP 通道的性能。
TCP性能
image.pngHTTP 事务的时延有以下几种主要原因。
(1) 通过 DNS 解析系统将 URI 中的主机名转换成一个 IP地址要花费对应的时
间
(2) 每条新的 TCP 连接都会有连接建立时延,但如果有数百个 HTTP 事务的
话, 这个时间消耗值会快速地叠加上去。
(3) 网络传输请求报文及服务器处理请求报文都需要时间。
(4) Web 服务器会回送 HTTP 响应的花费时间。
这些网络时延的大小取决于硬件速度、 网络和服务器的负载, 请求和响应报文的尺寸, 以及客户端和服务器之间的距离。 TCP 协议的技术复杂性也会对时延产生巨大的影响。
性能聚焦区域:
TCP 连接建立握手;
TCP 慢启动拥塞控制;
TCP 延迟确认算法;
Nagle 算法;
1)TCP连接的握手时延
image.png
TCP 连接握手需要经过以下几个步骤。
(1) 请求新的 TCP 连接时, 客户端要向服务器发送一个小的 TCP 分组(通常是 40 ~60 个字节)。 这个分组中设置了一个特殊的 SYN 标记, 说明这是一个连接请求。
(2) 如果服务器接受了连接, 就会对一些连接参数进行计算, 并向客户端回送一个TCP 分组, 这个分组中的 SYN 和 ACK 标记都被置位, 说明连接请求已被接受
(3) 客户端向服务器回送一条确认信息, 通知它连接已成功建立。现在的 TCP栈都允许客户端在这个确认分组中发送数据。
2)延迟确认(ACK)
由于网络自身无法确保可靠的分组传输( 如果网络设备超负荷的话, 可以随意丢弃分组), 所以 TCP 实现了自己的确认机制来确保数据的成功传输。每个 TCP 段都有一个序列号和数据完整性校验和。 服务端收到完好的TCP段时,都会向发送者回送小的确认报文。 如果发送者没有在指定的窗口时间内收到确认信息, 发送者就认为分组已损毁或丢失, 并重发数据。
由于确认报文很小, 所以 TCP 允许在发往相同方向的输出数据分组中对其进行“捎带”。 TCP 将返回的确认信息与输出的数据分组结合在一起, 可以更有效地利用网络。 为了增加确认报文找到同向传输数据分组的可能性, 很多 TCP栈都实现了一种“延迟确认” 算法。 延迟确认算法会在一个特定的窗口时间( 通
常是 100 ~ 200 毫秒) 内将输出确认存放在缓冲区中, 以寻找能够捎带它的
输出数据分组。 如果在那个时间段内没有输出数据分组, 就将确认信息放在单
独的分组中传送。
但是当希望有相反方向回传分组的时候, 偏偏没有那么多。 通常, 延迟确认算法会引入相当大的时延。
3)TCP慢启动
慢启动算法思路:
主机开发发送数据报时,如果立即将大量的数据注入到网络中,可能会出现网络的拥塞。慢启动算法就是在主机刚开始发送数据报的时候先探测一下网络的状况,如果网络状况良好,发送方每发送一次文段都能正确的接受确认报文段。那么就从小到大的增加拥塞窗口的大小,即增加发送窗口的大小, 用于防止因特网的突然过载和拥塞。
TCP 慢启动限制了一个 TCP 端点在任意时刻可以传输的分组数。 简单来说, 每成功接收一个分组, 发送端就有了发送另外两个分组的权限。 如果某个HTTP 事务有大量数据要发送, 是不能一次将所有分组都发送出去的。 必须发送一个分组, 等待确认; 然后可以发送两个分组, 每个分组都必须被确认,这样就可以发送四个分组了,以此类推。 这种方式被称为“打开拥塞窗口”。
4)Nagle算法与TCP_NODELAY
TCP/IP协议中,无论发送多少数据,总是要在数据前面加上协议头,同时,对方接收到数据,也需要发送ACK表示确认。为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据。(一个连接会设置MSS参数,因此,TCP/IP希望每次都能够以MSS尺寸的数据块来发送数据)。Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。如果 TCP 发送了大量包含少量数据的分组, 网络的性能就会严重下降。
Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。
Nagle 算法鼓励发送全尺寸的数据段:Nagle 算法试图在发送一个分组之前, 将大量TCP 数据绑定在一起, 以提高网络效率。
一个 TCP 连接上最多只能有一个未被确认的未完成的小分组,在该分组ACK 到达之前不能发送其他的小分组。如果其他分组仍然在传输过程中,就将那部分数据缓存起来。
只有当挂起分组被确认, 或者缓存中积累了足够发送一个全尺寸分组的数据时, 才会将缓存的数据发送出去。
Nagle 算法会引发几种 HTTP 性能问题。 首先, 小的 HTTP 报文可能无法填满一个分组, 可能会因为等待那些永远不会到来的额外数据而产生时延。 其次, Nagle 算法与延迟确认之间的交互存在问题——Nagle 算法会阻止数据的发送, 直到有确认分组抵达为止, 但确认分组自身会被延迟确认算法延迟 100~ 200 毫秒。
HTTP 应用程序常常会在自己的栈中设置参数 TCP_NODELAY, 禁用Nagle 算法,提高性能。(Tomcat通过server.xml进行设置,默认为true)
串行事务处理
如果只对连接进行简单的管理, TCP 的性能时延可能会叠加起来。 比如, 假设有一 个包含了 3 个嵌入图片的 Web 页面。 浏览器需要发起 4 个 HTTP 事务来显示此页面:1 个用于顶层的 HTML 页面, 3 个用于嵌入的图片。 如果每个事务都需要(串行地建立) 一条新的连接, 那么连接时延和慢启动时延就会叠加起来 。
image.png
除了串行加载引入的实际时延之外, 加载一幅图片时, 页面上其他地方都没有动静也会让人觉得速度很慢。 用户更希望能够同时加载多幅图片。 10串行加载的另一个缺点是, 有些浏览器在对象加载完毕之前无法获知对象的尺寸,而且它们可能需要尺寸信息来决定将对象放在屏幕的什么位置上, 所以在加载了足够多的对象之前, 无法在屏幕上显示任何内容。 在这种情况下, 可能浏览器串行装载对象的进度很正常, 但用户面对的却是一个空白的屏幕, 对装载的进度一无所知。
还有几种现存和新兴的方法可以提高 HTTP 的连接性能:
• 并行连接
通过多条 TCP 连接发起并发的 HTTP 请求。
• 持久连接
重用 TCP 连接, 以消除连接及关闭时延。
• 管道化连接
通过共享的 TCP 连接发起并发的 HTTP 请求。
并行连接
如前所述, 浏览器可以先完整地请求原始的 HTML 页面, 然后请求第一个嵌入对象, 然后请求第二个嵌入对象等, 以这种简单的方式对每个嵌入式对象进行串行处理。 但这样实在是太慢了!
HTTP 允许客户端(浏览器)打开多条TCP连接, 并行地执行多个 HTTP 事务(每个TCP连接处理一个HTTP事务)。 在这个例子中, 并行加载了四幅嵌入式图片, 每个事务都有自己的 TCP 连接。 页面上的每个组件都包含一个独立的HTTP 事务
image.png
image.png
并行连接不一定更快 :
打开大量连接会消耗很多内存资源, 从而引发自身的性能问题。 复杂的 Web页面可能会有数十或数百个内嵌对象。 客户端可能可以打开数百个连接, 但 Web服务器通常要同时处理很多其他用户的请求, 所以很少有 Web 服务器希望出现这样的情况。 一百个用户同时发出申请, 每个用户打开 100 个连接, 服务器就要负责处理10 000 个连接。 这会造成服务器性能的严重下降。 对高负荷的代理来说也同样如此。
实际上, 浏览器确实使用了并行连接, 但它们会将向同一个域名请求的并行连接的总数限制为一个较小的值。
浏览器同域名请求的最大并发数限制:
image.png
持久连接
Web 客户端经常会打开到同一个站点的连接。 比如, 一个 Web 页面上的大部分内嵌图片通常都来自同一个 Web 站点, 而且相当一部分指向其他对象的超链通常都指向同一个站点。 因此, 初始化了对某服务器 HTTP 请求的应用程序很可能会在不久的将来对那台服务器发起更多的请求(比如, 获取在线图片)。
这种性质被称为站点局部性(site locality)。
HTTP/1.1(以及 HTTP/1.0 的各种增强版本) 允许 HTTP 设备在事务处理结束之后将 TCP 连接保持在打开状态, 以便为未来的 HTTP 请求重用现存的连接。
在事务处理结束之后仍然保持在打开状态的 TCP 连接被称为持久连接。 非持久连接会在每个事务结束之后关闭。 持久连接会在不同事务之间保持打开状态,直到客户端或服务器决定将其关闭为止。
重用已对目标服务器打开的空闲持久连接, 就可以避开缓慢的连接建立阶段。而且,已经打开的连接还可以避免慢启动的拥塞适应阶段, 以便更快速地进行数据的传输。
Connection: Keep-Alive
image.png
在持久连接中,每一个HTTP请求都是串行的。每个请求的发送都必须等待上一个请求的响应。
管道化连接
HTTP/1.1 允许在持久连接上使用管道化(pipeline)技术。 这是相对于 keep-alive 连接的又一性能优化。
在响应到达之前, 可以将多条请求放入队列。 当第一条请求通过网络流向另一端的服务器时, 第二条和第三条请求也可以开始发送了。 在高时延网络条件下, 这样做可以降低网络的环回时间, 提高性能。
image.png
对管道化连接有几条限制:
必须按照与请求相同的顺序回送 HTTP 响应(如果顺序发送了请求1/2/3,那么无论服务器处理哪个请求更快,相应的时候必须按照请求的顺序响应)。 HTTP 报文中没有序列号标签, 因此如果收到的响应失序了, 就没办法将其与请求匹配起来了。此时容易引起一个问题:头部阻塞。
HTTP 客户端必须做好连接会在任意时刻关闭的准备。 如果客户端打开了一条持久连接, 并立即发出了 10 条请求,服务器可能在只处理了5 条请求之后关闭连接,剩下的 5 条请求会失败,客户端必须能够应对这些过早关闭连接的情况, 重新发出这些请求。
只有幂等的请求能够被管线化。HTTP 客户端不应该用管道化的方式发送会产生副作用的请求(比如 POST)。 由于无法安全地重试 POST 这样的非幂等请求, 所以出错时, 就存在某些方法永远不会被执行的风险。