iT邦幫忙

2024 iThome 鐵人賽

DAY 20
1
Software Development

微服務奇兵:30天Quarkus特訓營系列 第 20

開發概念建置-Quarkus 響應式架構解析與寫法

  • 分享至 

  • xImage
  •  

在上一篇中,我們大致介紹了響應式設計的原理及其應用情境。接下來,將進一步說明Reactive(非阻塞)與Blocking(阻塞)操作在實際應用中的區別。根據我個人的經驗,響應式程式設計的學習曲線較為陡峭,尤其是在開發前端應用程式和設備控制時,經常使用ReactiveX函式庫來處理異步資料流。不過,Quarkus並非直接基於RxJava,而是採用了Vert.x這個非同步處理架構。接下來,我會簡單介紹Vert.x的運作方式,這一點非常重要,因為它正是Quarkus在響應式架構中表現出色的關鍵之一(???)。

Servlet vs Vert.x

Servlet

不過在聊Vert.x架構前,先提一下Servlet**。傳統的REST服務,**大多數基於Java EE(Java企業版)的服務使用Servlet來實作。Servlet是Java處理HTTP請求的一個標準,為同步機制(3.0以前),通常會佔用一個執行緒來處理每一個HTTP請求。下圖為一個典型的Java Servlet架構,展示了Web瀏覽器如何與Web伺服器和Servlet容器進行互動,進而連接到資料庫。

https://ithelp.ithome.com.tw/upload/images/20240921/20115895hVc1uGyt6R.png

可以看到Servlet容器部分,負責管理多個Servlet。當收到請求時,Servlet容器會選擇合適的Servlet來處理該請求。它的細部流程如下

https://ithelp.ithome.com.tw/upload/images/20240921/20115895PdbVhyxXLL.png
Step1 : Servlet 接收到請求,並將其綁定到伺服器的一個執行緒來進行處理。

Step2 : Servlet可能會進行一些業務邏輯的初步處理,比如檢查請求的合法性或解析參數。

Step3 : 這部分通常是最耗時的。圖中的業務處理可能涉及到資料庫操作、外部API調用等耗時的操作。因為它是同步的,在業務處理完成之前,這個請求的執行緒會一直被佔用,無法釋放。這就是同步模型的一個特徵——請求必須等到業務邏輯完成,執行緒才能進入下一步。

Step4 : 線程結束:一旦業務處理完成,Servlet會將結果返回給客戶端,並在這時結束這個執行緒。此時伺服器可以重新分配這個執行緒去處理新的請求。

不過在3.0後,Servlet就加入非同步機制。另外你可以認知到,Servlet其實就是Java EE 架構中用於處理 HTTP 請求的底層 API。稍微簡單戴一下整體跟JVM關係,當你啟動一個Java Web服務,啟動流程會是

  1. 啟動JVM
  2. 在VJM啟動 Servlet容器
  3. Servlet 容器加載並初始化 Web 應用程式

角色運作職責為

  • JVM 提供 Java 程式的執行環境
  • Servlet 容器在 JVM 中運行,管理 Web 應用程式的生命週期
  • Web 應用程式(包含 Servlet)由容器加載並執行

所以當啟動一個 Java Web 服務時,通常是啟動了一個包含 Servlet 容器的應用程式伺服器(如 Tomcat)。這個伺服器運行在 JVM 中。

Vert.x

Servlet 講完了後,接下來來講Vert.x。Vert.x 是一個框架,它提供了一整套工具和函式庫,基於非阻塞、事件驅動的架構用來開發響應式、分散式和事件驅動的應用程式。且HTTP 處理不依賴 Servlet,它有自己的 HTTP 伺服器實作,完全獨立於 Servlet 規範。Vert.x 的 HTTP 處理基於非阻塞 I/O 和事件驅動模型,這與傳統的 Servlet 模型有很大不同。使用 Netty(一個非同步事件驅動的網路應用框架)作為底層,提供高效能的 HTTP 服務。

一樣借用網路現有架構圖如下,可以看到,Vert.x 的非阻塞事件驅動架構。
https://ithelp.ithome.com.tw/upload/images/20240921/20115895fZvfiW7ufE.png

  • Host 與 JVM : 每個 JVM 都可以運行一個或多個 Vert.x Instance,每個Instance內部可以包含多個 Verticle。Verticle 是 Vert.x 中的基本單元,類似於 Servlet 這樣的 Web 組件,但與 Servlet 不同的是,Verticle 完全基於非阻塞的事件模型。

  • Verticle : 小型的應用邏輯單元,負責處理特定的業務邏輯或 I/O 操作。Vert.x Instancee可以管理和調度多個 Verticle 並發運行,而不會阻塞執行緒。

  • Event Bus : Vert.x 架構的核心組件之一。它允許 Verticle 之間相互通訊,無論它們是在同一個 JVM 內,還是分佈在不同的 JVM 中。請求和事件會在 Event Bus 中進行傳遞,Verticle 可以根據不同的事件進行處理,類似於消息隊列系統。這種設計方式非常適合高併發、分散式系統。

  • Event Handler(事件處理器):事件經由 Event Bus 傳遞後,由相應的 Event Handler 來處理。這些事件處理器可以處理各種 I/O 操作、業務邏輯或網路請求。

