六、Alibaba sentinel之限流原理分析

沐雨金鱗發表於2020-11-22

1、找到該檔案,開啟 

 

2、程式碼如下:

有一個TimeTicker執行緒在做統計,每1秒鐘做一次。有N個RunTask執行緒在模擬請求,被訪問的business code被資源key保護著,根據規則,每秒只允許20個請求通過。

package com.sentinel.sentinel.test;

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;
import com.alibaba.csp.sentinel.util.TimeUtil;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

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);
        // 流控針對的呼叫來源
        // default,代表不區分呼叫來源
        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
                }
            }
        }
    }
}

 

3、程式碼分析

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

        // 設定規則
        initFlowQpsRule();

        // 統計執行緒,每一秒執行以此
        tick();

        // 模擬qps,Task執行緒,while不斷建立,看1s內能有多少通過
        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);
        // 流控針對的呼叫來源
        // default,代表不區分呼叫來源
        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();
        }
    }
// 統計執行緒,每s執行1次
    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!!!");

            // 總qps
            long oldTotal = 0;
            // 通過的qps
            long oldPass = 0;
            // 未通過的qps
            long oldBlock = 0;

            // 不斷執行統計
            while (!stop) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                }
                long globalTotal = total.get();
                // 獲得最新1s的總qps
                long oneSecondTotal = globalTotal - oldTotal;
                oldTotal = globalTotal;

                // 獲得最新1s的通過的qps
                long globalPass = pass.get();
                long oneSecondPass = globalPass - oldPass;
                oldPass = globalPass;

                // 獲得最新1s的未通過的qps
                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);

                // 30s後停止
                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);
                    // 通過+1
                    pass.addAndGet(1);
                } catch (BlockException e1) {
                    // 未通過+1
                    block.incrementAndGet();
                } catch (Exception e2) {
                    // biz exception
                } finally {
                    // 總+1
                    total.incrementAndGet();
                    if (entry != null) {
                        entry.exit();
                    }
                }

                // 睡一會,不然建立的匯流排程太大,qps太大,只是模擬一下,夠大就行
                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。
  • 由於pass、block、total等計數器是全域性共享的,而多個RunTask執行緒在執行SphU.entry申請獲取entry時,內部沒有鎖保護,所以會存在pass的個數超過設定的閾值。

問題二:可以看到pass數基本上維持在20,但是第一次統計的pass值還是超過了20。這又是什麼原因導致的呢?

  • 其實仔細看下Demo中的程式碼可以發現,模擬請求是用的一個執行緒,統計結果是用的另外一個執行緒,統計執行緒每1秒鐘統計一次結果,這兩個執行緒之間是有時間上的誤差的。從TimeTicker執行緒列印出來的時間戳可以看出來,雖然每隔一秒進行統計,但是當前列印時的時間和上一次的時間還是有誤差的,不完全是1000ms的間隔。 

 

4、深入原理

我們通過上面可以看到,最主要的程式碼其實就是這個:

entry = SphU.entry(KEY);

SphU.entry() 。這個方法會去申請一個entry,如果能夠申請成功,則說明沒有被限流,否則會丟擲BlockException,表面已經被限流了。

 

剩下的看這個吧:https://zhuanlan.zhihu.com/p/67469675,之後有時間自己再寫。

 

 

 

 

 

相關文章