SpringCloud alibaba - Sentinel哨兵
Sentinel 以 流量 为切入点,在流量控制,断路和负载保护等多个领域工作,以保护服务可靠性。官网地址 (opens new window)
# Sentinel 基本概念
# 资源
资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。在接下来的文档中,我们都会用资源来描述代码块。
只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。
# 规则
围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。
# Sentinel 功能和设计理念
# 流量控制
# 什么是流量控制
流量控制在网络传输中是一个常用的概念,它用于调整网络包的发送数据。然而,从系统稳定性角度考虑,在处理请求的速度上,也有非常多的讲究。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状,如下图所示:
# 流量控制设计理念 (opens new window)
流量控制有以下几个角度:
- 资源的调用关系,例如资源的调用链路,资源和资源之间的关系;
- 运行指标,例如 QPS、线程池、系统负载等;
- 控制的效果,例如直接限流、冷启动、排队等。
Sentinel 的设计理念是让您自由选择控制的角度,并进行灵活组合,从而达到想要的效果。
# 熔断降级
# 什么是熔断降级
除了流量控制以外,降低调用链路中的不稳定资源也是 Sentinel 的使命之一。由于调用关系的复杂性,如果调用链路中的某个资源出现了不稳定,最终会导致请求发生堆积。
Sentinel 和 Hystrix 的原则是一致的:当检测到调用链路中某个资源出现不稳定的表现,例如请求响应时间长或异常比例升高的时候,则对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联故障。
# 熔断降级设计理念
在限制的手段上,Sentinel 和 Hystrix 采取了完全不一样的方法。
Hystrix 通过 线程池隔离 (opens new window) 的方式,来对依赖(在 Sentinel 的概念中对应 资源)进行了隔离。这样做的好处是资源和资源之间做到了最彻底的隔离。缺点是除了增加了线程切换的成本(过多的线程池导致线程数目过多),还需要预先给各个资源做线程池大小的分配。
Sentinel 对这个问题采取了两种手段:
- 通过并发线程数进行限制
和资源池隔离的方法不同,Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其它资源的影响。这样不但没有线程切换的损耗,也不需要您预先分配线程池的大小。当某个资源出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步堆积。当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝。堆积的线程完成任务后才开始继续接收请求。
- 通过响应时间对资源进行降级
除了对并发线程数进行控制以外,Sentinel 还可以通过响应时间来快速降级不稳定的资源。当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的时间窗口之后才重新恢复。
# 实战
# 导包
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- 监控控制台 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
</dependency>
2
3
4
5
6
7
8
9
# 注解形式
注解的形式要做限流的话需要和 Sentinel 控制台 搭配才能做到,这里我们先说注解的形式使用。
@GetMapping("/sentinelTest")
@SentinelResource("sentinelTest")
public String sentinelTest(){
return "hello Sentinel";
}
2
3
4
5
对于 @SentinelResource 参数解释 看这里 (opens new window)
下载 sentinel-dashboard.jar 并启动
java -Dserver.port=8888 -Dcsp.sentinel.dashboard.server=localhost:8810 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.7.1.jar
建议 Dserver.port 和 Dcsp.sentinel.dashboard.server 的 port 不要一样,否则自己也会被监控,没有必要。也可以启动不加 Dcsp.sentinel.dashboard.server,其意义是为了让在 dashbord 被发现。
sentinel-dashboard 并不用考虑负载和并发问题,因为 sentinel-dashboard 是主动去请求你的 client 端的,会把配置的规则发给 client,client 在执行的时候自己做校验,只有在 client 被执行的时候 会向 sentinel-dashboard 通信,看是否在 sentinel-dashboard 注册,或者说是看 sentinel-dashboard 是否记录了我的 IP peort applicationName 等信息。
刚启动的控制台实则什么也没有,需要 client 被执行后才能注册到 sentinel-dashboard 上。
启动 client
-Dcsp.sentinel.dashboard.server=192.168.31.34:8888
spring boot 项目中可以使用以下让其发现 client 端
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8888
2
3
4
5
访问接口
# 测试接口限流
# 日志
######## sentinel 的日志在请求之后,默认用户路径下生成,在 windows 下是在 C 盘,mmp (C 盘本来就不大),改掉日志的存储需要在项目中加入以下,就会在当前项目下创建 /sentinel/log 日志。
## sentinel 日志
spring.cloud.sentinel.log.dir=./sentinel/log
2
# 限流
# 单机限流操作
单机限流是比较简单的,我们直接看图是如何操作的
以上测试为限制 single 接口的 qps 为 2,实际测试数量为 4,通过 3 个请求,拒绝 1 个请求。
# 单机限线程数操作
并发线程数限流用于保护业务线程数不被耗尽。例如,当应用所依赖的下游应用由于某种原因导致服务不稳定、响应延迟增加,对于调用者来说,意味着吞吐量下降和更多的线程数占用,极端情况下甚至导致线程池耗尽。为应对太多线程占用的情况,业内有使用隔离的方案,比如通过不同业务逻辑使用不同线程池来隔离业务自身之间的资源争抢(线程池隔离)。这种隔离方案虽然隔离性比较好,但是代价就是线程数目太多,线程上下文切换的 overhead 比较大,特别是对低延时的调用有比较大的影响。Sentinel 并发线程数限流不负责创建和管理线程池,而是简单统计当前请求上下文的线程数目,如果超出阈值,新的请求会被立即拒绝,效果类似于信号量隔离。
public static <T> T flow(String resourceName, Supplier supplier){
// 资源名可使用任意有业务语义的字符串,比如方法名、接口名或其它可唯一标识的字符串。
T result = null;
Entry entry = null;
try {
entry = SphU.entry(resourceName);
// 被保护的业务逻辑
result = (T)supplier.get();
return result;
} catch (BlockException ex) {
ex.fillInStackTrace().getStackTrace();
System.out.println(ex.fillInStackTrace());
// 资源访问阻止,被限流或被降级
// 在此处进行相应的处理操作
} catch (Exception ex) {
// 若需要配置降级规则,需要通过这种方式记录业务异常
Tracer.traceEntry(ex, entry);
}finally {
// 务必保证 exit,务必保证每个 entry 与 exit 配对
if (entry != null) {
entry.exit();
}
}
return result;
}
public static void main(String[] args) throws InterruptedException {
String resourceName = "test";
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule(resourceName);
rule.setCount(1);
rule.setGrade(RuleConstant.FLOW_GRADE_THREAD);
rule.setLimitApp("default");
rules.add(rule);
FlowRuleManager.loadRules(rules);
ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(100);
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
3,3,5,TimeUnit.SECONDS,arrayBlockingQueue
);
for(int i=0;i<6;i++){
int index = i;
CompletableFuture.runAsync(() -> {
flow(resourceName,() -> {
int num = (int)(Math.random()*11);
try {
TimeUnit.SECONDS.sleep(num);
} catch (InterruptedException e) {
e.printStackTrace();
}
String result = "idnex: "+index + " time: " +num+ "s name: " + Thread.currentThread().getName()+ " queueSize: "+arrayBlockingQueue.size();
System.out.println(result);
return result;
});
},threadPoolExecutor);
}
TimeUnit.SECONDS.sleep(10);
threadPoolExecutor.shutdown();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
运行结果
idnex: 1 time: 2s name: pool-1-thread-2 queueSize: 3
com.alibaba.csp.sentinel.slots.block.flow.FlowException
com.alibaba.csp.sentinel.slots.block.flow.FlowException
com.alibaba.csp.sentinel.slots.block.flow.FlowException
idnex: 0 time: 4s name: pool-1-thread-1 queueSize: 0
idnex: 2 time: 8s name: pool-1-thread-3 queueSize: 0
2
3
4
5
6
结果表示,我只允许运行 3 个线程,但请求 (并发) 数量实际大于 3 个线程,且我定的 3 个线程都在执行任务,所以其余请求被异常处理。
留一个疑问点,关于 FlowRule 有一个 count 的设置,如果说 Grade 为 THREAD 模式,count 到底是一个开关 还是一个数字,如果是数字,测试 count 值 0<count<3 的时候有线程拒绝,3<=count<= 无穷大 无线程拒绝,count 在 Grade 为 THREAD 模式是什么意思,限制的是什么?
# 集群限流操作
# 集群线程数操作
# 降级
# RT 降级操作
@SentinelResource(fallback = "hystrixException")
@GetMapping("/hystrix")
public String hystrix(){
int num = (int) (Math.random() * 10);
try {
TimeUnit.SECONDS.sleep(num);
}catch(Exception e){
}
System.out.println("熔断请求 -> 处理时间: "+num+"s");
return "熔断请求";
}
2
3
4
5
6
7
8
9
10
11
12
上图可以看出,我设置 hystrix 接口要被请求 20 次,当响应时间在 规定时间 内的平均值大于设置的阈值 (RT:2000ms),则拒绝所有请求。时间窗口 的意思是,若发生熔断则特定时间内的所有请求均被拒绝,直到时间窗口数值耗尽才开始接收请求。
这里有个问题 规定时间 如何限制??查到官方说法如下,可我测试一定是大于 1s 了。
# 异常比列
@SentinelResource(fallback = "hystrixException")
@GetMapping("/hystrixExcep")
public String hystrixExcep(){
int num = (int) (Math.random() * 10);
System.out.println("熔断请求 -> num: "+num);
if(num >= 5){
throw new RuntimeException("num: "+num);
}
return "熔断请求";
}
public String hystrixException(Throwable throwable){
System.out.println("熔断异常 -> "+throwable.getMessage());
return "熔断异常";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 运行结果
熔断请求 -> num: 1
熔断请求 -> num: 7
熔断异常 -> num: 7
熔断请求 -> num: 2
熔断请求 -> num: 0
熔断请求 -> num: 9
熔断异常 -> num: 9
熔断异常 -> null
熔断异常 -> null
熔断异常 -> null
熔断异常 -> null
熔断异常 -> null
2
3
4
5
6
7
8
9
10
11
12
13
假设 1 秒有 10 个请求,我的异常比是 30%,那就是只要有 3 个请求发生异常,其他请求均被拒绝,拒绝的请求还是有走定义的熔断异常方法。
# 异常数
异常数是很好理解的,和异常比是相似的。假设 1 分钟内有 100 个请求,100 个请求中有部分请求发生异常,且 1 分钟内异常数刚好 >= 设置的阈值,则其余请求均被拒绝,拒绝的请求还是有走定义的熔断异常方法。
# 热点
热点数据限流就是对访问频次比较高的数据进行限制。在 dashbord 中参数索引的意思就是,方法中的参数列表的下标,默认是 0 开始
在阅读文档的时候发现,文档中之说了 统计一段时间内的 频次,但并没有明确说明是 1s 还是 1minute。