可以看到這樣的設計,如同前一章節提到的,架構符合Reactive 程式的核心特徵:非阻塞、事件驅動、高效能和可伸縮性。

Netty

這邊稍微提一下Netty,其實講到這邊,也許會思考Java WebFlux它底層的機制是什麼,其實也是Netty。Netty 是一個基於事件驅動的網路應用框架。下圖為Netty的框架圖

https://ithelp.ithome.com.tw/upload/images/20240921/20115895hiSd2mB0Mw.png

架構會有三大部分

核心層 (Core)

  • Zero-Copy-Capable Rich Byte Buffer: Netty 高效處理 I/O 操作的基礎,Zero Copy允許在傳輸資料時,不需要多次在用戶態和核心態之間進行拷貝操作,這樣可大幅降低 CPU 和記憶體的消耗。說透了就是用高效的 Byte Buffer 來管理 I/O 資料,這讓網路 I/O 操作變得更加快速。

  • niversal Communication API : 提供了一個統一的通信 API,無論你是處理 HTTP、WebSocket 還是其他協定,Netty 都能以一致的方式來進行處理。這種抽象層的設計使得開發者能夠用相同的程式碼處理不同的協定,極大簡化了開發的複雜度。

  • Extensible Event Model : 事件驅動的框架,可擴展事件模型允許開發者自定義處理不同的 I/O 事件。

傳輸服務層(Transport Services Layer)

  • Socket & Datagram: 支持傳統的 TCP 和 UDP 協定,負責低階網路傳輸。
  • HTTP Tunnel : 支持 HTTP 隧道化通信
  • In-VM Pipe : 支持在同一個 Java 虛擬機內的管道通信,它可以用來在同一個 JVM 實例內部的不同應用之間進行高效通信,而不需要通過網路層。

協定支持層(Protocol Support Layer)

  • HTTP & WebSocket:支持 HTTPWebSocket 協定
  • SSL & StartTLS : 支持 SSL/TLS 加密協定
  • Google Protocol Buffers(Protobuf): 高效的二進位協定,常用於高性能的數據序列化。
  • zlib/gzip 壓縮 : 支持 zlib 和 gzip 的資料壓縮,這有助於降低網路傳輸時的資料量,提升傳輸速度。
  • 大型檔案傳輸 : 支持大型檔案的傳輸。
  • RTSP : 支持 RTSP(即時串流協定),用來處理即時視訊、音訊等串流傳輸場景。
  • Legacy和二進位協定 : 支持一些舊式的文字和二進位協定

Netty 的 核心層 提供高效的 I/O 處理能力,傳輸服務層 支持多種傳輸方式,而 協定支持層 則讓 Netty 能夠處理多樣化的網路協定,這些層次相輔相成,使得Vert.x 可以藉由 Netty 高效運行並處理大量並發請求。

阻塞 VS 非阻塞(響應式寫法)

硬核的觀念講完後,接著來簡易提一下響應式寫法的差異性,假如現在有一個需求會有兩個資料原,並且要將兩個資料原彙整後再回傳,以下分別用阻塞和非阻塞的寫法來說明。

簡易概念

  • 阻塞

阻塞寫法通常是線性、同步的,這代表執行緒在等待I/O操作完成時會被「阻塞」。例如,假設有兩個資料源Data1Data2,需要彙整兩者後返回結果:

public Data aggregateData() {
    Data data1 = fetchDataFromDatabase();     // 請求資料庫,此時執行緒被阻塞
    Data data2 = fetchDataFromExternalAPI();  // 請求外部 API,此時執行緒再次被阻塞
    
    return combine(data1, data2);             // 整合兩部分的資料
}

fetchDataFromDatabase()完成後,才會發出下一個請求fetchDataFromExternalAPI()。這樣的寫法導致無法充分利用系統的資源,因為每個I/O操作都需要等待結果,從而增加了處理時間。

  • 非阻塞

響應式程式設計使用非阻塞I/O,允許在等待操作完成的同時繼續執行其他任務。這樣可以同時處理多個操作,大幅提高效能。我們使用Mutiny框架來完成:

public Uni<Data> aggregateData() {
    Uni<Data> data1Uni = fetchDataFromDatabaseAsync();      // 非阻塞地請求資料庫
    Uni<Data> data2Uni = fetchDataFromExternalAPIAsync();   // 非阻塞地請求外部 API
    
    return Uni.combine().all().unis(data1Uni, data2Uni)     // 同時等待兩個 Uni 完成
             .asTuple()
             .onItem().transform(tuple -> combine(tuple.getItem1(), tuple.getItem2()));
}

利用Uni類型來代表異步操作。data1Unidata2Uni會同時發送請求,無需等待前一個操作完成,減少了等待時間,從而提升了程序的處理效能。

Mutiny中的Uni與Multi

