尼恩说在前面
在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,并且拿了很多大厂offer。
其中 SpringCloud 工业级底座 ,是大家的面试核心,面试重点。 比如小伙伴在面试 网易的时候,就遇到以下面试题:
dubbo 和 spring cloud区别是什么? dubbo 3.0 和 spring cloud 如何整合?
dubbo 3.0 和 spring cloud Gateway 如何整合?
小伙伴由于之前没有系统的去梳理和总结,所以支支吾吾的说了几句,面试官不满意,面试挂了。
所以,尼恩给大家做一下系统化、体系化的梳理,联合社群小伙伴,来一个 Sa-Token学习圣经:从入门到精通 Sa-Token学习圣经 。
特别说明的是, 本文属于 尼恩团队 从0到1 大实战:穿透 SpringCloud 工业级 底座工程(一共包括 15大圣经的 ) 其中之一。
15大圣经 ,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
尼恩团队 从0到1 大实战 SpringCloud 工业级底座 的 知识体系的轮廓如下,详情请点击:15大圣经的介绍:
工业级脚手架实现的包括的 15大学习圣经,目录如下:
其中,专题1 权限设计以及 安全认证相关的两个圣经,具体如下:
-
Dubbo学习圣经:从入门到精通 Dubbo3.0 + SpringCloud
本文,就是 Dubbo学习圣经。这个版本,稍后会录制视频.
录完之后,Dubbo学习圣经 正式版本会有更新, 最新版本找尼恩获取。
1 Dubbo简介
1.1 Dubbo简介
Apache Dubbo 是一款 RPC 服务开发框架,用于解决微服务架构下的服务治理与通信问题,官方提供了 Java、Golang 等多语言 SDK 实现。使用 Dubbo 开发的微服务原生具备相互之间的远程地址发现与通信能力, 利用 Dubbo 提供的丰富服务治理特性,可以实现诸如服务发现、负载均衡、流量调度等服务治理诉求。
2022年阿里巴巴将其内部 HSF 系统与开源社区 Dubbo 相融合,与社区一同推出了云原生时代的 Dubbo3 架构,截止 2022 年双十一结束,Dubbo3 已经在阿里巴巴内部广泛落地,实现了老版本 HSF2 框架升级,包括电商核心、阿里云等核心系统已经全面运行在 Dubbo3 之上。
以上是 Dubbo 的工作原理图,从抽象架构上分为两层:服务治理抽象控制面 和 Dubbo 数据面 。
- 服务治理控制面。服务治理控制面不是特指如注册中心类的单个具体组件,而是对 Dubbo 治理体系的抽象表达。控制面包含协调服务发现的注册中心、流量管控策略、Dubbo Admin 控制台等,如果采用了 Service Mesh 架构则还包含 Istio 等服务网格控制面。
- Dubbo 数据面。数据面代表集群部署的所有 Dubbo 进程,进程之间通过 RPC 协议实现数据交换,Dubbo 定义了微服务应用开发与调用规范并负责完成数据传输的编解码工作。
- 服务消费者 (Dubbo Consumer),发起业务调用或 RPC 通信的 Dubbo 进程
- 服务提供者 (Dubbo Provider),接收业务调用或 RPC 通信的 Dubbo 进程
1.2 浅谈RPC
1.2.1 简介
RPC(Remote Procedure Call)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。
RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。现在业界有很多开源的优秀 RPC 框架,例如 Spring Cloud、Dubbo、Thrift 等。
RPC 这个概念术语在上世纪 80 年代由 Bruce Jay Nelson 提出。这里我们追溯下当初开发 RPC 的原动机是什么?在 Nelson 的论文 “Implementing Remote Procedure Calls” 中他提到了几点:
- 简单:RPC 概念的语义十分清晰和简单,这样建立分布式计算就更容易。
- 高效:过程调用看起来十分简单而且高效。
- 通用:在单机计算中过程往往是不同算法部分间最重要的通信机制。
通俗一点说,就是一般程序员对于本地的过程调用很熟悉,那么我们把 RPC 作成和本地调用完全类似,那么就更容易被接受,使用起来毫无障碍。Nelson 的论文发表于 30 年前,其观点今天看来确实高瞻远瞩,今天我们使用的 RPC 框架基本就是按这个目标来实现的。
1.2.2 基本架构
RPC的设计由Client,Client stub,Network ,Server stub,Server构成。 其中Client就是用来调用服务的,Cient stub是用来把调用的方法和参数序列化的(因为要在网络中传输,必须要把对象转变成字节),Network用来传输这些信息到Server stub, Server stub用来把这些信息反序列化的,Server就是服务的提供者,最终调用的就是Server提供的方法。
- Client像调用本地服务似的调用远程服务;
- Client stub接收到调用后,将方法、参数序列化
- 客户端通过sockets将消息发送到服务端
- Server stub 收到消息后进行解码(将消息对象反序列化)
- Server stub 根据解码结果调用本地的服务
- 本地服务执行(对于服务端来说是本地执行)并将结果返回给Server stub
- Server stub将返回结果打包成消息(将结果消息对象序列化)
- 服务端通过sockets将消息发送到客户端
- Client stub接收到结果消息,并进行解码(将结果消息反序列化)
- 客户端得到最终结果。
RPC 调用分以下两种:
- 同步调用:客户方等待调用执行完成并返回结果。
- 异步调用:客户方调用后不用等待执行结果返回,但依然可以通过回调通知等方式获取返回结果。若客户方不关心调用返回结果,则变成单向异步调用,单向调用不用返回结果。
异步和同步的区分在于是否等待服务端执行完成并返回结果。
1.3 Dubbo 与 Spring Cloud
- Dubbo 和 Spring Cloud 相同点:
- 对于服务治理(如服务发现、负载均衡、动态配置等),形成了一套微服务整体解决方案,让使用Dubbo 及 Spring Cloud 的用户在开发微服务应用时可以专注在业务逻辑开发上。
- 都基于和兼容 Spring,Spring Boot 体系的应用开发模式。
- Spring Cloud 优点:
- 同样都支持 Spring 开发体系的情况下,Spring Cloud 得到更多的原生支持
- 对一些常用的微服务模式做了抽象如服务发现、动态配置、异步消息等,同时包括一些批处理任务、定时任务、持久化数据访问等领域也有涉猎。
- 基于 HTTP 的通信模式,加上相对比较完善的入门文档和演示 demo 和 starters,让开发者在第一感觉上更易于上手
- Dubbo优点
- 相对于Cloud Open Feign基于Rest通讯,Dubbo有更高性能的 RPC 协议编码与实现
- 相对于Cloud通讯协议绑定HTTP,Dubbo在通信协议和编码上选择更灵活,包括 rpc 通信层协议如 HTTP、HTTP/2(Triple、gRPC)、TCP 二进制协议、rest等,序列化编码协议Protobuf、JSON、Hessian2 等,支持单端口多协议。
- 完全支持 Spring & Spring Boot 开发模式,同时在服务发现、动态配置等基础模式上提供与 Spring Cloud 对等的能力。
- 是企业级微服务实践方案的整体输出,Dubbo 考虑到了企业微服务实践中会遇到的各种问题如优雅上下线、多注册中心、流量管理等,因此其在生产环境的长期维护成本更低
- Dubbo 从设计上突出服务服务治理能力,如权重动态调整、标签路由、条件路由等,支持 Proxyless 等多种模式接入 Service Mesh 体系
- Dubbo 是在超大规模微服务集群实践场景下开发的框架,可以做到百万实例规模的集群水平扩容,应对集群增长带来的各种问题
- Dubbo 提供 Java 外的多语言实现,使得构建多语言异构的微服务体系成为可能
官网说dubbo3.3和spring cloud可以互通,参考官方案例dubbo-samples-springcloud
1.4 Dubbo的超高性能
使用Dubbo最主要的原因是,Dubbo 在通信性能、稳定性方面具有无可比拟的优势,非常适合构建近乎无限水平伸缩的微服务集群,这也是 Dubbo 从实践层面优于业界很多同类的产品的巨大优势。
Dubbo 内置支持 Dubbo2、Triple 两款高性能通信协议。其中
- Dubbo2 是基于 TCP 传输协议之上构建的二进制私有 RPC 通信协议,是一款非常简单、紧凑、高效的通信协议。
- Triple 是基于 HTTP/2 的新一代 RPC 通信协议,在网关穿透性、通用性以及 Streaming 通信上具备优势,Triple 完全兼容 gRPC 协议。
以下是基于 Dubbo 3.2 版本得出的压测指标数据,您也可以通过 dubbo-benchmark 项目自行压测。
对比 Dubbo 2.x 及早期 3.x 版本
- 较小报文场景 createUser、getUser 下,提升率约 180%。
- 极小报文 existUser(仅一个boolean值)下提升率约 24%
- 较大报文 listUser 提升率最高,达到了 1000%!
- 较小报文场景 createUser、existUser、getUser 下,较 3.1 版本性能提升约 40-45%,提升后的性能与 gRPC 同场景的性能基本持平。
- 较大报文场景 listUser 下较 3.1 版本提升了约 17%,相较于同场景下的 gRPC 低 11%。
2 Dubbo入门案例
2.1 项目介绍
在本任务中,将分为 3 个子模块进行独立开发,模拟生产环境下的部署架构。
. /dubbo-samples/1-basic/dubbo-samples-spring-boot
├── dubbo-samples-spring-boot-interface // 共享 API 模块
├── dubbo-samples-spring-boot-consumer // 消费端模块
└── dubbo-samples-spring-boot-provider // 服务端模块
如上所示,共有 3 个模块,其中 interface
模块被 consumer
和 provider
两个模块共同依赖,存储 RPC 通信使用的 API 接口。
. // apache/dubbo-samples/1-basic/dubbo-samples-spring-boot
├── dubbo-samples-spring-boot-interface // 共享 API 模块
│ ├── pom.xml
│ └── src
│ └── main
│ └── java
│ └── org
│ └── apache
│ └── dubbo
│ └── springboot
│ └── demo
│ └── DemoService.java // API 接口
├── dubbo-samples-spring-boot-consumer // 消费端模块
│ ├── pom.xml
│ └── src
│ ├── main
│ │ ├── java
│ │ │ └── org
│ │ │ └── apache
│ │ │ └── dubbo
│ │ │ └── springboot
│ │ │ └── demo
│ │ │ └── consumer
│ │ │ ├── ConsumerApplication.java // 消费端启动类
│ │ │ └── Task.java // 消费端模拟调用任务
│ │ └── resources
│ │ └── application.yml // Spring Boot 配置文件
├── dubbo-samples-spring-boot-provider // 服务端模块
│ ├── pom.xml
│ └── src
│ └── main
│ ├── java
│ │ └── org
│ │ └── apache
│ │ └── dubbo
│ │ └── springboot
│ │ └── demo
│ │ └── provider
│ │ ├── DemoServiceImpl.java // 服务端实现类
│ │ └── ProviderApplication.java // 服务端启动类
│ └── resources
│ └── application.yml // Spring Boot 配置文件
└── pom.xml
2.2 快速部署
- 获取测试工程
案例工程代码/dubbo-samples-spring-boot
- 启动一个简易的注册中心
对于一个微服务化的应用来说,注册中心是不可或缺的一个组件。只有通过注册中心,消费端才可以成功发现服务端的地址信息,进而进行调用。
在本地准备测试用Zookeeper
解压zookeeper配置zoo.cfg,然后执行zkServer.cmd
命令启动
生产环境参考高可用Zookeeper集群安装
- 部署启动
- 打包项目后
- 启动服务提供者
- 启动服务消费者
启动一个服务消费者来调用服务提供者后,打印出的数据就是服务提供者处理之后返回的,标志着一次服务调用的成功。
2023-02-08 17:14:33.045 INFO 80740 --- [lication.main()] o.a.d.s.d.consumer.ConsumerApplication : Started ConsumerApplication in 11.052 seconds (JVM running for 31.62)
Receive result ======> Hello world
2023-02-08 17:14:33.146 INFO 80740 --- [pool-1-thread-1] .b.c.e.AwaitingNonWebApplicationListener : [Dubbo] Current Spring Boot Application is await...
Wed Feb 08 17:14:34 CST 2023 Receive result ======> Hello world
Wed Feb 08 17:14:35 CST 2023 Receive result ======> Hello world
Wed Feb 08 17:14:36 CST 2023 Receive result ======> Hello world
Wed Feb 08 17:14:37 CST 2023 Receive result ======> Hello world
2.3 动手实践1(从零代码开发版)
初始化项目
从本小节开始,将基于 IntelliJ IDEA 进行工程的搭建以及测试。
如上图所示,可以建立一个基础的项目。
搭建了基础项目之后,我们还需要创建 dubbo-spring-boot-demo-interface
、dubbo-spring-boot-demo-provider
和 dubbo-spring-boot-demo-consumer
三个子模块。
创建了三个子模块之后,需要创建一下几个文件夹:
- 在
dubbo-spring-boot-demo-consumer/src/main/java
下创建org.apache.dubbo.springboot.demo.consumer
package - 在
dubbo-spring-boot-demo-interface/src/main/java
下创建org.apache.dubbo.springboot.demo
package - 在
dubbo-spring-boot-demo-provider/src/main/java
下创建org.apache.dubbo.springboot.demo.provider
package
最终的文件夹参考如上图所示。
3. 添加 Maven 依赖
在初始化完项目以后,我们需要先添加 Dubbo 相关的 maven 依赖。
对于多模块项目,首先需要在父项目的 pom.xml
里面配置依赖信息。
编辑 ./pom.xml
这个文件,添加下列配置。
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>dubbo-spring-boot-demo-interface</module>
<module>dubbo-spring-boot-demo-provider</module>
<module>dubbo-spring-boot-demo-consumer</module>
</modules>
<properties>
<dubbo.version>3.2.0-beta.4</dubbo.version>
<spring-boot.version>2.7.8</spring-boot.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencyManagement>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Dubbo -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-bom</artifactId>
<version>${dubbo.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-dependencies-zookeeper-curator5</artifactId>
<version>${dubbo.version}</version>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</pluginManagement>
</build>
然后在 dubbo-spring-boot-consumer
和 dubbo-spring-boot-provider
两个模块 pom.xml
中进行具体依赖的配置。
编辑 ./dubbo-spring-boot-consumer/pom.xml
和 ./dubbo-spring-boot-provider/pom.xml
这两文件,都添加下列配置。
<dependencies>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-demo-interface</artifactId>
<version>${project.parent.version}</version>
</dependency>
<!-- dubbo -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-dependencies-zookeeper-curator5</artifactId>
<type>pom</type>
<exclusions>
<exclusion>
<artifactId>slf4j-reload4j</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- spring boot starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
在这份配置中,定义了 dubbo 和 zookeeper(以及对应的连接器 curator)的依赖。
添加了上述的配置以后,可以通过 IDEA 的 Maven - Reload All Maven Projects
刷新依赖。
4. 定义服务接口
服务接口 Dubbo 中沟通消费端和服务端的桥梁。
在 dubbo-spring-boot-demo-interface
模块的 org.apache.dubbo.samples.api
下建立 DemoService
接口,定义如下:
package org.apache.dubbo.springboot.demo;
public interface DemoService {
String sayHello(String name);
}
在 DemoService
中,定义了 sayHello
这个方法。后续服务端发布的服务,消费端订阅的服务都是围绕着 DemoService
接口展开的。
5. 定义服务端的实现
定义了服务接口之后,可以在服务端这一侧定义对应的实现,这部分的实现相对于消费端来说是远端的实现,本地没有相关的信息。
在dubbo-spring-boot-demo-provider
模块的 org.apache.dubbo.samples.provider
下建立 DemoServiceImpl
类,定义如下:
package org.apache.dubbo.springboot.demo.provider;
import org.apache.dubbo.config.annotation.DubboService;
import org.apache.dubbo.springboot.demo.DemoService;
@DubboService
public class DemoServiceImpl implements DemoService {
@Override
public String sayHello(String name) {
return "Hello " + name;
}
}
在 DemoServiceImpl
中,实现了 DemoService
接口,对于 sayHello
方法返回 Hello name
。
注:在DemoServiceImpl
类中添加了 @DubboService
注解,通过这个配置可以基于 Spring Boot 去发布 Dubbo 服务。
6. 配置服务端 Yaml 配置文件
从本步骤开始至第 7 步,将会通过 Spring Boot 的方式配置 Dubbo 的一些基础信息。
首先,我们先创建服务端的配置文件。
在 dubbo-spring-boot-demo-provider
模块的 resources
资源文件夹下建立 application.yml
文件,定义如下:
dubbo:
application:
name: dubbo-springboot-demo-provider
protocol:
name: dubbo
port: -1
registry:
address: zookeeper://${zookeeper.address:127.0.0.1}:2181
在这个配置文件中,定义了 Dubbo 的应用名、Dubbo 协议信息、Dubbo 使用的注册中心地址。
7. 配置消费端 YAML 配置文件
同样的,我们需要创建消费端的配置文件。
在 dubbo-spring-boot-demo-consumer
模块的 resources
资源文件夹下建立 application.yml
文件,定义如下:
dubbo:
application:
name: dubbo-springboot-demo-consumer
protocol:
name: dubbo
port: -1
registry:
address: zookeeper://${zookeeper.address:127.0.0.1}:2181
在这个配置文件中,定义了 Dubbo 的应用名、Dubbo 协议信息、Dubbo 使用的注册中心地址。
8. 基于 Spring 配置服务端启动类
除了配置 Yaml 配置文件之外,我们还需要创建基于 Spring Boot 的启动类。
首先,我们先创建服务端的启动类。
在 dubbo-spring-boot-demo-provider
模块的 org.apache.dubbo.springboot.demo.provider
下建立 Application
类,定义如下:
package org.apache.dubbo.springboot.demo.provider;
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableDubbo
public class ProviderApplication {
public static void main(String[] args) {
SpringApplication.run(ProviderApplication.class, args);
}
}
在这个启动类中,配置了一个 ProviderApplication
去读取我们前面第 6 步中定义的 application.yml
配置文件并启动应用。
9. 基于 Spring 配置消费端启动类
同样的,我们需要创建消费端的启动类。
在 dubbo-spring-boot-demo-consumer
模块的 org.apache.dubbo.springboot.demo.consumer
下建立 Application
类,定义如下:
package org.apache.dubbo.springboot.demo.consumer;
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableDubbo
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}
在这个启动类中,配置了一个 ConsumerApplication
去读取我们前面第 7 步中定义的 application.yml
配置文件并启动应用。
10. 配置消费端请求任务
除了配置消费端的启动类,我们在 Spring Boot 模式下还可以基于 CommandLineRunner
去创建
在 dubbo-spring-boot-demo-consumer
模块的 org.apache.dubbo.springboot.demo.consumer
下建立 Task
类,定义如下:
package org.apache.dubbo.springboot.demo.consumer;
import java.util.Date;
import org.apache.dubbo.config.annotation.DubboReference;
import org.apache.dubbo.springboot.demo.DemoService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class Task implements CommandLineRunner {
@DubboReference
private DemoService demoService;
@Override
public void run(String... args) throws Exception {
String result = demoService.sayHello("world");
System.out.println("Receive result ======> " + result);
new Thread(()-> {
while (true) {
try {
Thread.sleep(1000);
System.out.println(new Date() + " Receive result ======> " + demoService.sayHello("world"));
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
}
}).start();
}
}
在 Task
类中,通过@DubboReference
从 Dubbo 获取了一个 RPC 订阅,这个 demoService
可以像本地调用一样直接调用。在 run
方法中创建了一个线程进行调用。
11. 启动应用
截止第 10 步,代码就已经开发完成了,本小节将启动整个项目并进行验证。
首先是启动 org.apache.dubbo.samples.provider.Application
,等待一会出现如下图所示的日志(Current Spring Boot Application is await
)即代表服务提供者启动完毕,标志着该服务提供者可以对外提供服务了。
[Dubbo] Current Spring Boot Application is await...
然后是启动org.apache.dubbo.samples.client.Application
,等待一会出现如下图所示的日志(Hello world
)即代表服务消费端启动完毕并调用到服务端成功获取结果。
Receive result ======> Hello world
2.4 动手实践2(基于脚手架开发)
Dubbo Initializer 可用来快速生成 Java 项目脚手架,帮助简化微服务项目搭建、基本配置、组件依赖管理等。
选择 Dubbo 版本
Initializer 将使用 dubbo-spring-boot-starter
创建 Spring Boot 项目,因此我们首先需要选择 Dubbo 与 Spring Boot 的版本。
录入项目基本信息
接下来,填入项目基本信息,包括项目坐标、项目名称、包名、JDK 版本等。
选择项目结构
有两种项目结构可共选择,分别是 单模块
和 多模块
,在这个示例中我们选择 单模块
。
- 单模块,所有组件代码存放在一个 module 中,特点是结构简单。
- 多模块,生成的项目有
API
、Service
两个模块,其中API
用于存放 Dubbo 服务定义,Service
用于存放服务实现或调用逻辑。通常多模块更有利于服务定义的单独管理与发布。
选择依赖组件
我们为模板默认选择如下几个依赖组件:
- Dubbo 组件
- Java Interface
- 注册中心,zookeeper
- 协议 TCP
- 常用微服务组件
- Web
- Mybatis
- 模版引擎
基于以上选项,生成的项目将以 Zookeeper 为注册中心,以高性能 Dubbo2 TCP 协议为 RPC 通信协议,并且增加了 Web、Mybatis 等组件依赖和示例。
生成项目模板
- 点击 “浏览代码” 可在线浏览项目结构与代码
- 点击 “获取代码” 生成项目下载地址
项目下载到本地后,解压并导入 IDE 后即可根据需要开发定制 Dubbo 应用。
2.5 Dubbo Admin
- 简介
Admin 控制台可视化展示了集群中的应用、服务、实例及依赖关系,支持流量治理规则下发,同时还提供如服务测试、mock、文档管理等提升研发测试效率的工具
基本功能包括:
- 文档查询
- 服务测试
- 服务Mock
- 流量管控
- 部署架构
总体上来说,Admin 部署架构分为以下几个部分:
- Admin 主进程,包括服务发现元数据管理、可视化控制台、安全认证策略管控、其他定制化服务治理能力等组件。
- 强依赖组件,包括 Mysql 数据库、注册/配置/元数据中心(可以是 Kubernetes、Nacos、Zookeeper 等)
- 可选依赖组件,包括 Prometheus、Grafana、Zipkin 等
- 安装
生产环境下可以参考官方文档使用k8s和docker进行容器化安装,
测试用环境,下载dubbo-admin-0.6.0,修改zookeeper配置,打包运行/dubbo-admin-server/target/dubbo-admin-server-0.6.0.jar即可
java -jar dubbo-admin-server-0.6.0.jar
3 服务注册和发现
3.1 注册和发现简介
Dubbo 提供的是一种 Client-Based 的服务发现机制,依赖第三方注册中心组件来协调服务发现过程,支持常用的注册中心如 Nacos、Consul、Zookeeper 等。
- Dubbo 提供者: 注册 URL 地址到注册中心,
- 注册中心: 负责对数据进行聚合,每当地址列表发生变化,注册中心将最新的列表通知到所有订阅的消费者实例。
- Dubbo 消费者: 从注册中心读取地址列表并订阅变更,
3.1.1 服务注册
从注册中心视角来看,它负责以应用名 (dubbo.application.name) 对整个集群的实例地址进行聚合,每个对外提供服务的实例将自身的应用名、实例ip:port 地址信息 (通常还包含少量的实例元数据,如机器所在区域、环境等) 注册到注册中心。
Dubbo2 版本注册中心以服务粒度聚合实例地址,比应用粒度更细,也就意味着传输的数据量更大,因此在大规模集群下也遇到一些性能问题。
3.1.2 服务发现(服务订阅)
- 全量订阅
- 按需订阅
dubbo采用的是按需订阅
每个消费服务的实例从注册中心订阅实例地址列表,相比于一些产品直接将注册中心的全量数据 (应用 + 实例地址) 加载到本地进程,Dubbo 实现了按需精准订阅地址信息。比如一个消费者应用依赖 app1、app2,则只会订阅 app1、app2 的地址列表更新,大幅减轻了冗余数据推送和解析的负担。
3.1.3 服务元数据
除了与注册中心的交互,Dubbo3 的完整地址发现过程还有一条额外的元数据通路,我们称之为元数据服务 (MetadataService),实例地址与元数据共同组成了消费者端有效的地址列表。
完整工作流程如上图所示,
- 首先,消费者从注册中心接收到地址 (ip:port) 信息,
- 然后与提供者建立连接并通过元数据服务读取到对端的元数据配置信息,
- 两部分信息共同组装成 Dubbo 消费端有效的面向服务的地址列表。
- 以上两个步骤都是在实际的 RPC 服务调用发生之前。
3.1.4 Dubbo的优势
- 首先,Dubbo 注册中心以应用粒度聚合实例数据,消费者按消费需求精准订阅,避免了大多数开源框架如 Istio、Spring Cloud 等全量订阅带来的性能瓶颈。
- 其次,Dubbo SDK 在实现上对消费端地址列表处理过程做了大量优化,地址通知增加了异步、缓存、bitmap 等多种解析优化,避免了地址更新常出现的消费端进程资源波动。
- 最后,在功能丰富度和易用性上,服务发现除了同步 ip、port 等端点基本信息到消费者外,Dubbo 还将服务端的 RPC/HTTP 服务及其配置的元数据信息同步到消费端,这让消费者、提供者两端的更细粒度的协作成为可能,Dubbo 基于此机制提供了很多差异化的治理能力。
区别于其他很多微服务框架的是,Dubbo3 的服务发现机制诞生于阿里巴巴超大规模微服务电商集群实践场景,因此,其在性能、可伸缩性、易用性等方面的表现大幅领先于业界大多数主流开源产品。是企业面向未来构建可伸缩的微服务集群的最佳选择。
3.2 应用级服务发现设计
Dubbo3采用应用级服务发现方案
3.2.1 基本原理
- 每个 Provider 通过特定的 key 向注册中心注册本机可访问地址;
- 注册中心通过这个 key 对 provider 实例地址进行聚合;
- Consumer 通过同样的 key 从注册中心订阅,以便及时收到聚合后的地址列表;
3.2.2 接口级数据结构
- 首先,看右下角 provider 实例内部的数据与行为。Provider 部署的应用中通常会有多个 Service,也就是 Dubbo2 中的服务,每个 service 都可能会有其独有的配置,我们所讲的 service 服务发布的过程,其实就是基于这个服务配置生成地址 URL 的过程,生成的地址数据如图所示;同样的,其他服务也都会生成地址。
- 然后,看一下注册中心的地址数据存储结构,注册中心以 service 服务名为数据划分依据,将一个服务下的所有地址数据都作为子节点进行聚合,子节点的内容就是实际可访问的ip地址,也就是我们 Dubbo 中 URL,格式就是刚才 provider 实例生成的。
这里把 URL 地址数据划分成了几份:
- 首先是实例可访问地址,主要信息包含 ip port,是消费端将基于这条数据生成 tcp 网络链接,作为后续 RPC 数据的传输载体
- 其次是 RPC 元数据,元数据用于定义和描述一次 RPC 请求,一方面表明这条地址数据是与某条具体的 RPC 服务有关的,它的版本号、分组以及方法相关信息,另一方面表明
- 下一部分是 RPC 配置数据,部分配置用于控制 RPC 调用的行为,还有一部分配置用于同步 Provider 进程实例的状态,典型的如超时时间、数据编码的序列化方式等。
- 最后一部分是自定义的元数据,这部分内容区别于以上框架预定义的各项配置,给了用户更大的灵活性,用户可任意扩展并添加自定义元数据,以进一步丰富实例状态。
结合以上两页对于 Dubbo2 接口级地址模型的分析,以及最开始的 Dubbo 基本原理图,我们可以得出这么几条结论:
- 第一,地址发现聚合的 key 就是 RPC 粒度的服务
- 第二,注册中心同步的数据不止包含地址,还包含了各种元数据以及配置
- 得益于 1 与 2,Dubbo 实现了支持应用、RPC 服务、方法粒度的服务治理能力
这就是一直以来 Dubbo2 在易用性、服务治理功能性、可扩展性上强于很多服务框架的真正原因。
一个事物总是有其两面性,Dubbo2 地址模型带来易用性和强大功能的同时,也给整个架构的水平可扩展性带来了一些限制。这个问题在普通规模的微服务集群下是完全感知不到的,而随着集群规模的增长,当整个集群内应用、机器达到一定数量时,整个集群内的各个组件才开始遇到规模瓶颈。在总结包括阿里巴巴、工商银行等多个典型的用户在生产环境特点后,我们总结出以下两点突出问题(如图中红色所示):
- 首先,注册中心集群容量达到上限阈值。由于所有的 URL 地址数据都被发送到注册中心,注册中心的存储容量达到上限,推送效率也随之下降。
- 而在消费端这一侧,Dubbo2 框架常驻内存已超 40%,每次地址推送带来的 cpu 等资源消耗率也非常高,影响正常的业务调用。
为什么会出现这个问题?我们以一个具体 provider 示例进行展开,来尝试说明为何应用在接口级地址模型下容易遇到容量问题。 青蓝色部分,假设这里有一个普通的 Dubbo Provider 应用,该应用内部定义有 10 个 RPC Service,应用被部署在 100 个机器实例上。这个应用在集群中产生的数据量将会是 “Service 数 * 机器实例数”,也就是 10 * 100 = 1000 条。数据被从两个维度放大:
- 从地址角度。100 条唯一的实例地址,被放大 10 倍
- 从服务角度。10 条唯一的服务元数据,被放大 100 倍
3.2.3 应用级数据结构
面对这个问题,在 Dubbo3 架构下,我们不得不重新思考两个问题:
- 如何在保留易用性、功能性的同时,重新组织 URL 地址数据,避免冗余数据的出现,让 Dubbo3 能支撑更大规模集群水平扩容?
- 如何在地址发现层面与其他的微服务体系如 Kubernetes、Spring Cloud 打通?
Dubbo3 的应用级服务发现方案设计本质上就是围绕以上两个问题展开。其基本思路是:地址发现链路上的聚合元素也就是我们之前提到的 Key 由服务调整为应用,这也是其名称叫做应用级服务发现的由来;另外,通过注册中心同步的数据内容上做了大幅精简,只保留最核心的 ip、port 地址数据。
这是升级之后应用级地址发现的内部数据结构进行详细分析。 对比之前接口级的地址发现模型,我们主要关注橙色部分的变化。首先,在 provider 实例这一侧,相比于之前每个 RPC Service 注册一条地址数据,一个 provider 实例只会注册一条地址到注册中心;而在注册中心这一侧,地址以应用名为粒度做聚合,应用名节点下是精简过后的 provider 实例地址;
应用级服务发现的上述调整,同时实现了地址单条数据大小和总数量的下降,但同时也带来了新的挑战:我们之前 Dubbo2 强调的易用性和功能性的基础损失了,因为元数据的传输被精简掉了,如何精细的控制单个服务的行为变得无法实现。
针对这个问题,Dubbo3 的解法是引入一个内置的 MetadataService 元数据服务,由中心化推送转为 Consumer 到 Provider 的点对点拉取,在这个模式下,元数据传输的数据量将不在是一个问题,因此可以在元数据中扩展出更多的参数、暴露更多的治理数据。
这里我们个重点看消费端 Consumer 的地址订阅行为,消费端从分两步读取地址数据,首先是从注册中心收到精简后的地址,随后通过调用 MetadataService 元数据服务,读取对端的元数据信息。在收到这两部分数据之后,消费端会完成地址数据的聚合,最终在运行态还原出类似 Dubbo2 的 URL 地址格式。因此从最终结果而言,应用级地址模型同时兼顾了地址传输层面的性能与运行层面的功能性。
3.3 三中心架构
3.3.1 三中心逻辑架构
- 注册中心。协调 Consumer 与 Provider 之间的地址注册与发现
- 配置中心。
- 存储 Dubbo 启动阶段的全局配置,保证配置的跨环境共享与全局一致性
- 负责服务治理规则(路由规则、动态配置等)的存储与推送。
- 元数据中心。
- 接收 Provider 上报的服务接口元数据,为 Admin 等控制台提供运维能力(如服务测试、接口文档等)
- 作为服务发现机制的补充,提供额外的接口/方法级别配置信息的同步能力,相当于注册中心的额外扩展
以上三个中心并不是运行 Dubbo 的必要条件,用户完全可以根据自身业务情况决定只启用其中一个或多个,以达到简化部署的目的。通常情况下,所有用户都会以独立的注册中心 以开始 Dubbo 服务开发,而配置中心、元数据中心则会在微服务演进的过程中逐步的按需被引入进来。
3.3.2 注册中心
图中没有部署配置中心和元数据中心,在Dubbo中会默认将注册中心的实例同时作为配置中心和元数据中心,这是Dubbo的默认行为
如果确实不需要配置中心或者元数据中心的能力,可在配置中关闭,在注册中心的配置中有两个配置分别为use-as-config-center和use-as-metadata-center,将配置置为false即可。
3.3.3 元数据中心
在以下几种情况下会需要部署元数据中心:
- 对于一个原先采用老版本Dubbo搭建的应用服务,在迁移到Dubbo 3时,Dubbo 3 会需要一个元数据中心来维护RPC服务与应用的映射关系(即接口与应用的映射关系),因为如果采用了应用级别的服务发现和服务注册,在注册中心中将采用“应用 —— 实例列表”结构的数据组织形式,不再是以往的“接口 —— 实例列表”结构的数据组织形式,而以往用接口级别的服务注册和服务发现的应用服务在迁移到应用级别时,得不到接口与应用之间的对应关系,从而无法从注册中心得到实例列表信息,所以Dubbo为了兼容这种场景,在Provider端启动时,会往元数据中心存储接口与应用的映射关系。
- 为了让注册中心更加聚焦于地址的发现和推送能力,减轻注册中心的负担,元数据中心承载了所有的服务元数据、大量接口/方法级别配置信息等,无论是接口粒度还是应用粒度的服务发现和注册,元数据中心都起到了重要的作用。
该图中不配备配置中心,意味着可以不需要全局管理配置的能力。该图中不配备注册中心,意味着可能采用了Dubbo mesh的方案,也可能不需要进行服务注册,仅仅接收直连模式的服务调用。
3.3.4 配置中心
配置中心与其他两大中心不同,它无关于接口级还是应用级,它与接口并没有对应关系,它仅仅与配置数据有关,即使没有部署注册中心和元数据中心,配置中心也能直接被接入到Dubbo应用服务中。在整个部署架构中,整个集群内的实例(无论是Provider还是Consumer)都将会共享该配置中心集群中的配置,如下图所示:
该图中不配备注册中心,意味着可能采用了Dubbo mesh的方案,也可能不需要进行服务注册,仅仅接收直连模式的服务调用。
该图中不配备元数据中心,意味着Consumer可以从Provider暴露的MetadataService获取服务元数据,从而实现RPC调用
dubbo:
scan:
base-packages: com.crazymaker.cloud.dubbo
application:
name: ${spring.application.name}
protocol:
name: dubbo
port: -1
registry:
address: nacos://${NACOS_SERVER:cdh1:8848}
username: nacos
password: nacos
parameters:
namespace: dubbo
group: DUBBO_GROUP
config-center:
address: nacos://${NACOS_SERVER:cdh1:8848}
username: nacos
password: nacos
group: DUBBO_GROUP
metadata-report:
address: nacos://${NACOS_SERVER:cdh1:8848}
username: nacos
password: nacos
group: DUBBO_GROUP
consumer:
timeout: 1000000 #默认超时时间为1秒(1000毫秒) ,不利于调试,改为 1000000
3.3.5 三中心高可用的部署架构
虽然三大中心已不再是Dubbo应用服务所必须的,但是在真实的生产环境中,一旦已经集成并且部署了该三大中心,三大中心还是会面临可用性问题,Dubbo需要支持三大中心的高可用方案。在Dubbo中就支持多注册中心、多元数据中心、多配置中心,来满足同城多活、两地三中心、异地多活等部署架构模式的需求。
Dubbo SDK对三大中心都支持了Multiple模式。
- 多注册中心:Dubbo 支持多注册中心,即一个接口或者一个应用可以被注册到多个注册中心中,比如可以注册到ZK集群和Nacos集群中,Consumer也能够从多个注册中心中进行订阅相关服务的地址信息,从而进行服务发现。通过支持多注册中心的方式来保证其中一个注册中心集群出现不可用时能够切换到另一个注册中心集群,保证能够正常提供服务以及发起服务调用。这也能够满足注册中心在部署上适应各类高可用的部署架构模式。
- 多配置中心:Dubbo支持多配置中心,来保证其中一个配置中心集群出现不可用时能够切换到另一个配置中心集群,保证能够正常从配置中心获取全局的配置、路由规则等信息。这也能够满足配置中心在部署上适应各类高可用的部署架构模式。
- 多元数据中心:Dubbo 支持多元数据中心:用于应对容灾等情况导致某个元数据中心集群不可用,此时可以切换到另一个元数据中心集群,保证元数据中心能够正常提供有关服务元数据的管理能力。
拿注册中心举例,下面是一个多活场景的部署架构示意图:
3.3.6 多注册中心配置
- 多注册中心注册
比如:中文站有些服务来不及在青岛部署,只在杭州部署,而青岛的其它应用需要引用此服务,就可以将服务同时注册到两个注册中心。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
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="world" />
<!-- 多注册中心配置 -->
<dubbo:registry id="hangzhouRegistry" address="10.20.141.150:9090" />
<dubbo:registry id="qingdaoRegistry" address="10.20.141.151:9010" default="false" />
<!-- 向多个注册中心注册 -->
<dubbo:service interface="com.alibaba.hello.api.HelloService" version="1.0.0" ref="helloService" registry="hangzhouRegistry,qingdaoRegistry" />
</beans>
- 不同服务使用不同注册中心
比如:CRM 有些服务是专门为国际站设计的,有些服务是专门为中文站设计的。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
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="world" />
<!-- 多注册中心配置 -->
<dubbo:registry id="chinaRegistry" address="10.20.141.150:9090" />
<dubbo:registry id="intlRegistry" address="10.20.154.177:9010" default="false" />
<!-- 向中文站注册中心注册 -->
<dubbo:service interface="com.alibaba.hello.api.HelloService" version="1.0.0" ref="helloService" registry="chinaRegistry" />
<!-- 向国际站注册中心注册 -->
<dubbo:service interface="com.alibaba.hello.api.DemoService" version="1.0.0" ref="demoService" registry="intlRegistry" />
</beans>
- 多注册中心引用
比如:CRM 需同时调用中文站和国际站的 PC2 服务,PC2 在中文站和国际站均有部署,接口及版本号都一样,但连的数据库不一样。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
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="world" />
<!-- 多注册中心配置 -->
<dubbo:registry id="chinaRegistry" address="10.20.141.150:9090" />
<dubbo:registry id="intlRegistry" address="10.20.154.177:9010" default="false" />
<!-- 引用中文站服务 -->
<dubbo:reference id="chinaHelloService" interface="com.alibaba.hello.api.HelloService" version="1.0.0" registry="chinaRegistry" />
<!-- 引用国际站服务 -->
<dubbo:reference id="intlHelloService" interface="com.alibaba.hello.api.HelloService" version="1.0.0" registry="intlRegistry" />
</beans>
如果只是测试环境临时需要连接两个不同注册中心,使用竖号分隔多个不同注册中心地址:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
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="world" />
<!-- 多注册中心配置,竖号分隔表示同时连接多个不同注册中心,同一注册中心的多个集群地址用逗号分隔 -->
<dubbo:registry address="10.20.141.150:9090|10.20.154.177:9010" />
<!-- 引用服务 -->
<dubbo:reference id="helloService" interface="com.alibaba.hello.api.HelloService" version="1.0.0" />
</beans>
3.4 集成Nacos实战
参考示例代码/dubbo-samples-spring-boot-nacos
3.4.1 部署单机Nacos
生产环境,参考文档部署高可用Nacos集群,测试环境部署本地单机版Nacos,步骤下
- 下载Nacos2.2.1版本
当Dubbo使用
3.0.0
及以上版本时,需要使用Nacos2.0.0
及以上版本
- 配置单机启动模式
- 配置秘钥
2.0以上Nacos需要配置秘钥
- 启动运行
3.4.2 增加依赖
<dependencies>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>2.2.1</version>
</dependency>
<!-- Introduce Dubbo Nacos extension, or you can add Nacos dependency directly as shown above-->
<!--
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-nacos</artifactId>
<version>3.2.6</version>
</dependency>
-->
</dependencies>
增加 Dubbo 与 Nacos 依赖
3.4.3 配置并启用 Nacos
# application.yml (Spring Boot)
dubbo
registry
address: nacos://${nacos.address:localhost}:8848?username=nacos&password=nacos
3.4.4 高级配置
- 认证
# application.yml (Spring Boot)
dubbo
registry
address: nacos://localhost:8848?username=nacos&password=nacos
- 自定义命名空间
# application.yml (Spring Boot)
dubbo:
registry:
address: nacos://localhost:8848?namespace=5cbb70a5-xxx-xxx-xxx-d43479ae0932
或者
# application.yml (Spring Boot)
dubbo:
registry:
address: nacos://localhost:8848
parameters.namespace: 5cbb70a5-xxx-xxx-xxx-d43479ae0932
3.4.5 自定义分组
# application.yml
dubbo:
registry:
address: nacos://localhost:8848
group: dubbo
如果不配置的话,group 是由 Nacos 默认指定。group 和 namespace 在 Nacos 中代表不同的隔离层次,通常来说 namespace 用来隔离不同的用户或环境,group 用来对同一环境内的数据做进一步归组。
3.4.6 注册接口级消费者
Dubbo3.0.0版本以后,增加了是否注册消费者的参数,如果需要将消费者注册到nacos注册中心上,需要将参数(register-consumer-url)设置为true,默认是false。
# application.yml
dubbo:
registry:
address: nacos://localhost:8848?register-consumer-url=true
或者
# application.yml
dubbo:
registry:
address: nacos://localhost:8848
parameters.register-consumer-url: true
3.4.7 更多配置
参数名 | 中文描述 | 默认值 |
---|---|---|
username | 连接Nacos Server的用户名 | nacos |
paasword | 连接Nacos Server的密码 | nacos |
backup | 备用地址 | 空 |
namespace | 命名空间的ID | public |
group | 分组名称 | DEFAULT_GROUP |
register-consumer-url | 是否注册消费端 | false |
com.alibaba.nacos.naming.log.filename | 初始化日志文件名 | naming.log |
endpoint | 连接Nacos Server指定的连接点,可参考文档 | 空 |
endpointPort | 连接Nacos Server指定的连接点端口,可以参考文档 | 空 |
endpointQueryParams | endpoint查参数询 | 空 |
isUseCloudNamespaceParsing | 是否解析云环境中的namespace参数 | true |
isUseEndpointParsingRule | 是否开启endpoint 参数规则解析 | true |
namingLoadCacheAtStart | 启动时是否优先读取本地缓存 | true |
namingCacheRegistryDir | 指定缓存子目录,位置为 …/nacos/{SUB_DIR}/naming | 空 |
namingClientBeatThreadCount | 客户端心跳的线程池大小 | 机器的CPU数的一半 |
namingPollingThreadCount | 客户端定时轮询数据更新的线程池大小 | 机器的CPU数的一半 |
namingRequestDomainMaxRetryCount | client通过HTTP向Nacos Server请求的重试次数 | 3 |
namingPushEmptyProtection | 在服务没有有效(健康)实例时,是否开启保护,开启后则会使用旧的服务实例 | false |
push.receiver.udp.port | 客户端UDP的端口 | 空 |
在nacos-server@1.0.0
版本后,支持客户端通过上报一些包含特定的元数据的实例到服务端来控制实例的一些行为。
参数名 | 中文描述 | 默认值 |
---|---|---|
preserved.heart.beat.timeout | 该实例在不发送心跳后,从健康到不健康的时间(毫秒) | 15000 |
preserved.ip.delete.timeout | 该实例在不发送心跳后,被服务端下掉该实例的时间(毫秒) | 30000 |
preserved.heart.beat.interval | 该实例在客户端上报心跳的间隔时间(毫秒) | 5000 |
preserved.instance.id.generator | 该实例的id生成策略,值为snowflake | |
时,从0开始增加 | simple | |
preserved.register.source | 注册实例注册时服务框架类型(例如Dubbo,Spring Cloud等) | 空 |
这些参数都可以类似 namespace
的方式通过通过参数扩展配置到 Nacos,如
dubbo.registry.parameters.preserved.heart.beat.timeout=5000
3.4.8 Dubbo 注册数据
随后,重启您的 Dubbo 应用,Dubbo 的服务提供和消费信息在 Nacos 控制台中可以显示:
如图所示,服务名前缀为 providers:
的信息为服务提供者的元信息,consumers:
则代表服务消费者的元信息。点击“详情”可查看服务状态详情:
注:应用级服务发现的 “服务名” 为应用名
Dubbo3 默认采用 “应用级服务发现 + 接口级服务发现” 的双注册模式,因此会发现应用级服务(应用名)和接口级服务(接口名)同时出现在 Nacos 控制台,可以通过配置dubbo.registry.register-mode=instance/interface/all
来改变注册行为。
4 服务配置
有三种配置方式
- Annotation配置
- XML配置
- API配置
4.1 Annotation配置
参考示例/dubbo-samples-spring-boot
在 Dubbo Spring Boot 开发中,你只需要增加几个注解,并配置 application.properties
或 application.yml
文件即可完成 Dubbo 服务定义:
- 注解有
@DubboService
、@DubboReference
与EnableDubbo
。其中@DubboService
与@DubboReference
用于标记 Dubbo 服务,EnableDubbo
启动 Dubbo 相关配置并指定 Spring Boot 扫描包路径。 - 配置文件
application.properties
或application.yml
4.1.1 application.yml
除 service、reference 之外的组件都可以在 application.yml 文件中设置,如果要扩展 service 或 reference 的注解配置,则需要增加 dubbo.properties
配置文件或使用其他非注解如 Java Config 方式,具体请看下文 扩展注解的配置。
service、reference 组件也可以通过 id
与 application 中的全局组件做关联,以下面配置为例:
dubbo:
application:
name: dubbo-springboot-demo-provider
protocol:
name: dubbo
port: -1
registry:
id: zk-registry
address: zookeeper://127.0.0.1:2181
config-center:
address: zookeeper://127.0.0.1:2181
metadata-report:
address: zookeeper://127.0.0.1:2181
通过注解将 service 关联到上文定义的特定注册中心
@DubboService(registry="zk-registry")
public class DemoServiceImpl implements DemoService {}
通过 Java Config 配置进行关联也是同样道理
@Configuration
public class ProviderConfiguration {
@Bean
public ServiceConfig demoService() {
ServiceConfig service = new ServiceConfig();
service.setRegistry("zk-registry");
return service;
}
}
4.1.2 常用注解
- @DubboService 注解
定义好 Dubbo 服务接口后,提供服务接口的实现逻辑,并用 @DubboService
注解标记,就可以实现 Dubbo 的服务暴露
@DubboService
public class DemoServiceImpl implements DemoService {}
如果要设置服务参数,@DubboService
也提供了常用参数的设置方式。如果有更复杂的参数设置需求,则可以考虑使用其他设置方式
@DubboService(version = "1.0.0", group = "dev", timeout = 5000)
public class DemoServiceImpl implements DemoService {}
- @DubboReference 注解
@Component
public class DemoClient {
@DubboReference
private DemoService demoService;
}
@DubboReference
注解将自动注入为 Dubbo 服务代理实例,使用 demoService 即可发起远程服务调用
- @EnableDubbo 注解
@EnableDubbo
注解必须配置,否则将无法加载 Dubbo 注解定义的服务,@EnableDubbo
可以定义在主类上
@SpringBootApplication
@EnableDubbo
public class ProviderApplication {
public static void main(String[] args) throws Exception {
SpringApplication.run(ProviderApplication.class, args);
}
}
Spring Boot 注解默认只会扫描 main 类所在的 package,如果服务定义在其它 package 中,需要增加配置 EnableDubbo(scanBasePackages = {"org.apache.dubbo.springboot.demo.provider"})
4.1.3 扩展注解配置
虽然可以通过 @DubboService
和 DubboReference
调整配置参数(如下代码片段所示),但总体来说注解提供的配置项还是非常有限。在这种情况下,如果有更复杂的参数设置需求,可以使用 Java Config
或 dubbo.properties
两种方式。
@DubboService(version = "1.0.0", group = "dev", timeout = 5000)
@DubboReference(version = "1.0.0", group = "dev", timeout = 5000)
- 使用 Java Config 代替注解
注意,Java Config 是 DubboService
或 DubboReference
的替代方式,对于有复杂配置需求的服务建议使用这种方式。
@Configuration
public class ProviderConfiguration {
@Bean
public ServiceConfig demoService() {
ServiceConfig service = new ServiceConfig();
service.setInterface(DemoService.class);
service.setRef(new DemoServiceImpl());
service.setGroup("dev");
service.setVersion("1.0.0");
Map<String, String> parameters = new HashMap<>();
service.setParameters(parameters);
return service;
}
}
- 通过 dubbo.properties 补充配置
对于使用 DubboService
或 DubboReference
的场景,可以使用 dubbo.properties 作为配置补充,具体格式这里有更详细解释。
dubbo.service.org.apache.dubbo.springboot.demo.DemoService.timeout=5000
dubbo.service.org.apache.dubbo.springboot.demo.DemoService.parameters=[{myKey:myValue},{anotherKey:anotherValue}]
dubbo.reference.org.apache.dubbo.springboot.demo.DemoService.timeout=6000
4.2 XML 配置
参考/dubbo-samples-spring-xml案例
4.2.1 服务提供者
- 定义服务接口
DemoService.java:
package org.apache.dubbo.demo;
public interface DemoService {
String sayHello(String name);
}
- 在服务提供方实现接口
DemoServiceImpl.java:
package org.apache.dubbo.demo.provider;
import org.apache.dubbo.demo.DemoService;
public class DemoServiceImpl implements DemoService {
public String sayHello(String name) {
return "Hello " + name;
}
}
- 用 Spring 配置声明暴露服务
<?xml version="1.0" encoding="UTF-8"?>
<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" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:property-placeholder/>
<dubbo:application name="demo-provider"/>
<dubbo:registry address="zookeeper://${zookeeper.address:127.0.0.1}:2181"/>
<dubbo:provider token="true"/>
<bean id="demoService" class="org.apache.dubbo.samples.basic.impl.DemoServiceImpl"/>
<dubbo:service interface="org.apache.dubbo.samples.basic.api.DemoService" ref="demoService"/>
</beans>
- 加载 Spring 配置
public class Application {
public static void main(String[] args) throws InterruptedException {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/dubbo-demo-provider.xml");
context.start();
System.out.println("dubbo service started");
// to hang up main thread
new CountDownLatch(1).await();
}
}
4.2.2 服务消费者
- 通过 Spring 配置引用远程服务
<?xml version="1.0" encoding="UTF-8"?>
<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" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:property-placeholder/>
<dubbo:application name="demo-consumer"/>
<dubbo:registry address="zookeeper://${zookeeper.address:127.0.0.1}:2181"/>
<dubbo:reference id="demoService" check="true" interface="org.apache.dubbo.samples.basic.api.DemoService"/>
</beans>
- 加载 Spring 配置,并调用远程服务
public class Application {
public static void main(String[] args) throws IOException {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/dubbo-demo-consumer.xml");
context.start();
GreetingsService greetingsService = (GreetingsService) context.getBean("greetingsService");
String message = greetingsService.sayHi("dubbo");
System.out.println("Receive result ======> " + message);
System.in.read();
System.exit(0);
}
}
4.3 API 配置
通过 API 编码方式组装配置、启动 Dubbo、发布及订阅服务。此方式可以支持动态创建 ReferenceConfig/ServiceConfig,结合泛化调用可以满足 API Gateway 或测试平台的需要。
参考 API示例
4.3.1 服务提供者
通过 ServiceConfig 暴露服务接口,发布服务接口到注册中心。
注意:为了更好支持 Dubbo3 应用级服务发现,推荐使用新的 DubboBootstrap API。
import org.apache.dubbo.config.ApplicationConfig;
import org.apache.dubbo.config.RegistryConfig;
import org.apache.dubbo.config.ProviderConfig;
import org.apache.dubbo.config.ServiceConfig;
import com.xxx.DemoService;
import com.xxx.DemoServiceImpl;
public class DemoProvider {
public static void main(String[] args) {
// 服务实现
DemoService demoService = new DemoServiceImpl();
// 当前应用配置
ApplicationConfig application = new ApplicationConfig();
application.setName("demo-provider");
// 连接注册中心配置
RegistryConfig registry = new RegistryConfig();
registry.setAddress("zookeeper://10.20.130.230:2181");
// 服务提供者协议配置
ProtocolConfig protocol = new ProtocolConfig();
protocol.setName("dubbo");
protocol.setPort(12345);
protocol.setThreads(200);
// 注意:ServiceConfig为重对象,内部封装了与注册中心的连接,以及开启服务端口
// 服务提供者暴露服务配置
ServiceConfig<DemoService> service = new ServiceConfig<DemoService>(); // 此实例很重,封装了与注册中心的连接,请自行缓存,否则可能造成内存和连接泄漏
service.setApplication(application);
service.setRegistry(registry); // 多个注册中心可以用setRegistries()
service.setProtocol(protocol); // 多个协议可以用setProtocols()
service.setInterface(DemoService.class);
service.setRef(demoService);
service.setVersion("1.0.0");
// 暴露及注册服务
service.export();
// 挂起等待(防止进程退出)
System.in.read();
}
}
4.3.2 服务消费者
通过 ReferenceConfig 引用远程服务,从注册中心订阅服务接口。
注意:为了更好支持 Dubbo3 应用级服务发现,推荐使用新的 DubboBootstrap API。
import org.apache.dubbo.config.ApplicationConfig;
import org.apache.dubbo.config.RegistryConfig;
import org.apache.dubbo.config.ConsumerConfig;
import org.apache.dubbo.config.ReferenceConfig;
import com.xxx.DemoService;
public class DemoConsumer {
public static void main(String[] args) {
// 当前应用配置
ApplicationConfig application = new ApplicationConfig();
application.setName("demo-consumer");
// 连接注册中心配置
RegistryConfig registry = new RegistryConfig();
registry.setAddress("zookeeper://10.20.130.230:2181");
// 注意:ReferenceConfig为重对象,内部封装了与注册中心的连接,以及与服务提供方的连接
// 引用远程服务
ReferenceConfig<DemoService> reference = new ReferenceConfig<DemoService>(); // 此实例很重,封装了与注册中心的连接以及与提供者的连接,请自行缓存,否则可能造成内存和连接泄漏
reference.setApplication(application);
reference.setRegistry(registry); // 多个注册中心可以用setRegistries()
reference.setInterface(DemoService.class);
reference.setVersion("1.0.0");
// 和本地bean一样使用demoService
// 注意:此代理对象内部封装了所有通讯细节,对象较重,请缓存复用
DemoService demoService = reference.get();
demoService.sayHello("Dubbo");
}
}
4.3.3 Bootstrap API
通过 DubboBootstrap API 可以减少重复配置,更好控制启动过程,支持批量发布/订阅服务接口,还可以更好支持 Dubbo3 的应用级服务发现。
- 服务提供者
import org.apache.dubbo.config.bootstrap.DubboBootstrap;
import org.apache.dubbo.config.ApplicationConfig;
import org.apache.dubbo.config.RegistryConfig;
import org.apache.dubbo.config.ProviderConfig;
import org.apache.dubbo.config.ServiceConfig;
import com.xxx.DemoService;
import com.xxx.DemoServiceImpl;
public class DemoProvider {
public static void main(String[] args) {
ConfigCenterConfig configCenter = new ConfigCenterConfig();
configCenter.setAddress("zookeeper://127.0.0.1:2181");
// 服务提供者协议配置
ProtocolConfig protocol = new ProtocolConfig();
protocol.setName("dubbo");
protocol.setPort(12345);
protocol.setThreads(200);
// 注意:ServiceConfig为重对象,内部封装了与注册中心的连接,以及开启服务端口
// 服务提供者暴露服务配置
ServiceConfig<DemoService> demoServiceConfig = new ServiceConfig<>();
demoServiceConfig.setInterface(DemoService.class);
demoServiceConfig.setRef(new DemoServiceImpl());
demoServiceConfig.setVersion("1.0.0");
// 第二个服务配置
ServiceConfig<FooService> fooServiceConfig = new ServiceConfig<>();
fooServiceConfig.setInterface(FooService.class);
fooServiceConfig.setRef(new FooServiceImpl());
fooServiceConfig.setVersion("1.0.0");
...
// 通过DubboBootstrap简化配置组装,控制启动过程
DubboBootstrap.getInstance()
.application("demo-provider") // 应用配置
.registry(new RegistryConfig("zookeeper://127.0.0.1:2181")) // 注册中心配置
.protocol(protocol) // 全局默认协议配置
.service(demoServiceConfig) // 添加ServiceConfig
.service(fooServiceConfig)
.start() // 启动Dubbo
.await(); // 挂起等待(防止进程退出)
}
}
- 服务消费者
import org.apache.dubbo.config.bootstrap.DubboBootstrap;
import org.apache.dubbo.config.ApplicationConfig;
import org.apache.dubbo.config.RegistryConfig;
import org.apache.dubbo.config.ProviderConfig;
import org.apache.dubbo.config.ServiceConfig;
import com.xxx.DemoService;
import com.xxx.DemoServiceImpl;
public class DemoConsumer {
public static void main(String[] args) {
// 引用远程服务
ReferenceConfig<DemoService> demoServiceReference = new ReferenceConfig<DemoService>();
demoServiceReference.setInterface(DemoService.class);
demoServiceReference.setVersion("1.0.0");
ReferenceConfig<FooService> fooServiceReference = new ReferenceConfig<FooService>();
fooServiceReference.setInterface(FooService.class);
fooServiceReference.setVersion("1.0.0");
// 通过DubboBootstrap简化配置组装,控制启动过程
DubboBootstrap bootstrap = DubboBootstrap.getInstance();
bootstrap.application("demo-consumer") // 应用配置
.registry(new RegistryConfig("zookeeper://127.0.0.1:2181")) // 注册中心配置
.reference(demoServiceReference) // 添加ReferenceConfig
.reference(fooServiceReference)
.start(); // 启动Dubbo
...
// 和本地bean一样使用demoService
// 通过Interface获取远程服务接口代理,不需要依赖ReferenceConfig对象
DemoService demoService = DubboBootstrap.getInstance().getCache().get(DemoService.class);
demoService.sayHello("Dubbo");
FooService fooService = DubboBootstrap.getInstance().getCache().get(FooService.class);
fooService.greeting("Dubbo");
}
}
5 Dubbo进阶
5.1 异步调用
参考示例代码:/dubbo-samples-async-simple-boot
Dubbo异步调用分为Provider端异步调用和Consumer端异步调用。 Provider端异步执行将阻塞的业务从Dubbo内部线程池切换到业务自定义线程, 避免Dubbo线程池的过度占用,有助于避免不同服务间的互相影响。异步执行无异于节省资源或提升RPC响应性能。
注意
Provider 端异步执行和 Consumer 端异步调用是相互独立的,你可以任意正交组合两端配置
- Consumer同步 - Provider同步
- Consumer异步 - Provider同步
- Consumer同步 - Provider异步
- Consumer异步 - Provider异步
使用场景:
- 对于Provider端来说,如果接口比较耗时,避免dubbo线程被阻塞,可以使用异步将线程切换到业务线程。
- 对于Consumer端来说,调用Dubbo接口没有严格时序上的关系、不是原子操作、不影响逻辑情况下可以使用异步调用。
5.1.1 Provider异步
- 使用CompletableFuture实现异步
接口定义:
public interface AsyncService {
/**
* 同步调用方法
*/
String invoke(String param);
/**
* 异步调用方法
*/
CompletableFuture<String> asyncInvoke(String param);
}
服务实现:
@DubboService
public class AsyncServiceImpl implements AsyncService {
@Override
public String invoke(String param) {
try {
long time = ThreadLocalRandom.current().nextLong(1000);
Thread.sleep(time);
StringBuilder s = new StringBuilder();
s.append("AsyncService invoke param:").append(param).append(",sleep:").append(time);
return s.toString();
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return null;
}
@Override
public CompletableFuture<String> asyncInvoke(String param) {
// 建议为supplyAsync提供自定义线程池
return CompletableFuture.supplyAsync(() -> {
try {
// Do something
long time = ThreadLocalRandom.current().nextLong(1000);
Thread.sleep(time);
StringBuilder s = new StringBuilder();
s.append("AsyncService asyncInvoke param:").append(param).append(",sleep:").append(time);
return s.toString();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return null;
});
}
}
通过 return CompletableFuture.supplyAsync() ,业务执行已从 Dubbo 线程切换到业务线程,避免了对 Dubbo 线程池的阻塞。
- 使用AsyncContext实现异步
注:未测试,应该可以
Dubbo 提供了一个类似 Servlet 3.0 的异步接口AsyncContext,在没有 CompletableFuture 签名接口的情况下,也可以实现 Provider 端的异步执行。
接口定义:
public interface AsyncService {
String sayHello(String name);
}
服务实现:
public class AsyncServiceImpl implements AsyncService {
public String sayHello(String name) {
final AsyncContext asyncContext = RpcContext.startAsync();
new Thread(() -> {
// 如果要使用上下文,则必须要放在第一句执行
asyncContext.signalContextSwitch();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 写回响应
asyncContext.write("Hello " + name + ", response from provider.");
}).start();
return null;
}
}
5.1.2 Consumer异步
@DubboReference
private AsyncService asyncService;
@Override
public void run(String... args) throws Exception {
//调用异步接口
CompletableFuture<String> future1 = asyncService.asyncInvoke("async call request1");
future1.whenComplete((v, t) -> {
if (t != null) {
t.printStackTrace();
} else {
System.out.println("AsyncTask Response-1: " + v);
}
});
//两次调用并非顺序返回
CompletableFuture<String> future2 = asyncService.asyncInvoke("async call request2");
future2.whenComplete((v, t) -> {
if (t != null) {
t.printStackTrace();
} else {
System.out.println("AsyncTask Response-2: " + v);
}
});
//consumer异步调用
CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> {
return asyncService.invoke("invoke call request3");
});
future3.whenComplete((v, t) -> {
if (t != null) {
t.printStackTrace();
} else {
System.out.println("AsyncTask Response-3: " + v);
}
});
System.out.println("AsyncTask Executed before response return.");
}
注意结果不是顺序的
5.2 服务版本
参考案例 /version
Dubbo服务中,接口并不能唯一确定一个服务,只有接口+分组+版本号才能唯一确定一个服务。
服务版本使用场景:
- 在低压力时间段,先升级一半提供者为新版本
- 再将所有消费者升级为新版本
- 然后将剩下的一半提供者升级为新版本
当一个接口实现,出现不兼容升级时,可以用版本号过渡,版本号不同的服务相互间不引用。
5.2.1 服务提供者
老版本服务提供者配置
<dubbo:service interface="com.foo.BarService" version="1.0.0" />
新版本服务提供者配置
<dubbo:service interface="com.foo.BarService" version="2.0.0" />
5.2.2 服务消费者
老版本服务消费者配置
<dubbo:reference id="barService" interface="com.foo.BarService" version="1.0.0" />
新版本服务消费者配置
<dubbo:reference id="barService" interface="com.foo.BarService" version="2.0.0" />
如果不需要区分版本,可以按照以下的方式配置
<dubbo:reference id="barService" interface="com.foo.BarService" version="*" />
5.3 服务分组
参考案例/group
同一个接口针对不同的业务场景、不同的使用需求或者不同的功能模块等场景,可使用服务分组来区分不同的实现方式。同时,这些不同实现所提供的服务是可并存的,也支持互相调用。
当一个接口有多种实现时,可以用 group 区分。
使用方式
5.3.1 服务提供端(注解配置)
使用 @DubboService 注解,添加 group 参数
@DubboService(group = "demo")
public class DemoServiceImpl implements DemoService {
...
}
@DubboService(group = "demo2")
public class Demo2ServiceImpl implements DemoService {
...
}
启动 Dubbo 服务,可在注册中心看到相同服务名不同分组的服务,以 Nacos 作为注册中心为例,显示如下内容:
5.3.2 服务消费端(注解配置)
使用 @DubboReference 注解,添加 group 参数
@DubboReference(group = "demo")
private DemoService demoService;
@DubboReference(group = "demo2")
private DemoService demoService2;
//group值为*,标识匹配任意服务分组
@DubboReference(group = "*")
private DemoService demoService2;
5.3.3 分组聚合
参考案例 /dubbo-samples-merge
// 分组聚合,对所有分组进行merge后返回
@DubboReference(group = "*", merger = "true")
private DemoService demoService2;
// 分组聚合,对指定分组进行merge后返回
@DubboReference(group = "merge,merge2", merger = "true")
private DemoService demoService2;
同样启动 Dubbo 服务后,可在注册中心看到相同服务名不同分组的引用者,以 Nacos 作为注册中心为例,显示如下内容:
执行结果,显示多个分组觉和的结果
5.4 RPC调用上下文
- 简介
上下文中存放的是当前调用过程中所需的环境信息。所有配置信息都将转换为 URL 的参数,参见 schema 配置参考手册 中的对应URL参数一列。
RpcContext 是一个 ThreadLocal 的临时状态记录器,当接收到 RPC 请求,或发起 RPC 请求时,RpcContext 的状态都会变化。比如:A 调 B,B 再调 C,则 B 机器上,在 B 调 C 之前,RpcContext 记录的是 A 调 B 的信息,在 B 调 C 之后,RpcContext 记录的是 B 调 C 的信息。
- 使用场景
全局链路追踪和隐藏参数。
- 用法
服务消费方
// 远程调用
xxxService.xxx();
// 本端是否为消费端,这里会返回true
boolean isConsumerSide = RpcContext.getServiceContext().isConsumerSide();
// 获取最后一次调用的提供方IP地址
String serverIP = RpcContext.getServiceContext().getRemoteHost();
// 获取当前服务配置信息,所有配置信息都将转换为URL的参数
String application = RpcContext.getServiceContext().getUrl().getParameter("application");
// 注意:每发起RPC调用,上下文状态会变化
yyyService.yyy();
服务提供方
public class XxxServiceImpl implements XxxService {
public void xxx() {
// 本端是否为提供端,这里会返回true
boolean isProviderSide = RpcContext.getServiceContext().isProviderSide();
// 获取调用方IP地址
String clientIP = RpcContext.getServiceContext().getRemoteHost();
// 获取当前服务配置信息,所有配置信息都将转换为URL的参数
String application = RpcContext.getServiceContext().getUrl().getParameter("application");
// 注意:每发起RPC调用,上下文状态会变化
yyyService.yyy();
// 此时本端变成消费端,这里会返回false
boolean isProviderSide = RpcContext.getServiceContext().isProviderSide();
}
}
5.5 调用链路传递隐式参数
本小节参考案例/dubbo-samples-spring-boot-attachment
内部系统通过 Dubbo 调用时, traceId 如何透传到服务提供方。
参考上一小节调用上下文使用
可以通过 RpcContext
上的 setAttachment
和 getAttachment
在服务消费方和提供方之间进行参数的隐式传递。
5.5.1 隐式参数传递详解
上下文信息是 RPC 框架很重要的一个功能,使用 RpcContext 可以为单次调用指定不同配置。如分布式链路追踪场景,其实现原理就是在全链路的上下文中维护一个 traceId,Consumer 和 Provider 通过传递 traceId 来连接一次RPC调用,分别上报日志后可以在追踪系统中串联并展示完整的调用流程。这样可以更方便地发现异常,定位问题。 Dubbo 中的 RpcContext 是一个 ThreadLocal 的临时状态记录器,当接收到 RPC 请求,或发起 RPC 请求时,RpcContext 的状态都会变化。比如:A 调 B,B 调 C,则 B 机器上,在 B 调 C 之前,RpcContext 记录的是 A 和 B 的信息,在 B 调 C 之后,RpcContext 记录的是 B 和 C 的信息。
在 Dubbo 3 中,RpcContext 被拆分为四大模块(ServerContext、ClientAttachment、ServerAttachment 和 ServiceContext)。
它们分别承担了不同的职责:
- ServiceContext:在 Dubbo 内部使用,用于传递调用链路上的参数信息,如 invoker 对象等
- ClientAttachment:在 Client 端使用,往 ClientAttachment 中写入的参数将被传递到 Server 端
- ServerAttachment:在 Server 端使用,从 ServerAttachment 中读取的参数是从 Client 中传递过来的
- ServerContext:在 Client 端和 Server 端使用,用于从 Server 端回传 Client 端使用,Server 端写入到 ServerContext 的参数在调用结束后可以在 Client 端的 ServerContext 获取到
如上图所示,
- 消费端发起调用的时候可以直接通过 Method Invoke 向远程的服务发起调用,
- 同时消费端往 RpcClientAttachment 写入的数据会连同 Invoke 的参数信息写入到 Invocation 中。
- 消费端的 Invocation 经过序列化后通过网络传输发送给服务端,服务端解析 Invocation 生成 Method Invoke 的参数和 RpcServerAttachment,然后发起真实调用。
- 在服务端处理结束之后,Method Response 结果会连同 RpcServiceContext 一起生成 Result 对象。 服务端的 Result 结果对象经过序列化后通过网络传输发送回消费端,消费端解析 Result 生成 Method Response 结果和 RpcServiceContext,返回真实调用结果和上下文给消费端。
注意:path, group, version, dubbo, token, timeout 几个 key 是保留字段,请使用其它值。
5.5.2 隐式参数传递实现步骤
setAttachment
设置的 KV 对,在完成下面一次远程调用会被清空,即多次远程调用要多次设置。
5.5.2.1 在服务消费方端设置隐式参数
RpcContext.getClientAttachment().setAttachment("index", "1"); // 隐式传参,后面的远程调用都会隐式将这些参数发送到服务器端,类似cookie,用于框架集成,不建议常规业务使用
xxxService.xxx(); // 远程调用
// ...
5.5.2.2 在服务提供方端获取隐式参数
public class XxxServiceImpl implements XxxService {
public void xxx() {
// 获取客户端隐式传入的参数,用于框架集成,不建议常规业务使用
String index = RpcContext.getServerAttachment().getAttachment("index");
}
}
5.5.2.3 在服务提供方写入回传参数
public class XxxServiceImpl implements XxxService {
public void xxx() {
String index = xxx;
RpcContext.getServerContext().setAttachment("result", index);
}
}
5.5.2.4 在消费端获取回传参数
xxxService.xxx(); // 远程调用
String result = RpcContext.getServerContext().getAttachment("result");
// ...
5.5.2.5 过滤器中注意的问题
新版本中我们的建议是 在 Filter 里面的尽可能不要操作 RpcContext,上面的使用方式会导致不生效。原因在于新版本中,我们在ConsumerContextFilter
类中做了ClientAttachment
-> Invocation
属性的复制,该类是Dubbo内置Filter类,而内置Filter类先于用户定义Filter类执行,所以在自定义Filter类中这样使用不会生效。 可以直接使用这种方式进行传递:
@Activate(group = {CommonConstants.CONSUMER})
public class DubboConsumerFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
invocation.setAttachment("demo","demo02");
return invoker.invoke(invocation);
}
}
5.6 本地存根和伪装
本小节案例代码参考/mock-stub-demo
远程服务后,客户端通常只剩下接口,而实现全在服务器端,但提供方有些时候想在客户端也执行部分逻辑。
5.6.1 本地存根
- 使用场景
做 ThreadLocal 缓存,提前验证参数,调用失败后伪造容错数据等等,此时就需要在 API 中带上 Stub,客户端生成 Proxy 实例,会把 Proxy 通过构造函数传给 Stub 1,然后把 Stub 暴露给用户,Stub 可以决定要不要去调 Proxy。
- 使用方式
spring 配置文件配置
<dubbo:consumer interface="com.foo.BarService" stub="true" />
或
<dubbo:consumer interface="com.foo.BarService" stub="com.foo.BarServiceStub" />
提供 Stub 的实现
package com.foo;
public class BarServiceStub implements BarService {
private final BarService barService;
// 构造函数传入真正的远程代理对象
public BarServiceStub(BarService barService){
this.barService = barService;
}
public String sayHello(String name) {
// 此代码在客户端执行, 你可以在客户端做ThreadLocal本地缓存,或预先验证参数是否合法,等等
try {
return barService.sayHello(name);
} catch (Exception e) {
// 你可以容错,可以做任何AOP拦截事项
return "容错数据";
}
}
}
- Stub 必须有可传入 Proxy 的构造函数。 ︎
- 在 interface 旁边放一个 Stub 实现,它实现 BarService 接口,并有一个传入远程 BarService 实例的构造函数。
5.6.2 本地伪装
在 Dubbo3 中有一种机制可以实现轻量级的服务降级,也就是本地伪装。
Mock 是 Stub 的一个子集,便于服务提供方在客户端执行容错逻辑,因经常需要在出现 RpcException (比如网络失败,超时等)时进行容错,而在出现业务异常(比如登录用户名密码错误)时不需要容错, 如果用 Stub,可能就需要捕获并依赖 RpcException 类,而用 Mock 就可以不依赖 RpcException,因为它的约定就是只有出现 RpcException 时才执行。
- 使用场景
本地伪装常被用于服务降级。比如某验权服务,当服务提供方全部挂掉后,假如此时服务消费方发起了一次远程调用,那么本次调用将会失败并抛出一个 RpcException
异常。
为了避免出现这种直接抛出异常的情况出现,那么客户端就可以利用本地伪装来提供 Mock 数据返回授权失败。
- 开启 Mock 配置
在 Spring XML 配置文件中按以下方式配置:
<dubbo:reference interface="com.foo.BarService" mock="true" />
或
<dubbo:reference interface="com.foo.BarService" mock="com.foo.BarServiceMock" />
在工程中提供 Mock 实现 [^2]: 在 interface 旁放一个 Mock 实现,它实现 BarService 接口,并有一个无参构造函数。同时,如果没有在配置文件中显式指定 Mock 类的时候,那么需要保证 Mock 类的全限定类名是 原全限定类名+Mock
的形式,例如 com.foo.BarServiceMock
,否则将会 Mock 失败。
package com.foo;
public class BarServiceMock implements BarService {
public String sayHello(String name) {
// 你可以伪造容错数据,此方法只在出现RpcException时被执行
return "容错数据";
}
}
- 使用 return 关键字 Mock 返回值
使用 return
来返回一个字符串表示的对象,作为 Mock 的返回值。合法的字符串可以是:
- empty:代表空,返回基本类型的默认值、集合类的空值、自定义实体类的空对象,如果返回值是一个实体类,那么此时返回的将会是一个属性都为默认值的空对象而不是
null
。 - null:返回
null
- true:返回
true
- false:返回
false
- JSON 字符串:返回反序列化 JSON 串后所得到的对象
举个例子,如果服务的消费方经常需要 try-catch 捕获异常,如:
public class DemoService {
public Offer findOffer(String offerId) {
Offer offer = null;
try {
offer = offerService.findOffer(offerId);
} catch (RpcException e) {
logger.error(e);
}
return offer;
}
}
那么请考虑改为 Mock 实现,并在 Mock 实现中 return null
。如果只是想简单的忽略异常,在 2.0.11
以上版本可用:
<dubbo:reference interface="com.foo.BarService" mock="return null" />
- 使用 throw 关键字 Mock 抛出异常
使用 throw
来返回一个 Exception 对象,作为 Mock 的返回值。
当调用出错时,抛出一个默认的 RPCException:
<dubbo:reference interface="com.foo.BarService" mock="throw"/>
当调用出错时,抛出指定的 Exception:
自定义异常必须拥有一个入参为 String
的构造函数,该构造函数将用于接受异常信息。
<dubbo:reference interface="com.foo.BarService" mock="throw com.foo.MockException"/>
- 使用 force 和 fail 关键字来配置 Mock 的行为
force:
代表强制使用 Mock 行为,在这种情况下不会走远程调用。
fail:
与默认行为一致,只有当远程调用发生错误时才使用 Mock 行为。也就是说,配置的时候其实是可以不使用 fail
关键字的,直接使用 throw
或者 return
就可以了。
force:
和 fail:
都支持与 throw
或者 return
组合使用。
强制返回指定值:
<dubbo:reference interface="com.foo.BarService" mock="force:return fake"/>
强制抛出指定异常:
<dubbo:reference interface="com.foo.BarService" mock="force:throw com.foo.MockException"/>
调用失败时返回指定值:
<dubbo:reference interface="com.foo.BarService" mock="fail:return fake"/>
<!-- 等价于以下写法 -->
<dubbo:reference interface="com.foo.BarService" mock="return fake"/>
调用失败时抛出异常
<dubbo:reference interface="com.foo.BarService" mock="fail:throw com.foo.MockException"/>
<!-- 等价于以下写法 -->
<dubbo:reference interface="com.foo.BarService" mock="throw com.foo.MockException"/>
- 在方法级别配置 Mock
Mock 可以在方法级别上指定,假定 com.foo.BarService
上有好几个方法,我们可以单独为 sayHello()
方法指定 Mock 行为。
具体配置如下所示,在本例中,只要 sayHello()
被调用到时,强制返回 “fake”:
<dubbo:reference id="demoService" check="false" interface="com.foo.BarService">
<dubbo:parameter key="sayHello.mock" value="force:return fake"/>
</dubbo:reference>
5.7 Filter
通过自定义过滤器,可以对返回的结果进行统一的处理、验证等,减少对开发人员的打扰。
案例常见过滤器对所有调用Provider服务的请求在返回的结果的后面统一打印日志
在Provider中自定义一个Filter,在Filter中打印日志
- 代码结构
src
|-main
|-java
|-org
|-apache
|-dubbo
|-samples
|-extensibility
|-filter
|-provider
|-AppendedFilter.java (实现Filter接口)
|-resources
|-META-INF
|-application.properties (Dubbo Provider配置文件)
|-dubbo
|-org.apache.dubbo.rpc.Filter (纯文本文件)
- 代码详情
@Slf4j
public class AppendedFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
log.info("开始调用,接口:{},参数:{}", invocation.getMethodName(), JSON.toJSONString(invocation.getArguments()));
Result result = invoker.invoke(invocation);
log.info("调用完成,结果:{}", JSON.toJSONString(result.getValue()));
return result;
}
}
- SPI配置
在resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter
文件中添加如下配置:
appended=org.apache.dubbo.samples.extensibility.filter.provider.AppendedFilter
- 配置文件
在resources/application.properties
文件中添加如下配置:
# Apply AppendedFilter
dubbo.provider.filter=appended
6 Triple 协议
6.1 Dubbo的通信协议
Dubbo 框架提供了自定义的高性能 RPC 通信协议:基于 HTTP/2 的 Triple 协议 和 基于 TCP 的 Dubbo2 协议。除此之外,Dubbo 框架支持任意第三方通信协议,如官方支持的 gRPC、Thrift、REST、JsonRPC、Hessian2 等,更多协议可以通过自定义扩展实现。这对于微服务实践中经常要处理的多协议通信场景非常有用。
Dubbo 框架不绑定任何通信协议,在实现上 Dubbo 对多协议的支持也非常灵活,它可以让你在一个应用内发布多个使用不同协议的服务,并且支持用同一个 port 端口对外发布所有协议。
通过 Dubbo 框架的多协议支持,你可以做到:
- 将任意通信协议无缝地接入 Dubbo 服务治理体系。Dubbo 体系下的所有通信协议,都可以享受到 Dubbo 的编程模型、服务发现、流量管控等优势。比如 gRPC over Dubbo 的模式,服务治理、编程 API 都能够零成本接入 Dubbo 体系。
- 兼容不同技术栈,业务系统混合使用不同的服务框架、RPC 框架。比如有些服务使用 gRPC 或者 Spring Cloud 开发,有些服务使用 Dubbo 框架开发,通过 Dubbo 的多协议支持可以很好的实现互通。
- 让协议迁移变的更简单。通过多协议、注册中心的协调,可以快速满足公司内协议迁移的需求。比如如从自研协议升级到 Dubbo 协议,Dubbo 协议自身升级,从 Dubbo 协议迁移到 gRPC,从 HTTP 迁移到 Dubbo 协议等。
- HTTP/2 (Triple)
Triple 协议是 Dubbo3 发布的面向云原生时代的通信协议,它基于 HTTP/2 并且完全兼容 gRPC 协议,原生支持 Streaming 通信语义,Triple 可同时运行在 HTTP/1 和 HTTP/2 传输协议之上,让你可以直接使用 curl、浏览器访问后端 Dubbo 服务。
自 Triple 协议开始,Dubbo 还支持基于 Protocol Buffers 的服务定义与数据传输,但 Triple 实现并不绑定 IDL,比如你可以直接使用 Java Interface 定义和发布 Triple 服务。Triple 具备更好的网关、代理穿透性,因此非常适合于跨网关、代理通信的部署架构,如服务网格等。
Triple 协议的核心特性如下:
- 支持 TLS 加密、Plaintext 明文数据传输
- 支持反压与限流
- 支持 Streaming 流式通信
- 同时支持 HTTP/1 和 HTTP/2 传输协议
在编程与通信模型上,Triple 协议支持如下模式:
- 消费端异步请求(Client Side Asynchronous Request-Response)
- 提供端异步执行(Server Side Asynchronous Request-Response)
- 消费端请求流(Request Streaming)
- 提供端响应流(Response Streaming)
- 双向流式通信(Bidirectional Streaming)
开发实践
- Triple 协议使用请参见 Triple 协议开发任务 或 java sdk 示例文档
- Triple 设计思路与协议规范
- Dubbo2
Dubbo2 协议是基于 TCP 传输层协议之上构建的一套 RPC 通信协议,由于其紧凑、灵活、高性能的特点,在 Dubbo2 时代取得了非常广泛的应用,是企业构建高性能、大规模微服务集群的关键通信方案。在云原生时代,我们更推荐使用通用性、穿透性更好的 Triple 协议。
Dubbo2 协议也内置 HTTP 支持,因此你可以使用 curl 在开发阶段快速验证或调试服务。
- gRPC
你可以用 Dubbo 开发和治理微服务,然后设置使用 gRPC 协议进行底层通信。但为什么要这么做呢,与直接使用 gRPC 框架对比有什么优势?简单的答案是,这是使用 gRPC 进行微服务开发的常用模式,具体请往下看。
gRPC 是谷歌开源的基于 HTTP/2 的通信协议,如同我们在 产品对比 文档中提到的,gRPC 的定位是通信协议与实现,是一款纯粹的 RPC 框架,而 Dubbo 定位是一款微服务框架,为微服务实践提供解决方案。因此,相比于 Dubbo,gRPC 相对欠缺了微服务编程模型、服务治理等能力的抽象。
在 Dubbo 体系下使用 gRPC 协议 (gRPC over Dubbo Framework) 是一个非常高效和轻量的选择,它让你既能使用原生的 gRPC 协议通信,又避免了基于 gRPC 进行二次定制与开发的复杂度 (二次开发与定制 gRPC,是很多企业规模化实践后证实不可避免的环节,Dubbo 框架替开发者完成了这一步,让开发者可以直接以最简单的方式使用 gRPC)。
- REST
微服务领域常用的一种通信模式是 HTTP + JSON,包括 Spring Cloud、Microprofile 等一些主流的微服务框架都默认使用的这种通信模式,Dubbo 同样提供了对基于 HTTP 的编程、通信模式的支持。
- 其他通信协议
除了以上介绍的几种协议之外,你还可以将以下协议运行在 Dubbo 之上。对 Dubbo 而言,只需要修改一行简单的配置,就可以切换底层服务的通信协议,其他外围 API 和治理能力不受影响。
- Hessian2
- Thrift
- JsonRPC
6.2 Triple协议简介
Triple 是 Dubbo3 提出的基于 HTTP 的开放协议,旨在解决 Dubbo2 私有协议带来的互通性问题,Triple 基于 gRPC 和 gRPC-Web 设计而来,保留了两者的优秀设计,Triple 做到了完全兼容 gRPC 协议,并可同时运行在 HTTP/1 和 HTTP/2 之上。
- 相比于原有 Dubbo2 协议,Triple 有以下优势:
- 原生和 gRPC 协议互通。打通 gRPC 生态,降低从 gRPC 至 Dubbo 的迁移成本。
- 增强多语言生态。避免因 CPP/C#/RUST 等语言的 Dubbo SDK 能力不足导致业务难以选型适配的问题。
- 网关友好。网关无需参与序列化,方便用户从传统的 HTTP 转泛化 Dubbo 调用网关升级至开源或云厂商的 Ingress 方案。
- 完善的异步和流式支持。带来从底层协议到上层业务的性能提升,易于构建全链路异步以及严格保证消息顺序的流式服务。
- 相比于 gRPC 协议,Triple 有以下优势:
- 协议内置支持 HTTP/1,可以用 curl、浏览器直接访问你的 gRPC 服务
- 保持性能与 grpc-java 在同一水平的同时,实现上更轻量、简单,协议部分只有几千行代码
- 不绑定 IDL,支持 Java Interface 定义服务
- 保持与官方 gRPC 库的 100% 兼容性的同时,与 Dubbo 的微服务治理体系无缝融合
更多关于 Triple 协议设计与协议规范,请参考 triple协议规范。 目前 Java 和 Go 的 Dubbo SDK 已全面支持 Triple 协议。 在阿里巴巴,Triple 协议广泛用于跨环境、跨语言、跨生态互通,已有数十万容器生产级使用。
6.2.1 Triple协议通信模式
- Unary(单次请求/响应) :这是最基本的通信模式,客户端发送一个请求,服务端响应一次,然后连接关闭。这种模式适合于需要快速响应的场景,每个请求都有一个明确的响应,不会持续保持连接。
- Streaming:Streaming模式支持大文件、大数据的传输,或者直播流推送等需要持续数据传输的场景。Streaming模式进一步细分为Server-Stream(服务端流)、Client-Stream(客户端流)和Bi-Stream(双向流),它们各自有不同的特点和适用场景:
- Server-Stream:服务端可以持续发送数据给客户端,而客户端只能接收数据,不能发送数据给服务端。这种模式适合服务器向客户端推送大量数据的情况。
- Client-Stream:与Server-Stream相反,客户端可以持续发送数据给服务端,而服务端只能接收数据。这种模式适合客户端需要上传大量数据到服务器的情况。
- Bi-Stream:客户端和服务端都可以发送和接收数据,这种模式适用于需要双向实时通信的场景。
总的来说,Unary模式适用于简单的请求-响应场景,而Streaming模式则更适合需要持续数据传输或大数据量传输的复杂场景。Dubbo3通过支持多种Streaming模式,提供了更大的灵活性来满足不同的应用需求
6.2.2 Streaming 通信模式
Stream 是 Dubbo3 新提供的一种调用类型,在以下场景时建议使用流的方式:
- 接口需要发送大量数据,这些数据无法被放在一个 RPC 的请求或响应中,需要分批发送,但应用层如果按照传统的多次 RPC 方式无法解决顺序和性能的问题,如果需要保证有序,则只能串行发送
- 流式场景,数据需要按照发送顺序处理, 数据本身是没有确定边界的
- 推送类场景,多个消息在同一个调用的上下文中被发送和处理
Stream 分为以下三种:
- SERVER_STREAM(服务端流)
- CLIENT_STREAM(客户端流)
- BIDIRECTIONAL_STREAM(双向流)
由于
java
语言的限制,BIDIRECTIONAL_STREAM 和 CLIENT_STREAM 的实现是一样的。
在 Dubbo3 中,流式接口以 SteamObserver
声明和使用,用户可以通过使用和实现这个接口来发送和处理流的数据、异常和结束。
6.2.3 协议设计理念
Triple 协议的设计参考了 gRPC、gRPC-Web、通用 HTTP 等多种协议模式,吸取每个协议各自的特性和优点,最终设计成为一个易于浏览器访问、完全兼容 gRPC 且支持 Streaming 通信的协议,Triple 支持同时运行在 HTTP/1、HTTP/2 协议之上。
Triple 协议的设计目标如下:
- Triple 设计为对人类、开发调试友好的一款基于 HTTP 的协议,尤其是对 unary 类型的 RPC 请求。
- 完全兼容基于 HTTP/2 的 gRPC 协议,因此 Dubbo Triple 协议实现可以 100% 与 gRPC 体系互调互通。
- 仅依赖标准的、被广泛使用的 HTTP 特性,以便在实现层面可以直接依赖官方的标准 HTTP 网络库。
当与 Protocol Buffers 一起使用时(即使用 IDL 定义服务),Triple 协议可支持 unary、client-streaming、server-streaming 和 bi-streaming RPC 通信模式,支持二进制 Protobuf、JSON 两种数据格式 payload。 Triple 实现并不绑定 Protocol Buffers,比如你可以使用 Java 接口定义服务,Triple 协议有对这种模式的扩展 Content-type 支持。
6.2.4 Unary 请求示例
Unary 是 Triple 协议的一部分,用于处理单次请求和响应的场景
以 HTTP/1 请求为例,目前 HTTP/1 协议仅支持 Unary RPC,支持使用 application/proto 和 application/json 编码类型,使用方式与 REST 风格请求保持一致,同时响应也包含常规的 HTTP 响应编码(如 200 OK)。
> POST /org.apache.dubbo.demo.GreetService/Greet HTTP/1.1
> Host: 127.0.0.1:30551
> Content-Type: application/json
>
> ["Dubbo"]
< HTTP/1.1 200 OK
< Content-Type: application/json
<
< {"greeting": "Hello, Dubbo!"}
一个包含指定超时时间的调用请求。
> POST /org.apache.dubbo.demo.GreetService/Greet HTTP/1.1
> Host: 127.0.0.1:30551
> Content-Type: application/json
> Rest-service-timeout: 5000
>
> ["Dubbo"]
< HTTP/1.1 200 OK
< Content-Type: application/json
<
< {"greeting": "Hello, Buf!"}
目前仅支持 POST 请求类型,我们将考虑在未来支持 GET 请求类型,GET 请求可能适用于具有幂等属性的一些服务调用。
6.2.5 Streaming 调用请求示例
Streaming 是 Dubbo Triple 协议的一部分,支持流式通信。这允许客户端和服务端进行双向流通信,即客户端和服务端可以同时发送和接收多个消息。适用于需要长时间保持连接并处理大量数据的场景,比如实时数据处理和大文件传输
Triple 仅支持在 HTTP/2 上支持 Streaming RPC。并且为了与 gRPC 协议保持兼容,Triple 在 HTTP/2 协议实现上(包含 Streaming RPC)保持与标准 gRPC 协议完全一致。
Request
HEADERS (flags = END_HEADERS)
:method = POST
:scheme = http
:path = /google.pubsub.v2.PublisherService/CreateTopic
:authority = pubsub.googleapis.com
grpc-timeout = 1S
content-type = application/grpc+proto
grpc-encoding = gzip
authorization = Bearer y235.wef315yfh138vh31hv93hv8h3v
DATA (flags = END_STREAM)
<Length-Prefixed Message>
Response
HEADERS (flags = END_HEADERS)
:status = 200
grpc-encoding = gzip
content-type = application/grpc+proto
DATA
<Length-Prefixed Message>
HEADERS (flags = END_STREAM, END_HEADERS)
grpc-status = 0 # OK
trace-proto-bin = jher831yy13JHy3hc
6.3 Triple协议实战
参考案例/triple
Triple 协议是 Dubbo3 的主力协议,完整兼容 gRPC over HTTP/2,并在协议层面扩展了负载均衡和流量控制相关机制。本文档旨在指导用户正确的使用 Triple 协议。
在开始前,需要决定服务使用的序列化方式,如果为新服务,推荐使用 protobuf 作为默认序列化,在性能和跨语言上的效果都会更好。如果是原有服务想进行协议升级,Triple 协议也已经支持其他序列化方式,如 Hessian / JSON 等
6.3.1 Pojo 序列化兼容模式开发
此模式下 Triple 使用方式与 Dubbo2 协议一样
- 编写 Java 接口
import org.apache.dubbo.hello.HelloReply;
import org.apache.dubbo.hello.HelloRequest;
public interface IGreeter {
/**
* <pre>
* Sends a greeting
* </pre>
*/
HelloReply sayHello(HelloRequest request);
}
- 创建 Provider
public static void main(String[] args) throws InterruptedException {
ServiceConfig<IGreeter> service = new ServiceConfig<>();
service.setInterface(IGreeter.class);
service.setRef(new IGreeter1Impl());
// 这里需要显示声明使用的协议为triple
service.setProtocol(new ProtocolConfig(CommonConstants.TRIPLE, 50051));
service.setApplication(new ApplicationConfig("demo-provider"));
service.setRegistry(new RegistryConfig("zookeeper://127.0.0.1:2181"));
service.export();
System.out.println("dubbo service started");
new CountDownLatch(1).await();
}
- 创建 Consumer
public static void main(String[] args) throws IOException {
ReferenceConfig<IGreeter> ref = new ReferenceConfig<>();
ref.setInterface(IGreeter.class);
ref.setCheck(false);
ref.setProtocol(CommonConstants.TRIPLE);
ref.setLazy(true);
ref.setTimeout(100000);
ref.setApplication(new ApplicationConfig("demo-consumer"));
ref.setRegistry(new RegistryConfig("zookeeper://127.0.0.1:2181"));
final IGreeter iGreeter = ref.get();
System.out.println("dubbo ref started");
try {
final HelloReply reply = iGreeter.sayHello(HelloRequest.newBuilder()
.setName("name")
.build());
TimeUnit.SECONDS.sleep(1);
System.out.println("Reply:" + reply);
} catch (Throwable t) {
t.printStackTrace();
}
System.in.read();
}
- 运行 Provider 和 Consumer ,可以看到请求正常返回
> Reply:message: "name"
6.3.2 使用 IDL + Protobuf 跨语言定义服务
- 编写 IDL 文件
syntax = "proto3";
option java_multiple_files = true;
package org.apache.dubbo.springboot.demo.provider;
message GreeterRequest {
string name = 1;
}
message GreeterReply {
string message = 1;
}
service Greeter{
rpc biStream(stream GreeterRequest) returns (stream GreeterReply);
rpc serverStream(GreeterRequest) returns (stream GreeterReply);
}
- 添加编译 protobuf 的 extension 和 plugin (以 maven 为例)
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.6.1</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}</protocArtifact>
<protocPlugins>
<protocPlugin>
<id>dubbo</id>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-compiler</artifactId>
<version>${dubbo.version}</version>
<mainClass>org.apache.dubbo.gen.tri.Dubbo3TripleGenerator</mainClass>
</protocPlugin>
</protocPlugins>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>target/generated-sources/protobuf/java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
- 构建/ 编译生成 protobuf Message 类
mvn clean install
6.3.3 stream调用模式开发
本小节参考代码/triple
- 编写实现类
@DubboService
public class GreeterImpl extends DubboGreeterTriple.GreeterImplBase {
private static final Logger LOGGER = LoggerFactory.getLogger(GreeterImpl.class);
@Override
public StreamObserver<GreeterRequest> biStream(StreamObserver<GreeterReply> responseObserver) {
return new StreamObserver<GreeterRequest>() {
@Override
public void onNext(GreeterRequest data) {
GreeterReply resp = GreeterReply.newBuilder().setMessage("reply from biStream " + data.getName()).build();
responseObserver.onNext(resp);
}
@Override
public void onError(Throwable throwable) {
}
@Override
public void onCompleted() {
}
};
}
@Override
public void serverStream(GreeterRequest request, StreamObserver<GreeterReply> responseObserver) {
LOGGER.info("receive request: {}", request.getName());
for (int i = 0; i < 10; i++) {
GreeterReply reply = GreeterReply.newBuilder().setMessage("reply from serverStream. " + i).build();
responseObserver.onNext(reply);
}
responseObserver.onCompleted();
}
}
- 创建 Consumer
@Component
public class TaskStream implements CommandLineRunner {
private static final Logger LOGGER = LoggerFactory.getLogger(TaskStream.class);
@DubboReference
private Greeter greeter;
@Override
public void run(String... args) throws Exception {
// //server stream
// serverStream(greeter);
//bi stream
biStream(greeter);
}
private static void biStream(Greeter greeter) {
StreamObserver<GreeterRequest> requestStreamObserver = greeter.biStream(new SampleStreamObserver());
for (int i = 0; i < 10; i++) {
GreeterRequest request = GreeterRequest.newBuilder().setName("name-" + i).build();
requestStreamObserver.onNext(request);
}
requestStreamObserver.onCompleted();
}
private static void serverStream(Greeter greeter) {
GreeterRequest request = GreeterRequest.newBuilder().setName("server stream request.").build();
greeter.serverStream(request, new SampleStreamObserver());
}
private static class SampleStreamObserver implements StreamObserver<GreeterReply> {
@Override
public void onNext(GreeterReply data) {
LOGGER.info("stream <- reply:{}", data);
}
@Override
public void onError(Throwable throwable) {
LOGGER.error("stream onError", throwable);
throwable.printStackTrace();
}
@Override
public void onCompleted() {
LOGGER.info("stream completed");
}
}
}
7 Dubbo3.0 整合 Spring Cloud Gateway
7.1 Dubbo3.0 整合 Spring Cloud Gateway 的背景
在微服务架构中 微服务网关 非常重要,
微服务网关作为全局流量入口并不单单是一个反向路由,更多的是把各个边缘服务(Web层)的各种共性需求抽取出来放在一个公共的“服务”(网关)中实现,例如安全认证、权限控制、限流熔断、监控、跨域处理、聚合API文档等公共功能。
微服务 网关是微服务架构中的一个关键的角色,用来保护、增强和控制对于微服务的访问。
下面是微服务网关的主要作用:
SpringCloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。
SpringCloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Zuul,在Spring Cloud 2.0以上版本中,没有对新版本的Zuul 2.0以上最新高性能版本进行集成,仍然还是使用的Zuul 2.0之前的非Reactor模式的老版本。而为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。
Spring Cloud Gateway 的目标,不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。
特别说明:
Spring Cloud Gateway 底层使用了高性能的通信框架Netty。Netty 是高性能中间件的通讯底座, rocketmq 、seata、nacos 、sentinel 、redission 、dubbo 等太多、太多的的大名鼎鼎的中间件,无一例外都是基于netty。
可以毫不夸张的说: netty 是进入大厂、走向高端 的必备技能。
要想深入了解springcloud gateway ,最好是掌握netty 编程。
有关 netty学习,请参见 全网最为塔尖 的netty 三部曲视频: 《从netty 实操,到netty源码,到netty 两池架构》
在 Dubbo 框架体系,没有提供 对前端的 api 网关,
Dubbo服务,要给前端调用有两种方式:
- 方式一: 增加 bff 层, 通过Controller 调用dubbo, 对外暴露 Controller 的rest 接口
- 方式二: 增强API网关,让SpringCloud Gateway 能够进行 dubbo 协议的 转发和路由
问题:传统 dubbo 架构并没有提供http入口给客户端调用
首先看问题。
dubbo属于rpc调用,所以必须提供一个 bff web层的服务作为http入口给客户端调用,并在上面提供安全认证等基础功能,而web层前面对接Nginx等反向代理用于统一入口和负载均衡。
web层一般是根据业务模块来切分的,用于聚合某个业务模块所依赖的各个service服务
一般来说,我们把上图中的web层全部整合在一起,成为一个BFF层。
这就变成了前面讲到的方式一:
- 方式一: 增加 bff 层, 通过Controller 调用dubbo, 对外暴露 Controller 的rest 接口
方式一的问题:BFF层多了一层转发,多了一层网络传输,性能是比较低的。
7.2 方式二:整合 Spring Cloud Gateway 网关
Spring Cloud Gateway 网关要整合dubbo的话需要解决以下问题:
- 打通注册中心:spring cloud gateway 需要通过注册中心发现下游服务,而 dubbo 也需要通过注册中心实现服务的注册与发现,如果两者的注册中心不能打通的话就会变成双注册中心架构就非常复杂了!
- 协议转换: gateway 使用http传输协议调用下游服务,而dubbo服务默认使用的是tcp传输协议
Spring Cloud Gateway Dubbo 基于Spring Cloud Alibaba的SpringCloud集成Dubbo的方案之上开发, 用于在SpringCloudGateway中直接调用Dubbo接口,可以减少使用Web接口来实现与gateway对接。
Spring Cloud Gateway Dubbo 的架构图:
Spring Cloud Gateway 网关直接调用Dubbo后无需WebApp进行中转
Spring Cloud Gateway + Dubbo 的核心流程:
Spring Cloud Gateway + Dubbo 的源码和实操,后面尼恩写专门的文章,给大家展开介绍。
具体的内容,请参见 尼恩团队的 技术自由圈 公号 。
说在最后:有问题找老架构取经
尼恩团队15大技术圣经 ,使得大家内力猛增,
可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,里边有大量的大厂真题、面试难题、架构难题。
很多小伙伴刷完后, 吊打面试官, 大厂横着走。
在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。
另外,如果没有面试机会,可以找尼恩来改简历、做帮扶。
遇到职业难题,找老架构取经, 可以省去太多的折腾,省去太多的弯路。
尼恩指导了大量的小伙伴上岸,前段时间,刚指导一个40岁+被裁小伙伴,拿到了一个年薪100W的offer。
狠狠卷,实现 “offer自由” 很容易的, 前段时间一个武汉的跟着尼恩卷了2年的小伙伴, 在极度严寒/痛苦被裁的环境下, offer拿到手软, 实现真正的 “offer自由” 。
另外,尼恩也给一线企业提供 《DDD 的架构落地》企业内部培训,目前给不少企业做过内部的咨询和培训,效果非常好。
尼恩技术圣经系列PDF
- 《NIO圣经:一次穿透NIO、Selector、Epoll底层原理》
- 《Docker圣经:大白话说Docker底层原理,6W字实现Docker自由》
- 《K8S学习圣经:大白话说K8S底层原理,14W字实现K8S自由》
- 《SpringCloud Alibaba 学习圣经,10万字实现SpringCloud 自由》
- 《大数据HBase学习圣经:一本书实现HBase学习自由》
- 《大数据Flink学习圣经:一本书实现大数据Flink自由》
- 《响应式圣经:10W字,实现Spring响应式编程自由》
- 《Go学习圣经:Go语言实现高并发CRUD业务开发》
……完整版尼恩技术圣经PDF集群,请找尼恩领取
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