2流高手速成記(之八):基於Sentinel實現微服務體系下的限流與熔斷

2022-11-07 18:02:08

我們接上回

上一篇中,我們進行了簡要的微服務實現,也體會到了SpringCloudAlibaba的強大和神奇之處

我們僅改動了兩個註釋,其他全篇程式碼不變,原來的獨立服務就被我們分為了provider和consumer兩個獨立部分,二者各司其職,分工明確

篇尾我們留下了一個疑問,consumer對provider有依存關係,如果下游的provider出現異常,上游的consumer如何自處?

其實這種單一的上下游關係僅是微服務日常運轉下各種複雜情境的一個縮影,真實的生產環境下各個微服務節點可能會形成更為複雜的依賴關係,那我們該如何解決這些問題?

本節中我們引入Sentinel框架:

什麼是 Sentinel?

在基於 SpringCloud 構建的微服務體系中,服務間的呼叫鏈路會隨著系統的演進變得越來越長,這無疑會增加了整個系統的不可靠因素。

在並行流量比較高的情況下,由於網路呼叫之間存在一定的超時時間,鏈路中的某個服務出現宕機都會大大增加整個呼叫鏈路的響應時間,而瞬間的流量洪峰則會導致這條鏈路上所有服務的可用執行緒資源被打滿,從而造成整體服務的不可用,這也就是我們常說的 「雪崩效應」。

而在微服務系統設計的過程中,為了應對這樣的糟糕情況,最常用的手段就是進行 」流量控制「 以及對網路服務的呼叫實現「熔斷降級」。因此,Sentinel 就因運而生了。

Sentinel 是一款面向分散式服務架構的輕量級流量控制元件,主要以流量為切入點,從流量控制、熔斷降級、系統自適應保護等多個維度來保障服務的穩定性,核心思想是:根據對應資源設定的規則來為資源執行相應的流控/降級/系統保護策略,Sentinel 的主要特性如下圖:

 

 

 以上內容來源於Sentinel官方檔案,看到這一大堆不明所以的名詞接釋,新入坑的同學可能已經勸退了。。

接下來我們依然沿用前幾篇的思想,先不要管這一堆的理論,先來看怎麼用,實際運用過程中如何達到我們預期的效果,返回來再看這些名詞,你自然會有更清晰的認知

Sentinel怎麼用?

Sentinel的使用分為兩部分 —— 程式碼和控制檯

首先我們先下載Sentinel控制檯:https://github.com/alibaba/Sentinel/tags,本節中我使用的是Sentinel1.8版本

下載編譯好的jar包,而後執行命令列:

java -jar .\sentinel-dashboard.jar --server.port=9999

之後我們開啟瀏覽器,輸入地址:http://127.0.0.1:9999/

開啟Sentinel控制檯如圖所示:

 

 

整個面板是空白的,什麼都沒有?!不用著急,我們繼續往下看程式碼的部分

上一節我們提到了微服務環境下consumer工程存在的隱患問題,接下來我們就要消除這個隱患!

dubbo-nacos-consumer工程引入Sentinel依賴庫

        <!-- 引入sentinel依賴 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>

這一步肯定是必須的,沒什麼好解釋的

新增Sentinel相關設定

 

 

在SpringCloudAlibaba體系下,我們以Nacos為設定中心,所以我們編輯Nacos中的相關設定即可,非常方便

我把程式碼文字也貼出來,便於大家自行復制

# sentinel看板設定
spring.cloud.sentinel.transport.dashboard = 127.0.0.1:9999
# 開啟對sentinel看板的飢餓式載入。sentinel預設是懶載入機制,只有存取過一次的資源才會被監控,通過關閉懶載入,在專案啟動時就連線sentinel控制檯
spring.cloud.sentinel.eager = true

這兩句的主要意圖在於將Consumer工程關聯到Sentinel控制檯,便於後續通過控制檯統一管控

改造原有的PersonController

我們先建立一個ViewObject

 

 