UniMulti是Mutiny框架中處理異步任務的核心抽象

  • Uni<?>:代表0或1個結果的異步操作。類似於Java中的CompletionStage,通常用來表示單一結果的操作。

    // 建立一個直接回傳值的Uni
    Uni<String> uniFromValue = Uni.createFrom().item("Hello, Mutiny!");
    
    // 創建一個有邏輯處理過後的Uni
    Uni<String> uniFromSupplier = Uni.createFrom().item(() -> {
        // 進行某些操作
        return "Async result";
    });
    

    可以通過以下方式將CompletionStage轉換為Uni

    Uni<String> uniFromCompletionStage = Uni.createFrom().completionStage(this::asyncMethod);
    
  • Multi<?>:表示多個結果的異步操作,適用於處理多筆資料或事件流的情境。

    Multi<String> names = Multi.createFrom().items("Alice", "Bob", "Charlie");
    
    Multi<Integer> lengths = names
        .filter(name -> !name.equals("Bob"))      // 過濾掉 "Bob"
        .onItem().transform(String::length);      // 將名稱轉換為其長度
    

錯誤處理

在處理非同步操作時,錯誤管理是一個重要部分。Mutiny提供多種錯誤處理機制,允許在操作失敗時進行補救。例如:

Uni<String> recovered = uniFromValue
    .onFailure()
    .recoverWithItem("Fallback item in case of failure");

非阻塞轉同步

儘管Uni是非阻塞的,也可以將其轉換為阻塞的同步操作,但需注意這會失去響應式程式設計的效能優勢。例如

String result = uniFromValue.await().indefinitely();
System.out.println(result); // 輸出:Hello, Mutiny!

多個API並行請求

從多個API來源同時獲取資料,然後在所有資料到達後進行彙整時,範例如下,同時發送兩個請求,並使用combine(Mutiny中的Uni.combine())來等待兩個非阻塞操作完成,再進行資料合併。

public Uni<Data> aggregateData() {
    Uni<Data> data1Uni = fetchDataFromDatabaseAsync();      // 非阻塞地請求資料庫
    Uni<Data> data2Uni = fetchDataFromExternalAPIAsync();   // 非阻塞地請求外部 API
    
    return Uni.combine().all().unis(data1Uni, data2Uni)     // 同時等待兩個 Uni 完成
             .asTuple()
             .onItem().transform(tuple -> combine(tuple.getItem1(), tuple.getItem2()));
}

順序執行數據流(串聯API請求)

等某個操作完成後,再啟動另一個異步操作。例如,當需要先查詢一個API,然後根據它的結果去查詢另一個API時

public Uni<Data> fetchAndProcessData() {
    return fetchDataFromDatabaseAsync()                      // 第一步,取得資料庫資料
        .onItem().transformToUni(data1 -> fetchFromApiAsync(data1)) // 第二步,依據data1結果進行API查詢
        .onItem().transform(apiData -> process(apiData));      // 第三步,處理API結果
}

並行與串聯混合處理

一些更複雜的情境下,可能會遇到需要同時發送多個請求,但某些請求需要根據其他請求的結果來決定下一步。這種「並行與串聯混合」的需求可以通過結合combineflatMap來實現。例如,假設你有三個API,API1API2需要同時查詢,API3的查詢取決於API1的結果。

public Uni<Data> processMultipleApis() {
    Uni<Data1> api1 = fetchFromApi1Async();    // 非阻塞請求 API1
    Uni<Data2> api2 = fetchFromApi2Async();    // 非阻塞請求 API2
    
    return Uni.combine().all().unis(api1, api2)   // 並行執行 API1 和 API2
        .asTuple()
        .onItem().transformToUni(tuple -> fetchFromApi3Async(tuple.getItem1()))  // API1完成後執行 API3
        .onItem().transform(api3Data -> combineResults(api3Data, tuple.getItem2())); // 最後合併API2和API3的結果
}

錯誤處理和回退策略

在分布式系統中,網絡請求容易失敗,因此錯誤處理和回退(fallback)策略非常重要。
onFailure()(Mutiny),應對異常情況。

public Uni<Data> fetchWithFallback() {
    return fetchFromApiAsync()
        .onFailure().recoverWithItem(() -> getCachedData());  // 請求失敗時使用本地快取
}

重試機制

重試也是常見的錯誤處理策略之一,範例如下

public Uni<Data> fetchWithRetry() {
    return fetchFromApiAsync()
        .onFailure().retry().atMost(3);  // 失敗時重試最多3次
}

不過稍微提一下,響應式設計面臨的一個挑戰是,許多傳統的後端套件或依賴仍然是同步的,這時就需要我們手動將它們「包裝」成非阻塞的操作,以融入響應式工作流。例如,資料庫操作、文件系統讀寫或某些外部API呼叫,可能仍然是阻塞式API。例如

Uni<String> uniFromCompletionStage = Uni.createFrom().completionStage(() -> someBlockingMethod());

響應式程式設計在後端的場景經常用於解決高併發、多API聚合、串聯邏輯處理以及實時資料流的問題。這些情境下,響應式框架的並行處理與異步資料流能力能顯著提升系統效能。


上一篇
開發概念建置-阻塞式 vs 非阻塞式 vs 響應式API
下一篇
開發概念建置:CIDR表示格式(重要)
系列文
微服務奇兵:30天Quarkus特訓營25
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言