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

文章目录

  • 一、前言
  • 二、技术思路及方案
    • [2.1 实现思路](#2.1 实现思路)
    • [2.2 实现方案](#2.2 实现方案)
      • [2.2.1 nacos动态数据源实现类关系图](#2.2.1 nacos动态数据源实现类关系图)
  • 三、功能实现
    • [3.1 快速集成方案](#3.1 快速集成方案)
      • [3.1.1 引入依赖](#3.1.1 引入依赖)
      • [3.1.2 服务端熔断降级](#3.1.2 服务端熔断降级)
      • [3.1.3 feign调用降级](#3.1.3 feign调用降级)
  • 四、扩展
    • [4.1 SPI机制](#4.1 SPI机制)
    • [4.2 自定义Slot实现](#4.2 自定义Slot实现)
    • [4.3 基于 Sentinel 实现 Feign 全局异常兜底](#4.3 基于 Sentinel 实现 Feign 全局异常兜底)
      • [4.3.1 扩展SentinelFeign Builder](#4.3.1 扩展SentinelFeign Builder)
        • [4.3.1.1 思路](#4.3.1.1 思路)
        • [4.3.1.2 程序设计](#4.3.1.2 程序设计)
      • [4.3.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-rules
            groupId: SENTINEL_GROUP
            # 规则类型,取值见:
            # org.springframework.cloud.alibaba.sentinel.datasource.RuleType
            rule-type: flow
            namespace: ${xxxx.sentinel.nacos.namespace}
        degrade:
          nacos:
            server-addr: ${spring.cloud.nacos.config.server-addr}
            dataId: global-sentinel-degrade-rules
            groupId: SENTINEL_GROUP
            rule-type: degrade
            data-type: json
            namespace: ${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: true
  hystrix:
    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) { // 异常数取异常阈值-1
                earlyWarningRuleCount = 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);
    }

    @Override
    public 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);

    }

    @Override
    public 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 {
    @Override
    public 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> {
    @Override
    public HystrixClient create(Throwable cause) {
        return new HystrixClient() {
            @Override
            public 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
    @Override
    public 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);
            }
        }
    }

    @Override
    public 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);
    }

    @Override
    public 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;

        @Override
        public feign.Feign.Builder invocationHandlerFactory(feign.InvocationHandlerFactory invocationHandlerFactory) {
            throw new UnsupportedOperationException();
        }

        @Override
        public CustomSentinelFeign.Builder contract(Contract contract) {
            this.contract = contract;
            return this;
        }

        /**
         * 自定义feign构建器,在模式SentinelFeign基础增加 CustomCommonFallbackFactory,
         * 当feign配置不指定兜底函数将使用默认CustomCommonFallbackFactory
         * @return
         */
        @Override
        public 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 {
                        // 默认的 fallbackFactory
                        CustomCommonFallbackFactory 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;
        }

        @Override
        public 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")
    @Primary
    public 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]
相关推荐
王彬泽5 小时前
【微服务】组件、基础工程构建(day2)
微服务
Cikiss5 小时前
微服务实战——SpringCache 整合 Redis
java·redis·后端·微服务
Cikiss5 小时前
微服务实战——平台属性
java·数据库·后端·微服务
杨荧8 小时前
【JAVA开源】基于Vue和SpringBoot的洗衣店订单管理系统
java·开发语言·vue.js·spring boot·spring cloud·开源
攸攸太上9 小时前
JMeter学习
java·后端·学习·jmeter·微服务
妍妍的宝贝10 小时前
k8s 中微服务之 MetailLB 搭配 ingress-nginx 实现七层负载
nginx·微服务·kubernetes
架构师吕师傅12 小时前
性能优化实战(三):缓存为王-面向缓存的设计
后端·微服务·架构
sdg_advance14 小时前
Spring Cloud之OpenFeign的具体实践
后端·spring cloud·openfeign
王彬泽14 小时前
【微服务】服务注册与发现、分布式配置管理 - Nacos
微服务·服务注册与发现·分布式配置管理
杨荧15 小时前
【JAVA开源】基于Vue和SpringBoot的旅游管理系统
java·vue.js·spring boot·spring cloud·开源·旅游