程式碼如下:

package com.example.dubbonacosconsumer.vo;

import com.example.dubbonacosapi.model.Person;

import java.util.List;

public class SelectRetVo {
    private List<Person> persons;
    private String error;

    public List<Person> getPersons() {
        return persons;
    }

    public void setPersons(List<Person> persons) {
        this.persons = persons;
    }

    public String getError() {
        return error;
    }

    public void setError(String error) {
        this.error = error;
    }
}

然後是PersonController的改造

package com.example.dubbonacosconsumer.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.example.dubbonacosapi.model.Person;
import com.example.dubbonacosapi.service.PersonService;
import com.example.dubbonacosconsumer.vo.SelectRetVo;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;

@RestController
@RequestMapping("/person")
public class PersonController {
    @DubboReference
    PersonService service;

    @PostMapping("/insert")
    public Integer insert(Person person) {
        return service.insert(person);
    }

    @PostMapping("/update")
    public Integer update(Person person) {
        return service.update(person);
    }

    @PostMapping("/delete")
    public Integer delete(int id) {
        return service.delete(id);
    }

    @GetMapping("/select")
    @SentinelResource(value = "person/select", blockHandler = "selectBlock")
    public SelectRetVo select() {
        SelectRetVo vo = new SelectRetVo();
        vo.setPersons(service.select());
        vo.setError("ok");
        return vo;
    }

    public SelectRetVo selectBlock(BlockException e) {
        SelectRetVo vo = new SelectRetVo();
        vo.setPersons(new ArrayList<Person>());
        vo.setError(「當前存取人數過多,請稍後...」);
        return vo;
    }
}

我們將原有的select方法的返回值從原來的List<Person>升級為SelectPersonVo,後者在前者原有基礎上擴充套件了一個error欄位,用於返回異常資訊

接著是本節的關鍵:select方法新增@SentinelResource註解,我們前邊講過:Sentinel根據對應資源設定的規則來為資源執行相應的流控/降級/系統保護策略

因此這個註解的作用是——用於標明這是一個Sentinel系統中的資源

value代表這個資源的名稱是person/select,這個名字可以隨自己的習慣自定義

blockHandler指定了一個方法,這個方法在Sentinel系統觸發某種規則的時候會被執行,關於「某種規則」我們稍後會講

這裡指定的selectBlock方法,在定義時是有硬性要求的:

1. 保留select方法一樣的引數,外加一個BlockException引數

2. 返回值必須和select方法相同 —— 所以明白為什麼要額外定義一個SelectRetVo了吧?

控制檯的使用

我們像上一節一樣,分別啟動provider和consumer,然後重新整理Sentinel控制檯頁面

 

 

看到這個介面的時候,你是否有種豁然開朗的感覺?

因為consumer中定義了Sentinel資源,所以當dubbo-nacos-consumer工程執行之後,控制檯會有相關顯示

而功能選單中有N多項的名稱都是XX規則,這也就是前邊我們定義的blockHandler對應的某種規則

我們在post中呼叫consumer中的select方法,此時實時監控頁面顯示如下內容:

 

 

這裡的person/select自然就是我們定義的【資源】,控制檯配套顯示了其各項指標資料,很直觀也很方便

下邊還有一項/person/select(開頭多個/),這個又是什麼?這裡先直接告訴大家答案——Sentinel預設會將所有Controller新增請求對映的方法視為資源

那我們額外新增一個@SentinelResource註解是否多此一舉?答案是否,因為Controller生成的預設Sentinel資源是不帶自定義規則觸發方法的

因此@SentinelResource依然是有必要的,待本篇內容結束之後,大家可以自行驗證這個說法

而從第二項【簇點鏈路】中,我們也能看到person/select和/person/select本身具備從屬關係

 

 

流控規則

第三項【流控規則】對應了本節標題中提到的【限流】

 

 

這裡涉及到三個概念:

閾值型別

QPS —— 伺服器每秒接受的最大請求數

