sentinel實現秒殺活動

一、背景

之前在博客中寫過秒殺活動開發過程細節,之後用數據庫鎖簡單實現了一次,今天再給大家介紹一個工具Sentinel。

二、sentinel是什麼

Sentinel中文含義是哨兵,阿里巴巴給出的定義:Sentinel以流量爲切入點,從流量控制、熔斷降級、系統負載保護等多個維度保護服務的穩定性。我們可以從定義中的簡單的認識到該工具就是針對類似秒殺活動一樣的高流量、高併發場景下的一個保證服務穩定的工具。具體的介紹請參考:https://github.com/alibaba/Sentinel/wiki。

三、sentinel怎麼用

使用sentinel時,我們明確以下概念:
核心庫:sentinel以jar包的形式加入到自己工程中,不依賴的其他庫
控制檯:可以管理資源、定義規則的可視化工具
資源:它就是我們工程中的一個方法、類或者業務服務。sentinel正是要控制資源的工具。很顯然在我們的秒殺活動中,獲取商品庫存的方法即爲資源。當有大流量過來時,通過sentinel的控制檯指定的規則,來控制這些流量,保證我們的服務穩定性。

1、引入核心庫

在自己的工程中引入sentinel-core和sentinel-transport包。

compile 'com.alibaba.csp:sentinel-core:1.6.3'

Transport 模塊來與 Sentinel 控制檯進行通信。

compile 'com.alibaba.csp:sentinel-transport-simple-http:1.6.3'

2、定義資源

官方給出5種方式:主流框架適配、拋異常方式、返回布爾值方式、註解、異步調用。我們這裏使用拋異常方式作爲示例,使用SphU提供的方法將我們的資源(獲取庫存代碼塊)包裹起來。可能有人好奇爲什麼沒在方法上定義?這裏是因爲方法還有其他處理邏輯,而這些並不受高併發影響,如果我們將範圍擴大至整個方法,那麼會導致等個請求變慢。我們只需要在覈心的邏輯上定義即可。

// sentinel-1.5後支持try-with-resource特性。CAMPAIGN_CARD_PROMOTION即爲我們的資源名
boolean isHaveStock = false;
try (Entry youEntry = SphU.entry("CAMPAIGN_CARD_PROMOTION")) {
    if (isNewUser && isJulyCampaignDate && isNANJINGUser && !isHadBuyCampaignCard) {
        try {
            // 庫存數
            RAtomicLong cardStock = redissonClient.getAtomicLong("JULY_CAMPAIGN_ONE_YUAN_CARD_STOCK");
            boolean isSuccess = true;
            if (!cardStock.isExists() || cardStock.get() <= 0) {
                isSuccess = false;
            } else if (cardStock.decrementAndGet() < 0) {
                isSuccess = false;
            }
            if (isSuccess) {
                // 建立用戶和庫存關係
                RBucket<Integer> userStock = redissonClient.getBucket("JULY_CAMPAIGN_USER_STOCK_RELATION:" + uid);
                userStock.set(1, 60, TimeUnit.DAYS);
                // 訂單搶購成功發短信通知
                threadPoolTaskExecutor.submit(() -> mqService.sendRpcMessageService(uid, MessageActionEnum.PROMOTION, cardServiceConfig.getJulyCampaignSms2(), Maps.newHashMap()));
            }
            isHaveStock = isSuccess;
        } catch (Exception e) {
            log.error("1元月卡庫存操作異常 {} ", uid, e);
        }
    }
} catch (BlockException e){
    // 當拋出阻塞異常,定義自己的異常處理策略
}

3、控制檯

