0%

HTTP/2 实战(1):HTTP/2 上位之路

随着对 Nginx 维护的深入,学习掌握 HTTP/2 已经是一件较为急迫的事情。《HTTP/2 in Action》一书较为全面、深入地介绍了 HTTP/2 协议,如果想系统掌握 HTTP/2 协议,该书很有帮助。本系列文章是《HTTP/2 in Action》的读书笔记。

万维网的原理

HTTP 是访问远程 Web 应用和资源的关键技术。Tim 发明万维网(World Wide Web)时,一共创建了 3 项核心技术:

  • 用于传输数据的 HTTP(Hypertext Transfer Protocol,超文本传输协议)
  • 用于标识唯一资源的 URL(Uniform Resource Locator,统一资源定位符)
  • 用于构造网页的 HTML(Hypertext Markup Language,超文本标记语言)

随着越来越多的服务(甚至一些没有传统 Web 前端界面的服务)也开始使用 HTTP,HTTP 在互联网的应用已经不再局限于万维网了。

什么是 HTTP

HTTP 代表超文本传输协议,HTTP 最初是用来传输超文本文档的(文档中包含指向其他文档的链接),随着互联网的发展,HTTP 很快被应用于传输其他文件类型。HTTP 基于可靠的网络连接,通常由 TCP/IP 提供。由于网络协议栈的层次化结构,每一层可以专注于做好当前层的事情。HTTP 协议并不关心如何建立网络连接的底层细节。

本质上讲 HTTP 是一个请求/响应协议,Web 浏览器使用 HTTP 协议,向服务器发送一个请求,服务器响应一个消息,该响应消息包含了浏览器所请求的资源。HTTP 成功的关键在于简单(这种简单性反而成了 HTTP/2 要解决的问题,为了效率,HTTP/2 牺牲了一些简单性)。

建立一个 TCP 连接后,最简单的 HTTP 请求如下:

1
2
# nc 127.0.0.1 80
GET /index.html

HTTP 协议的首个版本(0.9)仅支持这种简单的语法。接下来使用 HTTP/1.0 测试 baidu

1
2
3
4
5
nc www.baidu.com 80
GET / HTTP/1.0

HTTP/1.0 200 OK
......

当使用 HTTP/1.0 时,每次发送完请求之后,连接都会被关闭。此时可以使用 HTTP/1.1,默认保持连接打开。需要注意,当使用 HTTP/1.0HTTP/1.1 时,需要输入两次回车,告诉 Web 服务器请求发送完成了。

HTTP 基本语法很简单,是一种基于文本的请求-响应格式(但是到了 HTTP/2 下它变成了二进制格式)。创建一个简单的服务来监听 HTTP 请求并响应数据,并不困难。HTTP 的简单性也促进了微服务体系结构的繁荣,在该结构中,应用程序被分成了很多独立的 Web 服务。

HTTP 语法和历史

HTTP/0.9

HTTP 的第一个规范是是 1991 年发布的 0.9 版本。该规范非常简单:

  • 通过 TCP/IP 与服务器的指定端口建立连接
  • 客户端应发送一行 ASCII 文本,包含 GET、文档地址、回车符和换行符(回车可选)
  • 服务器使用 HTML 格式的消息进行响应,消息被定义为 ASCII 字符的字节流
  • 通过服务器关闭连接来终止消息
  • 错误响应以可读的文本显示,使用 HTML 语法。除了文本的内容之外,没有办法区分错误响应和正确响应
  • 请求是幂等的,服务器不需要在断开连接后存储关于请求的任何信息

这些规范提供了 HTTP 的无状态特性,这很简单,但也有缺点:因为对于复杂的应用程序,需要通过 HTTP cookies 等技术来运行状态跟踪。

HTTP/1.0

HTTP/1.0 对应的文档是 RFC1945,尽管不是正式的标准,但是 HTTP/1.0 还是新增了一些关键特性,包括:

  • 更多的请求方法,例如 HEAD、POST
  • 为所有消息添加 HTTP 版本号字段,该字段是可选的,为了向后兼容,如果不存在则为 HTTP/0.9
  • HTTP 首部。HTTP 请求和响应中都可以包含 HTTP 首部,以提供关于请求或响应更多的信息
  • 一个三位整数的响应状态码,用来表示响应是否成功,也可以用来表示重定向等

HTTP 请求首部在原始请求行之后的单独行上提供。HTTP 首部是可选的,用一个空行表示 HTTP 首部的结束,之后则是请求 body。HTTP 首部使用 首部名称冒号首部内容 表示。首部名称是不区分大小写的。可以发送具有相同名称的多个首部,在语义上这与发送以逗号分割的版本完全相同。

HTTP 响应的第一行包含 HTTP 版本、三位数的 HTTP 状态码以及该状态码的文本描述。在返回响应的第一行之后,会有一到多行响应首部。响应首部和请求首部遵循相同的格式。在响应首部之后是一个空行,然后则是响应 body。

HTTP/1.1

HTTP/1.0 的发布是为了给已经在使用的 HTTP 提供一些标准和文档,而不是为客户端和服务器定义新语法。而 HTTP/1.1 更像是对 HTTP/1.0 的调整,它没有从根本上改变协议。HTTP/1.1 的许多附加功能是通过 HTTP 首部引入的,HTTP 首部本身是在 HTTP1.0 中引入的,这意味着 HTTP 的基本结构在两个版本之间没有变化。强制首部和持久连接是 HTTP/1.0 语法的两个显著变化。

Host 请求首部在 HTTP/1.0 中是可选的,但是在 HTTP/1.1 中则是必选的。但是大多数服务器实现都很宽松,即使 HTTP/1.1 中没有携带 Host 首部,对于这些请求也会使用一个默认 Host。

HTTP 中的另一个重大更新是持久连接,起初 HTTP 仅仅是一个请求/响应协议,客户端打开连接、请求资源、获取响应,然后断开连接。但是随着互联网内容更加丰富,关闭连接被认为是一种浪费性能的行为:显示一个页面需要多个资源,所以关闭连接后重新再打开,导致了不必要的延迟。Connection 首部被用来解决该问题,客户端可以将该首部的值设置为 keep-Alive,要求服务器保持连接打开,以支持发送更多的请求。服务器像往常一样响应,但是如果它支持持久连接,它会在响应中包含一个 Connection: Keep-Alive 首部,这个响应首部告诉客户端,在发送完响应之后,可以在同一个连接上发送一个新的请求。

当使用持久连接时,想要知道响应何时完成可能会更困难。对于非持久连接,关闭连接表明服务器已经完成了响应的发送。而对于持久连接,则需要使用 Content-Length 首部来定义响应 body 的长度,以便客户端感知当前响应的结束,可以继续发送下一个请求了。客户端或服务器可以在任意时候关闭 HTTP 连接,因此即使使用持久连接,客户端和服务器还是应该继续监视并处理连接关闭的情况。

HTTP/1.1 中,持久连接是默认行为,即使响应中没有 Connection: Keep-Alive 首部(请求中不包含也没有关系),也可以假定任何 HTTP/1.1 连接都使用持久连接。如果服务器确实想关闭连接,则它应该在响应中显示包含 Connection: close 首部。

在此基础上,HTTP/1.1 增加了管道的概念,因此可以通过一个持久连接发送多个请求并顺序获取响应。但是管道化并没有流行起来,客户端和服务器对管道化的支持都很差。所以虽然持久连接允许在同一个 TCP 顺序上发出多个请求,这也是很好的性能改进,但是大多数 HTTP/1.1 实现还是遵循 请求-响应-再请求-再响应 的模式。

HTTP/1.1 还引入了其他的新功能,包括:

  • 更多的方法,例如 PUT、OPTIONS、CONNECT、TRACE、DELETE 等
  • 更好的缓存方法,允许服务器指示客户端将资源存储在浏览器缓存中。HTTP/1.1 中引入的 Cache-Control 比 HTTP/1.0 中的 Expires 首部功能更加丰富
  • HTTP Cookies,允许 HTTP 维护状态
  • 引入字符集,在 HTTP 响应中新增语言选项
  • 支持代理
  • 支持权限验证
  • 新的状态码
  • 尾随首部

HTTP 协议不断添加新的首部以进一步扩展功能,HTTP/1.1 规范并未将 HTTP/1.1 固话下来,其鼓励添加新的首部。以前有个惯例,以 X- 开头的首部表明这些它们并没有被正式标准化,但是这个约定已经不推荐使用了,这也导致了新的实验性首部很难与标准首部区分开来。

HTTPS 简介

由于 HTTP 是一个纯文本协议,因此在消息被路由到目的地的过程中,消息可以被拦截、读取、篡改。HTTPS 是 HTTP 的安全版本,它使用 TLS(Transport Layer Security,传输层加密)协议对传输中的消息进行加密。TLS 的前身是 SSL(Secure Sockets Layer,安全套接字层)。HTTPS 对 HTTP 消息添加了如下三个重要的概念:

  • 加密:传输过程中第三方无法读取消息
  • 完整性校验:消息在传输过程中未被更改
  • 身份验证–服务器不是伪装的