執行緒數 —— 伺服器能容忍的最大執行緒佔用數,一般用於保護伺服器的業務執行緒池不被耗盡

流控模式

直接 —— 預設項,介面到達限流要求時,規則直接生效

關聯 —— 當關聯在資源到達閾值時,直接限流自己,一般應用於效率讓步的訴求

鏈路 —— 記錄鏈路流量,當入口資源到達閾值,則限流自己

流控效果

快速失敗 —— 預設項,超出閾值後新請求直接拒絕

排隊等待 —— 讓請求勻速通過(漏桶演演算法),每個請求在一個允許的延遲時長範圍

Warm up —— 冷啟動模式,防止流量瞬間暴增直接將服務壓垮,而是逐漸外放請求上限

我們先按圖所示建立一個最簡單的限流規則 —— QPS閾值為1,直接快速失敗

而後我們啟動兩個post,同時向伺服器端傳送select請求,則結果對比如下:

 

 

 由於QPS指定的閾值為1,因此同時發起的第二個請求會因觸發限流規則而執行blockHandler方法

而我們針對 閾值型別、流控模式、流控效果 這三個指標交叉組合,可以創造出適用於各種伺服器場景之下的限流規則

再比如我們按如下設定限流規則

 

這種設定方式代表我們所能允許select方法同時請求的上限值為2000,但是這個數值是在5分鐘(300秒)之間逐步放開的,比如我們用這種模式可以應對類似雙11期間大力度優惠而帶來的突發流量洪峰

通過【限流】可以很好的起到保護伺服器的作用,在特殊時期針對流量進行削峰填谷,使得伺服器處在一個長期穩定的環境

降級規則

第四項【降級規則】對應了本節標題中的【熔斷】

 

 所謂「熔斷」,其實最早這個概念來源於家庭電路中的保險絲,當家庭中的某件家用電器功率異常,或者某處電路出現短路,

為防止電流過大造成更大的損失,此刻保險絲即會【熔斷】,造成我們日常所說的「掉閘」

熔斷策略

慢呼叫比例 —— 慢呼叫其實就是響應超過預定時長,當這樣的呼叫到達一定比例後,觸發降級規則

異常比例 —— 方法呼叫出現異常數到達一定比例後,觸發降級規則

異常數 —— 方法呼叫出現異常到達一定數目後,觸發降級規則

我們按照圖中所示建立一條降級規則,其含義為【任意1次並行請求(QPS)中有1次異常則觸發熔斷規則,時長為10秒】

這裡的10秒時間是我們一個大概的預估值,一般理解為系統達到自愈效果需要大概10秒左右

之後我們直接關停下游的provider,則第一次請求會導致異常,而第二次請求則觸發降級規則

 

若我們不設定降級規則,則大量的異常請求會堆積在consumer一側,導致consumer最終崩潰

而熔斷機制的設立,可以使得下游provider異常的情況下,上游的consumer依然做出正常的應答,

而10秒鐘provider服務自愈之後,所有的異常影響將消失於無形 —— 這就是微服務方案下針對我們上一節所遺留的問題給出的答案

其他熔斷策略同理,我們可以自行嘗試

本節我們重點講述了Sentinel系統中的限流熔斷,而除此之外,Sentinel還有更多更豐富的規則設定,可以應對微服務體系中更加複雜多變的場景

這裡我們推薦幾篇文章,大家感興趣可以自行閱讀

sentinel 限流熔斷神器詳細介紹_張維鵬的部落格-CSDN部落格_sentinel熔斷

Sentinel限流熔斷最全教學_思月行雲的部落格-CSDN部落格

本節內容到此為止,而微服務領域的探索卻遠不止於此

眾多節點之間的複雜呼叫,不同微服務不同的認證方式,跨域問題等等一系列問題接踵而至,我們又當如何應對?

請期待下一節內容 —— 基於SpringCloudGateway的閘道器設計,謝謝~

 

 

e.getMessage()