使用
有關於sentinel的使用方法和工作原理,在官方文檔中都有詳細的介紹,並且源碼中也已經給出了一系列的demo,以下是示例:
<dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-core</artifactId> <version>1.4.0-SNAPSHOT</version> </dependency>
public class AuthorityDemo { private static final String RESOURCE_NAME = "testABC"; public static void main(String[] args) { System.out.println("========Testing for black list========"); initBlackRules(); testFor(RESOURCE_NAME, "appA"); testFor(RESOURCE_NAME, "appB"); testFor(RESOURCE_NAME, "appC"); testFor(RESOURCE_NAME, "appE"); System.out.println("========Testing for white list========"); initWhiteRules(); testFor(RESOURCE_NAME, "appA"); testFor(RESOURCE_NAME, "appB"); testFor(RESOURCE_NAME, "appC"); testFor(RESOURCE_NAME, "appE"); } private static void testFor(/*@NonNull*/ String resource, /*@NonNull*/ String origin) { ContextUtil.enter(resource, origin); Entry entry = null; try { entry = SphU.entry(resource); System.out.println(String.format("Passed for resource %s, origin is %s", resource, origin)); } catch (BlockException ex) { System.err.println(String.format("Blocked for resource %s, origin is %s", resource, origin)); } finally { if (entry != null) { entry.exit(); } ContextUtil.exit(); } } private static void initWhiteRules() { AuthorityRule rule = new AuthorityRule(); rule.setResource(RESOURCE_NAME); rule.setStrategy(RuleConstant.AUTHORITY_WHITE); rule.setLimitApp("appA,appE"); AuthorityRuleManager.loadRules(Collections.singletonList(rule)); } private static void initBlackRules() { AuthorityRule rule = new AuthorityRule(); rule.setResource(RESOURCE_NAME); rule.setStrategy(RuleConstant.AUTHORITY_BLACK); rule.setLimitApp("appA,appB"); AuthorityRuleManager.loadRules(Collections.singletonList(rule)); } }
原理
SphU.entry(resource); // SphU.java public static Entry entry(String name) throws BlockException { return Env.sph.entry(name, EntryType.OUT, 1, OBJECTS0); } // Env.java public static final Sph sph = new CtSph(); // CtSph.java public Entry entry(String name, EntryType type, int count, Object... args) throws BlockException { StringResourceWrapper resource = new StringResourceWrapper(name, type); return entry(resource, count, args); }
入口其實就是在CtSph類的entry方法中。這裏引出了一個“資源”的概念,“資源”在sentinel中可以是任何東西:服務,服務裏的方法,甚至是一段代碼,比如上面demo中的RESOURCE_NAME就是一個資源。當然這只是我們字面上理解的“資源”,sentinel對資源做了抽象,即:ResourceWrapper。比如這裏的RESOURCE_NAME是一個字符串,所以對應StringResourceWrapper,StringResourceWrapper 是ResourceWrapper的子類。
entry的具體實現如下,前面是一些校驗項目,重點關注lookProcessChain方法,其實就是ProcessorSlotChain的生成過程
public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException { Context context = ContextUtil.getContext(); if (context instanceof NullContext) { // The {@link NullContext} indicates that the amount of context has exceeded the threshold, // so here init the entry only. No rule checking will be done. return new CtEntry(resourceWrapper, null, context); } if (context == null) { // Using default context. 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); } ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper); /* * Means amount of resources (slot chain) 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 { chain.entry(context, resourceWrapper, null, count, args); } catch (BlockException e1) { e.exit(count, args); throw e1; } catch (Throwable e1) { // This should not happen, unless there are errors existing in Sentinel internal. RecordLog.info("Sentinel unexpected exception", e1); } return e; }
ProcessorSlotChain生成
ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper); 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 = SlotChainProvider.newSlotChain(); Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>( chainMap.size() + 1); newMap.putAll(chainMap); newMap.put(resourceWrapper, chain); chainMap = newMap; } } } return chain; }
該方法主要就是根據資源獲取到對應的ProcessorSlotChain,這裏通過一個HashMap將資源和ProcessorSlotChain的關係緩存起來了,如果根據資源沒有在緩存中找到ProcessorSlotChain,則創建一個新的ProcessorSlotChain。而ProcessorSlotChain則是具體限流、降級等操作的入口。在sentinel中定義了一系列的功能插槽(Solt),目前有7個:NodeSelectorSlot、ClusterBuilderSlot、LogSlot、StatisticSlot、SystemSlot、AuthoritySlot、FlowSlot、DegradeSlot。每個插槽對應不同的功能,比如FlowSlot負責流量控制、DegradeSlot用來做熔斷降級,具體的可以查看官方文檔,每個資源可以對應一個或多個Solt。ProcessorSlotChain主要就是針對資源調用具體插槽的邏輯,將一個或多個插槽泡拼裝成一條鏈,在執行完當期插槽邏輯的之後,出發下一個插槽的邏輯,直到整條鏈調用完成。
SlotChainProvider.newSlotChain()的具體邏輯如下:
private static volatile SlotChainBuilder builder = null; private static final ServiceLoader<SlotChainBuilder> LOADER = ServiceLoader.load(SlotChainBuilder.class); public static ProcessorSlotChain newSlotChain() { if (builder != null) { return builder.build(); } resolveSlotChainBuilder(); if (builder == null) { RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder, using default"); builder = new DefaultSlotChainBuilder(); } return builder.build(); }
這裏引入了SPI的概念,在sentinel-core模塊的resource/META-INF/services目錄下,有一個名爲com.alibaba.csp.sentinel.slotchain.SlotChainBuilder的文件,文件內容如下:
# Default slot chain builder com.alibaba.csp.sentinel.slots.DefaultSlotChainBuilder
即,這裏引用的是DefaultSlotChainBuilder,同時這也說明我們可以自定義SlotChainBuilder實現。
public class DefaultSlotChainBuilder implements SlotChainBuilder { @Override 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; } }
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; } }
主要就是將不同的插槽拼裝成一條鏈路,addFirst表示加在鏈表的頭部,主要通過改變first的next指向來實現;addLast表示加在鏈表的尾部,主要通過改變end的next指向來實現,如果不是很理解,在紙上比劃比劃就很清楚了。
ProcessorSlotChain執行
以上是有關於ProcessorSlotChain的生成邏輯,接下來看看ProcessorSlotChain的執行邏輯,繼續回到Ctsph中的entry方法,在上面已經粘貼過一次,這裏省略部分非關鍵代碼:
public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException { ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper); // 如果chain爲空,說明資源數已經超過sentinel設置的最帶值了,默認是6000 if (chain == null) { return new CtEntry(resourceWrapper, null, context); } Entry e = new CtEntry(resourceWrapper, chain, context); try { // ProcessorSlotChain執行入口 chain.entry(context, resourceWrapper, null, count, args); } catch (BlockException e1) { e.exit(count, args); throw e1; } catch (Throwable e1) { // This should not happen, unless there are errors existing in Sentinel internal. RecordLog.info("Sentinel unexpected exception", e1); } return e; }
不難發現,入口在chain.entry(context, resourceWrapper, null, count, args),從上面的ProcessorSlotChain生成邏輯可以發現,生成的是DefaultProcessorSlotChain,所以主要關注DefaultProcessorSlotChain的entry方法
// DefaultProcessorSlotChain.java public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, Object... args) throws Throwable { first.transformEntry(context, resourceWrapper, t, count, args); } // AbstractLinkedProcessorSlot.java void transformEntry(Context context, ResourceWrapper resourceWrapper, Object o, int count, Object... args) throws Throwable { T t = (T)o; entry(context, resourceWrapper, t, count, args); } //DefaultProcessorSlotChain.java public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, Object... args) throws Throwable { super.fireEntry(context, resourceWrapper, t, count, args); } // AbstractLinkedProcessorSlot.java public void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, Object... args) throws Throwable { // 這裏的next其實就是指具體的插槽實現了 if (next != null) { next.transformEntry(context, resourceWrapper, obj, count, args); } }
最關鍵的部分其實就在fireEntry方法中,這裏的next其實就是指具體的插槽實現,比如這裏以FlowSlot爲例:
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) throws Throwable { // FlowSlot具體的插槽邏輯 checkFlow(resourceWrapper, context, node, count); // 通過調用AbstractLinkedProcessorSlot的fireEntry方法,用來觸發下一個插槽邏輯的調用 fireEntry(context, resourceWrapper, node, count, args); }
其實這就是插槽鏈的調用,比如SpringAOP中的Intercepterl鏈、Mybatis中的plugin鏈路,雖然具體的實現方式不同,但是目的都是一樣的:執行完整條鏈上的邏輯。
上面的調用都是理想的情況,即:所有的請求都通過,沒有被限制的情況。如果請求被拒絕,該怎麼處理?這裏還是以FlowSlot爲例:
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) throws Throwable { checkFlow(resourceWrapper, context, node, count); fireEntry(context, resourceWrapper, node, count, args); } void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count) throws BlockException { // Flow rule map cannot be null. Map<String, List<FlowRule>> flowRules = FlowRuleManager.getFlowRules(); List<FlowRule> rules = flowRules.get(resource.getName()); if (rules != null) { for (FlowRule rule : rules) { if (!canPassCheck(rule, context, node, count)) { throw new FlowException(rule.getLimitApp()); } } } }
可以看到,如果請求被拒絕,即:被限流了,則拋出BlockException異常,在外層如果捕獲到BlockException異常,則在裏面處理對應的邏輯。
sentinel-dashboard
sentinel-dashboard是sentinel的輕量級控制檯,該控制檯主要提供兩個功能:監控、配置。即:針對資源的監控和針對資源的配置,比如:可以配置一些規則。
sentinel-dashboard是基於spring-boot2,所以直接啓動DashboardApplication就可以了,當然也可以以jar包的方式啓動,啓動之後的界面效果如下:
沒錯,什麼都沒有,因爲這時候沒有可監控的應用。
接入到sentinel-dashboard的流程也很簡單,新建一個應用, 添加以下依賴
<!-- sentinel-dashboard --> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-transport-simple-http</artifactId> <version>1.4.0-SNAPSHOT</version> </dependency>
以下是測試代碼,其實就是源碼中的demo,這裏直接搬過來
package com.hand.sxy.sentinel; import java.util.ArrayList; import java.util.List; import java.util.Random; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import com.alibaba.csp.sentinel.util.TimeUtil; import com.alibaba.csp.sentinel.Entry; import com.alibaba.csp.sentinel.SphU; import com.alibaba.csp.sentinel.slots.block.BlockException; import com.alibaba.csp.sentinel.slots.block.RuleConstant; import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; public class FlowQps { 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 = 600000 + 40; 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); 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 } } } } }
啓動的時候,添加以下參數:
-Djava.net.preferIPv4Stack=true -Dcsp.sentinel.dashboard.server=localhost:8080 -Dcsp.sentinel.api.port=8720 -Dproject.name=我的APP
啓動之後,刷新界面,查看效果
有關於控制檯的一些功能就不過多介紹了,有興趣可以自己看看。
Dubbo適配
看官方文檔,除了dubbo適配,文檔上還有與其它主流框架適配的介紹。dubbo適配主要是涉及到兩個Filtter:SentinelDubboConsumerFilter、SentinelDubboProviderFilter。從名字上也可以看出來,SentinelDubboConsumerFilter主要是限制調用方請求;SentinelDubboProviderFilter主要就是限制提供方提供。有關於這兩個應用場景,推薦看看dubbo的官方文檔,上面有詳細的說明,並且還列舉了比較好的例子,例如: