Dubbo
DUBBO
RPC框架
什么是RPC
RPC(Remote Procedure Call Protocol)远程过程调用协议。一个通俗的描述是:客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个对象,就像调用本地应用程序中的对象一样。
比较正式的描述是:一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。
那么我们至少从这样的描述中挖掘出几个要点:
- RPC是协议:既然是协议就只是一套规范,那么就需要有人遵循这套规范来进行实现。目前典型的RPC实现包括:Dubbo、Thrift、GRPC、Hetty等。
- 网络协议和网络IO模型对其透明:既然RPC的客户端认为自己是在调用本地对象。那么传输层使用的是TCP/UDP还是HTTP协议,又或者是一些其他的网络协议它就不需要关心了。
- 信息格式对其透明:我们知道在本地应用程序中,对于某个对象的调用需要传递一些参数,并且会返回一个调用结果。至于被调用的对象内部是如何使用这些参数,并计算出处理结果的,调用方是不需要关心的。那么对于远程调用来说,这些参数会以某种信息格式传递给网络上的另外一台计算机,这个信息格式是怎样构成的,调用方是不需要关心的。
- 应该有跨语言能力:为什么这样说呢?因为调用方实际上也不清楚远程服务器的应用程序是使用什么语言运行的。那么对于调用方来说,无论服务器方使用的是什么语言,本次调用都应该成功,并且返回值也应该按照调用方程序语言所能理解的形式进行描述。
为什么要用RPC
其实这是应用开发到一定的阶段的强烈需求驱动的。如果我们开发简单的单一应用,逻辑简单、用户不多、流量不大,那我们用不着。当我们的系统访问量增大、业务增多时,我们会发现一台单机运行此系统已经无法承受。此时,我们可以将业务拆分成几个互不关联的应用,分别部署在各自机器上,以划清逻辑并减小压力。此时,我们也可以不需要RPC,因为应用之间是互不关联的。
当我们的业务越来越多、应用也越来越多时,自然的,我们会发现有些功能已经不能简单划分开来或者划分不出来。此时,可以将公共业务逻辑抽离出来,将之组成独立的服务Service应用 。而原有的、新增的应用都可以与那些独立的Service应用 交互,以此来完成完整的业务功能。
所以此时,我们急需一种高效的应用程序之间的通讯手段来完成这种需求,所以你看,RPC大显身手的时候来了!
其实描述的场景也是服务化 、微服务和分布式系统架构的基础场景。即RPC框架就是实现以上结构的有力方式。
常用的RPC框架
- Thrift:thrift是一个软件框架,用来进行可扩展且跨语言的服务的开发。它结合了功能强大的软件堆栈和代码生成引擎,以构建在 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml 这些编程语言间无缝结合的、高效的服务。
- gRPC:一开始由 google 开发,是一款语言中立、平台中立、开源的远程过程调用(RPC)系统。
- Dubbo:Dubbo是一个分布式服务框架,以及SOA治理方案。其功能主要包括:高性能NIO通讯及多协议集成,服务动态寻址与路由,软负载均衡与容错,依赖分析与降级等。Dubbo是阿里巴巴内部的SOA服务化治理方案的核心框架,Dubbo自2011年开源后,已被许多非阿里系公司使用。
- Spring Cloud:Spring Cloud由众多子项目组成,如Spring Cloud Config、Spring Cloud Netflix、Spring Cloud Consul 等,提供了搭建分布式系统及微服务常用的工具,如配置管理、服务发现、断路器、智能路由、微代理、控制总线、一次性token、全局锁、选主、分布式会话和集群状态等,满足了构建微服务所需的所有解决方案。Spring Cloud基于Spring Boot, 使得开发部署极其简单。
RPC原理
RPC调用流程
要让网络通信细节对使用者透明,我们需要对通信细节进行封装,我们先看下一个RPC调用的流程涉及到哪些通信细节:
- 服务消费方(client)调用以本地调用方式调用服务;
- client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
- client stub找到服务地址,并将消息发送到服务端;
- server stub收到消息后进行解码;
- server stub根据解码结果调用本地的服务;
- 本地服务执行并将结果返回给server stub;
- server stub将返回结果打包成消息并发送至消费方;
- client stub接收到消息,并进行解码;
- 服务消费方得到最终结果。
RPC的目标就是要2~8这些步骤都封装起来,让用户对这些细节透明。
下面是网上的另外一幅图,感觉一目了然:
如何做到透明化远程服务调用
怎么封装通信细节才能让用户像以本地调用方式调用远程服务呢?对java来说就是使用代理!java代理有两种方式:1) jdk 动态代理;2)字节码生成。尽管字节码生成方式实现的代理更为强大和高效,但代码维护不易,大部分公司实现RPC框架时还是选择动态代理方式。
其实就是通过动态代理模式,在执行该方法的前后对数据进行封装和解码等,让用于感觉就像是直接调用该方法一样,殊不知,我们对方法前后都经过了复杂的处理。(通过代理实现在调用方法前后进行编码和译码)
如何对消息进行编码和解码
确定消息数据结构
客户端的请求消息结构一般需要包括以下内容:
- 接口名称:在我们的例子里接口名是“HelloWorldService”,如果不传,服务端就不知道调用哪个接口了;
- 方法名:一个接口内可能有很多方法,如果不传方法名服务端也就不知道调用哪个方法;
- 参数类型&参数值:参数类型有很多,比如有bool、int、long、double、string、map、list,甚至如struct等,以及相应的参数值;
- 超时时间 + requestID(标识唯一请求id)
服务端返回的消息结构一般包括以下内容:
- 状态code + 返回值
- requestID(可以根据requestID去查验调用结果)
序列化
一旦确定了消息的数据结构后,下一步就是要考虑序列化与反序列化了。
什么是序列化?序列化就是将数据结构或对象转换成二进制串的过程,也就是编码的过程。
什么是反序列化?将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程。
为什么需要序列化?转换为二进制串后才好进行网络传输嘛!
为什么需要反序列化?将二进制转换为对象才好进行后续处理!
现如今序列化的方案越来越多,每种序列化方案都有优点和缺点,它们在设计之初有自己独特的应用场景,那到底选择哪种呢?从RPC的角度上看,主要看三点:
- 通用性:比如是否能支持Map等复杂的数据结构;
- 性能:包括时间复杂度和空间复杂度,由于RPC框架将会被公司几乎所有服务使用,如果序列化上能节约一点时间,对整个公司的收益都将非常可观,同理如果序列化上能节约一点内存,网络带宽也能省下不少;
- 可扩展性:对互联网公司而言,业务变化飞快,如果序列化协议具有良好的可扩展性,支持自动增加新的业务字段,而不影响老的服务,这将大大提供系统的灵活度。
目前互联网公司广泛使用Protobuf、Thrift、Avro等成熟的序列化解决方案来搭建RPC框架,这些都是久经考验的解决方案。
如何发布自己的服务
通过注册中心将生产者和消费者联系起来,将生产者(消息的提供者)注册到注册中心,消费者通过与注册中心的长连接获得生产者的ip去获得服务. java常用的是zookeeper
Dubbo Architecture
Dubbo 提供了构建云原生微服务业务的一站式解决方案,可以使用 Dubbo 快速定义并发布微服务组件,同时基于 Dubbo 开箱即用的丰富特性及超强的扩展能力,构建运维整个微服务体系所需的各项服务治理能力,如 Tracing、Transaction 等,Dubbo 提供的基础能力包括:
- 服务发现
- 流式通信
- 负载均衡
- 流量治理
- …..
Dubbo 计划提供丰富的多语言客户端实现,其中 Java、Golang 版本是当前稳定性、活跃度最好的版本,其他多语言客户端[]正在持续建设中。
Dubbo的优点:
- 开箱即用
- 易用性高,如 Java 版本的面向接口代理特性能实现本地透明调用
- 功能丰富,基于原生库或轻量扩展即可实现绝大多数的微服务治理能力
- 超大规模微服务集群实践
- 高性能的跨进程通信协议
- 地址发现、流量治理层面,轻松支持百万规模集群实例
- 企业级微服务治理能力
- 服务测试
- 服务Mock
Dubbo3 是在云原生背景下诞生的,使用 Dubbo 构建的微服务遵循云原生思想,能更好的复用底层云原生基础设施、贴合云原生微服务架构。这体现在:
- 服务支持部署在容器、Kubernetes平台,服务生命周期可实现与平台调度周期对齐;
- 支持经典 Service Mesh 微服务架构,引入了 Proxyless Mesh 架构,进一步简化 Mesh 的落地与迁移成本,提供更灵活的选择;
- 作为桥接层,支持与 SpringCloud、gRPC 等异构微服务体系的互调互通
服务发现
服务发现,即消费端自动发现服务地址列表的能力,是微服务框架需要具备的关键能力,借助于自动化的服务发现,微服务之间可以在无需感知对端部署位置与 IP 地址的情况下实现通信。
Dubbo提供的是一种Client-Based的服务发现机制,通常还需要部署第三方的注册中心来实现,例如Nacos,Consul,Zookeeper等,Dubbo自身也提供了对多种注册中心组件的对接,用户可以灵活选择.
Dubbo 基于消费端的自动服务发现能力,其基本工作原理如下图:
服务发现的一个核心组件是注册中心,Provider注册ip地址到注册中心,Consumer从注册中心读取和订阅Provider地址列表,因此要启用服务发现需要为Dubbo增加注册中心的配置
dubbo-spring-boot-starter
使用方式 增加registry配置
# application.properties |
]Dubbo的协议](RPC 通信协议 | Apache Dubbo)
Triple
Triple是一种兼容 gRPC ,以 HTTP2 作为传输层构建新的协议.
[Dubbo 服务流量管理](服务流量管理 | Apache Dubbo)
流量管理
流量管理的本质是将请求根据制定好的路由规则分发到应用服务上,如下图所示:
- 路由规则可以有多个,不同的路由规则之间存在优先级。如:Router(1) -> Router(2) -> …… -> Router(n)
- 一个路由规则可以路由到多个不同的应用服务。如:Router(2)既可以路由到Service(1)也可以路由到Service(2)
- 多个不同的路由规则可以路由到同一个应用服务。如:Router(1)和Router(2)都可以路由到Service(2)
- 路由规则也可以不路由到任何应用服务。如:Router(m)没有路由到任何一个Service上,所有命中Router(m)的请求都会因为没有对应的应用服务处理而导致报错
- 应用服务可以是单个的实例,也可以是一个应用集群。
Dubbo的配置
XML配置
即采用xml格式的配置文件 配置Dubbo 详情参数和配置参考官方文档
[注解配置](注解配置 | Apache Dubbo)
服务提供方
**@DubboService
**注解暴露服务
|
增加应用共享配置
# dubbo-provider.properties |
增加spring扫描路径
|
服务消费方
|
增加应用共享配置
# dubbo-consumer.properties |
指定Spring扫描路径
|
调用服务
配置完一切就可以(先启动生产者 在启动消费者)像调用本地方法一样去调用远程服务了(不用去考虑底层的传输协议 编码解码之类的繁琐的东西)
Dubbo的使用
对于Dubbo而言一个完整的简单的Dubbo项目应该有如下三个模块 生产者(服务的提供者) 消费者(服务的消费者) API(暴露的接口名称和公共可用的信息)
创建一个简单的Demo Maven工程
创建三个子工程分别是**
consumer provider dubbo-api
**首先编写dubbo-api编写其中的service接口和需要用的其他信息(consumer和provider都能用到的信息 比如实体类 访问的param对象等)注意: 类应当实现序列化
public interface MailUserService {
Result save(MailUser mailUser);
Result<MailUser> selectUserById(Integer id);
Result<MailUser> selectUserByInfo(MailUser mailUser);
Result<List<MailUser>> selectUserList();
Result<String> sendMail(MailDTO mailDTO);
}然后将其打包上传到本地仓库或是远程仓库供生产者和消费者使用
在provider 和 consumer中都加入
<!--这个是dubbo的依赖-->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
</dependency>
<!--这个是注册中心的依赖 这里使用的是zookeeper-->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-dependencies-zookeeper</artifactId>
<type>pom</type>
</dependency>
<!--这个是公用的api依赖-->
<dependency>
<groupId>com.dyw</groupId>
<artifactId>Demo-api</artifactId>
<version>0.0.2-SNAPSHOT</version>
</dependency>在provider中实现dubbo-api中的编写的接口
//使用该注解可以将该服务注册到注册中心
public class MailUserServiceImpl implements MailUserService {
private RocketMQTemplate rocketMQTemplate;
UserMapper userMapper;
public Result save(MailUser mailUser) {
userMapper.insert(mailUser);
return R.success();
}
public Result<MailUser> selectUserById(Integer id) {
LambdaQueryWrapper<MailUser> mailUserLambdaQueryWrapper = new LambdaQueryWrapper<>();
mailUserLambdaQueryWrapper.eq(MailUser::getId, id).last("last 1");
MailUser mailUser = userMapper.selectOne(mailUserLambdaQueryWrapper);
return R.success(mailUser);
}
public Result<MailUser> selectUserByInfo(MailUser mailUser) {
MailUser mailUser1 = userMapper.selectById(mailUser);
return R.success(mailUser1);
}
public Result<List<MailUser>> selectUserList() {
List<MailUser> mailUsers = userMapper.selectList(new LambdaQueryWrapper<>());
return R.success(mailUsers);
}
public Result<String> sendMail(MailDTO mailDTO) {
List<Integer> idList = mailDTO.getId();
String sendTime = mailDTO.getSendTime();
String subject = mailDTO.getSubject();
String content = mailDTO.getContent();
List<MailUser> userList = null;
if (StringUtils.isBlank(subject) || StringUtils.isBlank(content)) {
return R.fail("参数有误");
}
if (idList.size() <= 0) {
userList = userMapper.selectList(new LambdaQueryWrapper<>());
} else {
userList = userMapper.selectBatchIds(idList);
}
if (StringUtils.isBlank(sendTime)) {
sendTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
}
mailDTO.setUserList(userList);
mailDTO.setSendTime(sendTime);
rocketMQTemplate.asyncSend("notice:mail", mailDTO, new SendCallback() {
public void onSuccess(SendResult sendResult) {
log.info("发送信息成功");
}
public void onException(Throwable e) {
log.error("发送失败");
}
});
return R.success();
}
}编写实现完所有的接口后 记得将服务注册到注册中心 这里需要配置dubbo 和 服务中心的信息
>dubbo:
application:
name: Demo-provider #应用名称
registry:
address: zookeeper://localhost:2181 #注册中心地址
timeout: 6000 #获取配置的超时时间
protocol:
port: 20880 #服务端口
name: dubbo #协议名称
scan:
base-packages: com.dyw.demoprovider.service.Impl # 扫描服务实现的包的位置 使用注解时才使用紧接着就是配置需要注册到注册中心的服务的信息(可以使用注解)如果使用spring-config配置服务的信息
><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.xsd
http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<dubbo:application name="demo-provider"/>
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>
<dubbo:protocol name="dubbo" port="20890"/>
<bean id="demoService" class="org.apache.dubbo.samples.basic.impl.DemoServiceImpl"/>
<dubbo:service interface="org.apache.dubbo.samples.basic.api.DemoService" ref="demoService"/>
></beans>在consumer中也需要配置注册中心的信息 来获取远端信息
>dubbo:
registry:
address: zookeeper://localhost:2181
application:
name: Demo-Consumer完成上述配置后 就可以进行接口的调用了
>
>
>public class UserController {
MailUserService mailUserService;
public Result<List<MailUser>> selectUserList() {
return mailUserService.selectUserList();
}
public Result<MailUser> addUser( { MailUser mailUser)
return mailUserService.save(mailUser);
}
public Result<MailUser> selectUserById( { Integer id)
return mailUserService.selectUserById(id);
}
>}>
>
>public class MailController {
MailUserService mailUserService;
public Result<String> sendMail( { MailDTO mailDTO)
return mailUserService.sendMail(mailDTO);
}
>}你会发现一个有意思的事情 比如 我的mybatis log配置在生产者端 但是确实我的消费端看到了日志信息. 这也说明了服务是在消费者端实现的 证明了上述RPC框架的图
Dubbo高级用法
参考官方文档 用法很多