Spring Cloud 微服务集成Sentinel实现服务熔断降级

文章目录

  • 一、前言
  • 二、技术思路及方案
    • 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整体架构
实际处理流控、熔断降级是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。因此解决思路是:

  1. 自定义全局异常兜底处理函数CustomCommonFallbackFactory;
  2. 改写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]

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/23714.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Html5如何播放hls格式的视频

目录 一、什么情况下需要播放 二、hls的原理 1、 切片 2、编码 3、分发 4、M3U8文件 5、客户端请求 6、动态码率调整 7、缓存 8、实时性 三、方法一&#xff1a;使用hls.js播放 四、方法二&#xff1a;使用video.js播放 一、什么情况下需要播放 当前端需要嵌入设备视…

[数组] 子数组最大平均数

给你一个由 n 个元素组成的整数数组 nums 和一个整数 k 。 请你找出平均数最大且 长度为 k 的连续子数组&#xff0c;并输出该最大平均数。 任何误差小于 10-5 的答案都将被视为正确答案。 示例 1&#xff1a; 输入&#xff1a;nums [1,12,-5,-6,50,3], k 4 输出&#xff…

操作系统教材第6版——个人笔记5

3.2 单连续分区存储管理 3.2.1 单连续分区存储管理 单连续分区存储管理 每个进程占用一个物理上完全连续的存储空间(区域) 单用户连续分区存储管理固定分区存储管理可变分区存储管理 单用户连续分区存储管理 主存区域划分为系统区与用户区设置一个栅栏寄存器界分两个区域…

每日题库:Huawe数通HCIA——13

所有资料均来源自网络&#xff0c;但个人亲测有效&#xff0c;特来分享&#xff0c;希望各位能拿到好成绩&#xff01; PS&#xff1a;别忘了一件三连哈&#xff01; 今日题库&#xff1a; 186. 下列协议中属于动态IGP路由协议的是&#xff1f;-单选 A.stA.tiC. B.OSPF c…

使用Django Channels和WebSocket构建聊天应用

一、引言 WebSocket提供了一种在客户端和服务器之间进行实时双向通信的方式。结合Django Channels&#xff0c;我们可以轻松地在Django项目中实现WebSocket功能。本文将通过一个简单的聊天应用示例&#xff0c;展示如何使用Django Channels和WebSocket。 二、环境搭建 项目的…

Nacos的下载使用

下载 本地Java8Maven环境 下载地址 https://github.com/alibaba/nacos/releases使用 解压安装包&#xff0c;直接运行bin目录下的startup.cmd 账号密码都是nacos

Win32和c++11多线程

Win32和c11多线程 一、概念1.线程的特点线程内核对象线程控制块线程是独立调度和分派的基本单位共享进程的资源 2.线程的上下文切换引起上下文切换的原因 3.线程的状态 二、Windows多线程API1.CreateThread创建线程2.获取线程ID3.关闭线程句柄4.挂起线程5.恢复线程6.休眠线程的…

算法金 | 读者问了个关于深度学习卷积神经网络(CNN)核心概念的问题

​大侠幸会&#xff0c;在下全网同名[算法金] 0 基础转 AI 上岸&#xff0c;多个算法赛 Top [日更万日&#xff0c;让更多人享受智能乐趣] 读者问了个关于卷积神经网络核心概念的问题&#xff0c;如下&#xff0c; 【问】神经元、权重、激活函数、参数、图片尺寸&#xff0c;卷…

手机相册的排列方式探讨

不论你是不是程序员&#xff0c;你一定留意过一个问题&#xff1a;相册 App 基本都将图片裁剪成了居中的 1:1 正方形。那么手机相册 App&#xff0c;为什么要将图片切割成 1:1 正方形&#xff0c;然后以网格排列&#xff1f;是行业标准吗&#xff1f; 自适应图片宽度的图库&a…

vscode 离线下载指定版本插件和安装方法

