Dubbo基本概念和使用
基础
架构演变:
网站流量很小时,只需一个应用,将所有功能都部署在一起,因此使用数据访问框架(ORM)
访问量逐渐增大,需要将应用拆成互不相干的几个应用(垂直应用框架,加速前端页面开发)(MVC)——切分业务,实现各个模块独立部署
- 每个独⽴部署的服务之间,公共的部分需要部署多份。对公共部分的修改、部署、更新都需要重复的操作,带来⽐较⼤的成本
垂直应用越来越多,应用之间交互不可避免。抽取核心业务作为独立的服务,形成稳定的服务中心,以需要提高业务复用(分布式服务框架)
服务越来越多,需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率(SOA,Service Oriented Architecture)
RPC:远程过程调用
一种技术的思想,允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的函数,而不用显式编码远程调用的细节——无论调用本地的还是远程的函数,本质上编写的调用代码基本相同
核心模块:通讯,序列化
Dubbo
高性能、轻量级的开源Java RPC框架
- 面向接口的远程方法调用
- 容错和负载均衡
- 服务自动注册和发现
服务消费者去注册中心订阅到服务提供者的信息。然后通过dubbo进行远程调⽤
基本概念:
- 服务提供者(Provider):暴露服务的服务提供方
- 启动时,向注册中心注册自己提供的服务
- 服务消费者(Consumer):调用远程服务的消费方
- 启动时,向注册中心订阅自己所需的服务
- 从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用
- 注册中心(Registry):
- 返回服务提供者地址列表给消费者
- 如果有变更,将基于长连接推送变更数据给消费者
- 监控中心(Monitor):Provider和Consumer在内存中累计调用次数和调用时间,每分钟发送一次统计数据到监控中心
- 服务容器负责启动,加载,运行Provider
- 服务提供者(Provider):暴露服务的服务提供方
环境:
- 安装zookeeper(作为注册中心)
- 安装dubbo-admin
- dubbo是一个jar包,能够使java程序连接到zookeeper,利用zookeeper消费、提供服务
- dubbo-admin是一个可视化的监控服务
其他注册中心包括:
- nacos:nacos既可以作为注册中心使⽤,也可以作为分布式配置中心使用
- eureka
- redis
注意:
- 每个服务方法应代表一个功能,而不是某功能的一个步骤,否则将面临分布式事务问题,Dubbo 暂未提供分布式事务支持
- 服务接口建议以业务场景为单位划分
Demo
订单服务需要调用用户服务获取某个用户的所有地址
- 订单服务:创建订单,位于A服务器
- 用户服务:查询用户地址,位于B服务器
公共接口层/模块(存放model,service,exception)
1
2
3
4
5
6
7
8
9
10
11
12
13
14// 项目名:pub-interface
// Bean模型
public class UserAddress implements Serializable{
private Integer id;
private String userAddress;
private String userId;
private String consignee;
private String phoneNum;
private String isDefault;
}
// UserService接口
UserService
public List<UserAddress> getUserAddressList(String userId)User模块:
引入公共接口依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30<dependencies>
<dependency>
<groupId>xxxxx</groupId>
<artifactId>pub-interface</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- 引入dubbo -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.6.2</version>
</dependency>
<!-- 由于我们使用zookeeper作为注册中心,所以需要操作zookeeper
dubbo 2.6以前的版本引入zkclient操作zookeeper
dubbo 2.6及以后的版本引入curator操作zookeeper
下面两个zk客户端根据dubbo版本2选1即可
-->
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.10</version>
</dependency>
<!-- curator-framework -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.12.0</version>
</dependency>
</dependencies>配置(将dubbo和spring ioc整合)
1
2
3
4
5
6
7
8
9
10<!--当前应用的名字 -->
<dubbo:application name="user"></dubbo:application>
<!--指定注册中心的地址 -->
<dubbo:registry address="zookeeper://118.24.44.169:2181" />
<!--配置当前这个服务在dubbo容器中的端⼝号,每个dubbo容器内部的服务的端⼝号必须是不⼀样的-->
<dubbo:protocol name="dubbo" port="20880" />
<!-- 指定需要暴露的服务,指明该服务具体的实现bean是userServiceImpl-->
<dubbo:service interface="xxxxx.service.UserService" ref="userServiceImpl" />
<!--将服务提供者的bean注⼊到ioc容器中-->
<bean id="userServiceImpl" class="xxxxx.service.impl.SiteServiceImpl"/>实现UserService接口
1
2
3
4
5
6
7
8public class UserServiceImpl implements UserService {
public List<UserAddress> getUserAddressList(String userId) {
// TODO Auto-generated method stub
return userAddressDao.getUserAddressById(userId);
}
}启动服务,关联bean配置文件
1
2
3
4
5
6public static void main(String[] args) throws IOException {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("classpath:spring-beans.xml");
System.in.read(); // 让当前服务⼀直在线,不会被关闭,按任意键退出
}
Order模块(Consumer):
引入依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25<dependencies>
<dependency>
<groupId>xxxxx</groupId>
<artifactId>pub-interface</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- 引入dubbo -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.6.2</version>
</dependency>
<!-- 由于我们使用zookeeper作为注册中心,所以需要引入zkclient和curator操作zookeeper -->
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.10</version>
</dependency>
<!-- curator-framework -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.12.0</version>
</dependency>
</dependencies>配置(将dubbo和spring ioc整合)
1
2
3
4
5
6<!-- 应用名 -->
<dubbo:application name="order"></dubbo:application>
<!-- 指定注册中心地址 -->
<dubbo:registry address="zookeeper://118.24.44.169:2181" />
<!-- 生成远程服务代理,可以和本地bean一样使用demoService -->
<dubbo:reference id="userService" interface="xxxxx.service.UserService"></dubbo:reference>服务:
1
2
3
4
5
6
7
8
9
10
11
12
13public class OrderService {
UserService userService;
/**
* 初始化订单,查询用户的所有地址并返回
* @param userId
* @return
*/
public List<UserAddress> initOrder(String userId){
return userService.getUserAddressList(userId);
}
}
Demo(注解)
Provider:
1
2
3
4
5
6
7
8
9
10import com.alibaba.dubbo.config.annotation.Service;
import xxxxx.bean.UserAddress;
import xxxxx.service.UserService;
import xxxxx.user.mapper.UserAddressMapper;
//使用dubbo提供的service注解,注册暴露服务
public class UserServiceImpl implements UserService {
UserAddressMapper userAddressMapper;Consumer:
1
2
3
4
5
public class OrderController {
//使用dubbo提供的reference注解引用远程服务
UserService userService;
Demo(SpringBoot)
引入依赖:
1
2
3
4
5<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>0.2.0</version>
</dependency>配置application.properties:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17#提供者配置:
user =
zookeeper =
192.168.67.159:2181 =
xxxxx =
dubbo =
# application.name就是服务名,不能跟别的dubbo提供端重复
# registry.protocol 是指定注册中心协议
# registry.address 是注册中心的地址加端口号
# protocol.name 是分布式固定是dubbo,不要改。
# base-package 注解方式要扫描的包
#消费者配置:
order =
zookeeper =
192.168.67.159:2181 =
com.atguigu.gmall =
dubbo =dubbo注解:@Service、@Reference(如果没有在配置中写dubbo.scan.base-package,需要在启动类上使用@EnableDubbo注解)
Dubbo配置
配置的优先级:JVM 启动 -D 参数优先,XML 次之,Properties 最后
重试:请求失败时自动切换另一个provider地址。通过 retries=”2” 来设置重试次数(不含第一次)
1
2
3
4
5
6
7<dubbo:service retries="2" />
或
<dubbo:reference retries="2" />
或
<dubbo:reference>
<dubbo:method name="findFoo" retries="2" />
</dubbo:reference>超时时间设置:
Consumer:从发起服务调⽤到收到服务响应的整个过程的时间
1
2
3
4
5
6
7全局超时配置
<dubbo:consumer timeout="5000" />
指定接口以及特定方法超时配置
<dubbo:reference interface="com.foo.BarService" timeout="2000">
<dubbo:method name="sayHello" timeout="3000" />
</dubbo:reference>1
Provider:执⾏该服务的超时时间
1
2
3
4
5
6
7全局超时配置
<dubbo:provider timeout="5000" />
指定接口以及特定方法超时配置
<dubbo:provider interface="com.foo.BarService" timeout="2000">
<dubbo:method name="sayHello" timeout="3000" />
</dubbo:provider>1
推荐在Provider上尽量多配置Consumer端属性
- Provider比Consumer更清楚服务性能参数,如调用的超时时间,合理的重试次数
- Provider配置后,Consumer不配置则会使用Provider的配置值,即Provider配置可以作为Consumer的缺省值
版本号:一个接口的实现出现不兼容升级时使用。此时用版本号过渡,版本号不同的服务相互间不引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14老版本服务提供者配置:
<dubbo:service interface="com.foo.BarService" version="1.0.0" />
新版本服务提供者配置:
<dubbo:service interface="com.foo.BarService" version="2.0.0" />
老版本服务消费者配置:
<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="*" />1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class DefaultSiteServiceImpl implements SiteService {
public String siteName(String name) {
return "default:"+name;
}
}
public class AsyncSiteServiceImpl implements SiteService {
public String siteName(String name) {
return "async:" + name;
}
}
private SiteService siteService;
高可用
宕机处理:
- 监控中心宕掉不影响使用,只是丢失部分采样数据
- 数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务
- 注册中心的任意一台宕掉后,将自动切换到另一台
- 注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯
- 服务提供者无状态,任意一台宕掉后,不影响使用
- 服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复
负载均衡:缺省为 random 随机调用
1
2Random LoadBalance
- 按权重设置随机概率。
- 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重
RoundRobin LoadBalance
- 轮循,按公约后的权重设置轮循比率。
- 存在慢的Provider累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上
LeastActive LoadBalance
- 最少活跃调用数,相同活跃数的随机,活跃数为调用前后计数差
- 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大
- 执行过程:
- Consumer在本地缓存所有Provider
- Consumer在调⽤某⼀个服务时,会选择本地的所有Provider中,属性active值最小的Provider
- 选定该Provider,对其active属性+1
- 开始调用该服务
- 完成调用后,对该Provider的active属性-1
ConsistentHash LoadBalance
一致性 Hash,相同参数的请求总是发到同一Provider
当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动
服务熔断和降级
服务降级:服务器压力剧增的情况下,根据实际业务情况及流量,对一些服务和页面有策略的不处理或换种简单的方式处理,从而释放服务器资源以保证核心交易正常运作或高效运作
向注册中心写入动态配置覆盖规则
1
2
3RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181"));
registry.register(URL.valueOf("override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&mock=force:return+null"));- mock=force:return+null:Consumer对该服务的方法调用都直接返回 null ,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响
- mock=fail:return+null:Consumer对该服务的方法调用在失败后,再返回 null 值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响
集群容错:缺省时为failover
Failover Cluster:当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟
- 会出现幂等性问题
- Provider在业务层⾯解决幂等性问题
- 把数据的业务id作为数据库的联合主键,业务id不能重复
- 使⽤分布式锁来解决重复消费问题
Failfast Cluster:快速失败。只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录
Failsafe Cluster:失败安全。出现异常时,直接忽略。通常用于写入审计日志等操作
Failback Cluster:失败自动恢复。后台记录失败请求,定时重发。通常用于消息通知操作
Forking Cluster:并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源
Broadcast Cluster:广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有Provider更新缓存或日志等本地资源信息
配置:
1
2
3<dubbo:service cluster="failsafe" />
或
<dubbo:reference cluster="failsafe" />
本地存根stub’:Provider可能需要在Consumer端执行部分逻辑,如:做 ThreadLocal 缓存,提前验证参数,调用失败后伪造容错数据
核心:API 中带上 Stub,客户端生成 Proxy 实例,会把 Proxy 通过构造函数传给 Stub ,然后把 Stub 暴露给用户,Stub 可以决定要不要去调 Proxy
Stub使用代理模式包装原有的远程调用服务,让使用者在远程服务调用前后做一些通用处理
本地存根执行顺序:
- Consumer发起调用
- 如果Consumer存在本地存根 Stub,会先执行本地存根
- 本地存根 Stub 持有远程服务的 Proxy 对象。Stub 执行的时候,先执行自己的逻辑(before),然后通过Proxy 发起远程调用,最后在返回之前也执行自己的逻辑(after-returning)
![image-20230503174609719](Dubbo-1/image-20230503174609719.png)
配置:如果默认将
stub
属性设置为true,则必须保证本地存根实现类以Stub
命名结尾Provider:
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class SiteServiceStub implements SiteService {
private final SiteService siteService;
public SiteServiceStub(SiteService siteService) {
this.siteService = siteService;
}
public String siteName(String name) {
try {
return siteService.siteName(name);
} catch (Exception e) {
return "stub:"+name;
}
}
}Consumer:
1
2
3
4
5
6
7
8
9
10
11
public class StubDubboConsumer {
private SiteService siteService;
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(StubDubboConsumer.class);
SiteService siteService = (SiteService) context.getBean(SiteService.class);
String name = siteService.siteName("q-face");
System.out.println(name);
}
}
参数回调:Dubbo基于长连接生成反向代理,在服务端执行客户端的逻辑(消费者调用方法)
接口层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public interface SiteService {
//同步调用方法
String siteName(String name);
//回调方法
//default关键字:接口可以有实现方法,而且不需要实现类去实现其方法
default String siteName(String name, String key, SiteServiceListener siteServiceListener){
return null;
}
}
// 回调接口和实现类
public interface SiteServiceListener {
void changed(String data);
}
public class SiteServiceListenerImpl implements SiteServiceListener, Serializable {
public void changed(String data) {
System.out.println("changed:" + data);
}
}Provider
1
2
3
4
5
6
7
8
9
10
11
12
public class CallbackSiteServiceImpl implements SiteService {
public String siteName(String name) {
return null;
}
public String siteName(String name, String key, SiteServiceListener siteServiceListener) {
siteServiceListener.changed("provider data");
return "callback:"+name;
}
}Consumer
1
2
3
4
5
6
7
8
9
10
11
12
13
public class CallbackDubboConsumer {
private SiteService siteService;
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(CallbackDubboConsumer.class);
SiteService siteService = (SiteService) context.getBean(SiteService.class);
// key 目的是指明实现类在Provider和Consumer之间保证是同一个
System.out.println(siteService.siteName("q-face", "c1", new SiteServiceListenerImpl()));
System.out.println(siteService.siteName("q-face", "c2", new SiteServiceListenerImpl()));
System.out.println(siteService.siteName("q-face", "c3", new SiteServiceListenerImpl()));
}
}
异步调用:
从 2.7.0 开始,Dubbo 的所有异步编程接口开始以 CompletableFuture 为基础
基于 NIO 的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销小
Consumer通过异步调用,不用等待Provider返回结果就⽴即完成任务,待有结果后再执行之前设定好的监听逻辑
接口层
1
2
3
4
5
6
7
8
9
10public interface SiteService {
//同步调用方法
//回调方法
//异步调用方法
default CompletableFuture<String> siteNameAsync(String name){
return null;
}
}Provider
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AsyncSiteServiceImpl implements SiteService {
public String siteName(String name) {
return "async:" + name;
}
public CompletableFuture<String> siteNameAsync(String name) {
System.out.println("异步调用:" + name);
return CompletableFuture.supplyAsync(() -> {
return siteName(name);
});
}
}Consumer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class AsyncDubboConsumer {
private SiteService siteService;
public static void main(String[] args) {
ConfigurableApplicationContext context =
SpringApplication.run(AsyncDubboConsumer.class);
SiteService siteService = (SiteService) context.getBean(SiteService.class);
//调用异步方法
CompletableFuture<String> future = siteService.siteNameAsync("q-face");
//设置监听,非阻塞
future.whenComplete((v, e) -> {
if (e != null) {
e.printStackTrace();
} else {
System.out.println("result:" + v);
}
});
System.out.println("异步调用结束");
}
}
原理
权重轮询算法:
假如有三台服务器A、B、C,权重分别是6、2、2
每台服务器确定两个权重变量:weight、currentWeight,前者不变,后者初始化为0,且currentWeight = currentWeight+weight
集群选择currentWeight最大的服务器作为选择结果。并将该最大服务器的 currentWeight减去各服务器的weight总数
调整currentWeight = currentWeight+weight,开始新⼀轮的选择