SpringCloudAlibaba 微服務講解(四)Sentinel--服務容錯(二)

ityml發表於2022-04-01

4.7 Sentinel 規則

4.7.1 流控規則

流量控制,其原理是監控應用流量的QPS(每秒查詢率)或併發執行緒數等指標,當達到指定的閾值時對流量進行控制,以避免被瞬時的榴蓮高峰沖垮,從而保障應用的高可用性。

點選簇點鏈路,我們就可以看到訪問過的介面地址,然後點選對應的流控按鈕,進入流控規則配置頁面。新增流控規則介面如下

資源名:唯一名稱,預設是請求路徑,可以自定義

針對來源:指定對哪個微服務進行限流,預設指default,意思是不區分來源,全部限制

閾值型別單機閾值:

  • QPS:當呼叫該介面的QPS達到閾值時,進行限流
  • 執行緒數:當呼叫該介面的執行緒數達到閾值的時候,進行限流

是否叢集L:暫時不需要叢集

我們接下來以QPS為例來研究限流規則的配置

4.7.1.1 簡單配置

我們先做一個簡單配置:設定閾值型別為QPS,單機閾值為3。即每秒請求量大於3的時候開始限流,接下來,在流控規則頁面就可以看到這個配置。

4.7.1.2 配置流控模式

點選上面設定流控規則的編輯按鈕,然後在編輯頁面點選高階選項,會看到有流控模式一欄

Sentinel共有三種流控模式,分別是:

  • 直接(預設):介面達到限流條件時,開啟限流
  • 關聯:當關聯的資源達到限流條件時,開啟限流
  • 鏈路:當從某個介面過來的資源達到限流條件時,開啟限流

下面分別演示三種模式:

  1. 直接流控模式

    直接留空模式是最簡單的模式,當指定掉介面達到限流條件時開啟。上面案例使用的就是直接留空模式

  2. 關聯流控模式

    關聯流控模式指的是,當指定介面關聯的介面達到限流條件時,開啟對指定介面開啟限流。

    第一步:配置限流規則,將流控模式設定為關聯,關聯資源設定為/order/message2。

    第二步:通過postman軟體向/order/message2連續傳送請求,注意QPS一定大於3

    第三步:訪問/order/message1,會發現已經限流了

  3. 鏈路流控模式

    鏈路流控模式指的是,當從某個介面過來的資源達到限流條件時,開啟限流。它的功能有點類似於針對來源配置項,區別在於:針對來源是針對上級服務,而鏈路流控是針對上級介面,也就是說他的粒度更細

    第一步:編寫一個service,在裡面新增一個方法message

    	@Service
    	public class OrderServiceImpl3{
    		@SentinelResource("message")
    		public void message(){
    			System.out.println("message");
    		}
    	}
    

    第二步:在Controller中宣告兩個方法,分別呼叫service中的方法message

    	@RestController
    	@Slf4j
    	public class OrderController3{
        @Autowired
        private OrderServiceImpl3 orderServiceImpl3;
        
        @requestMapping("/order/message1")
        public String message1(){
          return "message1";
        }
        
        @RequestMapping("/order/message2")
        public String message2(){
          orderServiceImpl3.message();
          return "message2";
        }
      }
    

    第三步:禁止收斂URL的入口context

    從1.6.3版本開始,Sentinel Web filter預設收斂所有URL的入口context,因此鏈路限流不生效

    1.7.0版本開始(對應SCA的2.1.1.RELEASE),官方在CommonFilter引入了 WEB_CONTEXT_UNIFY引數,用於控制是否收斂context。將其配置為false即可根據不同的URL進行鏈路限流。

    SCA 2.1.1.RELEASE之後的版本,可以通過配置spirng.cloud.sentinel.web-context-unify=false即可關閉收斂。我們當前使用的版本是SpringCloud Alibaba 2.1.0.RELEASE,無法實現鏈路限流

    目前官方還未釋出SCA 2.1.2.RELEASE,所以我們只能用2.1.1.RELEASE,需要寫程式碼的形式實現

    1. 暫時將SpringCloud Alibaba 的版本調整為2.1.1.RELEASE

      <spring-cloud-alibaba.version>2.1.1.RELEASE</spring-cloud-alibaba.version>
      
    2. 配置檔案中關閉sentinel的CommonFilter例項化

      spring:
      	cloud:
      		sentinel:
      			filter:
      				enable: false
      
    3. 新增一個配置類,自己構建CommonFilter 例項

      	@Configuration
      	public class FilterContextConfig{
          @Bean
          public FilterRegistrationBean sentinelFilterRegistration(){
            FilterRegistrationBean registration = new FilterRegistrationBean();
            registration.setFilter(new COmmonFilter());
            registration.addUrlPatterner("/*");
            // 入口資源關閉整合
            registration.addInitParameter(CommonFilter.WEB_CONTEXT_UNIFY,"false");
            registration.setName("sentinelFilter");
            registration.setOrder(1);
            return registration
          }
        }
      

    第四步:控臺配置限流規則

    第五步:分別通過/order/message1 /order/message2訪問,發現2沒問題,1 就被限流了

    4.7.1.3 配置流控效果

    • 快速失敗(預設):直接失敗,丟擲異常,不做任何額外的處理,是最簡單的效果
    • Warm Up:它從開始閾值到最大QPS閾值會有一個緩衝階段,一開始的閾值是最大QPS閾值的1/3,然後慢慢增長,知道最大閾值,適用於將突然增大的流量轉換為換不增長的場景。
    • 排隊等待:讓請求以均勻的速度銅鼓哦,單機閾值為每秒通過數量,其餘的排隊等待。它還會讓設定一個超時時間,當請求超過超時時間還未處理,則會被丟棄

