该文章内容发布已经超过一年,请注意检查文章中内容是否过时。

Dubbo Java 2.7.5 功能解析

2.7.5 发布,及其功能解析

近日,备受瞩目的 Dubbo 2.7.5 版本正式发布,在 2.7.5 版本中,Dubbo 引入了很多新的特性、对现有的很多功能做了增强、同时在性能上也有了非常大的提升,这个版本无论对 Dubbo 社区亦或是开发者来说,都将是一个里程碑式的版本。

  • 应用粒度服务注册【beta】
  • HTTP/2 (gRPC) 协议支持
  • Protobuf 支持
  • 性能优化,调用链路性能提升 30%
  • 支持 TLS 安全传输链路
  • 优化的消费端线程模型
  • 新增更适应多集群部署场景的负载均衡策略
  • 全新的应用开发 API (兼容老版本应用)【beta】
  • 其他一些功能增强与 bugfix

首先,从服务发现上,新版本突破以往基于接口粒度的模型,引入了全新的基于应用粒度的服务发现机制 - 服务自省,虽然该机制当前仍处于 beta 阶段,但对于 Dubbo 向整个微服务云原生体系靠齐,都打下了非常好的基础;得益于紧凑的协议设计和代码实现上的优化,Dubbo 一直以来都具有较好的性能表现,在 2.7.5 版本中,性能上有了进一步的提升,根据来自官方维护团队的压测,新版本在调用链路上性能提升达到 30%;云原生微服务时代,多语言需求变得越来越普遍,协议的通用性和穿透性对于构建打通前后端的整套微服务体系也变得非常关键,Dubbo 通过实现 gRPC 协议实现了对 HTTP/2 协议的支持,同时增加了与 Protobuf 的结合。

1. 应用粒度服务注册【beta】

从 Java 实现版本的角度来说,Dubbo 是一个面向接口代理的服务开发框架,服务定义、服务发布以及服务引用都是基于接口,服务治理层面包括服务发现、各种规则定义也都是基于接口定义的,基于接口可以说是 Dubbo 的一大优势,比如向开发者屏蔽了远程调用细节、治理粒度更精细等。但基于接口的服务定义同时也存在一些问题,如服务,与业界通用的微服务体系等。

servicediscovery-old.png

针对以上问题,2.7.5 版本引入了一种新的服务定义/治理机制:服务自省,简单来说这是一种基于应用粒度的服务治理方案。一个实例只向注册中心注册一条记录,彻底解决服务推送性能瓶颈,同时由于这样的模型与主流微服务体系如 SpringCloud、K8S 等天然是对等的,因此为 Dubbo 解决和此类异构体系间的互联互通清除了障碍。有兴趣进一步了解 Dubbo 服务自省机制如何解决异构微服务体系互联互通问题的,可具体参考我们之前的文章解析《Dubbo 如何成为联通异构微服务体系的最佳服务开发框架》。

以下是服务自省机制的基本工作原理图。

servicediscovery-new.png

要了解更多关于服务自省工作原理的细节,请参与官方文档及后续文章。

服务自省与当前已有的机制之间可以说是互补的关系,Dubbo 框架会继续保持接口粒度的服务治理的优势,实现接口和应用两个粒度互为补充的局面,兼顾性能、灵活性和通用性,力争使 Dubbo 成为微服务开发的最佳框架。

2. HTTP/2 (gRPC) 协议支持

Dubbo RPC 协议是构建在 TCP 之上,这有很多优势也有一些缺点,缺点比如通用性、协议穿透性不强,对多语言实现不够友好等。HTTP/2 由于其标准 HTTP 协议的属性,无疑将具有更好的通用性,现在或将来在各层网络设备上肯定都会得到很好的支持,gRPC 之所以选在 HTTP/2 作为传输层载体很大程度上也是因为这个因素。当前 gRPC 在云原生、Mesh 等体系下的认可度和采用度逐步提升,俨然有成为 RPC 协议传输标准的趋势,Dubbo 和 gRPC 在协议层面是对等竞争的,但是在框架实现上却各有侧重,Dubbo 无疑有更丰富的服务开发和治理体验 。

