1 连接模型
HTTP的传输协议依赖 TCP 提供从客户端到服务器端间的连接。早期 HTTP 使用一个简单模型处理这样的连接。这些连接的生命周期是短暂的:每发起一个请求时都会创建一个新连接,收到应答时立即关闭。
这简单模型对性能有先天限制:打开每个 TCP 连接都相当耗费资源。客户端、服务器端间需交换好多消息。当请求发起时,网络延迟和带宽都会对性能影响。现代浏览器往往要发起很多次请求(十几个或更多)才能拿到所需的完整信息,证明这早期模型效率低下。
于是 HTTP/1.1 诞生俩新模型:
- 长连接模型
- HTTP 流水线模型
2 长、短连接
2.1 短连接
HTTP协议最初(0.9/1.0)是个极简协议,通信过程采用“请求-应答”方式。底层数据传输基于TCP/IP,每次发送请求前,需先与服务器建立连接,收到响应报文后立即关闭连接。
因为客户端与服务器的整个连接过程短暂,不会与服务器保持长时间连接状态,称“短连接”(short-lived connections)。早期的HTTP协议也被称为是“无连接”协议。
HTTP最早期模型,也是 HTTP/1.0 的默认模型(若没有指定 Connection 协议头,或置 close)。而在 HTTP/1.1 中,只有当 Connection 被设置为 close 时才会用到该模型。
每个 HTTP 请求都由它自己独立的连接完成;这意味着发起每一个 HTTP 请求之前都会有一次 TCP 握手,而且连续不断。TCP 协议握手本身就耗时,所以 TCP 可保持更多热连接来适应负载。短连接破坏 TCP 具备的能力,新的冷连接就会降低其性能。
缺点
TCP协议里,建立连接和关闭连接都昂贵:
- 建立连接,三次握手,发送3个数据包,需要1个RTT
- 关闭连接,四次挥手,4个数据包,需要2个RTT
一个来回就是1RTT,三次握手准确来说是1.5个RTT,四次挥手是两个来回,所以是2RTT。
而HTTP的一次简单“请求-响应”通常只需4个包,若不计服务器内部处理时间,最多2个RTT。算下来,浪费的时间就是“3÷5=60%”的时间被浪费,传输效率低!
除非是要兼容古老的不支持长连接的系统,否则无理由再使用该模型。
2.2 长连接
保持连接,去完成多次连续的请求,减少不断重新打开连接的时间。
短连接的两大问题:
- 创建新连接的耗时长
- TCP连接的性能,只有在该连接被使用一段时间后(热连接)才能得到改善
为缓解这些问题,便设计了长连接,甚至在 HTTP/1.1 之前。或者这被称之为一个 keep-alive 连接。
一个长连接会保持一段时间,重复用于发送一系列请求,节省了新建 TCP 连接握手的时间,还可以利用 TCP 的性能增强能力。当然这个连接也不会一直保留着:连接在空闲一段时间后会被关闭(服务器可以使用 Keep-Alive 协议头来指定一个最小的连接保持时间)。
针对短连接暴露出的缺点,HTTP协议提出“长连接”通信方式,也叫“持久连接”(persistent connections)、“连接保活”(keep alive)、“连接复用”(connection reuse)。
就是“成本均摊”思路,既然TCP的连接和关闭很耗时,那就把这个时间成本由原来的一个“请求-应答”均摊到多个“请求-应答”。这虽不能改善TCP的连接效率,但基于“分母效应”,每个“请求-应答”的无效时间就会大大降低,整体传输效率也就提高了。
短连接 V.S 长连接示意图
- 短连接里发送三次HTTP“请求-应答”,每次都会浪费60%的RTT时间
- 长连接下,发送三次请求,只在第一次时建立连接,最后一次时关闭连接,所以浪费率“3÷9≈33%”,降低近一半时间损耗。因此在该长连接上发送的请求越多,分母越大,利用率越高。
缺点
长连接也还是有缺点的: 就算是在空闲状态,它还是会消耗服务器资源,而且在重负载时,还有可能遭受 DoS attacks 攻击。这种场景下,可以使用非长连接,即尽快关闭那些空闲的连接,也能对性能有所提升。
HTTP/1.0 里默认并不使用长连接。把 Connection 设置成 close 以外的其它参数都可以让其保持长连接,通常会设置为 retry-after。
在 HTTP/1.1 里,默认就是长连接的,协议头都不用再去声明它(但我们还是会把它加上,万一某个时候因为某种原因要退回到 HTTP/1.0 呢)。
4 连接相关的头字段
由于长连接对性能的改善效果非常显著,所以在HTTP/1.1中的连接都会默认启用长连接。只要向服务器发送了第一次请求,后续请求都会重复利用第一次打开的TCP连接,在这个长连接上收发数据。
当然,也能在请求头里明确指定使用长连接机制,即:
不管客户端是否显式要求长连接,若服务器支持长连接,它总会在响应报文里放个Connection: keep-alive,告诉客户端:“我支持长连接,接下来就继续用这个TCP一直收发数据”。
缺点
TCP连接长时不关闭,服务器必须在内存里保存它的状态,占用服务器资源。若大量空闲长连接只连而不发,很快就会耗尽服务器资源。
所以,长连接也得择机关闭,不能永远保持与服务器的连接,这在客户端或服务器端都能做到:
- 客户端,可在请求头加Connection: close字段,告诉服务器:“这次通信后就关闭连接”。服务器看到该字段,就知道客户端要主动关闭连接,于是在响应报文里也加上该字段,发送后就调用Socket API关闭TCP连接。
- 服务器端通常不主动关闭连接,但也能使用一些策略。如Nginx就有两种方式:
- 使用“keepalive_timeout”指令,设置长连接的超时时间,若在一段时间内连接上没有任何数据收发,就主动断开连接,避免空闲连接占用系统资源
- 使用“keepalive_requests”指令,设置长连接上可发送的最大请求次数。如1000,则当Nginx在这个连接上处理了1000个请求后,也会主动断开连接。
客户端和服务器都能在报文里附加通用header字段“Keep-Alive: timeout=value”,限定长连接超时时间。但这个字段的约束力不强,通信双方可能并不遵守,所以不常用。
我们的实验环境配置了“keepalive_timeout 60”和“keepalive_requests 5”,意思是空闲连接最多60秒,最多发送5个请求。所以,如果连续刷新五次页面,就能看到响应头里的“Connection: close”。
HTTP/1.x 里有多种模型:短连接, 长连接, 和 HTTP 流水线。
HTTP 流水线
多个连续的请求甚至都不用等待立即返回就能被发送,这就减少了耗费在网络延迟上的时间。
HTTP/2 新增了其它连接管理模型。
HTTP 的连接管理适用于两个连续节点之间的连接,如 hop-by-hop,而不是 end-to-end。当模型用于从客户端到第一个代理服务器的连接和从代理服务器到目标服务器之间的连接时(或者任意中间代理)效果可能是不一样的。HTTP 协议头受不同连接模型的影响,比如 Connection 和 Keep-Alive,就是 hop-by-hop 协议头,它们的值是可以被中间节点修改的。
一个相关的话题是HTTP连接升级,在这里,一个HTTP/1.1 连接升级为一个不同的协议,比如TLS/1.0,Websocket,甚至明文形式的HTTP/2。
HTTP 流水线在现代浏览器中并非默认启用:
- Web 开发者并不能轻易遇见和判断那些搞怪的代理服务器的各种莫名其妙行为
- 正确的实现流水线复杂:传输中的资源大小,多少有效的 RTT 会被用到,还有有效带宽,流水线带来的改善有多大的影响范围。不知道这些的话,重要的消息可能被延迟到不重要的消息后面。这个重要性的概念甚至会演变为影响到页面布局!因此 HTTP 流水线在大多数情况下带来的改善并不明显。
- 受制于 HOL 由于这些原因,流水线已经被更好的算法给代替,如 multiplexing,已经用在 HTTP/2。
默认 HTTP 请求按序发出。下一个请求只有在当前请求收到应答过后才会被发出。由于会受到网络延迟和带宽的限制,在下一个请求被发送到服务器之前,可能需要等待很长时间。
流水线是在同一条长连接上发出连续的请求,而不用等待应答返回。这样可以避免连接延迟。理论上性能还会因为两个 HTTP 请求有可能被打包到一个 TCP 消息包中而得到提升。就算 HTTP 请求不断继续,尺寸会增加,但设置 TCP 的 MSS(Maximum Segment Size) 选项,仍然足够包含一系列简单请求。
并不是所有类型的 HTTP 请求都能用到流水线:只有 idempotent 方式,比如 GET、HEAD、PUT 和 DELETE 能够被安全的重试:如果有故障发生时,流水线的内容要能被轻易的重试。
如今所有遵循 HTTP/1.1 的代理和服务器都应该支持流水线,虽然实际情况中还是有很多限制:一个很重要的原因是,目前没有现代浏览器默认启用这个特性。
5 队头阻塞(Head-of-line blocking)
也叫“队首阻塞”,“队头阻塞”与短连接和长连接无关,而是由HTTP基本的“请求-应答”模型导致。
因为HTTP规定报文必须“一发一收”,这就形成一个FIFO串行队列。队列里的请求无优先级,只有入队先后顺序,最前的请求优先处理。
若队首请求处理太慢,则队列里后面的所有请求也都得跟着等待,结果就是其它的请求承担了不应有的时间成本。
6 性能优化
因为“请求-应答”模型不能变,所以“队头阻塞”问题在HTTP/1.1里无法解决,只能缓解,有什么办法呢?
在HTTP里就是“并发连接”(concurrent connections),同时对一个域名发起多个长连接,用数量来解决质量问题。
缺点
若每个客户端都想自己更快,建立很多连接,用户数×并发数就会是天文数字。服务器资源扛不住或被服务器认为是恶意攻击,反而还会造成“拒绝服务”。
所以,HTTP协议建议客户端使用并发,但不“滥用”。RFC2616里明确限制每个客户端最多并发2个连接。但实践证明这值太小,众多浏览器都将该上限提高到6~8。后来修订的RFC7230也就取消了这个“2”的限制。
但“并发连接”所压榨出的性能跟不上互联网需求,还有啥优化方案呢?
7 域名分片(domain sharding)
还是用数量来解决质量。
HTTP协议和浏览器不是限制并发连接数量吗?好,那我就多开几个域名,如shard1.chrono.com、shard2.chrono.com,而这些域名都指向同一台服务器这样实际长连接的数量就又上去了。
比如域名分片开了三个域名,那么客户端就可以并发18个连接,这样它就可以用比一个域名多三倍的连接来收发数据,效果肯定会好一些。虽然实际上还有可能存在阻塞,但因为并发多了,总体上会变好。当然,这只方便了客户端,但加大了服务器的负担。
除非你有紧急而迫切的需求,不要使用这一过时的技术,升级到 HTTP/2 就好了。在 HTTP/2 里,做域名分片就没必要了:HTTP/2 的连接可以很好的处理并发的无优先级的请求。域名分片甚至会影响性能。大多数 HTTP/2 的实现还会使用一种称作连接凝聚的技术去尝试合并被分片的域名。
作为 HTTP/1.x 的连接,请求是序列化的,哪怕本来是无序的,在没有足够庞大可用的带宽时,也无从优化。一个解决方案是,浏览器为每个域名建立多个连接,以实现并发请求。曾经默认的连接数量为 2 到 3 个,现在比较常用的并发连接数已经增加到 6 条。如果尝试大于这个数字,就有触发服务器 DoS 保护的风险。
如果服务器端想要更快速的响应网站或应用程序的应答,它可以迫使客户端建立更多的连接。例如,不要在同一个域名下获取所有资源,假设有个域名是 www.2.com,我们可以把它拆分成好几个域名:www1.example.com、www2.example.com、www3.example.com。所有这些域名都指向同一台服务器,浏览器会同时为每个域名建立 6 条连接(在我们这个例子中,连接数会达到 18 条)。这一技术被称作域名分片。
FAQ
使用长连接时,不同 HTTP 如何复用相同TCP的?
套接字里的确无记录使用的次数和时长。一个tcp连接打开后就不关闭,在上面发送多个请求响应,不需要,看图其实比较好理解。TCP不记录次数和时长,但服务器在外部可以记录,到次数或时间就关闭。
X 总结
- 服务器会发送“Connection: keep-alive”字段表示启用了长连接;
- 报文头里如果有“Connection: close”就意味着长连接即将关闭;
- 过多的长连接会占用服务器资源,所以服务器会用一些策略有选择地关闭长连接;
- “队头阻塞”问题会导致性能下降,可以用“并发连接”和“域名分片”技术缓解。
改进后的连接管理极大的提升了 HTTP 的性能。不管是 HTTP/1.1 还是 HTTP/1.0,使用长连接 – 直到进入空闲状态 – 都能达到最佳的性能。然而,解决流水线故障需要设计更先进的连接管理模型,HTTP/2 已经在尝试。
参考