Spring Cloud 微服務(wù)集成Sentinel實(shí)現(xiàn)服務(wù)熔斷降級(jí)

[toc]

一、前言

關(guān)于Sentinel和Hystrix之間對(duì)比以及Sentinel原理在官方文檔有詳細(xì)文檔樱哼,這里就不再做多余贅述尺迂,Sentinel常規(guī)集成通常是借助Sentinel Dashboard服務(wù)端整合實(shí)現(xiàn)服務(wù)的限流骗露、熔斷降級(jí)以及多維護(hù)的監(jiān)控。但是項(xiàng)目當(dāng)下已經(jīng)集成promethus監(jiān)控怪瓶、aws云原生服務(wù)自帶流量監(jiān)控等,因此Sentinel Dashboard服務(wù)端提供的多維監(jiān)控模項(xiàng)目需求優(yōu)先級(jí)并不高践美。綜合項(xiàng)目實(shí)際情況以及節(jié)約成本的理念我們提出:Spring Cloud + Sentinel + nacos 動(dòng)態(tài)數(shù)據(jù)源模式(無(wú)Dashboard服務(wù)端)實(shí)現(xiàn)微服務(wù)的服務(wù)降級(jí)功能洗贰。

本文主要包含圍Sentinel繞微服務(wù)的服務(wù)降級(jí)功能實(shí)現(xiàn)、自定義slot實(shí)現(xiàn)熔斷降級(jí)預(yù)警功能以及基于-sentinel-實(shí)現(xiàn)-feign-全局異常兜底陨倡。


二敛滋、技術(shù)思路及方案

2.1 實(shí)現(xiàn)思路

從官方提供Sentinel整體架構(gòu)可以看出Dashboard服務(wù)端在Sentinel整體架構(gòu)中僅負(fù)責(zé)規(guī)則配置、實(shí)時(shí)監(jiān)控兴革、機(jī)器發(fā)現(xiàn)等輔助模塊绎晃。


Sentinel整體架構(gòu)

實(shí)際處理流控、熔斷降級(jí)是Sentinel-core完成帖旨。因此剝離Dashboard服務(wù)端箕昭,獨(dú)立實(shí)現(xiàn)服務(wù)的熔斷降級(jí)功能是可行的。
調(diào)研官方文檔不難發(fā)現(xiàn)解阅,Sentinel針對(duì)Spring Cloud微服務(wù)提供了依賴:

  • spring-cloud-starter-alibaba-sentinel 微服務(wù)快速集成Sentinel提供支持
  • spring-cloud-alibaba-sentinel-datasource Sentinel規(guī)則動(dòng)態(tài)數(shù)據(jù)源支持自動(dòng)化配置
  • sentinel-datasource-nacos 提供了Sentinel規(guī)則動(dòng)態(tài)數(shù)據(jù)源支持落竹。

2.2 實(shí)現(xiàn)方案

如上圖,基于nacos配置中心實(shí)現(xiàn)Sentinel規(guī)則動(dòng)態(tài)數(shù)據(jù)源管理货抄,微服務(wù)啟動(dòng)時(shí)拉取熔斷降級(jí)規(guī)則并維持心跳動(dòng)態(tài)更新數(shù)據(jù)源配置述召。

2.2.1 nacos動(dòng)態(tài)數(shù)據(jù)源實(shí)現(xiàn)類關(guān)系圖

根據(jù)源碼分析,可以看出nacos動(dòng)態(tài)數(shù)據(jù)源實(shí)現(xiàn)如下:



從入口程序SentinelAutoConfiguration開(kāi)始蟹地,應(yīng)用程序從環(huán)境配置Properties獲取指定的數(shù)據(jù)源配置积暖,最終通過(guò)靜態(tài)規(guī)則管理類DegradeRuleManager注冊(cè)到數(shù)據(jù)源,從而實(shí)現(xiàn)動(dòng)態(tài)刷新規(guī)則配置怪与。


