Dubbo 最基础的 RPC 应用(使用 ZooKeeper)

看国内的一些项目时 Dubbo 这个词经常闪现,一直也不以为然,未作搜索,当然也不知道它是做什么用的。直到最近阅读关于大型网站架构相关的书中反复提到 Dubbo 后,觉得不能再对它视而不见。Google 了一下,它是在阿里巴巴创建贡献给了 Apache 的开源项目,在阿里巴巴的大型应用中久经考验过的。Dubbo 是什么呢?借用官方 Dubbo 介绍

Apache Dubbo 是一款 RPC 服务开发框架,用于解决微服务架构下的服务治理与通信问题,官方提供了 Java、Golang 等多语言 SDK 实现。使用 Dubbo 开发的微服务原生具备相互之间的远程地址发现与通信能力, 利用 Dubbo 提供的丰富服务治理特性,可以实现诸如服务发现、负载均衡、流量调度等服务治理诉求。Dubbo 被设计为高度可扩展,用户可以方便的实现流量拦截、选址的各种定制逻辑。

Dubbo 是国内企业贡献的,所以官方有原生的中文文档,它某些时候与 Spring Cloud 齐名,又有些像 AWS 的 ECS Service Discovery, Service Connect 加上 ELB 的功能。

Dubbo 可以与 Spring/Spring Boot 中很好的工作,不过本文只想完全脱离 Spring 框架,实际操作一个最基本的 Dubbo RPC 应用来理解 Dubbo 的架构。熟悉之后,Spring 的介入不外乎就是把本来代码实现的内容移入到配置文件或 Java 注解,不会是难事。

这是一个 Dubbo 服务的基本架构

  1. 服务注册器是一个 Zookeeper
  2. Provider 启动时向 Registry 注册自己的服务,下线后 Zookeeper 会清除该服务注册
  3. Consumer 从 Registry  获得可用的服务列表,然后直接向对应的 Provider 发起远程调用

可以是多个不同的 Provider 提供不同的服务,或者相同的 Provider 启动多个副本提供相同的服务(集群),Consumer 获得服务后按一定的规则(比如轮询,失败重试下一个服务等规则)调用某个 Provider 实例上的服务。

首先大体上介绍本试验的内容

  1. 启动一个 ZooKeeper 服务器
  2. 向 ZooKeeper 注册 user-service 的两个副本 [192.168.86.49:50053, 192.168.86.49:50054]
  3. 再向 ZooKeeper 注册 order-service 一份 [192.168.86.49:50052]
  4. user-service 客户端从 ZooKeeper 找到有两个副本 [192.168.86.49:50053, 192.168.86.49:50054] 提供服务,多个调用会 user-service 时,请求会均衡到这两个副本上。如是其中的一个副本(如 192.168.86.49:50053) 下线,则只会调用剩下的 192.168.86.49:50054。假如调用某个服务失败(出现异常)也能重试另一个副本(192.168.86.49:50054)
  5. order-service 客户端从 ZooKeeper 找到 192.168.86.49:50052 服务进行调用
  6. 我们将用 ZooKeeper 客户端 zkCli.sh 观察服务注册和下线后在 ZooKeeper 中发生了什么

下面试验开始

启动一个 ZooKeeper 作为服务注册器

使用 Docker

$ docker run -p 2181:2181 zookeeper:3.9.2

从容器中暴露出 2181 端口号

Maven 项目引入所需的依赖

不管是 Java 的服务端还是客户端都需要引入 Dubbo 依赖,假设是用 Maven 管理项目。

当前 Dubbo 的正式版本是 3.2.15, 经过一番测试后发现它实际支持的 Java 版本是 1.8。用高版本的 JDK 如 11, 17, 21 的话,服务端能启动成功并完成向 ZooKeeper 的注册,但时而也会出现如下错误

java.lang.reflect.InaccessibleObjectException: Unable to make field private java.lang.String java.lang.StackTraceElement.fileName accessible: module java.base does not "opens java.lang" to unnamed module

但客户端用 Dubbo 3.2.15 + 高于 JDK 1.8 的版本调用服务总是失败的

所以如果用 Dubbo 3.2.15 的版本,请用 JDK 1.8, 在 pom.xml 中需要的依赖是

(官方的例子用的组合是 Dubbo 3.2.0-beta.4 + JDK 17,读者可自行尝试)

Dubbo 会引入其他许多的运行时依赖,如 spring-beans,  spring-conext, spring-aop, spring-core, spring-jcl, spring-expression, spring 组件的版本是 5.3.37; io.netty 等一系列通信用的组件, alibaba:hessian 以及其他

