SpringCloud04:Sentinel

基本介绍

功能介绍

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Spring Cloud Alibaba Sentinel 以流量为切入点,从流量控制流量路由熔断降级系统自适应过载保护热点流量防护等多个维度保护服务的稳定性

Sentinel 提供开箱即⽤的与其它开源框架/库的整合模块,例如与 Spring Cloud、Apache Dubbo、gRPC、Quarkus 的整合。您只需要引⼊相应的依赖并进⾏简单的配置即可快速地接⼊ Sentinel。同时 Sentinel 提供 Java/Go/C++ 等多语⾔的原⽣实现。

架构原理

一个应用会有多个微服务,每一个微服务会有众多资源。我们只需要引入Sentinel Client,Sentinel Client 会连接Sentinel Dashboard(Sentinel控制台)。线上的运维人员(Ops)可以在Sentinel Dashboard上面定义各种规则。定义的规则可以存储在配置中心当中(Naco,Zookeeper)或者本地内存(Memory)中。接着Sentinel Dashboard会把规则推送给每一个微服务,发生变更了也会推送。这样,当有请求去访问资源的时候,Sentinel Client会去检查该访问是否符合规则,如果符合规则就放行,如果不符合规则就拒绝服务。

定义资源:

  • 所有Web接口均为资源
  • 编程式:SphU API(自定义一段代码成为受保护的资源)
  • 声明式:@SentinelResource(自定义一段代码成为受保护的资源)

定义规则:

  • 流量控制(FlowRule)
  • 熔断降级(DegradeRule)
  • 系统保护(SystemRule)
  • 来源访问控制(AuthorityRule)
  • 热点参数(ParamFlowRule)

工作原理

用户访问一个资源(如发送Http请求去访问订单接口)。这个资源受到一个规则的保护(Sentinel制订)。每一次访问,Sentinel都会去检查该次访问是否违反了这个规则(比如规则是1秒的访问量是1次)。如果没有违反,就放行。如果违反了规则就会抛出异常。如果没有到底处理,就返回默认错误。如果有到底处理,就执行fallback。

整合使用

启动Sentinel Dashboard

下载地址:https://github.com/alibaba/Sentinel/releases/tag/1.8.8

启动命令:java -jar sentinel-dashboard-1.8.8.jar

后台启动:nohup java -jar sentinel-dashboard-1.8.8.jar > sentinel.log 2>&1 &

  • nohup:忽略挂断信号,确保终端关闭后进程仍运行
  • > sentinel.log:将标准输出重定向到日志文件
  • 2>&1:将错误输出合并到标准输出
  • &:后台运行

验证是否启动:ps aux | grep sentinel-dashboard

关闭Sentinel:针对使用 nohup 启动

# 查找后台任务
jobs -l
# 终止任务(假设任务号为1)
kill -15 %1

登录Sentinel Dashboard:http://localhost:8080/

  • 用户名: sentinel
  • 密码:sentinel

微服务整合Sentinel

  1. 引入依赖

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
    
  2. 配置Sentinel Dashboard的地址,让微服务可以连接上控制台

    spring:
      cloud:
        sentinel:
          transport:
            dashboard: localhost:8080
          eager: true
    
    spring.cloud.sentinel.transport.dashboard=localhost:8080
    # eager=true 表示在应用启动时就加载 Sentinel 规则
    # 因为 Sentinel 是懒加载机制,意味着第一次访问的时候才会加载规则
    spring.cloud.sentinel.eager=true
    
  3. 启动微服务,让微服务连接Sentinel Dashboard

  4. 访问/create就可以在Sentinel里面看到资源

    Sentinel认为GET:http://service-product/product/{id}这个远程调用也算一个资源

  5. 配置规则:流控规则-每秒访问量为1个请求

    如果违反这个规则,就会报错:

异常处理

当访问违背规则之后,系统就会抛出一个BlockException异常。

public abstract class BlockException extends Exception {
  ......
}

但是BlockException是一个总的异常,他的下面还要5个实现:

上面5个异常分别对应五个规则:

  • 流量控制(FlowRule)

  • 熔断降级(DegradeRule)

  • 系统保护(SystemRule)

  • 来源访问控制(AuthorityRule)

  • 热点参数(ParamFlowRule)