HTTPS 是基于 HTTP 构建的,几乎可以与 HTTP 协议无缝衔接,它并没有从根本上改变 HTTP 的语法或消息格式,但是它的协议名是 HTTPS。当客户端连接到 HTTPS 服务器,其首先需要经历 TLS 握手。此时服务器会提供公钥,客户端和服务器协商所使用的加密方法,然后协商接下来要使用的共享秘钥(非对称加密性能不如对称加密,因此非对称加密只用于秘钥协商,协商出的秘钥会用于后续流量的对称加密)。

对于 Web 程序开发人员而言,HTTPS 和 HTTP 基本没有区别,除非你需要通过网络查看发送的原始消息,否则一切都是透明的。而这正是因为 HTTPS 使用 TLS 传输标准的 HTTP 请求和响应,而不是用其他协议替代 HTTP。

在测试 HTTPS 站点时,需要一个能够支持 TLS 的客户端工具,可以使用 openssl 命令行工具,例如:

1
openssl s_client -connect www.baidu.com:443

查看、发送和接收 HTTP 消息的工具

一般浏览器都带有开发者工具,可以让我们查看网站背后的细节,包括 HTTP 请求和响应。例如通过 F12 进入浏览器的开发者工具后,通过 Network 可以查看访问一个站点页面时所涉及的 HTTP 请求及响应。

Web 浏览器的开发者工具是查看 HTTP 请求和响应的好方法,但是却不适合发送 HTTP 请求。Advanced REST Client 提供了一种发送和查看 HTTP 请求/响应的方法,其他类似工具包括 Postman、Rested、RESTClient、RESTMan 等等。

HTTP/1.1 和当前的万维网

网页最初是静态页面,但是随着 Web 的动态性变好,网页开始在服务端动态生成,例如使用 CGI(Common Gateway Interface,公共网关接口)或 Java Servelet/Java Server Page(JSP)动态生成。从服务端生成完整页面变成只生成基本的 HTML 页面,其他由客户端 JavaScript 进行 AJAX(Asynchronous JavaScript and XML)调用。这些 AJAX 调用向 Web 服务器发出额外请求,以更改网页内容,而无需重新加载整个页面。

现代互联网最大的问题之一是延迟而不是带宽,延迟受物理(光速)上的限制,通过光纤传输数据,传输速度非常接近光速,无论技术上有多大改进,速度都只能提升一点点。

HTTP/1.1 不是一种高效的协议,因为它在等待响应时会阻塞发送,导致在当前请求完成之前,无法发送另一个请求。如果解决不了 HTTP/1.1 的根本性能问题,互联网就无法继续增长。在发送和接收 HTTP 消息时浪费了太多时间(大量时间被用于等待消息在网络上传送),尽管这些消息可能很小。

HTTP/1.1 管道化

HTTP/1.1 尝试引入管道化,从而在接收响应之前并发发出请求,实现并行发送请求。管道化技术应该会对 HTTP 带来巨大的性能提升,但由于多种原因,它难以实现,易于出错,并且没有获得 Web 浏览器和 Web 服务器的良好支持,因此它很少被使用。

而且即使管道化技术得到了支持,它仍然需要按照请求的顺序返回响应。它也仍然存在 HTTP 队头阻塞问题(head-of-line blocking),简单来说,HTTP HOL 指的是如果一个响应返回延迟了,那么其后续的响应都会被延迟,直至队头的响应送达。

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

目前已经有了各种突破 HTTP/1.1 性能限制的技术,这些技术可以分为两类:

  • 使用多个 HTTP 连接
  • 合并 HTTP 请求

其他和 HTTP 关联不大的性能优化技术包括:

  • 优化用户请求资源的方式(例如先请求关键 CSS)
  • 减少下载资源的大小
  • 减少浏览器的渲染任务
    …..

使用多个 HTTP 连接

打开多个连接是解决 HTTP/1.1 阻塞问题的最简单方法,这样可以同时开启多个 HTTP 请求。而且该技术也不会导致 HTTP HOL 阻塞,因为每个 HTTP 连接都独立于其他 HTTP 连接。大多数浏览器可以为每个域名打开 6 个连接,为了进一步突破 6 个连接的限制,许多网站从子域提供静态资源,浏览器从而可以为每一个新域名打开另外 6 个连接,这种技术也称为域名分片技术。