4.7.2 降級規則

降級規則就是設定當滿足什麼條件的時候,對服務進行降級。Sentinel提供了三個衡量條件:

  • 平均響應時間:當資源的平均響應時間超過閾值(以ms為單位)之後, 資源進入準降級狀態。如果接下來1s內持續進入5個請求,它們的RT都將持續超過這個閾值,那麼在接下來的時間視窗(以s為單位)之內,就會對這個方法進行服務降級。

    注意Sentinel預設統計的RT上限是4900ms,超過此閾值都會算作4900ms,若需要變更此上限可以通過啟動配置項-Dcsp.sentinel.statistic.max.rt=xxx來配置

  • 異常比例:當資源的每秒異常總數佔通過量的桌布超過閾值之後,資源進入降級狀態,即在接下來的時間視窗(以s為單位)之內,對這個方法的呼叫都會自動地返回,異常比率的閾值範圍是【0.0,0.1】

    第一步:首先模擬一個異常

    int i=0;
    @RequestMapping("/order/message2")
    public String message2(){
      i++;
      //異常比例為0.333
      if(i%3==0){
        throw new RuntimeException();
      }
      return "message2";
    }
    

    第二步:設定異常比例為0.25

    • 異常數:當資源近1分鐘的異常數目超過閾值之後會進行服務降級,注意由於統計時間視窗是分鐘級別的,若時間視窗小於60s,則結束熔斷狀態後仍可能在進入熔斷狀態。

      問題:流控規則和降級規則返回的異常頁面是一樣的,我們怎麼來區分到底是什麼原因導致的呢?

4.7.3 熱點規則

熱點引數流控規則是一種耕細粒度的流控規則,它允許將規則具體到引數上。

熱點規則的簡單使用

第一步:編寫程式碼

@RequestMapping("order/message3")
//注意這裡必須使用這個註解標識,熱點規則不生效
@SentinelResource("message3")
public String message3(String name, Integer age){
  return name+age;
}

第二步:配置熱點規則

第三步:分別用兩個引數訪問,會發現只對一個引數限流了

熱點規則增強使用

引數例外項允許對一個引數的具體值進行流控

編輯剛才定義的規則,增加引數例外項

4.7.4 授權規則

很多時候,我們需要根據呼叫來源來判斷該次請求是否允許方放行,這時候可以使用Sentinel的來源訪問控制的功能,來源訪問控制根據資源的請求來源(origin)限制資源是否通過:

  • 若配置白名單,則只有請求來源位於白名單內時才通過;
  • 若配置黑名單,則請求來源位於黑名單時不通過,其餘的請求通過;

上面的資源名和授權型別不難理解,但是流控應用怎麼填寫呢?

其實這個位置要填寫的是來源標識,Sentinel提供了ReuestOriginParser 介面來處理來源。

只要Sentinel保護的介面資源被訪問,Sentinel就會呼叫RequestOriginParser的實現類去解析訪問來源。

第一步:自定義來源處理規則

@Component
public class RequestOriginParserDefinition implements RequestOriginParser{
  @Override
  public String parserOrigin(HttpServletRequest request){
    String serviceName =  request.getParameter("serviceName");
    return serviceName;
  }
}

第二步:授權規則配置

這個配置的意思是隻有serviceName=pc 不能訪問(黑名單)

第三步:訪問:http://localhost:8091/order/message1?serviceName=pc 觀察結果

4.6.5 系統規則

