Soul網關學習Sentinel插件原理解析

概述

在業務網關中熔斷和流量控制都是很是必要的功能。soul在實現這部分功能時使用了不一樣的成熟組件,用戶能夠根據本身的喜愛選擇。本文將介紹如何在soul中使用阿里的Sentinel組件實現熔斷及流控功能。本文首先會介紹熔斷和流控的場景及意義。而後介紹如何在soul上配置使用sentinel插件作流控和熔斷。最後從源碼的層面簡略分析soul是如何使用Sentinel組件的。nginx

熔斷和流量控制

場景描述

業務網關做爲流量的入口,有保護後繼服務的職責。如下兩個對服務有嚴重危害的場景在生產中常常會遇到,也是業務網關必需要關注處理的問題。一種狀況是在好比雙11或雙12這些大型促銷時,接口的請求量是平時是數倍,若是沒有評估好容量,這種激增的請求很容易致使整個服務徹底不可用。這種宕機每每不是由於業務邏輯的漏洞而是由於請求過多資源不夠致使的。另外一種狀況是在整個服務體系中有一些核心服務,多個業務流程都依賴該服務。然而是服務都有出現處理不穩定或者服務損壞的狀況,致使請求處理時間長或者總是頻繁拋出異常。排除業務BUG的狀況,可能就是突發的很是隨機的阻塞,通常減緩請求量就會自動修復,可是若是不加保護就有出現多米諾效應致使整個服務不可用。此場景和第一種場景有略微不一樣,第一種場景是實際流量確實出現了不可處理的峯值,而第二種場景主要考慮的是服務自己出現了不可避免、不可預測的抖動而引起的連鎖反應。後端

流量控制

針對第一種場景咱們一般的作法是進行流量控制,核心思路是業務網關保證打到後面的請求是業務能夠承受的量,多餘的請求直接拒絕或者加入等待隊列,保證服務不會宕掉,大部分請求仍是能夠正常處理。在考慮流量控制的策略時,咱們應該主要思考如下幾個問題:markdown

  1. 經過什麼角度控制流量?
  2. 閾值是多少?
  3. 流量控制的策略是什麼?

對於第一個問題,正常思路是經過QPS來監控流量,即每秒鐘請求的數量超過某限額時進行流控。但其實還有一種思路是從併發數來監控流量。這種控制場景也是很是有意義的,例如當下遊應用因爲某種緣由致使服務不穩定、響應延遲增長,對於網關來講,意味着吞吐量降低和更多的線程數佔用,極端狀況下甚至致使線程池耗盡。從某種意義上講經過併發進行流控能夠必定程度上保護網關服務自己。對於第二個問題閾值來講比較好理解,就是觸發流控的邊界,若是從QPS來考慮就是每秒達到多少時開始流控,從併發數來考量的話就是請求上下文的線程數目超過多少進行流控。對於第三個問題,咱們通常有如下3中處理方案:架構

  1. 直接拒絕,這種策略很是好理解就是當QPS高於閾值時直接拒接服務,不把請求傳輸到後面的服務中。
  2. 預熱啓動,這個策略所針對的場景是系統長期處於低水位的狀況下,可能出現流量忽然增長時,而直接把系統拉昇到高水位可能瞬間把系統壓垮。預熱啓動的方式是讓閾值緩慢增長,在必定時間內逐漸增長閾值直至達到設置,給冷系統一個預熱的時間,避免冷系統被壓垮。對於超出閾值的請求也是觸發拒絕。
  3. 勻速排隊,此策略核心思路是以固定間隔時間讓請求經過。當請求到來的時候,若是當前請求距離上個經過的請求經過的時間間隔不小於預設值,則讓當前請求經過;不然,計算當前請求的預期經過時間,若是該請求的預期經過時間小於規則預設的 timeout 時間,則該請求會等待直到預設時間到來經過(排隊等待處理);若預期的經過時間超出最大排隊時長,則直接拒接這個請求。

熔斷

針對第二種場景一般的處理方式是設置服務熔斷。簡單的說就是當咱們探測的一個服務出現了異常,則再也不訪問它以避免更多的請求對它形成更大的壓力。一段時間後若是探測到服務恢復了再將流量發送過去。咱們首先須要判斷出這個服務是否出現了不穩定\抖動的狀況。而後思考若是發現了抖動的服務咱們應該怎麼辦。如何判斷服務是否恢復正常了。對於服務是否不穩定這一點咱們通常能夠經過一下3個方式進行判斷。併發

  1. 慢調用比例:當單位統計時長內請求數目大於設置的最小請求數目,而且超過最大忍受時間的請求大於閾值,則判斷服務異常,觸發熔斷;
  2. 異常比例:當單位統計時長內異常請求的比例大於閾值則咱們斷定服務異常,觸發熔斷;
  3. 異常數:當單位時長內出現異常的請求的數量的達到閾值則斷定服務異常,觸發熔斷;

