1. 程式人生 > 其它 >Spring Cloud Alibaba - Sentinel

Spring Cloud Alibaba - Sentinel

Sentinel

簡介

在專案配置檔案中新增配置

開啟控制檯dashboard頁面需要下載Sentinel-dashboard.jar,並啟動該spring boot專案。控制頁面預設為:localhost:8080 密碼和使用者名稱都為:sentinel

Sentinel控制檯呼叫的AIP:

localhost:8719/api

Dashboard頁面所有的資訊都是通過這個地址進行獲取的


流量控制規則

開啟對應的服務面板,點選簇點鏈路 進入配置頁面。選擇對應的資源名稱,點選流控按鈕開始配置。

在面板中直接配置即可

配置完成後會顯示配置記錄資訊:

當訪問請求數量超過設定的閾值時,預設返回的資訊為:

此資訊可以自定義

tips:沒有做持久化的Sentinel每次重啟都不會儲存做過的配置,所有的配置在無持久化的情況下都是儲存在記憶體中的


實現限流自定義返回結果

自定義RestObject,返回結果部分資訊存在此物件中:

@Builder  //生成構建器模式
@Data
public class RestObject {
    private int statusCode;
    private String statusMessage;
    private Object data;
}

編寫一個類實現BlockExceptionHandler介面:

@Slf4j
@SuppressWarnings("all")
@Component
public class MyBlockExceptionHandler implements BlockExceptionHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
        log.info("UrlBolckHandler--------------------------");
        RestObject restObject = null;

        //不同異常返回不同提示語
        if (e instanceof FlowException){
            restObject = RestObject.builder().statusCode(100).statusMessage("介面限流了").build();
        }else if (e instanceof DegradeException){
            restObject = RestObject.builder().statusCode(101).statusMessage("服務降級了").build();
        }else if (e instanceof ParamFlowException){
            restObject = RestObject.builder().statusCode(102).statusMessage("熱點引數限流了").build();
        }else if (e instanceof SystemBlockException){
            restObject = RestObject.builder().statusCode(103).statusMessage("觸發系統保護規則").build();
        }else if (e instanceof AuthorityException){
            restObject = RestObject.builder().statusCode(104).statusMessage("授權規則不通過 ").build();
        }

        httpServletResponse.setStatus(500);
        httpServletResponse.setCharacterEncoding("utf-8");
        httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
        //Spring MVC 的一個JSON轉換類(Jackson)
        new ObjectMapper().writeValue(httpServletResponse.getWriter(),restObject);
    }
}

當超過限流設定的閾值時,返回資訊:

如果需要限流以後返回一個頁面則需要更改MyBlockExceptionHandler中的部分程式碼:

@Slf4j
@SuppressWarnings("all")
@Component
public class MyBlockExceptionHandler implements BlockExceptionHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
        log.info("UrlBolckHandler--------------------------");
        RestObject restObject = null;

        //不同異常返回不同提示語
        if (e instanceof FlowException){
            restObject = RestObject.builder().statusCode(100).statusMessage("介面限流了").build();
        }else if (e instanceof DegradeException){
            restObject = RestObject.builder().statusCode(101).statusMessage("服務降級了").build();
        }else if (e instanceof ParamFlowException){
            restObject = RestObject.builder().statusCode(102).statusMessage("熱點引數限流了").build();
        }else if (e instanceof SystemBlockException){
            restObject = RestObject.builder().statusCode(103).statusMessage("觸發系統保護規則").build();
        }else if (e instanceof AuthorityException){
            restObject = RestObject.builder().statusCode(104).statusMessage("授權規則不通過 ").build();
        }
        
        httpServletResponse.sendRedirect("http://www.baidu.com");
//進行頁面重定向,前後端分離不知道是否可行
    }
}

Sentinel配置中一些引數

關聯設定:

鏈路:

對入口資源監聽,超過設定閾值限制本資源的訪問。入口資源為所屬組名


Sentinel服務降級RT


Sentinel服務降級異常比例和異常數

異常比例:範圍為0.0 - 1.0,總請求數量*異常比例的請求數會進入服務降級時間為時間視窗設定的時間

異常數:


Sentinel熱點引數規則

通過@SentinelResource註解對方法加入Sentinel監控:

