Dubbo3 Triple協(xié)議遷移指南

2022-04-12 17:54 更新

Triple 介紹

Triple 協(xié)議的格式和原理請參閱 RPC 通信協(xié)議

根據(jù) Triple 設(shè)計的目標(biāo),?Triple ?協(xié)議有以下優(yōu)勢:

  • 具備跨語言交互的能力,傳統(tǒng)的多語言多 SDK 模式和 Mesh 化跨語言模式都需要一種更通用易擴展的數(shù)據(jù)傳輸協(xié)議。
  • 提供更完善的請求模型,除了支持傳統(tǒng)的 Request/Response 模型(Unary 單向通信),還支持 Stream(流式通信) 和 Bidirectional(雙向通信)。
  • 易擴展、穿透性高,包括但不限于 Tracing / Monitoring 等支持,也應(yīng)該能被各層設(shè)備識別,網(wǎng)關(guān)設(shè)施等可以識別數(shù)據(jù)報文,對 Service Mesh 部署友好,降低用戶理解難度。
  • 完全兼容 grpc,客戶端/服務(wù)端可以與原生grpc客戶端打通。
  • 可以復(fù)用現(xiàn)有 grpc 生態(tài)下的組件, 滿足云原生場景下的跨語言、跨環(huán)境、跨平臺的互通需求。

當(dāng)前使用其他協(xié)議的 Dubbo 用戶,框架提供了兼容現(xiàn)有序列化方式的遷移能力,在不影響線上已有業(yè)務(wù)的前提下,遷移協(xié)議的成本幾乎為零。

需要新增對接 Grpc 服務(wù)的 Dubbo 用戶,可以直接使用 Triple 協(xié)議來實現(xiàn)打通,不需要單獨引入 grpc client 來完成,不僅能保留已有的 Dubbo 易用性,也能降低程序的復(fù)雜度和開發(fā)運維成本,不需要額外進行適配和開發(fā)即可接入現(xiàn)有生態(tài)。

對于需要網(wǎng)關(guān)接入的 Dubbo 用戶,Triple 協(xié)議提供了更加原生的方式,讓網(wǎng)關(guān)開發(fā)或者使用開源的 grpc 網(wǎng)關(guān)組件更加簡單。網(wǎng)關(guān)可以選擇不解析 payload ,在性能上也有很大提高。在使用 Dubbo 協(xié)議時,語言相關(guān)的序列化方式是網(wǎng)關(guān)的一個很大痛點,而傳統(tǒng)的 HTTP 轉(zhuǎn) Dubbo 的方式對于跨語言序列化幾乎是無能為力的。同時,由于 Triple 的協(xié)議元數(shù)據(jù)都存儲在請求頭中,網(wǎng)關(guān)可以輕松的實現(xiàn)定制需求,如路由和限流等功能。

Dubbo2 協(xié)議遷移流程

Dubbo2 的用戶使用 dubbo 協(xié)議 + 自定義序列化,如 hessian2 完成遠程調(diào)用。

而 Grpc 的默認僅支持 Protobuf 序列化,對于 Java 語言中的多參數(shù)以及方法重載也無法支持。

Dubbo3的之初就有一條目標(biāo)是完美兼容 Dubbo2,所以為了 Dubbo2 能夠平滑升級, Dubbo 框架側(cè)做了很多工作來保證升級的無感,目前默認的序列化和 Dubbo2 保持一致為?hessian2?。

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

接下來我們我們以一個使用 Dubbo2 協(xié)議的工程 來舉例,如何一步一步安全的升級。

  1. 僅使用 ?dubbo ?協(xié)議啟動 ?provider ?和 ?consumer?,并完成調(diào)用。
  2. 使用 ?dubbo ?和 ?tri ?協(xié)議 啟動?provider?,以 ?dubbo? 協(xié)議啟動 ?consumer?,并完成調(diào)用。
  3. 僅使用 ?tri ?協(xié)議 啟動 ?provider?和 ?consumer?,并完成調(diào)用。

