Sentinel限流框架

前言

2020年,由於疫情大爆發。A所在的圖書館接到了上面的任務:1天內不允許超過50人同進在館內看書,外省人禁入內。如果你是此圖書管理員,你會怎麼做?

  1. 數據收集(訪客的身份證,來自哪)
  2. 數據統計(對一天內的訪客總數統計,兒童館統計)
  3. 規則限制(根據當前統計數據,比對規則)

簡單的例子

String resource = "進入圖書館";
Rule rule = new Rule();
rule.setResource(resource);
rule.setLimitApp("外省"); //限制外省人
rule.setCount(50);//一天只能50人
//載入規則
AuthorityRuleManager.loadRules(Collections.singletonList(rule);


String name = "訪客身份證";
String origin = "來自省份";
ContextUtil.enter(name, origin);
Entry entry = null;
try {
      //請求進入圖書館
      entry = SphU.entry(resource);
      //我是XX來自XX省,已拿到圖書館的入場證明     
} catch (BlockException ex) {
      //我是XX來自XX省,被圖書館拒絕入內   
} finally {
      if (entry != null) {
           //我出圖書館了
           entry.exit();
      }
      //我回家了
      ContextUtil.exit();
}

Sentinel的調用鏈

ContextUtil.enter(name, origin);

在這裏插入圖片描述
創建一Context對象,它表示一次訪問有檔案(上下文)。保存在ThreadLocal中的,每次執行的時候會優先到ThreadLocal中獲取。

  • name: 名字
  • origin:調用源,哪個App請求過來的
  • curEntry:調用鏈的當前entry
  • entranceNode:存入當前Entry的一些數據
  SphU.entry(1);
  SphU.entry(2);
  SphU.entry(3);

在這裏插入圖片描述
entry表示是否成功申請資源的一個憑證。

  • parent和child 表示上一個資源和下一個資源
  • createTime:當前Entry的創建時間,主要用來後期計算rt
  • resourceWrapper:當前Entry所關聯的資源
  • node:當前Entry所關聯的node,該node主要是記錄了當前context下該資源的統計信息
 entry.exit();

在這裏插入圖片描述

Sentinel中的Slot鏈

//SphU.entry最終會調用這方法
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
        throws BlockException {
        Context context = ContextUtil.getContext();
       
        //找對資源對應配置的Slot
        ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);

        Entry e = new CtEntry(resourceWrapper, chain, context);
         
        //根據資源配置的slot鏈一個一個調用 
        chain.entry(context, resourceWrapper, null, count, prioritized, args);
       
        return e;
    }

在這裏插入圖片描述

  • NodeSelectorSlot 資源和調用者雙維度的存儲對象。
  • ClusterBuilderSlot 資源維度的存儲對象,用於存儲資源的統計信息以及調用者信息。
  • StatisticsSlot 則用於記錄,統計不同維度的 runtime 信息;
  • SystemSlot 則通過系統的狀態,例如 load1 等,來控制總的入口流量;
  • AuthoritySlot 則根據黑白名單,來做黑白名單控制;
  • FlowSlot 則用於根據預設的限流規則,以及前面 slot 統計的狀態,來進行限流;
  • DegradeSlot 則通過統計信息,以及預設的規則,來做熔斷降級;

原來Sentinel通過實現不同的Slot及調用順序實現了數據收集,統計,及規則的較檢。注意,所有的Slot是資源維度的,不同資源Slot對象是不同的

Slot中的存儲Node

node是保存資源的實時統計數據的,例如:passQps,blockQps,rt等實時數據.

NodeSelectorSlot
private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(10);

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, Object... args) throws Throwable {
    // 根據「上下文」的名稱獲取DefaultNode
    // 多線程環境下,每個線程都會創建一個context,
    // 只要資源名相同,則context的名稱也相同,那麼獲取到的節點就相同
    DefaultNode node = map.get(context.getName());
    if (node == null) {
        synchronized (this) {
            node = map.get(context.getName());
            if (node == null) {
                // 如果當前「上下文」中沒有該節點,則創建一個DefaultNode節點
                node = Env.nodeBuilder.buildTreeNode(resourceWrapper, null);
                // 省略部分代碼
            }
            // 將當前node作爲「上下文」的最後一個節點的子節點添加進去
            // 如果context的curEntry.parent.curNode爲null,則添加到entranceNode中去
            // 否則添加到context的curEntry.parent.curNode中去
            ((DefaultNode)context.getLastNode()).addChild(node);
        }
    }
    // 將該節點設置爲「上下文」中的當前節點
    // 實際是將當前節點賦值給context中curEntry的curNode
    // 在Context的getLastNode中會用到在此處設置的curNode
    context.setCurNode(node);
    fireEntry(context, resourceWrapper, node, count, args);
}

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
DefaultNode是基於資源和context雙維度的一個存儲對象。

