找回密码
 立即注册
首页 业界区 业界 SpringCloud进阶--Sentinel 流量防卫兵

SpringCloud进阶--Sentinel 流量防卫兵

昝沛珊 3 天前
Sentinel 流量防卫兵

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

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


  • 下载并安装下载地址
下载下来是一个jar包,直接启动这个jar包。端口默认8080,我这里指定了8858端口。
访问地址就是localhost:8858 ;  用户名和密码都是sentinel

  • 在服务中引入sentinel依赖
    1. <dependency>
    2.     <groupId>com.alibaba.cloud</groupId>
    3.     spring-cloud-starter-alibaba-sentinel</artifactId>
    4. </dependency>
    复制代码

    • 在配置文件进行配置
      1. spring:
      2.   cloud:
      3.     sentinel:
      4.       transport:
      5.         # 添加监控页面地址
      6.         dashboard: localhost:8858
      复制代码
      然后启动服务,注意,这里要先调用一次服务,sentinel才会加载这个服务(它使用了懒加载机制)

流量控制

我们不能无限制的接收和处理客户端请求,如果不加以限制,当发生高并发情况时,系统资源很快就会被耗尽。
这时可以使用流量控制(限流),当一段时间内的流量达到一定的阈值时,新的请求将不再进行处理。这样能合理应对高并发,也能保护服务器不受外界的攻击。
那么,实现限流的策略有哪些呢?

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

  • 漏桶算法
1.png


  • 令牌桶算法
现在有一个令牌桶,这个桶专门存放令牌,每隔一段时间就向桶中丢入一个令牌(速度由我们指定)当新的请求到达时,将从桶中删除令牌,接着请求就可以通过并给到服务,但是如果桶中的令牌数量不足,那么不删除令牌,而是然那个此数据包等待。
当流量下降时,令牌桶中的令牌会逐渐积累,这样如果突然出现高并发,那么就能在短时间内拿到大量的令牌。
2.png


  • 固定时间窗口算法:
3.png


  • 滑动时间窗口算法
4.png

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

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

  • 阈值类型:QPS就是每秒种的请求数量,并发线程数是按服务当前十一月的线程数据进行统计的。
  • 流控模式:当达到阈值时,流控的对象,这里暂时使用直接。
  • 流控效果:对应上面的三种限流策略。
这里我们选择QPS、阈值设为1,流控模式选择直接、流控效果选择快速失败。可以看到当我们快速地进行请求时,会直接返回失败信息。
那这些流控模式有什么区别?

  • 直接:只针对于当前接口
  • 关联:当关联的其他接口超过阈值时,会导致当前接口被限流
  • 链路:更细粒度的限流,能精确到具体的方法。
比如关联模式,我们将/borrow/{uid}和自带的/error接口关联,然后进行限流
6.png

此时,如果对/error的请求达到阈值时,请求/borrow/{uid}就会被限流,会访问失败!
注意:限流是作用于关联资源的,一旦关联资源超过阈值,那么就会对当前资源进行限流!
那什么是链路流控模式呢?
链路流控模式指的是当从指定接口过来的请求达到限流条件时,开启限流。需要@SentinelResource注解配合使用。
@SentinelResource注解用来标注一个方法。将这个方法纳入限流控制。比如在controller里有2个方法调用被监控的那个方法:
  1. @RequestMapping("/borrow/{uid}")
  2. public BorrowDetail getBorrowById(@PathVariable("uid") int id) {
  3.     return borrowService.findBorrowById(id);
  4. }
  5. @RequestMapping("/borrow2/{uid}")
  6. public BorrowDetail getBorrowById2(@PathVariable("uid") int id) {
  7.     return borrowService.findBorrowById(id);
  8. }