控制檯是一個springboot工程。我們可以下載jar包或者github下載源碼打包。有了jar包,在本地啓動。啓動命令如下,本人以將下好的包放在Sentinel下。命令中包含Hosts、port、projectName。啓動後在瀏覽器使用localhost:8080訪問,默認用戶名和密碼都是 sentinel。
在這裏插入圖片描述
在這裏插入圖片描述
JVM 參數-Dcsp.sentinel.dashboard.server=consoleIp:port是我們在啓動服務時需要添加的,其中consoleIp代表控制檯IP,port是端口。我在idea中啓動活動服務campaign-service。過幾分鐘刷新控制檯可以看到我們的服務,如下圖。紅色爲我們的服務,藍色時控制檯本身。黃色框爲規則配置欄。接下來會介紹如何配置規則。
在這裏插入圖片描述
我們通過postman-runner請求1000次,通過控制檯-實時監控查看有什麼變化?可以看到當前請求不斷髮起時,在實時監控看到訪問曲線,在8~10左右/QPS浮動。由此也可以看出來服務在本人機器上所能提供的訪問極限了。
在這裏插入圖片描述
在這裏插入圖片描述
下圖是我們在啓動服務後通過postman調用http接口時出現的,代表資源訪問日誌。在這裏插入圖片描述
打開相應目錄下的日誌是如下內容,其中第3列是我們配置的資源名稱,其他列含義,自左向右:|–timestamp-|------date time----|–resource-|p |block|s |e|rt,其中 p 代表通過的請求, block 代表被阻止的請求, s 代表成功執行完成的請求個數, e 代表用戶自定義的異常, rt 代表平均響應時長。在這裏插入圖片描述

4、規則

因爲調本地請求,響應事件太短,導致控制檯響應時間都爲0,怕引起誤會。爲了方便測試,我在獲取庫存方法內加了線程休眠代碼,增加請求響應時長。讓每次請求的響應時間隨機增加0~50毫秒。

Random random = new Random();
Thread.sleep(random.nextInt(50));

可在實時監控看到,QPS降到6~7左右,響應時間也在我們的預期範圍(50ms以內)。接下來我們就以流量規則和降級規則,示例sentinel的規則配置。
在這裏插入圖片描述

4.1 流控規則

首先看簇點鏈路,如下圖。給出了我們的資源名稱-CAMPAIGN_CARD_PROMOTION、QPS、以及一分鐘內的請求通過數,最右側則給出了四個控制按鈕:流控、降級、熱點、授權。其實這四個按鈕就對應簇點鏈路下方的菜單欄,只不過放在這裏統一了,每個菜單欄可以看的更詳細。
在這裏插入圖片描述
首先,點擊流控或者流控規則菜單欄裏的增加流控規則按鈕,彈窗可以看到閾值類型有QPS或線程數,其含義就是我們使用的限制類型。我們選擇QPS,單機閾值指定3,點擊新增按鈕。含義就是限制QPS<=3,即使請求數過高,請求響應也是在規則指定的範圍內。
在這裏插入圖片描述
當我們增加好流量控制規則後,刷新實時監控,可看到左側的訪問曲線,綠色(代表通過的請求數)出現下降趨勢並穩定在2和4QPS中間,藍色(代表拒絕的數量)則由之前的0上升到2和6QPS之間。右側也可以看到通過QPS都變成了3,拒絕QPS則在3~5之間,二者相加的數值也符合流控前的QPS。
在這裏插入圖片描述
而刪除流控規則後,兩個曲線也開始迴歸,右側的QPS也說明了,不再受流量規則控制。
在這裏插入圖片描述

4.2 降級規則

在配置降級規則前,我們可以看到資源響應平均時間爲23ms。這將作爲降級配置的參考值。
在這裏插入圖片描述
點擊降級按鈕,出現如下彈窗。可看到降級策略包含:RT(平均相應事件)、異常比例、異常數。RT(response-time)的含義時影響時間,即一次請求響應時間超過該值,將會被熔斷,特別時當服務不穩定時,響應時間忽高忽低,有了這個規則後就可以將不穩定,響應時間過長的攔截。異常比例含義是當資源的每秒請求量 >= 5,並且每秒異常總數佔通過量的比值超過閾值(DegradeRule 中的 count)之後,資源進入降級狀態。異常數含義當資源近 1 分鐘的異常數目超過閾值之後會進行熔斷。注意由於統計時間窗口是分鐘級別的,若 timeWindow 小於 60s,則結束熔斷狀態後仍可能再進入熔斷狀態。我們以RT爲例。時間指定爲30ms,時間窗口指定爲20s。即熔斷響應時間超過30毫秒請求,在第一次觸發該規則後的20s內,都將會自動熔斷。
在這裏插入圖片描述
增加RT降級策略後刷新實時監控,可以看到兩條曲線呈現上下交替波動,右側的QPS下方有通過有拒絕。也就是將響應時間超過30ms,時間窗口在20s的請求熔斷。可是圖中明明有也請求時間超過30ms的,
在這裏插入圖片描述

5、迴歸秒殺活動

