外部接口大量超时,把整个系统拖垮,引发雪崩!如何解决?
点击关注公众号,Java干货及时送达
粉丝福利:小编会从今天留言的小伙伴中随机抽赠送8.88元现金红包。娱乐抽奖,大家随缘积极参与啦,给生活一点小幸运~感谢大家的支持
互联网+ 时代,业务数字化已经蔓延到你能想到的各个行业。各种业务功能、营销玩法越来越多,系统也越来越复杂。
面对不断复杂的业务系统,脑子越来越不够用了。
于是聪明的人们提出了微服务的设计思想。
本着复杂的事情简单化的原则,我们将一个大的系统拆分成若干个子系统,每个子系统职责单一。按 DDD 的设计理念,承载一个子域的业务建设。
于是,人们可以将精力聚焦,专心完成某一个业务点的深度建设。
多个微服务系统之间通过 RPC 框架(如 Dubbo、Spring Cloud、gRPC 等)完成了串联,但随着调用量越来越大,人们发现服务与服务之间的稳定性变得越来越重要。
举个例子:
Service D 挂了,响应很慢; Service G 和 Service F 都依赖 Service D 也会受到牵连,对外响应也会变慢; 影响层层向上传递,Service A 和 Service B 也会被拖垮; 最后引发雪崩效应,系统的故障影响面会越来越大。
为了解决这种问题,我们需要引入熔断机制。“当断则断,不受其乱。当断不断,必受其难”。
什么是熔断?
熔断,其实是对调用链路中某个资源出现不稳定状态时(如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败。避免影响到其它的资源而导致级联错误。
当资源被降级后,在接下来的降级时间窗口内,对该资源的调用都自动熔断(默认是抛出 BlockException)。
目前市面上的熔断框架很多,如 Sentinel、Hystrix、Resilience4j 等,这些框架的设计理念都差不多。
本文重点讲下 Sentinel 是如何在项目中使用的。
Sentinel (分布式系统的流量防卫兵)是阿里开源的一套用于服务容错的综合性解决方案。它以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来保护服务的稳定性。
核心分为两部分:
核心库(Java 客户端):能够运行在所有 Java 环境,对 Dubbo 、Spring Cloud 等框架也有较好的支持。
控制台(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 毫秒则标记为慢请求。如果在一个 1000ms 的统计周期内(可自行调整)。慢请求比例超过 30% 且数量超过 3 个,则对后续请求进行熔断,熔断时长为 10 秒钟。10 秒以后恢复正常。
注解式接入
接入非常简单,只需要提前在控制台配置好资源规则,然后在代码中添加 @SentinelResource 注解即可。
// 资源名称为 handle1
"/handle1") (
"handle1", blockHandler = "blockHandlerTestHandler") (value =
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 的依赖包:
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-coreartifactId>
<version>1.8.3version>
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 秒运行一次。
"0/10 * * * * ? ") (cron =
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<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 方法。
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 {
// 常规处理,不走熔断判断逻辑
// 省略
}
}
实验结果: