限流降級(jí)神器默赂,帶你解讀阿里巴巴開源 Sentinel 實(shí)現(xiàn)原理

Sentinel 是阿里中間件團(tuán)隊(duì)開源的,面向分布式服務(wù)架構(gòu)的輕量級(jí)高可用流量控制組件括勺,主要以流量為切入點(diǎn)缆八,從流量控制、熔斷降級(jí)疾捍、系統(tǒng)負(fù)載保護(hù)等多個(gè)維度來幫助用戶保護(hù)服務(wù)的穩(wěn)定性奈辰。

大家可能會(huì)問:Sentinel 和之前常用的熔斷降級(jí)庫 Netflix Hystrix 有什么異同呢?Sentinel官網(wǎng)有一個(gè)對比的文章乱豆,這里摘抄一個(gè)總結(jié)的表格奖恰,具體的對比可以點(diǎn)此 鏈接 查看。
image

從對比的表格可以看到咙鞍,Sentinel比Hystrix在功能性上還要強(qiáng)大一些房官,本文讓我們一起來了解下Sentinel的源碼,揭開Sentinel的神秘面紗续滋。

項(xiàng)目結(jié)構(gòu)

將Sentinel的源碼fork到自己的github庫中翰守,接著把源碼clone到本地,然后開始源碼閱讀之旅吧疲酌。

首先我們看一下Sentinel項(xiàng)目的整個(gè)結(jié)構(gòu):

限流降級(jí)神器蜡峰,帶你解讀阿里巴巴開源 Sentinel 實(shí)現(xiàn)原理
  • sentinel-core 核心模塊了袁,限流、降級(jí)湿颅、系統(tǒng)保護(hù)等都在這里實(shí)現(xiàn)
  • sentinel-dashboard 控制臺(tái)模塊载绿,可以對連接上的sentinel客戶端實(shí)現(xiàn)可視化的管理
  • sentinel-transport 傳輸模塊,提供了基本的監(jiān)控服務(wù)端和客戶端的API接口油航,以及一些基于不同庫的實(shí)現(xiàn)
  • sentinel-extension 擴(kuò)展模塊崭庸,主要對DataSource進(jìn)行了部分?jǐn)U展實(shí)現(xiàn)
  • sentinel-adapter 適配器模塊,主要實(shí)現(xiàn)了對一些常見框架的適配
  • sentinel-demo 樣例模塊谊囚,可參考怎么使用sentinel進(jìn)行限流怕享、降級(jí)等
  • sentinel-benchmark 基準(zhǔn)測試模塊,對核心代碼的精確性提供基準(zhǔn)測試

運(yùn)行樣例

基本上每個(gè)框架都會(huì)帶有樣例模塊镰踏,有的叫example函筋,有的叫demo,sentinel也不例外奠伪。

那我們從sentinel的demo中找一個(gè)例子運(yùn)行下看看大致的情況吧跌帐,上面說過了sentinel主要的核心功能是做限流、降級(jí)和系統(tǒng)保護(hù)绊率,那我們就從“限流”開始看sentinel的實(shí)現(xiàn)原理吧谨敛。

限流降級(jí)神器,帶你解讀阿里巴巴開源 Sentinel 實(shí)現(xiàn)原理

可以看到sentinel-demo模塊中有很多不同的樣例滤否,我們找到basic模塊下的flow包佣盒,這個(gè)包下面就是對應(yīng)的限流的樣例,但是限流也有很多種類型的限流顽聂,我們就找根據(jù)qps限流的類看吧,其他的限流方式原理上都大差不差盯仪。

public class FlowQpsDemo {

private static final String KEY = "abc";

private static AtomicInteger pass = new AtomicInteger();

private static AtomicInteger block = new AtomicInteger();

private static AtomicInteger total = new AtomicInteger();

private static volatile boolean stop = false;

private static final int threadCount = 32;

private static int seconds = 30;

public static void main(String[] args) throws Exception {

initFlowQpsRule();

tick();

// first make the system run on a very low condition

simulateTraffic();

System.out.println("===== begin to do flow control");

System.out.println("only 20 requests per second can pass");

}

private static void initFlowQpsRule() {

List<FlowRule> rules = new ArrayList<FlowRule>();

FlowRule rule1 = new FlowRule();

rule1.setResource(KEY);

// set limit qps to 20

rule1.setCount(20);

// 設(shè)置限流類型:根據(jù)qps

rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);

rule1.setLimitApp("default");

rules.add(rule1);

// 加載限流的規(guī)則

FlowRuleManager.loadRules(rules);

}

private static void simulateTraffic() {

for (int i = 0; i < threadCount; i++) {

Thread t = new Thread(new RunTask());

t.setName("simulate-traffic-Task");

t.start();

}

}

private static void tick() {

Thread timer = new Thread(new TimerTask());

timer.setName("sentinel-timer-task");

timer.start();

}

static class TimerTask implements Runnable {

@Override

public void run() {

long start = System.currentTimeMillis();

System.out.println("begin to statistic!!!");

long oldTotal = 0;

long oldPass = 0;

long oldBlock = 0;

while (!stop) {

try {

TimeUnit.SECONDS.sleep(1);

} catch (InterruptedException e) {

}

long globalTotal = total.get();

long oneSecondTotal = globalTotal - oldTotal;

oldTotal = globalTotal;

long globalPass = pass.get();

long oneSecondPass = globalPass - oldPass;

oldPass = globalPass;

long globalBlock = block.get();

long oneSecondBlock = globalBlock - oldBlock;

oldBlock = globalBlock;

System.out.println(seconds + " send qps is: " + oneSecondTotal);

System.out.println(TimeUtil.currentTimeMillis() + ", total:" + oneSecondTotal

  • ", pass:" + oneSecondPass

  • ", block:" + oneSecondBlock);

if (seconds-- <= 0) {

stop = true;

}

}

long cost = System.currentTimeMillis() - start;

System.out.println("time cost: " + cost + " ms");

System.out.println("total:" + total.get() + ", pass:" + pass.get()

  • ", block:" + block.get());

System.exit(0);

}

}

static class RunTask implements Runnable {

@Override

public void run() {

while (!stop) {

Entry entry = null;

try {

entry = SphU.entry(KEY);

// token acquired, means pass

pass.addAndGet(1);

} catch (BlockException e1) {

block.incrementAndGet();

} catch (Exception e2) {

// biz exception

} finally {

total.incrementAndGet();

if (entry != null) {

entry.exit();

}

}

Random random2 = new Random();

try {

TimeUnit.MILLISECONDS.sleep(random2.nextInt(50));

} catch (InterruptedException e) {

// ignore

}

}

}

}

}

執(zhí)行上面的代碼后紊搪,打印出如下的結(jié)果:

限流降級(jí)神器,帶你解讀阿里巴巴開源 Sentinel 實(shí)現(xiàn)原理

可以看到全景,上面的結(jié)果中耀石,pass的數(shù)量和我們的預(yù)期并不相同,我們預(yù)期的是每秒允許pass的請求數(shù)是20個(gè)爸黄,但是目前有很多pass的請求數(shù)是超過20個(gè)的滞伟。

原因是,我們這里測試的代碼使用了多線程炕贵,注意看 threadCount 的值梆奈,一共有32個(gè)線程來模擬,而在RunTask的run方法中執(zhí)行資源保護(hù)時(shí)称开,即在 SphU.entry 的內(nèi)部是沒有加鎖的亩钟,所以就會(huì)導(dǎo)致在高并發(fā)下乓梨,pass的數(shù)量會(huì)高于20。

可以用下面這個(gè)模型來描述下清酥,有一個(gè)TimeTicker線程在做統(tǒng)計(jì)扶镀,每1秒鐘做一次。有N個(gè)RunTask線程在模擬請求焰轻,被訪問的business code被資源key保護(hù)著臭觉,根據(jù)規(guī)則,每秒只允許20個(gè)請求通過辱志。

由于pass蝠筑、block、total等計(jì)數(shù)器是全局共享的荸频,而多個(gè)RunTask線程在執(zhí)行SphU.entry申請獲取entry時(shí)菱肖,內(nèi)部沒有鎖保護(hù),所以會(huì)存在pass的個(gè)數(shù)超過設(shè)定的閾值旭从。

限流降級(jí)神器稳强,帶你解讀阿里巴巴開源 Sentinel 實(shí)現(xiàn)原理

那為了證明在單線程下限流的正確性與可靠性,那我們的模型就應(yīng)該變成了這樣:

限流降級(jí)神器和悦,帶你解讀阿里巴巴開源 Sentinel 實(shí)現(xiàn)原理

那接下來我把 threadCount 的值改為1退疫,只有一個(gè)線程來執(zhí)行這個(gè)方法,看下具體的限流結(jié)果鸽素,執(zhí)行上面的代碼后打印的結(jié)果如下:

限流降級(jí)神器褒繁,帶你解讀阿里巴巴開源 Sentinel 實(shí)現(xiàn)原理

可以看到pass數(shù)基本上維持在20,但是第一次統(tǒng)計(jì)的pass值還是超過了20馍忽。這又是什么原因?qū)е碌哪兀?/p>

