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 网关组件更加简单。网关可以选择不解析 payload ,在性能上也有很大提高。在使用 Dubbo 协议时,语言相关的序列化方式是网关的一个很大痛点,而传统的 HTTP 转 Dubbo 的方式对于跨语言序列化几乎是无能为力的。同时,由于 Triple 的协议元数据都存储在请求头中,网关可以轻松的实现定制需求,如路由和限流等功能。

Dubbo2 协议迁移流程

Dubbo2 的用户使用 dubbo 协议 + 自定义序列化,如 hessian2 完成远程调用。

而 Grpc 的默认仅支持 Protobuf 序列化,对于 Java 语言中的多参数以及方法重载也无法支持。

Dubbo3的之初就有一条目标是完美兼容 Dubbo2,所以为了 Dubbo2 能够平滑升级, Dubbo 框架侧做了很多工作来保证升级的无感,目前默认的序列化和 Dubbo2 保持一致为hessian2

所以,如果决定要升级到 Dubbo3 的 Triple 协议,只需要修改配置中的协议名称为 tri (注意: 不是triple)即可。

接下来我们我们以一个使用 Dubbo2 协议的工程 来举例,如何一步一步安全的升级。

  1. 仅使用 dubbo 协议启动 providerconsumer,并完成调用。
  2. 使用 dubbotri 协议 启动provider,以 dubbo 协议启动 consumer,并完成调用。
  3. 仅使用 tri 协议 启动 providerconsumer,并完成调用。

定义服务

  1. 定义接口
  1. public interface IWrapperGreeter {
  2. //...
  3. /**
  4. * 这是一个普通接口,没有使用 pb 序列化
  5. */
  6. String sayHello(String request);
  7. }
  1. 实现类如下
  1. public class IGreeter2Impl implements IWrapperGreeter {
  2. @Override
  3. public String sayHello(String request) {
  4. return "hello," + request;
  5. }
  6. }

仅使用 dubbo 协议

为保证兼容性,我们先将部分 provider 升级到 dubbo3 版本并使用 dubbo 协议。

使用 dubbo 协议启动一个 ProviderConsumer ,完成调用,输出如下: result

同时使用 dubbo 和 triple 协议

对于线上服务的升级,不可能一蹴而就同时完成 provider 和 consumer 升级, 需要按步操作,保证业务稳定。 第二步, provider 提供双协议的方式同时支持 dubbo + tri 两种协议的客户端。

结构如图所示: strust

按照推荐升级步骤,provider 已经支持了tri协议,所以 dubbo3的 consumer 可以直接使用 tri 协议

使用dubbo协议和triple协议启动ProviderConsumer,完成调用,输出如下:

result

仅使用 triple 协议

当所有的 consuemr 都升级至支持 Triple 协议的版本后,provider 可切换至仅使用 Triple 协议启动

结构如图所示: strust

ProviderConsumer 完成调用,输出如下:

result

实现原理

通过上面介绍的升级过程,我们可以很简单的通过修改协议类型来完成升级。框架是怎么帮我们做到这些的呢?

通过对 Triple 协议的介绍,我们知道Dubbo3的 Triple 的数据类型是 protobuf 对象,那为什么非 protobuf 的 java 对象也可以被正常传输呢。

这里 Dubbo3 使用了一个巧妙的设计,首先判断参数类型是否为 protobuf 对象,如果不是。用一个 protobuf 对象将 requestresponse 进行 wrapper,这样就屏蔽了其他各种序列化带来的复杂度。在 wrapper 对象内部声明序列化类型,来支持序列化的扩展。

wrapper 的protobuf的 IDL如下:

  1. syntax = "proto3";
  2. package org.apache.dubbo.triple;
  3. message TripleRequestWrapper {
  4. // hessian4
  5. // json
  6. string serializeType = 1;
  7. repeated bytes args = 2;
  8. repeated string argTypes = 3;
  9. }
  10. message TripleResponseWrapper {
  11. string serializeType = 1;
  12. bytes data = 2;
  13. string type = 3;
  14. }

对于请求,使用TripleRequestWrapper进行包装,对于响应使用TripleResponseWrapper进行包装。

