[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)等輔助模塊绎晃。
實(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。因此解決思路是:
- 自定義全局異常兜底處理函數(shù)CustomCommonFallbackFactory泊碑;
- 改寫(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]