sentinel+springboot+zookeeper,通过dashboard实现熔断(修改dashboard源码)

熔断功能紧接上一篇限流功能:

https://blog.csdn.net/zero_no1/article/details/104050089

好像有帖子说sentinel成为springcloud推荐的熔断,在调研限流的基础上有进行了熔断的调研,略微有些坑,搜了半天文章发现大家都是一样的,只实现了对限流的集成,看了下官方的源码,官方的源码只实现了对限流功能在zookeeper等可配置化的功能,并没有对熔断降级、热点这些的实现。其他文章无处借鉴.....没有办法,只好参照限流的动态配置以及结合熔断降级自己写了。以下内容均为原创哦~

首先看下熔断降级提供的三个方式

分别为RT,异常比例,异常数

RT为接口的响应时间,1秒钟同时有5个请求超过这个响应时间,则进行熔断

异常比例为一秒钟访问的接口数量,以及接口异常比例对应总访问量的比例

异常数即为一秒钟异常的个数

时间窗口即是在触发熔断时会持续的时间,举个栗子:触发熔断后,会在接下10秒内直接熔断,然后效果消失,如果在此触发熔断,会在持续10秒,以此类推

在这里直接贴出配置的json

[
  {
    "resource": "/action/test/test3",//访问的资源地址
    "count": 500,//访问的个数,此处是一个double值,若grade=1,则是0.1~1.0之间的一个值,0和2时则为毫秒
    "grade": 0,//0:RT 1:异常比例 2:异常数
    "timeWindow": 10 //时间窗口,秒为单位
  },
  {
    "resource": "POST:http://test-sentinel-provider/service/test/sentinel",
    "count": 500,
    "grade": 0,
    "timeWindow": 10
  }
]

在第二个json才是我们经常使用的微服务熔断,这个是sentinel支持springcloud的feign功能,需要在配置文件中进行开启,否则无法扫描到,配置如下:

feign:
  sentinel:
    enabled: true

说完配置,直接来说如何进行配置,首先直接说dashboard的配置:

在上一篇使用限流功能时阿里在test方法中提供了对限流功能的样例,那么这里同理,添加一个对服务降级的两个demo

首先打开zookeeperConfigUtil

public class ZookeeperConfigUtil {
    public static final String RULE_ROOT_PATH = "/sentinel_rule_config";

    public static final int RETRY_TIMES = 3;
    public static final int SLEEP_TIME = 1000;

    //之前ruleName不存在,因为只有针对限流的操作,此处需要增加一个路径,让每个项目有限流,熔断不同路径,同理限流需要同样修改
    public static String getPath(String ruleName, String appName) {
        StringBuilder stringBuilder = new StringBuilder(RULE_ROOT_PATH + "/" + ruleName);

        if (StringUtils.isBlank(appName)) {
            return stringBuilder.toString();
        }
        if (appName.startsWith("/")) {
            stringBuilder.append(appName);
        } else {
            stringBuilder.append("/")
                    .append(appName);
        }
        return stringBuilder.toString();
    }
}

zookeeperConfig

@Configuration
public class ZookeeperConfig {

    @Value("${zookeeper.servers}")
    private String zookeeperAddress;

    @Bean
    public Converter<List<FlowRuleEntity>, String> flowRuleEntityEncoder() {
        return JSON::toJSONString;
    }

    @Bean
    public Converter<String, List<FlowRuleEntity>> flowRuleEntityDecoder() {
        return s -> JSON.parseArray(s, FlowRuleEntity.class);
    }
    
    //此处为新增的降级转换类,在这里还是膜拜一下阿里的大神们,虽然没有提供test样例,但是设计模式上的通用上值得我们学习
    @Bean
    public Converter<List<DegradeRuleEntity>, String> degradeRuleEntityEncoder() {
        return JSON::toJSONString;
    }

    @Bean
    public Converter<String, List<DegradeRuleEntity>> degradeRuleEntityDecoder() {
        return s -> JSON.parseArray(s, DegradeRuleEntity.class);
    }

