dubbo学习
1. dubbo注解整合基础案例
参考下列博客:
手把手教你使用Springboot整合dubbo,搭建一个微服务
2. dubbo负载均衡
负载均衡:负载均衡是指在集群中,将多个数据请求分散在不同单元上进行执行,主要为了提高系统容错能力和加强系统对数据的处理能力。
dubbo负载均衡机制是决定一次服务调用哪个提供者的服务
在dubbo运行的过程中,有两个位置会用到负载均衡:
- 有多个注册中心时,客户端使用负载均衡选择其中一个注册中心上注册的服务
- 客户端使用负载均衡选择一个注册中心上注册的多个服务
dubbo的负载均衡算法有下列五种:(默认选择RandomLoadBalance随机算法)
RandomLoadBalance:随机负载均衡。随机的选择一个。是Dubbo的默认负载均衡策略。
RoundRobinLoadBalance:轮询负载均衡。轮询选择一个。
LeastActiveLoadBalance:最少活跃调用数,相同活跃数的随机。活跃数指调用前后计数差。使慢的 Provider 收到更少请求,因为越慢的 Provider 的调用前后计数差会越大。
ConsistentHashLoadBalance:一致性哈希负载均衡。相同参数的请求总是落在同一台机器上。
我们修改dubbo-provider的代码
UserServiceImpl.java
@DubboService
@Service
@Slf4j
public class UserServiceImpl implements IUserService {
@Autowired
private UserMapper userMapper;
@Value("${server.port}")
private Integer port;
@Override
public User selectUserById(Long id){
log.info("当前访问的提供者的port是:{}",port);
User user=userMapper.selectUserById(id);
return user;
}
}
修改dubbo-consumer的代码
ConsumerUserController.java
@RestController
@RequestMapping("/consumer")
@Slf4j
public class ConsumerUserController {
@DubboReference(protocol = "dubbo",loadbalance = "random")
private IUserService userService;
@RequestMapping("/selectUserById/{id}")
public User getUser(@PathVariable("id")Long id){
User user=userService.selectUserById(id);
log.info("response from provider:{}",user);
return user;
}
}
修改provider的application.yml
server:
port: 8091
spring:
application:
name: dubbo-samples-provider-springCloud
datasource:
username: root
password: 3fa4d180
url: jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: /mapper/*.xml
dubbo:
registry:
address: zookeeper://127.0.0.1:2181
protocol:
name: dubbo
port: 20811
修改配置,设置多实例
运行一份ProviderApp后,修改application.yml的server.port和protocol.port为8182和20812,8183和20813,总共创建三份实例,然后运行ConsumerApp,多次访问consumer提供的接口
控制台结果如下:
说明确实为随机访问。
接下来我们修改dubbo-consumer,修改负载均衡的方式
@RestController
@RequestMapping("/consumer")
@Slf4j
public class ConsumerUserController {
@DubboReference(protocol = "dubbo",loadbalance = "roundrobin")
private IUserService userService;
@RequestMapping("/selectUserById/{id}")
public User getUser(@PathVariable("id")Long id){
User user=userService.selectUserById(id);
log.info("response from provider:{}",user);
return user;
}
}
重启consumer,多次访问selectuserById,一共访问9次,每个ProviderApp实例出现此时都为3次
Dubbo 的配置,如果存在多个的话,是会有覆盖关系的:
- 方法级优先,接口级次之,全局配置次之
- 如果级别一样,则消费方优先,提供方次之
所以,上面4中配置的优先级是:
- 客户端方法级别配置
- 客户端接口级别配置
- 服务端方法级别配置
- 服务端接口级别配置
我们修改ProviderApp的配置,在UserServiceImpl修改负载均衡策略为轮询
@DubboService(loadbalance = "roundrobin")
@Service
@Slf4j
public class UserServiceImpl implements IUserService {
@Autowired
private UserMapper userMapper;
@Value("${server.port}")
private Integer port;
@Override
public User selectUserById(Long id){
log.info("当前访问的提供者的port是:{}",port);
User user=userMapper.selectUserById(id);
return user;
}
}
修改Consumer,将负载均衡策略改为random
@RestController
@RequestMapping("/consumer")
@Slf4j
public class ConsumerUserController {
@DubboReference(protocol = "dubbo",loadbalance = "random")
private IUserService userService;
@RequestMapping("/selectUserById/{id}")
public User getUser(@PathVariable("id")Long id){
User user=userService.selectUserById(id);
log.info("response from provider:{}",user);
return user;
}
}
重启实例,多次访问selectUserById,虽然服务提供方的策略是轮询,但消费方的策略是随机,结果如下:
参考连接:Dubbo的负载均衡
3. dubbo调用机制
远程调用是dubbo框架的核心,基本过程是,向服务端发送参数,并等待获取结果。如果调⽤过程出错则需要对异常进⾏处理。Dubbo调用类别有四种,分别是同步,异步,并行以及广播调用。默认情况下Dubbo是采⽤同步⽅式进⾏调⽤,即发起调⽤后线程将会阻塞,直到结果返回。同时为了提⾼性能,也可采用异⽅式调⽤。另外⼀些特殊的业务场景下,则可以使⽤⼴播⽅式调⽤、以及并⾏调⽤。
#### 3.1 同步调用:向远程服务器发送请求,该请求是阻塞的,直到服务器返回结果。
客户端线程发送给服务端时,其实有专门的io线程完成,它是异步发送的,但dubbo会构建一个CompletableFuture,通过它阻塞当前线程去等待结果返回,但服务端结果返回之后completableFuture填充结果,并释放阻塞的调用线程
3.2 异步调用
在特殊场景下,异步调用可提高性能,比如要远程调用A、B、C,分别用时1s,2s,3s,如果是同步的话,用时6秒,如果异步调用,同时去执行这三个接口,理想情况下用时3秒,不过前提是ABC这三个方法没有参数依赖并且没有顺序依赖。
修改dubbo-api模块,修改接口
public interface IUserService {
User selectUserById(Long id);
User asyncSelectUserById(Long id);
User syncSelectUserById(long id);
}
修改dubbo-provider,修改UserServiceImpl
@DubboService(loadbalance = "roundrobin")
@Service
@Slf4j
public class UserServiceImpl implements IUserService {
@Autowired
private UserMapper userMapper;
@Value("${server.port}")
private Integer port;
@Override
public User selectUserById(Long id){
log.info("当前访问的提供者的port是:{}",port);
User user=userMapper.selectUserById(id);
return user;
}
@Override
public User asyncSelectUserById(Long id) {
log.info("异步访问获取用户,port:{}",port);
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
return userMapper.selectUserById(id);
}
@Override
public User syncSelectUserById(Long id) {
log.info("同步访问获取用户,port:{}",port);
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
return userMapper.selectUserById(id);
}
}
修改dubbo-consumer,修改consumerUserController
@RestController
@RequestMapping("/consumer")
@Slf4j
public class ConsumerUserController {
//设置asyncSelectUserById为异步调用
@DubboReference(protocol = "dubbo",loadbalance = "random",
methods = {@Method(name = "asyncSelectUserById",async = true)})
private IUserService userService;
@RequestMapping("/selectUserById/{id}")
public User getUser(@PathVariable("id")Long id){
User user=userService.selectUserById(id);
log.info("response from provider:{}",user);
return user;
}
@RequestMapping("/syncSelectUserById/{id}")
public User asyncGetUser(@PathVariable("id")Long id){
long start = System.currentTimeMillis();
User user=null;
user=userService.syncSelectUserById(id);
user=userService.syncSelectUserById(id);
user=userService.syncSelectUserById(id);
long end=System.currentTimeMillis();
log.info("用时:{}",end-start);
return user;
}
@RequestMapping("/asyncSelectUserById/{id}")
public User asyncGetUser2(@PathVariable("id")Long id){
long start = System.currentTimeMillis();
User user=null;
userService.asyncSelectUserById(id);
Future<User> future1 = RpcContext.getServletContext().getFuture();
userService.asyncSelectUserById(id);
Future<User> future2 = RpcContext.getServletContext().getFuture();
userService.asyncSelectUserById(id);
Future<User> future3 = RpcContext.getServletContext().getFuture();
try{
user=future1.get();
user=future2.get();
user=future3.get();
}catch (InterruptedException e){
e.printStackTrace();
}catch (ExecutionException e){
e.printStackTrace();
}
long end=System.currentTimeMillis();
log.info("用时:{}",end-start);
return user;
}
}
重启服务,分别访问syncSelectUserById和asyncSelectUserById
syncSelectUserById
asyncSelectUserById
实现原理:事实上Dubbo的调用本身就是异步的,其常规的调用是通过AsyncToSyncInvoker组件,由异步转成了同步,所以异步的实现就是让该组件不去执行阻塞逻辑即可,此外,为了顺利拿到结果回执(Future),在调用发起之后其回执会被填充到RpcContext当中
3.3 并行调用
为了尽可能获得更⾼的性能,以及最⾼级别的保证服务的可⽤性。⾯对多个服务,并不知道哪个处理更快。这时客户端可并⾏发起多个调⽤,只要其中⼀个成功即返回,某个服务异常直接勿略,只有所有服务多出现异常情况下才会判定调⽤出错。
由于并行调用特殊性,生产环境不建议使用,如果一定要用,请配置在method级别,这里不做案例演示
并⾏调⽤原理是通过线程池异步发送远程请求。其流程如下:
- 根据forks数量挑选出服务节点
- 基于线程池(ExecutorService)并⾏发起远程调⽤
- 基于阻塞队列(BlockingQueue)等待结果返回
- 第⼀个结果返回,填充阻塞列,并释放线程
注:并行调用时,不能同时设置为异步调用,即async=true
3.4 广播调用
在分布式环境,⼀个服务通常有多个提供⽅。原则上调⽤任意⼀个服务,结果都应该是⼀样的。但⼀些特殊场景除外,⽐如:“通知缓存更新”,需要通知到每个服务器都要更新各⾃节点缓存。要确保每个服务都被调⽤到。⼴播调⽤⼀次调⽤,会遍历所有提供者并发起调⽤,确保所有节点都被调⽤到。任意⼀台报错就算失败
参考文章:Dubbo调用及容错机制详解
4. 超时、重试机制
Dubbo主要内置了如下几种策略:
Failover(失败自动切换):高可用系统中的一个常用概念,服务器通常拥有主备两套机器配置,如果主服务器出现故障,则自动切换到备服务器中,从而保证了整体的高可用性。这是Dubbo对的默认容错策略,当调用出现失败时,根据配置的重试次数,会自动从其他可用地址中重新选择一个可用的地址进行调用,直到调用成功,或者达到重试的上限位置。
Failsafe(失败安全):失败安全策略的核心是即使失败了也不会影响整个调用流程。通常情况下用于旁路系统或流程中,它的失败不影响核心业务的正确性。在实现上,当出现调用失败时,会忽略此错误,并记录一条日志,同时返回一个空结果,在上游看来调用是成功的。应用场景,可以用于写入审计日志等操作。
Failfast(快速失败):某些业务场景中,某些操作可能是非幂等的,如果重复发起调用,可能会导致出现脏数据等。例如调用某个服务,其中包含一个数据库的写操作,如果写操作完成,但是在发送结果给调用方的过程中出错了,那么在调用发看来这次调用失败了,但其实数据写入已经完成。这种情况下,重试可能并不是一个好策略,这时候就需要使用到Failfast策略,调用失败立即报错。让调用方来决定下一步的操作并保证业务的幂等性。
Failback(失败自动恢复):Failback通常和Failover两个概念联系在一起。在高可用系统中,当主机发生故障,通过Failover进行主备切换后,待故障恢复后,系统应该具备自动恢复原始配置的能力。
Dubbo中的Failback策略中,如果调用失败,则此次失败相当于Failsafe,将返回一个空结果。而与Failsafe不同的是,Failback策略会将这次调用加入内存中的失败列表中,对于这个列表中的失败调用,会在另一个线程中进行异步重试,重试如果再发生失败,则会忽略,即使重试调用成功,原来的调用方也感知不到了。因此它通常适合于,对于实时性要求不高,且不需要返回值的一些异步操作。
我们以failover为例,编写案例
修改Dubbo-api的接口
在IUserService接口中添加retrySelectUserById方法
public interface IUserService {
User selectUserById(Long id);
User asyncSelectUserById(Long id);
User syncSelectUserById(Long id);
User retrySelectUserById(Long id);
}
在dubbo-provider模块中,修改UserServiceImpl,添加retrySelectUserById方法
@Override
public User retrySelectUserById(Long id) {
log.info("超时重传获取用户id,port:{}",port);
try{
Thread.sleep(4000);
}catch (InterruptedException e){
e.printStackTrace();
}
User user = userMapper.selectUserById(id);
log.info("user:{}",user);
return user;
}
在dubbo-consumer中,修改ConsumerUserController.java
@DubboReference(protocol = "dubbo",loadbalance = "random",
methods = {@Method(name = "asyncSelectUserById",async = true),
@Method(name = "retrySelectUserById",retries = 3,timeout = 2000)})
private IUserService userService;
@RequestMapping("/retrySelectUserById/{id}")
public User retrySelectUserById(@PathVariable("id")Long id){
User user = userService.retrySelectUserById(id);
log.info("retryUser:{}",user);
return user;
}
重启,访问retrySelectUserById一次,因为我们设置超时时间为2000ms,而服务端Thread.sleep(4000),所以会超时,同时因为在消费端设置retry为3,所以会重试3次,也就是说一共会访问4次方法
超时重试存在一些问题,如果我们执行的是一个插入操作,假设插入业务用时3秒,而我们超时时间设置为2秒,那么因为超时,可能会导致重复插入数据,这时就需要考虑到分布式事务以及幂等性方面的问题了,这里我们不做详细探讨。
5. 本地存根
远程服务后,客户端通常只剩下接口,而实现全在服务端,但提供方有时想在客户端也执行部分逻辑
使用场景:做ThreadLocal缓存,提前验证参数,调用失败后伪造容错数据等等,此时就需要在API中带上Stub,客户端生成Proxy实例,会把Proxy通过构造函数传给Stub,然后把Stub暴露给用户,Stub可以决定要不要去调Proxy
在dubbo-api模块中,添加IUserService2接口
public interface IUserService2 {
User getUserById(Long id);
}
在dubbo-provider模块中,添加IUserService2的实现
@Service
@DubboService
@Slf4j
public class UserService2Impl implements IUserService2 {
@Autowired
private UserMapper userMapper;
@Value("${server.port}")
private Integer port;
@Override
public User getUserById(Long id) {
log.info("超时重传获取用户id,port:{}",port);
try{
Thread.sleep(4000);
}catch (InterruptedException e){
e.printStackTrace();
}
User user = userMapper.selectUserById(id);
log.info("user:{}",user);
return user;
}
}
修改dubbo-consumer模块,添加上IUserService2的本地存根
public class UserServiceStub implements IUserService2 {
private IUserService2 userService2;
public UserServiceStub(IUserService2 userService2){
this.userService2=userService2;
}
@Override
public User getUserById(Long id) {
try{
//检查参数是否有误
if (id<=0){
System.out.println("参数有误");
return null;
}
return userService2.getUserById(id);
}catch (Exception e){
// e.printStackTrace();
System.out.println("启动容错===========");
User user=new User();
user.setId(0L);
user.setUsername("容错");
user.setPassword("本地存根");
return user;
}
}
}
修改ConsumerUserController,添加下述方法
@RestController
@RequestMapping("/consumer")
@Slf4j
public class ConsumerUserController {
@DubboReference(stub = "com.young.customer.stub.UserServiceStub")
private IUserService2 userService2;
@RequestMapping("/stubSelectUserById/{id}")
public User stubSelectUserById(@PathVariable("id")Long id){
return userService2.getUserById(id);
}
}
重启,访问
参考文章:Dubbo(十二)dubbo的服务版本配置以及本地存根使用介绍
6. 灰度发布
灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。灰度发布开始到结束期间的这一段时间,称为灰度期。
下面简单演示一下dubbo通过version版本号进行灰度发布
创建一个项目,假设叫dubbo02,引入下列依赖:
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.young</groupId>
<artifactId>dubbo02</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>dubbo-api</module>
<module>dubbo-provider</module>
<module>dubbo-consumer</module>
</modules>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<spring-boot-dependencies.version>2.7.0</spring-boot-dependencies.version>
<spring-cloud-dependencies.version>2021.0.1</spring-cloud-dependencies.version>
<spring-dubbo.version>2.0.0</spring-dubbo.version>
<lombok.version>1.18.22</lombok.version>
<dubbo.version>3.0.2.1</dubbo.version>
<apache.commons-fileupload.version>1.4</apache.commons-fileupload.version>
<apache.commons-text.version>1.9</apache.commons-text.version>
<apache.commons-configuration.version>1.10</apache.commons-configuration.version>
<httpclient.version>4.5.12</httpclient.version>
<hutool.version>5.7.7</hutool.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot-dependencies.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<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-spring-boot-starter</artifactId>
<version>${dubbo.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
创建dubbo-api模块,引入依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>dubbo02</artifactId>
<groupId>com.young</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>dubbo-api</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
</project>
User.java实体类
package com.young.entity;
import lombok.Data;
import java.io.Serializable;
@Data
public class User implements Serializable {
private Integer id;
private String username;
private String password;
private String school;
}
UserService.java接口
package service;
import com.young.entity.User;
public interface UserService {
User getUserById();
}
创建dubbo-provider模块,提供服务
引入依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>dubbo02</artifactId>
<groupId>com.young</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>dubbo-consumer</artifactId>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-zookeeper</artifactId>
</dependency>
<dependency>
<groupId>com.young</groupId>
<artifactId>dubbo-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
UserServiceImpl.java实现类
package com.young.service.impl;
import com.young.entity.User;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.stereotype.Service;
import service.UserService;
@Service
@DubboService(version = "1.0.0")
public class UserServiceImpl implements UserService {
@Override
public User getUserById() {
User user=new User();
user.setUsername("cxy");
user.setPassword("123456");
user.setId(1);
user.setSchool("华南理工大学");
return user;
}
}
UserServiceImpl2.java实现类
package com.young.service.impl;
import com.young.entity.User;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.stereotype.Service;
import service.UserService;
@Service
@DubboService(version = "2.0.0")
public class UserServiceImpl2 implements UserService {
@Override
public User getUserById() {
User user=new User();
user.setUsername("dhi");
user.setPassword("123456");
user.setId(1);
user.setSchool("潮阳一中");
return user;
}
}
ProviderApp.java
package com.young;
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableDubbo
public class ProviderApp {
public static void main(String[] args) {
SpringApplication.run(ProviderApp.class,args);
}
}
application.yml
server:
port: 8089
spring:
application:
name: provider-service
dubbo:
registry:
address: zookeeper://127.0.0.1:2181
protocol:
name: dubbo
port: 29089
创建dubbo-consumer模块,依赖和刚才provider模块一样
DemoController.java
package com.young.controller;
import com.young.entity.User;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import service.UserService;
@RestController
@RequestMapping("/demo")
public class DemoController {
@DubboReference(version = "1.0.0")
private UserService userService1;
@DubboReference(version = "2.0.0")
private UserService userService2;
@GetMapping("/v1")
public User getUser1(){
return userService1.getUserById();
}
@GetMapping("/v2")
public User getUser2(){
return userService2.getUserById();
}
}
application.yml
server:
port: 8099
spring:
application:
name: consumer-service
dubbo:
protocol:
name: dubbo
port: 29099
registry:
address: zookeeper://localhost:2181
分别访问/demo/v1 和/demo/v2
刚才的项目,只是演示了如何区分不同版本的服务,我们一般使用version或group来对新旧服务进行区分,至于灰度发布的实现,除了刚才提到的新旧版本区分之外,还需要通过使用nginx方向代理、dubbo路由规则等,来实现灰度发布。
除此之外,在灰度发布的实践过程中,我们还会遇到项目的问题:
- 如何设置流量比例
- 如何确保新旧版本共存
dubbo灰度发布带来的好处:
- 减小对用户的影响,提高用户体验
- 降低系统升级风险,提高系统可靠性
- 更好地支持持续集成和持续交付,提高开发效率
7. dubbo服务鉴权
在dubbo中,我们有时候要对调用者进行鉴权,保证我们的服务只提供给某些服务调用。因此这里就设计到Dubbo服务鉴权,关于dubbo服务鉴权,有两种方式:
- 将token作为参数发送到服务提供者端,获取RpcContext中的token参数,并进行鉴权验证
- 基于IP白名单的服务鉴权:将允许访问服务的IP地址添加到白名单中,服务提供者在接收到请求后比对请求后比对请求IP地址与白名单是否匹配,从而判断请求是否合法
我们分别对上述两种方式进行案例实现
首先是token,我们要将token从消费者服务传递到提供者服务,这里便涉及到一个隐式传参的问题,而隐式传参,可以使用上下文信息,即RpcContext实现,下面是Dubbo3中RpcContext的四大模块
ServiceContext:在 Dubbo 内部使用,用于传递调用链路上的参数信息,如 invoker 对象等
ClientAttachment:在 Client 端使用,往 ClientAttachment 中写入的参数将被传递到 Server 端
ServerAttachment:在 Server 端使用,从 ServerAttachment 中读取的参数是从 Client 中传递过来的
ServerContext:在 Client 端和 Server 端使用,用于从 Server 端回传 Client 端使用,Server 端写入到 ServerContext 的参数在调用结束后可以在 Client 端的 ServerContext 获取到
我们修改刚才灰度发布相关的代码,在dubbo-consumer模块和dubbo-provider模块加上spring-boot-starter-data-redis的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
先修改消费者模块的DemoController.java,这里的hasToken中,我们将token的值隐式传递给消费者
package com.young.controller;
import com.young.entity.User;
import org.apache.dubbo.config.annotation.DubboReference;
import org.apache.dubbo.rpc.RpcContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import service.UserService;
import java.util.UUID;
@RestController
@RequestMapping("/demo")
public class DemoController {
@DubboReference(version = "1.0.0")
private UserService userService1;
@DubboReference(version = "2.0.0")
private UserService userService2;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/v1")
public User getUser1(){
return userService1.getUserById();
}
@GetMapping("/v2")
public User getUser2(){
return userService2.getUserById();
}
@GetMapping("/token/yes")
public User hasToken(){
String token= UUID.randomUUID().toString();
stringRedisTemplate.opsForValue().set("authorization",token);
System.out.println("token:"+token);
RpcContext.getClientAttachment().setAttachment("authorization",token);
RpcContext.getClientAttachment().setAttachment("name","cxy");
User user = userService1.getUserById();
return user;
}
@GetMapping("/token/no")
public User notToken(){
User user = userService1.getUserById();
return user;
}
@GetMapping("/name")
public String name(){
String interfaceName = RpcContext.getServerContext().getInterfaceName();
String methodName = RpcContext.getServerContext().getMethodName();
String localHostName = RpcContext.getServerContext().getLocalHostName();
String remoteApplicationName = RpcContext.getServerContext().getRemoteApplicationName();
String remoteHostName = RpcContext.getServerContext().getRemoteHostName();
System.out.println("interface:"+interfaceName);
System.out.println("method:"+methodName);
System.out.println("localHost"+localHostName);
System.out.println("remoteHost:"+remoteHostName);
System.out.println("remoteApplication:"+remoteApplicationName);
return remoteApplicationName;
}
}
修改application.yml
server:
port: 8099
spring:
application:
name: consumer-service
redis:
jedis:
pool:
max-wait: 1000
max-idle: 8
min-idle: 0
timeout: 5000
host: 127.0.0.1
port: 6379
dubbo:
protocol:
name: dubbo
port: 29099
registry:
address: zookeeper://localhost:2181
修改dubbo-provider模块,首先,先添加TokenAuthFilter类
package com.young.filter;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@Activate
@Component
public class TokenAuthFilter implements Filter {
@Resource
private StringRedisTemplate stringRedisTemplate;
private static final String TOKEN_KEY="authorization";
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
String token = RpcContext.getServerAttachment().getAttachment(TOKEN_KEY);
String name = RpcContext.getServerAttachment().getAttachment("name");
System.out.println("token:"+token+",name:"+name);
if (token == null || !validateToken(token)) {
throw new RpcException("UnValid Token");
}
return invoker.invoke(invocation);
}
private boolean validateToken(String token) {
String resToken = stringRedisTemplate.opsForValue().get(TOKEN_KEY);
if (token.equals(resToken)) {
return true;
}
return false;
}
}
接着,在resource目录下,新建META-INF目录,在META-INF目录下创建dubbo目录,然后在dubbo目录下创建org.apache.dubbo.rpc.Filter文件,文件内容如下:
tokenAuthFilter=com.young.filter.TokenAuthFilter
# 格式为: 过滤器名称=过滤器对应的包路径
然后最重要的一点,就是修改application.yml
server:
port: 8089
spring:
application:
name: provider-service
redis:
jedis:
pool:
max-wait: 1000
max-idle: 8
min-idle: 0
timeout: 5000
host: 127.0.0.1
port: 6379
dubbo:
provider:
filter: tokenAuthFilter
dubbo:
provider:
filter: tokenAuthFilter #这个必须配置,不然filter不生效
registry:
address: zookeeper://127.0.0.1:2181
protocol:
name: dubbo
port: 29089
项目的完整结构如下:
启动项目,进行测试
访问/demo/token/yes
访问其他接口
第二种方式,是使用IP白名单来进行权限校验,我这里修改一下,改为使用服务名进行权限校验,我们先创建一个表,表中包含服务名称,以及调用者的服务名称,用来表示有哪些调用者可以使用当前服务,结构如下:
数据内容如下:
在dubbo-provider模块中,引入mysql和mybatis-plus的依赖
修改application.yml
server:
port: 8089
spring:
application:
name: provider-service
datasource:
username: root
password: 3fa4d180
driver-class-name: com.mysql.cj.jdbc.Driver
url: mysql:jdbc://localhost:3306/young?useSSL=false&serverTimezone
redis:
jedis:
pool:
max-wait: 1000
max-idle: 8
min-idle: 0
timeout: 5000
host: 127.0.0.1
port: 6379
dubbo:
provider:
filter: tokenAuthFilter
dubbo:
provider:
filter: tokenAuthFilter
registry:
address: zookeeper://127.0.0.1:2181
protocol:
name: dubbo
port: 29089
mybatis-plus:
global-config:
db-config:
logic-not-delete-value: 0
logic-delete-value: 1
添加ServiceAuth实体类
package com.young.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@TableName(value = "m_service_auth")
public class ServiceAuth implements Serializable {
@TableId(type = IdType.AUTO)
private Integer id;
private String providerService;
private String consumerService;
private LocalDateTime createTime;
@TableLogic
private Integer isDelete;
}
ServiceAuthMapper.java
package com.young.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.young.entity.ServiceAuth;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ServiceAuthMapper extends BaseMapper<ServiceAuth> {
}
ProviderConfigProperty.java
package com.young.config;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@Data
public class ProviderConfigProperty {
@Value("${spring.application.name}")
private String name;
}
ServiceAuthServiceImpl.java
package com.young.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.young.entity.ServiceAuth;
import com.young.mapper.ServiceAuthMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ServiceAuthServiceImpl {
@Autowired
private ServiceAuthMapper serviceAuthMapper;
public Boolean canVisit(String providerName,String consumerName){
LambdaQueryWrapper<ServiceAuth>queryWrapper=new LambdaQueryWrapper<>();
queryWrapper.eq(ServiceAuth::getProviderService,providerName)
.eq(ServiceAuth::getConsumerService,consumerName);
List<ServiceAuth> serviceAuths = serviceAuthMapper.selectList(queryWrapper);
if (serviceAuths!=null&&serviceAuths.size()>0){
return true;
}
return false;
}
}
修改TokenAuthFilter.java,这里有个坑,在dubbo的拦截器中,采用@Autowired自动注入是无效的,可以采取setter方式来注入其他的bean,且不用标注注解,dubbo自己会对这些bean进行注入
package com.young.filter;
import com.young.config.ProviderConfigProperty;
import com.young.service.ServiceAuthService;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.config.ProviderConfig;
import org.apache.dubbo.rpc.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@Activate
@Component
public class TokenAuthFilter implements Filter {
@Resource
private StringRedisTemplate stringRedisTemplate;
private ServiceAuthService serviceAuthService;
private ProviderConfigProperty providerConfigProperty;
public void setProviderConfigProperty(ProviderConfigProperty providerConfigProperty){
this.providerConfigProperty=providerConfigProperty;
}
public void setServiceAuthService(ServiceAuthService serviceAuthService){
this.serviceAuthService=serviceAuthService;
}
private static final String TOKEN_KEY="authorization";
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
// String token = RpcContext.getServerAttachment().getAttachment(TOKEN_KEY);
// String name = RpcContext.getServerAttachment().getAttachment("name");
// System.out.println("token:"+token+",name:"+name);
// if (token == null || !validateToken(token)) {
// throw new RpcException("UnValid Token");
// }
String consumerName = RpcContext.getServerContext().getRemoteApplicationName();
if (consumerName!=null){
if (!serviceAuthService.canVisit(providerConfigProperty.getName(),consumerName)){
throw new RpcException("UnValid Token");
}
}
return invoker.invoke(invocation);
}
private boolean validateToken(String token) {
String resToken = stringRedisTemplate.opsForValue().get(TOKEN_KEY);
if (token.equals(resToken)) {
return true;
}
return false;
}
}
dubbo-provider的完整结构如下:
dubbo-consumer不需要修改,然后我们运行项目
修改dubbo-consumer的服务名,然后重新运行项目,访问
参考文章:
pringboot之dubbo实现过滤器-yellowcong
调用链路传递隐式参数