系統保護規則是從應用級別的入口流量進行控制,從單臺機器的總體Load、RT、入口QPS、CPU使用率和執行緒數五個維度,而不是資源維度的,並且僅對入口流量(進入應用的流量)生效。

  • Load(僅對Linux/Unix-like及其生效):當系統load1超過閾值,且系統當前的併發執行緒數超過系統容量時才會觸發系統保護。系統容量由系統的maxQps* minRt計算得出。設定參考值一版是CPU cores)*2.5。
  • RT:當單臺機器上所有入口流量的平均RT達到閾值即觸發系統保護,單位是毫秒。
  • 執行緒數:當單臺機器上所有有入口流量的併發執行緒數達到閾值即觸發系統保護。
  • 入口QPS:當單臺機器上所有入口流量QPS達到閾值即觸發系統保護。
  • CPU使用率:當單臺機器上所有入口流量的CPU使用率達到閾值即觸發系統保護

擴充套件:自定義異常返回

// 異常處理頁面
@Companent
public class ExceptionHandlerPage implements UrlBlockHandler{
  // BlockException 異常介面,包含Sentinel的五個異常
  // 用於定義資源,並提供可選的異常處理和fallback配置項。其主要引數如下:
  // FlowException 限流異常
  // DegradeException 降級異常
  // ParamFlowException 引數限流異常
  // AuthorityException 授權異常
  // SystemBlockException 系統負載異常
  @SentinelResource 
  @Override
  public void blocked(HttpServletRequest request ,HttpServletResponse response,BlockException e) throws IOEception{
    respnse.setContentType("application/json;charset=utf-8");
    ResponseData data = null;
    if(e instanceof FlowException){
      data = new ResponseData(-1,"介面被限流了。。。。");
    }else if(e instanceof DegradeException){
      data = new ResponseData(-2,"介面被降級了。。。");
    }
    response.gerWriter().wirte(JSON.toJSONString(data));
  }
}
@Data
@AllArgsConstructor
class ResponseData{
  private int code;
  private String message;
}

4.8 @SentinelResource 的使用

在定義了資源點之後,我們可以通過Dashboard來設定限流和降級策略來對資源點進行保護。同時還能通過@SentinelResource 來指定出現異常時的處理策略

@SentinelResource 用於定義資源,並提供可選擇的異常處理和fallback配置項。其主要引數如下:

定義限流和降級後的處理方法

方式一:直接將限流和降級方法定義在方法中

@Service
@Slf4j
public class OrderServiceImpl3{
  int i=0;
  @SentinelResource(
  	value="message",
    blockHandler = "blockHandler",//指定發生BlockException 時進入的方法
    fallback = "fallback"//指定發生Throwable時進入的方法
  )
  public String message(){
    i++;
    if(i % 3==0){
      throw new RuntimeException();
    }
    return "message";
  }
  // BlockException 時進入的方法
  public String blockHandler(BlockException ex){
    log.error("{}",ex);
    return "介面被限流和或降級了。。。。";
  }
  // Throwable 時進入的方法
  public String fallback(Throwable throwable){
    log.error("{}",throwable);
    return "介面發生異常了。。。。";
  }
}

方式二:將限流和降級方法外接到單獨類中

@Service
@Slf4j
public class OrderServiceImpl3{
  int i=0;
  @SentinelResource(
  	value = "message",
    blockHandlerClass = OrderServiceImpl3BlockHandlerClass.class,
    blockHandler = "blockHandler",
    fallbackClass = OrderServiceImpl3FallbackClass.class,
    fallback = "fallback"
  )
  public String message(){
    i++;
    if(i % 3 ==0){
      throw new RuntimeException();
    }
    return "message4";
  }
}


@Slf4j
public class OrderServiceImpl3BlockHandlerClass{
  // 注意這裡必須使用static 修飾方法
  public static String blockHandler(BlockException ex){
    log.error("{}",ex);
    return "介面被限流或者降級了。。。。";
    
  }
}

@Slf4j
public class OrderServiceImpl3FallbackClass {
   // 注意這裡必須使用static 修飾方法
  public static String fallback(Throwable throwable){
    log.error("{}",throwable);
    return "介面發生異常了。。。。";
  }
}

4.9 sentinel 規則持久化

通過前面的講解,我們已經知道,可以通過Dashboard來為每個Sentinel客戶端設定各種各樣的規則,但是這裡有一個問題,就是這些規則預設是存放在記憶體中的,非常不穩定,所以需要持久化。

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

