文章目录
- 一、前言
- 二、技术思路及方案
- 2.1 实现思路
- 2.2 实现方案
- 2.2.1 nacos动态数据源实现类关系图
- 三、功能实现
- 3.1 快速集成方案
- 3.1.1 引入依赖
- 3.1.2 服务端熔断降级
- 3.1.3 feign调用降级
- 四、扩展
- 4.1 SPI机制
- 4.2 自定义Slot实现
- 4.3 基于 Sentinel 实现 Feign 全局异常兜底
- 4.3.1 扩展SentinelFeign Builder
- 4.3.1.1 思路
- 4.3.1.2 程序设计
- 4.3.2 使用
一、前言
关于Sentinel和Hystrix之间对比以及Sentinel原理在官方文档有详细文档,这里就不再做多余赘述,Sentinel常规集成通常是借助Sentinel Dashboard服务端整合实现服务的限流、熔断降级以及多维护的监控。但是项目当下已经集成promethus监控、aws云原生服务自带流量监控等,因此Sentinel Dashboard服务端提供的多维监控模项目需求优先级并不高。综合项目实际情况以及节约成本的理念我们提出:Spring Cloud + Sentinel + nacos 动态数据源模式(无Dashboard服务端)实现微服务的服务降级功能。
本文主要包含围Sentinel绕微服务的服务降级功能实现、自定义slot实现熔断降级预警功能以及基于-sentinel-实现-feign-全局异常兜底。
二、技术思路及方案
2.1 实现思路
从官方提供Sentinel整体架构可以看出Dashboard服务端在Sentinel整体架构中仅负责规则配置、实时监控、机器发现等辅助模块。
实际处理流控、熔断降级是Sentinel-core完成。因此剥离Dashboard服务端,独立实现服务的熔断降级功能是可行的。
调研官方文档不难发现,Sentinel针对Spring Cloud微服务提供了依赖:
- spring-cloud-starter-alibaba-sentinel 微服务快速集成Sentinel提供支持
- spring-cloud-alibaba-sentinel-datasource Sentinel规则动态数据源支持自动化配置
- sentinel-datasource-nacos 提供了Sentinel规则动态数据源支持。
2.2 实现方案
如上图,基于nacos配置中心实现Sentinel规则动态数据源管理,微服务启动时拉取熔断降级规则并维持心跳动态更新数据源配置。
2.2.1 nacos动态数据源实现类关系图
根据源码分析,可以看出nacos动态数据源实现如下:
从入口程序SentinelAutoConfiguration开始,应用程序从环境配置Properties获取指定的数据源配置,最终通过静态规则管理类DegradeRuleManager注册到数据源,从而实现动态刷新规则配置。
三、功能实现
3.1 快速集成方案
3.1.1 引入依赖
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-sentinel-datasource</artifactId>
</dependency>
<dependency><groupId>com.alibaba.csp</groupId><artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
spring-cloud-starter-alibaba-sentinel 默认开启sentinel功能,引入依赖便可以使用sentinel,源码片段如下:
@Configuration(proxyBeanMethods = false
)
@ConditionalOnProperty(name = {"spring.cloud.sentinel.enabled"},matchIfMissing = true
)
@EnableConfigurationProperties({SentinelProperties.class})
public class SentinelAutoConfiguration {
spring-cloud-alibaba-sentinel-datasource依赖会从数据源中动态加载sentinel规则,源码片段如下:
# AbstractDataSourceProperties
public void postRegister(AbstractDataSource dataSource) {switch(this.getRuleType()) {case FLOW:FlowRuleManager.register2Property(dataSource.getProperty());break;case DEGRADE:DegradeRuleManager.register2Property(dataSource.getProperty());break;case PARAM_FLOW:ParamFlowRuleManager.register2Property(dataSource.getProperty());break;case SYSTEM:SystemRuleManager.register2Property(dataSource.getProperty());break;case AUTHORITY:AuthorityRuleManager.register2Property(dataSource.getProperty());break;case GW_FLOW:GatewayRuleManager.register2Property(dataSource.getProperty());break;case GW_API_GROUP:GatewayApiDefinitionManager.register2Property(dataSource.getProperty());}}
3.1.2 服务端熔断降级
@SentinelResource 可以作用于方法上的熔断降级保护,并提供可选的异常处理和 fallback 配置项。 @SentinelResource 注解包含以下属性:
- value:资源名称,必需项(不能为空),如果不填,会自动以全路径为key
- entryType:entry 类型,可选项(默认为 EntryType.OUT)
- blockHandler / blockHandlerClass: blockHandler 对应处理 BlockException 的函数名称,可选项。blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
- fallback:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求:
- 返回值类型必须与原函数返回值类型一致;
- 方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常
- fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
- defaultFallback(since 1.6.0):默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所以类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。defaultFallback函数要求与fallback一致。
- exceptionsToIgnore(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。
这里补充说明下blockHandler和fallback触发机制
- fallback如上述所讲,是异常降级兜底函数,当资源函数出现异常将会进入fallback如上述所讲。
- blockHandler是当资源函数某项指标超过设定的规则时触发
异常 | 说明 |
---|---|
FlowException | 限流异常 |
ParamFlowException | 热点参数限流的异常 |
DegradeException | 降级异常 |
AuthorityException | 授权规则异常 |
SystemBlockException | 系统规则异常 |
代码示例
下面代码示例通过@SentinelResource 注解在方法上进行埋点,标记getBaseUserInfo1函数为Sentinel资源,并指定了兜底函数和降级函数。
public class DsUserBaseQueryApplicationImpl implements DsUserBaseQueryApplication {@Override@SentinelResource(value = "baseUserInfo", entryType = EntryType.IN, fallback = "defaultFallback", blockHandler = "exceptionHandler")public String getBaseUserInfo1(String userId) {if (StringUtils.isEmpty(userId)) {throw new IllegalArgumentException("userId is empty.");}return System.currentTimeMillis() + userId;}//默认的 fallback 函数名称public String defaultFallback(String userId) {log.info("Go to default fallback");return "defaultFallback降级了";}// Block 异常处理函数,参数最后多一个 BlockException,其余与原函数一致.public String exceptionHandler(String userId, BlockException ex) {log.error("blockHandler服务降级了", ex);// Do some log here.return "Oops,blockHandler, error occurred at " + userId;}
}
增加动态数据源配置
动态数据源配置直接在SpringCloud配置模块增加sentinel.datasource数据源,支持flow限流规则和degrade降级规则。在flow/degrade层下添加具体的数据源配置介质,下面为基于nacos配置中心介质的动态数据源配置
#sentinel配置相关
spring:cloud:sentinel:datasource:flow:nacos:server-addr: ${spring.cloud.nacos.config.server-addr}dataId: ${spring.application.name}-flow-rulesgroupId: SENTINEL_GROUP# 规则类型,取值见:# org.springframework.cloud.alibaba.sentinel.datasource.RuleTyperule-type: flownamespace: ${xxxx.sentinel.nacos.namespace}degrade:nacos:server-addr: ${spring.cloud.nacos.config.server-addr}dataId: global-sentinel-degrade-rulesgroupId: SENTINEL_GROUPrule-type: degradedata-type: jsonnamespace: ${spring.cloud.nacos.discovery.namespace}
指定资源熔断规则
上文通过动态数据源配置指定了nacos降级规则配置文件,配置文件采用json格式的数组配置,详细配置如下:
[{"resource":"baseUserInfo", # 资源名称"grade":2, # 规则编号,2代表异常次数降级规则"count":5, # 阈值"timeWindow":10, # 降级窗口时间,单位s"MinRequestAmount": 2 # 最小触发请求数}
]
降级规则结果验证测试
配置完降级规则启动服务,首次访问接口,参数传递为空,服务端资源出现异常,直接进入兜底函数。
http://192.168.132.49:7041/user/base?userId=
下图为fallback兜底函数降级结果:
此后一秒内连续5次访问后,资源异常次数达到阈值,服务进入
blockHandler规则降级函数,并且在此后10秒内都会进入规则降级流程。下图为异常次数达到阈值后,进行blockHandler规则降级结果:
3.1.3 feign调用降级
开启sentinel feign支持
要启用sentinel feign降级功能需要在应用配置中显示关闭Spring Cloud 默认Hystrix降级开关 和 启用 feign sentinel 开启:
#打开sentinel对feign的支持
feign:sentinel:enabled: truehystrix:enabled: false
开启feign支持后,应用启动将初始化sentinel feign 资源:
@Configuration(proxyBeanMethods = false
)
@ConditionalOnClass({SphU.class, Feign.class})
public class SentinelFeignAutoConfiguration {public SentinelFeignAutoConfiguration() {}@Bean@Scope("prototype")@ConditionalOnMissingBean@ConditionalOnProperty(name = {"feign.sentinel.enabled"})public Builder feignSentinelBuilder() {return SentinelFeign.builder();}
}
sentinel 对@FeignClient 注解中的所有属性,Sentinel 都做了兼容,查看源码片段:
if (Void.TYPE != fallback) {Object fallbackInstance = this.getFromContext(beanName, "fallback", fallback, target.type());return new SentinelInvocationHandler(target, dispatch, new feign.hystrix.FallbackFactory.Default(fallbackInstance));
} else if (Void.TYPE != fallbackFactory) {FallbackFactory fallbackFactoryInstance = (FallbackFactory)this.getFromContext(beanName, "fallbackFactory", fallbackFactory, FallbackFactory.class);return new SentinelInvocationHandler(target, dispatch, fallbackFactoryInstance);
} else {return new SentinelInvocationHandler(target, dispatch);
}
sentinel会根据定义的feing接口构建相应的资源,资源名策略定义:httpmethod:protocol://requesturl。
编码,定义feign调用服务端和调用方
# api 定义
@GetMapping(value = "/inner/user/base")
String getBaseUserInfo(@RequestParam("userId") String userId);# feign api 定义
@FeignClient(contextId = "dsUserBaseApiClient", name = "xxxx", fallback = DsUserBaseApiClientFallback.class, configuration = FeignFallbackConfiguration.class)
public interface DsUserBaseApiClient extends DsUserBaseApi {
}# feign 接口调用
@GetMapping(value = "/user/info")
public String getBaseUserInfo1(String userId) {return client.getBaseUserInfo(userId);
}# feign server 定义
@Override
public String getBaseUserInfo(String userId) {log.info("降级测试start...");try {log.info("降级测试, 我开始休眠了...");Thread.sleep(3 * 1000);} catch (InterruptedException e) {e.printStackTrace();}log.info("降级测试, 我睡醒了...");return baseQueryApplication.getBaseUserInfo(userId);
}
配置feign超时规则
0-超时异常降级策略,阈值2000ms
[{"resource": "GET:http://demo-xxxx-server/inner/user/base","grade": 0,"count": 2000,"timeWindow":10}
]
项目默认超时时间10 * 1000MS,Server接口设置睡眠时间3 * 1000MS,阈值2 * 1000 ms,访问接口feign会正常返回。
使用Jmster进行压力测试,在50 * 50 循环调用过程,当feign调用平均响应时间超过设定的阈值后,将会提前进行熔断降级,调用feign接口定义的fallback函数,而不是一直等待服务端响应。
四、扩展
在 Sentinel 里面,所有的资源都对应一个资源名称(resourceName),每次资源调用都会创建一个 Entry 对象。Entry 可以通过对主流框架的适配自动创建,也可以通过注解的方式或调用 SphU API 显式创建。Entry 创建的时候,同时也会创建一系列功能插槽(slot chain):
- NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
- ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;
- StatisticSlot 则用于记录、统计不同纬度的 runtime 指标监控信息;
- FlowSlot 则用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制;
- AuthoritySlot 则根据配置的黑白名单和调用来源信息,来做黑白名单控制;
- DegradeSlot 则通过统计信息以及预设的规则,来做熔断降级;
- SystemSlot 则通过系统的状态,例如 load1 等,来控制总的入口流量;
ProcessorSlotChain(核心骨架):将不同的 Slot 按照顺序串在一起(责任链模式),从而将不同的功能(限流、降级、系统保护)组合在一起。slot chain 其实可以分为两部分:统计数据构建部分(statistic)和判断部分(rule checking)。
系统会为每个资源创建一套SlotChain。
Sentinel框架对feign适配自动为feign创建Entry,源码片段如下:
# SentinelInvocationHandler.invoke(...)
String resourceName = methodMetadata.template().method().toUpperCase() + ":" + hardCodedTarget.url() + methodMetadata.template().path();
Entry entry = null;Object var12;
try {Throwable ex;try {ContextUtil.enter(resourceName);entry = SphU.entry(resourceName, EntryType.OUT, 1, args);result = methodHandler.invoke(args);return result;} catch (Throwable var22) {ex = var22;if (!BlockException.isBlockException(var22)) {Tracer.trace(var22);}}if (this.fallbackFactory == null) {throw var22;}
Sentinel框架通过AOP 切莫入口SentinelResourceAspect为@SentinelResource注解标记的资源自动创建Entry对象,源码片段如下:
@Around("sentinelResourceAnnotationPointcut()")
public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable {Method originMethod = this.resolveMethod(pjp);SentinelResource annotation = (SentinelResource)originMethod.getAnnotation(SentinelResource.class);if (annotation == null) {throw new IllegalStateException("Wrong state for SentinelResource annotation");} else {String resourceName = this.getResourceName(annotation.value(), originMethod);EntryType entryType = annotation.entryType();int resourceType = annotation.resourceType();Entry entry = null;Object var10;try {Object var18;try {# 为资源构建 entry对象entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs());Object result = pjp.proceed();var18 = result;return var18;} catch (BlockException var15) {var18 = this.handleBlockException(pjp, annotation, var15);return var18;} catch (Throwable var16) {Class<? extends Throwable>[] exceptionsToIgnore = annotation.exceptionsToIgnore();if (exceptionsToIgnore.length > 0 && this.exceptionBelongsTo(var16, exceptionsToIgnore)) {throw var16;}}if (!this.exceptionBelongsTo(var16, annotation.exceptionsToTrace())) {throw var16;}this.traceException(var16);var10 = this.handleFallback(pjp, annotation, var16);} finally {if (entry != null) {entry.exit(1, pjp.getArgs());}}return var10;}
}
4.1 SPI机制
Sentinel槽链中Slot执行顺序是固定的,但并不是绝对的。Sentinel将ProcessorSlot作为SPI接口进行扩展,使得SlotChain具备了扩展能力。用户可以自定义Slot并编排Slot间的顺序。
下图为Sentinel默认Slot链路实现:
4.2 自定义Slot实现
熔断降级是保障微服务稳定性的重要手段,而在服务降级前提前预警,以便开发人员提前处理导致请求响应超时、接口异常等问题能够更加有效保障微服务的稳定性。
自定义Slot实现降级提前预警功能
熔断降级提前预警实现思路是分析了Sentinel默认ProcessorSlotChain构建思路并结合SPI机制,自定义熔断降级提前预警Slot并重新构建ProcessorSlotChain。代码实现如下:
# 降级预警实现
@Slf4j
public class DegradeEarlyWarningSlot extends AbstractLinkedProcessorSlot<DefaultNode> {/*** 从熔断降级规则管理器中提取降级规则并构建预警阈值规则* @param resource* @return*/private List<DegradeRule> getRuleProvider(String resource) {List<DegradeRule> rules = DegradeRuleManager.getRules();List<DegradeRule> earlyWarningRuleList = Lists.newArrayList();for (DegradeRule rule : rules) {DegradeRule earlyWarningRule = new DegradeRule();BeanUtils.copyProperties(rule, earlyWarningRule);double earlyWarningRuleCount;if (rule.getGrade() == 2) { // 异常数取异常阈值-1earlyWarningRuleCount = rule.getCount() - 1;} else { // 异常比例 和 平均超时时间取阈值的80%作为提前预警阈值earlyWarningRuleCount = rule.getCount() * 0.8;}earlyWarningRule.setCount(earlyWarningRuleCount);earlyWarningRuleList.add(earlyWarningRule);}return earlyWarningRuleList.stream().filter(rule -> resource.equals(rule.getResource())).collect(Collectors.toList());}/*** get origin rule** @param resource* @return*/private DegradeRule getOriginRule(String resource) {List<DegradeRule> originRule = DegradeRuleManager.getRules().stream().filter(rule -> rule.getResource().equals(resource)).collect(Collectors.toList());if (CollectionUtils.isEmpty(originRule)) {return null;}return originRule.get(0);}@Overridepublic void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode defaultNode, int count, boolean prioritized, Object... args) throws Throwable {String resource = context.getCurEntry().getResourceWrapper().getName();List<DegradeRule> rules = getRuleProvider(resource);// 这里日志打印只是为了演示,后期计划集成disputhcer内存队列 + 飞书预警if (rules != null) {for (DegradeRule rule : rules) {if (!rule.passCheck(context, defaultNode, count)) {DegradeRule originRule = getOriginRule(resource);String originRuleCount = originRule == null ? "未知" : String.valueOf(originRule.getCount());log.info("DegradeEarlyWarning: 服务{} 资源{} 目前的熔断指标已经超过{},接近配置的熔断阈值:{},",rule.getLimitApp(),resource,rule.getCount(),originRuleCount);break;}}}fireEntry(context, resourceWrapper, defaultNode, count, prioritized, args);}@Overridepublic void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {this.fireExit(context, resourceWrapper, count, args);}
}
实现SlotChainBuilder,重新定义ProcessorSlotChain。
public class CustomerSlotChainBuilder implements SlotChainBuilder {public CustomerSlotChainBuilder() {}public ProcessorSlotChain build() {ProcessorSlotChain chain = new DefaultProcessorSlotChain();chain.addLast(new NodeSelectorSlot());chain.addLast(new ClusterBuilderSlot());chain.addLast(new LogSlot());chain.addLast(new StatisticSlot());chain.addLast(new SystemSlot());chain.addLast(new AuthoritySlot());chain.addLast(new FlowSlot());chain.addLast(new DegradeSlot());# 在默认调用链基础上添加预警功能chain.addLast(new DegradeEarlyWarningSlot());return chain;}
}
添加SPI机制配置文件,在META-INF/services目录下定义一个名字为接口全限定名的文件,文件命名如下:
com.alibaba.csp.sentinel.slotchain.SlotChainBuilder
com.xxxx.xx.common.sentinel.slot.CustomerSlotChainBuilder
应用集成依赖添加预警功能
应用在pom文件中引入依赖,这里的依赖根据项目实际定义的基础包
<dependency><groupId>com.xxxx.framework</groupId><artifactId>sentinel-spring-boot-starter</artifactId><version>3.0.0-SNAPSHOT</version>
</dependency>
启动项目验证预警功能
2022-05-16 09:53:42.016 INFO [http-nio-7041-exec-1]c.m.s.d.application.impl.DsUserBaseQueryApplicationImpl.defaultFallback:46 -Go to default fallback
2022-05-16 09:53:45.902 INFO [http-nio-7041-exec-2]c.m.s.d.application.impl.DsUserBaseQueryApplicationImpl.defaultFallback:46 -Go to default fallback
2022-05-16 09:53:47.709 INFO [http-nio-7041-exec-3]c.m.s.d.application.impl.DsUserBaseQueryApplicationImpl.defaultFallback:46 -Go to default fallback
2022-05-16 09:53:49.001 INFO [http-nio-7041-exec-4]c.m.s.d.application.impl.DsUserBaseQueryApplicationImpl.defaultFallback:46 -Go to default fallback
2022-05-16 09:53:50.471 INFO [http-nio-7041-exec-5]c.m.saas.common.sentinel.slot.DegradeEarlyWarningSlot.entry:78 -DegradeEarlyWarning: 服务default 资源baseUserInfo 目前的熔断指标已经超过4.0,接近配置的熔断阈值:5.0,
2022-05-16 09:53:50.472 INFO [http-nio-7041-exec-5]c.m.s.d.application.impl.DsUserBaseQueryApplicationImpl.defaultFallback:46 -Go to default fallback
2022-05-16 09:53:51.923 ERROR[http-nio-7041-exec-6]c.m.s.d.appli
根据上述日志可以看出当异常次数达到4时会提前预警。
4.3 基于 Sentinel 实现 Feign 全局异常兜底
Spring CLoud微服务间交互使用Feign技术框架,在网络请求时,可能会出现异常请求,如果还想再异常情况下使系统可用,那么就需要容错处理,使用FeignClient时可对fallback进行配置,但随着接口数不断增加,配置也越来越重复繁琐,且大多容错逻辑均一致,因此需要对容错配置进行代理,提供全局统一容错处理。
通过官方文档我们知道feign支持基于Hystrix fallbackFactory 和 fallback模式的,但是两者均需要定义相应的fallbackFactory 和 fallback处理类。参考官方示例:
@FeignClient(name = "hello", fallback = HystrixClientFallback.class)
protected interface HystrixClient {@RequestMapping(method = RequestMethod.GET, value = "/hello")Hello iFailSometimes();
}static class HystrixClientFallback implements HystrixClient {@Overridepublic Hello iFailSometimes() {return new Hello("fallback");}
}@FeignClient(name = "hello", fallbackFactory = HystrixClientFallbackFactory.class)
protected interface HystrixClient {@RequestMapping(method = RequestMethod.GET, value = "/hello")Hello iFailSometimes();
}@Component
static class HystrixClientFallbackFactory implements FallbackFactory<HystrixClient> {@Overridepublic HystrixClient create(Throwable cause) {return new HystrixClient() {@Overridepublic Hello iFailSometimes() {return new Hello("fallback; reason was: " + cause.getMessage());}};}
}
如上述示例,随着接口的增加势必会产生大量类似的模板代码。
4.3.1 扩展SentinelFeign Builder
4.3.1.1 思路
通过对 spring-cloud-starter-alibaba-sentinel包源码分析,该包仅简单使用了四个类就实现对feign的支持。核心原理是通过自定义SentinelFeign构建器重新实现了feign对象初始化,添加了对Sentinel熔断限流的支持。查看核心源码如下:
public Feign build() {
super.invocationHandlerFactory(new InvocationHandlerFactory() {
public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {Object feignClientFactoryBean = Builder.this.applicationContext.getBean("&" + target.type().getName());Class fallback = (Class)Builder.this.getFieldValue(feignClientFactoryBean, "fallback");Class fallbackFactory = (Class)Builder.this.getFieldValue(feignClientFactoryBean, "fallbackFactory");String beanName = (String)Builder.this.getFieldValue(feignClientFactoryBean, "contextId");if (!StringUtils.hasText(beanName)) {beanName = (String)Builder.this.getFieldValue(feignClientFactoryBean, "name");}if (Void.TYPE != fallback) {Object fallbackInstance = this.getFromContext(beanName, "fallback", fallback, target.type());return new SentinelInvocationHandler(target, dispatch, new feign.hystrix.FallbackFactory.Default(fallbackInstance));} else if (Void.TYPE != fallbackFactory) {FallbackFactory fallbackFactoryInstance = (FallbackFactory)this.getFromContext(beanName, "fallbackFactory", fallbackFactory, FallbackFactory.class);return new SentinelInvocationHandler(target, dispatch, fallbackFactoryInstance);} else {return new SentinelInvocationHandler(target, dispatch);}
}
可以看出当未设置fallback 或者 fallbackFactory时,不会传递fallbackFactory到SentinelInvocationHandler。因此解决思路是:
- 自定义全局异常兜底处理函数CustomCommonFallbackFactory;
- 改写Feign build()逻辑,当未定义fallback 或者 fallbackFactory时,传入公共的CustomCommonFallbackFactory到SentinelInvocationHandler。
4.3.1.2 程序设计
自定义全局异常兜底处理函数CustomCommonFallbackFactory,具体实现如下:
@Slf4j
@AllArgsConstructor
public class CustomCommonFallback<T> implements MethodInterceptor {private final Class<T> targetType;private final String targetName;private final Throwable cause;@Nullable@Overridepublic Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) {String errorMessage = cause.getMessage();log.error("Feign API Fallback:[{}.{}] serviceId:[{}] message:[{}]", targetType.getName(), method.getName(), targetName, errorMessage);// BusinessException,直接返回if (cause instanceof BusinessException) {BusinessException be = (BusinessException) cause;return Result.of(false, null, be.getCode(), be.getMsg(), null);} else if (cause instanceof FeignException) {FeignException exception = (FeignException) cause;// 提取业务异常return Result.of(false, null, exception.status(), exception.contentUTF8(), null);} else {// 提取原始异常Throwable causeA = cause.getCause();if (causeA != null && causeA instanceof ClientException) {return Result.of(false, null, -1, String.format("%s服务已下线&服务状态不正常.", method.getName()), null);} else {return Result.of(false, null, -1, "系统未知异常.", null);}}}@Overridepublic boolean equals(Object o) {if (this == o) {return true;}if (o == null || getClass() != o.getClass()) {return false;}CustomCommonFallback<?> that = (CustomCommonFallback<?>) o;return targetType.equals(that.targetType);}@Overridepublic int hashCode() {return Objects.hash(targetType);}
}@AllArgsConstructor
public class CustomCommonFallbackFactory<T> implements FallbackFactory<T> {private final Target<T> target;@Override@SuppressWarnings("unchecked")public T create(Throwable cause) {final Class<T> targetType = target.type();final String targetName = target.name();Enhancer enhancer = new Enhancer();enhancer.setSuperclass(targetType);enhancer.setUseCache(true);enhancer.setCallback(new CustomCommonFallback<>(targetType, targetName, cause));return (T) enhancer.create();}
}
改写Feign build()逻辑,当未定义fallback 或者 fallbackFactory时,传入公共的CustomCommonFallbackFactory到SentinelInvocationHandler。这里需要注意,由于SentinelInvocationHandler访问权限限制包内访问,因此将新建的类放com.alibaba.cloud.sentinel.feign目录下。具体代码如下:
public final class CustomSentinelFeign {private CustomSentinelFeign() {}public static CustomSentinelFeign.Builder builder() {return new CustomSentinelFeign.Builder();}public static final class Builder extends feign.Feign.Builder implements ApplicationContextAware {private Contract contract = new Contract.Default();private ApplicationContext applicationContext;private FeignContext feignContext;@Overridepublic feign.Feign.Builder invocationHandlerFactory(feign.InvocationHandlerFactory invocationHandlerFactory) {throw new UnsupportedOperationException();}@Overridepublic CustomSentinelFeign.Builder contract(Contract contract) {this.contract = contract;return this;}/*** 自定义feign构建器,在模式SentinelFeign基础增加 CustomCommonFallbackFactory,* 当feign配置不指定兜底函数将使用默认CustomCommonFallbackFactory* @return*/@Overridepublic Feign build() {super.invocationHandlerFactory(new InvocationHandlerFactory() {public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {Object feignClientFactoryBean = CustomSentinelFeign.Builder.this.applicationContext.getBean("&" + target.type().getName());Class fallback = (Class) getFieldValue(feignClientFactoryBean, "fallback");Class fallbackFactory = (Class) getFieldValue(feignClientFactoryBean, "fallbackFactory");String beanName = (String) CustomSentinelFeign.Builder.this.getFieldValue(feignClientFactoryBean, "contextId");if (!StringUtils.hasText(beanName)) {beanName = (String) CustomSentinelFeign.Builder.this.getFieldValue(feignClientFactoryBean, "name");}if (Void.TYPE != fallback) {Object fallbackInstance = this.getFromContext(beanName, "fallback", fallback, target.type());return new SentinelInvocationHandler(target, dispatch, new FallbackFactory.Default(fallbackInstance));} else if (Void.TYPE != fallbackFactory) {FallbackFactory fallbackFactoryInstance = (FallbackFactory) this.getFromContext(beanName, "fallbackFactory", fallbackFactory, FallbackFactory.class);return new SentinelInvocationHandler(target, dispatch, fallbackFactoryInstance);} else {// 默认的 fallbackFactoryCustomCommonFallbackFactory customFallbackFactory = new CustomCommonFallbackFactory(target);return new SentinelInvocationHandler(target, dispatch, customFallbackFactory);}}private Object getFromContext(String name, String type,Class fallbackType, Class targetType) {Object fallbackInstance = feignContext.getInstance(name,fallbackType);if (fallbackInstance == null) {throw new IllegalStateException(String.format("No %s instance of type %s found for feign client %s",type, fallbackType, name));}if (!targetType.isAssignableFrom(fallbackType)) {throw new IllegalStateException(String.format("Incompatible %s instance. Fallback/fallbackFactory of type %s is not assignable to %s for feign client %s",type, fallbackType, targetType, name));}return fallbackInstance;}});super.contract(new SentinelContractHolder(contract));return super.build();}private Object getFieldValue(Object instance, String fieldName) {Field field = ReflectionUtils.findField(instance.getClass(), fieldName);field.setAccessible(true);try {return field.get(instance);} catch (IllegalAccessException e) {// ignore}return null;}@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {this.applicationContext = applicationContext;feignContext = this.applicationContext.getBean(FeignContext.class);}}
}
最后注入新定义的Bean
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({SphU.class, Feign.class})
public class CustomFeignAutoConfiguration {@Bean@Scope("prototype")@ConditionalOnClass({SphU.class, Feign.class})@ConditionalOnProperty(name = "feign.sentinel.enabled")@Primarypublic Feign.Builder feignSentinelBuilder() {return CustomSentinelFeign.builder();}
}
4.3.2 使用
要使用Sentinel全局异常兜底需要引入基础依赖包并且在配置文件中配置feign.sentinel.enabled=true,注释掉feign.hystrix.enabled=true
feign.sentinel.enabled=true
feign.hystrix.enabled=false
<dependency><groupId>com.xxxx.framework</groupId><artifactId>sentinel-spring-boot-starter</artifactId><version>3.0.0-SNAPSHOT</version>
</dependency>
定义feign不配置fullback
@FeignClient(contextId = "dsUserBaseApiClient", name = "demo-xxxx-server")
public interface DsUserBaseApiClient extends DsUserBaseApi {
}
调用feign接口,当出现异常打印日志,可以看出定义的公共CustomCommonFallback触发生效。
2022-06-13 20:23:52.626 ERROR[http-nio-7042-exec-1]c.m.s.c.sentinel.feign.fallback.CustomCommonFallback.intercept:32 -Feign API Fallback:[com.xxxxx.saas.demoapi.feign.DsUserBaseApiClient.getBaseUserInfo] serviceId:[demo-xxxxx-server] message:[com.netflix.client.ClientException: Load balancer does not have available server for client: demo-xxxx-server]