    @Bean
    public CuratorFramework zkClient() {
        CuratorFramework zkClient =
                CuratorFrameworkFactory.newClient(zookeeperAddress,
                        new ExponentialBackoffRetry(ZookeeperConfigUtil.SLEEP_TIME, ZookeeperConfigUtil.RETRY_TIMES));
        zkClient.start();

        return zkClient;
    }
}
DegradeRuleZookeeperProvider
@Component("degradeRuleZookeeperProvider")
public class DegradeRuleZookeeperProvider implements DynamicRuleProvider<List<DegradeRuleEntity>> {

    @Autowired
    private CuratorFramework zkClient;
    @Autowired
    private Converter<String, List<DegradeRuleEntity>> converter;

    @Override
    public List<DegradeRuleEntity> getRules(String appName) throws Exception {
        //这里添加的降级rule,限流需要同样修改
        String zkPath = ZookeeperConfigUtil.getPath("degrade_rule", appName);
        Stat stat = zkClient.checkExists().forPath(zkPath);
        if (stat == null) {
            zkClient.create().creatingParentContainersIfNeeded().withMode(CreateMode.PERSISTENT).forPath(zkPath, null);
        }
        byte[] bytes = zkClient.getData().forPath(zkPath);
        if (null == bytes || bytes.length == 0) {
            return new ArrayList<>();
        }
        String s = new String(bytes);

        return converter.convert(s);
    }
}
DegradeRuleZookeeperPublisher
@Component("degradeRuleZookeeperPublisher")
public class DegradeRuleZookeeperPublisher implements DynamicRulePublisher<List<DegradeRuleEntity>> {
    @Autowired
    private CuratorFramework zkClient;
    @Autowired
    private Converter<List<DegradeRuleEntity>, String> converter;

    @Override
    public void publish(String app, List<DegradeRuleEntity> rules) throws Exception {
        AssertUtil.notEmpty(app, "app name cannot be empty");

        String path = ZookeeperConfigUtil.getPath("degrade_rule", app);
        Stat stat = zkClient.checkExists().forPath(path);
        if (stat == null) {
            zkClient.create().creatingParentContainersIfNeeded().withMode(CreateMode.PERSISTENT).forPath(path, null);
        }
        byte[] data = CollectionUtils.isEmpty(rules) ? "[]".getBytes() : converter.convert(rules).getBytes();
        zkClient.setData().forPath(path, data);
    }
}

在这里修改原先zookeeper只支持限流的源码,改为同时支持限流、熔断。同理之后可以继续添加支持的规则

之后查看controller,直接找到对应熔断进行复制,将之前的controller注释即可

打开新增的controller进行修改

此处加一个说明,熔断方面sentinel支持对appName,IP,端口号进行区分。也就是说,哪怕是对于同一个项目,只要设置不同的IP或端口号也可以配置不同的熔断方案,此处为了方便,我只对appName进行了熔断配置,有兴趣的可以单独更改

@Controller
@RequestMapping(value = "/degrade", produces = MediaType.APPLICATION_JSON_VALUE)
public class DegradeControllerV2 {

    private final Logger logger = LoggerFactory.getLogger(DegradeControllerV2.class);

    @Autowired
    private InMemDegradeRuleStore repository;

    @Autowired
    private AuthService<HttpServletRequest> authService;

    //此处参照限流方法同样引入
    @Autowired
    @Qualifier("degradeRuleZookeeperProvider")
    private DynamicRuleProvider<List<DegradeRuleEntity>> ruleProvider;
    @Autowired
    @Qualifier("degradeRuleZookeeperPublisher")
    private DynamicRulePublisher<List<DegradeRuleEntity>> rulePublisher;

    //以下只展示修改的方法
    @ResponseBody
    @RequestMapping("/rules.json")
    public Result<List<DegradeRuleEntity>> queryMachineRules(HttpServletRequest request, String app, String ip, Integer port) {
        AuthUser authUser = authService.getAuthUser(request);
        authUser.authTarget(app, PrivilegeType.READ_RULE);
        logger.info("app is :" + app);
        if (StringUtil.isEmpty(app)) {
            return Result.ofFail(-1, "app can't be null or empty");
        }
        if (StringUtil.isEmpty(ip)) {
            return Result.ofFail(-1, "ip can't be null or empty");
        }
        if (port == null) {
            return Result.ofFail(-1, "port can't be null");
        }
        try {
            //获取的地址修改为从zookeeper获取
            List<DegradeRuleEntity> rules = ruleProvider.getRules(app);
            //此处必须添加,打开实体类可知,对应的rule的appName为主键,所以需要将可能缺失的appName赋值,否则报错
            if (rules != null && !rules.isEmpty()) {
                for (DegradeRuleEntity entity : rules) {
                    entity.setApp(app);
                }
            }
            rules = repository.saveAll(rules);
            return Result.ofSuccess(rules);
        } catch (Throwable throwable) {
            logger.error("queryApps error:", throwable);
            return Result.ofThrowable(-1, throwable);
        }
    }
    