定義服務(wù)

  1. 定義接口
public interface IWrapperGreeter {

    //... 
    
    /**
     * 這是一個普通接口,沒有使用 pb 序列化
     */
    String sayHello(String request);

}
  1. 實現(xiàn)類如下
public class IGreeter2Impl implements IWrapperGreeter {

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

僅使用 dubbo 協(xié)議

為保證兼容性,我們先將部分 provider 升級到 ?dubbo3 ?版本并使用 ?dubbo ?協(xié)議。

使用 dubbo 協(xié)議啟動一個 Provider 和 Consumer ,完成調(diào)用,輸出如下:result

同時使用 dubbo 和 triple 協(xié)議

對于線上服務(wù)的升級,不可能一蹴而就同時完成 provider 和 consumer 升級, 需要按步操作,保證業(yè)務(wù)穩(wěn)定。 第二步, provider 提供雙協(xié)議的方式同時支持 dubbo + tri 兩種協(xié)議的客戶端。

結(jié)構(gòu)如圖所示:  strust

按照推薦升級步驟,provider 已經(jīng)支持了tri協(xié)議,所以 dubbo3的 consumer 可以直接使用 tri 協(xié)議

使用dubbo協(xié)議和triple協(xié)議啟動ProviderConsumer,完成調(diào)用,輸出如下:

result

僅使用 triple 協(xié)議

當(dāng)所有的 consuemr 都升級至支持 ?Triple ?協(xié)議的版本后,?provider ?可切換至僅使用 ?Triple ?協(xié)議啟動

結(jié)構(gòu)如圖所示:  strust

Provider 和 Consumer 完成調(diào)用,輸出如下:


result

實現(xiàn)原理

通過上面介紹的升級過程,我們可以很簡單的通過修改協(xié)議類型來完成升級??蚣苁窃趺磶臀覀冏龅竭@些的呢?

通過對 Triple 協(xié)議的介紹,我們知道Dubbo3的 Triple 的數(shù)據(jù)類型是 protobuf 對象,那為什么非 protobuf 的 java 對象也可以被正常傳輸呢。

這里 Dubbo3 使用了一個巧妙的設(shè)計,首先判斷參數(shù)類型是否為 protobuf 對象,如果不是。用一個 protobuf 對象將 request 和 response 進行 wrapper,這樣就屏蔽了其他各種序列化帶來的復(fù)雜度。在 wrapper 對象內(nèi)部聲明序列化類型,來支持序列化的擴展。

wrapper 的protobuf的 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進行包裝,對于響應(yīng)使用TripleResponseWrapper進行包裝。

對于請求參數(shù),可以看到 args 被repeated修飾,這是因為需要支持 java 方法的多個參數(shù)。當(dāng)然,序列化只能是一種。序列化的實現(xiàn)沿用 Dubbo2 實現(xiàn)的 spi

多語言用戶 (正在使用 Protobuf)

建議新服務(wù)均使用該方式

對于 Dubbo3 和 Triple 來說,主推的是使用 protobuf 序列化,并且使用 proto 定義的 IDL 來生成相關(guān)接口定義。以 IDL 做為多語言中的通用接口約定,加上 Triple 與 Grpc 的天然互通性,可以輕松地實現(xiàn)跨語言交互,例如 Go 語言等。

將編寫好的 .proto 文件使用 dubbo-compiler 插件進行編譯并編寫實現(xiàn)類,完成方法調(diào)用:

result

從上面升級的例子我們可以知道,Triple 協(xié)議使用 protbuf 對象序列化后進行傳輸,所以對于本身就是 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 新提供的一種調(diào)用類型,在以下場景時建議使用流的方式:

  • 接口需要發(fā)送大量數(shù)據(jù),這些數(shù)據(jù)無法被放在一個 RPC 的請求或響應(yīng)中,需要分批發(fā)送,但應(yīng)用層如果按照傳統(tǒng)的多次 RPC 方式無法解決順序和性能的問題,如果需要保證有序,則只能串行發(fā)送
  • 流式場景,數(shù)據(jù)需要按照發(fā)送順序處理, 數(shù)據(jù)本身是沒有確定邊界的
  • 推送類場景,多個消息在同一個調(diào)用的上下文中被發(fā)送和處理

Stream 分為以下三種:

  • SERVER_STREAM(服務(wù)端流)  SERVER_STREAM
  • CLIENT_STREAM(客戶端流)  CLIENT_STREAM
  • BIDIRECTIONAL_STREAM(雙向流)  BIDIRECTIONAL_STREAM
由于 java 語言的限制,BIDIRECTIONAL_STREAM 和 CLIENT_STREAM 的實現(xiàn)是一樣的。

在 Dubbo3 中,流式接口以 SteamObserver 聲明和使用,用戶可以通過使用和實現(xiàn)這個接口來發(fā)送和處理流的數(shù)據(jù)、異常和結(jié)束。

對于 Dubbo2 用戶來說,可能會對StreamObserver感到陌生,這是Dubbo3定義的一種流類型,Dubbo2 中并不存在 Stream 的類型,所以對于遷移場景沒有任何影響。

流的語義保證

  • 提供消息邊界,可以方便地對消息單獨處理
  • 嚴格有序,發(fā)送端的順序和接收端順序一致
  • 全雙工,發(fā)送不需要等待
  • 支持取消和超時

非 PB 序列化的流

  1. api
public interface IWrapperGreeter {

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

    void sayHelloServerStream(String request, StreamObserver<String> response);
}
Stream 方法的方法入?yún)⒑头祷刂凳菄栏窦s定的,為防止寫錯而導(dǎo)致問題,Dubbo3 框架側(cè)做了對參數(shù)的檢查, 如果出錯則會拋出異常。 對于 雙向流(BIDIRECTIONAL_STREAM), 需要注意參數(shù)中的 StreamObserver 是響應(yīng)流,返回參數(shù)中的 StreamObserver 為請求流。
  1. 實現(xiàn)類
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. 調(diào)用方式
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);
}

流的實現(xiàn)原理

Triple協(xié)議的流模式是怎么支持的呢?

  • 從協(xié)議層來說,Triple 是建立在 HTTP2 基礎(chǔ)上的,所以直接擁有所有 HTTP2 的能力,故擁有了分 stream 和全雙工的能力。
  • 框架層來說,StreamObserver 作為流的接口提供給用戶,用于入?yún)⒑统鰠⑻峁┝魇教幚???蚣茉谑瞻l(fā) stream data 時進行相應(yīng)的接口調(diào)用, 從而保證流的生命周期完整。

Triple 與應(yīng)用級注冊發(fā)現(xiàn)

關(guān)于 Triple 協(xié)議的應(yīng)用級服務(wù)注冊和發(fā)現(xiàn)和其他語言是一致的,可以通過下列內(nèi)容了解更多。

與 GRPC 互通

通過對于協(xié)議的介紹,我們知道 Triple 協(xié)議是基于 HTTP2 并兼容 GRPC。為了保證和驗證與GRPC互通能力,Dubbo3 也編寫了各種從場景下的測試。詳細的可以通過這里 了解更多。

未來: Everything on Stub

用過 Grpc 的同學(xué)應(yīng)該對 Stub 都不陌生。 Grpc 使用 compiler 將編寫的 proto 文件編譯為相關(guān)的 protobuf 對象和相關(guān) rpc 接口。默認的會同時生成幾種不同的 stub

  • blockingStub
  • futureStub
  • reactorStub

stub 用一種統(tǒng)一的使用方式幫我們屏蔽了不同調(diào)用方式的細節(jié)。不過目前 Dubbo3 暫時只支持傳統(tǒng)定義接口并進行調(diào)用的使用方式。

在不久的未來,Triple 也將實現(xiàn)各種常用的 Stub,讓用戶寫一份proto文件,通過 comipler 可以在任意場景方便的使用,請拭目以待。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號