Sentinel
SpringCloud Alibaba也有自己的微服务容错组件,但是它相比Hystrix更加的强大。
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Sentinel 具有以下特征:
- 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
- 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Apache Dubbo、gRPC、Quarkus 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。同时 Sentinel 提供 Java/Go/C++ 等多语言的原生实现。
- 完善的 SPI 扩展机制:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
安装与部署
和Nacos一样,它是独立安装和部署的,下载地址:下载地址
注意下载下来之后是一个jar
文件(其实就是个SpringBoot项目),默认是8080端口,接着就可以直接启动
启动之后,就可以访问到Sentinel的监控页面了,用户名和密码都是sentinel
,地址:http://localhost:8858/#/dashboard
接着我们需要让我们的服务连接到Sentinel控制台,导入依赖:
xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
1
2
3
4
5
2
3
4
5
然后在配置文件中添加Sentinel相关信息
yaml
spring:
application:
name: userservice
cloud:
sentinel:
transport:
# 添加监控页面地址即可
dashboard: localhost:8085
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
现在启动我们的服务,然后访问一次服务,这样Sentinel中就会存在信息了(懒加载机制,不会一上来就加载)
流量控制
机器不可能无限制的接受和处理客户端的请求,如果不加以限制,当发生高并发情况时,系统资源将很快被耗尽。为了避免这种情况,我们就可以添加流量控制(也可以说是限流)当一段时间内的流量到达一定的阈值的时候,新的请求将不再进行处理,这样不仅可以合理地应对高并发请求,同时也能在一定程度上保护服务器不受到外界的恶意攻击。
那么要实现限流,正常情况下,我们该采取什么样的策略呢?
- 方案一:快速拒绝,既然不再接受新的请求,那么我们可以直接返回一个拒绝信息,告诉用户访问频率过高。
- 方案二:预热,依然基于方案一,但是由于某些情况下高并发请求是在某一时刻突然到来,我们可以缓慢地将阈值提高到指定阈值,形成一个缓冲保护。
- 方案三:排队等待,不接受新的请求,但是也不直接拒绝,而是进队列先等一下,如果规定时间内能够执行,那么就执行,要是超时就算了。
我们在Sentinel中进行实际测试一下,打开管理页面的簇点链路模块:
这里演示对我们的借阅接口进行限流,点击流控
,会看到让我们添加流控规则:
- 阈值类型:QPS就是每秒钟的请求数量,并发线程数是按服务当前使用的线程数据进行统计的。
- 流控模式:当达到阈值时,流控的对象,这里暂时只用直接。
- 流控效果:就是我们上面所说的三种方案。
这里我们选择QPS
、阈值设定为1
,流控模式选择直接
、流控效果选择快速失败
,可以看到,当我们快速地进行请求时,会直接返回失败信息: Blocked by Sentinel (flow limiting)
限流和异常处理
限流状态下的返回结果该怎么修改呢,我们看到被限流之后返回的是Sentinel默认的数据,现在我们希望自定义改如何操作?
这里我们先创建好被限流状态下需要返回的内容,定义一个请求映射:
java
@RestController
public class SentinelBlockController {
@GetMapping("/blocked")
public Map<String, Object> block() {
Map<String, Object> block = Maps.newHashMap();
block.put("code", 403);
block.put("success", false);
block.put("massage", "您的请求频率过快,请稍后再试!");
return block;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
接着我们在配置文件中将此页面设定为限流页面:
yaml
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8085
# 将刚刚编写的请求映射设定为限流页面
block-page: /blocked
1
2
3
4
5
6
7
2
3
4
5
6
7
这样,当被限流时,就会被重定向到指定页面:
json
{
"code": 403,
"success": false,
"massage": "您的请求频率过快,请稍后再试!"
}
1
2
3
4
5
2
3
4
5
对于方法级别的限流呢?经过前面的学习我们知道,当某个方法被限流时,会直接在后台抛出异常,那么这种情况我们该怎么处理呢,比如我们之前在Hystrix中可以直接添加一个替代方案,这样当出现异常时会直接执行我们的替代方法并返回,Sentinel也可以。
比如我们还是在getUserBorrowDetailByUid
方法上进行配置:
java
public class BorrowServiceImpl implements BorrowService {
@SentinelResource(blockHandler = "borrowBlocked")
public UserBorrowDetail getUserBorrowDetailByUid(int uid) {
List<Borrow> borrow = mapper.getBorrowsByUid(uid);
//获取User信息
User user = userClient.getUserById(uid);
//获取每一本书的详细信息
List<Book> bookList = borrow
.stream()
.map(b -> bookClient.getBookById(b.getBid()))
.collect(Collectors.toList());
return new UserBorrowDetail(user, bookList);
}
/**
* 替代方案,注意参数和返回值需要保持一致,并且参数最后还需要额外添加一个BlockException
*/
public UserBorrowDetail borrowBlocked(int uid, BlockException e) {
log.warn("block {} - {}", uid, e.getMessage(), e);
return new UserBorrowDetail(null, Collections.emptyList());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
注意blockHandler
只能处理限流情况下抛出的异常,包括下面即将要介绍的热点参数限流也是同理,如果是方法本身抛出的其他类型异常,不在管控范围内,但是可以通过其他参数进行处理:
java
public class TestController {
@RequestMapping("/test")
@SentinelResource(value = "test",
fallback = "except", //fallback指定出现异常时的替代方案
exceptionsToIgnore = IOException.class)
//忽略那些异常,也就是说这些异常出现时不使用替代方案
String test() {
throw new RuntimeException("HelloWorld!");
}
//替代方法必须和原方法返回值和参数一致,最后可以添加一个Throwable作为参数接受异常
String except(Throwable t) {
return t.getMessage();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
特别注意这种方式会在没有配置blockHandler
的情况下,将Sentinel机制内(也就是限流的异常)的异常也一并处理了,如果配置了blockHandler
,那么在出现限流时,依然只会执行blockHandler
指定的替代方案(因为限流是在方法执行之前进行的)