本內容是對知名性能評測博主 Anton Putra .NET (C#) vs. Fiber (Go): Performance (Latency - Throughput - Saturation - Availability) 內容的翻譯與整理, 有適當刪減, 相關指標和結論以原作為準
在本視頻中,我們將對比 C# 與 .NET 框架和 Golang 的表現。在第一個測試中,我們將專注于各自的最小框架實現。我們將測量 CPU 使用率、內存使用率、每秒可處理的請求數量,以及最重要的——最終用戶延遲。
我們會運行第一個測試直到其中一個應用程序失敗,然后再繼續運行一段時間,以找出另一個應用程序的崩潰臨界點。
在第二個測試中,我們將模擬一個常見的使用場景:從本地文件系統讀取一個文件(在本例中是圖片),然后將其上傳到 S3 存儲桶。同時,我們會將有關該圖片的一些元數據(比如使用時間戳、文件名等)保存到一個關系型數據庫中,例如 Postgres。此外,我們還會測量上傳圖片到 S3 的延遲以及將數據插入數據庫的查詢延遲。
基于之前的基準測試和反饋,我還會測量每個應用程序創建的數據庫連接數。
我們還將測量在云環境中啟動應用程序所需的時間。這不僅包括啟動時間,還包括拉取鏡像和通過健康檢查所需的時間。這將直接影響在云端的自動擴展能力。
對于 C# 我們將使用最新的 .NET 8 框架,這是由微軟構建并開源的跨平臺框架,它支持構建各種類型的應用程序,從簡單的 Web 應用與移動應用,到微服務和機器學習模型。
在本視頻中,我們將使用 .NET Core 框架搭配 Minimal API,以構建該語言與框架中最快的應用程序。
根據微軟的說法,這種設置在性能上應該優于 Golang。然而,他們的對比使用的是 Go 平臺上的 Gin 框架。
另一方面,對于Golang我們將使用 Fiber 框架,這是該語言中最快的 HTTP 框架之一。但有些人并不喜歡 Fiber,因為它并不完全兼容標準庫,因此并不是所有的中間件、可觀測性工具以及其他相關組件都能直接與 Fiber 一起使用。
現在,讓我們來看一下具體的測試。我有一個家用實驗室(Home Lab
),使用 VMware Hypervisor 創建了一個多節點的 Kubernetes 集群。
我已經在 Kubernetes 中設置了一些監控組件:使用 Prometheus 服務器來抓取指標,Grafana 用于可視化這些指標并創建儀表盤,cAdvisor 用來抓取每個 Kubernetes 節點的指標,提供每個 Pod 的 CPU、內存與網絡使用情況。我還部署了 kube-state-metrics,我們將用它來測量啟動時間。
首先,我們將應用程序部署到 Kubernetes 集群。我有一個客戶端程序,可以配置來生成負載并測量每個請求的延遲。
測量延遲的最佳方式是使用外部客戶端,因為這能模擬任何最終用戶的真實體驗。當客戶端開始發送請求時,Prometheus 會抓取這些指標,并在儀表盤中展示。
在第二個測試中,我們將使用應用程序中的另一個端點 /api/images
。每當客戶端發送請求時,應用程序會從本地文件系統讀取一個文件并上傳到 S3 存儲桶。我們不會使用 AWS S3,而是使用 MinIO,它是一個兼容 S3 的對象存儲,同樣部署在 Kubernetes 中。
在客戶端上傳圖片之后,我們會將一些圖片的元數據(比如創建時間戳、文件名等)寫入一個關系型數據庫——Postgres,這也部署在 Kubernetes 中。
你可以在我的 GitHub 公共倉庫中找到每個應用程序的源代碼,以及用于部署監控組件的 Terraform 腳本、Helm 圖表和 YAML 文件。
在這個測試中,我們還將使用 Prometheus 客戶端對每個應用程序進行指標注入,用來測量一些特定函數的調用時間。例如,我們將跟蹤上傳圖片所需的時間,以及將數據寫入數據庫所需的時間。
因此,在第二個測試中,我們將從外部客戶端和應用程序本身中收集指標。
接下來,我們來比較兩個應用程序的鏡像大小。對于 .NET 應用程序,我們將使用多階段 Docker 構建,并在最終階段使用 distroless 鏡像。這樣可以減少最終鏡像的體積。你也可以使用 Alpine 作為最終階段的基礎鏡像,但那樣會增加 1 到 2 兆字節的大小。
對于 Go 應用程序,我們也將使用多階段構建,并在最終階段使用 distroless 鏡像,這個鏡像由 Google 提供和構建。
現在,讓我們查看最終的鏡像大小。Go 的鏡像比 .NET 的小兩倍以上。實際上,你甚至可以將 Go 的鏡像壓縮到大約 39MB (使用go build -ldflags "-s -w"
)。更小的鏡像意味著 Kubernetes 在拉取鏡像和啟動應用程序時所需時間更短。
接下來,我們來測量啟動時間。這包括應用程序的啟動時間、Kubernetes 拉取鏡像的時間以及 Pod 通過健康檢查的時間。
這并不是一個非常科學的方法,因為網絡速度會極大地影響拉取鏡像的時間。但你仍然可以獲得一個大致的概念。我們可以使用 kube-state-metrics 來測量 Pod 就緒所需的時間。相關儀表盤也可以在我的代碼倉庫中找到。
好了,讓我們創建兩個 Pod,看看它們需要多長時間啟動。
由于鏡像更小,Golang 第一個啟動(耗時7s),而 C# 緊隨其后(耗時12s)。我多次運行了這個測試,并在每次之前都從 Kubernetes 節點中刪除了鏡像。如果你使用 Karpenter、自動擴縮容工具或云端的 Spot 節點,需要考慮 Kubernetes 每次都必須重新拉取鏡像。否則,兩者的啟動時間差異不大。
好了,我們開始第一次測試。首先,我會將這些應用程序部署到 Kubernetes,并在沒有任何負載的情況下運行大約 20 分鐘。
你可能會注意到 C# 的 CPU 使用率略高,但真正的區別在于內存使用。在空閑狀態下,C# 和 .NET 框架的內存消耗遠高于 Golang。
現在我們開始測試。首先,我們運行 10 個客戶端,持續約 5 分鐘,大約每個應用程序會接收到 20 個請求每秒。你會立即注意到 C# 的 CPU 使用率飆升,而且從客戶端側測量的整體延遲明顯更高。但內存使用幾乎沒有變化,因為我們只是返回了硬編碼的 JSON 數據。
接下來,我們增加到 50 個客戶端,大約是每秒 100 個請求。CPU 使用的差異變得更大,而延遲的差異保持一致。
現在,我們將客戶端數量增加到 200。整體延遲有所下降,但兩者之間的相對比例保持不變。此時,Golang 更高效,CPU 使用更少。
我們繼續,將客戶端數量增加到 400,測試 5 分鐘。
然后,我們將客戶端數量降到 5,觀察應用程序的適應能力。
接著,嘗試 10 個客戶端。
從現在開始,我們將從 550 個客戶端開始,每隔 5 分鐘持續增加,直到某個應用程序失敗。
當請求數達到每秒約 1200 時,.NET 應用程序開始丟棄部分請求。
請求達到每秒約 1300 時,.NET 應用程序開始出現故障,并伴隨延遲激增。我們會繼續運行幾分鐘。
最后,Golang 開始丟棄一些請求,但它的延遲仍保持在幾毫秒的范圍內。
整個測試持續了大約 2.5 小時。現在讓我打開各項圖表,查看整個測試期間的表現。
首先是 CPU 使用率圖表。
接著是內存使用圖表。
然后是每秒請求數圖表。
最后是整體延遲圖表。
這就是第一個測試的全部內容,我們對比了框架本身:.NET Minimal API 與 Golang Fiber 框架,兩者在 Kubernetes 中并行運行。
現在我們開始第二個測試,先從 10 個客戶端開始。你可以立刻看出 CPU 和內存使用顯著上升。同時我們還會測量延遲,看看讀取文件并上傳到 S3 所需的時間。目前來看,Golang 在低負載下表現更高效。我們還會測量保存文件元數據到關系型數據庫的延遲。
目前看起來,Golang 的性能更好,使用更少的 CPU 和內存完成任務。
接下來我們模擬一個突發流量,將客戶端數量暫時增加到 50,持續 5 分鐘。
S3 的延遲幾乎不變,但數據庫延遲的差異增大。
好了,我們將客戶端數量降回 10。
接下來的一個小時左右,我將逐步將客戶端數量從 100 增加到 200,以觀察哪一個應用程序表現更好。
你還可以看到 .NET 增加了連接池大小,而 Golang 保持相對穩定。我本可以手動設置連接池大小,但我的目標是測試這些框架和庫在默認設置下的表現,因為很多人就是這么使用的。
好了,我再運行幾分鐘的測試。到目前為止,這大概就是兩個應用程序所能處理的最大請求量了。你可以看到每秒請求數保持平穩。
現在讓我打開整個測試期間的 CPU 使用圖表。
接下來是內存使用圖表。
然后是數據庫延遲圖表。
這是每個應用程序的連接池大小圖。順便一提,Postgres 默認可以打開 100 個并發連接——至少這是我用 Helm 圖表部署數據庫時的默認設置。
現在是客戶端延遲圖表。
在每秒請求數指標中,你可以看到 Golang 實際上能處理更多請求。
最后是 S3 延遲圖表。