前言
上周經(jīng)歷了合作方未按照約定在客戶端進行緩存忆家,以高QPS調(diào)用我這邊某個接口的問題携添,當時帶來的影響是接口RT變高袍睡,當時如果QPS繼續(xù)增加,將會導(dǎo)致整個應(yīng)用級別的服務(wù)不可用艺玲。那么有沒有辦法括蝠,來限制系統(tǒng)的某個服務(wù)被調(diào)用的QPS,以保護系統(tǒng)不會過載呢饭聚?Alibaba Sentinel就是這樣的一個產(chǎn)品忌警。本文只介紹限流的功能,Sentinel本身是極其強大的秒梳,支持流量控制法绵、熔斷降級、系統(tǒng)負載保護酪碘,詳見官方文檔朋譬。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>
- 若希望在代碼塊級別限流,使用SphU#entry和Entry#exit將代碼塊包住即可兴垦,這跟很多打點的工具是一樣的徙赢。
Entry entry = SphU.entry(resourceName);
businessCode();
entry.exit();
初始化流控規(guī)則配置見下,流控規(guī)則里面的resourceName和Entry初始化的時候一致即可。(Sentinel也支持控制臺的方式來配置限流規(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);
}
這種在代碼塊級別硬編碼的方式并不是我們的主要場景探越,更多的時候狡赐,我們希望在某個服務(wù),或者某個接口級別進行限流钦幔,Sentinel支持注解的方式來配置限流枕屉。
@GetMapping("/hello")
@SentinelResource("resourceName")
public String hello() {
return "Hello";
}
只需要一個注解,即可添加限流功能节槐。
Sentinel執(zhí)行過程
使用注解來引入限流搀庶,其實就是使用了一個aop切面自動幫你初始化一個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());
}
}
整個限流的核心就在SphU#entry方法哥倔,如果被限流攔截,就拋出異常BlockException揍庄,不再執(zhí)行業(yè)務(wù)代碼咆蒿。
如果讓我們自己實現(xiàn)一套針對服務(wù)的限流邏輯,會有兩個關(guān)鍵點需要考慮,一個點是沃测,請求進來了缭黔,需要去檢查當前服務(wù)qps,判斷是否需要進行攔截蒂破;另一個點是統(tǒng)計當前服務(wù)的QPS馏谨,每處理一個請求,去更新當前服務(wù)QPS值附迷。
SphU#entry方法主要就是做這兩個事情惧互。Sentinel對每一個限流的Resouce維護了一個基于滑動窗口的計數(shù)器rollingCounterInSecond。
public class StatisticNode implements Node {
private transient volatile Metric rollingCounterInSecond;
}
請求進來之后喇伯,先進行canPassCheck喊儡,判斷是否攔截,判斷的邏輯
curCount 為當前qps,通過滑動窗口計數(shù)器rollingCounterInSecond計算得出
acquireCount 為請求個數(shù)稻据,這個數(shù)寫死的是1
this.count為限流配置
如果curCount+1>this.count則返回false艾猜,進行攔截,然后拋出FlowException捻悯。
否則通過匆赃,去更新計數(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ì)是為每個resource(限流單元)維護一個基于滑動窗口的計數(shù)器,當請求進來今缚,先檢查計數(shù)器炸庞,校驗是否需要攔截,通過后荚斯,更新這個計數(shù)器。限流功能可以保證我們的接口在一個可控的負載范圍內(nèi)查牌。不至于因為某一個接口的過載導(dǎo)致整個應(yīng)用級別的不可用事期。