    //此处为推送消息,将rules同步到zookeeper上
    private boolean publishRules(String app, String ip, Integer port) {
        logger.info("publish app is :" + app);
        List<DegradeRuleEntity> rules = repository.findAllByMachine(MachineInfo.of(app, ip, port));
        try {
            rulePublisher.publish(app, rules);
        } catch (Exception e) {
            return false;
        }
        return true;
    }
}

做完此处修改后,对dashboard进行打包,重新部署

然后是客户端的修改,首先添加feign的支持,上面已经说了,之后查看zookeeper获取地址

ZookeeperSentinelConfig
/**
 * @author zhangrui
 * @createDate 2020/1/14 0014
 */
@Slf4j
@Component
public class ZookeeperSentinelConfig {

    @Value("${spring.application.name}")
    private String appName;
    @Value("${zookeeper.servers}")
    private String zookeeperAddress;

    @PostConstruct
    public void loadRules() {
        //此处为限流地址获取
        final String flowPath = "/sentinel_rule_config/flow_rule/" + appName;
        ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new ZookeeperDataSource<>(zookeeperAddress, flowPath,
                source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {
                }));
        FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
        //此处为降级地址获取,之后有其他操作均需要配置然后引入,否则配置不会生效
        final String degradePath = "/sentinel_rule_config/degrade_rule/" + appName;
        ReadableDataSource<String, List<DegradeRule>> degradeRuleDataSource = new ZookeeperDataSource<>(zookeeperAddress, degradePath,
                source -> JSON.parseObject(source, new TypeReference<List<DegradeRule>>() {
                }));
        DegradeRuleManager.register2Property(degradeRuleDataSource.getProperty());
    }
}

完成以上配置即可,无需做其他操作,除非我们需要专门处理一些异常信息

之后我们写一个test测试一下,test写的相对low...,调用的接口直接让线程sleep超过当前RT值即可。其实就是每隔1秒同时访问10个请求访问5次,之后每隔1秒调用一次查看是否发生熔断


import common.http.request.CommonRequest;
import common.http.request.RequestParams;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

import java.io.IOException;

/**
 * @author zhangrui
 * @createDate 2020/03/26 0026
 */
public class TestSentinel {