对于请求参数,可以看到 args 被repeated修饰,这是因为需要支持 java 方法的多个参数。当然,序列化只能是一种。序列化的实现沿用 Dubbo2 实现的 spi

多语言用户 (正在使用 Protobuf)

建议新服务均使用该方式

对于 Dubbo3 和 Triple 来说,主推的是使用 protobuf 序列化,并且使用 proto 定义的 IDL 来生成相关接口定义。以 IDL 做为多语言中的通用接口约定,加上 TripleGrpc 的天然互通性,可以轻松地实现跨语言交互,例如 Go 语言等。

将编写好的 .proto 文件使用 dubbo-compiler 插件进行编译并编写实现类,完成方法调用:

result

从上面升级的例子我们可以知道,Triple 协议使用 protbuf 对象序列化后进行传输,所以对于本身就是 protobuf 对象的方法来说,没有任何其他逻辑。

使用 protobuf 插件编译后接口如下:

  1. public interface PbGreeter {
  2. static final String JAVA_SERVICE_NAME = "org.apache.dubbo.sample.tri.PbGreeter";
  3. static final String SERVICE_NAME = "org.apache.dubbo.sample.tri.PbGreeter";
  4. static final boolean inited = PbGreeterDubbo.init();
  5. org.apache.dubbo.sample.tri.GreeterReply greet(org.apache.dubbo.sample.tri.GreeterRequest request);
  6. default CompletableFuture<org.apache.dubbo.sample.tri.GreeterReply> greetAsync(org.apache.dubbo.sample.tri.GreeterRequest request){
  7. return CompletableFuture.supplyAsync(() -> greet(request));
  8. }
  9. void greetServerStream(org.apache.dubbo.sample.tri.GreeterRequest request, org.apache.dubbo.common.stream.StreamObserver<org.apache.dubbo.sample.tri.GreeterReply> responseObserver);
  10. 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);
  11. }

开启 Triple 新特性 —— Stream (流)

Stream 是 Dubbo3 新提供的一种调用类型,在以下场景时建议使用流的方式:

  • 接口需要发送大量数据,这些数据无法被放在一个 RPC 的请求或响应中,需要分批发送,但应用层如果按照传统的多次 RPC 方式无法解决顺序和性能的问题,如果需要保证有序,则只能串行发送
  • 流式场景,数据需要按照发送顺序处理, 数据本身是没有确定边界的
  • 推送类场景,多个消息在同一个调用的上下文中被发送和处理

Stream 分为以下三种:

  • SERVER_STREAM(服务端流) SERVER_STREAM
  • CLIENT_STREAM(客户端流) CLIENT_STREAM
  • BIDIRECTIONAL_STREAM(双向流) BIDIRECTIONAL_STREAM

由于 java 语言的限制,BIDIRECTIONAL_STREAM 和 CLIENT_STREAM 的实现是一样的。

在 Dubbo3 中,流式接口以 SteamObserver 声明和使用,用户可以通过使用和实现这个接口来发送和处理流的数据、异常和结束。

对于 Dubbo2 用户来说,可能会对StreamObserver感到陌生,这是Dubbo3定义的一种流类型,Dubbo2 中并不存在 Stream 的类型,所以对于迁移场景没有任何影响。

流的语义保证

  • 提供消息边界,可以方便地对消息单独处理
  • 严格有序,发送端的顺序和接收端顺序一致
  • 全双工,发送不需要等待
  • 支持取消和超时

非 PB 序列化的流

  1. api
  1. public interface IWrapperGreeter {
  2. StreamObserver<String> sayHelloStream(StreamObserver<String> response);
  3. void sayHelloServerStream(String request, StreamObserver<String> response);
  4. }

