SpringCloud进阶--Sentinel 流量防卫兵

Sentinel 流量防卫兵

之前,我们了解到了微服务雪崩问题,就是一个微服务出现问题,有可能导致整个联络直接不可用,这时候就需要进行即使的熔断和降级,之前我们使用Hystrix来实现。现在我们使用Sentinel 。

Sentinel 有以下特征:

  • 丰富的应用场景:例如秒杀、消息削峰填谷、集群流量控制、实时熔断下游不可用服务等。
  • 完备的实时监控:Sentinel 提供实时监控功能。
  • 广泛的开源生态:Sentinel 可以与其他开源框架整合。
  • 完善的SPI扩展机制:Sentinel 提供简单易用的SPI接口。可以通过接口快速定制逻辑、规则。

安装与部署

  1. 下载并安装下载地址

下载下来是一个jar包,直接启动这个jar包。端口默认8080,我这里指定了8858端口。

访问地址就是localhost:8858 ; 用户名和密码都是sentinel

  1. 在服务中引入sentinel依赖

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
    
    1. 在配置文件进行配置

      spring:
        cloud:
          sentinel:
            transport:
              # 添加监控页面地址
              dashboard: localhost:8858
      

      然后启动服务,注意,这里要先调用一次服务,sentinel才会加载这个服务(它使用了懒加载机制)

流量控制

我们不能无限制的接收和处理客户端请求,如果不加以限制,当发生高并发情况时,系统资源很快就会被耗尽。

这时可以使用流量控制(限流),当一段时间内的流量达到一定的阈值时,新的请求将不再进行处理。这样能合理应对高并发,也能保护服务器不受外界的攻击。

那么,实现限流的策略有哪些呢?

  1. 快速拒绝:不再接收新请求。直接返回一个拒绝信息,告诉用户访问频率过高。
  2. 预热:基于方案一,但由于某些情况下高并发请求时在某一时刻突然到来,我们可以缓慢地将阈值提高到指定阈值,形成一个缓冲保护
  3. 排队等待:不接受新请求,也不直接拒绝,而是进入排队,要是规定时间内能执行就执行,超时就算了。

针对是否超过流量阈值的判断,有4种算法:

  1. 漏桶算法

无标题

  1. 令牌桶算法

现在有一个令牌桶,这个桶专门存放令牌,每隔一段时间就向桶中丢入一个令牌(速度由我们指定)当新的请求到达时,将从桶中删除令牌,接着请求就可以通过并给到服务,但是如果桶中的令牌数量不足,那么不删除令牌,而是然那个此数据包等待。

当流量下降时,令牌桶中的令牌会逐渐积累,这样如果突然出现高并发,那么就能在短时间内拿到大量的令牌。

无标1题

  1. 固定时间窗口算法:

无标2题

  1. 滑动时间窗口算法

无标3题

具体使用哪种算法和策略可以由我们自己制定。

无标4题

按照上图指示进入流控规则页面。

  • 阈值类型:QPS就是每秒种的请求数量,并发线程数是按服务当前十一月的线程数据进行统计的。
  • 流控模式:当达到阈值时,流控的对象,这里暂时使用直接。
  • 流控效果:对应上面的三种限流策略。

这里我们选择QPS、阈值设为1,流控模式选择直接、流控效果选择快速失败。可以看到当我们快速地进行请求时,会直接返回失败信息。

那这些流控模式有什么区别?

  • 直接:只针对于当前接口
  • 关联:当关联的其他接口超过阈值时,会导致当前接口被限流
  • 链路:更细粒度的限流,能精确到具体的方法。

比如关联模式,我们将/borrow/{uid}和自带的/error接口关联,然后进行限流

无标5题

此时,如果对/error的请求达到阈值时,请求/borrow/{uid}就会被限流,会访问失败!

注意:限流是作用于关联资源的,一旦关联资源超过阈值,那么就会对当前资源进行限流!

那什么是链路流控模式呢?

链路流控模式指的是当从指定接口过来的请求达到限流条件时,开启限流。需要@SentinelResource注解配合使用。

@SentinelResource注解用来标注一个方法。将这个方法纳入限流控制。比如在controller里有2个方法调用被监控的那个方法:

@RequestMapping("/borrow/{uid}")
public BorrowDetail getBorrowById(@PathVariable("uid") int id) {
    return borrowService.findBorrowById(id);
}