    public String httpRequest(Request request) {
        OkHttpClient okHttpClient = new OkHttpClient();
        Call call = okHttpClient.newCall(request);
        try {
            Response response = call.execute();
            return response.body().string();
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    public static String url = "http://localhost:8080/action/test/test3";

    public String startRequest(String json) {
        String requestUrl = url;
        RequestParams headers = new RequestParams();
        headers.put("Post-ID", UidUtil.getUId());
        Request request = CommonRequest.createPostRequest(requestUrl, headers, json);
        return httpRequest(request);
    }

    public static void main(String[] args) throws Exception {
        String json = "{ \"test\": \"test\"}";
        for (int i = 0; i < 10; i++) {
            new Thread(new Runner(i)).start();
        }
        Thread.sleep(1000);
        for (int i = 11; i < 20; i++) {
            new Thread(new Runner(i)).start();
        }
        Thread.sleep(1000);
        for (int i = 21; i < 30; i++) {
            new Thread(new Runner(i)).start();
        }
        Thread.sleep(1000);
        for (int i = 31; i < 40; i++) {
            new Thread(new Runner(i)).start();
        }
        Thread.sleep(1000);
        for (int i = 41; i < 50; i++) {
            new Thread(new Runner(i)).start();
        }
        Thread.sleep(1000);
        for (int i = 51; i < 60; i++) {
            new Thread(new Runner(i)).start();
        }
        Thread.sleep(1000);
        for (int i = 61; i < 70; i++) {
            new Thread(new Runner(i)).start();
        }
        Thread.sleep(1000);
        long time1=System.currentTimeMillis();
        String result = new TestSentinel().startRequest(json);
        long time2=System.currentTimeMillis();
        System.out.println("测试结果1:" + result+",耗时:"+(time2-time1));
        Thread.sleep(1000);
        time1=System.currentTimeMillis();
        result = new TestSentinel().startRequest(json);
        time2=System.currentTimeMillis();
        System.out.println("测试结果2:" + result+",耗时:"+(time2-time1));
        Thread.sleep(1000);
        time1=System.currentTimeMillis();
        result = new TestSentinel().startRequest(json);
        time2=System.currentTimeMillis();
        System.out.println("测试结果3:" + result+",耗时:"+(time2-time1));
        Thread.sleep(1000);
        time1=System.currentTimeMillis();
        result = new TestSentinel().startRequest(json);
        time2=System.currentTimeMillis();
        System.out.println("测试结果4:" + result+",耗时:"+(time2-time1));
        Thread.sleep(1000);
        time1=System.currentTimeMillis();
        result = new TestSentinel().startRequest(json);
        time2=System.currentTimeMillis();
        System.out.println("测试结果5:" + result+",耗时:"+(time2-time1));
        Thread.sleep(1000);
        time1=System.currentTimeMillis();
        result = new TestSentinel().startRequest(json);
        time2=System.currentTimeMillis();
        System.out.println("测试结果6:" + result+",耗时:"+(time2-time1));
        Thread.sleep(1000);
        time1=System.currentTimeMillis();
        result = new TestSentinel().startRequest(json);
        time2=System.currentTimeMillis();
        System.out.println("测试结果7:" + result+",耗时:"+(time2-time1));
        Thread.sleep(1000);
        time1=System.currentTimeMillis();
        result = new TestSentinel().startRequest(json);
        time2=System.currentTimeMillis();
        System.out.println("测试结果8:" + result+",耗时:"+(time2-time1));
        Thread.sleep(1000);
        time1=System.currentTimeMillis();
        result = new TestSentinel().startRequest(json);
        time2=System.currentTimeMillis();
        System.out.println("测试结果9:" + result+",耗时:"+(time2-time1));
    }
}

class Runner implements Runnable {
    private int i;

    public Runner(int i) {
        this.i = i;
    }

    @Override
    public void run() {
        String json = "{ \"test\": \"test\"}";
        String result = new TestSentinel().startRequest(json);
        System.out.println("第" + i + "个结果:" + result);
    }
}

运行一下查看结果,由于使用微服务化,直接抛出了系统异常。在发生熔断后10秒后重新建立了链接

此处提一下我遇到的一个坑,为什么我要连续请求调用5次。因为在我第一次时,为了省事,我只调用了2次,然后发现没有发生熔断,让废了半天劲的我怀疑了半天人生.....

此处简单讲一下sentinel的原理,sentinel的原理是滑动窗口,将1秒分为两个500ms的map,统计的个数分别put进入map中进行统计,这在限流中是没有问题的。但是在熔断中,首先我们需要判断当前接口返回的时间是否超过了我们设定的过期值,但如果这时我们并没有收到上个接口的返回时间,熔断是没有生效的。

举个栗子:也就是说在第一秒时我们通过10个请求,在第二秒时又经过了10个请求,实际上应该拒绝掉第二秒的10个请求,但是第一秒的10个请求这时还在超时没有返回结果,并没有超过设定的过期值,因此没有发生熔断。所以导致第二秒的10个请求也通过了(实际在我测试时由于本地机器启动的项目过多,机器变慢,导致我线程睡眠1s的返回变成了2.5s才返回,需要在第3秒才会发生熔断),所以我们需要一段持续的访问等到第一秒的返回结果返回且这时还在有持续不断的请求访问时,才会触发熔断。以图例说明

由此看出,超时时间是在下一秒返回的,所以需要有持续的访问

第一秒的19个请求并没有被拒绝,而是在第二秒又访问的7个请求才会被拒绝。

这里说不上是熔断漏洞,而是需要验证才可以,明白了sentinel的原理才能更好使用

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值