Dubbo 支持 gRPC 协议带来的直观好处有:

  • 正式支持基于 HTTP/2 的远程通信,在协议通用性和穿透性上进一步提升。
  • 支持跨进程的 Stream 流式通信,支持 Reactive 风格的 RPC 编程。
  • 解决了 gRPC 框架难以直接用于微服务开发的问题,将其纳入 Dubbo 的服务治理体系。
  • 为联通组织内部已有的 gRPC 或多语言体系提供支持。

2.7.5 版本开始,gRPC (HTTP/2) 成为 Dubbo 协议体系中的一等公民,对于有需求的开发者完全可以在 Dubbo 开发的微服务体系中启用 gRPC 协议,而不必束缚在 Dubbo 协议自身上,关于这点我们在《Dubbo 如何成为联通异构微服务体系的最佳服务开发框架》一文中也有类似的观点表述。

关于 Dubbo 中如何开发 grpc (HTTP/2) 服务的细节,请参考文章《Dubbo 在跨语言与协议穿透性等方面的探索》,关于如何开启 TLS 和使用 Reactive RPC 编程,请参见示例。另外,Dubbo 的 go 版本目前同样也提供了对 gRPC 协议对等的支持,具体请关注 dubbogo 社区的发版计划。

3. Protobuf 支持

支持 Protobuf 更多的是从解决 Dubbo 跨语言易用性的角度考虑的。

跨语言的服务开发涉及到多个方面,从服务定义、RPC 协议到序列化协议都要做到语言中立,同时还针对每种语言有对应的 SDK 实现。虽然得益于社区的贡献,现在 Dubbo 在多语言 SDK 实现上逐步有了起色,已经提供了包括 Java, Go, PHP, C#, Python, NodeJs, C 等版本的客户端或全量实现版本,但在以上提到的跨语言友好性方面,以上三点还是有很多可改进之处。

协议上 2.7.5 版本支持了 gRPC,而关于服务定义与序列化,Protobuf 则提供了很好的解决方案。

  • 服务定义。当前 Dubbo 的服务定义和具体的编程语言绑定,没有提供一种语言中立的服务描述格式,比如 Java 就是定义 Interface 接口,到了其他语言又得重新以另外的格式定义一遍。因此 Dubbo 通过支持 Protobuf 实现了语言中立的服务定义。
  • 序列化。Dubbo 当前支持的序列化包括 Json、Hessian2、Kryo、FST、Java 等,而这其中支持跨语言的只有 Json、Hessian2,通用的 Json 有固有的性能问题,而 Hessian2 无论在效率还是多语言 SDK 方面都有所欠缺。为此,Dubbo 通过支持 Protobuf 序列化来提供更高效、易用的跨语言序列化方案。

日后,不论我们使用什么语言版本来开发 Dubbo 服务,都可以直接使用 IDL 定义如下服务,具体请参见示例

syntax = "proto3";

option java_multiple_files = true;
option java_package = "org.apache.dubbo.demo";
option java_outer_classname = "DemoServiceProto";
option objc_class_prefix = "DEMOSRV";

package demoservice;