每种异常,又因为不同的资源类型有着不同的处理方式。

Web接口的异常处理

默认的异常处理器

对于Web接口(如/create),Sentinel 使用的是SentinelWebInterceptor机制。SentinelWebInterceptor 是Spring MVC里面的一个Web拦截器。

public class SentinelWebInterceptor extends AbstractSentinelInterceptor {

这个拦截器实现了HandlerInterceptor接口。

public abstract class AbstractSentinelInterceptor implements HandlerInterceptor {

在请求到达资源之前,会执行preHandle方法

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    String resourceName = "";
  	......
    // 获取资源名字
		resourceName = this.getResourceName(request);
  	......
    // 检查是否符合规则,如果不符合规则会抛出异常:BlockException
    Entry entry = SphU.entry(resourceName, 1, EntryType.IN);
  	......
    // 符合规则,返回true
    return true;
  	// 捕获异常
    catch (BlockException var12) {
          BlockException e = var12;

          try {
              // 调用handleBlockException方法处理异常
              this.handleBlockException(request, response, resourceName, e);
          } finally {
              ContextUtil.exit();
          }

          return false;
    }
}

捕获异常之后会调用handleBlockException方法处理异常:让blockExceptionHandler去处理异常。她是一个默认的异常处理器DefaultBlockExceptionHandler

protected void handleBlockException(HttpServletRequest request, HttpServletResponse response, String resourceName, BlockException e) throws Exception {
    if (this.baseWebMvcConfig.getBlockExceptionHandler() != null) {
        this.baseWebMvcConfig.getBlockExceptionHandler().handle(request, response, resourceName, e);
    } else {
        throw e;
    }
}

public BlockExceptionHandler getBlockExceptionHandler() {
    return this.blockExceptionHandler;
}
protected BlockExceptionHandler blockExceptionHandler = new DefaultBlockExceptionHandler();

这个处理器只负责打印Blocked by Sentinel (flow limiting)

public class DefaultBlockExceptionHandler implements BlockExceptionHandler {
    public DefaultBlockExceptionHandler() {
    }

    public void handle(HttpServletRequest request, HttpServletResponse response, String resourceName, BlockException ex) throws Exception {
        response.setStatus(429);
        PrintWriter out = response.getWriter();
        out.print("Blocked by Sentinel (flow limiting)");
        out.flush();
        out.close();
    }
}

自定义BlockExceptionHandler

  1. 实现BlockExceptionHandler接口
  2. @Component :将该类注册为Spring Bean,放入Spring容器中
@Component // 将该类注册为Spring Bean,放入Spring容器中
public class MyBlockExceptionHandler implements BlockExceptionHandler {
    private ObjectMapper objectMapper = new ObjectMapper();
    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       String resourceName,
                       BlockException e) throws Exception {
        // HttpServletResponse response是用来设置响应的状态码和内容类型等信息的
        response.setStatus(429); //too many requests
        response.setContentType("application/json;charset=utf-8");
        // response.getWriter()获取一个PrintWriter对象,用于向客户端发送响应数据
        PrintWriter writer = response.getWriter();
        // 封装错误信息
        R error = R.error(500, resourceName + " 被Sentinel限制了,原因:" + e.getClass());
        // 将错误信息转换为JSON字符串
        String json = objectMapper.writeValueAsString(error);
        // 将JSON字符串写入响应体
        writer.write(json);
        // 刷新并关闭PrintWriter
        writer.flush();
        // 关闭PrintWriter
        writer.close();
    }
}

以下是R的内容:

@Data
public class R {

    private Integer code;
    private String msg;
    private Object data;


    public static R ok() {
        R r = new R();
        r.setCode(200);
        return r;
    }

    public static R ok(String msg,Object data) {
        R r = new R();
        r.setCode(200);
        r.setMsg(msg);
        r.setData(data);
        return r;
    }

    public static R error() {
        R r = new R();
        r.setCode(500);
        return r;
    }

    public static R error(Integer code,String msg){
        R r = new R();
        r.setCode(code);
        r.setMsg(msg);
        return r;
    }
}

@SentinelResource标注的异常处理