@RestController
@SuppressWarnings("all")
public class SentinelController {
    @Autowired
    private RestTemplate restTemplate;
    
    @GetMapping("/app")
    @SentinelResource(value = "app",fallback = "fallback2",fallbackClass = MyFallbackClass.class)
    public String app(@RequestParam(value = "a",required = false)String a,
                      @RequestParam(value = "b",required = false)String b){
        System.out.println("/app/--->"+a+"---"+b);
        return restTemplate.getForObject("http://nacos-discovery-provider/test",String.class);
    }
}

設定的發生熔斷時,呼叫的類:MyFallbackClass

@Component
public class MyFallbackClass {
    public static String fallback(String a,String b){
        System.out.println("Fall back--->"+a+"---"+b);
        return "Fall back";
    }
    public static String fallback2(String a,String b){
        System.out.println("Fall back2--->"+a+"---"+b);
        return "Fall back2";
    }
}

blockHandler = "block",blockHandlerClass = MyBlockHanlderClass.class處理限流

fallback = "fallback",fallbackClass = MyFallbackClass.class 處理降級

在這裡,發生熔斷時呼叫MyFallbackClass中的fallback2方法

設定規則:

超過閾值則顯示:

如果設定如下:

則表示,當設定的引數值為5時,它的閾值為2。超過閾值進行限流服務降級。


Sentinel系統保護規則


Sentinel授權規則

白名單:可以呼叫;黑名單:不能呼叫

對於黑白名單的標識可以放在引數、header中。

對於下列設定:

則請求為:

localhost:9000/app?origin=order

對於這裡的源origin,我們需要實現一個解析類,將其中的源名返回,如果和設定的流程應用名相同,並且設定為黑名單則該請求無法訪問。返回:

解析類:

@Component
public class MyRequestOriginParser implements RequestOriginParser {
    //其中獲取的欄位名字需要和請求中的名字相對應
    @Override
    public String parseOrigin(HttpServletRequest httpServletRequest) {
        //從引數中獲取
        String origin = httpServletRequest.getParameter("origin");
        //從header中獲取
        String origin_header = httpServletRequest.getHeader("origin");
        if (StringUtils.isBlank(origin)){
            try {
                throw new IllegalAccessException("origin引數未指定");
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        return origin;
    }
}

請求中的origin欄位也可以變為其他的,但是需要和解析類中的獲取的欄位一致即可(猜測)。如果使用origin_header,則請求中就不應該再帶有origin=order,而是應該將該資訊放入到請求header中去。


Sentinel-dashboard控制檯通訊原理

微服務啟動後,暴露很多http介面,Sentinel-Dashboard通過呼叫這些介面(localhost:8719/api)進行對微服務的操控。


tips: Sentinel的一些配置


Sentinel三種保護應用的方式

這種方式用的比較少:

@GetMapping("/protect")
public String protect(@PathVariable("/app")String app){
    System.out.println("protect: "+app);

    ContextUtil.enter("protect","protect");//origin 服務請求來源,去掉也可以。用來實現授權規則的。
    Entry entry = null;
    try {
        entry = SphU.entry("protect"); //分散式鎖,開始保護下面的程式碼
        //業務邏輯
        int a = 10/0;
        return restTemplate.getForObject("http://nacos-discovery-provider/test",String.class);
    } catch (BlockException e) {
        e.printStackTrace();
        //手動寫服務降級程式碼 (參考MyBlockExceptionHandler.class)
        return "熔斷降級了";
    }catch (ArithmeticException e){
        Tracer.trace(e);  //對此類異常進行跟蹤(trace),Sentinel才能對異常進行監控
        return "除數不能為0";
    }finally {
        if (entry!=null){
            entry.exit();
        }
        ContextUtil.exit();
    }

}

MyFallbackClass:

@Component
public class MyFallbackClass {
    public static String fallback(String a,String b){
        System.out.println("Fall back--->"+a+"---"+b);
        return "Fall back";
    }
    public static String fallback2(String a,String b){
        System.out.println("Fall back2--->"+a+"---"+b);
        return "Fall back2";
    }
}

也可使用blockHandler+blockHandlerClass,規則與fallback+fallbackClass一致。需要注意的是:在MyBlockHandlerClass和MyFallbackClass中的所有被呼叫的方法都要定義為Static型別!!!


RestTemplate整合Sentinel

這樣Sentinel可以對RestTemplate的呼叫進行保護(限流,許可權......)注意註解中需要的是降級還是限流。

限流-->blockHandler+blockHandlerClass

降級-->fallback+fallbackClass


Feign整合Sentinel

第一種方法-配置fallback:

//name 遠端呼叫服務提供者的名字  fallback 降級處理   configuration feign 配置
@FeignClient(name = "nacos-discovery-provider",
            fallback = EchoServiceFallback.class,
            configuration = FeignConfiguration.class)
public interface EchoService {
    //getMapping 中的定義需要和對應的服務提供者中的介面呼叫一致
    @GetMapping("/echo-feign")
    public String echo();

}

EchoServiceFallback:

@SuppressWarnings("all")
public class EchoServiceFallback implements EchoService{
    //對EchoService中的同名方法進行服務降級時的操作
    @Override
    public String echo() {
        return "has been low down";
    }
}

第二種方法-配置fallbackFactory:

@FeignClient(name = "nacos-discovery-provider",
//            fallback = EchoServiceFallback.class,
        fallbackFactory = EchoServiceFallbackFactory.class,
            configuration = FeignConfiguration.class)
public interface EchoService {
    //getMapping 中的定義需要和對應的服務提供者中的介面呼叫一致
    @GetMapping("/echo-feign")
    public String echo();
}

EchoServiceFallbackFactory.class:

@SuppressWarnings("all")
//FallbackFactory<EchoService>中泛型的種類為controller介面中呼叫的型別
public class EchoServiceFallbackFactory implements FallbackFactory<EchoService> {
    @Override
    public EchoService create(Throwable throwable) {
        return new EchoService() {
            //對其中的所有方法設定降級實現
            @Override
            public String echo() {
                return "from fallbackFactory";
            }
        };
    }

Sentinel持久化

原始模式

拉模式

Pull模式

pull 模式的資料來源(如本地檔案、RDBMS 等)一般是可寫入的。使用時需要在客戶端註冊資料來源:將對應的讀資料來源註冊至對應的 RuleManager,將寫資料來源註冊至 transport 的 WritableDataSourceRegistry 中。以本地檔案資料來源為例:

public class FileDataSourceInit implements InitFunc {

    @Override
    public void init() throws Exception {
        String flowRulePath = "xxx";

        ReadableDataSource<String, List<FlowRule>> ds = new FileRefreshableDataSource<>(
            flowRulePath, source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {})
        );
        // 將可讀資料來源註冊至 FlowRuleManager.
        FlowRuleManager.register2Property(ds.getProperty());

        WritableDataSource<List<FlowRule>> wds = new FileWritableDataSource<>(flowRulePath, this::encodeJson);
        // 將可寫資料來源註冊至 transport 模組的 WritableDataSourceRegistry 中.
        // 這樣收到控制檯推送的規則時,Sentinel 會先更新到記憶體,然後將規則寫入到檔案中.
        WritableDataSourceRegistry.registerFlowDataSource(wds);
    }

    private <T> String encodeJson(T t) {
        return JSON.toJSONString(t);
    }
}

本地檔案資料來源會定時輪詢檔案的變更,讀取規則。這樣我們既可以在應用本地直接修改檔案來更新規則,也可以通過 Sentinel 控制檯推送規則。以本地檔案資料來源為例,推送過程如下圖所示:

首先 Sentinel 控制檯通過 API 將規則推送至客戶端並更新到記憶體中,接著註冊的寫資料來源會將新的規則儲存到本地的檔案中。使用 pull 模式的資料來源時一般不需要對 Sentinel 控制檯進行改造。

這種實現方法好處是簡單,不引入新的依賴,壞處是無法保證監控資料的一致性。

實操:自定義類FileDataSourceInit:

@SuppressWarnings("all")
public class FileDataSourceInit implements InitFunc {
    @Override
    public void init() throws Exception {
        //可以根據需要指定規則檔案的存放位置
        String ruleDir = "E:\\software\\JAVA\\springcloud-alibaba\\package\\Sentinel-dashboard";

        String flowRulePath = ruleDir+"/flow-rule.json";
        String degradeRulePath = ruleDir+"/degrade-rule.json";
        String paramFlowRulePath = ruleDir+"/param-flow-rule.json";
        String systemRulePath = ruleDir+"/system-rule.json";
        String authorityRulePath = ruleDir+"/authority-rule.json";

        this.mkdirIfNotExist(ruleDir);
        this.createFileIfNotExist(flowRulePath);
        this.createFileIfNotExist(degradeRulePath);
        this.createFileIfNotExist(paramFlowRulePath);
        this.createFileIfNotExist(systemRulePath);
        this.createFileIfNotExist(authorityRulePath);

        //流控規則:可讀資料來源
        ReadableDataSource<String, List<FlowRule>> ds = new FileRefreshableDataSource<>(
                flowRulePath, source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {})
        );
        // 將可讀資料來源註冊至 FlowRuleManager.
        FlowRuleManager.register2Property(ds.getProperty());

        WritableDataSource<List<FlowRule>> wds = new FileWritableDataSource<>(flowRulePath, this::encodeJson);
        // 將可寫資料來源註冊至 transport 模組的 WritableDataSourceRegistry 中.
        // 這樣收到控制檯推送的規則時,Sentinel 會先更新到記憶體,然後將規則寫入到檔案中.
        WritableDataSourceRegistry.registerFlowDataSource(wds);

        //降級規則,可讀資料來源
        ReadableDataSource<String, List<DegradeRule>> degradeRuleRDS = new FileRefreshableDataSource<>(
                degradeRulePath, source -> JSON.parseObject(source, new TypeReference<List<DegradeRule>>() {})
        );
        // 將可讀資料來源註冊至 FlowRuleManager.
        DegradeRuleManager.register2Property(degradeRuleRDS.getProperty());

        WritableDataSource<List<DegradeRule>> degradeRuleWDS = new FileWritableDataSource<>(degradeRulePath, this::encodeJson);
        // 將可寫資料來源註冊至 transport 模組的 WritableDataSourceRegistry 中.
        // 這樣收到控制檯推送的規則時,Sentinel 會先更新到記憶體,然後將規則寫入到檔案中.
        WritableDataSourceRegistry.registerDegradeDataSource(degradeRuleWDS);

        //熱點引數規則:可讀資料來源
        ReadableDataSource<String,List<ParamFlowRule>> paramFlowRuleRDS = new FileRefreshableDataSource<List<ParamFlowRule>>(
                paramFlowRulePath,source-> JSON.parseObject(source,new TypeReference<List<ParamFlowRule>>(){})
        );
        ParamFlowRuleManager.register2Property(paramFlowRuleRDS.getProperty());
        //熱點引數規則:可寫資料來源
        WritableDataSource<List<ParamFlowRule>> paramFlowRuleWDS = new FileWritableDataSource<List<ParamFlowRule>>(
                paramFlowRulePath,
                this::encodeJson
        );
        ModifyParamFlowRulesCommandHandler.setWritableDataSource(paramFlowRuleWDS);

        //系統規則:可讀資料來源
        ReadableDataSource<String,List<SystemRule>> systemRuleRDS = new FileRefreshableDataSource<List<SystemRule>>(
                systemRulePath,source-> JSON.parseObject(source,new TypeReference<List<SystemRule>>(){})
        );
        SystemRuleManager.register2Property(systemRuleRDS.getProperty());
        //系統規則:可寫資料來源
        WritableDataSource<List<SystemRule>> systemRuleWDS = new FileWritableDataSource<List<SystemRule>>(
                systemRulePath,
                this::encodeJson
        );
        WritableDataSourceRegistry.registerSystemDataSource(systemRuleWDS);

        //授權規則:可讀資料來源
        ReadableDataSource<String,List<AuthorityRule>> authorityRuleRDS = new FileRefreshableDataSource<List<AuthorityRule>>(
                authorityRulePath,source-> JSON.parseObject(source,new TypeReference<List<AuthorityRule>>(){})
        );
        AuthorityRuleManager.register2Property(authorityRuleRDS.getProperty());
        //授權規則:可寫資料來源
        WritableDataSource<List<AuthorityRule>> authorityRuleWDS = new FileWritableDataSource<List<AuthorityRule>>(
                authorityRulePath,
                this::encodeJson
        );
        WritableDataSourceRegistry.registerAuthorityDataSource(authorityRuleWDS);
    }

    private <T> String encodeJson(T t) {
        return JSON.toJSONString(t);
    }

    private void mkdirIfNotExist(String filePath) throws IOException{
        File file = new File(filePath);
        if (!file.exists())
        {
            file.mkdirs();
        }
    }

    private void createFileIfNotExist(String filePath) throws IOException{
        File file = new File(filePath);
        if (!file.exists())
        {
            file.createNewFile();
        }
    }
}

然後在專案的 resources/META-INF/services 目錄下建立檔案,名為 com.alibaba.csp.sentinel.init.InitFunc ,內容為:

# 改成上面FileDataSourceInit的包名類名全路徑即可。
#(copy reference)
com.nacos.consumer.nacosconsumer.sentinel.FileDataSourceInit

優點

