HTTP/2 in Action 中文版
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.2 解决HTTP/1.1性能问题的方案

如前所述,HTTP/1.1不是一种高效的协议,因为它为等待响应会阻塞发送。导致在当前请求完成之前,无法发送另一个请求。如果网络或服务器速度较慢,HTTP性能会比较差。由于通过HTTP访问的服务器通常距离客户端较远,因此网络缓慢是HTTP必须面对的事实。在起初使用HTTP时(单个HTML文档时代),这种缓慢并不是一个大问题。但随着网页变得越来越复杂,渲染页面需要的资源越来越多,速度慢的问题就突显出来了。

由于网站响应缓慢而催生了网络性能优化行业,市面上有许多关于如何改进网络性能的书籍和教程。虽然克服HTTP/1.1的问题并不是唯一的网络性能优化方法,但它是一个重要的问题。随着时间的推移,已经有各种突破HTTP/1.1的性能限制的技术,这些技术分为以下两类:

• 使用多个HTTP连接。

• 合并HTTP请求。

其他的和HTTP关联不大的性能优化技术,包括优化用户请求资源的方式(比如先请求关键CSS),减小下载资源的大小(压缩和使用响应式图片),减少浏览器的渲染任务(更高效的CSS和JavaScript)。这些技术的细节超出了本书的讨论范围,但我们会在第6章涉及一些。Manning出版社所出版的Web Performance in Action(Jeremy Wagner著)一书[9],是学习这些技术的绝佳资料。

2.2.1 使用多个HTTP连接

打开多个连接是解决HTTP/1.1阻塞问题的最简单方法,这样可以同时开启多个HTTP请求。另外,与管道化技术不同,该技术不会导致HOL阻塞,因为每个HTTP连接都独立于其他HTTP连接。因此,大多数浏览器可以为每个域名打开6个连接。

为了进一步突破6个连接的限制,许多网站从子域(例如,static.example.com)提供静态资源,如图像、CSS和JavaScript,Web浏览器从而可以为每个新域名打开另外6个连接。这种技术称为域名分片(尽管性能原因相似,但非Web背景的读者不要与数据库分片混淆)。除了提高并发数,域名发散还有其他优势,比如减小HTTP请求首部(如cookies)(参见2.3节)。通常,这些域名托管在同一台服务器上。共享相同的资源但使用不同的域名会让浏览器误以为服务器是相互独立的。如图2.7所示的stackoverflow.com网站使用多个域:从Google域加载JQuery,从cdn.static.net域加载脚本和样式表,从i.stack.imgur.com域加载图像。

图2.7 stackoverflow.com使用多个域名加载资源

使用多个HTTP连接听起来不错,但它也有缺点。当开启多个HTTP连接时,客户端和服务器都有额外的开销:打开TCP连接需要时间,维护连接需要更多的内存和CPU资源。

开启多个HTTP连接的主要问题是,没有充分利用底层的TCP协议。TCP是一种可靠的协议。在TCP中,发送数据包时会附一个序列号,如果序列中哪个数据包丢了,其会要求重发。TCP要求三次握手来建立连接,如图2.8所示。

图2.8 TCP三次握手

TCP三次握手的过程:

1. 客户端发送一个同步(SYN)消息,并附上首个序列号,在这个TCP连接之后的数据包都会基于此序列号。

2. 服务器向客户端确认收到发来的序列号(ACK),并发送一个服务器同步消息(SYN),告诉客户端它要使用的序列号。这两个消息被合并为一个SYN-ACK消息。

3. 最后,客户端确认收到服务器的序列号,发送一个ACK消息。

这个过程需要三次网络消息传递(或者1.5次往返),发生在发送HTTP请求之前。

另外,TCP在开启连接时比较小心,在确认网络不拥堵之前只会发送比较少的数据包。CWND(Congestion Window,拥塞窗口)随着时间的推移逐渐增加,只要连接没发现丢包,就可以处理更大的流量。TCP拥塞窗口的大小受TCP慢启动算法控制。因为TCP是一个不会让网络过载的可靠协议,所以在拥塞窗口中的TCP数据包需要在收到ACK消息之后发送。这些数据包使用在三次握手过程中协商的递增的序列号。所以,在CWND比较小时,可能需要多个TCP ACK消息才能发出一个完整的HTTP请求。因为HTTP响应常常比请求大很多,所以它同样也会受到拥塞窗口的影响。由于TCP连接的应用更加广泛,所以人们提升了拥塞窗口的大小以使它更高效,但是在创建它时是给它限流了的,不管它所处环境的网络有多快,带宽有多大。在第9章,我们会再次讨论TCP,这里只是为了说明使用多个HTTP连接的问题。