默认的异常处理

  1. 我们在Controller的createOrder方法上添加@SentinelResource。资源名为create-order

    @SentinelResource:主要标注在非Controller层。

    @SentinelResource(value = "create-order")
    @Override
    public Order createOrder(Long productId, Long userId) {
        Product product = productFeignClient.getProductById(productId);
        Order order = new Order();
        order.setId(1L);
        // 总金额
        order.setTotalAmount(product.getPrice().multiply(new BigDecimal(product.getNum())));
        order.setUserId(userId);
        order.setNickName("zhangsan");
        order.setAddress("beijing");
        // 目前只有一个商品
        order.setProductList(Arrays.asList(product));
    
        return order;
    }
    
  2. 添加资源名为create-order的流控规则

  3. 如果这次失败之后,会出现一个默认达到错误页面

  4. 为了应对@SentinelResource标注的资源,Sentinel有一个切面SentinelResourceAspect来处理异常。

    @Aspect
    public class SentinelResourceAspect extends AbstractSentinelAspectSupport {
        public SentinelResourceAspect() {
        }
            // 切入点表达式:所有标注SentinelResource的注解,都会执行这个切面
        @Pointcut("@annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)")
        public void sentinelResourceAnnotationPointcut() {
        }
    
        @Around("sentinelResourceAnnotationPointcut()")
        public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable {
            Method originMethod = this.resolveMethod(pjp);
            // 先获取@SentinelResource的注解
            SentinelResource annotation = (SentinelResource)originMethod
                                          .getAnnotation(SentinelResource.class);
              ......
                    // 拿到资源名:create-order
            String resourceName = this.getResourceName(annotation.value(), originMethod);
              ......
              // 检查规则, 如果违反规则会抛出异常: BlockException
            entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs());
              // 如果符合回归,就会继续处理业务
              Object var8 = pjp.proceed();
            return var8;
              ......
            // 异常会被捕获
               catch (BlockException var15) {
                  /// 由handleBlockException去处理异常
                Object var18 = this.handleBlockException(pjp, annotation, var15);
                return var18;
            } 
            ......
        }
    }
    
  5. handleBlockException:如果知道了方法处理,就用指定的。没有没有就用默认的。

    protected Object handleBlockException(ProceedingJoinPoint pjp, 
                                          SentinelResource annotation, 
                                          BlockException ex) throws Throwable {
       // 从注解里面获取:blockHandlerMethod方法
        Method blockHandlerMethod 
          = this.extractBlockHandlerMethod(pjp, 
                                           annotation.blockHandler(), 
                                           annotation.blockHandlerClass());
          // 如果制定了,就按指代的方法this.invoke(pjp, blockHandlerMethod, args) 
        if (blockHandlerMethod != null) {
            Object[] originArgs = pjp.getArgs();
            Object[] args = Arrays.copyOf(originArgs, originArgs.length + 1);
            args[args.length - 1] = ex;
            return this.invoke(pjp, blockHandlerMethod, args);
        } else {
              // 没有指定,就按默认的方法
            return this.handleFallback(pjp, annotation, ex);
        }
    }
    
  6. handleFallback

    protected Object handleFallback(ProceedingJoinPoint pjp, 
                                    SentinelResource annotation, 
                                    Throwable ex) throws Throwable {
        return this.handleFallback(pjp, annotation.fallback(), 
                                   annotation.defaultFallback(), 
                                   annotation.fallbackClass(), ex);
    }
    
    protected Object handleFallback(ProceedingJoinPoint pjp, String fallback, 
                                    String defaultFallback, Class<?>[] fallbackClass, 
                                    Throwable ex) throws Throwable {
        Object[] originArgs = pjp.getArgs();
          // 获取fallbackMethod方法
        Method fallbackMethod = this.extractFallbackMethod(pjp, fallback, fallbackClass);
          // 如果指定了fallbackMethod,就要指定的fallbackMethod处理
        if (fallbackMethod != null) {
            int paramCount = fallbackMethod.getParameterTypes().length;
            Object[] args;
            if (paramCount == originArgs.length) {
                args = originArgs;
            } else {
                args = Arrays.copyOf(originArgs, originArgs.length + 1);
                args[args.length - 1] = ex;
            }
    
            return this.invoke(pjp, fallbackMethod, args);
        } else {
              // 如果没有,就安装默认的 handleDefaultFallback 处理
            return this.handleDefaultFallback(pjp, defaultFallback, fallbackClass, ex);
        }
    }
    