其實(shí)仔細(xì)看下Demo中的代碼可以發(fā)現(xiàn)棒坏,模擬請求是用的一個(gè)線程,統(tǒng)計(jì)結(jié)果是用的另外一個(gè)線程遭笋,統(tǒng)計(jì)線程每1秒鐘統(tǒng)計(jì)一次結(jié)果坝冕,這兩個(gè)線程之間是有時(shí)間上的誤差的。從TimeTicker線程打印出來的時(shí)間戳可以看出來瓦呼,雖然每隔一秒進(jìn)行統(tǒng)計(jì)喂窟,但是當(dāng)前打印時(shí)的時(shí)間和上一次的時(shí)間還是有誤差的,不完全是1000ms的間隔央串。

要真正驗(yàn)證每秒限制20個(gè)請求磨澡,保證數(shù)據(jù)的精準(zhǔn)性,需要做基準(zhǔn)測試质和,這個(gè)不是本篇文章的重點(diǎn)稳摄,有興趣的同學(xué)可以去了解下jmh,sentinel中的基準(zhǔn)測試也是通過jmh做的饲宿。

深入原理

通過一個(gè)簡單的示例程序秩命,我們了解了sentinel可以對請求進(jìn)行限流尉共,除了限流外,還有降級(jí)和系統(tǒng)保護(hù)等功能弃锐。那現(xiàn)在我們就撥開云霧袄友,深入源碼內(nèi)部去一窺sentinel的實(shí)現(xiàn)原理吧。

首先從入口開始: SphU.entry() 霹菊。這個(gè)方法會(huì)去申請一個(gè)entry剧蚣,如果能夠申請成功,則說明沒有被限流旋廷,否則會(huì)拋出BlockException鸠按,表面已經(jīng)被限流了。

從 SphU.entry() 方法往下執(zhí)行會(huì)進(jìn)入到 Sph.entry() 饶碘,Sph的默認(rèn)實(shí)現(xiàn)類是 CtSph 目尖,在CtSph中最終會(huì)執(zhí)行到 entry(ResourceWrapperresourceWrapper,intcount,Object...args)throwsBlockException 這個(gè)方法。

我們來看一下這個(gè)方法的具體實(shí)現(xiàn):

public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {

Context context = ContextUtil.getContext();

if (context instanceof NullContext) {

// Init the entry only. No rule checking will occur.

return new CtEntry(resourceWrapper, null, context);

}

if (context == null) {

context = MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME, "", resourceWrapper.getType());

}

// Global switch is close, no rule checking will do.

if (!Constants.ON) {

return new CtEntry(resourceWrapper, null, context);

}

// 獲取該資源對應(yīng)的SlotChain

ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);

/*

  • Means processor cache size exceeds {@link Constants.MAX_SLOT_CHAIN_SIZE}, so no

  • rule checking will be done.

*/

if (chain == null) {

return new CtEntry(resourceWrapper, null, context);

}

Entry e = new CtEntry(resourceWrapper, chain, context);

try {

// 執(zhí)行Slot的entry方法

chain.entry(context, resourceWrapper, null, count, args);

} catch (BlockException e1) {

e.exit(count, args);

// 拋出BlockExecption

throw e1;

} catch (Throwable e1) {

RecordLog.info("Sentinel unexpected exception", e1);

}

return e;

}

這個(gè)方法可以分為以下幾個(gè)部分:

  • 1.對參數(shù)和全局配置項(xiàng)做檢測扎运,如果不符合要求就直接返回了一個(gè)CtEntry對象瑟曲,不會(huì)再進(jìn)行后面的限流檢測,否則進(jìn)入下面的檢測流程豪治。
  • 2.根據(jù)包裝過的資源對象獲取對應(yīng)的SlotChain
  • 3.執(zhí)行SlotChain的entry方法
  • 3.1.如果SlotChain的entry方法拋出了BlockException洞拨,則將該異常繼續(xù)向上拋出
  • 3.2.如果SlotChain的entry方法正常執(zhí)行了,則最后會(huì)將該entry對象返回
  • 4.如果上層方法捕獲了BlockException负拟,則說明請求被限流了烦衣,否則請求能正常執(zhí)行

其中比較重要的是第2、3兩個(gè)步驟掩浙,我們來分解一下這兩個(gè)步驟花吟。

創(chuàng)建SlotChain

首先看一下lookProcessChain的方法實(shí)現(xiàn):

private ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {

ProcessorSlotChain chain = chainMap.get(resourceWrapper);

if (chain == null) {

synchronized (LOCK) {

chain = chainMap.get(resourceWrapper);

if (chain == null)

// Entry size limit.

if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {

return null;

}

// 具體構(gòu)造chain的方法

chain = Env.slotsChainbuilder.build();

Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>(chainMap.size() + 1);

newMap.putAll(chainMap);

newMap.put(resourceWrapper, chain);

chainMap = newMap;

}

}

}

return chain;

}

