1.使用的项目背景
系统的庞大,多个微服务系统之间通过 RPC 框架(如:dubbo、spring cloud、gRPC 等)完成了串联,但随着调用量越来越大,人们发现服务与服务之间的稳定性变得越来越重要。
举个例子:
- Service D 挂了,响应很慢
- Service G 和 Service F ,都依赖 Service D,也会受到牵连,对外响应也会变慢
- 影响层层向上传递,Service A 和 Service B 也会被拖垮
- 最后,引发雪崩效应,系统的故障影响面会越来越大
为了解决这种问题,我们需要引入 熔断 机制。“当断则断,不受其乱。当断不断,必受其难”
2.Sentinel 安装
都是方法层面的进行熔断处理,不推荐使用,改动量太大。
首先,官网下载 sentinel 控制台安装包
下载地址:https://github.com/alibaba/Sentinel/releases
下载 Jar 包后,打开终端,运行命令
java -Dserver.port=8180 -Dcsp.sentinel.dashboard.server=localhost:8180 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.1.jar
注解式接入
接入非常简单,只需要提前在控制台配置好资源规则,然后在代码中添加 @SentinelResource注解即可。
// 资源名称为handle1
@RequestMapping("/handle1")
@SentinelResource(value = "handle1", blockHandler = "blockHandlerTestHandler")
public String handle1(String params) {
// 业务逻辑处理
return "success";
}
// 接口方法 handle1 的 兜底方法
public String blockHandlerTestHandler(String params, BlockException blockException) {
return "兜底返回";
}
达到阈值后,系统的默认提示是一段英文,很不友好,我们可以自定义兜底方法。在@SentinelResource注解中进一步配置 blockHandler、fallback 属性字段
- blockHandler:主观层面,如果被限流或熔断,则调用该方法,进行兜底处理
- fallback:对业务的异常兜底,比如,执行过程中抛了各种Exception,则调用该方法,进行兜底处理
通过上面两层兜底,可以让Sentinel 框架更加人性化,体验更好。
3.项目中使用熔断器
1、我们通过 Proxy.newProxyInstance 为所有的接口创建了代理子类
2、所有对代理子类的方法调用全部收拢到 InvocationHandler
3、我们讲类名和方法名做一个拼接,然后去 熔断规则表查询,看是否配置了规则
4、如果没有,那么走常规则远程调用逻辑
5、如果有,将远程调用逻辑纳入 Sentinel 的监控管辖
6、如果触发了 熔断机制,则直接抛出 BlockException ,上层业务拦截异常,做特殊处理,比如:修饰下给用户更合适的文案提示。
4.上代码
首先,引入 Sentinel 的依赖包:
<!-- 限流、熔断框架 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.8.3</version>
</dependency>
熔断规则表设计:
CREATE TABLE `degrade_rule` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`resource_name` varchar(256) NOT NULL COMMENT '资源名称',
`count` double NOT NULL COMMENT '慢调用时长,单位 毫秒',
`slow_ratio_threshold` double NOT NULL COMMENT '慢调用比例阈值',
`min_request_amount` int NOT NULL COMMENT '熔断触发的最小请求数',
`stat_interval` int NOT NULL COMMENT '统计时长,单位 毫秒',
`time_window` int NOT NULL COMMENT '熔断时长,单位为 s',
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_resource_name` (`resource_name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb3 COMMENT='熔断规则表';
早期可以采用更简单粗暴方式,在数据库表手动初始化数据。如果要调整规则,走 SQL 订正。
为了尽可能实时感知规则表数据变更,开发了定时任务,每 10 秒运行一次。
@Scheduled(cron = "0/10 * * * * ? ")
public void loadDegradeRule() {
List<DegradeRuleDO> degradeRuleDOList = degradeRuleDao.queryAllRule();
if (CollectionUtils.isEmpty(degradeRuleDOList)) {
return;
}
String newMd5Hex = DigestUtils.md5Hex(JSON.toJSONString(degradeRuleDOList));
if (StringUtils.isBlank(newMd5Hex) || StringUtils.equals(lastMd5Hex, newMd5Hex)) {
return;
}
List<DegradeRule> rules = null;
List<String> resourceNameList = new ArrayList<>();
rules = degradeRuleDOList.stream().map(degradeRuleDO -> {
//资源名,即规则的作用对象
DegradeRule rule = new DegradeRule(degradeRuleDO.getResourceName())
// 熔断策略,支持慢调用比例/异常比例/异常数策略
.setGrade(CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType())
//慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值
.setCount(degradeRuleDO.getCount())
// 熔断时长,单位为 s
.setTimeWindow(degradeRuleDO.getTimeWindow())
// 慢调用比例阈值
.setSlowRatioThreshold(degradeRuleDO.getSlowRatioThreshold())
//熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断
.setMinRequestAmount(degradeRuleDO.getMinRequestAmount())
//统计时长(单位为 ms)
.setStatIntervalMs(degradeRuleDO.getStatInterval());
resourceNameList.add(degradeRuleDO.getResourceName());
return rule;
}).collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(rules)) {
DegradeRuleManager.loadRules(rules);
ConsumerProxyFactory.resourceNameList = resourceNameList;
lastMd5Hex = newMd5Hex;
}
log.error("[DegradeRuleConfig] 熔断规则加载: " + rules);
}
考虑到规则变更频率不会很高,没有必要每次都DegradeRuleManager.loadRules重新加载规则。这里设计了个小窍门
DigestUtils.md5Hex(JSON.toJSONString(degradeRuleDOList));
对查询的规则内容 JSON 序列化,然后计算其md5摘要,如果跟上一次的结果一致,说明这期间没有变更,直接 return ,不做处理。
定义子类,实现了 InvocationHandler 接口。通过 Proxy.newProxyInstance 为目标接口创建一个代理子类。
这样,每次调用接口方法,实际都是在调用 invoke 方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Class<?> clazz = proxy.getClass().getInterfaces()[0];
String urlCode = clazz.getName() + "#" + method.getName();
if (resourceNameList.contains(urlCode)) {
// 增加熔断处理
Entry entry = null;
try {
entry = SphU.entry(urlCode); //就是匹配上了规则
// 远程网络调用,获取结果
responseString = HttpClientUtil.postJsonRequest(url, header, body);
} catch (BlockException blockException) {
// 触发熔断
log.error("degrade trigger ! remote url :{} ", urlCode);
throw new DegradeBlockExcetion(urlCode);
} finally {
if (entry != null) {
entry.exit();
}
}
} else {
// 常规处理,不走熔断判断逻辑
// 省略
}
}
所有的接口通过代理类来管理,在代理类中进行熔断器逻辑处理,利用动态代理的特性,不改变方法的前提下对方法进行增强。
这个讲一下动态代理的用法:
//抽象角色(动态代理只能代理接口)
public interface Subject {
public void request();
}
//真实角色:实现了Subject的request()方法
public class RealSubject implements Subject{
public void request(){
System.out.println("From real subject.");
}
}
1 //实现了InvocationHandler
2 public class DynamicSubject implements InvocationHandler
3 {
4 private Object obj;//这是动态代理的好处,被封装的对象是Object类型,接受任意类型的对象
5
6 public DynamicSubject()
7 {
8 }
9
10 public DynamicSubject(Object obj)
11 {
12 this.obj = obj;
13 }
14
15 //这个方法不是我们显示的去调用
16 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
17 {
18 System.out.println("before calling " + method);
19
20 method.invoke(obj, args);
21
22 System.out.println("after calling " + method);
23
24 return null;
25 }
26
27 }
1 //实现了InvocationHandler
2 public class DynamicSubject implements InvocationHandler
3 {
4 private Object obj;//这是动态代理的好处,被封装的对象是Object类型,接受任意类型的对象
5
6 public DynamicSubject()
7 {
8 }
9
10 public DynamicSubject(Object obj)
11 {
12 this.obj = obj;
13 }
14
15 //这个方法框架会后台帮我们调用
16 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
17 { //这里可以写熔断器逻辑
18 System.out.println("before calling " + method);
19
20 method.invoke(obj, args);
21
22 System.out.println("after calling " + method);
23
24 return null;
25 }
26
27 }
1 //客户端:生成代理实例,并调用了request()方法 //调用代理方法
2 public class Client {
3
4 public static void main(String[] args) throws Throwable{
5 // TODO Auto-generated method stub
6
7 Subject rs=new RealSubject();//这里指定被代理类
8 InvocationHandler ds=new DynamicSubject(rs);
9 Class<?> cls=rs.getClass();
10
11 //以下是一次性生成代理
12
13 Subject subject=(Subject) Proxy.newProxyInstance(
14 cls.getClassLoader(),cls.getInterfaces(), ds);
15
16 //这里可以通过运行结果证明subject是Proxy的一个实例,这个实例实现了Subject接口
17 System.out.println(subject instanceof Proxy);
18
19 //这里可以看出subject的Class类是$Proxy0,这个$Proxy0类继承了Proxy,实现了Subject接口
20 System.out.println("subject的Class类是:"+subject.getClass().toString());
21
22 System.out.print("subject中的属性有:");
23
24 Field[] field=subject.getClass().getDeclaredFields();
25 for(Field f:field){
26 System.out.print(f.getName()+", ");
27 }
28
29 System.out.print("\n"+"subject中的方法有:");
30
31 Method[] method=subject.getClass().getDeclaredMethods();
32
33 for(Method m:method){
34 System.out.print(m.getName()+", ");
35 }
36
37 System.out.println("\n"+"subject的父类是:"+subject.getClass().getSuperclass());
38
39 System.out.print("\n"+"subject实现的接口是:");
40
41 Class<?>[] interfaces=subject.getClass().getInterfaces();
42
43 for(Class<?> i:interfaces){
44 System.out.print(i.getName()+", ");
45 }
46
47 System.out.println("\n\n"+"运行结果为:");
48 subject.request();
49 }
50 }