Dubbo 协议迁移到 Triple 协议指南

Triple 协议迁移指南

Triple 简介

关于 Triple 协议的格式和原理,请参考 RPC 通信协议

根据 Triple 设计目标,Triple 协议具有以下优势

  • 能够进行跨语言交互。传统的多种语言和多种 SDK 模式以及 Mesh 跨语言模式都需要更通用和可扩展的数据传输协议。
  • 提供更完整的请求模型。除了支持传统的请求/响应模型(Unary 单向通信)外,还支持流(Stream 流式通信)和双向(Bidirectional 双向通信)。
  • 易于扩展,高渗透性,包括但不限于追踪/监控等支持,也应该被各级设备所识别,网关设施等可以识别数据包,对服务网格部署友好,降低用户理解难度。
  • 完全兼容 grpc,客户端/服务器可以与原生 grpc 客户端通信。
  • 可以复用现有 grpc 生态系统中的组件,以满足云原生场景下的跨语言、跨环境和跨平台互通需求。

对于当前使用其他协议的 Dubbo 用户,框架提供了与现有序列化方法兼容的迁移能力。在不影响现有在线业务的情况下,迁移协议的成本几乎为零。

需要新增连接 Grpc 服务的 Dubbo 用户可以直接使用 Triple 协议完成连接,无需单独引入 grpc 客户端完成。不仅可以保留现有的 Dubbo 易用性,还可以降低程序的复杂性和开发维护成本,无需额外适配和开发即可接入现有生态。

对于需要网关接入的 Dubbo 用户,Triple 协议提供了一种更原生的方式,使网关开发或使用开源 grpc 网关组件更加容易。网关可以选择不解析有效负载,这大大提高了性能。在使用 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 协议

为了保证兼容性,我们首先将部分提供者升级到 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,完成调用,输出如下: result

同时使用 dubbo 和 triple 协议

为了升级在线服务,不可能同时完成提供者和消费者的升级。需要逐步操作以确保业务稳定性。在第二步中,提供者提供了一种双协议方式,同时支持 dubbo + tri 客户端。

结构如图所示: trust

根据推荐的升级步骤,提供者已经支持 tri 协议,所以 dubbo3 的消费者可以直接使用 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 协议

当所有消费者都升级到支持 Triple 协议的版本时,提供者可以切换为仅使用 Triple 协议启动

结构如图所示: trust

[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 的 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)

流是 Dubbo3 提供的一种新的调用类型。建议在以下场景中使用流

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

流分为以下三种类型

  • 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
public interface IWrapperGreeter {

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

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

Stream 方法的方法输入参数和返回值有严格的约定。为了防止写入错误导致的问题,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 接口。默认情况下,将同时生成多个不同的 stubs

-blockingStub

  • futureStub -reactorStub -…

stub 帮助我们以统一的方式屏蔽了不同调用方法的细节。但是,目前 Dubbo3 只支持传统的定义和调用接口的方式。

在不久的将来,Triple 也将实现各种常用的 Stub,让用户写一个 proto 文件,就可以通过 comipler 方便地在任何场景下使用,敬请期待。


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