三夺刑、功能實(shí)現(xiàn)

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 默認(rèn)開(kāi)啟sentinel功能,引入依賴便可以使用sentinel分别,源碼片段如下:

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnProperty(
    name = {"spring.cloud.sentinel.enabled"},
    matchIfMissing = true
)
@EnableConfigurationProperties({SentinelProperties.class})
public class SentinelAutoConfiguration {

spring-cloud-alibaba-sentinel-datasource依賴會(huì)從數(shù)據(jù)源中動(dòng)態(tài)加載sentinel規(guī)則遍愿,源碼片段如下:

# 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 服務(wù)端熔斷降級(jí)

@SentinelResource 可以作用于方法上的熔斷降級(jí)保護(hù),并提供可選的異常處理和 fallback 配置項(xiàng)耘斩。 @SentinelResource 注解包含以下屬性:

  • value:資源名稱沼填,必需項(xiàng)(不能為空),如果不填括授,會(huì)自動(dòng)以全路徑為key
  • entryType:entry 類型坞笙,可選項(xiàng)(默認(rèn)為 EntryType.OUT)
  • blockHandler / blockHandlerClass: blockHandler 對(duì)應(yīng)處理 BlockException 的函數(shù)名稱岩饼,可選項(xiàng)。blockHandler 函數(shù)訪問(wèn)范圍需要是 public薛夜,返回類型需要與原方法相匹配籍茧,參數(shù)類型需要和原方法相匹配并且最后加一個(gè)額外的參數(shù),類型為 BlockException却邓。blockHandler 函數(shù)默認(rèn)需要和原方法在同一個(gè)類中硕糊。若希望使用其他類的函數(shù),則可以指定 blockHandlerClass 為對(duì)應(yīng)的類的 Class 對(duì)象腊徙,注意對(duì)應(yīng)的函數(shù)必需為 static 函數(shù)简十,否則無(wú)法解析。
  • fallback:fallback 函數(shù)名稱撬腾,可選項(xiàng)螟蝙,用于在拋出異常的時(shí)候提供 fallback 處理邏輯。fallback 函數(shù)可以針對(duì)所有類型的異常(除了 exceptionsToIgnore 里面排除掉的異常類型)進(jìn)行處理民傻。fallback 函數(shù)簽名和位置要求:
    • 返回值類型必須與原函數(shù)返回值類型一致胰默;
    • 方法參數(shù)列表需要和原函數(shù)一致,或者可以額外多一個(gè) Throwable 類型的參數(shù)用于接收對(duì)應(yīng)的異常
    • fallback 函數(shù)默認(rèn)需要和原方法在同一個(gè)類中漓踢。若希望使用其他類的函數(shù)牵署,則可以指定 fallbackClass 為對(duì)應(yīng)的類的 Class 對(duì)象,注意對(duì)應(yīng)的函數(shù)必需為 static 函數(shù)喧半,否則無(wú)法解析奴迅。
  • defaultFallback(since 1.6.0):默認(rèn)的 fallback 函數(shù)名稱,可選項(xiàng)挺据,通常用于通用的 fallback 邏輯(即可以用于很多服務(wù)或方法)取具。默認(rèn) fallback 函數(shù)可以針對(duì)所以類型的異常(除了 exceptionsToIgnore 里面排除掉的異常類型)進(jìn)行處理。若同時(shí)配置了 fallback 和 defaultFallback扁耐,則只有 fallback 會(huì)生效暇检。defaultFallback函數(shù)要求與fallback一致。
  • exceptionsToIgnore(since 1.6.0):用于指定哪些異常被排除掉婉称,不會(huì)計(jì)入異常統(tǒng)計(jì)中块仆,也不會(huì)進(jìn)入 fallback 邏輯中,而是會(huì)原樣拋出王暗。

這里補(bǔ)充說(shuō)明下blockHandler和fallback觸發(fā)機(jī)制

  • fallback如上述所講榨乎,是異常降級(jí)兜底函數(shù),當(dāng)資源函數(shù)出現(xiàn)異常將會(huì)進(jìn)入fallback如上述所講瘫筐。
  • blockHandler是當(dāng)資源函數(shù)某項(xiàng)指標(biāo)超過(guò)設(shè)定的規(guī)則時(shí)觸發(fā)
異常 說(shuō)明
FlowException 限流異常
ParamFlowException 熱點(diǎn)參數(shù)限流的異常
DegradeException 降級(jí)異常
AuthorityException 授權(quán)規(guī)則異常
SystemBlockException 系統(tǒng)規(guī)則異常

代碼示例

下面代碼示例通過(guò)@SentinelResource 注解在方法上進(jìn)行埋點(diǎn),標(biāo)記getBaseUserInfo1函數(shù)為Sentinel資源铐姚,并指定了兜底函數(shù)和降級(jí)函數(shù)策肝。

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;
    }

    //默認(rèn)的 fallback 函數(shù)名稱
    public String defaultFallback(String userId) {
        log.info("Go to default fallback");
        return "defaultFallback降級(jí)了";
    }

    // Block 異常處理函數(shù)肛捍,參數(shù)最后多一個(gè) BlockException,其余與原函數(shù)一致.
    public String exceptionHandler(String userId, BlockException ex) {

        log.error("blockHandler服務(wù)降級(jí)了", ex);
        // Do some log here.
        return "Oops,blockHandler, error occurred at " + userId;
    }
}

