SpringCloudAlibaba 微服务讲解(四)Sentinel--服务容错(二)
阅读原文时间:2022年04月02日阅读:1

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;
}

在定义了资源点之后,我们可以通过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 "接口发生异常了。。。。";
  }
}

通过前面的讲解,我们已经知道,可以通过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&lt;String,List&lt;FlowRule&gt;&gt; flowRuleRDS = new FileRefresableDataSource&lt;&gt;(
    flowRulePath,flowRuleListParser);
    FlowRuleManager.register2Property(flowRuleRDS.getProperty());
    WritableDataSource&lt;List&lt;FlowRule&gt;&gt; flowRulesWDS = new FileWritableDataSource&lt;&gt;(flowRulePath,this::encodeJson);
    WritableDataSourceRegistry.registerFlowDataSource(flowRuleWDS);
    
    // 降级规则
    ReadableDataSource&lt;String,List&lt;DegradeRule&gt;&gt; degradeRuleRDS = new FileRefreshableDataSource&lt;&gt;(
    degradeRulePath,degradeRuleListParser);
    DegradeRuleManager.register2Poperty(degradeRuleRDS.getProperty());
    WritableDataSource&lt;List&lt;DegradeRule&gt;&gt; degradeRuleWDS = new FileWritableDataSource&lt;&gt;(degradeRulePath,this::encodeJson);
    
    // 系统规则
    ReadableDataSource&lt;String,List&lt;SystemRule&gt;&gt; systemRuleRDS = new FileRefreshableDataSource&lt;&gt;                                (systemRulePath,systemRuleListParser);
    SystemRuleManager.register2Property(systemRuleRds.getProperty());
    WritableDataSource&lt;List&lt;SystemRule&gt;&gt; systemRuleWDS = new FileWritableDataSource(systemnRulePath,this::encodeJson);
    WritableDataSourceRegistry.registerSystemDataSource(systemRuleWDS);
    
    // 授权规则
    ReadableDataSource&lt;String,List&lt;AuthorityRule&gt;&gt; authorityRuleRDS = new FileRefreshableDataSource&lt;&gt;(authorityRulePath,authorityRuleListParser);
    AuthorityRuleManager.register2Property(authorityRuleRDS.getProperty());
    WritableDataSource&lt;List&lt;AuthorityRule&gt;&gt; authorityRuleWDS = new FileWritableDataSource&lt;&gt;(authorityRulePath,this::encodeJson);
    WiritableDataSourceRegistry.registerAuthorityDataSource(authorityRuleWDS);
    
    // 热点参数规则
    ReadbaleDataSource&lt;String,List&lt;ParamFlowRule&gt;&gt; paramFlowRuleRDS = new FileRefreshableDataSource&lt;&gt;(paramFlowRulePath,paramFlowRuleListParaser);
    ParamFlowRuleManager.register2Property(paramFlowRuleRDS.getProperty());
    WritableDataSouece&lt;List&lt;ParamFlowRule&gt;&gt; paramFlowRuleWDS = new FileWirtableDataSouece&lt;&gt;(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