1、背景 由于不同的vscode版本需要安装对应的插件版本&#xff0c;一般情况下&#xff0c;vscode版本会落后于vscode插件库提供的可以下载的插件版本&#xff0c;网页一般只会提供最新的插件下载版本&#xff0c;因此我们需要下载指定的版本需要采取一些措施。 2、获取需要安…

c语言速成系列指针上篇

那么这一篇文章带大家学习一下c语言的指针的概念、使用、以及一些注意事项。 指针的概念 指针也就是内存地址&#xff0c;指针变量是用来存放内存地址的变量。就像其他变量或常量一样&#xff0c;您必须在使用指针存储其他变量地址之前&#xff0c;对其进行声明。 大白话讲解…

【数据集划分】oracle数据集划分(总结版)

【数据集划分】假如你有接近百万条oracle数据库数据&#xff08;成真版&#xff09; 写在最前面最终代码原理&#xff1a;生成随机索引并打乱顺序示例作用应用场景 遇到报错&#xff1a;ORA-01795&#xff0c;通过CTE&#xff08;Common Table Expressions&#xff09;和窗口函…

Next.js Tailwind CSS UI组件

摘要&#xff1a; 官网 今天公司使用到一个前端ui框架——Next.js Tailwind CSS UI组件&#xff01;这从头构建一个AI驱动的前端UI组件生成器&#xff0c;生成Next.js Tailwind CSS UI组件&#xff1a; 1、用Next.js、ts和Tailwind CSS构建UI组件生成器Web应用程序。 2、用Copi…

08-指针与数组的结合——数组指针与指针数组的区别

指针与数组的结合 示例 1:指针访问数组元素 通过指针访问数组元素的例子&#xff1a; #include <stdio.h>int main() {int arr[5] {1,2,3,4,5};//int *p1 &arr;int *p1 (int *)&arr; // 需要强制类型转换int *p2 arr;printf("*p1:%d\n", *(p1 …

Python第二语言(四、Python数据容器)

目录 一、 数据容器&#xff08;list、tuple、str、map、dict&#xff09; 1. 数据容器概念 2. 列表list&#xff08; [] &#xff09; 2.1 定义方式 2.2 嵌套列表 2.3 list通过获取下标索引获取值 2.4 下标使用概念 2.5 list列表的使用&#xff08;列表的方法&#xff…

​在 The Sandbox 元宇宙的 CU 超商中寻找Milk币!

CU&#xff08;韩国领先的便利店&#xff09;和 MiL.k&#xff08;基于区块链的忠诚度整合平台&#xff09;合作在 The Sandbox 推出了首款元宇宙游戏&#xff0c;通过独家活动在 Web2 和 Web3 之间建立联系。 在元宇宙中玩转 “Play CU X MiL.k” 体验 通过引人入胜的游戏内容…

Apple开发者证书创建完整过程

1.创建CSR文件: 打开钥匙串访问程序 选择从证书颁发机构请求 创建证书 保存CSR文件到桌面 成功如下: 开始创建证书: 选择

每天一道大厂SQL题【Day32】按消息量给广东省qq打标记

文章目录 每天一道大厂SQL题【Day32】按消息量给广东省qq打标记每日语录第32题 需求三&#xff1a;按消息量给广东省qq打标记思路分析附表 答案获取加技术群讨论文末SQL小技巧 后记 每天一道大厂SQL题【Day32】按消息量给广东省qq打标记 大家好&#xff0c;我是Maynor。相信大…

vue3引入cesium和olcs

首先引入包 pnpm i olcs; pnpm i -D vite-plugin-cesium pnpm i -S cesium在vite.config.js中配置&#xff0c;参考这位大佬的笔记 添加链接描述 import { defineConfig } from vite import vue from vitejs/plugin-vue import cesium from vite-plugin-cesium; // https://…

芝麻IP好用吗?来测试了!

作为老牌代理IP服务厂商&#xff0c;芝麻IP和青果网络代理IP都做的不错&#xff0c;市场上几乎可以是有口皆碑了&#xff0c;上次测试了青果网络的代理IP&#xff0c;效果表现得还挺不错&#xff0c;和他们自己宣传的以及客户对他们的评价大差不差。 总的来说&#xff0c;他们家…