打开多个 HTTP 连接的缺点是,客户端和服务器都有额外的开销:打开 TCP 连接需要时间,维护 TCP 连接需要更多的内存和 CPU 资源。在 TCP 和 HTTPS 层面来看,开启多个连接并不高效,尽管在 HTTP 层面这么做是一种很棒的优化。

发送更少的请求

另外一个常见的优化技术是发送更少的请求,包括:

  • 减少不必要的请求(比如在浏览器中缓存静态资源)
  • 以更少的请求获取同样的资源,例如打包合并静态资源。这种方法的缺点是引入了额外的复杂度,而且可能会有浪费下载的资源

目前优化 HTTP/1.1 性能的这些方法是一些小技巧,更好的解决方法应该是在协议层面解决这些问题,而这正是 HTTP/2 要做的。最后需要说明,HTTP/1.1 的效率问题是互联网的问题之一,这可以通过改善 HTTP(例如 HTTP/2)来解决,但网络慢远远不止这一个问题。

HTTP/1.1 的其他问题

HTTP/1.1 是一个简单的文本协议,尽管 HTTP 消息体也可以包含二进制数据,但是请求行、响应行、首部必须是文本的形式。HTTP 文本行处理起来相对更为复杂,而且容易出错,导致安全问题。另外文本格式不能高效编码数据,而且首部内容也有重复。

性能问题是 HTTP/1.1 需要改善的其中一个问题,除此之外,还存在纯文本协议的安全和隐私问题(HTTPS 加密很好地解决了这个问题)、缺少状态(cookie 一定程度上解决了该问题)等等问题。

可以使用 www.webpagetest.org 对现实中的 HTTP 站点进行测试。每个网站都是不一样的,每个站长或者 Web 开发者都需要花时间分析网站本身的性能瓶颈,并使用瀑布图之类的工具,查明网站受到 HTTP/1.1 性能问题的影响有多大。

从 HTTP/1.1 到 HTTP/2

从 1999 HTTP/1.1 走上历史舞台,HTTP 并没有真正发生改变。2014 年发布的 RFC 只是澄清了这些规范,并没有对协议做什么变更。

SPDY

2009 年,Google 宣布他们正在开发 SPDY(读作 speedy)的新协议,页面加载时间改善了 65%。SPDY 基于 HTTP 构建,没有从根本上改变协议。SPDY 工作在更低的层面,并且对开发者、服务器管理员、用户而言,SPDY 几乎是透明的。SPDY 的实现只基于 HTTPS。

SPDY 的主要目标是解决 HTTP/1.1 的性能问题,它引入了一些关键概念来解决 HTTP/1.1 的问题:

  • 流多路复用:请求和响应使用单个 TCP 连接传输数据,它们被分成不同的数据包,以流的方式分组
  • 请求优先级:在同时发送所有请求时,为了避免引入新的性能问题,引入了请求优先级的概念
  • HTTP 首部压缩:之前 HTTP body 可以压缩,现在首部也支持压缩了

为了实现以上功能,SPDY 是一个二进制协议。由于 Google 的市场地位,SPDY 很快获得了大多数浏览器、Web 服务器的支持。但是随着 HTTP/2 问世,SPDY 的使用率有所下降。

HTTP/2

SPDY 的成功,证明了对 HTTP/1.1 的优化可以真正应用于现实世界中。2012 年,IETF 的 HTTP 工作组基于 SPDY 发布了 HTTP/2 初稿,2014 年底,HTTP/2 规范作为互联网的标准被提出,而在 2015.5 月被正式通过,即 RFC7450。

HTTP/2 很快就获得了支持,主要因为它很大程度上是基于 SPDY 实现的。在2018.9,根据 w3tech.com 的数据,已经有 30.1% 的网站支持 HTTP/2。

HTTP/2 对 Web 性能的影响

可以对比 HTTP/1 和 HTTP/2 的瀑布图,从而查看 HTTP/2 对 Web 性能的改善,预期会有如下改进:当需要加载资源时,在开始时没有额外的连接,也没有那么多阶梯式的瀑布加载。Web 资源相互依赖,所以在 HTTP/2 中,还是可以在瀑布图中看到阶梯式的加载过程,但是 HTTP/2 没有浪费时间来建立连接和排队,因此没有因为 HTTP 约束而导致的瀑布效应。还有一点需要注意,在查看 HTTP/1 和 HTTP/2 的瀑布图时,需要理解瀑布图里各自请求时间的含义,避免理解出现偏差。

