介绍
目前很多大厂中,Dubbo 框架是 Java 语言中用作 RPC 的不二选择,有着阿里巴巴大厂的技术背书,可靠性和稳定性有着保证。目前很多 java 系的大厂依旧用着 Dubbo 2.6.x 或者 Dubbox 等比较成熟的应用,满足日常业务快速开发迭代的需要。不过目前 Dubbo 面临如下的一些问题,亟待开发工程师去解决,不断地更新迭代目前 Dubbo 的功能。
说明,此章虽然只是说明,但是很好地说明了如下的几个问题:
- Dubbo 3 为什么出现
- Dubbo 3 这些新特性产品的原因
云原生(Cloud Native)目前正在各个云厂商中不断地进行深耕,作为 Apache 顶级项目的 Dubbo 估计很想变成 CNCF(Cloud Native Computing Foundation)的项目成员,估计很多开发的小伙伴还是听过 CNCF 的大名的,CNCF 是一个面向当下和未来的云原生的一个计算基金会,口号是坚持和整合开源技术来编排容器作为微服务架构的一部分 ,它目前名下有很多知名的项目,我可以一一举例给你看看。
你是一名 DBA 工程师:
如果你是一名优秀的 DBA 工程师,你肯定听过如下的一些项目,如下图所示,有目前新生的使用 Golang 和 Rust 开发的 TiDB、TiKV,也有老牌的关系型数据库 MySQL 和 Oracle,这些存储类的都开始有意识地去适应云原生项目。
你是一个运维开发工程师:
如果你是一名运维开发工程师,你肯定也听过 HELM、Bitnami、Jenkins、Gitlab 等工具,在日常的 DevOps 的过程中,这些工具是必不可少的。
你是一个中间件工程师:
如果你是一个中间件开发工程师,你肯定知道 Etcd、ZooKeeper、gRPC、Tars 等项目。
你是一个业务开发工程师,那么你更加了不起,虽然和上述的几位专业的工程师相比,在某一个单独的领域了解地不如这些同学的深,但是你想作为一个合格的业务开发,这些技术你都务必能够清楚的了解他们的基本原理,使用场景。例如你必须学会如下的技术:
- 数据库相的:MySQL、Redis、TiDB、MongoDB、Elasticsearch、HBase、InfluxDB 等
- 中间件分布式协调组件:Nacos、ZooKeeper、Etcd、Eureka
- RPC 技术方向:Tars、Dubbo、gRPC
- DevOps 的常用工具:GitLab、Docker、Jenkins、Kubernetes、Promethues、Zipkin
这样想想作为一个业务开发真的不容易,如果后续有机会,可以和大家一起交流交流上述的技术集合,帮助大家很好地去面试和日常工作。
综上而言,Dubbo 3 的一些新特性是为了云原生而生的,确保符合目前时代的发展。
温故而知新
第零章,我们很啰嗦地介绍了 Dubbo 3 产品的原因,Dubbo 3 的演进的原因,演进式架构就是如此,针对目前面临的业务问题,通过思考给出解决方案,而不是闭门造车,臆想开发这个是不现实的,理论介绍完成,我们开始动手实操。想要了解 Dubbo 3 新的特性,我们可以通过与老版本进行对比的方式来进行,这样可以帮助我们可以更好地了解相关的特性,本章对 Dubbo 的新手用户比较友好,可以按照如下比较详细的步骤来一步步地进行自己搭建上手。
我们选用 Dubbo 2.7.3 版本与 Spring Boot 2.x 进行整合,因为 Dubbo 2.7.5 版本开始就有一些新的特性 Beta 版本的放出:
如上图所示,我们可以看出 2.7.5 版本开发,开始支持应用级别的注册,所以我们先选用之前的 Dubbo 版本进行 Spring Boot 2.x 的整合。
工欲善其事必先利其器:ZooKeeper 和 Nacos 的搭建
后续我们使用分布式协调组件 ZooKeeper 和 Nacos 作为注册中心,Nacos 作为配置中心和元数据中心。因此我们使用 Docker 来进行改组件的快速搭建。
ZooKeeper 的搭建
ZooKeeper 我们仅搭建一个单机的测试环境即可,我们仅适用 docker run -d -p 2181:2181 --name zookeeper zookeeper:latest 一行命令,即可很快地在本地启动一个单机的 ZooKeeper,足够我们做实验使用。
然后我们在使用一个 ZooKeeper 的数据可视化工具进行查看方便我们后续对比 Dubbo 2 和 Dubbo 3 注册时候,ZooKeeper 节点的区别。我们打开链接 zkui 的链接 https://github.com/DeemOpen/zkui,如下所示:
将其代码 clone 到本地后,按照官方的操作建议,如下图所示,可以快速搭建:
本地使用 mvn clean install 后,本地进行如下的操作,即可快速启动 zkui 的客户端。
然后我们再次修改 config.cfg 中链接 ZooKeeper 的地址和登录的用户名密码:
最后我们使用浏览器输入 localhost:9090 即可打开页面,输入 admin/manager 即可进入主页面:
主页面如下所示:
到此为止,一个轻巧简单的 ZooKeeper 单机部署和可视化工具都搭建好了,后续进行使用。
Nacos 的搭建
Nacos 是阿里巴巴开源的一个用于动态服务发现,配置管理和服务管理的平台,官方网址:https://nacos.io/zh-cn/
我们进入它的官方文档,如下图所示
clone 好项目后,按照官方文档,我们使用 docker-compose 命令来启动项目,不过在项目启动之前,我们要修改一下 standalone-derby.yaml 文档,如下图所示,我们把 ${NACOS_VERSION} 替换成 2.0.2 版本,最后我们再使用 docker-compose -f example/standalone-mysql-8.yaml up -d 进行启动。
启动如下图所示:
最后使用浏览器访问:http://localhost:8848/nacos/#/login 即可访问,默认用户名密码是 nacos/nacos 即可登录访问。
到此为止,我们的准备工作就完成了,接下来我们就可以正式开始搭建 2.7.3 版本的 Dubbo 了,我们为了方便,使用 Maven 多模块的方式进行搭建,真实企业级开发的根本原理也与这个相似,最后,我们也会使用 dubbo-admin 这个官方的可视化工具进行服务治理。
Maven 多模块 Spring Boot 整合 Dubbo 2.7.3
本小节比较的步骤比较详细,记录了搭建的完整过程,如果有经验的小伙伴直接跳过,刚刚开始接触 Dubbo 的可以与我一起动手体验一下整个过程,相信物也有所值。
打开 Intellij IDEA:
选择 Maven,然后不要勾选 Create from archetype:
输入项目名后,根据不同版本的 IDEA,即可跳入到项目的主页面,我们把整个 src 目录都删除掉。
然后右击项目名,创建子模块 mumaren-dubbo2-basic-api 这个模块主要放置 consumer 和 product 的接口依赖,因为我们知道 dubbo 消费方是拿着接口的凭证来调用服务消费方代码的,然后我们依次构建三个子模块,mumaren-dubbo2-basic-provider 和 mumaren-dubbo2-basic-consumer。
最后项目的骨架结构图如下所示:
接下来,我们引入 Dubbo 的 Maven 依赖,因为我们这个是多模块搭建,所以我们把 Maven 的依赖就放在跟 pom 文件里面,这样可以方便项目里面的子模块可以引用到这个模块里面的 jar 包依赖,因为我们是使用 Spring Boot 直接整合 dubbo,所以我们引入的依赖是 2.3.5 版本的 dubbo-spring-boot-starter 和 Spring Boot 目前比较新的依赖 spring-boot-starter 的 2.7.3 版本依赖,当然我们这次默认使用 ZooKeeper 作为注册中心,所以我们也要引入 ZooKeeper 的 Java 客户端 curator-framework 的依赖,整体依赖 pom 如下图所示:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.mumaren</groupId>
<artifactId>mumaren-dubbo2-basic-sample</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>mumaren-dubbo2-basic-api</module>
<module>mumaren-dubbo2-basic-provider</module>
<module>mumaren-dubbo2-basic-consumer</module>
</modules>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.mumaren</groupId>
<artifactId>mumaren-dubbo2-basic-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.mumaren</groupId>
<artifactId>mumaren-dubbo2-basic-provider</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.mumaren</groupId>
<artifactId>mumaren-dubbo2-basic-consumer</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.3.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.6.3</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.3</version>
</dependency>
</dependencies>
</project>
引入好我们的 jar 包依赖,接下来我们就开始按照 Dubbo 的规范,定义 Dubbo 服务提供者的接口,我们这边为了可以更加直观地看一些问题,我们这边定义两个接口,代码写在 mumaren-dubbo2-basic-api 这个模块下,然后服务提供者和服务消费者都分别引用这个模块,这样我们就可以面向接口调用了。我们定义如下两个接口,分别是 DemoService 和 HelloService。
package org.mumaren;
public interface DemoService {
String demoSample(String name);
int saveUser(int id,String username);
}
HelloService 代码如下:
package org.mumaren;
public interface HelloService {
String hello(String name);
}
可以看到我们定义了两个很简单的接口,我们现在再看下整体项目结构图:
接下来,我们就要写具体服务提供者的代码实现了,我们把具体的代码实现放在 mumaren-dubbo2-basic-provider 里面,写完结构如下图所示:
首先,我们现在 mumaren-dubbo2-basic-provider 子模块的 pom.xml 按照上文说的引入 mumaren-dubbo2-basic-api 的依赖,整体依赖如下图所示:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>mumaren-dubbo2-basic-sample</artifactId>
<groupId>org.mumaren</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>mumaren-dubbo2-basic-provider</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.mumaren</groupId>
<artifactId>mumaren-dubbo2-basic-api</artifactId>
</dependency>
</dependencies>
</project>
接下来,我们就快速地实现一下接口实现:
package org.mumaren.service.impl;
import org.apache.dubbo.config.annotation.Service;
import org.mumaren.DemoService;
@Service(version = "1.0.0")
public class DemoServiceImpl implements DemoService {
@Override
public String demoSample(String name) {
return "demo " + name;
}
@Override
public int saveUser(int id, String username) {
System.out.println("save user ");
return id;
}
}
HelloServiceImpl.java 如下所示,实现也非常简单:
package org.mumaren.service.impl;
import org.apache.dubbo.config.annotation.Service;
import org.mumaren.HelloService;
@Service(version = "1.0.0")
public class HelloServiceImpl implements HelloService {
@Override
public String hello(String name) {
return "hello " + name;
}
}
接口逻辑实现都非常简单,不过我们这次是用注解进行实现的,所以这次我们使用的注解是 Apache 的 dubbo 包下的 @Service 的依赖,而不是 spring 包下的依赖,这块大家要特别注意一下,当然版本不一样,注解的名称也有可能不一样,Dubbo 有的版本注解名称叫做 @DubboService,应该是作者想要与 Spring 的 @Service 注解进行区分,其次需要注意的就是注解里面的属性写明 version=1.0.0 这样也比较规范,尽量不要使用缺省值,最后我们再使用比较经典的 Spring Boot 的 main 函数启动类来快速结束我们服务提供者代码模块的书写。
Dubbo2ProviderSampleApplication.java 如下所示,主要使用两个注解,完整代码如下所示:
package org.mumaren;
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableDubbo(scanBasePackages = "org.mumaren.service.impl")
public class Dubbo2ProviderSampleApplication {
public static void main(String[] args){
SpringApplication.run(Dubbo2ProviderSampleApplication.class, args);
}
}
代码模块到此就结束了,接下来,我们在使用 application.properties 来快速配置一下:
dubbo.application.name=mumaren-dubbo2-provider
dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.protocol.port=20880
dubbo.protocol.name=dubbo
整体目录结果如下所示,到此为止,我们很快地也搭建好了 Dubbo 2 版本的服务提供者端的代码了。
接下来,我们也快速地实现 Dubbo 2 服务消费者端的代码逻辑,为了方便测试,我们引入 spring-boot-starter-web 的依赖,这样我们可以通过 HTTP 接口调用的方式来调用,首先我们先看下我们整体的代码结构图:
mumaren-dubbo2-basic-consumer 模块的 pom 如下图所示,也比较简单,就是引入 api 和 boot-web 两个依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>mumaren-dubbo2-basic-sample</artifactId>
<groupId>org.mumaren</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>mumaren-dubbo2-basic-consumer</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.mumaren</groupId>
<artifactId>mumaren-dubbo2-basic-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.5.RELEASE</version>
</dependency>
</dependencies>
</project>
其中引入 Dubbo 服务的是 HelloConsumerServiceImpl 的我们日常的服务依赖,完整代码如下所示,我们两个方法都进行了引用,方便我们测试,这边我们需要注意的就是这边的 Service 注解已经变成了 Spring 包下的经典注解,不在是刚才我们使用的 Apache 包下的 Dubbo 的 @Service 注解了,这边是我们需要注意的,其次我们在 HelloService 和 DemoService 上也加了一个 @Reference 注解,这个注解是 dubbo 包下的依赖,这边我们的 version 一定要与刚才在 provider 中 version 写成一样的值,这样才可以正确调用到对应版本的服务,当然有时候会有不同版本的出现,所以我们只需要根据不同的版本业务需求,服务消费方在 reference 注解中填写不同的 version 即可,当然这边一定要写好 version 否者会调用不通。
package org.mumaren.service.impl;
import org.apache.dubbo.config.annotation.Reference;
import org.mumaren.DemoService;
import org.mumaren.HelloService;
import org.mumaren.service.HelloConsumerService;
import org.springframework.stereotype.Service;
@Service
public class HelloConsumerServiceImpl implements HelloConsumerService {
@Reference(version = "1.0.0")
private HelloService helloService;
@Reference(version = "1.0.0")
private DemoService demoService;
@Override
public String hello(String name) {
return helloService.hello(name);
}
@Override
public String demo(String name) {
return demoService.demoSample(name);
}
}
其他不重要的代码,我们这边快速罗列一下。
HelloController.java
package org.mumaren.controller;
import org.mumaren.service.HelloConsumerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(path = "/api")
public class HelloController {
@Autowired
private HelloConsumerService helloConsumerService;
@GetMapping(value = "/hello")
public String hello(String name){
return helloConsumerService.hello(name);
}
@GetMapping(value = "/demo")
public String demo(String name){
return helloConsumerService.demo(name);
}
}
Dubbo2ConsumerApplication.java
package org.mumaren;
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableDubbo
public class Dubbo2ConsumerApplication {
public static void main(String[] args){
SpringApplication.run(Dubbo2ConsumerApplication.class, args);
}
}
最后我们再简单地配置一下 application.properties:
dubbo.application.name=mumaren-dubbo2-consumer
dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.protocol.port=20881
dubbo.protocol.name=dubbo
到此为止,所有的代码实现都结束了,接下来我们进行简单的项目启动测试。
代码测试
到目前为止我们完成了 Dubbo 2 的代码逻辑书写,并且我们也用 Docker 启动了 ZooKeeper,接下来我们先后启动 Dubbo 服务提供者和服务消费者进行方法调用,看整体流程能够打通。
首先我们先启动服务提供者 mumaren-dubbo2-provider:
项目启动没有任何报错,接下来,我们先用 zkui 看下 ZooKeeper 的节点信息:
整体 ZooKeeper 的结构如下图所示:
我们再启动 mumaren-dubbo2-consumer,我们可以看到项目能够正常启动,这个时候我们再看一下 ZooKeeper 的结构。
ZooKeeper 的结构图如下图所示:
具体的消费者节点信息也注册上来了:
最后我们在浏览器中输入 http://localhost:8080/api/hello?name=mumaren 进行访问,结果和我们预期的一样,符合我们的预期。
Dubbo 3 应用级注册初体验
Maven 多模块整合 Spring Boot 和 Dubbo 3.0.2.1
与 Dubbo 2 的多模块使用 IDEA 比较相似,我们这边就不再一步步截图进行介绍了,整体项目的结构图如下图所示。这边我们使用 ZooKeeper 的 Java 客户端工具 Curator 5.x 版本和本文章发布前 Dubbo 最新的 release 版本 3.0.2.1 版本进行整合。
接下来我们快速地罗列一下代码,我们先看下项目的根 pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>mumaren-dubbo3-basic-sample</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>mumaren-dubbo3-basic-api</module>
<module>mumaren-dubbo3-basic-provider</module>
<module>mumaren-dubbo3-basic-consumer</module>
</modules>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>mumaren-dubbo3-basic-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>mumaren-dubbo3-basic-provider</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>mumaren-dubbo3-basic-consumer</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.5.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.dubbo/dubbo-spring-boot-starter -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>3.0.2.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.curator/curator-framework -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-x-discovery</artifactId>
<version>5.2.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.dubbo/dubbo-registry-nacos -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-nacos</artifactId>
<version>3.0.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-metadata-report-nacos</artifactId>
<version>3.0.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-configcenter-nacos</artifactId>
<version>3.0.2.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
</dependencies>
</project>
通过上面的 xml 文件,我们引入了 Dubbo 3.0.2.1 目前最近的版本,并且我们使用 Curator 5.x 进行整合,并且为了后续的方便,我们一起引入了 Dubbo 的依赖于 Nacos 的注册中心、元配置中心、配置中心的配置。
接下来,我们再简单地介绍一下 Dubbo 3 的 provider 模块的代码,还是简单列一下 provider 模块的 pom.xml 代码,因为根 pom.xml 已经引入了 Dubbo 的依赖了,所以这边的依赖引用就比较简单了,只需要引入最基本的 Dubbo 需要实现的接口依赖即可。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>mumaren-dubbo3-basic-sample</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>mumaren-dubbo3-basic-provider</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>mumaren-dubbo3-basic-api</artifactId>
</dependency>
</dependencies>
</project>
我们接下来再列一下一个最简单的实现 DemoServiceImpl.java 和 HelloServiceImpl.java,这边我们需要注意的是,Dubbo 3 的版本的注解是 @DubboService 和我们之前提及的 dubbo 包下的 @Service 进行了区别,其他没有什么需要注意的地方,其中 main 函数没有任何改变,这边我们节省篇幅,我们就不具体罗列代码了,后面我们给出完整的各个项目的完整代码。
package org.mumaren.provider.service;
import org.apache.dubbo.config.annotation.DubboService;
import org.mumaren.api.DemoService;
@DubboService(version = "1.0.0")
public class DemoServiceImpl implements DemoService {
@Override
public String demoSample(String name) {
return "demo " + name;
}
@Override
public int saveUser(int id, String username) {
System.out.println("save user ");
return id;
}
}
package org.mumaren.provider.service;
import org.apache.dubbo.config.annotation.DubboService;
import org.mumaren.api.HelloService;
@DubboService(version = "1.0.0")
public class HelloServiceImpl implements HelloService {
@Override
public String hello(String name) {
System.out.println("hello name");
return "hello " + name;
}
}
接下来,我们就要注意的就是 application.properties 新的 dubbo 的配置文件了,我们可以看到最基本的 Dubbo 依赖多了两个新的地址,一个叫做元数据中心的地址,还有一个是配置中心的地址。这个我们后面具体介绍,我们现在就先简单地罗列一下配置项,到此为止 Dubbo 3 服务提供者端的代码已经配置完成了,接下来我们使用 2mins 我们在快速地介绍一下服务消费者端代码的配置。
dubbo.application.name=mumaren-dubbo3-provider
dubbo.registry.address=zookeeper://localhost:2181
dubbo.metadata-report.address=zookeeper://localhost:2181
dubbo.config-center.address=zookeeper://localhost:2181
dubbo.protocol.name=dubbo
Dubbo 3 服务消费这段的配置,因为我们需要使用 HTTP 接口进行调用触发,我们按照 Dubbo 2 之前的惯例,我们引入 boot-web 的依赖,方便我们进行测试 pom.xml。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>mumaren-dubbo3-basic-sample</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>mumaren-dubbo3-basic-consumer</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>mumaren-dubbo3-basic-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.5.3</version>
</dependency>
</dependencies>
</project>
其他和 Dubbo 2 相关的 controller 和 service 代码比较相近的代码,大家可以在完整地代码进行查看,我们还是贴一下具体的实现服务实现,这边和 Dubbo 2 不同的地方就是我们开始引用 DubboReference 而不是之前的 Reference 注解,其他没有任何区别,从这点可以看出从原生的 dubbo 协议的基础上,Dubbo 2 和 Dubbo 3 对于使用者来说区别并不是很大,降低了用户的使用成本。
package org.mumaren.consumer.service.impl;
import org.apache.dubbo.config.annotation.DubboReference;
import org.mumaren.api.HelloService;
import org.mumaren.consumer.service.HelloConsumerService;
import org.springframework.stereotype.Service;
@Service
public class HelloConsumerServiceImpl implements HelloConsumerService {
@DubboReference(version = "1.0.0")
private HelloService helloService;
@Override
public String hello(String name) {
return helloService.hello(name);
}
}
接下来需要我们重点关注的就是服务消费端的配置了,我们先简单地看下 application.properties 和提供者的 Dubbo 相关的配置相差不多。
dubbo.application.name=mumaren-dubbo3-consumer
dubbo.registry.address=zookeeper://localhost:2181
dubbo.protocol.name=dubbo
dubbo.metadata-report.address=zookeeper://localhost:2181
到目前为止,我们也很快地搭建好 Dubbo 3 的代码了,我们很快地进行测试一下。
Dubbo 3 的代码测试
首先我们按照与 Dubbo 2 的启动顺序一样的方式,先启动服务提供者,如下图所示:
启动完成之后,我们再使用 zkui 可视化工具看下,注册中心和之前的区别,第一个区别如下图所示,根目录有了两个,在 Dubbo 2 的之前 /dubbo 的基础上,多了一个 /services 的目录,接下来,我们再看一下 /dubbo 子节点和之前的区别。
如下图所示,我们进入 Dubbo 的子目录,我们可以看到与之前相比又了三个目录文件分别如下图所示 /config、mapping 和 metadata 如下三个配置文件,其他没有任何区别,这边我们先简单记录一下,后面再来介绍一下。
接下来,我们看下与 Dubbo 2 相比之下的,Dubbo 3 新增了一个 ZooKeeper 节点,根节点叫做 services 我们可以看下里面的具体信息,如下图所示,我们可以看到在 services 下只有一个节点,节点的名称就叫做我们服务提供者的模块名称 mumaren-Dubbo 3-provider 这个看起来就是和 Dubbo 3 官方宣传的应用级注册的就比较相似的,这个注册信息的改变,应该就是 Dubbo 官方说的“应用级注册”,这边我们先简单地记录一下,后续我们再进行具体的分析。
接下来,我们再启动一下我们的服务消费者,我们看能不能正常启动,并且调用成功,MumarenDubbo3ConsumerApplication 的 main 函数进行服务消费者的启动,启动过程如下图所示:
启动成功后,我们在浏览器中进行接口调用测试的触发。我们输入 http://localhost:8080/api/hello2?name=mucoding 可以看到服务能够调用成功。
我们在详细地看下 ZooKeeper 的节点变化,如下图我们可以看到在老的 Dubbo 的 org.mumaren.api.HelloService 下多了一个 consumers 节点:
到此为止,Dubbo 3 的基于原始的 Dubbo 协议的调用代码,我们已经写完了,不过你肯定又很多疑问,你也会觉得我在水这篇文章,接下来,我们继续进行分析,慢慢地进行 Dubbo 3 应用级注册的初步探索,如果后续大家对这块的知识比较感兴趣的话,后续可以对 Dubbo 3 的源码进行针对性的分析,方便大家对 Dubbo 3 应用级的注册原理有一个进一步的了解和学习。
注册中心、配置中心、元数据中心
我们可以从 Dubbo 3 的官网上来看,我们看到为了降低注册中心的压力,Dubbo 官方把原有的注册中心的功能进行了拆分,把一些注册,配置的信息分成 3 个部分进行存储,其中注册中心提供的功能和能力没有改变,还是利用 ZooKeeper 等分布式协调工具来协调服务提供者和服务消费者之间的地址注册和发现。
并且新增了配置中心的能力,配置中心主要提供的功能,就是提供一个全局配置,例如 Dubbo 的一些基础配置,路由信息的动态改变,做一些流量的切换等等,这块都可以进行开发和控制。
元数据中心,其实我个人感觉就是注册中心的能力扩展,帮助注册中心分担一部分的 watch 通知和存储的压力,我们可以做一个简单地解释,举例来说,我们在刚才搭建的 Dubbo 3 的例子上来说,可以看到我们新增了一个节点信息就是 /services 的节点信息,如果是应用级注册,那么在 /services 下就会注册的信息就是服务提供者的名字,例如我们叫做 mumaren-dubbo3-provider 这个节点,然后这个节点下存储的信息如下:
{
"name": "mumaren-dubbo3-provider",
"id": "172.27.176.1:20880",
"address": "172.27.176.1",
"port": 20880,
"sslPort": null,
"payload": {
"@class": "org.apache.dubbo.registry.zookeeper.ZookeeperInstance",
"id": null,
"name": "mumaren-dubbo3-provider",
"metadata": {
"anyhost": "true",
"application": "mumaren-dubbo3-provider",
"deprecated": "false",
"dubbo": "2.0.2",
"dubbo.endpoints": "[{\"port\":20880,\"protocol\":\"dubbo\"}]",
"dubbo.metadata-service.url-params": "{\"version\":\"1.0.0\",\"dubbo\":\"2.0.2\",\"release\":\"3.0.2.1\",\"port\":\"20880\",\"protocol\":\"dubbo\"}",
"dubbo.metadata.revision": "97f3e5ac1c7a755659b4ae02e7aa1f89",
"dubbo.metadata.storage-type": "local",
"dynamic": "true",
"generic": "false",
"interface": "org.mumaren.api.DemoService",
"metadata-type": "remote",
"methods": "demoSample,saveUser",
"pid": "17648",
"release": "3.0.2.1",
"revision": "1.0.0",
"service-name-mapping": "true",
"side": "provider",
"timestamp": "1630725344517",
"version": "1.0.0"
}
},
"registrationTimeUTC": 1630725344752,
"serviceType": "DYNAMIC",
"uriSpec": null
}
这个里面存储的信息很全,能够查询到服务提供者的一些基本信息,但是我们要从服务消费者的角度思考一下如下的问题,例如我们 mumaren-dubbo3-consumer 现在消费的服务是 org.mumaren.api.DemoService 版本是 1.0.0 如果以前在 Dubbo 2 的场景下,我们可以订阅 /dubbo/org.mumaren.api.DemoService/provider/ 这个目录下的节点信息,如下图所示,这个节点下如果有多个提供者的会有多个子节点。里面的信息使用了 URL 编码,这些子节点,里面存储了服务提供者的 IP 地址,我们只需要把这些信息读取出来,根据条件过滤,例如 version 版本,远程方法是否存在等条件进行过滤,然后根据 loadbalance 策略就可以进行调用了。
URLDecode 结果如下,可以很清楚的看出服务提供者的所有数据信息:
dubbo://172.27.176.1:20880/org.mumaren.api.DemoService?anyhost=true&application=mumaren-dubbo3-provider&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.mumaren.api.DemoService&metadata-type=remote&methods=demoSample,saveUser&pid=17648&release=3.0.2.1&revision=1.0.0&service-name-mapping=true&side=provider×tamp=1630725344517&version=1.0.0
上述就是 Dubbo 2 基于接口作为注册中心的调用流程,但是现在是 Dubbo 3,你可以看出注册的信息不再是接口了,对于服务消费方而言,我只知道我需要调用的接口名字和参数,但是现在注册中心并没有这样的节点可以给我们订阅,让我们 watch 这些节点的变化,没有这样的节点,我们甚至都不知道订阅什么节点,所以接下来我们要想一些办法,让服务消费者能够通过接口名找到对应的应用的名称,谜底就在谜面上,我们刚才的截图中,可以看到 ZooKeeper 上多了一些节点信息,我们可以仔细观察一下,就可以看到一些答案的踪迹,如下图所示:
我们可以看到上图所示,在 /dubbo/mapping/ 的目录下,有一些数据存储,节点映射信息,可以很容易地发现,这边存储了每一个接口类名映射出对应的应用名称,那么我们会不会有这样的想法,就是服务消费者在订阅服务的时候,首先查询注册中心 /dubbo/mapping 下节点的信息,通过方法名来获取应用名,然后再根据应用名称去订阅 /services/instances 的信息,并且监听变化,这样就可以获取到服务提供方的 IP,看起来这样的想法从逻辑上是能够说通的 ,这个我们后面通过实验再进行验证,最后结合官方的一些文档,这样我们就可以对 Dubbo 3 的应用级注册有一个初步的了解和掌握。
回到本小节的话题描述,我们现在知道了 Dubbo 3 把先前的注册中心的功能,分拆成三个“中心”,每一个"中心"承接的使命都不相同,从上面简单地分析来说,注册中心是基础,元数据中心是注册中心的信息补充,配置中心是用户和系统人工动态干预生成配置的存储中心,最后我们再给出官方给出每一个中心的定义和存储的内容,方便大家进一步理解。
- 注册中心。协调 Consumer 与 Provider 之间的地址注册与发现
- 配置中心:存储 Dubbo 启动阶段的全局配置,保证配置的跨环境共享与全局一致性负责服务治理规则(路由规则、动态配置等)的存储与推送。
- 元数据中心:接收 Provider 上报的服务接口元数据,为 Admin 等控制台提供运维能力(如服务测试、接口文档等)作为服务发现机制的补充,提供额外的接口/方法级别配置信息的同步能力,相当于注册中心的额外扩展
场景白话描述
上图是官方给的系统架构图,我们再进行一下具体的启动过程白话描述,假设背景:服务消费者 A 调用服务提供者 B 的 xxx.package.service.OrderService 的服务,启动流程是这样的:
服务提供者 B 开始启动,指挥者开始下达命令:
“我准备启动啦,开始工作啦,配置中心达兄弟麻烦问一下我们 Dubbo 启动的端口和 Dubbo 的协议是什么,注册中心的大兄弟的电话号码是多少啊?”
配置中心:“大兄弟,你好啊,你的服务端口是 20881,协议用的 Dubbo 的经典协议 Dubbo,注册中心打兄弟的电话号码是‘zookeeper://10.21.45.12:9876’,其他都是默认缺省配置!”
服务提供者 B:“收到,大兄弟,我再自我扫描一下,我这边有多少加了 @DubboService 注解的接口……ok,我这边有 5 个接口要提供……”
接下来给注册中心大兄弟打电话。
服务提供者 B 拿自己的手机,打开小纸条上面用钢笔歪歪扭扭地记录着刚才配置中心给的号码“zookeeper://10.21.45.12:9876”,心里想着“zookeeper”是移动的,“nacos”是联通的,“apollo”是电信的,加上“90”前缀就可以打通移动的了。
拨通号码后,服务提供者说:“hello,兄弟,我这边是一个商家,想提供外卖服务~”
注册中心:“ok,大兄弟,麻烦填写或者注册一下你的商户信息、IP 商户号、外卖品类、外卖具体的图片、净含量等等信息。”
服务提供者 B 心里比较懵逼,我怎么成了外卖单位了,不过还是说“大兄弟,我是提供服务的,我这边可以提供 5 个服务。”
注册中心:“我知道啦,我就是举一个例子,方便你听懂,辛苦填一下手机上的注册单子。”
服务提供者 B 开始填单,单子的第一页,提供了如下的信息:
{
"name": "mumaren-dubbo3-provider",
"id": "172.27.176.1:20880",
"address": "172.27.176.1",
"port": 20880,
"sslPort": null,
"payload": {
"@class": "org.apache.dubbo.registry.zookeeper.ZookeeperInstance",
"id": null,
"name": "mumaren-dubbo3-provider",
"metadata": {
"anyhost": "true",
"application": "mumaren-dubbo3-provider",
"deprecated": "false",
"dubbo": "2.0.2",
"dubbo.endpoints": "[{\"port\":20880,\"protocol\":\"dubbo\"}]",
"dubbo.metadata-service.url-params": "{\"version\":\"1.0.0\",\"dubbo\":\"2.0.2\",\"release\":\"3.0.2.1\",\"port\":\"20880\",\"protocol\":\"dubbo\"}",
"dubbo.metadata.revision": "97f3e5ac1c7a755659b4ae02e7aa1f89",
"dubbo.metadata.storage-type": "local",
"dynamic": "true",
"generic": "false",
"interface": "org.mumaren.api.DemoService",
"metadata-type": "remote",
"methods": "demoSample,saveUser",
"pid": "4464",
"release": "3.0.2.1",
"revision": "1.0.0",
"service-name-mapping": "true",
"side": "provider",
"timestamp": "1630741280920",
"version": "1.0.0"
}
},
"registrationTimeUTC": 1630741281173,
"serviceType": "DYNAMIC",
"uriSpec": null
}
注册中心说:“兄弟,你的信息填写完成,我检查过了 ok,不过你还要和工商局(元数据中心)那边备案一下。”
服务提供者 B 心想真麻烦,之前不是这样的啊,以前一个电话就搞定了啊。
注册中心说:“兄弟,你不要抱怨啦,打一个电话备案一下,很快的”
服务提供者说好的,然后给元数据中心打了一个电话,果然很快,就把 5 个服务名字报了一下,元数据中心就说 ok 了。
=============我是分割线,服务提供者 B 会话结束====================
服务消费者 A 这个时候也是刚刚和配置中心打完电话,询问了一些和服务提供者 B 差不多的信息,也给注册中心打了一个电话,兄弟我想点外卖,外卖名字叫做 xxx.package.service.OrderService,注册中心说,“您好,根据国家相关法律规定,辛苦你去元数据中心严重一下,你点的外卖是否靠谱,后续点新的外卖都需要这样查询一下,我这边不提供查询服务。”
服务消费者 A 一听,觉得不错,立马就打了元数据中心的电话:“您好,xxx.package.service.OrderService 这个外卖的提供者是谁,是谁背书的啊,靠不靠谱啊?”
元数据中心查了一下数据:“回复说,您订阅的 xxx.package.service.OrderService 的商家号叫做服务提供者 B,是一个国家老字号,提供的服务都很稳定监控。”
服务消费 A 听到回复,心里爽歪歪,给注册中心回拨电话了,兄弟给我“服务提供者 B 的”号码,我准备直接去给她电话点外卖了。
注册中心说:“目前服务提供者 B 这个商户,目前有 50 家分店,目前营业的有 37 家,我稍后把这 37 家的电话和地址发送给您。”
服务提供者 A 收到了注册中心的短信通知,拿到了 37 家的电话号码,这个时候,他又给配置中心打电话说:“麻烦问下,我女朋友有什么忌口的吗?或者有什么偏好吗?”
配置中心说:“根据你女朋友的注册信息表明,你女朋友喜欢门牌号为偶数的门店吃饭,这样吉利,并且希望离家近一点(指定路由规则和希望就近路由)。”
服务提供者 A 在这 37 家根据配置中心返回女朋友的一些定制化偏好设置,选了 5 家,作为接下来 5 次吃饭的地方的候选地,开心地点起了外卖。
=======================故事到此结束======================
读到此处,我想大家应该知道这 3 个中心的作用了吧,接下来我们通过迁移每一个中心的地址到 Nacos,我们观察一下 ZooKeeper 和 Nacos 数据信息的变化,这样可以方便大家更好的理解。
使用 Nacos 作为注册中心
接下来,我们修改一下 Dubbo 3 的 application.properties 的配置,将注册中心的地址改成 Nacos 的地址,这样可以方便我们能够更好的理解其中的注册中心存储的原理,首先我们先将配置改成如下,与之前相比,我们就是将 dubbo.registry.address 地址修改成 nacos。
dubbo.application.name=mumaren-dubbo3-provider
#修改成配置中心为 nacos
dubbo.registry.address=nacos://localhost:8848?username=nacos&password=nacos
dubbo.metadata-report.address=zookeeper://localhost:2181
dubbo.config-center.address=zookeeper://localhost:2181
dubbo.protocol.name=dubbo
接下来我们重新启动服务提供者的 main 函数,我们看下 Nacos 和 ZooKeeper 的服务提供者注册数据存储的情况。
首先我们启动之后,我们可以先打开浏览器访问 http://localhost:8848/nacos/ 地址,选择服务管理的服务列表的功能,我们可以看到如下的截图,与我们预期一致的就是服务名确实出现了应用级的服务名称 mumaren-dubbo3-provider 但是也出现了接口级别的注册信息,这个和我们的预期是不符合的,看来默认情况下,Dubbo 3 版本会出现两种级别服务都注册的情况,接下来,我们再看下 ZooKeeper 的配置,这个时候 ZooKeeper 的角色已经变成了元数据中心和配置中心了。
我们再打开 ZooKeeper,再看下 ZooKeeper 里面的数据节点情况:
如下图所示,目前现在只存在了一个根节点,之前注册应用级别的 /services 的节点信息不复存在,符合我们的预期。接下来我们再看下 /dubbo 这个子节点里面的信息,如下图所示,与之前相比,接口级别的信息也不存在了,只存在了预期的配置节点信息,映射节点信息,和元数据节点信息,ZooKeeper 的数据节点也符合我们的预期。
到此为止,将注册中心从 ZooKeeper 改成 Nacos,除了 Nacos 中的服务列表信息多了两个接口级别的信息外,其他的变化和我们上一章原理分析过程中的完全一致,如下所示,那为什么会出现这两个接口级别的信息呢?这个问题我们先保留,我们接着看。
使用 Nacos 作为配置中心
接下来我们把 application.properties 中的配置中心的地址再改成 Nacos,继续论证我们之前的原理猜想,修改后的配置如下:
dubbo.application.name=mumaren-dubbo3-provider
#修改成配置中心为 nacos
dubbo.registry.address=nacos://localhost:8848?username=nacos&password=nacos
dubbo.metadata-report.address=zookeeper://localhost:2181
dubbo.config-center.address=nacos://localhost:8848?username=nacos&password=nacos
dubbo.protocol.name=dubbo
重新启动 main 函数,我们再次观察一下 ZooKeeper 和 Nacos 的数据存储情况,首先我们看下 ZooKeeper 发生的变化,如下图所示 /config 的信息发生了改变,Nacos 经过作者论证并没有发生改变,不过启动没有报错,后续我们可以再 Nacos 配置中心新增配置,看其是否生效。
使用 Nacos 作为元数据中心
最后我们再讲 Nacos 作为元数据中心,再看下 ZooKeeper 和 Nacos 的存储数据节点的变化情况,修改配置如下:
dubbo.application.name=mumaren-dubbo3-provider
#修改成配置中心为 nacos
dubbo.registry.address=nacos://localhost:8848?username=nacos&password=nacos
dubbo.metadata-report.address=nacos://localhost:8848?username=nacos&password=nacos
dubbo.config-center.address=nacos://localhost:8848?username=nacos&password=nacos
dubbo.protocol.name=dubbo
老规矩重启启动 main 函数,我们再次观察一下数据变化情况,首先先观察一下 ZooKeeper 的信息变化情况,这个时候 ZooKeeper 已经没有任何节点信息了,符合预期。
最后再看下 Nacos 里面的信息,可以看到目前为止所有的信息已经全部迁移到 Nacos 里面来了,里面每一项的作用,后续原理简单分析地时候,也能够帮助我们快速了解其中的原理。
混合模式
接下来,我们再继续讲一下,我们刚才遇到但是没有解决的问题,我们刚才看到 Nacos 作为注册中心的时候,除了注册应用级服务外,同时也注册了接口级别的服务,这个和我们的预期是不相符合的。
现在我们看下这个里面的原因,我们在 Dubbo 官方文档中,找下其中的原因,地址如下:
https://dubbo.apache.org/zh/docs/migration/migration-service-discovery/
如下图所示,官方文档说明为了版本兼容性的问题,服务提供者会注册两种类型的注册数据,一个是应用级别的,一个是接口级别的,官方也提供了配置参数,方便我们灵活地进行切换配置,我们修改 application.properties 的配置文件,新增如下的配置 dubbo.application.service-discovery.migration 和 dubbo.application.register-mode 的参数,我们再看一下 main 函数重新启动之后的效果。
dubbo.application.name=mumaren-dubbo3-provider
dubbo.registry.address=nacos://localhost:8848?username=nacos&password=nacos
dubbo.metadata-report.address=nacos://localhost:8848?username=nacos&password=nacos
dubbo.config-center.address=nacos://localhost:8848?username=nacos&password=nacos
dubbo.protocol.name=dubbo
dubbo.application.service-discovery.migration=FORCE_APPLICATION
dubbo.application.register-mode=instance
这个时候,我们再看一下 Nacos 配置中心的效果,完全符合我们的预期,我们再将服务消费者的配置中心、注册中心、元数据中心全部修改成 Nacos,启动后,可以看:
Dubbo 2 和 Dubbo 3 应用级注册原理说明
到此为止,Dubbo 2 和 Dubbo 3 的应用级注册我们的示例都很清楚地用代码展示过了,上面的一些原理的分析也介绍了一下,从实际业务生产上来说,这个应用级注册确实为了面向未来云原生的,核心的诉求就是降低注册中心存储和通知的压力,防止注册中心例如 ZooKeeper 因为存储的接口级信息过多,耗尽了 ZooKeeper 的 id,并且 ZooKeeper 通知 watch 的压力很大,主要很多通知是无效地通知。
举例来说,假设一个商品系统提供 20 个接口给订单系统进行调用,那么当这个商品系统模块进行上线或者迭代升级的时候,接口级注册会有 20 次的通知,通知商品系统的一个 IP 正在上线或者下线,会有 20 次的通知到订单系统,如果你写过 RPC 的一些简单 Demo 的话,你会在内存中使用 Map 这样的数据结构,key 放接口名,value 可能是一个 list 里面存储这个接口服务提供者的 IP,然后再根据负载均衡的策略进行 IP 选择调用。如果是接口级别,你 RPC 调用了多少接口,那么你这个 key 的 size 就是多少,这样也会加大了你服务消费的内存成本,当然这个成本是可以忽略不计的,但是维护成本却大大增加,一般而言,在实际业务开发场景中,一个模块 RPC 调用的下游模块系统不会很多,一般也就 3~7 个,但是这 3~7 个的模块提供的接口可能会多大百个有余。设想一下,如果是应用级的注册,那么你的维护成本和通知成本也会大大降低。
第二个原因,就是对于大厂而言,例如每天亿级的大厂应用,例如支付宝、今日头条等 App,每秒 QPS 均值达到几十万的应用,按照现在还算比较流行的微服务云端部署的逻辑来说,可能某一个模块会部署 1000~5000 左右的实例,这样对于实际生产中的分布式协调工具或者注册中心,压力还是很大的,并且有时候会有一些活动,QPS 猛增的时候,使用 K8s 进行扩容的时候,对于注册中心的压力也是一个很大的挑战,扩容的同时,节点频繁地新增,通知的消息也在一瞬间大增,这对 ZooKeeper 无疑是一个很大地挑战,所以,应用级注册,对于大厂来说,还是很有必要进行升级和使用的。
Dubbo 3 使用 Protobuf 进行序列化传输
我们花了很多的篇幅对 Dubbo 3 的应用级注册进行了说明,本章开始一个新的篇章,其实 Dubbo 对于序列化工具 Protobuf 并不是 Dubbo 3 才给地特性,在 Dubbo 2.7 版本,Dubbo 官方就给出了具体的实现,在给具体示例代码之前,我们还是先简单说明一下为什么 Dubbo 开始官方支持 Protobuf。
我个人觉得原因如下:
- 传输效率,Protobuf 的性能确实很好,序列化后的字节数据相比之前缩小 3 倍,传递相同价值的信息,却使用更少地字节数据,能够降低网络传输的压力。
- 更快,序列化的效率很高。
- 跨语言跨平台,用过 Protobuf 的人都知道,我们只需要编写 .proto 的文件,然后使用 proto 的工具,就可以生成各个语言的代码。
- 最后一个,其实就是为了 Dubbo 3 的 triple 协议做好铺垫准备,说白了 triple 协议只是 Dubbo 在 gRPC 之上做的一些小优化,所以被官方称之为第三代 Dubbo 协议,而我们都知道 gRPC 和 Protobuf 是分不开的,就像吃奥利奥需要在牛奶里面泡一泡是同一个到底,两者搭配在一起,性能才能够达到极致。
再说说缺点,如果非要说缺点的话,可以这么说,世界上任何事情都是有两面性的,Protobuf 之所以序列化的效率这么高压缩效率也这么高,也是有其他的成本代码的,代价就是我们都需要编写 proto,不管是传输的实体类,还是定义 Protobuf 的 RPC 接口,我们都需要学习 Protobuf 的 IDL 规范,去定义然后去生成对应语言的文本,如果可以打比方的话,就是我们必须先生成好模具,被序列化的字节数组被接收到,然后被灌注到这个模具中,就可以很快地进行反序列化操作,得到我们想要的信息。这个模具非常精致,不做处理的话,Java 生成的模具类可能能够达到几百 K,这也是 Protobuf 的小小的缺点吧。
废话不多说,我们还是简单地按照 Dubbo 官网,使用 Spring 和 Protobuf 的示例,快速地搭建演示一下,方便我们对其有一个简单的入门了解。
我们先贴一下我们项目的基本骨架图,和之前的基本一致,没啥区别,如下图所示,也是 Maven 的多模块。
第一步,我们也是按照规范,先写 Protobuf 的 IDL 来定义一些实体模型类,我们和定义普通的 Dubbo 接口一样,把接口定义在 API 中,如下图所示,我们新建一个文件夹,用来存放 proto 文件:
然后可以按照 Protobuf 胡官方文档编写 proto 文件,我们可以直接使用 dubbo-samples 里面给的示例,进行如下 DemeService.proto 的编写,我们可以看出默认情况下,IDEA 是不能识别 proto 文件的,我们可以根据弹出来的插件安装提示,进行插件安装。
syntax = "proto3";
option java_multiple_files = true;
option java_package = "org.mumaren.api.service";
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;
}
会自动弹出我们需要可选的 Protobuf 的识别特性的插件,点击 OK:
完成之后,我们的 IDEA 已经可以识别出 Protobuf 的语法了,如下图所示,如果你写的 Protobuf 有语法问题,会给出提示:
好了,言归正传,接下来我们就要用 proto 给我们提供的 Maven 插件,生成我们对应的 Java 代码了,我们在 mumaren-dubbo-protobuf-api 中的 Maven 的 pom.xml 文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>mumaren-dubbo-protobuf</artifactId>
<groupId>org.mumaren</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>org.mumaren</groupId>
<artifactId>mumaren-dubbo-protobuf-api</artifactId>
<properties>
<target.level>1.8</target.level>
<source.level>1.8</source.level>
<dubbo.compiler.version>0.0.2</dubbo.compiler.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-serialization-protobuf</artifactId>
<version>2.7.13</version>
</dependency>
</dependencies>
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.6.1</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>${source.level}</source>
<target>${target.level}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.5.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:3.7.1:exe:${os.detected.classifier}</protocArtifact>
<outputDirectory>build/generated/source/proto/main/java</outputDirectory>
<clearOutputDirectory>false</clearOutputDirectory>
<protocPlugins>
<protocPlugin>
<id>dubbo</id>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-compiler</artifactId>
<version>${dubbo.compiler.version}</version>
<mainClass>org.apache.dubbo.gen.dubbo.Dubbo3Generator</mainClass>
</protocPlugin>
</protocPlugins>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>build/generated/source/proto/main/java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
然后我们点击右边的 Maven 栏,点击 install 按钮:
点击 install 后,我们可以看到 build success 的提示,这样我们就可以很快地生成对应的代码了,最后我们的代码块会按照配置中写的生成目录,最后生成 Java 代码:
我们可以看到在 mumaren-dubbo-protobuf-api 模块下有一个 build 的目录,目录下有如下的代码,我们这边为了方便测试,我们把代码拷贝到 mumaren-dubbo-protobuf-api 的 src 目录下来:
删掉 build 的目录后,我们最后的目录就变成如下的结构了,接下来,我们就按照官方的 sample 示例文档编写就可以了,逻辑也相对比较简单,和普通的 Dubbo 的 RPC 调用也比较相似,我们就快速地贴一下代码。
首先,我们这边先看一下服务提供者端的逻辑实现,代码如下所示,可以看出和普通代码没有什么不同。
package org.mumaren.provider.service.impl;
import org.apache.dubbo.rpc.RpcContext;
import org.mumaren.api.DemoService;
import org.mumaren.api.HelloReply;
import org.mumaren.api.HelloRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.CompletableFuture;
public class DemoServiceImpl implements DemoService {
private static final Logger logger = LoggerFactory.getLogger(DemoServiceImpl.class);
@Override
public HelloReply sayHello(HelloRequest request) {
logger.info("Hello " + request.getName() + ", request from consumer: " + RpcContext.getContext().getRemoteAddress());
return HelloReply.newBuilder()
.setMessage("Hello " + request.getName() + ", response from provider: "
+ RpcContext.getContext().getLocalAddress())
.build();
}
@Override
public CompletableFuture<HelloReply> sayHelloAsync(HelloRequest request) {
return CompletableFuture.completedFuture(sayHello(request));
}
}
接下来,我们再看一下 Dubbo 服务提供者端一些经典的配置,dubbo-provider.xml 配置,我们可以看到只是在 dubbo.service 这个 scheme 后面新增了一个 serialization 的参数,制定序列化方式是 Protobuf,其他没有任何差别。
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<dubbo:application name="mumaren-dubbo-protobuf-provider"/>
<dubbo:provider token="true"/>
<dubbo:registry address="zookeeper://${zookeeper.address:127.0.0.1}:2181"/>
<dubbo:protocol name="dubbo"/>
<bean id="demoServiceImpl" class="org.mumaren.provider.service.impl.DemoServiceImpl"/>
<dubbo:service serialization="protobuf" interface="org.mumaren.api.DemoService"
ref="demoServiceImpl"/>
</beans>
最后我们再用 ClassPathXmlApplicationContext 这个 Spring 的上下文快速启动测试一下。
package org.mumaren.provider;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.util.concurrent.CountDownLatch;
public class ProviderApplication {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/dubbo-provider.xml");
context.start();
System.out.println("dubbo service started");
new CountDownLatch(1).await();
}
}
接下来,我们再看下服务消费端的代码,代码逻辑也非常简单,只涉及到一个 xml 的配置,我们先看下 dubbo-consumer.xml 的配置,可以看到和我们普通的消费没有任何区别。
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<dubbo:application name="mumaren-dubbo-protobuf-consumer"/>
<dubbo:registry address="zookeeper://${zookeeper.address:127.0.0.1}:2181"/>
<dubbo:reference id="demoService" check="false" interface="org.mumaren.api.DemoService"/>
</beans>
然后我们使用最简单的代码进行测试,ConsumerApplication.java:
package org.mumaren.consumer;
import org.mumaren.api.DemoService;
import org.mumaren.api.HelloReply;
import org.mumaren.api.HelloRequest;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class ConsumerApplication {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/dubbo-consumer.xml");
context.start();
DemoService demoService = context.getBean("demoService", DemoService.class);
HelloRequest request = HelloRequest.newBuilder().setName("Hello").build();
HelloReply reply = demoService.sayHello(request);
System.out.println("result: " + reply.getMessage());
System.in.read();
}
}
代码到此为止就结束了,代码完整的结构如下所示:
接下来,我们进行简单测试,首先,我们先启动服务提供者端的 main 函数:
然后我们再启动服务消费端的代码,我们可以正常返回数据:
到此为止,我们很快地进行了一次小小的尝鲜,可以看出,除了在生成 API 接口定义的时候,其他没有什么不一样。接下来,我们再试一下 gRPC 的基本使用。
总结
最后,我们再来给一个结论:我们需要升级到 Dubbo 3 吗?
目前而言,我个人觉得暂时没有必要升级,除非是大厂,线上注册中心的压力比较大。亦或者是对性能的要求比较高,需要亟需升级来解决性能问题。
当然如果是兴趣爱好,可以进一步研究 Dubbo 3 的源代码,而不是直接用在生产环境,因为最近 Dubbo 3 目前迭代的还是很频繁,一些细小的 bug 还是存在的,Dubbo 3 的文档和 dubbo-sample 目前个人觉得都不是很齐全,如果您遇到一些问题,也很难去解决。
当然,我们还是很开心看到 Dubbo 的前进,看它的一点点改变,后续也会进一步研究它的源代码,并且尝试自己写一个应用级的 RPC 的 Demo,并且也能够支持 gRPC 这样的传输方式和 Protobuf 序列化方式,大家如果有兴趣,可以留言、关注、点赞,我们可以一起研究从头写一个比较简单的应用级注册的 RPC 框架。
本文暂时没有评论,来添加一个吧(●'◡'●)