ClusterBuilderSlot
//注意這邊是static
private static volatile Map<ResourceWrapper, ClusterNode> clusterNodeMap = new HashMap<>();

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) throws Throwable {
    if (clusterNode == null) {
        synchronized (lock) {
            if (clusterNode == null) {
                // Create the cluster node.
                clusterNode = Env.nodeBuilder.buildClusterNode();
                // 將clusterNode保存到全局的map中去
                HashMap<ResourceWrapper, ClusterNode> newMap = new HashMap<ResourceWrapper, ClusterNode>(16);
                newMap.putAll(clusterNodeMap);
                newMap.put(node.getId(), clusterNode);

                clusterNodeMap = newMap;
            }
        }
    }
    // 將clusterNode塞到DefaultNode中去
    node.setClusterNode(clusterNode);

    // 省略部分代碼

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

ClusterNode是基於資源爲維度的一個存儲對象。

資源角度:Node關係圖

在這裏插入圖片描述
其中entranceNode是每個上下文的入口,該節點是直接掛在root下的,是全局唯一的,每一個context都會對應一個entranceNode。另外defaultNode是記錄當前調用的實時數據的,每個defaultNode都關聯着一個資源和clusterNode,有着相同資源的defaultNode,他們關聯着同一個clusterNode。

Slot中的數據收集及統計

StatistcSlot
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) throws Throwable {
        // 觸發下一個Slot的entry方法
        fireEntry(context, resourceWrapper, node, count, args);
        // 如果能通過SlotChain中後面的Slot的entry方法,說明沒有被限流或降級
        // 統計信息
        node.increaseThreadNum();
        node.addPassRequest();
        // 省略部分代碼
}

@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
    DefaultNode node = (DefaultNode)context.getCurNode();
    if (context.getCurEntry().getError() == null) {
        long rt = TimeUtil.currentTimeMillis() - context.getCurEntry().getCreateTime();
        if (rt > Constants.TIME_DROP_VALVE) {
            rt = Constants.TIME_DROP_VALVE;
        }
        node.rt(rt);
        node.decreaseThreadNum();
        // 省略部分代碼
    } 
    fireExit(context, resourceWrapper, count);
}

收集請求資源線程的的數量,請求數量及響應時間。

利用時間窗口Metric做統計
//node.addPassRequest()
//最終會觸發clusterNode,defaultNode的addPassRequest
public void addPassRequest(int count) {
        super.addPassRequest(count);
        this.clusterNode.addPassRequest(count);
}

//分別以秒維度和分鐘維度計數
public void addPassRequest(int count) {
        rollingCounterInSecond.addPass(count);
        rollingCounterInMinute.addPass(count);
}

//獲取當前的時間窗口,然後把數加上去
public void addPass(int count) {
    WindowWrap<MetricBucket> wrap = data.currentWindow();
    wrap.value().addPass(count);
}

//以一秒爲統計間隔,分2個時間窗口
//rollingCounterInSecond = new ArrayMetric(2,1000);
//以一分鐘爲統計間隔,分60個時間窗口
//rollingCounterInMinute = new ArrayMetric(60, 60*1000);
public abstract class LeapArray<T> {
    /**
     * LeapArray對象
     * @param sampleCount 時間窗口的個數,單位:個
     * @param intervalInMs 統計的間隔,單位:毫秒
     */
    public LeapArray(int sampleCount, int intervalInMs) {
        //第個時間窗口長度 
        this.windowLengthInMs = intervalInMs / sampleCount;
        //統計的間隔
        this.intervalInMs = intervalInMs;
        //時間窗口的個數
        this.sampleCount = sampleCount;
        //時間窗口對象數組
        this.array = new AtomicReferenceArray<>(sampleCount);
    }

}

public class WindowWrap<T> {

    //時間窗口長度,多少秒
    private final long windowLengthInMs;

    //時間窗口的起始長度
    private long windowStart;

    //用來計數用的
    private MetricBucket value;
}

拿rollingCounterInSecond=new ArrayMetric(2,1000)舉列

  1. 把時間以500ms切割成塊,設當前時間x%500=200得在這裏插入圖片描述
  2. 將時間往後推移150,300ms,得到的時間窗口都在timeId1。因此這個時間段發生的請求都將統計到timeId1時間窗口對象的value值中。
    在這裏插入圖片描述
  3. 時間繼續向前推200,得到的時間窗口在timeId2,因此這個時間段發生的請求都將統計到timeId2時間窗口對象的value值中.
    在這裏插入圖片描述
  4. 時間繼續向前推600,得到的時間窗口在timeId3,因此這個時間段發生的請求都將統計到timeId3時間窗口對象的value值中。因爲只允許2個窗口,timeId1失效

