博客 / 詳情

返回

[DotNet] Kestrel 框架中, http1 與 http2 的性能對比

作者:張富春(ahfuzhang),轉載時請註明作者和引用鏈接,謝謝!

  • cnblogs博客
  • zhihu
  • Github
  • 公眾號:一本正經的瞎扯

(文中的 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 的性能優勢。
  • 從應用上説:
    • 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
$(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)
    • 為何限制,請看前一篇:C#的 ThreadPool.SetMaxThreads() 配置最大線程數到底對性能有多大影響
  • 客户端在同一個機器上請求服務器,儘量達到單核 100% 的 CPU 佔用率,然後在客户端統計 QPS
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 端口的命令如下:
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 的命令如下:

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 框架的性能相當強悍 (雖然多核情況下可能會變慢)

希望對你有用。😃

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.