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

[toc]

一、前言

关于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-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 # 最小触发请求数
    }
]

降级规则结果验证测试
配置完降级规则启动服务,首次访问接口,参数传递为空,服务端资源出现异常,直接进入兜底函数。==下图为fallback兜底函数降级结果==:

http://192.168.132.49:7041/user/base?userId=

此后一秒内连续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]
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,552评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,666评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,519评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,180评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,205评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,344评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,781评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,449评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,635评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,467评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,515评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,217评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,775评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,851评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,084评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,637评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,204评论 2 341

推荐阅读更多精彩内容