  • 簡單易懂
  • 沒有多餘依賴(比如配置中心、快取等)

缺點

  • 由於規則是用 FileRefreshableDataSource 定時更新的,所以規則更新會有延遲。如果FileRefreshableDataSource定時時間過大,可能長時間延遲;如果FileRefreshableDataSource過小,又會影響效能;
  • 規則儲存在本地檔案,如果有一天需要遷移微服務,那麼需要把規則檔案一起遷移,否則規則會丟失。

推模式

我們提供了 ZooKeeper, Apollo, Nacos 等的動態資料來源實現。以 ZooKeeper 為例子,如果要使用第三方的配置中心作為配置管理,您需要做下面的幾件事情:

  1. 實現一個公共的 ZooKeeper 客戶端用於推送規則,在 Sentinel 控制檯配置項中需要指定 ZooKeeper 的地址,啟動時即建立 ZooKeeper Client。
  2. 我們需要針對每個應用(appName),每種規則設定不同的 path(可隨時修改);或者約定大於配置(如 path 的模式統一為 /sentinel_rules/{appName}/{ruleType},e.g. sentinel_rules/appA/flowRule)。
  3. 規則配置頁需要進行相應的改造,直接針對應用維度進行規則配置;修改同個應用多個資源的規則時可以批量進行推送,也可以分別推送。Sentinel 控制檯將規則快取在記憶體中(如 InMemFlowRuleStore),可以對其進行改造使其支援應用維度的規則快取(key 為 appName),每次新增/修改/刪除規則都先更新記憶體中的規則快取,然後需要推送的時候從規則快取中獲取全量規則,然後通過上面實現的 Client 將規則推送到 ZooKeeper 即可。
  4. 應用客戶端需要註冊對應的讀資料來源以監聽變更,可以參考 相關文件

從 Sentinel 1.4.0 開始,Sentinel 控制檯提供 DynamicRulePublisherDynamicRuleProvider 介面用於實現應用維度的規則推送和拉取,並提供了相關的示例。Sentinel 提供應用維度規則推送的示例頁面(/v2/flow),使用者改造控制檯對接配置中心後可直接通過 v2 頁面推送規則至配置中心。改造詳情可參考 應用維度規則推送示例

部署多個控制檯例項時,通常需要將規則存至 DB 中,規則變更後同步向配置中心推送規則。

實操:

在元件中有的話就可以不用新增

#基於Nacos配置中心進行規則持久化
spring.cloud.sentinel.datasource.ds1.nacos.server-addr=localhost:80
spring.cloud.sentinel.datasource.ds1.nacos.data-id=${spring.application.name}.json
spring.cloud.sentinel.datasource.ds1.nacos.group-id=DEFAULT_GROUP
spring.cloud.sentinel.datasource.ds1.nacos.data-type=json
spring.cloud.sentinel.datasource.ds1.nacos.rule-type=flow

ds1為自定義名字,需要報紙配置資訊中的一致

Data id :nacos-discovery-consumer.json 專案名稱.json

Group:DEFAULT_GROUP

配置格式:JSON

配置內容:根據上圖的格式,自行配置。count = QPS 配置

流程:從Nacos讀取配置,快取到本地。Sentinel 從本地讀取配置完成持久化操作。