复制代码
  1. // 限流控制的方法
  2. //监控此方法,无论被谁执行都在监控范围内,这里的value是自定义名称
  3. // 这个注解可以加载任何方法上,包括Controller中的请求映射方法。
  4. @SentinelResource("getBorrow")
  5. @Override
  6. public BorrowDetail findBorrowById(int uid) {
  7.     List<Borrow> allByUid = borrowMapper.getAllByUid(uid);
  8.     // 获取用户信息 localhost:8101 改成服务名user-service
  9.     User user = userClient.findUserById(uid);
  10.     // 获取每本书的详细信息
  11.     List<Book> bookList = allByUid.stream().map(borrow -> bookClient.getBookById(borrow.getBid())).collect(Collectors.toList());
  12.     return new BorrowDetail(user, bookList);
  13. }
复制代码
然后进行配置:
  1. spring:
  2.   cloud:
  3.     sentinel:
  4.         # 关闭context收敛,这样被监控的方法可以进行不同链路的单独控制
  5.       web-context-unify: false
复制代码
然后在Sentinel控制台中添加流控规则,注意是针对此方法!
7.png

这样设置后会发现,无论请求哪个接口,只要调用了被监控的这个方法,都会被限流。注意:这里限流的形式是后台直接抛出异常。
而这个链路选项实际就是决定只限流从哪个方向来的调用,比如要求只限流从borrow2接口对方法的调用。我们就可以指定链路为:
8.png

然后就会发现,限流效果只对配置的链路接口有效,而其他链路不会被限流。
除了直接对接口使用限流控制外,还可以根据当前系统的资源使用情况。决定是否进行限流:
9.png

限流和异常处理

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

  • 先创建好需要返回的内容,定义一个请求映射:
    1. @RequestMapping("/blocked")
    2. JSONObject blocked(){
    3.     JSONObject jsonObject = new JSONObject();
    4.     jsonObject.put("code",403);
    5.     jsonObject.put("success",false);
    6.     jsonObject.put("message","您请求的频率过快,请稍后重试!");
    7.     return jsonObject;
    8. }
    复制代码
  • 在配置文件中将此页面设定为限流页面:
  1. spring:
  2.   cloud:
  3.     sentinel:
  4.       # 将刚刚编写的请求映射为限流页面
  5.       block-page: /blocked
复制代码
那么方法级别的限流怎么处理?因为方法被限流会在后台直接抛出异常,这种情况我们该怎么处理呢?
Sentinel可以指定一个替代方案,当出现异常时,会调用替代方案:
在@SentinelResource注解的blockHandler属性指定替代方法即可
  1. @SentinelResource(value = "getBorrow",blockHandler = "blocked")
  2. @Override
  3. public BorrowDetail findBorrowById(int uid) {
  4.     List<Borrow> allByUid = borrowMapper.getAllByUid(uid);
  5.     // 获取用户信息 localhost:8101 改成服务名user-service
  6.     User user = userClient.findUserById(uid);
  7.     // 获取每本书的详细信息
  8.     List<Book> bookList = allByUid.stream().map(borrow -> bookClient.getBookById(borrow.getBid())).collect(Collectors.toList());
  9.     return new BorrowDetail(user, bookList);
  10. }
  11. // 替代方案,参数和返回值必须和原方法一致,并且参数最后需要加一个BlockException类型的参数
  12. public BorrowDetail blocked(int uid, BlockException blockException) {
  13.     return new BorrowDetail(null, Collections.emptyList());
  14. }
复制代码
注意:这里的blockHandler只能处理限流情况下抛出的异常,如果时方法本身抛出的其他类型的异常,不再管控范围内,但是可以通过其他参数进行处理:
  1. @SentinelResource(value = "getBorrow",
  2.         fallback = "except", // 指定出现异常时的替代方案。"except"是替代方法的方法名
  3.         exceptionsToIgnore = IOException.class) // 忽略哪些异常,也就是说,出现这种异常不使用替代方案
  4. @Override
  5. public BorrowDetail findBorrowById(int uid) {
  6. ...
  7. }
复制代码
这种方式会在没有配置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")
    2. @SentinelResource("test")
    3. String findUserBorrow2(@RequestParam(value = "a",required = false) String a,
    4.                        @RequestParam(value = "b",required = false) String b,
    5.                        @RequestParam(value = "c",required = false) String c){
    6.     return "请求成功!a="+a+";b="+b+";c="+c;
    7. }
    复制代码

    • 进行热点配置

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

