Sentinel快速入门:这可能是目前最好的分布式系统限流降级框架
作者:鱼仔
博客首页: https://codeease.top
公众号:Java鱼仔
# (一)概述
在分布式系统中,许多服务之间通过远程调用实现信息交互,调用时难免会出现调用失败的情况,Sentinel能保证在一个服务出现问题的情况下,不会导致整体服务失败,防止服务雪崩,提高分布式系统的可用性。
常用的容错方式有:
1、超时:设置比较短的超时时间,如果调用不成功,在很短时间内就释放连接,避免大量线程堵塞等待。
2、限流:超过设置的阈值就拒绝请求。
3、断路器:保护服务过载,当有服务发生无法调用请求堆积时,能够及时切换该服务,防止整个服务的崩溃。
Sentinel的地位和SpringCloud中的Hystrix类似,Sentinel以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
# (二)SpringCloudAlibaba集成Sentinel
首先我们在sentinel的github官网上下载sentinel-dashboard-1.7.2.jar
然后通过jar命令启动起来,端口默认8080,直接ip+端口访问,这是sentinel的控制后台,账号密码都是sentinel
接着在项目中引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
2
3
4
在配置文件中,输入Sentinel的后台地址,地址和端口写自己的,端口默认8080
spring.cloud.sentinel.transport.dashboard=localhost:8080
启动后在Sentinel后台就可以看到这个应用
sentinel控制台调用的API默认是http://ip:8719/api,在本地启动了引入sentinel依赖的项目启动后,默认在8719这个端口上会暴露一个api出来。
# (三)sentinel的流控规则
写了一个简单的请求:
@RestController
public class TestController {
@GetMapping("/test")
public String test(){
return "test";
}
}
2
3
4
5
6
7
接下来通过这个请求去了解sentinel的流控规则,所谓流控规则,就是对请求的控制
资源名:默认是请求的路径
针对来源:Sentinel可以针对调用者进行限流,填写微服务名,指定对哪个微服务进行限流 ,默认default(不区分来源,全部限制)
阈值类型:可选QPS和线程数,可以设置达到多少后执行流控策略
是否集群:你的环境是否是集群
流控模式:
1、直接:直接对该资源进行控制,比如上面的/test访问达到阈值,就限流
2、关联:当关联的资源达到阈值时,就限流自己。
看图,如果/test2访问达到阈值,就限流/test
3、链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就可以限流),这个链路的名称可以从簇点链路中获取。
流控效果
1、快速失败:直接失败
2、Warm Up:预热模式,根据codeFactory的值(默认3),从阈值/codeFactory,经过预热时长,才达到设置的QPS阈值。比如设置QPS为90,设置预热为10秒,则最初的阈值为90/3=30,经过10秒后才达到90。
3、排队等待:比如设置阈值为10,超时时间为500毫秒,当第11个请求到的时候,不会直接报错,而是等待500毫秒,如果之后阈值还是超过10,则才会被限流。
设置完流控规则后,不停刷新去触发到阈值,看到出现了下面的提示:
这样的提示不太友好,可以自定义限流后返回的数据信息,分别对应于五种限流异常:
public class MyBlockException implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
JSONObject object=new JSONObject();
if (e instanceof FlowException){
object.put("status","100");
object.put("message","接口限流");
object.put("data",null);
}else if (e instanceof DegradeException){
object.put("status","101");
object.put("message","服务降级");
object.put("data",null);
}else if (e instanceof ParamFlowException){
object.put("status","102");
object.put("message","热点参数限流");
object.put("data",null);
}else if (e instanceof SystemBlockException){
object.put("status","103");
object.put("message","触发系统保护");
object.put("data",null);
}else if (e instanceof AuthorityException){
object.put("status","104");
object.put("message","授权规则不通过");
object.put("data",null);
}
httpServletResponse.setStatus(500);
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(httpServletResponse.getWriter(),object);
}
}
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
代码中定义了五种不同的
# (四)sentinel的降级规则
降级规则指的是在应用高峰期,将个别的服务关闭,使得能够有更多地资源去处理重要的业务,比如双十一的时候,我们会发现支付宝的部分功能会被暂时关闭。
RT模式:平均响应时间、当1S内持续进入N个请求,如果平均响应时间超过阈值,那么在接下来的时间窗口内,按降级逻辑进行处理(报一个DegradeException错误)。
异常比例:可以输入一个0.0~1.0的数字,表示出现异常的比例。如果一秒内超过这个比例,那么在接下来的时间窗口内,按降级逻辑进行处理。
异常数:指当资源近一分钟的异常数超过阈值之后会,那么在接下来的时间窗口内,按降级逻辑进行处理。
# (五)sentinel的热点规则
所谓热点规则,就是对某些经常访问的数据(热点数据),对其访问进行限制,
比如以商品ID为参数,限制这个商品的访问次数。在代码中需要对要限制的请求进行埋点
@GetMapping("/test3")
@SentinelResource(value = "test3",blockHandler = "handHotKey")
public String test3(@RequestParam(value = "a",required = false)String a,
@RequestParam(value = "b",required = false)String b){
return "test3"+a+b;
}
public String handHotKey(String a1, String a2, BlockException e){
return "热点数据限流";
}
2
3
4
5
6
7
8
9
10
设置热点规则,这里的资源名就是@SentinelResource中所设置的value,参数索引的表示对第几个参数进行限流控制,阈值和窗口时长表示在1秒内如果有2个对参数0的请求,就限流。限流后会执行自己设置的blockHandler方法。
高级设置中可以设置参数例外项,即根据设置参数的值进行限流:
这样设置后,如果访问/test3?a=1,则按照下面的阈值进行控制。
# (六)Sentinel的系统规则
通过监控系统的一些参数进行规则限流:
Load:这个参数只能在Linux或类Unix机器生效,将系统的1分钟的loadAverage作为指标,这个值一般设置为CPU核心数量*2.5。
RT:当单台机器上所有入口流量的平均RT达到阈值就触发系统保护,单位是毫秒。
并发线程数:当单台机器上所有入口流量的并发线程数达到阈值就触发系统保护。
入口QPS:当单台机器上所有入口流量的QPS达到阈值就触发阈值就触发系统保护。
CPU使用率:当系统CPU使用率超过阈值就触发系统保护。
# (七)Sentinel的授权规则
授权规则可以指定哪些请求可以访问哪些不能访问,首先来看如何配置:
资源名就是请求名,流控应用中可以手动输入一些应用名,如果是白名单,则流控应用中设置的这些可以访问,如果是黑名单则流控应用中设置的不能访问。
接着需要在代码中去获取请求:
@Component
public class MyRequestOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest httpServletRequest) {
String origin=httpServletRequest.getParameter("origin");
if (origin==null){
throw new IllegalArgumentException("origin参数未指定");
}
return origin;
}
}
2
3
4
5
6
7
8
9
10
11
12
通过设置后,所有请求必须带上参数origin=XXX,以上面的配置为例,只有origin=javayz的请求才能通过访问。如果不喜欢参数的方式,可以在代码中换成header传递,效果一样。
# (八)Sentinel控制台与服务之间的通信原理
使用Sentinel需要引入引入sentinel依赖,其中sentinel-transport-simple-http依赖会将微服务注册到SentinelDashboard中。启动微服务之后,会在8719端口自动开放一系列api接口,我们也可以通过http://ip:8719/api访问到这些api接口,SentinelDashboard就是通过这些API与微服务之间进行通信。
这个8719端口可以在配置文件中修改:
spring.cloud.sentinel.transport.port=8719
# (九)Sentinel的保护规则
默认情况下,我们代码中的所有GetMapping、PostMapping请求都会被Sentinel保护,也就是都会经过Sentinel的拦截器,但是也可以手动关闭这个拦截。
spring.cloud.sentinel.filter.enabled=false
这样的话sentinel就没法捕捉请求了。但是还是可以通过代码的方式使用Sentinel
@GetMapping("/test4")
public String test4(){
ContextUtil.enter("test4","abc");
Entry entry=null;
try {
entry= SphU.entry("test4");
//业务代码
return "业务处理结束";
} catch (BlockException e) {
e.printStackTrace();
//一系列的异常处理。参考MyBlockException
//.....
return "触发限流";
}catch (NullPointerException e){
//对异常进行监控
Tracer.trace(e);
return "空指针异常";
}finally {
if (entry!=null){
entry.exit();
}
ContextUtil.exit();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
或者还可以使用注解方式加入埋点
@SentinelResource(value = "test3",blockHandler = "handHotKey")
这样配置后就增加了资源test3的拦截,如果触发限流策略,就会进入当前类的handHotKey方法,或者配置blockHandlerClass,就会进入blockHandlerClass所配置类中的handHotKey方法。
# (十)RestTemplate整合Sentinel
首先需要在配置文件中开启对RestTemplate的支持,默认也是true
resttemplate.sentinel.enabled=true
接着在RestTemplate注入Bean的代码中增加一个Sentinel注解:
@SentinelRestTemplate(blockHandler = "fallback",blockHandlerClass = MyBlockHandlerClass.class)
@LoadBalanced //负载均衡
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
2
3
4
5
6
调用的限流处理方法如下:
public class MyBlockHandlerClass {
public static SentinelClientHttpResponse block(){
return new SentinelClientHttpResponse("block info");
}
}
2
3
4
5
# (十一)Feign整合Sentinel
首先需要在配置文件中开启对Feign的支持,默认为false:
feign.sentinel.enabled=true
接在在@FeignClient注解中增加fallback的类:
@FeignClient(name = "nacos-discovery-provider",fallback = TestServiceFallback.class,configuration = FeignConfiguration.class)
public interface TestService {
@GetMapping("/{name}")
String index(@PathVariable("name") String string);
}
最后设置规则后就会触发fallback中对应的方法,具体实现
public class TestServiceFallback implements TestService{
@Override
public String index(String string) {
return "fallback";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# (十二)Sentinel的持久化
默认模式:
在前面介绍Sentinel时,会发现每次重启微服务后Sentinel中的配置都会丢失,这是因为API将规则推送到了客户端的内存中,重启后就消失了。
Pull模式:
在Sentinel Dashboard中设置规则之后,推送给客户端后不仅保存在内存中,还会保存到本地文件中。Pull模式需要通过代码实现,这段代码可以直接拿去复用:
@Slf4j
public class FileDataSourceInit implements InitFunc {
@Override
public void init() throws Exception {
String ruleDir=System.getProperty("user.home")+"/sentinel/rules";
log.info(ruleDir);
//限流规则路径
String flowRulePath = ruleDir + "/flow-rule.json";
//降级规则路径
String degradeRulePath = ruleDir + "/degrade-rule.json";
//热点规则路径
String paramFlowRulePath = ruleDir+"/param-flow-rule.json";
//系统规则路径
String systemRulePath = ruleDir+"/system-rule.json";
//权限规则路径
String authorityRulePath=ruleDir+"/authority-rule.json";
this.mkdirIfNotExists(ruleDir);
this.createFileIfNotExists(flowRulePath);
this.createFileIfNotExists(flowRulePath);
this.createFileIfNotExists(flowRulePath);
this.createFileIfNotExists(flowRulePath);
this.createFileIfNotExists(flowRulePath);
// 流控规则,可读取数据
ReadableDataSource<String, List<FlowRule>> flowRuleRDS=new FileRefreshableDataSource<>(
flowRulePath,
flowRuleListParser
);
//将可读数据源注入到FlowRuleManager,当文件发生变化时就会更新规则到缓存
FlowRuleManager.register2Property(flowRuleRDS.getProperty());
//流控规则:可写数据源
WritableDataSource<List<FlowRule>> flowRuleWDS=new FileWritableDataSource<List<FlowRule>>(
flowRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerFlowDataSource(flowRuleWDS);
//降级规则:可读数据源
ReadableDataSource<String, List<DegradeRule>> degradeRuleRDS=new FileRefreshableDataSource<>(
degradeRulePath,
degradeRuleListParser
);
DegradeRuleManager.register2Property(degradeRuleRDS.getProperty());
//降级规则:可写数据源
WritableDataSource<List<DegradeRule>> degradeRuleWDS=new FileWritableDataSource<List<DegradeRule>>(
flowRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerDegradeDataSource(degradeRuleWDS);
//热点参数:可读数据源
ReadableDataSource<String, List<ParamFlowRule>> paramFlowRuleRDS=new FileRefreshableDataSource<>(
paramFlowRulePath,
paramFlowRuleListParser
);
ParamFlowRuleManager.register2Property(paramFlowRuleRDS.getProperty());
//热点参数:可写数据源
WritableDataSource<List<ParamFlowRule>> paramFlowRuleWDS=new FileWritableDataSource<List<ParamFlowRule>>(
paramFlowRulePath,
this::encodeJson
);
ModifyParamFlowRulesCommandHandler.setWritableDataSource(paramFlowRuleWDS);
//系统规则:可读数据源
ReadableDataSource<String, List<SystemRule>> systemRuleRDS=new FileRefreshableDataSource<>(
systemRulePath,
systemRuleListParser
);
SystemRuleManager.register2Property(systemRuleRDS.getProperty());
//系统规则:可写数据源
WritableDataSource<List<SystemRule>> systemRuleWDS=new FileWritableDataSource<List<SystemRule>>(
systemRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerSystemDataSource(systemRuleWDS);
//授权规则:可读数据源
ReadableDataSource<String, List<AuthorityRule>> authorityRuleRDS=new FileRefreshableDataSource<>(
authorityRulePath,
authorityRuleListParser
);
AuthorityRuleManager.register2Property(authorityRuleRDS.getProperty());
WritableDataSource<List<AuthorityRule>> authorityRuleWDS=new FileWritableDataSource<List<AuthorityRule>>(
authorityRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerAuthorityDataSource(authorityRuleWDS);
}
private Converter<String,List<FlowRule>> flowRuleListParser=source-> JSON.parseObject(
source,
new TypeReference<List<FlowRule>>(){}
);
private Converter<String,List<DegradeRule>> degradeRuleListParser=source-> JSON.parseObject(
source,
new TypeReference<List<DegradeRule>>(){}
);
private Converter<String,List<SystemRule>> systemRuleListParser=source-> JSON.parseObject(
source,
new TypeReference<List<SystemRule>>(){}
);
private Converter<String,List<AuthorityRule>> authorityRuleListParser=source-> JSON.parseObject(
source,
new TypeReference<List<AuthorityRule>>(){}
);
private Converter<String,List<ParamFlowRule>> paramFlowRuleListParser=source-> JSON.parseObject(
source,
new TypeReference<List<ParamFlowRule>>(){}
);
private void mkdirIfNotExists(String filePath) {
File file=new File(filePath);
if (!file.exists()){
file.mkdirs();
}
}
private void createFileIfNotExists(String filePath) throws IOException {
File file=new File(filePath);
if (!file.exists()){
file.createNewFile();
}
}
private <T> String encodeJson(T t){
return JSON.toJSONString(t);
}
}
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
sentinel的数据持久化是通过SPI机制实现的,因此需要在resource下新建文件夹META-INF/services,然后新建一个文件com.alibaba.csp.sentinel.init.InitFunc
文件中写入上面这个类的全限定名:
之后产生规则后就会在代码中设定的路径下产生json文件,重启后之前的配置也不会消失。
Push模式: 客户端通过注册监听器的方式时刻监听变化,比如使用Nacos、Zookeeper等配置中心,这种方式保证了很好的实时性和一致性,生产环境中一般采用push模式。我们用Nacos实现
1、添加sentinel-datasource-nacos依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
2
3
4
2、配置持久化数据源
spring.cloud.sentinel.datasource.ds1.nacos.server-addr=192.168.78.128:8848
spring.cloud.sentinel.datasource.ds1.nacos.data-id=${spring.application.name}.json
spring.cloud.sentinel.datasource.ds1.nacos.group-id=DEFAULT_GROUP
spring.cloud.sentinel.datasource.ds1.nacos.data-type=json
spring.cloud.sentinel.datasource.ds1.nacos.rule-type=flow
2
3
4
5
3、在Nacos中手动添加配置文件,这里的配置文件取的就是和本地配置文件相同格式
[
{
"clusterMode":false,
"controlBehavior":0,
"count":2,
"grade":1,
"limitApp":"default",
"maxQueueingTimeMs":500,
"resource":"/test",
"strategy":0,
"warmUpPeriodSec":10
}
]
2
3
4
5
6
7
8
9
10
11
12
13
push模式目前还有缺点,Nacos修改配置文件后可同步到Sentinel,但是在Sentinel中修改配置后无法同步到Nacos,需要手动去同步数据。
# 总结
工作中不一定会用到这些框架,但是我们需要有对应的知识储备。我是鱼仔,我们下期再见!