五、Spring Cloud 集成Sentinel实践
1、Sentinel接入Spring Cloud
创建一个基于Spring Boot的项目
添加Sentinel依赖包
1
2
3
4
5<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>创建一个REST接口,并通过@SentinelResource 配置限流保护资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class HelloController {
"hello",blockHandler = "blockHandlerHello") (value =
"/say") (
public String hello(){
return "hello cs-go";
}
public String blockHandlerHello(BlockException e){
return "被限流了";
}
}- Sentinel starter 在默认情况下会为所有的HTTP服务提供限流埋点,所有如果只想对HTTP服务进行限流,那么只需要添加依赖即可,不需要修改任何代码。
- 如果想要对特定的方法进行限流或者降级,则需要通过@SentinelResouce注解来实现限流资源的定义
- 可以通过 SphU.entry()方法来配置资源
手动配置流控规则,可以借用Sentinel的InitFunc SPI 扩展接口来实现
1
2
3
4
5
6
7
8
9
10
11
12
13public class FlowRuleInitFunc implements InitFunc {
public void init() throws Exception {
List<FlowRule> flowRuleList = new ArrayList<>();
FlowRule rule = new FlowRule();
rule.setCount(1);
rule.setResource("hello");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setLimitApp("default");
flowRuleList.add(rule);
FlowRuleManager.loadRules(flowRuleList);
}
}SPI 是扩展点机制,如果需要被Sentinel加载,要么还要在resouce目录下创建
META-INF\services\com.alibaba.csp.sentinel.init.InitFunc
按照上述配置好后,在初次访问任何资源的时候,Sentinel就会自动加载hello资源的流控规则启动服务后,访问localhost:8080/say方法, 当访问频率超过设定阈值时,就会触发限流。
2、基于Sentinel Dashboard 来实现流控配置
启动 Sentinel Dashboard
在application.properties添加配置:
1
2
3spring-cloud-sentinel-sample =
# 指的是Sentinel Dashboard的服务器地址,可以实现流控数据的监控和流控规则的分发
localhost:8080 =提供一个REST接口
1
2
3
4
5
6
7
8
9
10
public class DashboardController {
// 此处不需要添加任何的资源埋点,在默认情况下Sentinel Starter会对所有Http请求进行限流。
"/dash") (
public String dash(){
return "hello dash";
}
}启动服务后,此时访问 /dash接口,由于没有配置流控规则,所有不存在流控行为。
集成Sentinel就配置完成了,接下来,进入Sentinel Dashboard来实现流控规则的配置
访问 Sentinel Dashboard的地址
点击”簇点链接“,点击“流控”,设置单机阈值为1
添加完成后,访问对应接口,当QPS超过1时,就可以看到限流的效果
1
Blocked by Sentinel(flow limiting)
3、自定义URL限流异常
在默认情况下,URL触发限流后会直接返回。
1 | Blocked by Sentinel(flow limiting) |
在实际应用中,大都采用 JSON 格式的数据,所以如果希望修改触发限流之后的返回结果形式,则可以通过自定义限流异常来处理,实现BlockExceptionHandler接口并重写handle方法:
1 |
|
还有一个场景时,当触发限流之后,我们希望直接跳转到一个降级页面,可以通过下面这个配置来实现
1 | {url} = |
4、URL 资源清洗
Sentinel中的HTTP服务限流默认由Sentinel-Web-Servlet 包中的CommonFilter 来实现,从代码中可以看到,这个Filter会把每个URL 都作为不同的资源来处理。
但如果是携带{id}参数的 REST 风格 API (例如:@GetMapping(“clean/{id}”)),对于每一个不同的{id},URL 也都是不一样的,所以默认情况下 Sentinel 会把所有的URL都当作资源来进行流控。
这样会导致两个问题
- 限流统计的不准确,实际需求是控制clean方法总的QPS,结果统计的是每个URL的QPS。
- 导致Sentinel中的资源数量过多,默认资源数量的阈值是6000,对于多出来的资源规则将不会生效。
针对这个问题可以通过 UrlCleaner 接口来实现资源清洗,也就是对于/clean/{id} 这个Url我们可以统一归集到/clean/* 资源下,具体配置代码如下,实现UrlCleaner 接口,并重写clean方法即可。
1 |
|
六、Sentinel 集成 Nacos 实现动态流控规则
Sentinel的理念是只需要开发者关注资源的定义,他会默认对资源进行流控,当然,我们还需要对定义的资源设置流控规则:
- 通过FlowRuleManager.loadRules (List rules) 手动加载流控规则。
- 在Sentinel Dashboard 上针对资源动态创建流控规则。
基于Sentinel Dashboard所配置的流控规则,都是保存在内存中的,一旦重启,这些规则就会被清除。
Sentinel还支持很多数据源的扩展,下面演示Spring Cloud Sentinel 集成 Nacos 实现动态流控规则:
添加数据源的依赖包
1
2
3
4
5<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>1.7.0</version>
</dependency>创建REST接口
1
2
3
4
5
6
7
8
9
public class DynamicController {
"/dynamic") (
public String dynamic(){
return "HELLO Dynamic Rule";
}
}添加数据源配置
1
2
3
4
5localhost:8848 =
${spring.application.name}-sentinel =
DEFAULT_GROUP =
json =
flow =登录Nacos 控制台,创建流控配置规则
登录 Sentinel Dashboard ,找到执行项目名称菜单下的“流控规则”,就可以看到在Nacos上所配置的流控规则已经被加载了
当我们在Nacos上修改流控规则后,可以同步在Sentinel Dashboard 上看到流控规则的变化
问题:在Nacos控制台上修改流控规则,虽然可以同步到Sentinel Dashboard ,但是Nacos此时应该作为一个流控规则的持久化平台,所以正常的操作过程应该是开发者在Sentinel Dashboard 上修改流控规则后同步到Nacos上,遗憾的是,目前Sentinel Dashboard没有这个功能。直接在Nacos上修改流控规则,这种修改方式的危险系数很高,毕竟Sentinel Dashboard的 UI 才是专门负责流控规则维护的。
所以我们需要实现Sentinel Dashboard来动态维护流控规则并同步到Nacos上,目前官方没有提供支持,但是大家可以自己来实现。
七、 Sentinel Dashboard 集成Nacos实现规则同步
Sentinel Dashboard 的”流控规则“下的所有操作,都会调用 Sentinel Dashboard源码中的 FlowControllerV1这个类,这个类中包含流控规则本地化的CRUD操作。
另外,包下还存在一个FlowControllerV2类,这个类同样提供流控规则的CRUD,和V1版本不同的是,它可以实现指定数据源的规则拉取和发布,下面是部分代码:
1 |
|
这里我们扩展这两个类,然后集成Nacos来实现Sentinel Dashboard 规则的同步。
1、 Sentinel Dashboard 源码修改
下载 Sentinel Dashboard 源码并打开工程
在pom.xml文件中把sentinel-datasource-nacos依赖的
注释掉 1
2
3
4
5<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<!-- <scope>test</scope>-->
</dependency>修改 src\main\webapp\resources\app\scripts\directives\sidebar\sidebar.html 文件下的如下代码,将dashboard.flowV1 改为 dashboard.flow
1
2
3
4
5
6<li ui-sref-active="active" ng-if="!entry.isGateway">
<!-- <a ui-sref="dashboard.flowV1({app: entry.app})">-->
<a ui-sref="dashboard.flow({app: entry.app})">
<i class="glyphicon glyphicon-filter"></i> 流控规则
</a>
</li>修改之后,会调用FlowControllerV2中的接口
在com.alibaba.csp.sentinel.dashboard.rule 包中创建一个nacos包,并创建一个类用来加载外部化配置。
1
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
42package com.alibaba.csp.sentinel.dashboard.rule.nacos;
"sentinel.nacos") (prefix =
public class NacosPropertiesConfiguration {
private String serverAddr;
private String dataId;
private String groupId = "DEFAULT_GROUP";
private String namespace;
public String getServerAddr() {
return serverAddr;
}
public void setServerAddr(String serverAddr) {
this.serverAddr = serverAddr;
}
public String getDataId() {
return dataId;
}
public void setDataId(String dataId) {
this.dataId = dataId;
}
public String getGroupId() {
return groupId;
}
public void setGroupId(String groupId) {
this.groupId = groupId;
}
public String getNamespace() {
return namespace;
}
public void setNamespace(String namespace) {
this.namespace = namespace;
}
}创建一个Nacos配置类NacosConfiguration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23.class) (NacosPropertiesConfiguration
@Configuration
public class NacosConfiguration {
public Converter<List<FlowRuleEntity>,String> flowRuleEntityEncoder(){
return JSON::toJSONString;
}
public Converter<String,List<FlowRuleEntity>> flowRuleEntityDecoder(){
return s -> JSON.parseArray(s,FlowRuleEntity.class);
}
public ConfigService nacosConfigService(NacosPropertiesConfiguration nacosPropertiesConfiguration) throws NacosException {
Properties properties = new Properties();
properties.put(PropertyKeyConst.SERVER_ADDR,nacosPropertiesConfiguration.getServerAddr());
properties.put(PropertyKeyConst.NAMESPACE,nacosPropertiesConfiguration.getNamespace());
return ConfigFactory.createConfigService(properties);
}
}注入Converter 转换器,将FlowRuleEntity 转化为 FlowRule,以及反向转化
注入Nacos配置服务ConfigService
创建一个常量类 NacosConstants ,分别表示默认的 GROUP_ID 和 DATA_ID 的后缀
1
2
3
4
5
6
7public class NacosConstants {
public static final String DATA_ID_POSTFIX = "-sentinel-flow";
public static final String GROUP_ID = "DEFAULT_GROUP";
}实现动态从Nacos配置中心获取流控规则
1
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
public class FlowRuleNacosProvider implements DynamicRuleProvider<List<FlowRuleEntity>> {
private static Logger logger = LoggerFactory.getLogger(FlowRuleNacosProvider.class);
private NacosPropertiesConfiguration nacosConfigProperties;
private ConfigService configService;
private Converter<String, List<FlowRuleEntity>> converter;
public List<FlowRuleEntity> getRules(String appName) throws Exception {
String dataID = new StringBuilder(appName).append(NacosConstants.DATA_ID_POSTFIX).toString();
// 通过该方法从Nacos Config Server中读取指定配置信息,并通过converter转化为FlowRule 规则
String rules = configService.getConfig(dataID,
nacosConfigProperties.getGroupId(), 3000);
logger.info("pull FlowRule from Nacos Config:{}",rules);
if (StringUtil.isEmpty(rules)) {
return new ArrayList<>();
}
return converter.convert(rules);
}
}创建一个流控规则发布类,在Sentinel Dashboard上修改完配置之后,需要调用该发布方法将数据持久化到Nacos中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class FlowRuleNacosPublisher implements DynamicRulePublisher<List<FlowRuleEntity>> {
private NacosPropertiesConfiguration nacosPropertiesConfiguration;
private ConfigService configService;
private Converter<List<FlowRuleEntity>, String> converter;
public void publish(String appName, List<FlowRuleEntity> rules) throws Exception {
AssertUtil.notEmpty(appName, "appName cannot be empty");
if (rules == null){
return;
}
String dataID = new StringBuilder(appName).append(NacosConstants.DATA_ID_POSTFIX).toString();
configService.publishConfig(dataID,nacosPropertiesConfiguration.getGroupId(),converter.convert(rules));
}
}修改FlowControllerV2类,将上面配置的两个类注入进来,表示规则的拉取和规则的发布统一用我们之前自定义的两个实例
1
2
3
4
5
6
7
8
// @Qualifier("flowRuleDefaultProvider")
"flowRuleNacosProvider") (
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
// @Qualifier("flowRuleDefaultPublisher")
"flowRuleNacosPublisher") (
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;在application.properties 文件中添加Nacos服务端的配置信息
1
2
3localhost:8848 =
=
DEFAULT_GROUP =使用以下命令将代码打包成一个 fat jar ,根据前面介绍的操作方法启动服务。
1
mvn clean package
2、Sentinel Dashboard 规则数据同步
应用程序改动极少,只需要把配置文件中data-id的命名要以-sentinel-flow结尾就行,因为在Sentinel Dashboard中我们写了一个固定的后缀
1 | ${spring.application.name}-sentinel-flow = |
后面就可以通过在Sentinel Dashboard 修改流控规则,会同步到nacos控制台了。
八、Dubbo 集成 Sentinel 实现限流
Sentinel 提供了与Dubbo 整合的模块 sentinel-apache-dubbo-adapter,可以针对服务提供方和服务消费方进行流控,在使用的时候,只需要添加依赖:
1 | <dependency> |
添加好依赖后,Dubbo服务中的接口和方法(包括服务端和消费端)就会成为Sentinel中的资源,只需针对指定资源配置流控规则就可以实现Sentinel流控功能。
sentinel-apache-dubbo-adapter实现限流的核心原理是:基于Dubbo的SPI机制实现Filter扩展,Dubbo的Filter机制是专门为服务提供方和服务消费方调用过程进行拦截设计的,每次执行远程方法,该拦截都会被执行。
同时,sentinel-apache-dubbo-adapter还可以自定义开启或关闭某个过滤器的功能,下面这段代码表示关闭消费端的过滤器。
1 |
|
1、Dubbo服务接入Sentinel Dashborad
spring-cloud-starter-alibaba-sentinel 目前无法支持Dubbo服务的限流,所以针对Dubbo服务的限流只能使用 sentinel-apache-dubbo-adapter。这个适配组件需要手动接入 Sentinel Dashborad。
引入依赖,这个依赖可以上报应用相关信息到控制台。
1
2
3
4
5<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>1.7.1</version>
</dependency>添加启动参数
1
2
3
4-Djava.net.preferIPv4Stack=true 表示只支持IPv4。
-Dcsp.sentinel.api.port=8720 客户端的port,用于上报应用的信息
-Dscp.sentinel.dashborad.server=192.168.215.128:7777 Sentinel Dashborad地址
-Dproject.name=spring-cloud.sentinel-dubbo.provider 应用名称,会在Sentinel Dashboard 右侧展示登录 Sentinel Dashboard后,进入“簇点链路” ,就可以看到Dbbo资源信息。
注意:限流可以通过服务接口或服务方法设置
- 服务接口:resourceName 为接口的全限定名。
- 服务方法:resourceName 为接口全限定名:方法名。
2、Dubbo 服务限流规则配置
略