--- title: 外部接口大量超时,把整个系统拖垮,引发雪崩!如何解决?熔断... --- # 外部接口大量超时,把整个系统拖垮,引发雪崩!如何解决?熔断... > 作者:Tom哥 >
公众号:微观技术 >
博客:[https://offercome.cn](https://offercome.cn) >
人生理念:知道的越多,不知道的越多,努力去学 大家好,我是Tom哥~ 互联网+ 时代,业务数字化已经蔓延到你能想到的各个行业。各种业务功能、营销玩法越来越多,系统也越来越复杂。 面对不断复杂的业务系统,脑子越来越不够用了
于是 `聪明的人们` 提出了 `微服务` 的设计思想 本着 `复杂的事情简单化` 的原则,我们将一个大的系统拆分成若干个子系统,每个 `子系统` 职责单一,按 DDD 的设计理念,承载一个子域的业务建设。 于是,人们可以将精力聚焦,专心完成某一个业务点的深度建设。 多个微服务系统之间通过 `RPC` 框架(如:dubbo、spring cloud、gRPC 等)完成了串联,但随着调用量越来越大,人们发现服务与服务之间的稳定性变得越来越重要
**举个例子:** * Service D 挂了,响应很慢 * Service G 和 Service F ,都依赖 Service D,也会受到牵连,对外响应也会变慢 * 影响层层向上传递,Service A 和 Service B 也会被拖垮 * 最后,引发雪崩效应,系统的故障影响面会越来越大 为了解决这种问题,我们需要引入 `熔断` 机制。 **“当断则断,不受其乱。 当断不断,必受其难”** ## 什么是熔断? 熔断,其实是对调用链路中某个资源出现不稳定状态时(如:调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。 当资源被降级后,在接下来的`降级时间窗口`内,对该资源的调用都自动熔断(默认是抛出 `BlockException`) 目前市面上的熔断框架很多,如:`Sentinel`、`Hystrix`、`Resilience4j` 等,这些框架的设计理念都差不多。 本文重点讲下 Sentinel 是如何在项目中使用的 Sentinel (分布式系统的流量防卫兵) 是阿里开源的一套用于服务容错的综合性解决方案。它以流量为切入点, 从`流量控制`、`熔断降级`、`系统负载保护`等多个维度来保护服务的稳定性。 ### 核心分为两部分 1、核心库(Java 客户端):能够运行在所有 Java 环境,对 Dubbo 、Spring Cloud 等框架也有较好的支持。 2、控制台(Dashboard):基于 Spring Boot 开发,打包后可以直接运行。 ### Sentinel 熔断种类 * RT 响应时间 * 异常数 * 异常比例 ## 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 ``` ### 登陆Sentinal控制台
默认用户和密码都是 sentinel ,登录成功后的界面如下,先来个直观感受
### 控制台配置熔断规则
这里表示熔断策略选择 `慢调用比例`,响应时间超过200毫秒则标记为慢请求。如果在一个1000 ms的统计周期内(可自行调整),慢请求比例超过30%且数量超过3个,则对后续请求进行熔断,熔断时长为10秒钟,10秒以后恢复正常。 ### 注解式接入 接入非常简单,只需要提前在控制台配置好`资源规则`,然后在代码中添加 `@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` 框架更加人性化,体验更好。 > 注意:注解式开发,需要添加在方法上,作用域范围相对固定。下面的项目实战中,我们也可以采用 `显示` 形式,可以灵活圈定代码块范围。 ## 项目实战 我们这边有个项目,考虑到客户的部署成本,想做一个轻量级方案,需求如下: * 既想引入框架的熔断功能,又不想部署控制台 * 拦截点相对收拢,类似与dubbo消费端远程访问一样,在代理类的远程通讯位置做拦截处理 ### 概要方案--流程图
1、我们通过 `Proxy.newProxyInstance` 为所有的接口创建了代理子类 2、所有对代理子类的方法调用全部收拢到 `InvocationHandler` 3、我们讲类名和方法名做一个拼接,然后去 `熔断规则表`查询,看是否配置了规则 4、如果没有,那么走常规则远程调用逻辑 5、如果有,将远程调用逻辑纳入 `Sentinel` 的监控管辖 6、如果触发了 熔断机制,则直接抛出 `BlockException` ,上层业务拦截异常,做特殊处理,比如:修饰下给用户更合适的文案提示。 ### 熔断状态机
核心的代码逻辑,继续往下看 ### 引入 Sentinel 的依赖包 ``` com.alibaba.csp sentinel-core 1.8.3 ``` ### 熔断规则表设计 ``` 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 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 rules = null; List 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 { // 常规处理,不走熔断判断逻辑 // 省略 } } ``` ### 测试数据
> 上面案例的源代码已经上传到 GitHub,关注公众号:微观技术,回复关键词:「1819」 即可获取