// The demo service definition.
service DemoService {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

4. 性能优化

4.1 调用链路优化

2.7.5 版本对整个调用链路做了全面的优化,根据压测结果显示,总体 QPS 性能提升将近 30%,同时也减少了调用过程中的内存分配开销。其中一个值得提及的设计点是 2.7.5 引入了 Servicerepository 的概念,在服务注册阶段提前生成 ServiceDescriptor 和 MethodDescriptor,以减少 RPC 调用阶段计算 Service 元信息带来的资源消耗。

4.2 消费端线程池模型优化

对 2.7.5 版本之前的 Dubbo 应用,尤其是一些消费端应用,当面临需要消费大量服务且并发数比较大的大流量场景时(典型如网关类场景),经常会出现消费端线程数分配过多的问题,具体问题讨论可参见以下 issue :

https://github.com/apache/dubbo/issues/2013

改进后的消费端线程池模型,通过复用业务端被阻塞的线程,很好的解决了这个问题。

老的线程池模型

消费端线程池.png

我们重点关注 Consumer 部分:

  1. 业务线程发出请求,拿到一个 Future 实例。
  2. 业务线程紧接着调用 future.get 阻塞等待业务结果返回。
  3. 当业务数据返回后,交由独立的 Consumer 端线程池进行反序列化等处理,并调用 future.set 将反序列化后的业务结果置回。
  4. 业务线程拿到结果直接返回

2.7.5 版本引入的线程池模型

消费端线程池新.png

  1. 业务线程发出请求,拿到一个 Future 实例。
  2. 在调用 future.get() 之前,先调用 ThreadlessExecutor.wait(),wait 会使业务线程在一个阻塞队列上等待,直到队列中被加入元素。
  3. 当业务数据返回后,生成一个 Runnable Task 并放入 ThreadlessExecutor 队列
  4. 业务线程将 Task 取出并在本线程中执行:反序列化业务数据并 set 到 Future。
  5. 业务线程拿到结果直接返回

这样,相比于老的线程池模型,由业务线程自己负责监测并解析返回结果,免去了额外的消费端线程池开销。

关于性能优化,在接下来的版本中将会持续推进,主要从以下两个方面入手:

  1. RPC 调用链路。目前能看到的点包括:进一步减少执行链路的内存分配、在保证协议兼容性的前提下提高协议传输效率、提高 Filter、Router 等计算效率。
  2. 服务治理链路。进一步减少地址推送、服务治理规则推送等造成的内存、cpu 资源消耗。

5. TLS 安全传输链路

2.7.5 版本在传输链路的安全性上做了很多工作,对于内置的 Dubbo Netty Server 和新引入的 gRPC 协议都提供了基于 TLS 的安全链路传输机制。

TLS 的配置都有统一的入口,如下所示:

Provider 端

SslConfig sslConfig = new SslConfig();
sslConfig.setServerKeyCertChainPath("path to cert");
sslConfig.setServerPrivateKeyPath(args[1]);
// 如果开启双向 cert 认证
if (mutualTls) {
  sslConfig.setServerTrustCertCollectionPath(args[2]);
}

ProtocolConfig protocolConfig = new ProtocolConfig("dubbo/grpc");
protocolConfig.setSslEnabled(true);

Consumer 端

if (!mutualTls) {}
    sslConfig.setClientTrustCertCollectionPath(args[0]);
} else {
    sslConfig.setClientTrustCertCollectionPath(args[0]);
    sslConfig.setClientKeyCertChainPath(args[1]);
    sslConfig.setClientPrivateKeyPath(args[2]);
}

为尽可能保证应用启动的灵活性,TLS Cert 的指定还能通过 -D 参数或环境变量等方式来在启动阶段根据部署环境动态指定,具体请参见 Dubbo 配置读取规则与 TLS 示例

Dubbo 配置读取规则:/zh-cn/docs/user/configuration/configuration-load-process.html

TLS 示例:https://github.com/apache/dubbo-samples/tree/master/dubbo-samples-ssl

如果要使用的是 gRPC 协议,在开启 TLS 时会使用到协议协商机制,因此必须使用支持 ALPN 机制的 Provider,推荐使用的是 netty-tcnative,具体可参见 gRPC Java 社区的总结: https://github.com/grpc/grpc-java/blob/master/SECURITY.md

在服务调用的安全性上,Dubbo 在后续的版本中会持续投入,其中服务发现/调用的鉴权机制预计在接下来的版本中就会和大家见面。

6. Bootstrap API【beta】

在上面讲《服务自省》时,我们提到了 Dubbo 面向接口的设计,面向接口编程、面向接口做服务发现和服务治理。在引入应用粒度服务发现的同时,2.7.5 版本对编程入口也做了优化,在兼容老版本 API 的同时,新增了新的面向应用的编程接口 - DubboBootstrap。

以面向 Dubbo API 编程为例,以前我们要这么写:

ServiceConfig<GreetingsService> service1 = new ServiceConfig<>();
service1.setApplication(new ApplicationConfig("first-dubbo-provider"));
service1.setRegistry(new RegistryConfig("zookeeper://" + zookeeperHost + ":2181"));
service1.export();

ServiceConfig<GreetingsService> service2 = new ServiceConfig<>();
service2.setApplication(new ApplicationConfig("first-dubbo-provider"));
service2.setRegistry(new RegistryConfig("zookeeper://" + zookeeperHost + ":2181"));
service2.export();

......