首先Sentinel控制檯通過API將規則推送至客戶端並更新到記憶體中,接著註冊的寫資料來源會將新的規則儲存到本地的檔案中。

  1. 編寫處理類

    public class FilePersistence implement InitFunc{
      @Value("spring.application:name")
      private String applicationName;
      @Override
      public void init() throws Exception{
        String ruleDir = system.getProperty("user.home")+"/sentinel-rules/"+applicationName;
        String flowRulePath = ruleDir + "/flow-rule.json";
        String degradeRulePath = ruleDir + "/degrade-rule.json";
        String systemRulePath = ruleDir + "/system-rule.json";
        String authorityRulePath  = ruleDir +"/authority-rule.json";
        String paramFlowRulePath = ruleDir + "/param-flow-rule.json";
        this.mkdirIfNotExits(reuleDir);
        this.createFileIfNotExits(flowRulePath);
        this.createFileIfNotExits(degradeRulePath);
        this.createFileIfNotExits(systemRulePath);
        this.createFileIfNotExits(authorityRulePath);
        this.createFileIfNotExits(paramFlowRulePath);
        
        // 流控規則
        ReadableDataSource<String,List<FlowRule>> flowRuleRDS = new FileRefresableDataSource<>(
        flowRulePath,flowRuleListParser);
        FlowRuleManager.register2Property(flowRuleRDS.getProperty());
        WritableDataSource<List<FlowRule>> flowRulesWDS = new FileWritableDataSource<>(flowRulePath,this::encodeJson);
        WritableDataSourceRegistry.registerFlowDataSource(flowRuleWDS);
        
        // 降級規則
        ReadableDataSource<String,List<DegradeRule>> degradeRuleRDS = new FileRefreshableDataSource<>(
        degradeRulePath,degradeRuleListParser);
        DegradeRuleManager.register2Poperty(degradeRuleRDS.getProperty());
        WritableDataSource<List<DegradeRule>> degradeRuleWDS = new FileWritableDataSource<>(degradeRulePath,this::encodeJson);
        
        // 系統規則
        ReadableDataSource<String,List<SystemRule>> systemRuleRDS = new FileRefreshableDataSource<>			   					(systemRulePath,systemRuleListParser);
        SystemRuleManager.register2Property(systemRuleRds.getProperty());
        WritableDataSource<List<SystemRule>> systemRuleWDS = new FileWritableDataSource(systemnRulePath,this::encodeJson);
        WritableDataSourceRegistry.registerSystemDataSource(systemRuleWDS);
        
        // 授權規則
        ReadableDataSource<String,List<AuthorityRule>> authorityRuleRDS = new FileRefreshableDataSource<>(authorityRulePath,authorityRuleListParser);
        AuthorityRuleManager.register2Property(authorityRuleRDS.getProperty());
        WritableDataSource<List<AuthorityRule>> authorityRuleWDS = new FileWritableDataSource<>(authorityRulePath,this::encodeJson);
        WiritableDataSourceRegistry.registerAuthorityDataSource(authorityRuleWDS);
        
        // 熱點引數規則
        ReadbaleDataSource<String,List<ParamFlowRule>> paramFlowRuleRDS = new FileRefreshableDataSource<>(paramFlowRulePath,paramFlowRuleListParaser);
        ParamFlowRuleManager.register2Property(paramFlowRuleRDS.getProperty());
        WritableDataSouece<List<ParamFlowRule>> paramFlowRuleWDS = new FileWirtableDataSouece<>(paramFlowRulePath,this::encodeJson);
        ModifyParamFlowRulesCommandHandler.setWritableDataSouce(paramFlowRuleWDS);
      }
      
      private Converter<String,List<FlowRule>> flowRuleListParser = source -> JSON.parsObject(source,new TypeReference<List<FlowRule>>(){});
     
      private Converter<String,List<SystemRule>> flowRuleListParser = source -> JSON.parsObject(source,new TypeReference<List<SystemRule>>(){});
        
      private Converter<String,List<AuthorityRule>> flowRuleListParser = source -> JSON.parsObject(source,new TypeReference<List<AuthorityRule>>(){});
        
      private Converter<String,List<ParamFlowRule>> flowRuleListParser = source -> JSON.parsObject(source,new TypeReference<List<ParamFlowRule>>(){});
      
      private void mkdirIfNotExits(String filePath) throws IOException{
        File file = new File(filePath);
        if(!file.exists()){
          file.mkdirs();
        }
      }
     
      private void createFileIfNotExits(String filePath) throws IOException{
        File file = new File(filePath);
        if(!file.exists()){
          file.createNewFile();
        }
      }
      
      private void createFileIfNotExits(String filePath) throws IOException{
        File file = new File(fielPath);
        if(!file.exists()){
          file.createNewFile();
        }
      }
       private <T> String encodeJson(T t){
         return JSON.toJSONString(t);
       }
      
    }
    
  2. 新增配置

    在Resources下面建立配置目錄,META-INF/services,然後新增檔案com.alibaba.csp.sentinel.init.InitFunc

    在檔案中新增配置類的全路徑com.itheima.config.FilePersistence

相關文章