自定义异常处理器

  1. 指定blockHandler

    @SentinelResource(value = "create-order",
                      blockHandler = "createOrderFallback") // 指定blockHandler
    @Override
    public Order createOrder(Long productId, Long userId) {
        // 远程调用商品服务,获取商品信息
        Product product = productFeignClient.getProductById(productId);
        Order order = new Order();
        order.setId(1L);
        // 总金额
        order.setTotalAmount(product.getPrice().multiply(new BigDecimal(product.getNum())));
        order.setUserId(userId);
        order.setNickName("zhangsan");
        order.setAddress("beijing");
        // 目前只有一个商品
        order.setProductList(Arrays.asList(product));
    
        return order;
    }
    
  2. 编写createOrderFallback方法

    //兜底回调
    public Order createOrderFallback(Long productId, Long userId,
                                     BlockException e){
        Order order = new Order();
        order.setId(0L);
        order.setTotalAmount(new BigDecimal("0"));
        order.setUserId(userId);
        order.setNickName("未知用户");
        order.setAddress("异常信息:"+e.getClass());
        return order;
    }
    
  3. 测试:

blockHandlerfallback 的区别

名称 触发时机 原因 常用于
blockHandler 被 Sentinel 限流/熔断/系统保护 拦截 请求过多、服务过载等 流控、熔断保护
fallback 方法内部抛出异常 业务异常、远程服务调用失败等 业务容错、降级处理

blockHandler 要求

方法签名要求:

  1. 参数要和原方法一致,可多一个 BlockException 参数(通常放在最后)
  2. 必须是 public
  3. 必须和原方法在同一个类,或者指定 blockHandlerClass
  4. 返回值类型必须和原方法一致

blockHandler 写在本类中

@SentinelResource(value = "helloResource", blockHandler = "handleBlock")
public String hello(String name) {
    return "Hello, " + name;
}

// 限流降级处理方法(参数一致 + BlockException)
public String handleBlock(String name, BlockException ex) {
    return "Request blocked for: " + name;
}

blockHandler 写在外部类中

@SentinelResource(value = "helloResource", blockHandler = "handleBlock", blockHandlerClass = BlockHandlerClass.class)
public String hello(String name) {
    return "Hello, " + name;
}

// 外部类
public class BlockHandlerClass {
    public static String handleBlock(String name, BlockException ex) {
        return "Blocked from BlockHandlerClass: " + name;
    }
}

fallback 要求

方法签名要求

  1. 参数要和原方法一致
  2. 可以多一个 Throwable 类型参数(通常最后一个)
  3. 必须是 public
  4. 必须和原方法在同一个类,或者通过 fallbackClass 指定
  5. 返回值类型必须和原方法一致

fallback 写在本类中

@SentinelResource(value = "helloFallback", fallback = "fallbackMethod")
public String mayThrow(String name) {
    if (name.equals("fail")) {
        throw new RuntimeException("Oops!");
    }
    return "OK: " + name;
}

// fallback 处理方法
public String fallbackMethod(String name, Throwable ex) {
    return "Fallback: " + name;
}

fallback 写在外部类中

@SentinelResource(value = "helloFallback", fallback = "fallbackMethod", fallbackClass = MyFallbackHandler.class)
public String mayThrow(String name) {
    // ...
}

// 外部类
public class MyFallbackHandler {
    public static String fallbackMethod(String name, Throwable ex) {
        return "Handled by fallbackClass: " + name;
    }
}

OpenFeign调用的异常处理