增加動(dòng)態(tài)數(shù)據(jù)源配置
動(dòng)態(tài)數(shù)據(jù)源配置直接在SpringCloud配置模塊增加sentinel.datasource數(shù)據(jù)源之众,支持flow限流規(guī)則和degrade降級(jí)規(guī)則拙毫。在flow/degrade層下添加具體的數(shù)據(jù)源配置介質(zhì),==下面為基于nacos配置中心介質(zhì)的動(dòng)態(tài)數(shù)據(jù)源配置==

#sentinel配置相關(guān)
spring:
  cloud:
    sentinel:
      datasource:
        flow:
          nacos:
            server-addr: ${spring.cloud.nacos.config.server-addr}
            dataId: ${spring.application.name}-flow-rules
            groupId: SENTINEL_GROUP
            # 規(guī)則類型棺禾,取值見(jiàn):
            # 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}

指定資源熔斷規(guī)則
上文通過(guò)動(dòng)態(tài)數(shù)據(jù)源配置指定了nacos降級(jí)規(guī)則配置文件缀蹄,配置文件采用json格式的數(shù)組配置,詳細(xì)配置如下:

[
    {
        "resource":"baseUserInfo", # 資源名稱
        "grade":2, # 規(guī)則編號(hào)膘婶,2代表異常次數(shù)降級(jí)規(guī)則
        "count":5, # 閾值
        "timeWindow":10, # 降級(jí)窗口時(shí)間缺前,單位s
        "MinRequestAmount": 2 # 最小觸發(fā)請(qǐng)求數(shù)
    }
]

降級(jí)規(guī)則結(jié)果驗(yàn)證測(cè)試
配置完降級(jí)規(guī)則啟動(dòng)服務(wù),首次訪問(wèn)接口悬襟,參數(shù)傳遞為空衅码,服務(wù)端資源出現(xiàn)異常,直接進(jìn)入兜底函數(shù)脊岳。==下圖為fallback兜底函數(shù)降級(jí)結(jié)果==:

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

此后一秒內(nèi)連續(xù)5次訪問(wèn)后逝段,資源異常次數(shù)達(dá)到閾值,服務(wù)進(jìn)入
blockHandler規(guī)則降級(jí)函數(shù)割捅,并且在此后10秒內(nèi)都會(huì)進(jìn)入規(guī)則降級(jí)流程奶躯。==下圖為異常次數(shù)達(dá)到閾值后,進(jìn)行blockHandler規(guī)則降級(jí)結(jié)果==:


3.1.3 feign調(diào)用降級(jí)

開(kāi)啟sentinel feign支持

要啟用sentinel feign降級(jí)功能需要在應(yīng)用配置中顯示關(guān)閉Spring Cloud 默認(rèn)Hystrix降級(jí)開(kāi)關(guān) 和 啟用 feign sentinel 開(kāi)啟:

#打開(kāi)sentinel對(duì)feign的支持
feign:
  sentinel:
    enabled: true
  hystrix:
    enabled: false

開(kāi)啟feign支持后亿驾,應(yīng)用啟動(dòng)將初始化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 對(duì)@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會(huì)根據(jù)定義的feing接口構(gòu)建相應(yīng)的資源颊乘,資源名策略定義:==httpmethod:protocol://requesturl==参淹。

編碼若锁,定義feign調(diào)用服務(wù)端和調(diào)用方

# 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 接口調(diào)用
@GetMapping(value = "/user/info")
public String getBaseUserInfo1(String userId) {

    return client.getBaseUserInfo(userId);
}

