1. 程式人生 > 實用技巧 >Sentinel入門學習總結

Sentinel入門學習總結

  最近公司裡面再進行微服務開發,因為有使用到限流降級,所以取調研學習了一下Sentinel,在這裡做一個總結。

  Sentinel官方文件:https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D


一、Sentinel的作用

1、Sentinel是什麼

  Sentinel---分散式系統的流量衛兵。

  主要面向分散式架構的流量控制產品。以流量為切入點,從流量控制、熔斷降級、系統負載保護、等各個維度對服務提供保護。其官方稱其為輕量級產品。

2、基礎概念

  2.1、資源:Sentinel的核心概念,一切需要Sentinel保護的東西都稱為資源。它可以是一段程式碼、由應用程式提供的服務、或由應用程式呼叫的其它應用程式提供的服務。在程式中最直觀的呈現就是由Sentinel Api包圍起來的一段程式碼。

  2.2、規則:對資源的保護規則。如流量規則、熔斷降級規則、系統保護規則等。所有規則都是可以實時動態調整的。

  2.3、埋點:定義資源的過程。

3、基本用途

  3.1、流量控制

  因為流量是不規則的,在流量高峰期,大批量流量瞬間湧入系統會瞬間沖垮服務,這個時候我們需要限制流量對系統進行保護;同時在高峰期,如果直接丟棄掉超過服務承載能力的流量,而在流量低谷期由只有很少的流量或者無流量,那麼此時服務又是一種資源浪費,並且也無無法給使用者一個很好的體驗感,因為我們需要將流量高峰期的部分流量分流到低谷期,而不是在高峰期全數丟棄超過服務負載能力的流量。

  其原理就是監控應用流量的QPS或併發執行緒數,當達到指定的閾值時對流量進行控制。

  流控規則在Sentinel控制檯的直觀體現。

流控模式:

  直接:當介面達到限流條件時,開啟限流;

  關聯:當關聯的資源達到限流條件時,開啟限流;適合做應用讓步;比如一個查詢的介面新增關聯限流,關聯限流資源為一個更新的介面,當更新的介面達到閾值時,開啟查詢介面的限流,為更新介面讓步伺服器資源。

  比如說對於訂單,現在有兩種請求,一種是使用更新訂單量請求【QUERY_ORDER_NUM】,另一種是查詢請求【UPDATE_ORDER_NUM】。

  當正處於訂單使用高峰期,更新請求達到了設定閾值,這時可以暫時降低訂單查詢服務的可用性,優先保障訂單扣減邏輯的執行。

  鏈路:當從某個介面過來的資源達到限流條件時,開啟限流。這是一種更精細化的資源管理方式。

  比如說定義一個資源 【SAY_HELLO】:

  且同時有兩個介面 【/friendly】【/haughty】都呼叫該資源:

  這時我們配置SAY_HELLO的入口資源為 【/friendly】,那麼當【/friendly】呼叫超過閾值時就會觸發限流開啟,而【/haughty】則不會觸發。

流控效果

  快速失敗:當請求達到限流閾值的時候,後續請求會立即拒絕,拒絕方式就是丟擲FlowException。這種方式適用於對系統處理能力確切已知的情況下,比如通過壓測確定了系統的準確水位時。

  Warm Up(預熱/冷啟動):當系統長期處於低水位的情況下,而流量突然增加時,直接把系統拉昇到高水位可能瞬間把系統壓垮。通過"冷啟動",在指定的預熱時間內,讓通過的流量緩慢增加,在一定時間內逐漸增加到閾值上限,給冷系統一個預熱的時間,避免冷系統被壓垮。主要用於啟動需要額外開銷的場景;

  排隊等待:該種模式會嚴格控制請求通過的間隔時間,也即是讓請求以均勻的速度通過,從而達到一種流量整形的效果。

  3.2、熔斷降級

  一個服務常常會呼叫別的模組,可能是另外的一個遠端服務、資料庫,或者第三方 API 等。對呼叫鏈路中不穩定的資源進行熔斷降級也是保障高可用的重要措施之一。

  例如,支付的時候,可能需要遠端呼叫銀聯提供的 API;查詢某個商品的價格,可能需要進行資料庫查詢。然而,這個被依賴服務的穩定性是不能保證的。如果依賴的服務出現了不穩定的情況,請求的響應時間變長,那麼呼叫服務的方法的響應時間也會變長,執行緒會產生堆積,最終可能耗盡業務自身的執行緒池,服務本身也變得不可用。

  因此,我們需要對不穩定的依賴服務呼叫進行熔斷降級。

熔斷策略說明:

  慢呼叫比例:選擇以慢呼叫比例作為閾值,需要設定允許的慢呼叫 RT(即最大的響應時間),請求的響應時間大於該值則統計為慢呼叫。當單位統計時長(statIntervalMs)內請求數目大於設定的最小請求數目,並且慢呼叫的比例大於閾值,則接下來的熔斷時長內請求會自動被熔斷。

  異常比例:當單位統計時長內請求數目大於設定的最小請求數目,並且異常的比例大於閾值,則接下來的熔斷時長內請求會自動被熔斷。

  異常數:當單位統計時長內的異常數目超過閾值之後會自動進行熔斷。