Stream 方法的方法入参和返回值是严格约定的,为防止写错而导致问题,Dubbo3 框架侧做了对参数的检查, 如果出错则会抛出异常。 对于 双向流(BIDIRECTIONAL_STREAM), 需要注意参数中的 StreamObserver 是响应流,返回参数中的 StreamObserver 为请求流。

  1. 实现类
  1. public class WrapGreeterImpl implements WrapGreeter {
  2. //...
  3. @Override
  4. public StreamObserver<String> sayHelloStream(StreamObserver<String> response) {
  5. return new StreamObserver<String>() {
  6. @Override
  7. public void onNext(String data) {
  8. System.out.println(data);
  9. response.onNext("hello,"+data);
  10. }
  11. @Override
  12. public void onError(Throwable throwable) {
  13. throwable.printStackTrace();
  14. }
  15. @Override
  16. public void onCompleted() {
  17. System.out.println("onCompleted");
  18. response.onCompleted();
  19. }
  20. };
  21. }
  22. @Override
  23. public void sayHelloServerStream(String request, StreamObserver<String> response) {
  24. for (int i = 0; i < 10; i++) {
  25. response.onNext("hello," + request);
  26. }
  27. response.onCompleted();
  28. }
  29. }
  1. 调用方式
  1. delegate.sayHelloServerStream("server stream", new StreamObserver<String>() {
  2. @Override
  3. public void onNext(String data) {
  4. System.out.println(data);
  5. }
  6. @Override
  7. public void onError(Throwable throwable) {
  8. throwable.printStackTrace();
  9. }
  10. @Override
  11. public void onCompleted() {
  12. System.out.println("onCompleted");
  13. }
  14. });
  15. StreamObserver<String> request = delegate.sayHelloStream(new StreamObserver<String>() {
  16. @Override
  17. public void onNext(String data) {
  18. System.out.println(data);
  19. }
  20. @Override
  21. public void onError(Throwable throwable) {
  22. throwable.printStackTrace();
  23. }
  24. @Override
  25. public void onCompleted() {
  26. System.out.println("onCompleted");
  27. }
  28. });
  29. for (int i = 0; i < n; i++) {
  30. request.onNext("stream request" + i);
  31. }
  32. request.onCompleted();

使用 Protobuf 序列化的流

对于 Protobuf 序列化方式,推荐编写 IDL 使用 compiler 插件进行编译生成。生成的代码大致如下:

  1. public interface PbGreeter {
  2. static final String JAVA_SERVICE_NAME = "org.apache.dubbo.sample.tri.PbGreeter";
  3. static final String SERVICE_NAME = "org.apache.dubbo.sample.tri.PbGreeter";
  4. static final boolean inited = PbGreeterDubbo.init();
  5. //...
  6. void greetServerStream(org.apache.dubbo.sample.tri.GreeterRequest request, org.apache.dubbo.common.stream.StreamObserver<org.apache.dubbo.sample.tri.GreeterReply> responseObserver);
  7. 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);
  8. }

流的实现原理

Triple协议的流模式是怎么支持的呢?

  • 从协议层来说,Triple 是建立在 HTTP2 基础上的,所以直接拥有所有 HTTP2 的能力,故拥有了分 stream 和全双工的能力。

  • 框架层来说,StreamObserver 作为流的接口提供给用户,用于入参和出参提供流式处理。框架在收发 stream data 时进行相应的接口调用, 从而保证流的生命周期完整。

Triple 与应用级注册发现

关于 Triple 协议的应用级服务注册和发现和其他语言是一致的,可以通过下列内容了解更多。

与 GRPC 互通

通过对于协议的介绍,我们知道 Triple 协议是基于 HTTP2 并兼容 GRPC。为了保证和验证与GRPC互通能力,Dubbo3 也编写了各种从场景下的测试。详细的可以通过这里 了解更多。

未来: Everything on Stub

用过 Grpc 的同学应该对 Stub 都不陌生。 Grpc 使用 compiler 将编写的 proto 文件编译为相关的 protobuf 对象和相关 rpc 接口。默认的会同时生成几种不同的 stub

  • blockingStub
  • futureStub
  • reactorStub

stub 用一种统一的使用方式帮我们屏蔽了不同调用方式的细节。不过目前 Dubbo3 暂时只支持传统定义接口并进行调用的使用方式。

在不久的未来,Triple 也将实现各种常用的 Stub,让用户写一份proto文件,通过 comipler 可以在任意场景方便的使用,请拭目以待。

最后修改 December 16, 2022: Fix check (#1736) (97972c1)