HTTP/2 以流的形式,只使用一个连接,在理论上没有同时只能发送 6 个请求的限制,但是具体的实现不受约束,可以自行添加限制。例如 Apache 默认一个连接上只能有 100 个并发请求,同时发送的多个请求会共享资源,需要很长的下载时间。

HTTP/2 可能给网站带来巨大的性能提升,也可能没有什么改善,这可能由两个原因导致:

  • 这些网站已经优化得足够好了。但即便如此,相比于在 HTTP/1 中使用域名分片、CSS 合并、JavaScript 合并、精灵图等技术,HTTP/2 足够简单
  • 其他性能问题远超 HTTP/1 带来的影响。HTTP/2 主要解决网络性能问题

举一个例子,在一个跟带宽关系较大的网站中,HTTP/2 可能会使网站访问看起来更慢。这是因为使用 HTTP/2 时,多个下载请求使用自然排序,而在 HTTP/1.1 中,只开启有限的连接数,那么关键资源将会下载更快,看起来也就更快。

还有一个现象需要注意,HTTP/1.1 中的一些性能变通方法可能会成为 HTTP/2 世界中的反模式,因为它们可能会降低 HTTP/2 的使用效果。例如域名分片技术使用多个 TCP 连接,此时无法享受使用单个 TCP 连接加载网站带来的性能提升。

HTTP/2 的支持

由于早期的非标准化的 SPDY 已经在生产环境中得到了验证,因此 HTTP/2 的实际应用非常快速。在这个页面可以看到各个软件对 HTTP/2 的支持情况。

  • 目前主流浏览器基本上都提供了对 HTTP/2 的支持。需要注意,所有浏览器厂商都表示,他们仅支持基于 HTTPS 的 HTTP/2,这项限制也已经成为事实上的标准。
  • 目前几乎所有服务器都支持了 HTTP/2

为了使用 HTTP/2,浏览器和服务器都必须支持 HTTP/2。但是如果用户使用代理(即正向代理),把一个 HTTP 连接变成两个,且这个代理不支持 HTTP2,则 HTTP/2 还是不能启用。查看计算机是否使用正向代理最简单的方法是查看 HTTPS 证书,查看它是由谁发布的。由真正的证书颁发机构颁发还是由本地软件颁发。

网站开启 HTTP/2 的方法

最简单的升级到 HTTP/2 的方法,是开启服务器对 HTTP/2 的支持。除了这个方法外,可以在 Web 服务器之前再加一层支持 HTTP/2 的基础设施来处理 HTTP/2,例如 反向代理服务器、CDN。

当使用反向代理服务器时,它可以将用户的 HTTP/2 请求转换成 HTTP/1.1 请求,发送给 Web 服务器。此时 HTTP/2 连接在反向代理处终止,之后使用另外的连接(可能是 HTTP/1.1)和上游 Web 服务器通信。这个过程类似于使用反向代理终结 HTTPS,HTTPS 在反向代理处被终结,然后反向代理使用 HTTP 与基础架构的其余部分通信。这样做很常见,因为可以简化 HTTPS 配置,例如仅在反向代理处设置并管理证书。

那是否需要在整个链路都支持 HTTP/2 呢,HTTP/2 的主要优点是可以提升高延迟、低带宽的连接速度,连接到反向代理的用户通常就处于这样的网络环境中。而从反向代理到其他 Web 基础架构的流量一般处于低延迟、高带宽、短距离的数据中心网络中,此时通常不需要考虑 HTTP/1.1 的性能问题。如果反向代理到上游业务服务器也使用 HTTP/2,此时 HTTP/2 单个连接的方式收益并不高。Nginx 已经声明,它不会为反向代理实现 HTTP/2。

使用反向代理服务器也是一种测试 HTTP/2 效果的便捷方法。

CDN 是遍布全球的服务器集群,是网站在当地的介入点。通过 DNS 解析,用户连接到最近的 CDN 服务器。请求会被路由回你的 Web 服务器。CDN 上面可能会缓存一个副本,以便当下次相同的请求再来时可以快速响应。大多数 CDN 已经支持 HTTP/2 了,所可以通过 CDN 的方式来启用 HTTP/2,而源站只需要支持 HTTP/1.1 即可。但是需要注意,CDN 能够解密数据流,因此你必须接受 CDN 这个第三方能够读取你的数据这个事实。

当你开启 HTTP/2 后,但是协议版本仍然显示的是 HTTP/2。此时有可能是访问的缓存资源(代理侧缓存或浏览器缓存),此时会显示该缓存资源当初下载时的 HTTP 版本。