SpringCloud进阶--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();
}
}
本文来自博客园,作者:NE_STOP,转载请注明原文链接:https://www.cnblogs.com/alineverstop/p/19762735
浙公网安备 33010602011771号