@RequestMapping("/borrow2/{uid}")
public BorrowDetail getBorrowById2(@PathVariable("uid") int id) {
    return borrowService.findBorrowById(id);
}
// 限流控制的方法
//监控此方法,无论被谁执行都在监控范围内,这里的value是自定义名称
// 这个注解可以加载任何方法上,包括Controller中的请求映射方法。
@SentinelResource("getBorrow")
@Override
public BorrowDetail findBorrowById(int uid) {
    List<Borrow> allByUid = borrowMapper.getAllByUid(uid);
    // 获取用户信息 localhost:8101 改成服务名user-service
    User user = userClient.findUserById(uid);
    // 获取每本书的详细信息
    List<Book> bookList = allByUid.stream().map(borrow -> bookClient.getBookById(borrow.getBid())).collect(Collectors.toList());
    return new BorrowDetail(user, bookList);
}

然后进行配置:

spring:
  cloud:
    sentinel:
        # 关闭context收敛,这样被监控的方法可以进行不同链路的单独控制
      web-context-unify: false

然后在Sentinel控制台中添加流控规则,注意是针对此方法!

无6标题

这样设置后会发现,无论请求哪个接口,只要调用了被监控的这个方法,都会被限流。注意:这里限流的形式是后台直接抛出异常。

而这个链路选项实际就是决定只限流从哪个方向来的调用,比如要求只限流从borrow2接口对方法的调用。我们就可以指定链路为:

无7标题

然后就会发现,限流效果只对配置的链路接口有效,而其他链路不会被限流。

除了直接对接口使用限流控制外,还可以根据当前系统的资源使用情况。决定是否进行限流:

无标8题

限流和异常处理

现在我们已经实现了限流操作,那么限流状态下的返回结果该怎么修改呢?

  1. 先创建好需要返回的内容,定义一个请求映射:

    @RequestMapping("/blocked")
    JSONObject blocked(){
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("code",403);
        jsonObject.put("success",false);
        jsonObject.put("message","您请求的频率过快,请稍后重试!");
        return jsonObject;
    }
    
  2. 在配置文件中将此页面设定为限流页面:

spring:
  cloud:
    sentinel:
      # 将刚刚编写的请求映射为限流页面
      block-page: /blocked

那么方法级别的限流怎么处理?因为方法被限流会在后台直接抛出异常,这种情况我们该怎么处理呢?

Sentinel可以指定一个替代方案,当出现异常时,会调用替代方案:

在@SentinelResource注解的blockHandler属性指定替代方法即可

@SentinelResource(value = "getBorrow",blockHandler = "blocked")
@Override
public BorrowDetail findBorrowById(int uid) {
    List<Borrow> allByUid = borrowMapper.getAllByUid(uid);
    // 获取用户信息 localhost:8101 改成服务名user-service
    User user = userClient.findUserById(uid);
    // 获取每本书的详细信息
    List<Book> bookList = allByUid.stream().map(borrow -> bookClient.getBookById(borrow.getBid())).collect(Collectors.toList());
    return new BorrowDetail(user, bookList);
}

// 替代方案,参数和返回值必须和原方法一致,并且参数最后需要加一个BlockException类型的参数
public BorrowDetail blocked(int uid, BlockException blockException) {
    return new BorrowDetail(null, Collections.emptyList());
}

注意:这里的blockHandler只能处理限流情况下抛出的异常,如果时方法本身抛出的其他类型的异常,不再管控范围内,但是可以通过其他参数进行处理:

@SentinelResource(value = "getBorrow",
        fallback = "except", // 指定出现异常时的替代方案。"except"是替代方法的方法名
        exceptionsToIgnore = IOException.class) // 忽略哪些异常,也就是说,出现这种异常不使用替代方案
@Override
public BorrowDetail findBorrowById(int uid) {
...
}

这种方式会在没有配置blockHandler的情况下,将限流的异常也一并处理了,如果配置了blockHandler,那么出现限流时,依然只会执行blockHandler指定的替代方案(因为限流是在方法执行之前进行的)。

热点参数限流

我们可以对某一热点数据进行精准限流,比如在某一时刻,不同参数被携带访问的频率时不一样的:

  • http://localhost:8301/borrow?a=10 访问100次
  • http://localhost:8301/borrow?b=10访问0次
  • http://localhost:8301/borrow?c=10访问4次

由于携带参数a的请求比较多,我们就可以只针对携带参数a的请求进行限流

  1. 创建一个请求映射:

    @RequestMapping("/test")
    @SentinelResource("test")
    String findUserBorrow2(@RequestParam(value = "a",required = false) String a,
                           @RequestParam(value = "b",required = false) String b,
                           @RequestParam(value = "c",required = false) String c){
        return "请求成功!a="+a+";b="+b+";c="+c;
    }
    
    1. 进行热点配置