# feign server 定義
@Override
public String getBaseUserInfo(String userId) {

    log.info("降級(jí)測(cè)試start...");
    
    try {
        log.info("降級(jí)測(cè)試, 我開(kāi)始休眠了...");
        Thread.sleep(3 * 1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    
    log.info("降級(jí)測(cè)試, 我睡醒了...");
    return baseQueryApplication.getBaseUserInfo(userId);
}

配置feign超時(shí)規(guī)則

0-超時(shí)異常降級(jí)策略咸作,閾值2000ms

[
    {
        "resource": "GET:http://demo-xxxx-server/inner/user/base",
        "grade": 0,
        "count": 2000,
        "timeWindow":10
    }
]

項(xiàng)目默認(rèn)超時(shí)時(shí)間10 * 1000MS,Server接口設(shè)置睡眠時(shí)間3 * 1000MS悍抑,閾值2 * 1000 ms檩小,訪問(wèn)接口feign會(huì)正常返回开呐。

使用Jmster進(jìn)行壓力測(cè)試,在50 * 50 循環(huán)調(diào)用過(guò)程规求,當(dāng)feign調(diào)用平均響應(yīng)時(shí)間超過(guò)設(shè)定的閾值后筐付,將會(huì)提前進(jìn)行熔斷降級(jí),調(diào)用feign接口定義的fallback函數(shù)阻肿,而不是一直等待服務(wù)端響應(yīng)瓦戚。

四、擴(kuò)展

在 Sentinel 里面丛塌,所有的資源都對(duì)應(yīng)一個(gè)資源名稱(resourceName)较解,每次資源調(diào)用都會(huì)創(chuàng)建一個(gè) Entry 對(duì)象畜疾。Entry 可以通過(guò)對(duì)主流框架的適配自動(dòng)創(chuàng)建,也可以通過(guò)注解的方式或調(diào)用 SphU API 顯式創(chuàng)建印衔。Entry 創(chuàng)建的時(shí)候啡捶,同時(shí)也會(huì)創(chuàng)建一系列功能插槽(slot chain):

  • NodeSelectorSlot 負(fù)責(zé)收集資源的路徑,并將這些資源的調(diào)用路徑奸焙,以樹(shù)狀結(jié)構(gòu)存儲(chǔ)起來(lái)瞎暑,用于根據(jù)調(diào)用路徑來(lái)限流降級(jí);
  • ClusterBuilderSlot 則用于存儲(chǔ)資源的統(tǒng)計(jì)信息以及調(diào)用者信息与帆,例如該資源的 RT, QPS, thread count 等等了赌,這些信息將用作為多維度限流,降級(jí)的依據(jù)鲤桥;
  • StatisticSlot 則用于記錄揍拆、統(tǒng)計(jì)不同緯度的 runtime 指標(biāo)監(jiān)控信息;
  • FlowSlot 則用于根據(jù)預(yù)設(shè)的限流規(guī)則以及前面 slot 統(tǒng)計(jì)的狀態(tài)茶凳,來(lái)進(jìn)行流量控制嫂拴;
  • AuthoritySlot 則根據(jù)配置的黑白名單和調(diào)用來(lái)源信息,來(lái)做黑白名單控制贮喧;
  • DegradeSlot 則通過(guò)統(tǒng)計(jì)信息以及預(yù)設(shè)的規(guī)則筒狠,來(lái)做熔斷降級(jí);
  • SystemSlot 則通過(guò)系統(tǒng)的狀態(tài)箱沦,例如 load1 等辩恼,來(lái)控制總的入口流量;

ProcessorSlotChain(核心骨架):將不同的 Slot 按照順序串在一起(==責(zé)任鏈模式==)谓形,從而將不同的功能(限流灶伊、降級(jí)、系統(tǒng)保護(hù))組合在一起寒跳。slot chain 其實(shí)可以分為兩部分:統(tǒng)計(jì)數(shù)據(jù)構(gòu)建部分(statistic)和判斷部分(rule checking)聘萨。
系統(tǒng)會(huì)為每個(gè)資源創(chuàng)建一套SlotChain。

Sentinel框架對(duì)feign適配自動(dòng)為feign創(chuàng)建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框架通過(guò)AOP 切莫入口SentinelResourceAspect為@SentinelResource注解標(biāo)記的資源自動(dòng)創(chuàng)建Entry對(duì)象米辐,源碼片段如下:

@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 {
                # 為資源構(gòu)建 entry對(duì)象
                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機(jī)制

Sentinel槽鏈中Slot執(zhí)行順序是固定的,但并不是絕對(duì)的书释。Sentinel將ProcessorSlot作為SPI接口進(jìn)行擴(kuò)展翘贮,使得SlotChain具備了擴(kuò)展能力。用戶可以自定義Slot并編排Slot間的順序爆惧。



下圖為Sentinel默認(rèn)Slot鏈路實(shí)現(xiàn):



4.2 自定義Slot實(shí)現(xiàn)

熔斷降級(jí)是保障微服務(wù)穩(wěn)定性的重要手段狸页,而在服務(wù)降級(jí)前提前預(yù)警,以便開(kāi)發(fā)人員提前處理導(dǎo)致請(qǐng)求響應(yīng)超時(shí)扯再、接口異常等問(wèn)題能夠更加有效保障微服務(wù)的穩(wěn)定性芍耘。

自定義Slot實(shí)現(xiàn)降級(jí)提前預(yù)警功能
熔斷降級(jí)提前預(yù)警實(shí)現(xiàn)思路是分析了Sentinel默認(rèn)ProcessorSlotChain構(gòu)建思路并結(jié)合SPI機(jī)制腹侣,自定義熔斷降級(jí)提前預(yù)警Slot并重新構(gòu)建ProcessorSlotChain。代碼實(shí)現(xiàn)如下:

# 降級(jí)預(yù)警實(shí)現(xiàn)
@Slf4j
public class DegradeEarlyWarningSlot extends AbstractLinkedProcessorSlot<DefaultNode>  {

    /**
     * 從熔斷降級(jí)規(guī)則管理器中提取降級(jí)規(guī)則并構(gòu)建預(yù)警閾值規(guī)則
     * @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) { // 異常數(shù)取異常閾值-1
                earlyWarningRuleCount = rule.getCount() - 1;
            } else { // 異常比例 和 平均超時(shí)時(shí)間取閾值的80%作為提前預(yù)警閾值
                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);
        // 這里日志打印只是為了演示齿穗,后期計(jì)劃集成disputhcer內(nèi)存隊(duì)列 + 飛書(shū)預(yù)警
        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: 服務(wù){(diào)} 資源{} 目前的熔斷指標(biāo)已經(jīng)超過(guò){},接近配置的熔斷閾值:{},",
                            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);
    }
}

實(shí)現(xiàn)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());
        # 在默認(rèn)調(diào)用鏈基礎(chǔ)上添加預(yù)警功能
        chain.addLast(new DegradeEarlyWarningSlot());
        return chain;
    }
}

添加SPI機(jī)制配置文件,在META-INF/services目錄下定義一個(gè)名字為接口全限定名的文件,文件命名如下:
com.alibaba.csp.sentinel.slotchain.SlotChainBuilder

com.xxxx.xx.common.sentinel.slot.CustomerSlotChainBuilder

應(yīng)用集成依賴添加預(yù)警功能
應(yīng)用在pom文件中引入依賴复濒,這里的依賴根據(jù)項(xiàng)目實(shí)際定義的基礎(chǔ)包

<dependency>
    <groupId>com.xxxx.framework</groupId>
    <artifactId>sentinel-spring-boot-starter</artifactId>
    <version>3.0.0-SNAPSHOT</version>
</dependency>

啟動(dòng)項(xiàng)目驗(yàn)證預(yù)警功能

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: 服務(wù)default 資源baseUserInfo 目前的熔斷指標(biāo)已經(jīng)超過(guò)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

根據(jù)上述日志可以看出當(dāng)異常次數(shù)達(dá)到4時(shí)會(huì)提前預(yù)警。

4.3 基于 Sentinel 實(shí)現(xiàn) Feign 全局異常兜底

Spring CLoud微服務(wù)間交互使用Feign技術(shù)框架巧颈,在網(wǎng)絡(luò)請(qǐng)求時(shí)畦木,可能會(huì)出現(xiàn)異常請(qǐng)求,如果還想再異常情況下使系統(tǒng)可用砸泛,那么就需要容錯(cuò)處理十籍,使用FeignClient時(shí)可對(duì)fallback進(jìn)行配置,但隨著接口數(shù)不斷增加唇礁,配置也越來(lái)越重復(fù)繁瑣勾栗,且大多容錯(cuò)邏輯均一致,因此需要對(duì)容錯(cuò)配置進(jìn)行代理盏筐,提供全局統(tǒng)一容錯(cuò)處理围俘。

通過(guò)官方文檔我們知道feign支持基于Hystrix fallbackFactory 和 fallback模式的,但是兩者均需要定義相應(yīng)的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());
            }
        };
    }
}

如上述示例界牡,隨著接口的增加勢(shì)必會(huì)產(chǎn)生大量類似的模板代碼。

4.3.1 擴(kuò)展SentinelFeign Builder

4.3.1.1 思路

通過(guò)對(duì) spring-cloud-starter-alibaba-sentinel包源碼分析漾抬,該包僅簡(jiǎn)單使用了四個(gè)類就實(shí)現(xiàn)對(duì)feign的支持宿亡。核心原理是通過(guò)自定義SentinelFeign構(gòu)建器重新實(shí)現(xiàn)了feign對(duì)象初始化,添加了對(duì)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);
    }
}

可以看出當(dāng)未設(shè)置fallback 或者 fallbackFactory時(shí)她混,不會(huì)傳遞fallbackFactory到SentinelInvocationHandler。因此解決思路是:

  1. 自定義全局異常兜底處理函數(shù)CustomCommonFallbackFactory泊碑;
  2. 改寫(xiě)Feign build()邏輯坤按,當(dāng)未定義fallback 或者 fallbackFactory時(shí),傳入公共的CustomCommonFallbackFactory到SentinelInvocationHandler馒过。

4.3.1.2 程序設(shè)計(jì)

自定義全局異常兜底處理函數(shù)CustomCommonFallbackFactory臭脓,具體實(shí)現(xiàn)如下:

@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;
            // 提取業(yè)務(wù)異常
            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服務(wù)已下線&服務(wù)狀態(tài)不正常.", method.getName()), null);
            } else {
                return Result.of(false, null, -1, "系統(tǒng)未知異常.", 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();
    }
}

改寫(xiě)Feign build()邏輯腹忽,當(dāng)未定義fallback 或者 fallbackFactory時(shí)来累,傳入公共的CustomCommonFallbackFactory到SentinelInvocationHandler砚作。這里需要注意,由于SentinelInvocationHandler訪問(wèn)權(quán)限限制包內(nèi)訪問(wèn)嘹锁,因此將新建的類放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構(gòu)建器,在模式SentinelFeign基礎(chǔ)增加 CustomCommonFallbackFactory领猾,
         * 當(dāng)feign配置不指定兜底函數(shù)將使用默認(rèn)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 {
                        // 默認(rèn)的 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全局異常兜底需要引入基礎(chǔ)依賴包并且在配置文件中配置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 {
}

調(diào)用feign接口,當(dāng)出現(xiàn)異常打印日志,可以看出定義的公共CustomCommonFallback觸發(fā)生效摔竿。

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]
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末面粮,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子继低,更是在濱河造成了極大的恐慌熬苍,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件袁翁,死亡現(xiàn)場(chǎng)離奇詭異柴底,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)梦裂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)似枕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人年柠,你說(shuō)我怎么就攤上這事凿歼。” “怎么了冗恨?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵答憔,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我掀抹,道長(zhǎng)虐拓,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任傲武,我火速辦了婚禮蓉驹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘揪利。我一直安慰自己态兴,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布疟位。 她就那樣靜靜地躺著瞻润,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上绍撞,一...
    開(kāi)封第一講書(shū)人閱讀 51,688評(píng)論 1 305
  • 那天正勒,我揣著相機(jī)與錄音,去河邊找鬼傻铣。 笑死章贞,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的非洲。 我是一名探鬼主播阱驾,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼怪蔑!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起丧荐,我...
    開(kāi)封第一講書(shū)人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤缆瓣,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后虹统,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體弓坞,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年车荔,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了渡冻。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡忧便,死狀恐怖族吻,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情珠增,我是刑警寧澤超歌,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站蒂教,受9級(jí)特大地震影響巍举,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜凝垛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一懊悯、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧梦皮,春花似錦炭分、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春岖妄,著一層夾襖步出監(jiān)牢的瞬間型将,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工荐虐, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留七兜,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓福扬,卻偏偏與公主長(zhǎng)得像腕铸,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子铛碑,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容