二、Sentinel的使用

  1、基礎使用

  1.1、引入jar包

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-core</artifactId>
    <version>1.8.0</version>
</dependency>

  1.2、定義資源

  用 Sentinel API SphU.entry("HelloWorld") 和 entry.exit()將需要進行流量控制的程式碼包圍起來。被包圍起來的程式碼就作為資源,用API包圍起來即是埋點。

public static void fun() {
    Entry entry = null;
    try {
        entry = SphU.entry(SOURCE_KEY);
        pass.incrementAndGet();
        // todo 業務邏輯
        int temp = 10 / 0;
    } catch (BlockException e1) {
        block.incrementAndGet();
        // todo 流控處理
    } catch (Throwable e) {
        Tracer.traceEntry(e, entry);
        // todo 業務異常處理
    } finally {
        total.incrementAndGet();
        if (entry != null) {
            entry.exit();
        }
    }
}

  1.3、定義規則

private static void initFlowQpsRule() {
    List<FlowRule> rules = new ArrayList<FlowRule>();
    FlowRule rule1 = new FlowRule();
    rule1.setResource(SOURCE_KEY);
    // 採用qps策略,每秒允許通過1個請求
    rule1.setCount(1);
    rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
    rule1.setLimitApp("default");
    rules.add(rule1);
    FlowRuleManager.loadRules(rules);
}

  1.4、完整程式碼

/**
 * 流量控制
 */
public class FlowQpsDemo {

    private static final String SOURCE_KEY = "CESHI_KEY";

    private static AtomicInteger pass = new AtomicInteger();
    private static AtomicInteger block = new AtomicInteger();
    private static AtomicInteger total = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        initFlowQpsRule();
        for (int i = 0;i < 10;i++) {
            fun();
        }
        System.out.println("total=" + total.get() + " pass=" + pass.get() + " block=" + block.get());
    }

    public static void fun() {
        Entry entry = null;
        try {
            entry = SphU.entry(SOURCE_KEY);
            // todo 業務邏輯
            pass.incrementAndGet();
        } catch (BlockException e1) {
            // todo 流控處理
            block.incrementAndGet();
        } finally {
            total.incrementAndGet();
            if (entry != null) {
                entry.exit();
            }
        }
    }

    private static void initFlowQpsRule() {
        List<FlowRule> rules = new ArrayList<FlowRule>();
        FlowRule rule1 = new FlowRule();
        rule1.setResource(SOURCE_KEY);
        // 採用qps策略,每秒允許通過1個請求
        rule1.setCount(1);
        rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
        rule1.setLimitApp("default");
        rules.add(rule1);
        FlowRuleManager.loadRules(rules);
    }

    private static void sleep(int sleep) {
        try {
            Thread.sleep(sleep);
        } catch (InterruptedException e) {
        }
    }
}

  2、生產使用

  上面的示例程式碼演示了基礎的使用方式,但是實際生產環境不會如此使用。一是直接將Sentinel Api程式碼寫入到程式碼,對業務程式碼造成了侵入;二是將規則寫死在程式碼裡面,不能動態調整。所以針對實際生產使用,我們使用Sentinel註解埋點,且整合Nacos,將配置資料同步到Nacos配置中心進行儲存;

  2.1、Sentinel推模式

  在介紹生產環境使用Sentinel的時候,首先介紹一下Sentinel控制檯、Sentinel客戶端、配置中心的關係:

  首先在Sentinel Dashboard中維護各種限流、降級規則,Sentinel Dashboard將規則推送到Nacos進行持久化儲存;同時Sentinel客戶端註冊Nacos資料來源,訂閱Nacos資料中心的流控規則,當發生變化時更新客戶端本地記憶體規則。

  2.2、引入jar包

<!-- spring cloud 與 sentinel 整合包 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    <version>2.2.2.RELEASE</version>
</dependency>
<!-- SpringCloud ailibaba sentinel-datasource-nacos 配置nacos動態配置中心 -->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
    <version>1.8.0</version>
</dependency>

  2.3、配置Sentinel

  2.3、註冊動態配置中心

import com.alibaba.cloud.sentinel.SentinelProperties;
import com.alibaba.cloud.sentinel.datasource.config.NacosDataSourceProperties;
import com.alibaba.csp.sentinel.datasource.ReadableDataSource;
import com.alibaba.csp.sentinel.datasource.nacos.NacosDataSource;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import java.util.List;

@Configuration
public class SentinelConfig {

    @Autowired
    private SentinelProperties sentinelProperties;

