阿里巴巴熔断器Sentinel框架的使用

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     }

5.结果

在这里插入图片描述

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bst@微胖子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值