没有 curator-framework 可能出现错误  java.lang.NoClassDefFoundError: org/apache/curator/framework/api/ACLProvider 或 java.lang.NoClassDefFoundError: org/apache/curator/framework/CuratorFrameworkFactory

没有 curator-x-discovery 可能会出现错误 java.lang.ClassNotFoundException: org.apache.curator.x.discovery.ServiceDiscoveryBuilder

但从 Dubbo 3.3.0 开始可完美支持 Java 21, 当前尚处于 beta.5 的版本

如果在高于 Java 1.8 的 JDK(如 Java 21) 中使用 Dubbo, 就可以引入如下依赖

而且我们发现,使用 Dubbo 3.3.0-beta.5 让整个项目更干净了,因为它不引用任何 Spring 相关的依赖

定义并实现服务

UserService

其中定义两个实现方法

OrderService

然后是它们的模拟实现类

计划在不同的端口上启动两个 user-service 服务,以此来测试服务的集群。用 servicePort 区分出来,在调用 login 接口时将从控制台输出分辨出谁被调用

实现服务启动程序

有了前面的接口方法和实现类,还要用主程序把它们启动后并注册到 ZooKeeper 上供客户端查询,若是用 Spring/SpringBoot 的话,这些任务可通过配置文件和框架来实现

UserServiceStarter

由于将在本机上不同的端口上启动 user-serivce 服务, 所以可通过参数指定端口号。如果上面未指定 registry,则服务不会注册到 ZooKeeper 上,但服务仍能启动,启动在 localhost:<servicePort> 上,客户端也能跳过 ZooKeeper 直接调用 tri://localhost:<servicePort>

