作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢!
(文中的 http2 是指明文的 http2 协议,也叫 h2c, 并未测试 TLS 加密的情况)
如果仅从协议的角度对比,http2 会比 http1 更快吗?如果更快,能快多少?
基于以上疑问,我基于 C# 的 Kestrel 框架,做了一个协议性能的对比。
结论
节约大家的时间,先说结论:
- 在同样的运行环境,同样的业务逻辑情况下。最好情况,http2 比 http1 快 4.03 倍
- 从处理字节数上看:http1 平均每个请求 230 字节,http2 平均每个请求 112 字节。就单个请求而言,http2 的包体积只有 http1 的包的 48.7%.
- http2 是二进制协议,理论上一定比 http1 这样的文本协议更快。
- 发挥 http2 的性能优势的关键参数是
MaxStreamsPerConnection(我测试时设置为 200),也就是说,一定要在一个 tcp 上并发多个 stream,才能发挥 http2 的性能优势。
- 发挥 http2 的性能优势的关键参数是
- 从应用上说:
- api 服务、rpc 服务,使用 http2 更好
- 文件下载(图片、资源文件等)、html 页面输出、文件上传、大数据量的 post 等等,使用 http1 更好
- 使用 http1 在代理服务器上也能获得性能优势。请看前一篇: 为什么在代理服务器上测试, http2 的转发性能比 http 1 更低?
- 是否简单的使用 http2 的客户端,就能轻松实现 http2 比 http1 提升了 4 倍?答案是否定的,http2 的客户端并不简单。
- 我一共使用了四种 http2 的客户端来测试:
- 使用 nghttp2 客户端:C 语言实现,专门用于压测的工具,测试得到 http2 比 http1 快 4.03 倍
- 使用 golang 客户端,每个 HttpClient 对象对应一个协程,每个协程内一发一收:http2 的吞吐量是 http1 的 80%
- 如果以 http1 的模式来使用 http2,http2 会比 http1 慢
- 使用 golang 客户端,每个 HttpClient 对象上限制只有一个 tcp 连接,每个 HttpClient 对应 40 个协程一发一收:http2 的吞吐量是 http1 的 1.86 倍
- 通过限制 tcp 连接,来让每个 tcp 连接上并行多个 stream,这才是 http2 client 的正确用法
- 压测客户端启动两个进程:http2 的吞吐量是 http1 的 2.28 倍,由此说明 golang 的 http2 的 client 内部有很多锁,单个进程不如多个进程性能好。
- 使用 golang 客户端,完全基于 tcp 协议来实现,每个 tcp 连接上,一个协程专门用于 send,一个协程专门用于 recv: http2 的吞吐量是 http1 的 2.68 倍
- nghttp2 客户端可能做了一些 socket option 的优化,导致做到了最好的压测性能
- 如果希望做到 http2 上的极致性能,基于 tcp 来实现是个好主意
压测环境说明
基于 kestrel 的C# 服务端
- 源码位置: https://github.com/ahfuzhang/QiWa/tree/v0.1-http-compare/code-snippets/Http2EchoServer
- 使用 dotnet 10 编译
make -f Makefile_linux build
bash
$(DOTNET) publish $(PRJ).csproj \
-r linux-x64 \
-p:DefineConstants=UNIX -p:AllowUnsafeBlocks=true \
-p:PublishAot=true \
-p:StripSymbols=false \
--self-contained true \
-c Release -o $(BUILD_DIR)
- http1 和 http2 采用同样的 callback 函数
服务器运行环境
- 运行于 linux amd64 环境,在 docker 容器中运行
- 限定一个 cpu
- CPU 型号:Intel(R) Core(TM) Ultra 7 265KF, 小核, 4.5GHz
- 内存 256 MB
- 限制线程池的线程数为 1:
ThreadPool.SetMaxThreads(1, 1) - 客户端在同一个机器上请求服务器,尽量达到单核 100% 的 CPU 占用率,然后在客户端统计 QPS
bash
docker run -it --rm \
--platform=linux/amd64 \
--cpuset-cpus="19" \
-m 256m \
-v $(BUILD_DIR):/app \
--network=host \
mcr.microsoft.com/dotnet/runtime:10.0 \
/app/Http2EchoServer \
-http2.port=9081 \
-http1.port=9082 \
-threadpool.max=1
nghttp2 压测
- 压测 http1 端口的命令如下:
bash
docker run --rm -it --network host goodideal/nghttp2:latest \
h2load -p http/1.1 -c 120 -t 8 -n 2000000 \
http://127.0.0.1:9082/echo?seq=9999
-
120 个 tcp 连接时,测试得到最优性能表现
-
压测 http2 的命令如下:
bash
docker run --rm -it --network host goodideal/nghttp2:latest \
h2load -p h2c -c 8 -t 8 --max-concurrent-streams 60 -n 1000000 \
http://127.0.0.1:9081/echo?seq=8888
- 最优性能组合为:
- 连接数 8
- max-concurrent-streams = 60, 每个 tcp 连接上并发 60 个 stream
golang 客户端+http1的模式压测
- 源码请看: https://github.com/ahfuzhang/QiWa/tree/v0.1-http-compare/code-snippets/GolangHttp2Client
- 编译:
make build - 运行:
make run
golang 客户端 + 每个 tcp 连接上多个并发的模式
- 源码: https://github.com/ahfuzhang/QiWa/tree/v0.1-http-compare/code-snippets/GolangHttp2ClientV2
- 编译:
make build - 运行:
make run - 最佳配置:
- tcp 连接数 8
- 每个 tcp 连接上 40 个并发
golang 客户端 + 基于 tcp 协议来实现 http2 客户端
- 源码: https://github.com/ahfuzhang/QiWa/tree/v0.1-http-compare/code-snippets/GolangHttp2ClientV3
- 编译:
make build - 运行:
make run - 最佳配置:
- tcp 连接数 16
- 每个 tcp 连接上 80 个并发stream (不是 80 个协程)
总结
- http2 虽然比 http1 快了 4 倍,但是 rpc 的场景,自定义协议肯定更快
- c 实现的 nghttp2 的性能很强悍,我用 golang 实现的版本与之还有一大段距离
- Kestrel 框架的性能相当强悍 (虽然多核情况下可能会变慢)
希望对你有用。😃