該方法使用了一個(gè)HashMap做了緩存,key是資源對象厨姚。這里加了鎖示辈,并且做了 doublecheck 。具體構(gòu)造chain的方法是通過: Env.slotsChainbuilder.build() 這句代碼創(chuàng)建的遣蚀。那就進(jìn)入這個(gè)方法看看吧。

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());

return chain;

}

Chain是鏈條的意思纱耻,從build的方法可看出芭梯,ProcessorSlotChain是一個(gè)鏈表,里面添加了很多個(gè)Slot弄喘。具體的實(shí)現(xiàn)需要到DefaultProcessorSlotChain中去看玖喘。

public class DefaultProcessorSlotChain extends ProcessorSlotChain {

AbstractLinkedProcessorSlot<?> first = new AbstractLinkedProcessorSlot<Object>() {

@Override

public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, Object... args)

throws Throwable {

super.fireEntry(context, resourceWrapper, t, count, args);

}

@Override

public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {

super.fireExit(context, resourceWrapper, count, args);

}

};

AbstractLinkedProcessorSlot<?> end = first; @Override public void addFirst(AbstractLinkedProcessorSlot<?> protocolProcessor) { protocolProcessor.setNext(first.getNext()); first.setNext(protocolProcessor); if (end == first) { end = protocolProcessor; } } @Override

public void addLast(AbstractLinkedProcessorSlot<?> protocolProcessor)

end.setNext(protocolProcessor);

end = protocolProcessor;

}

}

DefaultProcessorSlotChain中有兩個(gè)AbstractLinkedProcessorSlot類型的變量:first和end,這就是鏈表的頭結(jié)點(diǎn)和尾節(jié)點(diǎn)蘑志。

創(chuàng)建DefaultProcessorSlotChain對象時(shí)累奈,首先創(chuàng)建了首節(jié)點(diǎn)贬派,然后把首節(jié)點(diǎn)賦值給了尾節(jié)點(diǎn),可以用下圖表示:

限流降級(jí)神器澎媒,帶你解讀阿里巴巴開源 Sentinel 實(shí)現(xiàn)原理

將第一個(gè)節(jié)點(diǎn)添加到鏈表中后搞乏,整個(gè)鏈表的結(jié)構(gòu)變成了如下圖這樣:

限流降級(jí)神器,帶你解讀阿里巴巴開源 Sentinel 實(shí)現(xiàn)原理

將所有的節(jié)點(diǎn)都加入到鏈表中后戒努,整個(gè)鏈表的結(jié)構(gòu)變成了如下圖所示:

限流降級(jí)神器请敦,帶你解讀阿里巴巴開源 Sentinel 實(shí)現(xiàn)原理

這樣就將所有的Slot對象添加到了鏈表中去了,每一個(gè)Slot都是繼承自AbstractLinkedProcessorSlot储玫。而AbstractLinkedProcessorSlot是一種責(zé)任鏈的設(shè)計(jì)侍筛,每個(gè)對象中都有一個(gè)next屬性,指向的是另一個(gè)AbstractLinkedProcessorSlot對象撒穷。其實(shí)責(zé)任鏈模式在很多框架中都有匣椰,比如Netty中是通過pipeline來實(shí)現(xiàn)的。

知道了SlotChain是如何創(chuàng)建的了端礼,那接下來就要看下是如何執(zhí)行Slot的entry方法的了禽笑。

執(zhí)行SlotChain的entry方法

lookProcessChain方法獲得的ProcessorSlotChain的實(shí)例是DefaultProcessorSlotChain,那么執(zhí)行chain.entry方法齐媒,就會(huì)執(zhí)行DefaultProcessorSlotChain的entry方法蒲每,而DefaultProcessorSlotChain的entry方法是這樣的:

@Override

public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, Object... args)

throws Throwable {

first.transformEntry(context, resourceWrapper, t, count, args);

}

也就是說,DefaultProcessorSlotChain的entry實(shí)際是執(zhí)行的first屬性的transformEntry方法喻括。

而transformEntry方法會(huì)執(zhí)行當(dāng)前節(jié)點(diǎn)的entry方法邀杏,在DefaultProcessorSlotChain中first節(jié)點(diǎn)重寫了entry方法,具體如下:

@Override

public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, Object... args)

throws Throwable {

super.fireEntry(context, resourceWrapper, t, count, args);

}

first節(jié)點(diǎn)的entry方法唬血,實(shí)際又是執(zhí)行的super的fireEntry方法望蜡,那繼續(xù)把目光轉(zhuǎn)移到fireEntry方法,具體如下:

@Override

public void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, Object... args)

throws Throwable {

if (next != null) {

next.transformEntry(context, resourceWrapper, obj, count, args);

}

}

從這里可以看到拷恨,從fireEntry方法中就開始傳遞執(zhí)行entry了脖律,這里會(huì)執(zhí)行當(dāng)前節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)transformEntry方法,上面已經(jīng)分析過了腕侄,transformEntry方法會(huì)觸發(fā)當(dāng)前節(jié)點(diǎn)的entry小泉,也就是說fireEntry方法實(shí)際是觸發(fā)了下一個(gè)節(jié)點(diǎn)的entry方法。具體的流程如下圖所示:

限流降級(jí)神器冕杠,帶你解讀阿里巴巴開源 Sentinel 實(shí)現(xiàn)原理

從圖中可以看出微姊,從最初的調(diào)用Chain的entry()方法,轉(zhuǎn)變成了調(diào)用SlotChain中Slot的entry()方法分预。從上面的分析可以知道兢交,SlotChain中的第一個(gè)Slot節(jié)點(diǎn)是NodeSelectorSlot。

執(zhí)行Slot的entry方法

現(xiàn)在可以把目光轉(zhuǎn)移到SlotChain中的第一個(gè)節(jié)點(diǎn)NodeSelectorSlot的entry方法中去了笼痹,具體的代碼如下:

@Override

public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, Object... args)

throws Throwable {

DefaultNode node = map.get(context.getName());

if (node == null) {

synchronized (this) {

node = map.get(context.getName());

if (node == null) {

node = Env.nodeBuilder.buildTreeNode(resourceWrapper, null);

HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size());

cacheMap.putAll(map);

cacheMap.put(context.getName(), node);

map = cacheMap;

}

// Build invocation tree

((DefaultNode)context.getLastNode()).addChild(node);

}

}

context.setCurNode(node);

// 由此觸發(fā)下一個(gè)節(jié)點(diǎn)的entry方法

fireEntry(context, resourceWrapper, node, count, args);

}

從代碼中可以看到配喳,NodeSelectorSlot節(jié)點(diǎn)做了一些自己的業(yè)務(wù)邏輯處理酪穿,具體的大家可以深入源碼繼續(xù)追蹤,這里大概的介紹下每種Slot的功能職責(zé):

  • NodeSelectorSlot 負(fù)責(zé)收集資源的路徑晴裹,并將這些資源的調(diào)用路徑被济,以樹狀結(jié)構(gòu)存儲(chǔ)起來,用于根據(jù)調(diào)用路徑來限流降級(jí)息拜;
  • ClusterBuilderSlot 則用于存儲(chǔ)資源的統(tǒng)計(jì)信息以及調(diào)用者信息溉潭,例如該資源的 RT, QPS, thread count 等等,這些信息將用作為多維度限流少欺,降級(jí)的依據(jù)喳瓣;
  • StatistcSlot 則用于記錄,統(tǒng)計(jì)不同緯度的 runtime 信息赞别;
  • FlowSlot 則用于根據(jù)預(yù)設(shè)的限流規(guī)則畏陕,以及前面 slot 統(tǒng)計(jì)的狀態(tài)迫卢,來進(jìn)行限流矩距;
  • AuthorizationSlot 則根據(jù)黑白名單蜡吧,來做黑白名單控制高诺;
  • DegradeSlot 則通過統(tǒng)計(jì)信息,以及預(yù)設(shè)的規(guī)則孝宗,來做熔斷降級(jí)厢洞;
  • SystemSlot 則通過系統(tǒng)的狀態(tài)祠挫,例如 load1 等蜈膨,來控制總的入口流量;

執(zhí)行完業(yè)務(wù)邏輯處理后,調(diào)用了fireEntry()方法,由此觸發(fā)了下一個(gè)節(jié)點(diǎn)的entry方法。此時(shí)我們就知道了sentinel的責(zé)任鏈就是這樣傳遞的:每個(gè)Slot節(jié)點(diǎn)執(zhí)行完自己的業(yè)務(wù)后羔挡,會(huì)調(diào)用fireEntry來觸發(fā)下一個(gè)節(jié)點(diǎn)的entry方法利术。

所以可以將上面的圖完整了轮蜕,具體如下:

限流降級(jí)神器,帶你解讀阿里巴巴開源 Sentinel 實(shí)現(xiàn)原理

至此就通過SlotChain完成了對每個(gè)節(jié)點(diǎn)的entry()方法的調(diào)用细燎,每個(gè)節(jié)點(diǎn)會(huì)根據(jù)創(chuàng)建的規(guī)則,進(jìn)行自己的邏輯處理,當(dāng)統(tǒng)計(jì)的結(jié)果達(dá)到設(shè)置的閾值時(shí),就會(huì)觸發(fā)限流、降級(jí)等事件,具體是拋出BlockException異常。

總結(jié)

sentinel主要是基于7種不同的Slot形成了一個(gè)鏈表谜诫,每個(gè)Slot都各司其職牢屋,自己做完分內(nèi)的事之后皱炉,會(huì)把請求傳遞給下一個(gè)Slot,直到在某一個(gè)Slot中命中規(guī)則后拋出BlockException而終止。

前三個(gè)Slot負(fù)責(zé)做統(tǒng)計(jì)锁蠕,后面的Slot負(fù)責(zé)根據(jù)統(tǒng)計(jì)的結(jié)果結(jié)合配置的規(guī)則進(jìn)行具體的控制舌仍,是Block該請求還是放行。

控制的類型也有很多可選項(xiàng):根據(jù)qps、線程數(shù)隙姿、冷啟動(dòng)等等。

然后基于這個(gè)核心的方法靡馁,衍生出了很多其他的功能:

  • 1欲鹏、dashboard控制臺(tái),可以可視化的對每個(gè)連接過來的sentinel客戶端 (通過發(fā)送heartbeat消息)進(jìn)行控制臭墨,dashboard和客戶端之間通過http協(xié)議進(jìn)行通訊赔嚎。
  • 2、規(guī)則的持久化胧弛,通過實(shí)現(xiàn)DataSource接口尤误,可以通過不同的方式對配置的規(guī)則進(jìn)行持久化,默認(rèn)規(guī)則是在內(nèi)存中的
  • 3结缚、對主流的框架進(jìn)行適配损晤,包括servlet,dubbo红竭,rRpc等

Dashboard控制臺(tái)

sentinel-dashboard是一個(gè)單獨(dú)的應(yīng)用尤勋,通過spring-boot進(jìn)行啟動(dòng),主要提供一個(gè)輕量級(jí)的控制臺(tái)茵宪,它提供機(jī)器發(fā)現(xiàn)最冰、單機(jī)資源實(shí)時(shí)監(jiān)控、集群資源匯總稀火,以及規(guī)則管理的功能暖哨。

我們只需要對應(yīng)用進(jìn)行簡單的配置,就可以使用這些功能憾股。

1 啟動(dòng)控制臺(tái)

1.1 下載代碼并編譯控制臺(tái)

  • 下載 控制臺(tái) 工程
  • 使用以下命令將代碼打包成一個(gè) fat jar: mvn cleanpackage

1.2 啟動(dòng)

使用如下命令啟動(dòng)編譯后的控制臺(tái):

$ java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -jar target/sentinel-dashboard.jar

上述命令中我們指定了一個(gè)JVM參數(shù)鹿蜀, -Dserver.port=8080 用于指定 Spring Boot 啟動(dòng)端口為 8080。

2 客戶端接入控制臺(tái)

控制臺(tái)啟動(dòng)后服球,客戶端需要按照以下步驟接入到控制臺(tái)茴恰。

2.1 引入客戶端jar包

通過 pom.xml 引入 jar 包:

<dependency>

<groupId>com.alibaba.csp</groupId>

<artifactId>sentinel-transport-simple-http</artifactId>

<version>x.y.z</version>

</dependency>

2.2 配置啟動(dòng)參數(shù)

啟動(dòng)時(shí)加入 JVM 參數(shù) -Dcsp.sentinel.dashboard.server=consoleIp:port 指定控制臺(tái)地址和端口。若啟動(dòng)多個(gè)應(yīng)用斩熊,則需要通過 -Dcsp.sentinel.api.port=xxxx 指定客戶端監(jiān)控 API 的端口(默認(rèn)是 8719)往枣。

除了修改 JVM 參數(shù),也可以通過配置文件取得同樣的效果。更詳細(xì)的信息可以參考 啟動(dòng)配置項(xiàng)分冈。

2.3 觸發(fā)客戶端初始化

確被恚客戶端有訪問量,Sentinel 會(huì)在客戶端首次調(diào)用的時(shí)候進(jìn)行初始化雕沉,開始向控制臺(tái)發(fā)送心跳包集乔。

sentinel-dashboard是一個(gè)獨(dú)立的web應(yīng)用,可以接受客戶端的連接坡椒,然后與客戶端之間進(jìn)行通訊扰路,他們之間使用http協(xié)議進(jìn)行通訊。他們之間的關(guān)系如下圖所示:

限流降級(jí)神器倔叼,帶你解讀阿里巴巴開源 Sentinel 實(shí)現(xiàn)原理

dashboard

dashboard啟動(dòng)后會(huì)等待客戶端的連接汗唱,具體的做法是在 MachineRegistryController 中有一個(gè) receiveHeartBeat 的方法,客戶端發(fā)送心跳消息丈攒,就是通過http請求這個(gè)方法哩罪。

dashboard接收到客戶端的心跳消息后,會(huì)把客戶端的傳遞過來的ip巡验、port等信息封裝成一個(gè) MachineInfo對象际插,然后將該對象通過 MachineDiscovery 接口的 addMachine 方法添加到一個(gè)ConcurrentHashMap中保存起來。

這里會(huì)有問題显设,因?yàn)榭蛻舳说男畔⑹潜4嬖赿ashboard的內(nèi)存中的腹鹉,所以當(dāng)dashboard應(yīng)用重啟后,之前已經(jīng)發(fā)送過來的客戶端信息都會(huì)丟失掉敷硅。

client

client在啟動(dòng)時(shí)功咒,會(huì)通過CommandCenterInitFunc選擇一個(gè),并且只選擇一個(gè)CommandCenter進(jìn)行啟動(dòng)绞蹦。

啟動(dòng)之前會(huì)通過spi的方式掃描獲取到所有的CommandHandler的實(shí)現(xiàn)類力奋,然后將所有的CommandHandler注冊到一個(gè)HashMap中去,待后期使用幽七。

PS:考慮一下景殷,為什么CommandHandler不需要做持久化,而是直接保存在內(nèi)存中澡屡。

注冊完CommandHandler之后猿挚,緊接著就啟動(dòng)CommandCenter了,目前CommandCenter有兩個(gè)實(shí)現(xiàn)類:

  • SimpleHttpCommandCenter 通過ServerSocket啟動(dòng)一個(gè)服務(wù)端驶鹉,接受socket連接
  • NettyHttpCommandCenter 通過Netty啟動(dòng)一個(gè)服務(wù)端绩蜻,接受channel連接

CommandCenter啟動(dòng)后,就等待dashboard發(fā)送消息過來了室埋,當(dāng)接收到消息后办绝,會(huì)把消息通過具體的CommandHandler進(jìn)行處理伊约,然后將處理的結(jié)果返回給dashboard。

這里需要注意的是孕蝉,dashboard給client發(fā)送消息是通過異步的httpClient進(jìn)行發(fā)送的屡律,在HttpHelper類中。

但是詭異的是降淮,既然通過異步發(fā)送了超埋,又通過一個(gè)CountDownLatch來等待消息的返回,然后獲取結(jié)果佳鳖,那這樣不就失去了異步的意義的嗎纳本?具體的代碼如下:

private String httpGetContent(String url) { final HttpGet httpGet = new HttpGet(url); final CountDownLatch latch = new CountDownLatch(1);

final AtomicReference<String> reference = new AtomicReference<>();

httpclient.execute(httpGet, new FutureCallback<HttpResponse>() {

@Override

public void completed(final HttpResponse response) {

try {

reference.set(getBody(response));

} catch (Exception e) {

logger.info("httpGetContent " + url + " error:", e);

} finally {

latch.countDown();

}

}

@Override

public void failed(final Exception ex) {

latch.countDown();

logger.info("httpGetContent " + url + " failed:", ex);

}

@Override

public void cancelled() {

latch.countDown();

}

});

try {

latch.await(5, TimeUnit.SECONDS);

} catch (Exception e) {

logger.info("wait http client error:", e);

}

return reference.get();

}

主流框架的適配

sentinel也對一些主流的框架進(jìn)行了適配,使得在使用主流框架時(shí)腋颠,也可以享受到sentinel的保護(hù)。目前已經(jīng)支持的適配器包括以下這些:

  • Web Servlet
  • Dubbo
  • Spring Boot / Spring Cloud
  • gRPC
  • Apache RocketMQ

其實(shí)做適配就是通過那些主流框架的擴(kuò)展點(diǎn)吓笙,然后在擴(kuò)展點(diǎn)上加入sentinel限流降級(jí)的代碼即可淑玫。拿Servlet的適配代碼看一下,具體的代碼是:

public class CommonFilter implements Filter {

@Override

public void init(FilterConfig filterConfig) {

}

@Override

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)

throws IOException, ServletException

HttpServletRequest sRequest = (HttpServletRequest)request;

Entry entry = null;

try {

// 根據(jù)請求生成的資源

String target = FilterUtil.filterTarget(sRequest);

target = WebCallbackManager.getUrlCleaner().clean(target);

// “申請”該資源

ContextUtil.enter(target);

entry = SphU.entry(target, EntryType.IN);

// 如果能成功“申請”到資源面睛,則說明未被限流

// 則將請求放行

chain.doFilter(request, response);

} catch (BlockException e) {

// 否則如果捕獲了BlockException異常絮蒿,說明請求被限流了

// 則將請求重定向到一個(gè)默認(rèn)的頁面

HttpServletResponse sResponse = (HttpServletResponse)response;

WebCallbackManager.getUrlBlockHandler().blocked(sRequest, sResponse);

} catch (IOException e2) {

// 省略部分代碼

} finally {

if (entry != null) {

entry.exit();

}

ContextUtil.exit();

}

}

@Override

public void destroy() {

}

}

通過Servlet的Filter進(jìn)行擴(kuò)展,實(shí)現(xiàn)一個(gè)Filter叁鉴,然后在doFilter方法中對請求進(jìn)行限流控制土涝,如果請求被限流則將請求重定向到一個(gè)默認(rèn)頁面,否則將請求放行給下一個(gè)Filter幌墓。

規(guī)則持久化但壮,動(dòng)態(tài)化

Sentinel 的理念是開發(fā)者只需要關(guān)注資源的定義,當(dāng)資源定義成功常侣,可以動(dòng)態(tài)增加各種流控降級(jí)規(guī)則蜡饵。

Sentinel 提供兩種方式修改規(guī)則:

  • 通過 API 直接修改 ( loadRules)
  • 通過 DataSource適配不同數(shù)據(jù)源修改

通過 API 修改比較直觀,可以通過以下三個(gè) API 修改不同的規(guī)則:

FlowRuleManager.loadRules(List<FlowRule> rules); // 修改流控規(guī)則

DegradeRuleManager.loadRules(List<DegradeRule> rules); // 修改降級(jí)規(guī)則

SystemRuleManager.loadRules(List<SystemRule> rules); // 修改系統(tǒng)規(guī)則

DataSource 擴(kuò)展

上述 loadRules() 方法只接受內(nèi)存態(tài)的規(guī)則對象胳施,但應(yīng)用重啟后內(nèi)存中的規(guī)則就會(huì)丟失溯祸,更多的時(shí)候規(guī)則最好能夠存儲(chǔ)在文件、數(shù)據(jù)庫或者配置中心中舞肆。

DataSource 接口給我們提供了對接任意配置源的能力焦辅。相比直接通過 API 修改規(guī)則,實(shí)現(xiàn) DataSource 接口是更加可靠的做法椿胯。

官方推薦通過控制臺(tái)設(shè)置規(guī)則后將規(guī)則推送到統(tǒng)一的規(guī)則中心筷登,用戶只需要實(shí)現(xiàn) DataSource 接口,來監(jiān)聽規(guī)則中心的規(guī)則變化哩盲,以實(shí)時(shí)獲取變更的規(guī)則仆抵。

DataSource 拓展常見的實(shí)現(xiàn)方式有:

  • 拉模式:客戶端主動(dòng)向某個(gè)規(guī)則管理中心定期輪詢拉取規(guī)則跟继,這個(gè)規(guī)則中心可以是 SQL、文件镣丑,甚至是 VCS 等舔糖。這樣做的方式是簡單,缺點(diǎn)是無法及時(shí)獲取變更莺匠;
  • 推模式:規(guī)則中心統(tǒng)一推送金吗,客戶端通過注冊監(jiān)聽器的方式時(shí)刻監(jiān)聽變化,比如使用 Nacos趣竣、Zookeeper 等配置中心摇庙。這種方式有更好的實(shí)時(shí)性和一致性保證。

至此遥缕,sentinel的基本情況都已經(jīng)分析了卫袒,更加詳細(xì)的內(nèi)容,可以繼續(xù)閱讀源碼來研究单匣。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末夕凝,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子户秤,更是在濱河造成了極大的恐慌码秉,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鸡号,死亡現(xiàn)場離奇詭異转砖,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)鲸伴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進(jìn)店門府蔗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人汞窗,你說我怎么就攤上這事礁竞。” “怎么了杉辙?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵模捂,是天一觀的道長。 經(jīng)常有香客問我蜘矢,道長狂男,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任品腹,我火速辦了婚禮岖食,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘舞吭。我一直安慰自己泡垃,他們只是感情好析珊,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蔑穴,像睡著了一般忠寻。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上存和,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天奕剃,我揣著相機(jī)與錄音,去河邊找鬼捐腿。 笑死纵朋,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的茄袖。 我是一名探鬼主播操软,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼宪祥!你這毒婦竟也來了聂薪?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤品山,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后烤低,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肘交,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年扑馁,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了涯呻。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,577評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡腻要,死狀恐怖复罐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情雄家,我是刑警寧澤效诅,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站趟济,受9級(jí)特大地震影響乱投,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜顷编,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一戚炫、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧媳纬,春花似錦双肤、人聲如沸施掏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽七芭。三九已至,卻和暖如春限匣,著一層夾襖步出監(jiān)牢的瞬間抖苦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工米死, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留锌历,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓峦筒,卻偏偏與公主長得像究西,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子物喷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評論 2 348

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