Dubbo 協議遷移至 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) 為例,如何安全地逐步升級。
- 僅使用
dubbo
協定啟動provider
和consumer
,並完成調用。 - 使用
dubbo
和tri
協定啟動provider
,使用dubbo
協定啟動consumer
,並完成調用。 - 僅使用
tri
協定啟動provider
和consumer
,並完成調用。
定義服務
- 定義介面
public interface IWrapperGreeter {
//...
/**
* This is a normal interface, not serialized using pb
*/
String sayHello(String request);
}
- 實作類別如下
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),完成呼叫,輸出如下
僅使用 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)完成呼叫,輸出如下
實現原理
透過上述升級流程,我們可以輕鬆地透過修改協議類型來完成升級,框架是如何幫助我們做到這一點的呢?
透過 Triple
協議的介紹,我們知道 Dubbo3 中 Triple
的數據類型是 protobuf
對象,那麼為什麼非 protobuf
的 java 對象也可以正常傳輸呢?
這裡 Dubbo3 使用了一個巧妙的設計,首先判斷參數類型是否為 protobuf
對象,如果不是,則使用一個 protobuf
對象來包裹 request
和 response
,這樣就屏蔽了其他序列化帶來的複雜性。在 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
作為多語言的通用接口規範,再加上 Triple
和 Grpc
天然的互操作性,可以輕鬆實現跨語言交互,例如 Go 語言。
使用 dubbo-compiler
插件編譯準備好的 .proto
文件,並編寫實現類來完成方法呼叫
從上面的升級例子中,我們可以知道 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(伺服器端串流)
- CLIENT_STREAM(客戶端串流)
- BIDIRECTIONAL_STREAM(雙向串流)
由於
java
語言的限制,BIDIRECTIONAL_STREAM 和 CLIENT_STREAM 的實作方式相同。
在 Dubbo3 中,串流介面被宣告並使用為 StreamObserver
,使用者可以透過使用及實作此介面來傳送和處理串流資料、例外和結束訊號。
對於 Dubbo2 的使用者來說,他們可能不熟悉 StreamObserver,這是 Dubbo3 定義的串流類型。Dubbo2 中沒有 Stream 類型,因此它對遷移場景沒有影響。
串流語義保證
- 提供訊息邊界,可以輕鬆地分別處理訊息
- 嚴格排序,傳送端的順序與接收端的順序一致
- 全雙工,無需等待傳送
- 支援取消和逾時
非 PB 序列化串流
- API
public interface IWrapperGreeter {
StreamObserver<String> sayHelloStream(StreamObserver<String> response);
void sayHelloServerStream(String request, StreamObserver<String> response);
}
串流方法的輸入參數和回傳值有嚴格的約定。為了防止因撰寫錯誤造成的問題,Dubbo3 框架端會檢查參數,如果有錯誤則拋出例外。對於
BIDIRECTIONAL_STREAM
,需要注意的是參數中的StreamObserver
是回應串流,而回傳值中的StreamObserver
是請求串流。
- 實作類
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();
}
}
- 呼叫方法
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
方便地在任何場景中使用,敬請期待。