Sentinel使用原理sentinel-dashboardDubbo適配

源碼地址

使用

有關於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的官方文檔,上面有詳細的說明,並且還列舉了比較好的例子,例如:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章