當咱們經過以上3個指標判斷服務爲異常並熔斷服務後,對於必定時間內(熔斷時長內)的請求咱們能夠選擇直接報錯,不阻塞上游服務,讓請求方來自行決定如何處理。或者直接觸發服務降級。服務降級粗略的能夠理解爲請求此業務的簡版,該簡版省掉了不少非核心流程,而且只是最終保證流程處理完(最終一致性)。和現實中的熔斷同樣服務熔斷是會自動恢復的。通常是觸發熔斷後的一段時間內服務處於熔斷狀態不提供服務,而後進入半開狀態,若接下來的少許請求沒有報錯且響應時間合理則服務恢復,若是仍是異常則繼續熔斷。分佈式

soul中的Sentinel插件

Sentinel是阿里開源的面向分佈式服務架構的流量控制組件,主要以流量爲切入點,從流量控制、熔斷降級、系統自適應保護等多個維度來幫助您保障微服務的穩定性。Soul做爲國內優秀的開源網關,將Sentinel整合爲插件融入了本身的體系中,使用戶經過簡單的配置就可使用Sentinel提供的流量控制和服務熔斷功能。下面將簡要介紹在soul中如何配置使用sentinel插件。ide

首先登錄soul管理平臺在」插件列表」 –> 「sentinel」中配置插件。其中」選擇器」的配置不是本文的重點再也不介紹,點擊」增長規則」來進行具體設置以下圖。微服務

在這個配置頁面中」名稱」、」匹配方式」、」條件」、」日誌打印」、」是否開啓」、」執行順序」屬於soul插件的常規配置這裏也再也不贅述。咱們重點須要關注的是」處理」中的配置項。這些配置項主要能夠分爲2組,前4個選項是關於熔斷的配置,後4個選項是關於流量控制的配置。在soul中咱們能夠針對某一組請求同時設置它的流量控制和熔斷策略。下面來重點分析下各個配置項如何使用。this

熔斷

首先來看熔斷相關的配置,它有四個配置項」熔斷閾值」、」是否開啓熔斷」、」熔斷窗口大小」以及沒有註名字的是服務異常判斷方式。熔斷開關表示是否開啓熔斷(1開\0不開)。熔斷窗口大小指的是觸發熔斷後通過多少秒後進入半開狀態,在半開狀態若是請求正常則會進入正常狀態若是請求依然不正常則繼續熔斷。熔斷斷定方式和熔斷閾值須要結合來看。soul中使用了sentinel的3種服務異常斷定方式。分別是:spa

  1. 慢調用比例,在此模式下閾值指的是斷定爲慢調用的毫秒數。慢調用的比例默認是1不能更改即單位統計時長內所有超過閾值則觸發熔斷。該模式是sentinel的默認模式。
  2. 異常比例,在此模式下閾值指的是單位統計時長內異常請求的比例上限,須要填寫1個[0.0, 1.0]的數,表示0%-100%
  3. 異常數策略,在該模式下閾值指的是單位統計時間內異常請求個數的上限。

須要注意的是soul對於單位統計時長(statIntervalMs)和熔斷最小請求數(minRequestAmount)使用的是sentinel的默認參數。分別是1秒鐘和5次。單位時長指定的是異常判斷以是1秒鐘爲統計範圍,下一秒從新開始計數。最小請求數指的是1秒鐘內若是請求的次數少於5那麼即便達到閾值也不會觸發熔斷。

如上圖配置表示的意思是,開啓熔斷配置,若是此服務在1秒鐘內有5個請求都出現了異常那麼則熔斷10秒,10秒後進入如半開狀態,若是請求都正常則變爲正常狀態,若是還不正常則繼續熔斷。熔斷期間若是請求該服務則soul網關會直接返回請求錯誤,保護後端服務不會再接到請求。

流量控制