    @PostConstruct
    public void run() throws Exception {
        // sentinel客戶端註冊流控nacos動態資料來源
        NacosDataSourceProperties flowConfiguration = sentinelProperties.getDatasource().get("flow").getNacos();
        ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>(
                flowConfiguration.getServerAddr(), flowConfiguration.getGroupId(), flowConfiguration.getDataId(),
                source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {
                }));
        FlowRuleManager.register2Property(flowRuleDataSource.getProperty());

        // sentinel客戶端註冊降級nacos動態資料來源
        NacosDataSourceProperties degradeConfiguration = sentinelProperties.getDatasource().get("degrade").getNacos();
        ReadableDataSource<String, List<DegradeRule>> degradeRuleDataSource = new NacosDataSource<>(
                degradeConfiguration.getServerAddr(), degradeConfiguration.getGroupId(),
                degradeConfiguration.getDataId(),
                source -> JSON.parseObject(source, new TypeReference<List<DegradeRule>>() {
                }));
        DegradeRuleManager.register2Property(degradeRuleDataSource.getProperty());
    }
}

  2.4、使用註解埋點:@SentinelResource

@GetMapping("/ceshi/flow")
@SentinelResource(value = "CESHI_FLOW", blockHandler = "ceshiFlow")
public String ceshiFlow(@RequestParam String message) {
    return "hello " + message;
}

public String ceshiFlow(String message, BlockException exception) {
    return "當前服務被限流,暫不可用! message=" + message;
}

@GetMapping("/ceshi/degrade")
@SentinelResource(value = "CESHI_DEGRADE", blockHandler = "ceshiDegradeBlock", fallback = "ceshiDegradeFallback")
public String ceshiDegrade(@RequestParam int age) throws InterruptedException {
    if (age < 0) {
        throw new ApplicationBaseException("年齡非法");
    } else if (age == 0) {
        Thread.sleep(500);
        return "***剛出生***";
    }
    return "您的年齡為:" + age;
}

public String ceshiDegradeBlock(int age, BlockException e) {
    return "當前請求被阻塞,age=" + age + " exMessage=" + e.getMessage();
}

public String ceshiDegradeFallback(int age, Throwable t) {
    return "當前請求執行失敗,age=" + age + " exMessage=" + t.getMessage();
}

  2.6、在Sentinel控制檯維護各項流控規則

  3、註解埋點詳解-@SentinelResource

  各項屬性如下:

  1、value:資源名稱,必需項(不能為空)。

  2、entryType:entry 型別,可選項(預設為 EntryType.OUT)。

  3、blockHandler / blockHandlerClass:

  blockHandler 對應處理 BlockException 的函式名稱,可選項。blockHandler 函式訪問範圍需要是 public,返回型別需要與原方法相匹配,引數型別需要和原方法相匹配並且最後加一個額外的引數,型別為 BlockException。blockHandler 函式預設需要和原方法在同一個類中。

  若希望使用其他類的函式,則可以指定blockHandlerClass 為對應的類的 Class 物件,注意對應的函式必需為 static 函式,否則無法解析。

  4、fallback / fallbackClass:fallback 函式名稱,可選項,用於在丟擲異常的時候提供 fallback 處理邏輯。fallback 函式可以針對所有型別的異常(除了 exceptionsToIgnore 裡面排除掉的異常型別)進行處理。

  fallback 函式簽名和位置要求:返回值型別必須與原函式返回值型別一致;方法引數列表需要和原函式一致,或者可以額外多一個 Throwable 型別的引數用於接收對應的異常。

  fallback 函式預設需要和原方法在同一個類中。若希望使用其他類的函式,則可以指定 fallbackClass 為對應的類的 Class 物件,注意對應的函式必需為 static 函式,否則無法解析。

  5、defaultFallback(since 1.6.0):預設的 fallback 函式名稱,可選項,通常用於通用的 fallback 邏輯(即可以用於很多服務或方法)。預設 fallback 函式可以針對所有型別的異常(除了exceptionsToIgnore 裡面排除掉的異常型別)進行處理。若同時配置了 fallback 和 defaultFallback,則只有 fallback 會生效。

  defaultFallback 函式簽名要求:返回值型別必須與原函式返回值型別一致;方法引數列表需要為空,或者可以額外多一個 Throwable 型別的引數用於接收對應的異常。defaultFallback 函式預設需要和原方法在同一個類中。若希望使用其他類的函式,則可以指定 fallbackClass 為對應的類的 Class 物件,注意對應的函式必需為 static 函式,否則無法解析。

  6、exceptionsToIgnore(since 1.6.0):用於指定哪些異常被排除掉,不會計入異常統計中,也不會進入 fallback 邏輯中,而是會原樣丟擲。

  7、特別地,若 blockHandler 和 fallback 都進行了配置,則被限流降級而丟擲 BlockException 時只會進入 blockHandler 處理邏輯。若未配置 blockHandler、fallback 和 defaultFallback,則被限流降級時會將 BlockException 直接丟擲(若方法本身未定義 throws BlockException 則會被 JVM 包裝一層 UndeclaredThrowableException)。