ApplicationConfig、RegistryConfig、ProtocolConfig 等全局性的配置要在每个服务上去配置;并且从 Dubbo 框架的角度,由于缺少一个统一的 Server 入口,一些实例级别的配置如 ShutdownHook、ApplicationListener、应用级服务治理组件等都缺少一个加载驱动点。

在引入 DubboBootstrap 后,新的编程模型变得更简单,并且也为解决了缺少实例级启动入口的问题

ProtocolConfig protocolConfig = new ProtocolConfig("grpc");
protocolConfig.setSslEnabled(true);

SslConfig sslConfig = new SslConfig();
sslConfig.setXxxCert(...);

DubboBootstrap bootstrap = DubboBootstrap.getInstance();
bootstrap.application(new ApplicationConfig("ssl-provider"))
  .registry(new RegistryConfig("zookeeper://127.0.0.1:2181"))
  .protocol(protocolConfig)
  .ssl(sslConfig);

ServiceConfig<GreetingsService> service1 = new ServiceConfig<>();
ServiceConfig<GreetingsService> service2 = new ServiceConfig<>();

bootstrap.service(service1).service(service2);
bootstrap.start();

7. 多注册中心集群负载均衡

对于多注册中心订阅的场景,选址时的多了一层注册中心集群间的负载均衡:

cluster-lb.png

在 Cluster Invoker 这一级,我们支持的选址策略有(2.7.5+ 版本,具体使用请参见文档):

  • 指定优先级

    <!-- 来自 preferred=“true” 注册中心的地址将被优先选择,只有该中心无可用地址时才 Fallback 到其他注册中心 -->
    <dubbo:registry address="zookeeper://${zookeeper.address1}" preferred="true" />
    
  • 同 zone 优先

    <!-- 选址时会和流量中的 zone key 做匹配,流量会优先派发到相同 zone 的地址 -->
    <dubbo:registry address="zookeeper://${zookeeper.address1}" zone="beijing" />
    
  • 权重轮询

    <!-- 来自北京和上海集群的地址,将以 10:1 的比例来分配流量 -->
    <dubbo:registry id="beijing" address="zookeeper://${zookeeper.address1}" weight=”100“ />
    <dubbo:registry id="shanghai" address="zookeeper://${zookeeper.address2}" weight=”10“ />
    
  • 默认,stick to 任意可用

关于多注册中心订阅模型,Dubbo 同时也提供了 Multi-Registry 合并的解决思路,欢迎参与到以下 PR 的讨论中: https://github.com/apache/dubbo/issues/5399

8. 其他功能增强

  • 新增地址变更事件通知接口,方便业务侧感知地址变化

  • 新增外围配置加载入口,方便开发者在启动阶段定制服务启动参数

  • config 模块重构

  • parameters 扩展配置增强

  • 其他一些 Bugfix

从 Dubbo 框架自身的角度来说,2.7.5 版本也做了很多的重构与优化(比如说 config 模块的重构),这些改动对于使用者来说并无感知的,但是从优化整个 Dubbo 代码内部结构的角度来说,这些改动对后续的功能开发与新机制的引入是一个很好的铺垫。

9. 总结与展望

在后续的版本中,Dubbo 会持续快速的优化与迭代,主要从以下几个方面发力:

  • 继续探索服务自省成为 Dubbo 主推的服务治理模型。
  • 对于企业用户关心的微服务解决方案场景,会持续推进框架的演进,包括当前正在开发的配置、服务鉴权机制、熔断等功能。后续还会尝试联合社区推动周边配套设施如网关、治理平台 Admin 等的建设,非常期待社区能踊跃参与到此部分的建设中。
  • 性能优化上。主要从两个方面着手,一是调用链路的持续优化,同时继续探索新的更通用的 RPC 协议;另一方面是在服务治理推送机制上的优化,以进一步提高 Dubbo 在大规模服务地址推送场景下的表现。
  • 云原生方向。接下来的版本将重点探索,1. 如何更好的支持 Dubbo 在 Kubernetes 上的部署和服务治理;2. 对于混合部署的场景,如传统 VM 和 K8S 体系混合部署、SDK Dubbo 与 Mesh 混合部署的场景,如何提供更好的支持以实现混部场景的长期共存或迁移。
最后修改 February 22, 2023: Merge refactor website (#2293) (4517e8c1c9)