根据@FeignClient指定的fallback处理

  1. 根据上面的配置,目前监控的点有3个

  2. 给OpenFeign调用配置流量监控

  3. 查看异常的处理的情况

  4. 这是因为在FeignClient上指定了fallback = ProductFeignClientFallback.class

    @FeignClient(value = "service-product" ,
            configuration = ProductFeignConfig.class,
            fallback = ProductFeignClientFallback.class)
    public interface ProductFeignClient {
    

原理

Sentinel有一个自动配置类SentinelFeignAutoConfiguration,里面配置了Sentinel和OpenFeign的整合相关的配置。里面使用了@Bean创建了一个Feign.Builder

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnClass({SphU.class, Feign.class})
public class SentinelFeignAutoConfiguration {
    public SentinelFeignAutoConfiguration() {
    }

    @Bean
    @Scope("prototype")
    @ConditionalOnMissingBean
    @ConditionalOnProperty(
        name = {"feign.sentinel.enabled"}
    )
    public Feign.Builder feignSentinelBuilder() {
        return SentinelFeign.builder();
    }
}

SentinelFeign.builder()里面:在internalBuild()里面

FeignClientFactoryBean feignClientFactoryBean;
if (isLazyInit) {
  	// 所有FeignClient的创建需要有一个FeignClientFactoryBean,用于创建FeignClient
    feignClientFactoryBean = (FeignClientFactoryBean)def
      													.getAttribute("feignClientsRegistrarFactoryBean");
} else {
    feignClientFactoryBean = (FeignClientFactoryBean)Builder.this
      													.applicationContext.getBean("&" + target.type().getName());
}

FeignClientFactoryBean在创建客户端的时候,会找到fallback的回调。

  Class fallback = feignClientFactoryBean.getFallback();
  Class fallbackFactory = feignClientFactoryBean.getFallbackFactory();
  String beanName = feignClientFactoryBean.getContextId();

如果fallback不为空,就包装SentinelInvocationHandler。后面如果OpenFeign违反Sentinel的规则,就会通过SentinelInvocationHandler来处理异常,如果有fallback就会去执行fallback指定的兜底方法。

if (Void.TYPE != fallback) {
  	// 如果fallback不为空,就创建fallbackInstance
    Object fallbackInstance = this.getFromContext(beanName, "fallback", 
                                                  fallback, target.type());
  	// 把fallbackInstance包装成SentinelInvocationHandler
    return new SentinelInvocationHandler(target, dispatch, 
                                         new FallbackFactory.Default(fallbackInstance));
} else if (Void.TYPE != fallbackFactory) {
    FallbackFactory fallbackFactoryInstance 
      			= (FallbackFactory)this.getFromContext(beanName, "fallbackFactory", 
                                                   fallbackFactory, FallbackFactory.class);
    return new SentinelInvocationHandler(target, dispatch, fallbackFactoryInstance);
} else {
    return new SentinelInvocationHandler(target, dispatch);
}

流控规则

为什么流控可以保护系统

如果有大量请求访问APP,如果有Sentinel限流,比如每秒50个请求,那么多余的请求就会被丢弃,从而保护了系统资源不会被耗尽。

有哪些配置:

阈值类型

QPS:统计每秒请求数。底层使用的是一个计数器来统计请求数量,所以非常轻量化。(推荐

并发线程数:统计并发线程数。与QPS一样,统计的是每秒的请求数。但是是统计的是线程数,需要引入线程池,性能比较低下。

是否集群

在集群方式下统计有两种统计方式:

  • 单机均摊:如果单击均摊的阈值为1,如果有3个微服务,那么每个微服务的QPS是1
  • 总体阈值:如果是总体阈值的阈值为1,如果有3个微服务,那么这3个微服务合计的每秒访问的最大请求数是1。也就是,如果3个微服务,每秒有1次访问,对于集群来说,就是每秒3次访问,那么这就违反了规则。

流控模式

直接

所谓直接策略,就是对资源的访问进行直接限制(规则)。这是默认的策略。

链路

什么是链路策略

假设资源B是我们用@SentinelResource标注的创建订单的方法。如果我们资源B进行限流,每秒2个,但是有一个条件。如果是普通创建订单,也就是被资源A调用,可以不遵循这个限制规则。如果是秒杀创建的订单,也就是被资源C调用,就必须受到每秒2次访问的限制。这种策略称之为链路策略。

如何配置

  1. 再写一个方法m模拟资源C

    @GetMapping("/seckill")
    public Order seckill(@RequestParam(value = "userId",required = false) Long userId,
                             @RequestParam(value = "productId",defaultValue = "1000") Long productId){
        Order order = orderService.createOrder(productId, userId);
        order.setId(Long.MAX_VALUE);
        return order;
    }
    
  2. 配置:不再统一上下文

    spring:
      cloud:
        sentinel:
          web-context-unify: false
    

    配置项 spring.cloud.sentinel.web-context-unify: true 的作用是 统一 Web 上下文(Web Context)的处理。如下图就是统一上下文的效果。

    所有 HTTP 请求(如 /user/{id}/order/{id})会被统一统计为一个资源名(如 GET:/user/{id}),忽略路径中的动态部分(如 {id})。

    • 优点:避免因路径参数不同导致资源数量爆炸(如 /user/1/user/2 被视为同一资源)。
    • 缺点:无法针对特定路径参数设置细粒度流控。

    web-context-unify: false时,每个具体路径(如 /user/1/user/2)会被视为不同的资源。

    • 优点:支持更精确的流控(如限制 /user/1 但不限制 /user/2)。
    • 缺点:可能导致资源数量过多,增加内存和统计开销。

    以下是web-context-unify: false的效果:

  3. 配置:填写入口资源

关联

什么是关联

我又一个读数据库的请求,也有一个写数据库的请求。假设他们操作同一个数据库(比如读写订单),且操作是有关联的。如果读和写订单的请求都比较大,但是我们希望实现有先写的效果,也就是对读进行限流,但是写不受限制。这就称之为关联策略。

由于读写时相关的,只有在写的请求量非常大的情况下,读的限流才会触发。 如果没有写或者写的请求量非常小,随便读

怎么实现

  1. 模拟两个读写的方法

    @GetMapping("/writeDb")
    public String writeDb(){
        return "writeDb success....";
    }
    
    @GetMapping("/readDb")
    public String readDb(){
        log.info("readDb...");
        return "readDb success....";
    }
    
  2. 配置限流规则

  3. 实验:

    上面的配置,似乎是对readDb进行限制,可是如果writeDb没有访问量,readDb是不受任何限制的。只有疯狂访问writeDb之后,再去访问readDb,这个时候readDb才会被限制。

流控效果

快速失败

当流量超过QPS的限制数的时候,就会直接拒绝剩余的请求,抛出一个BlockException异常。

Warm up

对Warm up 的设置有两个。第一个是QPS(每秒通过几个请求),第二个是Period(周期,冷启动周期是几秒)。假设QPS=3,Period=3,那么大流量开始进入的时候,会从1/3的位置开始启动,也就是第一秒放一个请求进来。第二秒,逐步增加,放2个请求进入,第三秒3个请求。也就是到达峰值QPS=3需要经过3秒的时间。

Warm up 模式的QPS通过数的图:

如果做如下配置:

做压测的时候的效果:QPS通过数是逐步上升的

排队等待

在匀速排队下,QPS=2 表示每秒2个请求,则每500ms接受一个请求。多余的请求会不会马上丢弃,会让他们排队等待。但是等待时间不能超过timeout,如果timeout=20s,表如果等待超过20秒就会被丢弃。

如做如下配置:

熔断降级

基本介绍

什么叫熔断降级

在一个大型系统中,服务间的调用是非常复杂的,比如ServiceA 调用ServiceG 也调用ServiceF,而Service G 和 Service F都调用Service D。 这个是如果Service D 出现问题,比如总是超时。那么如果Service G 和 Service F 一直等待 Service D,可能会影响整体的调用时间,以及影响后面的调用。

这个时候我们需要想办法切断不稳定调用。我们希望,如果Service G 调用Service D之后总是失败,那么Service G就不在调用Service D了,自己返回一个失败,这样Service A就不需要等很久。这样可以做到快速返回不积压请求。只要请求不积压,就可以避免服务雪崩

我们把这种思想称为熔断降级

最佳实践:熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。

断路器

在熔断降级中有一个核心组件叫做断路器

断路器可以想象成一个开关。

比如Service A 想要调用 Service B。

如果Sservice B 是稳定的通畅的,那么A调用Service B就可以通过。这个时候,这个断路器是闭合状态(closed)。

如果B服务突然不稳定,那么断路器可以打开,这个时候A就不需要去调用B,A看到断路器是打开的,就会自动得到一个错误返回。

但是B服务某天会恢复,那么断路器怎么知道B服务恢复了呢?断路器还要一个状态叫做半开状态。在这个状态下,会先放一个请求去B服务,如果正常返回,就说明B服务是正常的了

断路器的工作原理

在Sentinel里面有3种熔断策略:

  • 慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间)请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。
  • 异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。
  • 异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。

以慢调用比例作为熔断策略。所有的请求进来,如果我们设置statIntervalMs=5minRequestAmount=5,那么就是在5秒内至少发5个请求以上断路器才会开始统计。假设这5秒内,A对B调用了100次,但是有70%都是慢调用(设置最大的响应时间=1,那么超过1秒没返回的都是慢调用)。如果设置的慢调用比例是30%,那么就超过了慢调用的阈值,那么断路器就会打开(Open)。这样A就不会调用B了。但是断路器也不能一直打开。因此断路器有一个熔断时长(timeWindow),比如timeWindow=30s,那么30s以内,所有A对B的调用,A都会自动失败。30秒以后,断路器就会进入半开(Half-Open)状态。在半开状态下,A调用B,断路器会放一个请求去探测。如果这个请求成功了,说明现在B正常了,那么断路器会进入关闭状态(Closed),所有请求都可以正常访问B。但是如果探测失败了,那么断路器又会进入**打开(Open)**状态。

有熔断降级和无熔断降级Fallback的区别

无熔断降级,A服务调用B服务的时候,无论是遇到超时还是错误,都会去调用一次B服务,然后才能触发Fallback。

有熔断降级,A就不会去调用B,直接触发Fallback。

慢调用比例

异常比例

异常数

热点(参数限流)规则

什么是热点(参数限流)规则

何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:

  • 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
  • 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制

热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效

比如一个请求resA进来,带了三个参数:axb、abc和xs。如果添加了热点限流规则,比如QPS=5,那么如果axb参数的QPS=5,而且是热点数据,那么就会被blocked。但是必然xs,不是热点参数,那么计算QPS=10,也会被通过。

基本使用

使用@SentinelResource标记资源

@GetMapping("/seckill")
// @SentinelResource 注解用于定义 Sentinel 资源,value 属性指定资源名称
@SentinelResource(value = "seckill-order",fallback = "seckillFallback")
// required = false 表示该参数不是必需的,如果不传递该参数,则默认为 null
// defaultValue = "1000" 表示如果没有传递 productId 参数,则默认为 1000
public Order seckill(@RequestParam(value = "userId",required = false) Long userId,
                         @RequestParam(value = "productId",defaultValue = "1000") Long productId){
    Order order = orderService.createOrder(productId, userId);
    order.setId(Long.MAX_VALUE);
    return order;
}

注意:目前 Sentinel 自带的 adapter 仅 Dubbo ,方法埋点带了热点参数,其它适配模块(如 Web)默认不支持热点规则,可通过自定义埋点方式指(@SentinelResource)定新的资源名,并传入希望的参数。注意自定义埋点的资源名不要和适配模块生成的资源名重复否则会导致重复统计。

编写fallback方法

public Order seckillFallback(Long userId,Long productId, Throwable exception){
    System.out.println("seckillFallback....");
    Order order = new Order();
    order.setId(productId);
    order.setUserId(userId);
    order.setAddress("异常信息:"+exception.getClass());
    return order;
}

对用户id进行限流

需求1:每个用户秒杀 QPS 不得超过 1(秒杀下单 userId 级别)

以秒杀为例,我们要求每个用户在秒杀的时候QPS不能超过1个,防止用户利用脚本工具进行秒杀我们产品(比如茅台)。对于方法来说,肯定是会上传一个userId,我们只需要对userId进行限制既可以。

这个时候如果用户上传了userId,那么如果每秒超过1个请求,就会报错误,走fallback兜底函数。

但是如果用户不带userId,就不会触发热点规则:

排除某个userId

需求2:6号用户是vvip,不限制QPS(例外情况)

虽然我们说,每个用户的QPS不能超过1。但是6号用户是我们商城中的vvip,不限制他的QPS。

要找到热点规则,然后点击编辑

点击高级选项就可以看到如下配置页面:

限制第二个参数

需求3:666号是下架商品,不允许访问

如果带的商品ID,且ID是666,那么我们就限制访问流量。

首先,我们对于所有productId不做任何限制

只对productId=666的情况禁止访问:


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1909773034@qq.com

×

喜欢就点赞,疼爱就打赏