在最近的一個專案中,我對用 Dropwizard 編寫的老化的整體 Java Web 服務進行了現代化改造。本服務透過 AWS Lambda 函數處理許多第三方 (3P) 依賴項,但由於架構的同步、阻塞性質,效能落後。此設定的 P99 延遲為 20 秒,在等待無伺服器功能完成時阻塞請求執行緒。這種阻塞導致執行緒池飽和,導致流量高峰時請求頻繁失敗。
問題的癥結是每個對 Lambda 函數的請求都會佔用 Java 服務中的一個請求執行緒。由於這些 3P 函數通常需要相當長的時間才能完成,因此處理它們的執行緒將保持阻塞狀態,從而消耗資源並限制可擴展性。以下是此阻塞行為在程式碼中的範例:
// Blocking code example public String callLambdaService(String payload) { String response = externalLambdaService.invoke(payload); return response; }
在此範例中,callLambdaService 方法會等待,直到 externalLambdaService.invoke() 回傳回應。同時,沒有其他任務可以使用該執行緒。
為了解決這些瓶頸,我使用非同步和非阻塞方法重新建構了服務。此變更涉及使用呼叫 Lambda 函數的 HTTP 用戶端來使用 org.asynchttpclient 庫中的 AsyncHttpClient,該程式庫內部使用 EventLoopGroup 非同步處理請求。
使用 AsyncHttpClient 有助於卸載阻塞操作,而無需消耗池中的執行緒。以下是更新後的非阻塞呼叫的範例:
// Non-blocking code example public CompletableFuture<String> callLambdaServiceAsync(String payload) { return CompletableFuture.supplyAsync(() -> { return asyncHttpClient.invoke(payload); }); }
除了使單一呼叫成為非阻塞之外,我還使用 CompletableFuture 連結了多個依賴項呼叫。使用 thenCombine 和 thenApply 等方法,我可以非同步獲取並組合來自多個來源的數據,從而顯著提高吞吐量。
CompletableFuture<String> future1 = callLambdaServiceAsync(payload1); CompletableFuture<String> future2 = callLambdaServiceAsync(payload2); CompletableFuture<String> combinedResult = future1.thenCombine(future2, (result1, result2) -> { return processResults(result1, result2); });
在實作過程中,我觀察到 Java 的預設 AsyncResponse 物件缺乏型別安全性,允許傳遞任意 Java 物件。為了解決這個問題,我創建了一個帶有泛型的 SafeAsyncResponse 類,它確保只能傳回指定的回應類型,從而提高可維護性並降低運行時錯誤的風險。如果多次寫入回應,此類也會記錄錯誤。
// Blocking code example public String callLambdaService(String payload) { String response = externalLambdaService.invoke(payload); return response; }
// Non-blocking code example public CompletableFuture<String> callLambdaServiceAsync(String payload) { return CompletableFuture.supplyAsync(() -> { return asyncHttpClient.invoke(payload); }); }
為了驗證這些變更的有效性,我使用虛擬執行緒編寫了負載測試來模擬單一電腦上的最大吞吐量。我產生了不同等級的無伺服器函數執行時間(範圍從1 到20 秒),發現新的非同步非阻塞實作在執行時間較短時將吞吐量提高了8 倍,在執行時間較長時吞吐量提高了約4 倍。
在設定這些負載測試時,我確保調整客戶端層級的連線限制以最大化吞吐量,這對於避免非同步系統中的瓶頸至關重要。
在執行這些壓力測試時,我在我們的自訂 HTTP 用戶端中發現了一個隱藏的錯誤。客戶端使用連接逾時設定為 Integer.MAX_VALUE 的信號量,這表示如果客戶端用完可用連接,它將無限期地阻塞執行緒。解決此錯誤對於防止高負載場景中潛在的死鎖至關重要。
人們可能想知道為什麼我們不簡單地切換到虛擬線程,虛擬線程可以透過允許線程阻塞而不需要大量的資源成本來減少對非同步程式碼的需求。然而,虛擬線程目前存在一個限制:它們在同步操作期間被固定。這意味著當虛擬執行緒進入同步區塊時,它無法卸載,可能會阻塞作業系統資源,直到操作完成。
例如:
CompletableFuture<String> future1 = callLambdaServiceAsync(payload1); CompletableFuture<String> future2 = callLambdaServiceAsync(payload2); CompletableFuture<String> combinedResult = future1.thenCombine(future2, (result1, result2) -> { return processResults(result1, result2); });
在此程式碼中,如果由於沒有可用資料而導致讀取阻塞,則虛擬線程將被固定到作業系統線程,從而防止其卸載並阻塞作業系統線程。
幸運的是,隨著 JEP 491 的出現,Java 開發人員可以期待虛擬執行緒行為的改進,其中可以更有效地處理同步程式碼中的阻塞操作,而不會耗盡平台執行緒。
透過將我們的服務重構為非同步非阻塞架構,我們實現了顯著的效能改進。透過實作 AsyncHttpClient、引入 SafeAsyncResponse 來實現類型安全性以及進行負載測試,我們能夠優化 Java 服務並大幅提高吞吐量。該專案是單體應用程式現代化方面的一次有價值的實踐,並揭示了適當的非同步實踐對可擴展性的重要性。
隨著 Java 的發展,我們未來也許能夠更有效地利用虛擬線程,但就目前而言,非同步和非阻塞架構仍然是高延遲、依賴第三方的服務中效能最佳化的重要方法。
以上是透過非同步和非阻塞架構實現 Java 整體現代化以獲得更好的效能的詳細內容。更多資訊請關注PHP中文網其他相關文章!