最后,就算没有TCP建立连接的开销和慢启动的问题,使用多个独立的连接也可能导致带宽问题。例如,如果所有带宽都用掉了,就会导致TCP超时,和其他的连接上的重传。在这些独立的连接之间,没有优先级的概念,这就无法更高效地利用带宽。

创建TCP连接之后,安全的网站要求建立HTTPS连接。这个过程可以节约开销,比如,重用TCP连接的参数,不从零开始。但这个过程依然需要更多的网络往返,这意味着更多的时间。这里不讨论HTTPS握手的细节,在第4章我们会深入研究。

所以,在TCP和HTTPS的层面,开启多个连接并不高效,尽管在HTTP的层面这么做是一种很棒的优化。这个解决HTTP/1.1延迟问题的方案需要更多额外的请求和响应,所以这个方案反而可能会导致延迟问题,这本是它应该解决的问题。

另外,当这些新增的TCP连接达到了TCP的最佳效率,网页所需要的大量资源已经加载完成后,这些增加的连接就没用了。如果公用的资源被缓存起来,那么甚至连后续的页面也不需要请求更多的资源。Mozilla的Patrick McManus在对HTTP/1的监控中发现,“74%的活跃连接仅传输一个会话。”在本章后面,会展示一些实际的案例。

所以,开启多个TCP连接并不是解决HTTP/1问题的满意方案,尽管在没有更好的解决方案时,它确实可以提升性能。另外,这也说明了为什么浏览器限制每个域名开启的连接数为6。尽管可以增大这个数字(一些浏览器允许这么做),但考虑到每个连接的额外开销,收益会比较低。

2.2.2 发送更少的请求

另外一个常见的优化技术是发送更少的请求,包括:减少不必要的请求(比如在浏览器中缓存静态资源),以更少的HTTP请求获取同样的资源。前一种方法会使用到HTTP首部配置,在第1章中简单介绍过它,在第6章还会详细说明。后一种方法需要打包合并静态资源。

对于图片来说,这种打包技术叫作精灵图。例如,如果你网站上有很多社交网站图标,则每个网站图标都可以使用一个单独的图片。但这种方式会导致很多低效的HTTP请求排队,因为图片比较小,所以相对于下载这些图片所需要的时间,发送请求的时间可能会较长。所以,可以将它们合并到一张大的图片里面,然后使用CSS来定位图片位置,让它们看起来像是独立的图片,这样更高效。图2.9显示了一个TinyPNG上面的精灵图,它将通用的图标合并到了一个文件中。

图2.9 TinyPNG的精灵图

如果是CSS和JavaScript文件,很多网站就将多个文件合并为一个文件,这样需要的请求数就少了,但是总的代码量并不少。在合并文件的时候,通常还会去掉代码中不必要的空格、注释和其他不必要的元素,以减小CSS和JavaScript的文件尺寸。这些方法都会提升效率,但是会增加配置的难度。

其他的技术还包括内联资源到其他文件。比如,Critical CSS经常直接被内联在HTML的<style>标签中。图片可以包含在CSS中,通过行内SVG,或者转换为Base64编码,也能减少HTTP请求数。

这个方案的主要问题是它引入的复杂度。创建精灵图需要额外的操作,通过独立的文件来提供图片更简单。不是所有的网站都有做优化(如合并CSS文件)的自动化工作流。如果你的网站使用一个内容管理系统(CMS,Content Management System),它可能不会自动合并JavaScript,或者合并图片。

另外一个问题是,合并会导致文件的浪费。一些网页可能只用到一张精灵图中的一两个图标,但却要下载整张精灵图。我们很难知道精灵图中哪些元素还在用,哪些需要删掉。当有新的精灵图时,还要重写CSS文件,以防止新的精灵图中图标的位置发生变化。同样,如果合并了太多文件,JavaScript也可能会变得臃肿。有时候我们只需要其中的很少一部分,却要下载一个大得多的文件。无论是从网络的方面(特别在开始的时候,TCP启动慢)还是从浏览器执行的方面(浏览器需要处理它不需要的代码)来说,这种技术都不够高效。

最后一个问题是缓存。如果把精灵图缓存了很长一段时间(这样用户就不需要频繁下载它),当需要添加一个图标的时候,必须让浏览器再次下载整个精灵图,但访客并不需要。可以使用很多技术来解决这个问题,比如添加版本号或者使用查询参数[10],但是这些技术也会浪费资源。使用CSS和JavaScript也一样,改变一行代码就需要重新下载整个合并文件。

2.2.3 HTTP/1性能优化总结

归根到底,优化HTTP/1性能的方法是一些解决HTTP/1基础缺陷的小技巧。应该有更好的办法在协议层面解决这个问题,从而节省时间,这正是HTTP/2要做的。