事情经过如下(可忽略直接从正文开始):
在之前的一次部门前端周会上,我考虑到当前负责的项目线上环境还是 http1.1 并且系统中存在大量并发请求,提议在线上环境开启 http2 以利用其多路复用的特性来提升并发请求数量,从而避免出现请求阻塞时间太长的情况。
但这个提议遭到大家一致的质疑和否定。否定的原因是如果只在 Nginx 开启 http2 而其反向代理的各个后端应用并不支持 http2 的情况下 http2 不会生效,如果让后端都支持 http2 可能会涉及到架构上的改动,到最后甚至搬出了水桶理论,指出 Nginx 反向代理的各个链路就像是水桶的短板,它会导致整个系统都无法支持 http2
这看似很有道理,但就是个怪圈,我感觉同事们都陷入了一个陷阱中,那就是 http2 必须要全链路都支持才生效,中间稍微有一个环节如果不是 http2 就会失效的陷阱。
虽然我极力解释我们需要用到的特性只是 http2 的多路复用来提升请求并发数量,至于服务端推送这些特性我们暂时不考虑,但在会议最后大家还是否定了我的提议。
实在没办法,我意识到只有拿出数据来说话了。于是在会后我写了一篇简短的 post 来模拟上述场景和对比真实情况,以下是实际内容。
正文开始
受公司软件限制,无法本地安装 Nginx,因此使用 Node 服务模拟 Nginx 反向代理。
我们分别启动了 HTTP2 和 HTTP1.1 服务,并且 HTTP2 服务直接代理到 HTTP1.1 的服务中,代码如下:
const http2 = require("http2");
const http = require("http");
const fs = require("fs");
const http2Server = http2.createSecureServer({
key: fs.readFileSync("key.pem"), // openssl genrsa 1024 > key.pem
cert: fs.readFileSync("key-cert.pem"), // openssl req -x509 -new -key key.pem > key-cert.pem
});
http2Server.on("stream", (stream, headers) => {
// 将 http2 代理到 http1.1
http.get(`http://localhost:8000?${Math.random()}`, (resp) =>
resp.pipe(stream)
);
});
http2Server.listen(8443); // 启动 http2 服务
const httpServer = http.createServer((req, res) => {
setTimeout(() => {
res.statusCode = 200;
res.end("Hello");
}, 2000);
});
httpServer.listen(8000); // 启动 http1.1 服务
其中证书部分可以通过代码备注的命令来生成
上面的代码在 8443 启动了 HTTP2 服务,在 8000 端口启动了 HTTP1.1 服务。 其中将 HTTP2 的请求代理到了 HTTP1.1 的服务中,模拟 Nginx 在开启 HTTP2 的情况下反向代理到 HTTP1.1 的场景。
另外将 HTTP1.1 的接口做了 2 秒延迟,模拟后端处理接口的耗时。
运行上面的代码,然后分别在 https://localhost:8443/ 和 http://localhost:8000/ 中同时并发发送 100 个请求,分别在控制台执行以下代码:
for (let i = 0; i <= 100; i++) {
fetch("/");
}
可以发现,在直接请求 HTTP1.1 对应的 8000 端口的情况下,请求排队导致后续请求迟迟得不到响应:
而在 HTTP2 服务的 8443 端口下,虽然其是直接代理到 HTTP1.1 的服务中,但由于其本身是多路复用,且是在服务端向 HTTP1.1 同时并发请求,所有请求几乎在 2 秒后同一时间全部返回,从而不会导致后续接口一直处于等待的情况产生:
当我们将 HTTP1.1 服务的返回时间设置为 [1 - 4) 秒(只需将 setTimeout 时间设置为 (1 + Math.random() * 3) * 1000) 即可)随机返回来模拟后端接口差异时,依然可以实现快速响应:
并发请求数量的限制只存在于浏览器中,当开启 HTTP2 后浏览器就会利用多路复用突破该限制,Nginx 反向代理并不会受到该限制制约。
通过对比,我们证实了并非全链路开启 HTTP2 才能使 HTTP2 生效。因此在实际应用中,只需要对外暴露的服务是 HTTP2 即可,其内部反向代理的各种服务并不需要全都实现 HTTP2,且本身服务器内部存在各种不同的协议,例如某些地方是更高效的 RPC 通信,其完全和 HTTP 并不沾边,但显然它们并不会影响到对外暴露的 HTTP2 服务。
在同时并发发送大量请求的情况下,HTTP2 能够明显提升前端收到接口响应的速度,从而提升用户体验,尤其是在 ICRM 这样庞大且多请求并发的项目中。
写在最后
如果你希望在本地开发时也开启 http2 服务,请参考我的另一篇短文《让你的本地开发支持http2》。