流量控制的相關配置有5個,從上到下從左到右分別是」流控效果」,」限流閾值」,」流控開關」,」限流閾值類型」。首先是限流類型,咱們能夠選擇」QPS」或」併發線程數」,這個參數規定了咱們從哪一個角度來設置限流的閾值。閾值則是QPS的上限或者是線程數量,達到此閾值則會啓動限流策略。具體的限流策略在」流控效果」中配置,流控策略裏咱們能夠選擇」直接拒絕」、」warm up(預熱)」、」勻速排隊」、」預熱+勻速排隊」。直接拒絕比較好理解,就是QPS或線程數達到閾值後,多餘的請求直接報錯返回。預熱指的是在10秒鐘內閾值逐步增加到指定閾值,即頭2-3秒的閾值是低於設置閾值的,但閾值是逐步增加的,10秒後達到指定閾值,這樣可使系統有個預熱過程。超過閾值的請求soul網關會直接報錯返回。勻速隊列這種模式會嚴格控制每一個請求的時間間隔,若是流控類型是QPS閾值是10,那麼soul會控制每100ms將1個請求傳導到後端服務上。多餘的請求首先會進入等待隊列,每一個請求最多等待500ms,若是請求預計等待時間超過500ms則直接報錯返回。須要注意的是若是限流類型是併發線程數,那麼流控效果只能是」直接拒絕」。以下圖所示該配置表示的是soul網關會保證該服務的QPS不超過10,多餘的請求將會直接報錯。

須要注意的是Sentinel組件獨立運行於soul的每一個網關中,若是網關是集羣,那麼在作流控時,實際傳到後面服務中的量是須要乘上soul網關服務的數量的。即若是咱們的soul網關部署了3個節點,經過nginx將全部請求平均負載到了每一個節點上。對應1個接口咱們配置的流控是10 qps,那麼實際後向服務須要處理的QPS是10*3。熔斷一樣須要考慮這種狀況,只有3個節點上某個服務都觸發熔斷時,那麼該服務纔不會再收到任何請求。

Sentinel插件源碼閱讀

soul中Sentinel插件的源碼主要有3塊,」SentinelRuleHandle」負責處理當有Sentinel規則從管理節點同步過來時的處理邏輯,」SentinelPlugin」插件的處理邏輯,」SentinelFallbackHandler」對於觸發了流控或熔斷的處理邏輯。下面我一個個來看一下。首先是」SentinelRuleHandle」,源碼以下:

public class SentinelRuleHandle implements PluginDataHandler {
    
    @Override
    public void handlerRule(final RuleData ruleData) {
        // 處理新的sentinel配置
        SentinelHandle sentinelHandle = GsonUtils.getInstance().fromJson(ruleData.getHandle(), SentinelHandle.class);
        sentinelHandle.checkData(sentinelHandle);
        // 獲取全部現有流控配置,刪除與新配置同resourceName的配置
        List<FlowRule> flowRules = FlowRuleManager.getRules()
                .stream()
                .filter(r -> !r.getResource().equals(getResourceName(ruleData)))
                .collect(Collectors.toList());
        if (sentinelHandle.getFlowRuleEnable() == Constants.SENTINEL_ENABLE_FLOW_RULE) {
            // 若是開啓了流控
            // 根據配置設置sentinel流控規則
            FlowRule rule = new FlowRule(getResourceName(ruleData));
            // 配置閾值
            rule.setCount(sentinelHandle.getFlowRuleCount());
            // 流控方式 QPS or 線程
            rule.setGrade(sentinelHandle.getFlowRuleGrade());
            // 流控行爲: 0. default(reject directly), 1. warm up, 2. rate limiter, 3. warm up + rate limiter
            rule.setControlBehavior(sentinelHandle.getFlowRuleControlBehavior());
            flowRules.add(rule);
        }
        // 更新所有流控配置
        FlowRuleManager.loadRules(flowRules);
        // 獲取全部現有熔斷配置,刪除與新配置同resourceName的配置
        List<DegradeRule> degradeRules = DegradeRuleManager.getRules()
                .stream()
                .filter(r -> !r.getResource().equals(getResourceName(ruleData)))
                .collect(Collectors.toList());
        if (sentinelHandle.getDegradeRuleEnable() == Constants.SENTINEL_ENABLE_DEGRADE_RULE) {
            // 若是開啓了流控
            // 根據配置設置sentinel熔斷規則
            DegradeRule rule = new DegradeRule(getResourceName(ruleData));
            // 熔斷閾值
            rule.setCount(sentinelHandle.getDegradeRuleCount());
            // 熔斷判斷的依據 0: average RT, 1: exception ratio, 2: exception count
            rule.setGrade(sentinelHandle.getDegradeRuleGrade());
            // 熔斷時間窗口
            rule.setTimeWindow(sentinelHandle.getDegradeRuleTimeWindow());
            degradeRules.add(rule);
        }
        // 更新所有熔斷配置
        DegradeRuleManager.loadRules(degradeRules);
    }
    
    @Override
    public void removeRule(final RuleData ruleData) {
        // 刪除指定規則
        FlowRuleManager.loadRules(FlowRuleManager.getRules()
                .stream()
                .filter(r -> !r.getResource().equals(getResourceName(ruleData)))
                .collect(Collectors.toList()));
        DegradeRuleManager.loadRules(DegradeRuleManager.getRules()
                .stream()
                .filter(r -> !r.getResource().equals(getResourceName(ruleData)))
                .collect(Collectors.toList()));
    }
    