无9标题

  这样就实现了对某个参数进行精准限流。

  除了对某个参数进行精准限流外,还可以对参数携带的指定值单独设定阈值,比如,希望现在不仅对参数a限流,而且还希望当参数a=10 时,QPS达到3时再进行限流,就可以如下设置:

无标10题

服务熔断与降级

无11标题

如果在某一时刻,服务B出现故障,而这时服务A依然有大量请求在调用服务B。由于服务A没办法在短时间内完成处理,新来的请求会导致线程数不断增加,这样,CPU的资源很快就会被耗尽!

要防止这种情况,就只能进行隔离,一共有两种隔离方案:

  1. 线程池隔离

    线程池隔离实际上就是对每个服务的远程调用单独开放线程池,比如服务A调用服务B。只基于固定数量的线程池。这样即使在短时间内出现大量请求,由于没有线程可以分配。所以就不会导致资源耗尽!

无标12题

  1. 信号量隔离

    信号量隔离是使用Semaphore类实现的。其思想基本与上面相同,也是限定指定的线程数量,但它相对于线程池隔离。开销会更小。使用效果相同,也支持超时等,而Sentinel正是采用的这种方案实现隔离的。

    那什么是服务降级?

    当下游服务因为某种原因变得不可用或响应过慢时,上游服务为了保证自己整体服务的可用性,不再继续调用目标服务,而是快速返回或是执行自己的替代方案,这便是服务降级。

    整个过程分为三个状态:

    • 关闭:熔断器不工作,所有请求全部该干嘛干嘛
    • 打开:熔断器工作,所有请求一律降级处理
    • 半打开:尝试进行一下正常流程,要是还不行,就继续保持打开状态,否则关闭

    以下时Sentinel中进行熔断和降级操作:

无13标题

其中,熔断策略有三种模式:

  1. 慢调用比例:这种如果出现那种半天都处理不完的调用,有可能就是服务出现故障,导致卡顿。这个选项是按照最大响应时间(RT)进行判定,如果一次请求的处理时间超过了指定的RT,那么就被判断为慢调用。在一个统计时长内,如果请求数目大于最小请求数目,并且被判断为慢调用的请求比例已超过阈值,将触发熔断。经过熔断时长之后,将会进入到半开状态进行试探(这里和Hystrix一致)! 资源名填写服务地址比如:/borrow/

  2. 异常比例:与慢调用类似,不过这里判断的是出现异常的比例

  3. 异常数:只要达到指定的异常数量,就熔断

那在Sentinel中如何自定义服务降级?

只需要在@SentinelResource()中配置blockHandler参数(其实和处理方法级别的限流异常一样)

@RequestMapping("/borrow2/{uid}")
@SentinelResource(value = "getBorrowById2",blockHandler = "test")
public BorrowDetail getBorrowById2(@PathVariable("uid") int id) {
    return borrowService.findBorrowById(id);
}

BorrowDetail test(int uid, BlockException blockException){
    return new BorrowDetail(null, Collections.emptyList());
}

这样设置好,注意添加熔断规则时,资源名填写的是getBorrowById2。

这样在熔断后就不会返回到限流页面,而是返回替代方案!

如何让Feign也支持Sentinel的服务降级?

  1. 现在配置中开启支持:

    feign:
      sentinel:
        enabled: true
    
  2. 创建UserClient接口

    @FeignClient(value = "user-service",fallback = UserClientImpl.class)
    public interface UserClient {
    
        @RequestMapping("/user/{uid}")
        User findUserById(@PathVariable("uid") int uid);
    }
    
    1. 创建UserClient接口的实现类:

      @Component
      public class UserClientImpl implements UserClient {
          @Override
          public User findUserById(int uid) {
              User user = new User();
              user.setName("我是替代方案");
              return user;
          }
      }
      

这样就让Feign实现了服务降级

如何让传统的RestTemplate也实现服务降级呢?

可以使用 @SentinelRestTemplate注解实现!

@Configuration
// 指定为user-service服务,只要调用此服务,就会使用我们指定的策略
//configuration = LoadBanancerConfig.class 指定我们自定义的策略类
// @LoadBalancerClient(value = "user-service",configuration = LoadBanancerConfig.class)
public class BeanConfiguration {

    @Bean
    // 负载均衡
    @LoadBalanced
    @SentinelRestTemplate(blockHandler = "handleException", blockHandlerClass = ExceptionUtil.class,
            fallback = "fallback",fallbackClass = ExceptionUtil.class)
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
posted @ 2026-03-24 11:24  NE_STOP  阅读(14)  评论(0)    收藏  举报