在這裏插入圖片描述

Slot中的規則效驗

FlowSlot
FlowRule rule1 = new FlowRule();
rule1.setResource(KEY);
//設置允許通過的最大請求數20; 限流閾值
rule1.setCount(20);
//設置限流閾值類型, QPS 或線程數模式, 默認是QPS. 
rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule1.setLimitApp("default");

//通過FlowSlot最終會調用DefaultController
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
        //最終會調用node.passQps()
        int curCount = avgUsedTokens(node);
        if (curCount + acquireCount > count) {
            //省略代碼
            return false;
        }
        return true;
    }

//各時間窗口的通過數,除以統計時間
 public double passQps() {
        return rollingCounterInSecond.pass() / rollingCounterInSecond.getWindowIntervalInSec();
    }

FlowSlot 很好的利用的前面收集到的數據。SystemSlot,AuthoritySlot,DegradeSlot過程相似。

Sentinel中的項目結構介紹

在這裏插入圖片描述

  1. sentinel-core 核心模塊,限流、降級、系統保護等都在這裏實現(我們上面講的)
  2. sentinel-transport 傳輸模塊,提供了基本的監控服務端和客戶端的API接口,以及一些基於不同庫的實現(與外界交互)
  3. sentinel-dashboard 控制檯模塊,可以對連接上的sentinel客戶端實現可視化的管理(客戶端裝上傳輸模塊把數據傳到這)
  4. sentinel-adapter 適配器模塊,主要實現了對一些常見框架的適配(dubbo,spring-mvc等等)
  5. sentinel-extension 擴展模塊,主要對DataSource進行了部分擴展實現(數據持久化)
  6. sentinel-benchmark 基準測試模塊,對核心代碼的精確性提供基準測試
  7. sentinel-demo 樣例模塊,可參考怎麼使用sentinel進行限流、降級等
  8. sentinel-cluster 集羣模式,提供統一tokenService實現(客戶端裝上傳輸模塊把數據傳到這統一處理)

Sentinel集羣模式

在這裏插入圖片描述

假設經過壓測,機器配置爲4C8G最高能承受的TPS爲 1500,而機器配置爲8C16G能承受的TPS爲3000,那如果採取單機限流,其闊值只能設置爲1500,因爲如果超過1500,會將4C8G的機器壓垮。

爲了充分利用硬件的資源,諸如 Dubbo 都提供了基於權重的負載均衡機制,例如可以將8C16G的機器設置的權重是4C8G的兩倍,這樣充分利用硬件資源.

如果是單機模式,我們要讓程序根據不同的機型,分別設立闊值。
如果是集羣模式,只需要對整個集羣設置一個闊值。

原理

集羣限流的原理很簡單,和單機限流一樣,都需要對 qps 等數據進行統計,區別就在於單機版是在每個實例中進行統計,而集羣版是有一個專門的實例進行統計。
在這裏插入圖片描述

  • token client:集羣流控客戶端,用於向所屬 token server 通信請求 token。集羣限流服務端會返回給客戶端結果,決定是否限流。
  • token server:即集羣流控服務端,處理來自 token client 的請求,根據配置的集羣規則判斷是否應該發放 token(是否允許通過)。
//FlowRule策略中
 public boolean canPassCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount,
                                                    boolean prioritized) {
        String limitApp = rule.getLimitApp();
        if (limitApp == null) {
            return true;
        }
        //如果是集羣模式,則調用Cluster
        if (rule.isClusterMode()) {
            return passClusterCheck(rule, context, node, acquireCount, prioritized);
        }
        //否則調用本地模式
        return passLocalCheck(rule, context, node, acquireCount, prioritized);
    }

部署方式

  1. 獨立部署,就是單獨啓動一個 token server 服務來處理 token client 的請求
  2. 嵌入部署,就是在多個 sentinel-core 中選擇一個實例設置爲 token server,隨着應用一起啓動,其他的 sentinel-core 都是集羣中 token client

若在生產環境使用集羣限流,管控端還需要關注以下的問題:

  1. Token Server 自動管理(分配/選舉 Token Server)
  2. Token Server 高可用,在某個 server 不可用時自動 failover 到其它機器

參考文章

Sentinel 原理-調用鏈
Sentinel 集羣限流設計原理

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