Dubbo 協議遷移至 Triple 協議指南

Triple 協議遷移指南

Triple 簡介

關於 Triple 協議的格式和原理,請參考 RPC 通訊協議

根據 Triple 設計目標,Triple 協議具有以下優點

  • 具備跨語言互動能力。傳統的多語言多 SDK 模式和 Mesh 跨語言模式都需要更通用、可擴展的資料傳輸協議。
  • 提供更完整的請求模型。除了支援傳統的 Request/Response 模型(Unary 單向通訊)外,還支援 Stream(串流通訊)和 Bidirectional(雙向通訊)。
  • 易於擴展,高穿透性,包括但不限於 Tracing / Monitoring 等支援,也應能被各層級的設備識別,閘道器設施等可以識別資料封包,對 Service Mesh 部署友好,降低使用者理解難度。
  • 完全相容 grpc,用戶端/伺服器端可以與原生 grpc 用戶端通訊。
  • 可以複用現有 grpc 生態中的組件,滿足雲原生場景下跨語言、跨環境、跨平台的互通需求。

對於目前使用其他協議的 Dubbo 使用者,框架提供了相容現有序列化方式的遷移能力。在不影響現有線上業務的情況下,遷移協議的成本幾乎為零。

需要新增連線到 Grpc 服務的 Dubbo 使用者可以直接使用 Triple 協定來完成連線,不需要另外引入 grpc client 即可完成。不僅可以保留現有的 Dubbo 易用性,還可以降低程式的複雜度以及開發維護成本,無需額外適配和開發即可連接到現有的生態。

對於需要閘道器訪問的 Dubbo 使用者來說,Triple 協定提供了一種更原生的方式,讓閘道器的開發或者使用開源 grpc gateway 組件更加容易。閘道器可以選擇不解析負載,這極大地提高了性能。使用 Dubbo 協定的時候,語言相關的序列化方式是閘道器的一大痛點,傳統的 HTTP 轉 Dubbo 方式對於跨語言序列化幾乎無能為力。同時,由於 Triple 的協定元數據存儲在請求頭中,閘道器可以很容易地實現定制化需求,例如路由和限流。

Dubbo2 協定遷移流程

Dubbo2 使用者使用 dubbo 協定 + 自定義序列化,例如 hessian2 來完成遠程調用。

Grpc 預設只支持 Protobuf 序列化,並且在 Java 語言中無法支持多參數和方法重載。

Dubbo3 在設計之初就以完全相容 Dubbo2 為目標,因此為了保證 Dubbo2 的平滑升級,Dubbo 框架做了大量的工作以保證升級過程無縫衔接,目前預設序列化方式和 Dubbo2 保持一致為 hessian2

因此如果決定升級到 Dubbo3 的 Triple 協定,只需要將配置中的協定名稱修改為 tri 即可(注意:不是 triple)。

接下來我們以一個 [項目] (https://github.com/apache/dubbo-samples/tree/master/3-extensions/protocol/dubbo-samples-triple/src/main/java/ org/apache/dubbo/sample/tri/migration) 為例,如何安全地逐步升級。

  1. 僅使用 dubbo 協定啟動 providerconsumer,並完成調用。
  2. 使用 dubbotri 協定啟動 provider,使用 dubbo 協定啟動 consumer,並完成調用。
  3. 僅使用 tri 協定啟動 providerconsumer,並完成調用。

定義服務

  1. 定義介面
public interface IWrapperGreeter {

    //...
    
    /**
     * This is a normal interface, not serialized using pb
     */
    String sayHello(String request);

}
  1. 實作類別如下
public class IGreeter2Impl implements IWrapperGreeter {

    @Override
    public String sayHello(String request) {
        return "hello," + request;
    }
}

僅使用 dubbo 協定

為了保證相容性,我們首先將部分 provider 升級到 dubbo3 版本,並且使用 dubbo 協定。

使用 dubbo 協定啟動一個 [Provider] (https://github.com/apache/dubbo-samples/blob/master/3-extensions/protocol/dubbo-samples-triple/src/main/java/org /apache/dubbo/sample/tri/migration/ApiMigrationDubboProvider.java) 和 Consumer,完成調用,輸出結果如下: 結果

同時使用 dubbo 和 triple 協定

對於線上服務的升級,不可能同時完成 provider 和 consumer 的升級,需要分步驟操作,保證業務的穩定性。第二步,provider 提供雙協定的方式,同時支持 dubbo + tri 的客戶端。

結構如圖所示: 結構

按照推薦的升級步驟,provider 已經支持了 tri 協定,那麼 dubbo3 的 consumer 可以直接使用 tri 協定

使用 dubbo 協議和 triple 協議啟動 [Provider](https://github.com/apache/dubbo-samples/blob/master/3-extensions/protocol/dubbo-samples-triple/src/main /java/org/apache/dubbo/sample/tri/migration/ApiMigrationBothProvider.java)和 [Consumer](https://github.com/apache/dubbo-samples/blob/master/3-extensions/protocol/ dubbo-samples-triple/src/main/java/org/apache/dubbo/sample/tri/migration/ApiMigrationBothConsumer.java),完成呼叫,輸出如下

result

僅使用 triple 協議

當所有 consumer 都升級到支援 Triple 協議的版本後,provider 可以切換為僅使用 Triple 協議啟動

結構如圖所示:信任

[Provider](https://github.com/apache/dubbo-samples/blob/master/3-extensions/protocol/dubbo-samples-triple/src/main/java/org/apache/dubbo/sample/tri/ migration/ApiMigrationTriProvider.java)和 [Consumer](https://github.com/apache/dubbo-samples/blob/master/3-extensions/protocol/dubbo-samples-triple/src/main/java/org/apache/dubbo/sample/tri /migration/ApiMigrationTriConsumer.java)完成呼叫,輸出如下

result

實現原理

透過上述升級流程,我們可以輕鬆地透過修改協議類型來完成升級,框架是如何幫助我們做到這一點的呢?

透過 Triple 協議的介紹,我們知道 Dubbo3 中 Triple 的數據類型是 protobuf 對象,那麼為什麼非 protobuf 的 java 對象也可以正常傳輸呢?

這裡 Dubbo3 使用了一個巧妙的設計,首先判斷參數類型是否為 protobuf 對象,如果不是,則使用一個 protobuf 對象來包裹 requestresponse,這樣就屏蔽了其他序列化帶來的複雜性。在 wrapper 對象內部聲明序列化類型,以支援序列化擴展。

protobuf 的 wrapper 的 IDL 如下

syntax = "proto3";

package org.apache.dubbo.triple;

message TripleRequestWrapper {
    //hessian4
    // json
    string serializeType = 1;
    repeated bytes args = 2;
    repeated string argTypes = 3;
}

message TripleResponseWrapper {
    string serializeType = 1;
    bytes data = 2;
    string type = 3;
}

對於請求,使用 TripleRequestWrapper 進行包裹,對於響應,使用 TripleResponseWrapper 進行包裹。

對於請求參數,可以看到 args 被 repeated 修飾,這是因為需要支援 java 方法的多個參數,當然序列化只能有一個,序列化的實現沿用 Dubbo2 實現的 spi

多語言使用者(使用 Protobuf)

建議所有新服務都使用這種方式

對於 Dubbo3 和 Triple 來說,主要的建議是使用 protobuf 序列化,並使用 proto 定義的 IDL 來生成相關的接口定義。使用 IDL 作為多語言的通用接口規範,再加上 TripleGrpc 天然的互操作性,可以輕鬆實現跨語言交互,例如 Go 語言。

使用 dubbo-compiler 插件編譯準備好的 .proto 文件,並編寫實現類來完成方法呼叫

result

從上面的升級例子中,我們可以知道 Triple 協議使用 protobuf 對象進行序列化傳輸,所以對於本身就是 protobuf 對象的方法來說,沒有其他邏輯。

使用 protobuf 插件編譯後的接口如下

public interface PbGreeter {

    static final String JAVA_SERVICE_NAME = "org.apache.dubbo.sample.tri.PbGreeter";
    static final String SERVICE_NAME = "org.apache.dubbo.sample.tri.PbGreeter";

    static final boolean inited = PbGreeterDubbo.init();

    org.apache.dubbo.sample.tri.GreeterReply greet(org.apache.dubbo.sample.tri.GreeterRequest request);

    default CompletableFuture<org.apache.dubbo.sample.tri.GreeterReply> greetAsync(org.apache.dubbo.sample.tri.GreeterRequest request){
        return CompletableFuture. supplyAsync(() -> greet(request));
    }

    void greetServerStream(org.apache.dubbo.sample.tri.GreeterRequest request, org.apache.dubbo.common.stream.StreamObserver<org.apache.dubbo.sample.tri.GreeterReply> responseObserver);

    org.apache.dubbo.common.stream.StreamObserver<org.apache.dubbo.sample.tri.GreeterRequest> greetStream(org.apache.dubbo.common.stream.StreamObserver<org.apache.dubbo.sample.tri.GreeterReply> responseObserver);
}

開啟 Triple 的新特性 - Stream (串流)

Stream 是 Dubbo3 提供的一種新的呼叫類型,建議在以下場景中使用 Stream

  • 接口需要發送大量的數據,這些數據無法放置在一個 RPC 請求或響應中,需要分批發送。但如果應用層無法解決傳統多次 RPC 方法的順序和性能問題,如果需要保證順序,則只能串行發送
  • 在流式場景下,需要按照數據發送的順序進行處理,數據本身沒有確定的邊界
  • 在推送場景下,在同一個呼叫的上下文中發送和處理多條消息

串流分為以下三種類型:

  • SERVER_STREAM(伺服器端串流) SERVER_STREAM
  • CLIENT_STREAM(客戶端串流) CLIENT_STREAM
  • BIDIRECTIONAL_STREAM(雙向串流) BIDIRECTIONAL_STREAM

由於 java 語言的限制,BIDIRECTIONAL_STREAM 和 CLIENT_STREAM 的實作方式相同。

在 Dubbo3 中,串流介面被宣告並使用為 StreamObserver,使用者可以透過使用及實作此介面來傳送和處理串流資料、例外和結束訊號。

對於 Dubbo2 的使用者來說,他們可能不熟悉 StreamObserver,這是 Dubbo3 定義的串流類型。Dubbo2 中沒有 Stream 類型,因此它對遷移場景沒有影響。

串流語義保證

  • 提供訊息邊界,可以輕鬆地分別處理訊息
  • 嚴格排序,傳送端的順序與接收端的順序一致
  • 全雙工,無需等待傳送
  • 支援取消和逾時

非 PB 序列化串流

  1. API
public interface IWrapperGreeter {

    StreamObserver<String> sayHelloStream(StreamObserver<String> response);

    void sayHelloServerStream(String request, StreamObserver<String> response);
}

串流方法的輸入參數和回傳值有嚴格的約定。為了防止因撰寫錯誤造成的問題,Dubbo3 框架端會檢查參數,如果有錯誤則拋出例外。對於 BIDIRECTIONAL_STREAM,需要注意的是參數中的 StreamObserver 是回應串流,而回傳值中的 StreamObserver 是請求串流。

  1. 實作類
public class WrapGreeterImpl implements WrapGreeter {

    //...

    @Override
    public StreamObserver<String> sayHelloStream(StreamObserver<String> response) {
        return new StreamObserver<String>() {
            @Override
            public void onNext(String data) {
                System.out.println(data);
                response.onNext("hello,"+data);
            }

            @Override
            public void onError(Throwable throwable) {
                throwable. printStackTrace();
            }

            @Override
            public void onCompleted() {
                System.out.println("onCompleted");
                response.onCompleted();
            }
        };
    }

    @Override
    public void sayHelloServerStream(String request, StreamObserver<String> response) {
        for (int i = 0; i < 10; i++) {
            response.onNext("hello," + request);
        }
        response.onCompleted();
    }
}
  1. 呼叫方法
delegate.sayHelloServerStream("server stream", new StreamObserver<String>() {
    @Override
    public void onNext(String data) {
        System.out.println(data);
    }

    @Override
    public void onError(Throwable throwable) {
        throwable. printStackTrace();
    }

    @Override
    public void onCompleted() {
        System.out.println("onCompleted");
    }
});


StreamObserver<String> request = delegate.sayHelloStream(new StreamObserver<String>() {
    @Override
    public void onNext(String data) {
        System.out.println(data);
    }

    @Override
    public void onError(Throwable throwable) {
        throwable. printStackTrace();
    }

    @Override
    public void onCompleted() {
        System.out.println("onCompleted");
    }
});
for (int i = 0; i < n; i++) {
    request.onNext("stream request" + i);
}
request.onCompleted();

使用 Protobuf 序列化的串流

對於 Protobuf 序列化方法,建議撰寫 IDL 並使用 compiler 插件進行編譯和產生程式碼。產生的程式碼大致如下:

public interface PbGreeter {

    static final String JAVA_SERVICE_NAME = "org.apache.dubbo.sample.tri.PbGreeter";
    static final String SERVICE_NAME = "org.apache.dubbo.sample.tri.PbGreeter";

    static final boolean inited = PbGreeterDubbo.init();
    
    //...

    void greetServerStream(org.apache.dubbo.sample.tri.GreeterRequest request, org.apache.dubbo.common.stream.StreamObserver<org.apache.dubbo.sample.tri.GreeterReply> responseObserver);

    org.apache.dubbo.common.stream.StreamObserver<org.apache.dubbo.sample.tri.GreeterRequest> greetStream(org.apache.dubbo.common.stream.StreamObserver<org.apache.dubbo.sample.tri.GreeterReply> responseObserver);
}

串流的實作原理

Triple 協定的串流模式是如何支援的?

  • 從協定層的角度來看,Triple 建構在 HTTP2 的基礎之上,因此它直接具備 HTTP2 的所有功能,所以它具有分割 stream 和全雙工的能力。

  • 在框架層面,框架提供 StreamObserver 作為串流介面給使用者,用於對輸入和輸出參數進行串流處理。框架在傳送和接收串流資料時會進行對應的介面呼叫,以確保串流生命週期的完整性。

Triple 與應用層級服務註冊與發現

Triple 協定的應用層級服務註冊與發現與其他語言一致,您可以透過以下內容了解更多資訊。

與 gRPC 互通

透過協定的介紹,我們知道 Triple 協定基於 HTTP2 並且相容 gRPC。為了確保並驗證與 gRPC 的互通性,Dubbo3 也在各種測試情境中撰寫了相關測試。詳情請透過這裡了解更多。

未來:基於 Stub 的一切

使用過 gRPC 的開發者應該熟悉 Stub。gRPC 使用 compiler 將撰寫的 proto 檔案編譯成相關的 Protobuf 物件和相關的 RPC 介面。預設情況下,會同時產生幾個不同的 stub

- blockingStub

  • - …

stub 幫助我們以統一的方式屏蔽不同呼叫方法的細節。然而,目前 Dubbo3 只支援傳統的介面定義和呼叫方式。

在不久的將來,Triple 也將實作各種常用的 Stub,讓使用者只需撰寫一個 proto 檔案,就可以透過 compiler 方便地在任何場景中使用,敬請期待。


上次修改時間:2023 年 2 月 9 日:更新 docsy 至 0.6.0 (#2141) (20081578326)