如果不指定 protocol 的端口号,默认为 50051; 端口号置为 -1 , 协议为 tri 的话,则会从 50051 开始自增选择端口号,如 protocol(new ProtocolConfig("tri", -1), 这样就可以一个服务在本机上启动多次,端口号依次为 50051, 50052, 50053,....。不同协议的起始端口号是不一样的。

Dubbo 支持的协议有 dubbo, rest, http, hessian, redis, thrift, grpc, memcached, rmi, webservice. 默认协议是 dubbo, 因此也可以完全省略 protocol() 方法调用。希望在 Dubbo 客户端中从 ZooKeeper 中发现并调用 redis, memcached 等服务,则需要预先把它们注册到 ZooKeeper 中。

OrderServiceStarter

启动服务并注册到 ZooKeeper 上

因为使用了 Maven 来管理项目依赖,编译后用 java 命令来启动服务必须为 classpath 指定众多的 jar 包(除非用 Maven 的  Assembly 或 SpringBoot 插件做成 fat.jar ),所以用 Maven 的 exec 插件来启动主程序方便些

在 50053 端口号上启动 user-service

mvn compile exec:java -Dexec.mainClass=blog.yanbin.dubbo.providers.UserServiceStarter -Dexec.args=50053

服务启动后在 Zookeeper 中增加了几个键,用 zkCli 查看下

zkCli -server localhost:2181

连上后执行 zookeeper 命令 

/dubbo/blog.yanbin.dubbo.providers.UserService/providers 的内容要进行 URL Decode

tri://192.168.86.49:50053/blog.yanbin.dubbo.providers.UserService?application=user-service&deprecated=false&dubbo=2.0.2&dynamic=true&environment=product&generic=false&interface=blog.yanbin.dubbo.providers.UserService&logger=jcl&methods=login,userInfo&prefer.serialization=fastjson2,hessian2&release=3.2.15&service-name-mapping=true&side=provider&timestamp=1724384955118

再 50054 端口上启动另一份 user-service 服务

mvn compile exec:java -Dexec.mainClass=blog.yanbin.dubbo.providers.UserServiceStarter -Dexec.args=50054

查看 ZooKeeper 上的值

user-service 服务有两个,分别是 192.168.86.49:50053 和 192.168.86.49.50053

看 URL Decoded /dubbo/blog.yanbin.dubbo.providers.UserService/providers

tri://192.168.86.49:50053/blog.yanbin.dubbo.providers.UserService?application=user-service&deprecated=false&dubbo=2.0.2&dynamic=true&environment=product&generic=false&interface=blog.yanbin.dubbo.providers.UserService&logger=jcl&methods=login,userInfo&prefer.serialization=fastjson2,hessian2&release=3.2.15&service-name-mapping=true&side=provider&timestamp=1724384955118,tri://192.168.86.49:50054/blog.yanbin.dubbo.providers.UserService?application=user-service&deprecated=false&dubbo=2.0.2&dynamic=true&environment=product&generic=false&interface=blog.yanbin.dubbo.providers.UserService&logger=jcl&methods=login,userInfo&prefer.serialization=fastjson2,hessian2&release=3.2.15&service-name-mapping=true&side=provider&timestamp=1724385567323

启动 OrderService 服务

mvn compile exec:java -Dexec.mainClass=blog.yanbin.dubbo.providers.OrderServiceStarter

查看 ZooKeeper

URL decoded /dubbo/blog.yanbin.dubbo.providers.OrderService/providers

tri://192.168.86.49:50052/blog.yanbin.dubbo.providers.OrderService?application=order-service&deprecated=false&dubbo=2.0.2&dynamic=true&environment=product&generic=false&interface=blog.yanbin.dubbo.providers.OrderService&logger=jcl&methods=addOrder&prefer.serialization=fastjson2,hessian2&release=3.2.15&service-name-mapping=true&side=provider&timestamp=1724385905085

现在 Zookeeper 上注册有 user-service 和 order-service 服务

客户端调用测试

UserServiceClient

调用

$ mvn compile exec:java -Dexec.mainClass=blog.yanbin.dubbo.clients.UserServiceClient
login result: true
login result: false
userInfo: Detailed information of admin from 50053
userInfo: Detailed information of admin from 50054
userInfo: Detailed information of admin from 50053
userInfo: Detailed information of admin from 50053
userInfo: Detailed information of admin from 50054

上面的 UserServiceClient 如果不用 addRegistries(...) 代码而启用 .url("tri://localhost:50053") 则不从 Zookeeper 中查询服务直接调用 localhost:50053 上的服务

如果关掉 50053 端口上对应的服务进程(Mac 下用 cmd + z),进程处在 suspended 状态,在 Zookeeper 上的  50053 也马上消失掉。如果用 kill -9 <pid> 来强杀进服务进程,在 ZooKeeper 上仍然有一会儿能看到 /services/user-service/192.168.86.49:50053 这个服务,但过个大约一分钟也就消失了。

测试

$ mvn clean compile exec:java -Dexec.mainClass=blog.yanbin.dubbo.clients.UserServiceClient
login result: true
login result: false
userInfo: Detailed information of admin from 50054
userInfo: Detailed information of admin from 50054
userInfo: Detailed information of admin from 50054
userInfo: Detailed information of admin from 50054
userInfo: Detailed information of admin from 50054

本例中未模拟调用失败重试下一次服务的情况,如调用 192.168.86.49:50053 失败,再重试 192.168.86.49:50054 上的 user-service。Dubbo 有更多的调用策略设置,实战时可进一步深究。

测试 OrderService

运行

$mvn clean compile exec:java -Dexec.mainClass=blog.yanbin.dubbo.clients.OrderServiceClient
new orderId: 525400174

Dubbo 3.2.15 与 高于 Java 1.8 的兼容问题

前面提到过 Dubbo 3.2.15 只能用 JDK 1.8, 实测时在运行客户端时,使用  Dubbo 3.2.15 时,如果用的 JDK 版本是 11, 17, 或 21 的话,会出都类似如下各种情形的错误

java.lang.reflect.InaccessibleObjectException: Unable to make field private java.lang.String java.lang.StackTraceElement.fileName accessible: module java.base does not "opens java.lang" to unnamed module
......
java.lang.reflect.InaccessibleObjectException: Unable to make field final int java.math.BigInteger.signum accessible
....
org.apache.dubbo.rpc.RpcException: java.util.concurrent.ExecutionException: org.apache.dubbo.rpc.StatusRpcException: CANCELLED : Canceled by remote peer, errorCode=8
......
java.lang.NoClassDefFoundError: io/netty/util/concurrent/DefaultPromise$1

配置的 JDK_JAVA_OPTIONS 环境变量也不管用

export JDK_JAVA_OPTIONS="--add-opens java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED"

最后一步步从 Java 21 降级到 Java 1.8 才跑通

链接

本文主要参考是官方的 Develop microservice applications based on Dubbo API,其中用的版本组合是 Dubbo 3.2.0-beta.4 + JDK 17。而本人使用 Dubbo 3.2.15 + JDK 1.8+ 都不行,或许是其他依赖的问题,还是因为通信协议是 dubbo 的缘故?以上试验中遇到的各种问题都是通过 Google 找到答案,感谢 Google。

以上只是一个简单的 RPC 远程调用的实例,实际不过是 Dubbo 功能的一小部分,它能做的远比这个丰富。一次粗浅的体验,就看现实中是否有机会用到才会去进一步深入。

本文链接 https://yanbin.blog/dubbo-basic-rpc-call-with-zookeeper/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments