逅弈 轉載請註明原創出處,謝謝!
系列文章
Sentinel 是阿里中介軟體團隊開源的,面向分散式服務架構的輕量級高可用流量控制元件,主要以流量為切入點,從流量控制、熔斷降級、系統負載保護等多個維度來幫助使用者保護服務的穩定性。
大家可能會問:Sentinel 和之前常用的熔斷降級庫 Netflix Hystrix 有什麼異同呢?Sentinel官網有一個對比的文章,這裡摘抄一個總結的表格,具體的對比可以點此 連結 檢視。
對比內容 | Sentinel | Hystrix |
---|---|---|
隔離策略 | 訊號量隔離 | 執行緒池隔離/訊號量隔離 |
熔斷降級策略 | 基於響應時間或失敗比率 | 基於失敗比率 |
實時指標實現 | 滑動視窗 | 滑動視窗(基於 RxJava) |
規則配置 | 支援多種資料來源 | 支援多種資料來源 |
擴充套件性 | 多個擴充套件點 | 外掛的形式 |
基於註解的支援 | 支援 | 支援 |
限流 | 基於 QPS,支援基於呼叫關係的限流 | 不支援 |
流量整形 | 支援慢啟動、勻速器模式 | 不支援 |
系統負載保護 | 支援 | 不支援 |
控制檯 | 開箱即用,可配置規則、檢視秒級監控、機器發現等 | 不完善 |
常見框架的適配 | Servlet、Spring Cloud、Dubbo、gRPC 等 | Servlet、Spring Cloud Netflix |
從對比的表格可以看到,Sentinel比Hystrix在功能性上還要強大一些,本文讓我們一起來了解下Sentinel的原始碼,揭開Sentinel的神祕面紗。
專案結構
將Sentinel的原始碼fork到自己的github庫中,接著把原始碼clone到本地,然後開始原始碼閱讀之旅吧。
首先我們看一下Sentinel專案的整個結構:
- sentinel-core 核心模組,限流、降級、系統保護等都在這裡實現
- sentinel-dashboard 控制檯模組,可以對連線上的sentinel客戶端實現視覺化的管理
- sentinel-transport 傳輸模組,提供了基本的監控服務端和客戶端的API介面,以及一些基於不同庫的實現
- sentinel-extension 擴充套件模組,主要對DataSource進行了部分擴充套件實現
- sentinel-adapter 介面卡模組,主要實現了對一些常見框架的適配
- sentinel-demo 樣例模組,可參考怎麼使用sentinel進行限流、降級等
- sentinel-benchmark 基準測試模組,對核心程式碼的精確性提供基準測試
執行樣例
基本上每個框架都會帶有樣例模組,有的叫example,有的叫demo,sentinel也不例外。
那我們從sentinel的demo中找一個例子執行下看看大致的情況吧,上面說過了sentinel主要的核心功能是做限流、降級和系統保護,那我們就從“限流”開始看sentinel的實現原理吧。
可以看到sentinel-demo模組中有很多不同的樣例,我們找到basic模組下的flow包,這個包下面就是對應的限流的樣例,但是限流也有很多種型別的限流,我們就找根據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);
// 設定限流型別:根據qps
rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule1.setLimitApp("default");
rules.add(rule1);
// 載入限流的規則
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
}
}
}
}
}
複製程式碼
執行上面的程式碼後,列印出如下的結果:
可以看到,上面的結果中,pass的數量和我們的預期並不相同,我們預期的是每秒允許pass的請求數是20個,但是目前有很多pass的請求數是超過20個的。
原因是,我們這裡測試的程式碼使用了多執行緒,注意看 threadCount
的值,一共有32個執行緒來模擬,而在RunTask的run方法中執行資源保護時,即在 SphU.entry
的內部是沒有加鎖的,所以就會導致在高併發下,pass的數量會高於20。
可以用下面這個模型來描述下,有一個TimeTicker執行緒在做統計,每1秒鐘做一次。有N個RunTask執行緒在模擬請求,被訪問的business code被資源key保護著,根據規則,每秒只允許20個請求通過。
由於pass、block、total等計數器是全域性共享的,而多個RunTask執行緒在執行SphU.entry申請獲取entry時,內部沒有鎖保護,所以會存在pass的個數超過設定的閾值。
那為了證明在單執行緒下限流的正確性與可靠性,那我們的模型就應該變成了這樣:
那接下來我把 threadCount
的值改為1,只有一個執行緒來執行這個方法,看下具體的限流結果,執行上面的程式碼後列印的結果如下:
可以看到pass數基本上維持在20,但是第一次統計的pass值還是超過了20。這又是什麼原因導致的呢?
其實仔細看下Demo中的程式碼可以發現,模擬請求是用的一個執行緒,統計結果是用的另外一個執行緒,統計執行緒每1秒鐘統計一次結果,這兩個執行緒之間是有時間上的誤差的。從TimeTicker執行緒列印出來的時間戳可以看出來,雖然每隔一秒進行統計,但是當前列印時的時間和上一次的時間還是有誤差的,不完全是1000ms的間隔。
要真正驗證每秒限制20個請求,保證資料的精準性,需要做基準測試,這個不是本篇文章的重點,有興趣的同學可以去了解下jmh,sentinel中的基準測試也是通過jmh做的。
深入原理
通過一個簡單的示例程式,我們瞭解了sentinel可以對請求進行限流,除了限流外,還有降級和系統保護等功能。那現在我們就撥開雲霧,深入原始碼內部去一窺sentinel的實現原理吧。
首先從入口開始:SphU.entry()
。這個方法會去申請一個entry,如果能夠申請成功,則說明沒有被限流,否則會丟擲BlockException,表面已經被限流了。
從 SphU.entry()
方法往下執行會進入到 Sph.entry()
,Sph的預設實現類是 CtSph
,在CtSph中最終會執行到 entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException
這個方法。
我們來看一下這個方法的具體實現:
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);
}
// 獲取該資源對應的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 {
// 執行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;
}
複製程式碼
這個方法可以分為以下幾個部分:
- 1.對引數和全域性配置項做檢測,如果不符合要求就直接返回了一個CtEntry物件,不會再進行後面的限流檢測,否則進入下面的檢測流程。
- 2.根據包裝過的資源物件獲取對應的SlotChain
- 3.執行SlotChain的entry方法
- 3.1.如果SlotChain的entry方法丟擲了BlockException,則將該異常繼續向上丟擲
- 3.2.如果SlotChain的entry方法正常執行了,則最後會將該entry物件返回
- 4.如果上層方法捕獲了BlockException,則說明請求被限流了,否則請求能正常執行
其中比較重要的是第2、3兩個步驟,我們來分解一下這兩個步驟。
建立SlotChain
首先看一下lookProcessChain的方法實現:
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;
}
// 具體構造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;
}
複製程式碼
該方法使用了一個HashMap做了快取,key是資源物件。這裡加了鎖,並且做了 double check
。具體構造chain的方法是通過: Env.slotsChainbuilder.build()
這句程式碼建立的。那就進入這個方法看看吧。
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是一個連結串列,裡面新增了很多個Slot。具體的實現需要到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中有兩個AbstractLinkedProcessorSlot型別的變數:first和end,這就是連結串列的頭結點和尾節點。
建立DefaultProcessorSlotChain物件時,首先建立了首節點,然後把首節點賦值給了尾節點,可以用下圖表示:
將第一個節點新增到連結串列中後,整個連結串列的結構變成了如下圖這樣:
將所有的節點都加入到連結串列中後,整個連結串列的結構變成了如下圖所示:
這樣就將所有的Slot物件新增到了連結串列中去了,每一個Slot都是繼承自AbstractLinkedProcessorSlot。而AbstractLinkedProcessorSlot是一種責任鏈的設計,每個物件中都有一個next屬性,指向的是另一個AbstractLinkedProcessorSlot物件。其實責任鏈模式在很多框架中都有,比如Netty中是通過pipeline來實現的。
知道了SlotChain是如何建立的了,那接下來就要看下是如何執行Slot的entry方法的了。
執行SlotChain的entry方法
lookProcessChain方法獲得的ProcessorSlotChain的例項是DefaultProcessorSlotChain,那麼執行chain.entry方法,就會執行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實際是執行的first屬性的transformEntry方法。
而transformEntry方法會執行當前節點的entry方法,在DefaultProcessorSlotChain中first節點重寫了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節點的entry方法,實際又是執行的super的fireEntry方法,那繼續把目光轉移到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方法中就開始傳遞執行entry了,這裡會執行當前節點的下一個節點transformEntry方法,上面已經分析過了,transformEntry方法會觸發當前節點的entry,也就是說fireEntry方法實際是觸發了下一個節點的entry方法。具體的流程如下圖所示:
從圖中可以看出,從最初的呼叫Chain的entry()方法,轉變成了呼叫SlotChain中Slot的entry()方法。從上面的分析可以知道,SlotChain中的第一個Slot節點是NodeSelectorSlot。
執行Slot的entry方法
現在可以把目光轉移到SlotChain中的第一個節點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);
// 由此觸發下一個節點的entry方法
fireEntry(context, resourceWrapper, node, count, args);
}
複製程式碼
從程式碼中可以看到,NodeSelectorSlot節點做了一些自己的業務邏輯處理,具體的大家可以深入原始碼繼續追蹤,這裡大概的介紹下每種Slot的功能職責:
NodeSelectorSlot
負責收集資源的路徑,並將這些資源的呼叫路徑,以樹狀結構儲存起來,用於根據呼叫路徑來限流降級;ClusterBuilderSlot
則用於儲存資源的統計資訊以及呼叫者資訊,例如該資源的 RT, QPS, thread count 等等,這些資訊將用作為多維度限流,降級的依據;StatistcSlot
則用於記錄,統計不同緯度的 runtime 資訊;FlowSlot
則用於根據預設的限流規則,以及前面 slot 統計的狀態,來進行限流;AuthorizationSlot
則根據黑白名單,來做黑白名單控制;DegradeSlot
則通過統計資訊,以及預設的規則,來做熔斷降級;SystemSlot
則通過系統的狀態,例如 load1 等,來控制總的入口流量;
執行完業務邏輯處理後,呼叫了fireEntry()方法,由此觸發了下一個節點的entry方法。此時我們就知道了sentinel的責任鏈就是這樣傳遞的:每個Slot節點執行完自己的業務後,會呼叫fireEntry來觸發下一個節點的entry方法。
所以可以將上面的圖完整了,具體如下:
至此就通過SlotChain完成了對每個節點的entry()方法的呼叫,每個節點會根據建立的規則,進行自己的邏輯處理,當統計的結果達到設定的閾值時,就會觸發限流、降級等事件,具體是丟擲BlockException異常。
總結
sentinel主要是基於7種不同的Slot形成了一個連結串列,每個Slot都各司其職,自己做完分內的事之後,會把請求傳遞給下一個Slot,直到在某一個Slot中命中規則後丟擲BlockException而終止。
前三個Slot負責做統計,後面的Slot負責根據統計的結果結合配置的規則進行具體的控制,是Block該請求還是放行。
控制的型別也有很多可選項:根據qps、執行緒數、冷啟動等等。
然後基於這個核心的方法,衍生出了很多其他的功能:
- 1、dashboard控制檯,可以視覺化的對每個連線過來的sentinel客戶端 (通過傳送heartbeat訊息)進行控制,dashboard和客戶端之間通過http協議進行通訊。
- 2、規則的持久化,通過實現DataSource介面,可以通過不同的方式對配置的規則進行持久化,預設規則是在記憶體中的
- 3、對主流的框架進行適配,包括servlet,dubbo,rRpc等
Dashboard控制檯
sentinel-dashboard是一個單獨的應用,通過spring-boot進行啟動,主要提供一個輕量級的控制檯,它提供機器發現、單機資源實時監控、叢集資源彙總,以及規則管理的功能。
我們只需要對應用進行簡單的配置,就可以使用這些功能。
1 啟動控制檯
1.1 下載程式碼並編譯控制檯
- 下載 控制檯 工程
- 使用以下命令將程式碼打包成一個 fat jar:
mvn clean package
1.2 啟動
使用如下命令啟動編譯後的控制檯:
$ java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -jar target/sentinel-dashboard.jar
複製程式碼
上述命令中我們指定了一個JVM引數,-Dserver.port=8080
用於指定 Spring Boot 啟動埠為 8080
。
2 客戶端接入控制檯
控制檯啟動後,客戶端需要按照以下步驟接入到控制檯。
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 配置啟動引數
啟動時加入 JVM 引數 -Dcsp.sentinel.dashboard.server=consoleIp:port
指定控制檯地址和埠。若啟動多個應用,則需要通過 -Dcsp.sentinel.api.port=xxxx
指定客戶端監控 API 的埠(預設是 8719)。
除了修改 JVM 引數,也可以通過配置檔案取得同樣的效果。更詳細的資訊可以參考 啟動配置項。
2.3 觸發客戶端初始化
確保客戶端有訪問量,Sentinel 會在客戶端首次呼叫的時候進行初始化,開始向控制檯傳送心跳包。
sentinel-dashboard是一個獨立的web應用,可以接受客戶端的連線,然後與客戶端之間進行通訊,他們之間使用http協議進行通訊。他們之間的關係如下圖所示:
dashboard
dashboard啟動後會等待客戶端的連線,具體的做法是在 MachineRegistryController
中有一個 receiveHeartBeat
的方法,客戶端傳送心跳訊息,就是通過http請求這個方法。
dashboard接收到客戶端的心跳訊息後,會把客戶端的傳遞過來的ip、port等資訊封裝成一個 MachineInfo
物件,然後將該物件通過 MachineDiscovery
介面的 addMachine
方法新增到一個ConcurrentHashMap中儲存起來。
這裡會有問題,因為客戶端的資訊是儲存在dashboard的記憶體中的,所以當dashboard應用重啟後,之前已經傳送過來的客戶端資訊都會丟失掉。
client
client在啟動時,會通過CommandCenterInitFunc選擇一個,並且只選擇一個CommandCenter進行啟動。
啟動之前會通過spi的方式掃描獲取到所有的CommandHandler的實現類,然後將所有的CommandHandler註冊到一個HashMap中去,待後期使用。
PS:考慮一下,為什麼CommandHandler不需要做持久化,而是直接儲存在記憶體中。
註冊完CommandHandler之後,緊接著就啟動CommandCenter了,目前CommandCenter有兩個實現類:
- SimpleHttpCommandCenter 通過ServerSocket啟動一個服務端,接受socket連線
- NettyHttpCommandCenter 通過Netty啟動一個服務端,接受channel連線
CommandCenter啟動後,就等待dashboard傳送訊息過來了,當接收到訊息後,會把訊息通過具體的CommandHandler進行處理,然後將處理的結果返回給dashboard。
這裡需要注意的是,dashboard給client傳送訊息是通過非同步的httpClient進行傳送的,在HttpHelper類中。
但是詭異的是,既然通過非同步傳送了,又通過一個CountDownLatch來等待訊息的返回,然後獲取結果,那這樣不就失去了非同步的意義的嗎?具體的程式碼如下:
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也對一些主流的框架進行了適配,使得在使用主流框架時,也可以享受到sentinel的保護。目前已經支援的介面卡包括以下這些:
- Web Servlet
- Dubbo
- Spring Boot / Spring Cloud
- gRPC
- Apache RocketMQ
其實做適配就是通過那些主流框架的擴充套件點,然後在擴充套件點上加入sentinel限流降級的程式碼即可。拿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 {
// 根據請求生成的資源
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異常,說明請求被限流了
// 則將請求重定向到一個預設的頁面
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進行擴充套件,實現一個Filter,然後在doFilter方法中對請求進行限流控制,如果請求被限流則將請求重定向到一個預設頁面,否則將請求放行給下一個Filter。
規則持久化,動態化
Sentinel 的理念是開發者只需要關注資源的定義,當資源定義成功,可以動態增加各種流控降級規則。
Sentinel 提供兩種方式修改規則:
- 通過 API 直接修改 (
loadRules
) - 通過
DataSource
適配不同資料來源修改
通過 API 修改比較直觀,可以通過以下三個 API 修改不同的規則:
FlowRuleManager.loadRules(List<FlowRule> rules); // 修改流控規則
DegradeRuleManager.loadRules(List<DegradeRule> rules); // 修改降級規則
SystemRuleManager.loadRules(List<SystemRule> rules); // 修改系統規則
複製程式碼
DataSource 擴充套件
上述 loadRules()
方法只接受記憶體態的規則物件,但應用重啟後記憶體中的規則就會丟失,更多的時候規則最好能夠儲存在檔案、資料庫或者配置中心中。
DataSource
介面給我們提供了對接任意配置源的能力。相比直接通過 API 修改規則,實現 DataSource
介面是更加可靠的做法。
官方推薦通過控制檯設定規則後將規則推送到統一的規則中心,使用者只需要實現 DataSource 介面,來監聽規則中心的規則變化,以實時獲取變更的規則。
DataSource
擴充常見的實現方式有:
- 拉模式:客戶端主動向某個規則管理中心定期輪詢拉取規則,這個規則中心可以是 SQL、檔案,甚至是 VCS 等。這樣做的方式是簡單,缺點是無法及時獲取變更;
- 推模式:規則中心統一推送,客戶端通過註冊監聽器的方式時刻監聽變化,比如使用 Nacos、Zookeeper 等配置中心。這種方式有更好的實時性和一致性保證。
至此,sentinel的基本情況都已經分析了,更加詳細的內容,可以繼續閱讀原始碼來研究。
更多原創好文,請關注公眾號「逅弈逐碼」