服务熔断与降级

12.png

如果在某一时刻,服务B出现故障,而这时服务A依然有大量请求在调用服务B。由于服务A没办法在短时间内完成处理,新来的请求会导致线程数不断增加,这样,CPU的资源很快就会被耗尽!
要防止这种情况,就只能进行隔离,一共有两种隔离方案:

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


  • 信号量隔离
    信号量隔离是使用Semaphore类实现的。其思想基本与上面相同,也是限定指定的线程数量,但它相对于线程池隔离。开销会更小。使用效果相同,也支持超时等,而Sentinel正是采用的这种方案实现隔离的。
    那什么是服务降级?
    当下游服务因为某种原因变得不可用或响应过慢时,上游服务为了保证自己整体服务的可用性,不再继续调用目标服务,而是快速返回或是执行自己的替代方案,这便是服务降级。
    整个过程分为三个状态:

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

14.png

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

  • 慢调用比例:这种如果出现那种半天都处理不完的调用,有可能就是服务出现故障,导致卡顿。这个选项是按照最大响应时间(RT)进行判定,如果一次请求的处理时间超过了指定的RT,那么就被判断为慢调用。在一个统计时长内,如果请求数目大于最小请求数目,并且被判断为慢调用的请求比例已超过阈值,将触发熔断。经过熔断时长之后,将会进入到半开状态进行试探(这里和Hystrix一致)! 资源名填写服务地址比如:/borrow/
  • 异常比例:与慢调用类似,不过这里判断的是出现异常的比例
  • 异常数:只要达到指定的异常数量,就熔断
那在Sentinel中如何自定义服务降级?
只需要在@SentinelResource()中配置blockHandler参数(其实和处理方法级别的限流异常一样)
  1. @RequestMapping("/borrow2/{uid}")
  2. @SentinelResource(value = "getBorrowById2",blockHandler = "test")
  3. public BorrowDetail getBorrowById2(@PathVariable("uid") int id) {
  4.     return borrowService.findBorrowById(id);
  5. }
  6. BorrowDetail test(int uid, BlockException blockException){
  7.     return new BorrowDetail(null, Collections.emptyList());
  8. }
复制代码
这样设置好,注意添加熔断规则时,资源名填写的是getBorrowById2。
这样在熔断后就不会返回到限流页面,而是返回替代方案!
如何让Feign也支持Sentinel的服务降级?


  • 现在配置中开启支持:
    1. feign:
    2.   sentinel:
    3.     enabled: true
    复制代码
  • 创建UserClient接口
    1. @FeignClient(value = "user-service",fallback = UserClientImpl.class)
    2. public interface UserClient {
    3.     @RequestMapping("/user/{uid}")
    4.     User findUserById(@PathVariable("uid") int uid);
    5. }
    复制代码

    • 创建UserClient接口的实现类:
      1. @Component
      2. public class UserClientImpl implements UserClient {
      3.     @Override
      4.     public User findUserById(int uid) {
      5.         User user = new User();
      6.         user.setName("我是替代方案");
      7.         return user;
      8.     }
      9. }
      复制代码

这样就让Feign实现了服务降级
如何让传统的RestTemplate也实现服务降级呢?
可以使用   @SentinelRestTemplate注解实现!
  1. @Configuration
  2. // 指定为user-service服务,只要调用此服务,就会使用我们指定的策略
  3. //configuration = LoadBanancerConfig.class 指定我们自定义的策略类
  4. // @LoadBalancerClient(value = "user-service",configuration = LoadBanancerConfig.class)
  5. public class BeanConfiguration {
  6.     @Bean
  7.     // 负载均衡
  8.     @LoadBalanced
  9.     @SentinelRestTemplate(blockHandler = "handleException", blockHandlerClass = ExceptionUtil.class,
  10.             fallback = "fallback",fallbackClass = ExceptionUtil.class)
  11.     public RestTemplate restTemplate() {
  12.         return new RestTemplate();
  13.     }
  14. }
复制代码
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册