<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          阿里 Sentinel 源碼解析

          共 36395字,需瀏覽 73分鐘

           ·

          2021-03-25 22:47

          點擊上方藍色“肉眼品世界”,選擇“設(shè)為星標”

          深度價值體系傳遞

          本文介紹阿里開源的 Sentinel 源碼,GitHub: alibaba/Sentinel,基于當(dāng)前(2019-12)最新的 release 版本 1.7.0。

          總體來說,Sentinel 的源碼比較簡單,復(fù)雜的部分在于它的模型對于初學(xué)者來說不好理解。

          雖然本文不是很長,最后兩節(jié)還和主流程無關(guān),但是,本文對于源碼分析已經(jīng)非常細致了。

          閱讀建議:在閱讀本文前,你應(yīng)該至少了解過 Sentinel 是什么,如果使用過 Sentinel 或已經(jīng)閱讀過部分源碼那就更好了。

          另外,本文不涉及到集群流控。由于很多讀者也沒使用過 Hystrix,所以本文也不做任何對比。

          更新 2019-12-11:更新了滑動窗口秒級數(shù)據(jù)統(tǒng)計 OccupiableBucketLeapArray 的分析。

          簡介

          Sentinel 的定位是流量控制、熔斷降級,你應(yīng)該把它理解為一個第三方 Jar 包。

          這個 Jar 包會進行流量統(tǒng)計,執(zhí)行流量控制規(guī)則。而統(tǒng)計數(shù)據(jù)的展示和規(guī)則的設(shè)置在 sentinel-dashboard 項目中,這是一個 Spring MVC 應(yīng)用,有后臺管理界面,我們通過這個管理后臺和各個應(yīng)用進行交互。

          當(dāng)然,你不一定需要 dashboard,很長一段時間,我僅僅使用 sentinel-core,它會將統(tǒng)計信息寫入到指定的日志文件中,我通過該文件內(nèi)容來了解每個接口的流量情況。當(dāng)然,這種情況下,我只是使用到了 Sentinel 的流量監(jiān)控功能而已。

          18

          從左側(cè)我們可以看到這個 dashboard 可以管理很多應(yīng)用,而對于每個應(yīng)用,我們還可以有很多機器實例(見機器列表)。我們在這個后臺,可以非常直觀地了解到每個接口的 QPS 數(shù)據(jù),我們可以對每個接口設(shè)置流量控制規(guī)則、降級規(guī)則等。

          這個 dashboard 應(yīng)用默認是不持久化數(shù)據(jù)的,它的所有數(shù)據(jù)都是在內(nèi)存中的,所以 dashboard 重啟意味著所有的數(shù)據(jù)都會丟失。你應(yīng)該按照自己的需要來定制化 dashboard,如至少你應(yīng)該要持久化規(guī)則設(shè)置,QPS 數(shù)據(jù)非常適合存放在時序數(shù)據(jù)庫中,當(dāng)然如果你的數(shù)據(jù)量不大,存 MySQL 也問題不大,定期清理一下過期數(shù)據(jù)即可,因為大部分人應(yīng)該不會關(guān)心一個月以前的 QPS 數(shù)據(jù)。

          sentinel-dashboard 并沒有定位為一個功能強大的管理后臺,一般來說,我們需要基于它來進行二次開發(fā),甚至于你也可以不使用這個 Java 項目,自己使用其他的語言來實現(xiàn)。在最后一小節(jié),我介紹了業(yè)務(wù)應(yīng)用是怎么和 dashboard 應(yīng)用交互的。

          Sentinel 的數(shù)據(jù)統(tǒng)計

          在正式開始介紹 Sentinel 的流程源碼之前,我想先和大家介紹一下 Sentinel 的數(shù)據(jù)統(tǒng)計模塊的內(nèi)容,這樣讀者在后面看到相應(yīng)的內(nèi)容的時候心里有一些底。這節(jié)內(nèi)容還是比較簡單的,當(dāng)然,如果你希望立馬進入 Sentinel 的主流程,可以先跳過這一節(jié)。

          Sentinel 的定位是流量控制,它有兩個維度的控制,一個是控制并發(fā)線程數(shù),另一個是控制 QPS,它們都是針對某個具體的接口來設(shè)置的,其實說資源比較準確,Sentinel 把控制的粒度定義為 Resource。

          既然要做控制,那么首先,Sentinel 就要先做統(tǒng)計,它要知道當(dāng)前接口的 QPS 和并發(fā)是多少,進而判斷一個新的請求能不能讓它通過。

          這里我們先拋開 Sentinel 的各種概念,直接先看下數(shù)據(jù)統(tǒng)計的代碼。數(shù)據(jù)統(tǒng)計的代碼在 StatisticNode 中,對于 QPS 數(shù)據(jù),它使用了滑動窗口的設(shè)計:

          private transient volatile Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT,
              IntervalProperty.INTERVAL);

          private transient Metric rollingCounterInMinute = new ArrayMetric(60, 60 * 1000, false);

          private AtomicInteger curThreadNum = new AtomicInteger(0);

          先看最后的屬性 curThreadNum,它使用 AtomicInteger 來統(tǒng)計并發(fā)量,就是原子加、原子減的操作,非常簡單,這里不浪費篇幅了,下面僅介紹 QPS 的統(tǒng)計。

          從上面的代碼也可以知道,Sentinel 統(tǒng)計了 兩個維度的數(shù)據(jù),下面我們簡單說說實現(xiàn)類 ArrayMetric 的源碼設(shè)計。

          public class ArrayMetric implements Metric {

              private final LeapArray<MetricBucket> data;

              public ArrayMetric(int sampleCount, int intervalInMs) {
                  this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
              }

              public ArrayMetric(int sampleCount, int intervalInMs, boolean enableOccupy) {
                  if (enableOccupy) {
                      this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
                  } else {
                      this.data = new BucketLeapArray(sampleCount, intervalInMs);
                  }
              }
              ......
          }

          ArrayMetric 的內(nèi)部是一個 LeapArray,我們以分鐘維度統(tǒng)計的使用來說,它使用子類 BucketLeapArray 實現(xiàn)。

          這里先介紹較為簡單的 BucketLeapArray 的實現(xiàn),然后在最后一節(jié)會介紹 OccupiableBucketLeapArray。

          public abstract class LeapArray<T> {

              protected int windowLengthInMs;
              protected int sampleCount;
              protected int intervalInMs;

              protected final AtomicReferenceArray<WindowWrap<T>> array;

              // 對于分鐘維度的設(shè)置,sampleCount 為 60,intervalInMs 為 60 * 1000
              public LeapArray(int sampleCount, int intervalInMs) {
                          // 單個窗口長度,這里是 1000ms
                  this.windowLengthInMs = intervalInMs / sampleCount;
                  // 一輪總時長 60,000 ms
                  this.intervalInMs = intervalInMs;
                  // 60 個窗口
                  this.sampleCount = sampleCount;

                  this.array = new AtomicReferenceArray<>(sampleCount);
              }
              // ......
          }

          它的內(nèi)部核心是一個數(shù)組 array,它的長度為 60,也就是有 60 個窗口,每個窗口長度為 1 秒,剛好一分鐘走完一輪。然后下一輪開啟“覆蓋”操作。

          2

          每個窗口是一個 WindowWrap 類實例。

          • 添加數(shù)據(jù)的時候,先判斷當(dāng)前走到哪個窗口了(當(dāng)前時間(s) % 60 即可),然后需要判斷這個窗口是否是過期數(shù)據(jù),如果是過期數(shù)據(jù)(窗口代表的時間距離當(dāng)前已經(jīng)超過 1 分鐘),需要先重置這個窗口實例的數(shù)據(jù)。

          • 統(tǒng)計數(shù)據(jù)同理,如統(tǒng)計過去一分鐘的 QPS 數(shù)據(jù),就是將每個窗口的值相加,當(dāng)中需要判斷窗口數(shù)據(jù)是否是過期數(shù)據(jù),即判斷窗口的 WindowWrap 實例是否是一分鐘內(nèi)的數(shù)據(jù)。

          核心邏輯都封裝在了 currentWindow(long timeMillis)values(long timeMillis)方法中。

          添加數(shù)據(jù)的時候,我們要先獲取操作的目標窗口,也就是 currentWindow 這個方法,Sentinel 在這里處理初始化和過期重置的情況:

          public WindowWrap<T> currentWindow(long timeMillis) {
              if (timeMillis < 0) {
                  return null;
              }
              // 獲取窗口下標
              int idx = calculateTimeIdx(timeMillis);
              // 計算該窗口的理論開始時間
              long windowStart = calculateWindowStart(timeMillis);

              // 嵌套在一個循環(huán)中,因為有并發(fā)的情況
              while (true) {
                  WindowWrap<T> old = array.get(idx);
                  if (old == null) {
                      // 窗口未實例化的情況,使用一個 CAS 來設(shè)置該窗口實例
                      WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
                      if (array.compareAndSet(idx, null, window)) {
                          return window;
                      } else {
                          // 存在競爭
                          Thread.yield();
                      }
                  } else if (windowStart == old.windowStart()) {
                      // 當(dāng)前數(shù)組中的窗口沒有過期
                      return old;
                  } else if (windowStart > old.windowStart()) {
                      // 該窗口已過期,重置窗口的值。使用一個鎖來控制并發(fā)。
                      if (updateLock.tryLock()) {
                          try {
                              return resetWindowTo(old, windowStart);
                          } finally {
                              updateLock.unlock();
                          }
                      } else {
                          Thread.yield();
                      }
                  } else if (windowStart < old.windowStart()) {
                      // 正常情況都不會走到這個分支,異常情況其實就是時鐘回撥,這里返回一個 WindowWrap 是容錯
                      return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
                  }
              }
          }

          獲取數(shù)據(jù),使用的是 values 方法,這個方法返回“有效的”窗口中的數(shù)據(jù):

          public List<T> values(long timeMillis) {
              if (timeMillis < 0) {
                  return new ArrayList<T>();
              }
              int size = array.length();
              List<T> result = new ArrayList<T>(size);

              for (int i = 0; i < size; i++) {
                  WindowWrap<T> windowWrap = array.get(i);
                  // 過濾掉過期數(shù)據(jù)
                  if (windowWrap == null || isWindowDeprecated(timeMillis, windowWrap)) {
                      continue;
                  }
                  result.add(windowWrap.value());
              }
              return result;
          }

          // 判斷當(dāng)前窗口的數(shù)據(jù)是否是 60 秒內(nèi)的
          public boolean isWindowDeprecated(long time, WindowWrap<T> windowWrap) {
              return time - windowWrap.windowStart() > intervalInMs;
          }

          這個 values 方法很簡單,就是過濾掉那些過期數(shù)據(jù)就可以了。

          到這里,我們就說完了 維度數(shù)據(jù)統(tǒng)計的問題。至于秒維度的數(shù)據(jù)統(tǒng)計,有些不一樣,稍微復(fù)雜一些,我在后面單獨起了一節(jié)。跳過這部分內(nèi)容對閱讀 Sentinel 源碼沒有影響。

          Sentinel 源碼分析

          下面,我們正式開始 Sentinel 的源碼介紹。

          官方文檔中,它的最簡單的使用是下面這樣的,這里用了 try-with-resource 的寫法:

          try (Entry entry = SphU.entry("HelloWorld")) {
              // Your business logic here.
              System.out.println("hello world");
          } catch (BlockException e) {
              // Handle rejected request.
              e.printStackTrace();
          }

          這個例子對于理解源碼其實不是很好,我們來寫一個復(fù)雜一些的例子,這樣對理解源碼有很大的幫助:

          3

          1、紅色部分,Context 代表一個調(diào)用鏈的入口,Context 實例設(shè)置在 ThreadLocal 中,所以它是跟著線程走的,如果要切換線程,需要手動切換。ContextUtil#enter 有兩個參數(shù):

          第一個參數(shù)是 context name,它代表調(diào)用鏈的入口,作用是為了區(qū)分不同的調(diào)用鏈路,個人感覺沒什么用,默認是 Constants.CONTEXT_DEFAULT_NAME 的常量值 "sentinel_default_context";

          第二個參數(shù)代表調(diào)用方標識 origin,目前它有兩個作用,一是用于黑白名單的授權(quán)控制,二是可以用來統(tǒng)計諸如從應(yīng)用 application-a 發(fā)起的對當(dāng)前應(yīng)用 interfaceXxx() 接口的調(diào)用,目前這個數(shù)據(jù)會被統(tǒng)計,但是 dashboard 中并不展示。

          2、進入 BlockException 異常分支,代表該次請求被流量控制規(guī)則限制了,我們一般會讓代碼走入到熔斷降級的邏輯里面。當(dāng)然,BlockException 其實有好多個子類,如 DegradeException、FlowException 等,我們也可以 catch 具體的子類來進行處理。

          3、Entry 是我們的重點,對于 SphU#entry 方法:

          第一個參數(shù)標識資源,通常就是我們的接口標識,對于數(shù)據(jù)統(tǒng)計、規(guī)則控制等,我們一般都是在這個粒度上進行的,根據(jù)這個字符串來唯一標識,它會被包裝成 ResourceWrapper 實例,大家要先看下它的 hashCodeequals 方法;

          第二個參數(shù)標識資源的類型,我們左邊的代碼使用了 EntryType.IN 代表這個是入口流量,比如我們的接口對外提供服務(wù),那么我們通常就是控制入口流量;EntryType.OUT 代表出口流量,比如上面的 getOrderInfo 方法(沒寫默認就是 OUT),它的業(yè)務(wù)需要調(diào)用訂單服務(wù),像這種情況,壓力其實都在訂單服務(wù)中,那么我們就指定它為出口流量。這個流量類型有什么用呢?答案在 SystemSlot 類中,它用于實現(xiàn)自適應(yīng)限流,根據(jù)系統(tǒng)健康狀態(tài)來判斷是否要限流,如果是 OUT 類型,由于壓力在外部系統(tǒng)中,所以就不需要執(zhí)行這個規(guī)則。

          4、上面的代碼,我們在 getOrderInfo 中嵌套使用了 Entry,也是為了我們后面的源碼分析需要。如果我們在一個方法中寫的話,要注意內(nèi)層的 Entry 先 exit,才能做外層的 exit,否則會拋出異常。源碼角度來看,是在 Context 實例中,保存了當(dāng)前的 Entry 實例。

          5、實際開發(fā)過程中,我們當(dāng)然不會每個接口都像上面的代碼這么寫,Sentinel 提供了很多的擴展和適配器,這里只是為了源碼分析的需要。

          Sentinel 提供了很多的 adapter 用于諸如 dubbo、grpc、網(wǎng)關(guān)等環(huán)境,它們其實都是封裝了上述的代碼。你只要認真看完本文,那些包裝都很容易看懂。

          16

          這里我們介紹了 Sentinel 的接口使用,不過它的類名字我現(xiàn)在都沒懂是什么意思,SphU、CtSph、CtEntry 這些名字有什么特殊含義,有知道的讀者請不吝賜教。

          下面,我們按照上面的代碼,開始源碼分析。這里我不會像之前分析 Spring IOC 和 Netty 源碼一樣,一行一行代碼說,所以大家一定要打開源碼配合著看。

          ContextUtil#enter

          我們先看 Context#enter 方法,這行代碼我們是可以不寫的,通常情況下,我們都不會顯示設(shè)置 context。

          ContextUtil.enter("user-center""app-A"); 

          下面我們就會看到,如果我們不顯式調(diào)用這個方法,那么會進入到默認的 context 中。

          進入到 ContextUtil 類,大家可能會漏看它的 static 代碼塊,這里會添加一個默認的 EntranceNode 實例。

          然后上面的這個方法會走到 ContextUtil#trueEnter 中,這里會添加名為 "user-center" 的 EntranceNode 節(jié)點。根據(jù)源碼,我們可以得出下面這棵樹:

          4

          這里的源碼非常簡單,如果我們從來不顯式調(diào)用 ContextUtil#enter 方法的話,那 root 就只有一個 default 子節(jié)點 sentinel_default_context。

          context 很好理解,它代表線程執(zhí)行的上下文,在各種開源框架中都有類似的語義,在 Sentinel 中,我們可以看到,對于一個新的 context name,Sentinel 會往樹中添加一個 EntranceNode 實例。它的作用是為了區(qū)分調(diào)用鏈路,標識調(diào)用入口。在 sentinel-dashboard 中,我們可以很直觀地看出調(diào)用鏈路:

          19

          SphU#entry

          接下來,我們看 SphU#entry。自己跟進去,我們會來到 CtSph#entryWithPriority 方法,這個方法是 Sentinel 的骨架,非常重要。

          private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
              throws BlockException {
              // 從 ThreadLocal 中獲取 Context 實例
              Context context = ContextUtil.getContext();
              // 如果是 NullContext,那么說明 context name 超過了 2000 個,參見 ContextUtil#trueEnter
              // 這個時候,Sentinel 不再接受處理新的 context 配置,也就是不做這些新的接口的統(tǒng)計、限流熔斷等
              if (context instanceof NullContext) {
                  return new CtEntry(resourceWrapper, null, context);
              }

              // 我們前面說了,如果我們不顯式調(diào)用 ContextUtil#enter,這里會進入到默認的 context 中
              if (context == null) {
                  context = MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME, "", resourceWrapper.getType());
              }

              // Sentinel 的全局開關(guān),Sentinel 提供了接口讓用戶可以在 dashboard 開啟/關(guān)閉
              if (!Constants.ON) {
                  return new CtEntry(resourceWrapper, null, context);
              }

              // 設(shè)計模式中的責(zé)任鏈模式。
              // 下面這行代碼用于構(gòu)建一個責(zé)任鏈,入?yún)⑹?nbsp;resource,前面我們說過資源的唯一標識是 resource name
              ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);

              // 根據(jù) lookProcessChain 方法,我們知道,當(dāng) resource 超過 Constants.MAX_SLOT_CHAIN_SIZE,
              // 也就是 6000 的時候,Sentinel 開始不處理新的請求,這么做主要是為了 Sentinel 的性能考慮
              if (chain == null) {
                  return new CtEntry(resourceWrapper, null, context);
              }

              // 執(zhí)行這個責(zé)任鏈。如果拋出 BlockException,說明鏈上的某一環(huán)拒絕了該請求,
              // 把這個異常往上層業(yè)務(wù)層拋,業(yè)務(wù)層處理 BlockException 應(yīng)該進入到熔斷降級邏輯中
              Entry e = new CtEntry(resourceWrapper, chain, context);
              try {
                  chain.entry(context, resourceWrapper, null, count, prioritized, 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;
          }

          前面的都比較簡單,這里說一說 lookProcessChain(resourceWrapper) 這個方法。Sentinel 的處理核心都在這個責(zé)任鏈中,鏈中每一個節(jié)點是一個 Slot 實例,這個鏈通過 BlockException 異常來告知調(diào)用入口最終的執(zhí)行情況。

          大家自己點進去源碼,這個責(zé)任鏈由 SlotChainProvider#newSlotChain 生產(chǎn),Sentinel 提供了 SPI 端點,讓我們可以自己定制 Builder,如添加一個 Slot 進去。由于 SlotChainBuilder 接口設(shè)計的問題,我們只能全局所有的 resource 使用相同的責(zé)任鏈配置。

          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 AuthoritySlot());
                  chain.addLast(new SystemSlot());
                  chain.addLast(new FlowSlot());
                  chain.addLast(new DegradeSlot());
                  return chain;
              }
          }

          接下來,我們就按照默認的 DefaultSlotChainBuilder 生成的責(zé)任鏈往下看源碼。

          這里要強調(diào)一點,對于相同的 resource,使用同一個責(zé)任鏈實例,不同的 resource,使用不同的責(zé)任鏈實例。

          另外,對于 resource 實例,我們前面也說了,它根據(jù) resource name 來判斷,和線程沒有關(guān)系。

          NodeSelectorSlot

          chain-1

          首先,鏈中第一個處理節(jié)點是 NodeSelectorSlot。

          // key 是 context name, value 是 DefaultNode 實例
          private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(10);

          @Override
          public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
              throws Throwable {
              DefaultNode node = map.get(context.getName());
              if (node == null) {
                  synchronized (this) {
                      node = map.get(context.getName());
                      if (node == null) {
                          node = new DefaultNode(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);
              fireEntry(context, resourceWrapper, node, count, prioritized, args);
          }

          我們前面說了,責(zé)任鏈實例和 resource name 相關(guān),和線程無關(guān),所以當(dāng)處理同一個 resource 的時候,會進入到同一個 NodeSelectorSlot 實例中。

          所以這塊代碼主要就是要處理:不同的 context name,同一個 resource name 的情況。

          如下面兩段代碼,它們都是處理同一個 resource("getUserInfo" 這個 resource),但是它們的入口 context 不一致。

          5

          然后我們再結(jié)合前面的那棵樹,我們可以得出下面這棵樹,看深色的部分:

          6

          NodeSelectorSlot 還是比較簡單的,只要讀者搞清楚 NodeSelectorSlot 實例是跟著 resource 一一對應(yīng)的就很清楚了。

          ClusterBuilderSlot

          chain-2

          接下來,我們來到了 ClusterBuilderSlot 這一環(huán),這一環(huán)的主要作用是構(gòu)建 ClusterNode。

          這里不貼源碼,根據(jù)上面的樹,然后在經(jīng)過該類的處理以后,我們可以得出下面這棵樹:

          7

          看上圖中深色部分,對于每一個 resource,這里會對應(yīng)一個 ClusterNode 實例,如果不存在,就創(chuàng)建一個實例。

          這個 ClusterNode 非常有用,因為我們就是使用它來做數(shù)據(jù)統(tǒng)計的。比如 getUserInfo 這個接口,由于從不同的 context name 中開啟調(diào)用鏈,它有多個 DefaultNode 實例,但是只有一個 ClusterNode,通過這個實例,我們可以知道這個接口現(xiàn)在的 QPS 是多少。

          另外,這個類還處理了 origin 不是默認值的情況:

          再說一次,origin 代表調(diào)用方標識,如 application-a, application-b 等。

          if (!"".equals(context.getOrigin())) {
              Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin());
              context.getCurEntry().setOriginNode(originNode);
          }

          我們可以看到,當(dāng)設(shè)置了 origin 的時候,會額外生成一個 StatisticsNode 實例,掛在 ClusterNode 上。

          我們把前面的代碼改改,看紅色部分:

          8

          我們的 getUserInfo 接收到了來自 application-aapplication-b 兩個應(yīng)用的請求,那么樹會變成下面這樣:

          9

          它的作用是用來統(tǒng)計從 application-a 過來的訪問 getUserInfo 這個接口的信息。目前這個信息在 dashboard 中是不展示的,畢竟也沒什么用。

          LogSlot

          chain-3

          這個類比較簡單,我們看到它直接 fire 出去了,也就是說,先處理責(zé)任鏈上后面的那些節(jié)點,如果它們拋出了 BlockException,那么這里才做處理。

          10

          這里調(diào)用了 EagleEyeLogUtil#log 方法,它其實就是,將被設(shè)置的規(guī)則 block 的信息記錄到日志文件 sentinel-block.log 中。也就是記錄哪些接口被規(guī)則擋住了。

          StatisticSlot

          chain-4

          這個 slot 非常重要,它負責(zé)進行數(shù)據(jù)統(tǒng)計。

          它也是先 fire 出去,等后面的節(jié)點處理完畢以后,它再進行統(tǒng)計數(shù)據(jù)。之所以這么設(shè)計,是因為后面的節(jié)點是做控制的,執(zhí)行的時候可能是正常通過的,也可能是拋出 BlockException 異常的。

          源碼非常簡單,對于 QPS 統(tǒng)計,使用前面介紹的滑動窗口,而對于線程并發(fā)的統(tǒng)計,它使用了 LongAdder。

          大家一定要看一遍這個類的源碼,這里沒有什么特別的內(nèi)容需要強調(diào),所以我就不展開說了。

          接下來,我們后面要介紹的幾個 Slot,需要通過 dashboard 進行開啟,因為需要配置規(guī)則。

          當(dāng)然,你也可以硬編碼規(guī)則到代碼中。但是要調(diào)整數(shù)值就比較麻煩,每次都要改代碼。

          AuthoritySlot

          chain-5

          這個類非常簡單,做權(quán)限控制,根據(jù) origin 做黑白名單的控制:

          11

          在 dashboard 中,是這么配置的:

          17

          這里的調(diào)用方就是我們前面介紹的 origin。

          SystemSlot

          chain-6

          這個是 Sentinel 中比較重要的一個東西了,用來實現(xiàn)自適應(yīng)限流。

          12

          規(guī)則校驗都在 SystemRuleManager#checkSystem 中:

          13

          我們先說說上面的代碼中的 RT、線程數(shù)、入口 QPS 這三項系統(tǒng)保護規(guī)則。dashboard 配置界面:

          21

          在前面介紹的 StatisticSlot 類中,有下面一段代碼:

          14

          Sentinel 針對所有的入口流量,使用了一個全局的 ENTRY_NODE 進行統(tǒng)計,所以我們也要知道,系統(tǒng)保護規(guī)則是全局的,和具體的某個資源沒有關(guān)系。

          由于系統(tǒng)的平均 RT、當(dāng)前線程數(shù)、QPS 都可以從 ENTRY_NODE 中獲得,所以限制代碼非常簡單,比較一下大小就可以了。如果超過閾值,拋出 SystemBlockException。

          ENTRY_NODE 是 ClusterNode 類型的,而 ClusterNode 對于 rt、qps 都是統(tǒng)計的維度的數(shù)據(jù)。

          當(dāng)然,對于 SystemSlot 類來說,最重要的其實并不是上面的這些,因為在實際使用過程中,對于 RT、線程數(shù)、QPS 每一項,我們其實都很難設(shè)置一個確定的閾值。

          我們往下看它的對于系統(tǒng)負載和 CPU 資源的保護:

          15

          我們可以看到,Sentinel 通過調(diào)用 MBean 中的方法獲取當(dāng)前的系統(tǒng)負載和 CPU 使用率,Sentinel 起了一個后臺線程,每秒查詢一次。

          OperatingSystemMXBean osBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class);

          currentLoad = osBean.getSystemLoadAverage();

          currentCpuUsage = osBean.getSystemCpuLoad();

          下圖展示 dashboard 中對于 CPU 使用率的規(guī)則配置:

          22

          FlowSlot

          chain-7

          Flow Control 是 Sentinel 的核心, 因為 Sentinel 本身定位就是一個流控工具,所以 FlowSlot 非常重要。

          對于讀者來說,最大的挑戰(zhàn)應(yīng)該也是這部分代碼,因為前面的代碼,只要讀者理得清楚里面各個類的關(guān)系,就不難。而這部分代碼由于涉及到限流算法,會稍微復(fù)雜一點點。

          我之前寫過一篇 RateLimiter 源碼分析(Guava 和 Sentinel 實現(xiàn)) 文章,里面介紹了 Sentinel 使用的流控算法,所以我這里就不準備再花篇幅介紹這部分內(nèi)容了。感興趣的讀者請閱讀那篇文章即可。

          DegradeSlot

          chain-8

          恭喜大家,終于到最后一個 slot 了。

          它有三個策略,我們首先說說根據(jù) RT 降級:

          23

          如果按照上面截圖所示的配置:對于 getUserInfo 這個資源,正常情況下,它只需要 50ms 就夠了,如果它的 RT 超過了 100ms,那么它會進入半降級狀態(tài),接下來的 5 次訪問,如果都超過了 100ms,那么在接下來的 10 秒內(nèi),所有的請求都會被拒絕。

          其實這個描述不是百分百準確,打開 DegradeRule#passCheck 源碼,我們用代碼來描述:

          25

          Sentinel 使用了 cut 作為開關(guān),開啟這個開關(guān)以后,會啟動一個定時任務(wù),過了 10秒 以后關(guān)閉這個開關(guān)。

          if (cut.compareAndSet(falsetrue)) {
              ResetTask resetTask = new ResetTask(this);
              pool.schedule(resetTask, timeWindow, TimeUnit.SECONDS);
          }

          對于異常比例和異常數(shù)的控制,非常簡單,大家看一下源碼就懂了。同理,達到閾值,開啟斷路器,之后由定時任務(wù)關(guān)閉,這里就不浪費篇幅了。

          應(yīng)用和 sentinel-dashboard 的交互

          這里花點篇幅介紹一下客戶端是怎么和 dashboard 進行交互的。

          在 Sentinel 的源碼中,打開 sentinel-transport 工程,可以看到三個子工程,common 是基礎(chǔ)包和接口定義。

          27

          如果客戶端要接入 dashboard,可以使用 netty-http 或 simple-http 中的一個。為什么不直接使用 Netty,而要同時提供 http 的選項呢?那是因為你不一定使用 Java 來實現(xiàn) dashboard,如果我們使用其他語言來實現(xiàn) dashboard 的話,使用 http 協(xié)議比較容易適配。

          下面我們只介紹 http 的使用,首先,添加 simple-http 依賴:

          <dependency>
             <groupId>com.alibaba.csp</groupId>
             <artifactId>sentinel-transport-simple-http</artifactId>
             <version>1.6.3</version>
          </dependency>

          然后在應(yīng)用啟動參數(shù)中添加 dashboard 服務(wù)器地址,同時可以指定當(dāng)前應(yīng)用的名稱:

          -Dcsp.sentinel.dashboard.server=127.0.0.1:8080 -Dproject.name=sentinel-learning

          這個時候我們打開 dashboard 是看不到這個應(yīng)用的,因為沒有注冊。

          當(dāng)我們在第一次使用 Sentinel 以后,Sentinel 會自動注冊。

          下面帶大家看看過程是怎樣的。首先,我們在使用 Sentinel 的時候會調(diào)用 SphU#entry

          public static Entry entry(String name) throws BlockException {
              return Env.sph.entry(name, EntryType.OUT, 1, OBJECTS0);
          }

          這里使用了 Env 類,其實就是這個類做的事情:

          public class Env {
              public static final Sph sph = new CtSph();
              static {
                  // If init fails, the process will exit.
                  InitExecutor.doInit();
              }
          }

          進到 InitExecutor.doInit 方法:

          public static void doInit() {
              if (!initialized.compareAndSet(falsetrue)) {
                  return;
              }
              try {
                  ServiceLoader<InitFunc> loader = ServiceLoader.load(InitFunc.class);
                  List<OrderWrapper> initList = new ArrayList<OrderWrapper>();
                  for (InitFunc initFunc : loader) {
                      insertSorted(initList, initFunc);
                  }
                  for (OrderWrapper w : initList) {
                      w.func.init();
                  }
                  // ...
          }

          這里使用 SPI 加載 InitFunc 的實現(xiàn),大家可以在這里斷個點,可以發(fā)現(xiàn)這里加載了 CommandCenterInitFunc 類和 HeartbeatSenderInitFunc 類。

          前者是客戶端啟動的接口服務(wù),提供給 dashboard 查詢數(shù)據(jù)和規(guī)則設(shè)置使用的。后者用于客戶端主動發(fā)送心跳信息給 dashboard。

          我們看 HeartbeatSenderInitFunc#init 方法:

          @Override
          public void init() {
              HeartbeatSender sender = HeartbeatSenderProvider.getHeartbeatSender();
              if (sender == null) {
                  RecordLog.warn("[HeartbeatSenderInitFunc] WARN: No HeartbeatSender loaded");
                  return;
              }

              initSchedulerIfNeeded();
              long interval = retrieveInterval(sender);
              setIntervalIfNotExists(interval);
              // 啟動一個定時器,發(fā)送心跳信息
              scheduleHeartbeatTask(sender, interval);
          }

          這里看到,init 方法的第一行就是去加載 HeartbeatSender 的實現(xiàn)類,這里又用到了 SPI 的機制,如果我們添加了 sentinel-transport-simple-http 這個依賴,那么 SimpleHttpHeartbeatSender 就會被加載。

          之后在上面的最后一行代碼,啟動了一個定時器,以一定的間隔(默認10秒)不斷地發(fā)送心跳信息到 dashboard 應(yīng)用,這個心跳信息中就包含應(yīng)用的名稱、ip、port、Sentinel 版本 等信息。

          而對于 dashboard 來說,有了這些信息,就可以對應(yīng)用進行規(guī)則設(shè)置、到應(yīng)用拉取數(shù)據(jù)用于頁面展示等。

          Sentinel 在客戶端并沒有使用第三方 http 包,而是自己基于 JDK 的 Socket 和 ServerSocket 接口實現(xiàn)了簡單的客戶端和服務(wù)端,主要也是為了不增加依賴。

          Sentinel 中秒級 QPS 的統(tǒng)計問題

          以下內(nèi)容建立在你對于滑動窗口有了較為深入的了解的基礎(chǔ)上,如果你覺得有點吃力,說明你對于 Sentinel 還不是完全熟悉,可以選擇性放棄這一節(jié)的內(nèi)容。

          我們前面介紹了滑動窗口用在 維度的數(shù)據(jù)統(tǒng)計上,當(dāng)我們在說 QPS 的時候,當(dāng)然我們一般指的是秒維度的數(shù)據(jù)。當(dāng)然,你在很多地方看到的 QPS 數(shù)據(jù),其實都是通過分維度的數(shù)據(jù)來得到的,包括 metrics 日志文件、dashboard 中的 QPS。

          下面,我們深入分析秒維度數(shù)據(jù)統(tǒng)計的一些問題。

          在開始的時候,我們說了 Sentinel 統(tǒng)計了 兩個維度的數(shù)據(jù):

          1、對于 來說,一輪是 60 秒,分為 60 個時間窗口,每個時間窗口是 1 秒;

          2、對于 來說,一輪是 1 秒,分為 2 個時間窗口,每個時間窗口是 0.5 秒;

          如果我們用上面介紹的統(tǒng)計分維度的 BucketLeapArray 來統(tǒng)計秒維度數(shù)據(jù)可以嗎?答案當(dāng)然是不行,因為會不準確。

          設(shè)想一個場景,我們的一個資源,訪問的 QPS 穩(wěn)定是 10,假設(shè)請求是均勻分布的,在相對時間 0.0 - 1.0 秒?yún)^(qū)間,通過了 10 個請求,我們在 1.1 秒的時候,觀察到的 QPS 可能只有 5,因為此時第一個時間窗口被重置了,只有第二個時間窗口有值。

          這個大家應(yīng)該很容易理解,如果你覺得不理解,可以不用浪費時間在這節(jié)了

          所以,我們可以知道,如果用 BucketLeapArray 來實現(xiàn),會有 0~50% 的數(shù)據(jù)誤差,這肯定是不能接受的。

          那能不能增加窗口的數(shù)量來降低誤差到一個合理的范圍內(nèi)呢?這個大家可以思考一下,考慮一下它對于性能是否有較大的損失。

          大家翻開 StatisticNode 的源碼,對于秒維度數(shù)據(jù)統(tǒng)計,Sentinel 使用下面的構(gòu)造方法:

          // 2 個時間窗口,每個窗口長度 0.5 秒
          public ArrayMetric(int sampleCount, int intervalInMs) {
              this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
          }

          OccupiableBucketLeapArray 實現(xiàn)類的源碼并不長,我們大概看一眼,可以發(fā)現(xiàn)它的 newEmptyBucketresetWindowTo 這兩個方法和 BucketLeapArray 有點不一樣,也就是在重置的時候,它不是直接重置成 0 的。

          所以,我們要大膽猜測一下,這個類里面的 borrowArray 做了一些事情,它是 FutureBucketLeapArray 的實例,這個類和前面接觸的 BucketLeapArray 差不多,但是加了一個 Future 單詞。這里我們先仔細看看它。

          它和 BucketLeapArray 唯一的不同是,它覆寫了下面這個方法:

          @Override
          public boolean isWindowDeprecated(long time, WindowWrap<MetricBucket> windowWrap) {
              // Tricky: will only calculate for future.
              return time >= windowWrap.windowStart();
          }

          我們發(fā)現(xiàn),如果按照它的這種定義,在調(diào)用 values() 方法的時候,所有的 2 個窗口都是過期的,將得不到任何的值。所以,我們大概可以判斷,給這個數(shù)組添加值的時候,使用的時間應(yīng)該不是當(dāng)前時間,而是一個未來的時間點。這大概就是 Future 要表達的意思。

          我們再回到 OccupiableBucketLeapArray 這個類,可以看到在重置的時候,它使用了 borrowArray 的值:

          @Override
          protected WindowWrap<MetricBucket> resetWindowTo(WindowWrap<MetricBucket> w, long time) {
              // Update the start time and reset value.
              w.resetTo(time);
              MetricBucket borrowBucket = borrowArray.getWindowValue(time);
              if (borrowBucket != null) {
                  w.value().reset();
                  w.value().addPass((int)borrowBucket.pass());
              } else {
                  w.value().reset();
              }
              return w;
          }

          所以我們大概可以猜一猜它是怎么利用這個 FutureBucketLeapArray 實例的:borrowArray 存儲了未來的時間窗口的值。當(dāng)主線到達某個時間窗口的時候,如果發(fā)現(xiàn)當(dāng)前時間窗口是過期的,前面介紹過,會需要重置這個窗口,這個時候,它會檢查一下 borrowArray 是否有值,如果有,將其作為這個窗口的初始值填充進來,而不是簡單重置為 0 值。

          有了這個思路,我們再看 borrowArray 中的值是怎么進來的。

          我們很容易可以找到,只可能通過這里的 addWaiting 方法設(shè)置:

          @Override
          public void addWaiting(long time, int acquireCount) {
              WindowWrap<MetricBucket> window = borrowArray.currentWindow(time);
              window.value().add(MetricEvent.PASS, acquireCount);
          }

          接下來,我們找這個方法被哪里調(diào)用了,找到最后,我們發(fā)現(xiàn)只有 DefaultController 這個類中有調(diào)用。

          這個類是流控中的 “快速失敗” 規(guī)則控制器,我們簡單看一下代碼:

          @Override
          public boolean canPass(Node node, int acquireCount, boolean prioritized) {
              int curCount = avgUsedTokens(node);
              if (curCount + acquireCount > count) {
                  // 只有設(shè)置了 prioritized 的情況才會進入到下面的 if 分支
                  // 也就是說,對于一般的場景,被限流了,就快速失敗
                  if (prioritized && grade == RuleConstant.FLOW_GRADE_QPS) {
                      long currentTime;
                      long waitInMs;
                      currentTime = TimeUtil.currentTimeMillis();
                      // 下面的這行 tryOccupyNext 非常復(fù)雜,大意就是說去占有"未來的"令牌
                      // 可以看到,下面做了 sleep,為了保證 QPS 不會因為預(yù)占而撐大
                      waitInMs = node.tryOccupyNext(currentTime, acquireCount, count);
                      if (waitInMs < OccupyTimeoutProperty.getOccupyTimeout()) {
                          // 就是這里設(shè)置了 borrowArray 的值
                          node.addWaitingRequest(currentTime + waitInMs, acquireCount);
                          node.addOccupiedPass(acquireCount);
                          sleep(waitInMs);

                          // PriorityWaitException indicates that the request will pass after waiting for {@link @waitInMs}.
                          throw new PriorityWaitException(waitInMs);
                      }
                  }
                  return false;
              }
              return true;
          }

          看到這里,我其實還有很多疑問沒有被解開 ??????

          首先,這里解開了一個問題,就是這個類為什么叫 OccupiableBucketLeapArray?

          • Occupiable 這里代表可以被預(yù)占的意思,結(jié)合上面 DefaultController 的源碼,可以知道它原來是用來滿足 prioritized 類型的資源的,我們可以認為這類請求有較高的優(yōu)先級。如果 QPS 達到閾值,這類資源通常不能用快速失敗返回, 而是讓它去預(yù)占未來的 QPS 容量。

          當(dāng)然,令人失望的是,這里根本沒有解開 QPS 是怎么準確計算的這個問題。

          下面,我思路倒回來,我來證明 Sentinel 的秒維度的 QPS 統(tǒng)計是不準確的:

          public static void main(String[] args) {
              // 下面幾行代碼設(shè)置了 QPS 閾值是 100
              FlowRule rule = new FlowRule("test");
              rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
              rule.setCount(100);
              rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);
              List<FlowRule> list = new ArrayList<>();
              list.add(rule);
              FlowRuleManager.loadRules(list);

              // 先通過一個請求,讓 clusterNode 先建立起來
              try (Entry entry = SphU.entry("test")) {
              } catch (BlockException e) {
              }

              // 起一個線程一直打印 qps 數(shù)據(jù)
              new Thread(new Runnable() {
                  @Override
                  public void run() {
                      while (true) {
                          System.out.println(ClusterBuilderSlot.getClusterNode("test").passQps());
                      }
                  }
              }).start();

              while (true) {
                  try (Entry entry = SphU.entry("test")) {
                      Thread.sleep(5);
                  } catch (BlockException e) {
                      // ignore
                  } catch (InterruptedException e) {
                      // ignore
                  }
              }
          }

          大家跑一下代碼,然后觀察下輸出,QPS 數(shù)據(jù)在 50~100 這個區(qū)間一直變化,印證了我前面說的,秒級 QPS 統(tǒng)計是極度不準確的。

          根據(jù)前面的分析,其實也沒有什么結(jié)論要說了。剩下的交給大家自己去思考,去探索,這個過程一定比看我的文章更有意思。

          小結(jié)

          本文比較簡單,大家應(yīng)該很快就可以看完了,歡迎大家反饋閱讀感受,有什么需要改進的歡迎提出。

          原文鏈接:https://www.javadoop.com/post/sentinel



          瀏覽 58
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  多人操穴视频在线播放 | 视频一区二区三区四区五 | 在线观看免费视频一区 | 成人网站日皮视频 | 欧美精品一级 |