前言
上周經(jīng)歷了合作方未按照約定在客戶端進(jìn)行緩存,以高QPS調(diào)用我這邊某個(gè)接口的問題,當(dāng)時(shí)帶來(lái)的影響是接口RT變高,當(dāng)時(shí)如果QPS繼續(xù)增加,將會(huì)導(dǎo)致整個(gè)應(yīng)用級(jí)別的服務(wù)不可用。那么有沒有辦法,來(lái)限制系統(tǒng)的某個(gè)服務(wù)被調(diào)用的QPS,以保護(hù)系統(tǒng)不會(huì)過載呢?Alibaba Sentinel就是這樣的一個(gè)產(chǎn)品。本文只介紹限流的功能,Sentinel本身是極其強(qiáng)大的,支持流量控制、熔斷降級(jí)、系統(tǒng)負(fù)載保護(hù),詳見官方文檔。https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D
Sentinel如何使用
引入maven依賴
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-annotation-aspectj</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
</dependency>
- 若希望在代碼塊級(jí)別限流,使用SphU#entry和Entry#exit將代碼塊包住即可,這跟很多打點(diǎn)的工具是一樣的。
Entry entry = SphU.entry(resourceName);
businessCode();
entry.exit();
初始化流控規(guī)則配置見下,流控規(guī)則里面的resourceName和Entry初始化的時(shí)候一致即可。(Sentinel也支持控制臺(tái)的方式來(lái)配置限流規(guī)則)
private static void initFlowQpsRule() {
List<FlowRule> rules = new ArrayList<FlowRule>();
FlowRule rule1 = new FlowRule();
rule1.setResource(resourceName);
// set limit qps to 20
rule1.setCount(20);
rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule1.setLimitApp("default");
rules.add(rule1);
FlowRuleManager.loadRules(rules);
}
這種在代碼塊級(jí)別硬編碼的方式并不是我們的主要場(chǎng)景,更多的時(shí)候,我們希望在某個(gè)服務(wù),或者某個(gè)接口級(jí)別進(jìn)行限流,Sentinel支持注解的方式來(lái)配置限流。
@GetMapping("/hello")
@SentinelResource("resourceName")
public String hello() {
return "Hello";
}
只需要一個(gè)注解,即可添加限流功能。
Sentinel執(zhí)行過程
使用注解來(lái)引入限流,其實(shí)就是使用了一個(gè)aop切面自動(dòng)幫你初始化一個(gè)Entry,執(zhí)行完畢之后exit。
詳見SentinelResourceAspect
@Around("sentinelResourceAnnotationPointcut()")
public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable {
Method originMethod = resolveMethod(pjp);
SentinelResource annotation = originMethod.getAnnotation(SentinelResource.class);
if (annotation == null) {
// Should not go through here.
throw new IllegalStateException("Wrong state for SentinelResource annotation");
}
String resourceName = getResourceName(annotation.value(), originMethod);
EntryType entryType = annotation.entryType();
int resourceType = annotation.resourceType();
Entry entry = null;
try {
entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs());
Object result = pjp.proceed();
return result;
} catch (BlockException ex) {
return handleBlockException(pjp, annotation, ex);
}finally {
if (entry != null) {
entry.exit(1, pjp.getArgs());
}
}
整個(gè)限流的核心就在SphU#entry方法,如果被限流攔截,就拋出異常BlockException,不再執(zhí)行業(yè)務(wù)代碼。
如果讓我們自己實(shí)現(xiàn)一套針對(duì)服務(wù)的限流邏輯,會(huì)有兩個(gè)關(guān)鍵點(diǎn)需要考慮,一個(gè)點(diǎn)是,請(qǐng)求進(jìn)來(lái)了,需要去檢查當(dāng)前服務(wù)qps,判斷是否需要進(jìn)行攔截;另一個(gè)點(diǎn)是統(tǒng)計(jì)當(dāng)前服務(wù)的QPS,每處理一個(gè)請(qǐng)求,去更新當(dāng)前服務(wù)QPS值。
SphU#entry方法主要就是做這兩個(gè)事情。Sentinel對(duì)每一個(gè)限流的Resouce維護(hù)了一個(gè)基于滑動(dòng)窗口的計(jì)數(shù)器rollingCounterInSecond。
public class StatisticNode implements Node {
private transient volatile Metric rollingCounterInSecond;
}
請(qǐng)求進(jìn)來(lái)之后,先進(jìn)行canPassCheck,判斷是否攔截,判斷的邏輯
curCount 為當(dāng)前qps,通過滑動(dòng)窗口計(jì)數(shù)器rollingCounterInSecond計(jì)算得出
acquireCount 為請(qǐng)求個(gè)數(shù),這個(gè)數(shù)寫死的是1
this.count為限流配置
如果curCount+1>this.count則返回false,進(jìn)行攔截,然后拋出FlowException。
否則通過,去更新計(jì)數(shù)器rollingCounterInSecond
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
int curCount = this.avgUsedTokens(node);
if ((double)(curCount + acquireCount) > this.count) {
if (prioritized && this.grade == 1) {
long currentTime = TimeUtil.currentTimeMillis();
long waitInMs = node.tryOccupyNext(currentTime, acquireCount, this.count);
if (waitInMs < (long)OccupyTimeoutProperty.getOccupyTimeout()) {
node.addWaitingRequest(currentTime + waitInMs, acquireCount);
node.addOccupiedPass(acquireCount);
this.sleep(waitInMs);
throw new PriorityWaitException(waitInMs);
}
}
return false;
} else {
return true;
}
}
總結(jié)
Sentinel限流的本質(zhì)是為每個(gè)resource(限流單元)維護(hù)一個(gè)基于滑動(dòng)窗口的計(jì)數(shù)器,當(dāng)請(qǐng)求進(jìn)來(lái),先檢查計(jì)數(shù)器,校驗(yàn)是否需要攔截,通過后,更新這個(gè)計(jì)數(shù)器。限流功能可以保證我們的接口在一個(gè)可控的負(fù)載范圍內(nèi)。不至于因?yàn)槟骋粋€(gè)接口的過載導(dǎo)致整個(gè)應(yīng)用級(jí)別的不可用。