SpringCloud Sentinel如何成为流量防卫兵的进阶利器?

摘要:Sentinel 流量防卫兵 之前,我们了解到了微服务雪崩问题,就是一个微服务出现问题,有可能导致整个联络直接不可用,这时候就需要进行即使的熔断和降级,之前我们使用Hystrix来实现。现在我们使用Sentinel 。 Sentinel 有
Sentinel 流量防卫兵 之前,我们了解到了微服务雪崩问题,就是一个微服务出现问题,有可能导致整个联络直接不可用,这时候就需要进行即使的熔断和降级,之前我们使用Hystrix来实现。现在我们使用Sentinel 。 Sentinel 有以下特征: 丰富的应用场景:例如秒杀、消息削峰填谷、集群流量控制、实时熔断下游不可用服务等。 完备的实时监控:Sentinel 提供实时监控功能。 广泛的开源生态:Sentinel 可以与其他开源框架整合。 完善的SPI扩展机制:Sentinel 提供简单易用的SPI接口。可以通过接口快速定制逻辑、规则。 安装与部署 下载并安装下载地址 下载下来是一个jar包,直接启动这个jar包。端口默认8080,我这里指定了8858端口。 访问地址就是localhost:8858 ; 用户名和密码都是sentinel 在服务中引入sentinel依赖 <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> 在配置文件进行配置 spring: cloud: sentinel: transport: # 添加监控页面地址 dashboard: localhost:8858 然后启动服务,注意,这里要先调用一次服务,sentinel才会加载这个服务(它使用了懒加载机制) 流量控制 我们不能无限制的接收和处理客户端请求,如果不加以限制,当发生高并发情况时,系统资源很快就会被耗尽。 这时可以使用流量控制(限流),当一段时间内的流量达到一定的阈值时,新的请求将不再进行处理。这样能合理应对高并发,也能保护服务器不受外界的攻击。 那么,实现限流的策略有哪些呢? 快速拒绝:不再接收新请求。直接返回一个拒绝信息,告诉用户访问频率过高。 预热:基于方案一,但由于某些情况下高并发请求时在某一时刻突然到来,我们可以缓慢地将阈值提高到指定阈值,形成一个缓冲保护 排队等待:不接受新请求,也不直接拒绝,而是进入排队,要是规定时间内能执行就执行,超时就算了。 针对是否超过流量阈值的判断,有4种算法: 漏桶算法 令牌桶算法 现在有一个令牌桶,这个桶专门存放令牌,每隔一段时间就向桶中丢入一个令牌(速度由我们指定)当新的请求到达时,将从桶中删除令牌,接着请求就可以通过并给到服务,但是如果桶中的令牌数量不足,那么不删除令牌,而是然那个此数据包等待。 当流量下降时,令牌桶中的令牌会逐渐积累,这样如果突然出现高并发,那么就能在短时间内拿到大量的令牌。 固定时间窗口算法: 滑动时间窗口算法 具体使用哪种算法和策略可以由我们自己制定。 按照上图指示进入流控规则页面。 阈值类型:QPS就是每秒种的请求数量,并发线程数是按服务当前十一月的线程数据进行统计的。 流控模式:当达到阈值时,流控的对象,这里暂时使用直接。 流控效果:对应上面的三种限流策略。 这里我们选择QPS、阈值设为1,流控模式选择直接、流控效果选择快速失败。可以看到当我们快速地进行请求时,会直接返回失败信息。 那这些流控模式有什么区别? 直接:只针对于当前接口 关联:当关联的其他接口超过阈值时,会导致当前接口被限流 链路:更细粒度的限流,能精确到具体的方法。 比如关联模式,我们将/borrow/{uid}和自带的/error接口关联,然后进行限流 此时,如果对/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控制台中添加流控规则,注意是针对此方法! 这样设置后会发现,无论请求哪个接口,只要调用了被监控的这个方法,都会被限流。注意:这里限流的形式是后台直接抛出异常。 而这个链路选项实际就是决定只限流从哪个方向来的调用,比如要求只限流从borrow2接口对方法的调用。我们就可以指定链路为: 然后就会发现,限流效果只对配置的链路接口有效,而其他链路不会被限流。 除了直接对接口使用限流控制外,还可以根据当前系统的资源使用情况。决定是否进行限流: 限流和异常处理 现在我们已经实现了限流操作,那么限流状态下的返回结果该怎么修改呢? 先创建好需要返回的内容,定义一个请求映射: @RequestMapping("/blocked") JSONObject blocked(){ JSONObject jsonObject = new JSONObject(); jsonObject.put("code",403); jsonObject.put("success",false); jsonObject.put("message","您请求的频率过快,请稍后重试!"); return jsonObject; } 在配置文件中将此页面设定为限流页面: 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的请求进行限流 创建一个请求映射: @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; } 进行热点配置 这样就实现了对某个参数进行精准限流。 除了对某个参数进行精准限流外,还可以对参数携带的指定值单独设定阈值,比如,希望现在不仅对参数a限流,而且还希望当参数a=10 时,QPS达到3时再进行限流,就可以如下设置: 服务熔断与降级 如果在某一时刻,服务B出现故障,而这时服务A依然有大量请求在调用服务B。由于服务A没办法在短时间内完成处理,新来的请求会导致线程数不断增加,这样,CPU的资源很快就会被耗尽! 要防止这种情况,就只能进行隔离,一共有两种隔离方案: 线程池隔离 线程池隔离实际上就是对每个服务的远程调用单独开放线程池,比如服务A调用服务B。只基于固定数量的线程池。这样即使在短时间内出现大量请求,由于没有线程可以分配。所以就不会导致资源耗尽! 信号量隔离 信号量隔离是使用Semaphore类实现的。其思想基本与上面相同,也是限定指定的线程数量,但它相对于线程池隔离。开销会更小。使用效果相同,也支持超时等,而Sentinel正是采用的这种方案实现隔离的。 那什么是服务降级? 当下游服务因为某种原因变得不可用或响应过慢时,上游服务为了保证自己整体服务的可用性,不再继续调用目标服务,而是快速返回或是执行自己的替代方案,这便是服务降级。 整个过程分为三个状态: 关闭:熔断器不工作,所有请求全部该干嘛干嘛 打开:熔断器工作,所有请求一律降级处理 半打开:尝试进行一下正常流程,要是还不行,就继续保持打开状态,否则关闭 以下时Sentinel中进行熔断和降级操作: 其中,熔断策略有三种模式: 慢调用比例:这种如果出现那种半天都处理不完的调用,有可能就是服务出现故障,导致卡顿。这个选项是按照最大响应时间(RT)进行判定,如果一次请求的处理时间超过了指定的RT,那么就被判断为慢调用。在一个统计时长内,如果请求数目大于最小请求数目,并且被判断为慢调用的请求比例已超过阈值,将触发熔断。经过熔断时长之后,将会进入到半开状态进行试探(这里和Hystrix一致)! 资源名填写服务地址比如:/borrow/ 异常比例:与慢调用类似,不过这里判断的是出现异常的比例 异常数:只要达到指定的异常数量,就熔断 那在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的服务降级? 现在配置中开启支持: feign: sentinel: enabled: true 创建UserClient接口 @FeignClient(value = "user-service",fallback = UserClientImpl.class) public interface UserClient { @RequestMapping("/user/{uid}") User findUserById(@PathVariable("uid") int uid); } 创建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(); } }