假設您有三個獨立的服務,它們是:- mycart.mycoolapp.com- mypayment.mycoolapp.com- mycoolproducts.mycoolapp.com
您的客戶使用這三個功能。因此,您可以創建一個反向代理。您的客戶端可以連接到此反向代理,例如,mycool-reverse-proxy.com。
現在,假設您的客戶想要獲得所有酷產品的列錶。因此,它請求代理mycool-reverse-proxy.com/products。現在代理可以在您請求產品服務的 API 路徑中看到。所以它將這個請求重定向到產品服務。
反向代理的全部意義在於抽象您的後端邏輯。因此,客戶端不需要知道您擁有多少服務、它們的地址或它們在哪裏。
很酷,不是嗎?是的!
反向代理在這裏做什麼?它比特於您的客戶端和您的服務之間並路由請求。
你可能認為反向代理是一個負載均衡器。但事實並非如此。他們有完全不同類型的責任,他們的意思也不同。
現在,讓我們快速計算一下!
例如,客戶端將調用服務 A 10 個請求,服務 B 20 次,服務 C 5 次。但是由於反向代理比特於客戶端和服務之間,所有請求都將通過它,不是嗎?
是的當然。所有 35 個請求都將通過它。現在,隨著您的業務增長,您將獲得更多負載。因此,您可以擴展您的服務。但正如我們所看到的,反向代理經曆了所有服務的總負載。因此,您將進行水平/垂直縮放。您添加更多機器。
因此,在開發反向代理時,您將選擇一種語言,例如 Java、Node、Golang 等。這種語言是否對反向代理有任何影響,或者反向代理中這些語言之間是否存在任何性能差异?讓我們來了解一下。
在本文中,我將只專注於分析 Spring Boot(這是一個基於 Java 的框架)與 Golang 中反向代理的性能。
Java線程
Java 是一種基於 JVM 的語言。當您在 Java 中創建線程時,JVM 會將這個線程映射到操作系統級別的線程。您的 Java 線程和 OS 線程基本上是 1:1 映射。操作系統級線程具有固定大小的堆棧。它不可能無限增長。在64 比特機器中,大小為1 MB。因此,如果您的內存為 1 GB,您可以擁有大約1000 個線程。由於 JVM 以1:1 映射將線程映射到操作系統級線程,因此您可以使用基於 JVM 的語言(例如 Java)創建大約1000 個線程。因此,Java 線程很重,具有固定的堆棧大小,並且您可以在1 GB內存中擁有大約1K線程。
什麼是 Goroutine?
goroutine 是由 Go 運行時管理的輕量級線程。它與我們之前看到的 Java 線程有點不同。Goroutine 調度器調度 goroutine 執行。但有趣的事實是 Goroutines 不是 1:1 映射到操作系統級線程的。多個 Goroutine 被映射到一個單一的 OS 線程中。所以它被多路複用到操作系統線程。關於 Goroutine 的另一個有趣的事實是 Go 沒有固定大小的堆棧。相反,它可以根據數據增長或縮小。所以 Goroutine 利用了這個特性。平均而言,新 Goroutine 堆棧的默認大小約為2 KB,並且可以根據需要增加或縮小。所以,在 1 GB 的內存中,1024 * 1024 / 2 = 5,24,288如果 Goroutines.
Java 和 Golang 的上下文切換比較
如上所述,JVM 使用操作系統級線程。所以當線程執行操作系統級調度程序調度時,執行線程。所以上下文切換發生在操作系統級別。當內核進行上下文切換時,它必須做大量的工作。因此,上下文切換發生時需要一些時間(微秒級)。
另一方面,Golang 有自己的 Goroutine 調度器,專門為此任務優化構建。Goroutine 調度器將 Goroutine 映射到內核中的 OS 級線程和進程。並且它是上下文切換的最佳選擇,以减少這樣做的時間。
Golang 和 Java(Spring Boot) 在反向代理中的性能比較
因此,在討論了所有理論問題之後,就到了這一點。由於 Spring boot 在 Tomcat 服務器中運行,因此每個請求都由單獨的線程處理。因此,當請求到來時,會分配一個線程來處理此請求。由於它在底層使用 JVM,我們看到這些線程是操作系統級線程。
另一方面,如果您使用 Goroutine 來處理每個請求,即當一個請求到來時,您分配一個 Goroutine 來處理該請求,它將比 Spring Boot 執行得更好。因為我們在上一段中已經看到 Goroutine 相對於線程有一定的優勢。
我們還看到,在 1GB RAM 中,Goroutine 的數量會比線程多(5,24,288 對 1000)。因此,您可以使用 Golang 服務處理更多請求。
由於反向代理通常會承受系統的所有負載,因此那裏總會有大量請求。如果你用輕量級的 Goroutine 處理它,你可以利用 Goroutines 的所有優點來獲得高吞吐量和更好的性能,並同時處理更多請求。
Goroutines 的缺點
盡管 Goroutines 有很多積極的方面,但也有一些缺點。在 Spring Boot 中會有固定數量的線程池。因此,當請求到來時,從池中取出一個線程,當工作完成時,該線程再次保留在池中。它由Tomcat服務器處理。
然而,它不會在 Golang 世界中自動處理。因此,無論您是使用著名的 Golangnet/http包設計傳輸層,還是使用Gin-gonic 之類的框架,默認情况下都沒有 Goroutine 池。所以,你必須手動處理它。
但是你可能想知道為什麼我需要一個池?這是為什麼?
部署代碼後,無論是在服務器中還是在 Kubernetes Pod 中,總會有一個操作系統。操作系統有一個術語叫做ulimit。ulimit 是一個文件描述符,也是訪問 I/O 資源的指示器。當我們從我們的代碼向外界發出網絡請求時,會打開一個 TCP 連接,然後在握手後發出請求。ulimit 錶示在負責建立 TCP 連接的操作系統中可以有多少文件描述符。您擁有的 ulimit 越多,您可以創建的 TCP 連接就越多。
Linux 操作系統的 ulimit 值約為2¹⁶ = 65,536。在 Mac 系統中,默認值為252。但是你總是可以增加它ulimit -n number_of_ulimit_you_want。
而這也是 Goroutine 的失敗點之一。
反向代理會發生什麼?我們看到一個請求到達代理,然後根據請求,反向代理將請求重定向到任何下遊服務。為此,反向代理從自身發出出站請求。並且要發出出站請求,需要 TCP 連接,這基本上是網絡 I/O。文件描述符處理它。ulimit 錶示操作系統可以擁有多少個文件描述符。
您可以在 1GB 內存中啟動近 5,24,288 個 Goroutine。現在在反向代理中,如果您使用 Golang 實現它並且您沒有任何 Goroutine 池,將會發生的情况是,您可以收到大量請求並且您的服務器不會陷入困境。但是由於反向代理會將請求重定向到所有不同的下遊,因此它將發出所有出站請求。結果,將打開如此多的 TCP 連接,並且所有這些連接都是網絡 I/O。所以文件描述符將處理所有這些。因此,如果對您的其他服務的出站請求數量超過您擁有的 ulimit 數量,那麼您將收到太多打開文件錯誤。
所以,這就是為什麼你應該有一個基於你擁有的 ulimit 的 Goroutine 池,這樣你就不會陷入上述錯誤。
實際比較
所以……我做了一個實驗,用 Spring boot 編寫了一個產品服務。然後我開發了兩個反向代理。一個是用 Spring boot 編寫的,另一個是用 Golang 編寫的(我用 Gin-gonic 作為路由器)。然後我使用JMeter對整個系統進行負載測試。這是結果。
說明:
- Number of Requests是具有相同標簽的樣本數。
- 平均值是一組結果的平均時間。
- Min是具有相同標簽的樣本的最短時間。
- Max是具有相同標簽的樣本的最長時間
- Throughput 吞吐量以每秒/分鐘/小時的請求數來衡量。選擇時間單比特以使顯示的速率至少為 1.0。吞吐量保存到CSV文件時,以requests/second錶示,即30.0 requests/minute保存為0.5。
- Received KB/sec是以每秒千字節為單比特的吞吐量。時間以毫秒為單比特。
- Standard Deviation標准差是數據集可變性的度量。JMeter 計算總體標准偏差(STDEVP 函數模擬)
- Avg Bytes — 具有相同標簽的樣本的響應字節的算術平均值。
可以看出,Golang 的性能比 Spring Boot 好。Spring Boot 系統偏差很大。
我們來看看Golang代理和Spring Boot代理的CPU和內存使用情况。
在我的 hexacore 機器上,我運行了用 Spring boot 編寫的產品服務。然後首先我啟動了spring boot代理,然後拿了矩陣。之後,我運行 Go 代理並獲取矩陣。它們如下。
Golang使用的CPU和內存比SpringBoot代理少。根據前面的理論討論,這是可以預期的。
因此,可以說,在服務器中部署代理時,就CPU和內存等資源而言,Golang的性能比Spring Boot要好。因此,在伸縮的情况下,就像水平伸縮一樣,需要更多的SpringBoot代理來滿足巨大的負載。然而,由於自動縮放組通常在進行縮放時監視實例的CPU和內存使用情况,因此它需要更少的Go代理。