    @Override
    public String pluginNamed() {
        return PluginEnum.SENTINEL.getName();
    }
    
    /**
     * return sentinel resource name.
     *
     * @param ruleData ruleData
     * @return string string
     */
    public static String getResourceName(final RuleData ruleData) {
        return ruleData.getSelectorId() + "_" + ruleData.getName();
    }
    
}

複製代碼

插件執行邏輯代碼」SentinelPlugin」以下

public class SentinelPlugin extends AbstractSoulPlugin {
    // 異常處理的handler
    private final SentinelFallbackHandler sentinelFallbackHandler;
    
    public SentinelPlugin(final SentinelFallbackHandler sentinelFallbackHandler) {
        this.sentinelFallbackHandler = sentinelFallbackHandler;
    }
    
    @Override
    protected Mono<Void> doExecute(final ServerWebExchange exchange, final SoulPluginChain chain, final SelectorData selector, final RuleData rule) {
        final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);
        assert soulContext != null;
        // 從插件配置中生成sentinel使用的資源名稱,該名稱對應1個流控或熔斷策略
        String resourceName = SentinelRuleHandle.getResourceName(rule);
        // 驗證sentinel插件的配置信息
        SentinelHandle sentinelHandle = GsonUtils.getInstance().fromJson(rule.getHandle(), SentinelHandle.class);
        sentinelHandle.checkData(sentinelHandle);
        // 引入sentinel官方的Transformer,將請求交給sentinel處理
        return chain.execute(exchange).transform(new SentinelReactorTransformer<>(resourceName))
                .doOnSuccess(v -> {
                    HttpStatus status = exchange.getResponse().getStatusCode();
                    if (status == null || !status.is2xxSuccessful()) {
                        exchange.getResponse().setStatusCode(null);
                        throw new SentinelFallbackException(status == null ? HttpStatus.INTERNAL_SERVER_ERROR : status);
                    }
                })
                //sentinel 觸發了流控或熔斷而報錯調用sentinelFallbackHandler返回錯誤信息
                .onErrorResume(throwable -> sentinelFallbackHandler.fallback(exchange, UriUtils.createUri(sentinelHandle.getFallbackUri()), throwable));
    }
    // 插件名sentinel
    @Override
    public String named() {
        return PluginEnum.SENTINEL.getName();
    }
    // 順序 45 
    @Override
    public int getOrder() {
        return PluginEnum.SENTINEL.getCode();
    }
    
    public static class SentinelFallbackException extends HttpStatusCodeException {
        public SentinelFallbackException(final HttpStatus statusCode) {
            super(statusCode);
        }
    }
}

複製代碼

異常處理」SentinelFallbackHandler」,在soul中不論是熔斷後請求的處理仍是被流控的請求,都是有soul直接返回報錯

public class SentinelFallbackHandler implements FallbackHandler {
    
    @Override
    public Mono<Void> generateError(final ServerWebExchange exchange, final Throwable throwable) {
        Object error;
        
        if (throwable instanceof DegradeException) {
            // 觸發熔斷
            // http status 設爲500
            exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
            // request body 設置
            error = SoulResultWrap.error(SoulResultEnum.SERVICE_RESULT_ERROR.getCode(), SoulResultEnum.SERVICE_RESULT_ERROR.getMsg(), null);
        } else if (throwable instanceof FlowException) {
            // 流控報錯 該錯提示客戶端再次嘗試
            //  http status 設爲429
            exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
            // request body 設置
            error = SoulResultWrap.error(SoulResultEnum.TOO_MANY_REQUESTS.getCode(), SoulResultEnum.TOO_MANY_REQUESTS.getMsg(), null);
        } else if (throwable instanceof BlockException) {
            // FlowException的父類 該錯提示服務已阻塞
            // http status 設爲429
            exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
            // request body 設置
            error = SoulResultWrap.error(SoulResultEnum.SENTINEL_BLOCK_ERROR.getCode(), SoulResultEnum.SENTINEL_BLOCK_ERROR.getMsg(), null);
        } else {
            return Mono.error(throwable);
        }
        return WebFluxResultUtils.result(exchange, error);
    }
}

複製代碼

總結

soul網關封裝了優秀的流控組件——sentinel,爲用戶提供了好用的流量控制和熔斷功能。須要注意的是soul在使用sentinel時部分參數是默認配置,若是有修改的需求則須要自行調整源碼。其次soul網關能夠分佈式部署,可是使用sentinel時並無用分佈式流控,每一個soul網關節點對於同一個資源的流控是獨立但相同的。

相關文章
相關標籤/搜索