經過以上分析講解。再回歸到我們的秒殺活動中。庫存是用戶搶購的熱點資源。會有大量的請求在短時間內進來,服務即使在數據庫層面、緩存層面的的做很多優化,仍然可能無法抵擋。此時就需要sentinel。當我們在控制檯發現搶購商品的流量很大,可能會超過我們在上線前的壓測數值,那就必須採取措施,限制請求處理。也正是上面示例sentinel控制訪問請求,對請求進行限制、熔斷,擋住一部分流量。而且相比於Netflix提供的Hystrix,sentinel可動態配置,彈性更高。

四、sentinel原理簡單剖析

該章節簡單的分析下sentinel的工作原理。我們在資源保護處會寫如下代碼:

Entry entry = SphU.entry("resource name");

它的作用是創建entry,同時會創建slot(官方文檔解使爲插槽)。一個資源會創建多個插槽,每個插槽都有不同的功能,如統計、限流、熔斷等,如下圖。
在這裏插入圖片描述
當我們創建了資源名稱爲”CAMPAIGN_CARD_PROMOTION“的entry後,便會自動創建多個插槽,我們以流量控制爲例,探究下工作過程。進入SphU.entry方法內,檢查資源下所有規則,繼續計入entry方法

/**
* 檢查資源下的所有規則
*/
public static Entry entry(String name) throws BlockException {
   return Env.sph.entry(name, EntryType.OUT, 1, OBJECTS0);
}

進來後首先是使用StringResourceWrapper對資源做簡單的包裝,之後執行entry。

@Override
public Entry entry(String name, EntryType type, int count, Object... args) throws BlockException {
    StringResourceWrapper resource = new StringResourceWrapper(name, type);
    return entry(resource, count, args);
}

之後我們看到entryWithPriority。我們關注chain.entry()。此處的chain即爲封裝了多了slot的責任鏈,其中FlowSlot爲其中一個,即實現流量控制。

private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, 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 = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
        }
        // 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, 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;
}

將進入FlowSlot,找到entry()下的checkFlow()方法。進入方法實現後,checker.checkFlow()開始獲取控制檯配置的規則,並檢查是否可以通過

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                  boolean prioritized, Object... args) throws Throwable {
    checkFlow(resourceWrapper, context, node, count, prioritized);

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

void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized)
    throws BlockException {
    checker.checkFlow(ruleProvider, resource, context, node, count, prioritized);
}

ruleProvider.apple()通過資源名獲取規則。如果規則不爲空則使用canPassCheck檢查是否可以通過,否則拋出FlowException異常。而該異常正是BlockException的子類。

public void checkFlow(Function<String, Collection<FlowRule>> ruleProvider, ResourceWrapper resource,
                          Context context, DefaultNode node, int count, boolean prioritized) throws BlockException {
    if (ruleProvider == null || resource == null) {
        return;
    }
    Collection<FlowRule> rules = ruleProvider.apply(resource.getName());
    if (rules != null) {
        for (FlowRule rule : rules) {
            if (!canPassCheck(rule, context, node, count, prioritized)) {
                throw new FlowException(rule.getLimitApp(), rule);
            }
        }
    }
}

檢查是否可以通過的代碼。

@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
    int curCount = avgUsedTokens(node);
    if (curCount + acquireCount > count) {
        if (prioritized && grade == RuleConstant.FLOW_GRADE_QPS) {
            long currentTime;
            long waitInMs;
            currentTime = TimeUtil.currentTimeMillis();
            waitInMs = node.tryOccupyNext(currentTime, acquireCount, count);
            if (waitInMs < OccupyTimeoutProperty.getOccupyTimeout()) {
                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;
}

同理,其他槽位指定的功能,也是通過該思路去工作,大家可以閱讀源碼。

五、總結

經過以上介紹,除了在秒殺活動保護庫存資源,那麼是不是也可大到保護一個類、一個服務,甚至一個服務集羣。比如我們在某服務提供的rpc接口處使用sentinel。再比如我們在整個redis集羣調用接口處使用sentinel。而且該工具控制檯上顯示的信息非常直觀,我們可以知道服務QPS、響應時間,統計請求、拒絕數等。總之,本片文章只是個人對Sentinel的初步使用,還有很多地方值得探究。

Sentinel :https://github.com/alibaba/Sentinel
在這裏插入圖片描述

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