[附源码]SpringBoot整合Sentinel限流及Sentinel-Dashboard持久化规则到Nacos
当我们在项目中使用Sentinel进行指定接口的限流规则及熔断规则后,默认是放置在内存中的,当我们重启项目后,之前配置的限流熔断规则会被清空,导致限制规则失效.那么我们如何来持久化规则数据呢?通过官网文档我们可以看到,Sentinel datasource支持动态规则拓展,动态规则拓展的方式分别有:
拉模式: 客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件,甚至是 VCS 等。这样做的方式是简单,缺点是无法及时获取变更.
推模式: 规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 Nacos、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证.
这样显而易见,我们肯定是比较推荐使用推模式.那么支持推模式的规则持久化数据源支持哪些方式呢?
Pull-based: 动态文件数据源、Consul、Eureka
Push-based: Zookeeper、Redis、Nacos、Apollo、Etcd
在这篇内容当中呢就来讲一下如何使用nacos来进行规则数据的持久化.
文档地址:
https://sentinelguard.io/zh-cn/docs/dynamic-rule-configuration.html
SpringBoot项目配置
springboot版本为2.1.9.RELEASE, 引入:
spring-cloud-starter-alibaba-sentinel
sentinel=datasource-nacos
因为版本问题,exclusion掉sentinel-datasource-nacos包中的nacos-client包,改为自己引入.
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<exclusions>
<exclusion>
<artifactId>nacos-client</artifactId>
<groupId>com.alibaba.nacos</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>2.1.2</version>
</dependency>
项目yml文件配置
server:
port: 8081
servlet:
context-path: /biz
spring:
application:
name: test-biz
cloud:
nacos:
config:
server-addr: localhost:31844
file-extension: yml
timeout: 60000
namespace: 1d6f66a8-b9f2-401f-9af0-781a1fa4ddce
sentinel:
transport:
#上报信息端口
port: 8719
#sentinel dashboard地址及端口
dashboard: localhost:8090
datasource:
#限流配置
flow:
nacos:
server-addr: ${spring.cloud.nacos.config.server-addr}
namespace: ${spring.cloud.nacos.config.namespace}
data-id: ${spring.application.name}-flow-rules
group-id: SENTINEL_GROUP
rule-type: flow
#熔断配置
degrade:
nacos:
server-addr: ${spring.cloud.nacos.config.server-addr}
data-id: ${spring.application.name}-degrade-rules
namespace: ${spring.cloud.nacos.config.namespace}
group-id: SENTINEL_GROUP
rule-type: degrade
还可以自定义返回接口被限流后的提示信息
package com.common.sentinel.config;
import cn.hutool.core.util.StrUtil;
import com.alibaba.csp.sentinel.adapter.servlet.callback.DefaultUrlBlockHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import com.alibaba.csp.sentinel.slots.system.SystemBlockException;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.amazonaws.services.ecs.model.BlockedException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
@Component
@Slf4j
public class SentinelExceptionHandler extends DefaultUrlBlockHandler {
@Value("${spring.application.name}")
private String applicationName;
@Override
public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException ex) throws IOException {
String msg = "";
if (ex instanceof FlowException) {
msg = "请求量过多,请稍后重试";
}else if (ex instanceof DegradeException) {
msg = "请求量过多,服务被降级";
}if (ex instanceof ParamFlowException) {
msg = "请求量过多,参数限流";
}if (ex instanceof SystemBlockException) {
msg = "请求量过多,系统规则限流";
}if (ex instanceof AuthorityException) {
msg = "授权规则不通过";
}
response.setStatus(200);
response.setCharacterEncoding("utf-8");
response.setHeader("Content-Type", "application:charset=utf-8");
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(R.ok(msg)));
}
}
到这里呢我们的springBoot项目的整个限流配置就好了
Sentinel Dashboard更改
在这里使用的sentinel-dashboard版本为1.8.6,您也可以官网下载最新的版本.dashboard默认是,当启动springboot项目是会上报应用信息至dashboard,dashboard操作限流或者熔断规则时通过请求把规则数据发送到具体的项目里.那么我们如何把数据保存到nacos,再由nacos自动推送到项目里呢?此时我们可能要对sentinel-dashboard进行二次开发.
DashboardConfig类新增:
public static final String CONFIG_NACOS = "sentinel.config.nacos.host";
public static final String CONFIG_NACOS_NAMESPACE = "sentinel.config.nacos.namespace";
public static String getConfigNacos() {
return getConfigStr(CONFIG_NACOS);
}
public static String getConfigNacosNamespace() {
return getConfigStr(CONFIG_NACOS_NAMESPACE);
}
com.alibaba.csp.sentinel.dashboard.rule下新增nacos包.
复制rule包下的FlowRuleApiProvider跟FlowRuleApiPublisher类到nacos包下.分别改名FlowRuleNacosProvider,FlowRuleNacosPublisher.
然后注释掉rule包下的两个类的@Component注解.
nacos包下新增NacosConfig及NacosConfigUtils类.
NacosConfig用于在dashboard中初始化nacos的连接及命名空间
package com.alibaba.csp.sentinel.dashboard.rule.nacos;
import com.alibaba.csp.sentinel.dashboard.config.DashboardConfig;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.api.config.ConfigFactory;
import com.alibaba.nacos.api.config.ConfigService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
import java.util.Properties;
@Configuration
public class NacosConfig {
@Bean
public Converter<List<FlowRuleEntity>, String> flowRuleEntityEncoder() {
return JSON::toJSONString;
}
@Bean
public Converter<String, List<FlowRuleEntity>> flowRuleEntityDecoder() {
return s -> JSON.parseArray(s, FlowRuleEntity.class);
}
@Bean
public ConfigService nacosConfigService() throws Exception {
Properties properties = new Properties();
properties.put("serverAddr", DashboardConfig.getConfigNacos());
properties.put("namespace", DashboardConfig.getConfigNacosNamespace());
return ConfigFactory.createConfigService(properties);
}
}
NacosConfigUtil用于定义nacos相关data-id后缀跟group相关常量
package com.alibaba.csp.sentinel.dashboard.rule.nacos;
public final class NacosConfigUtil {
public static final String GROUP_ID = "SENTINEL_GROUP";
public static final String FLOW_DATA_ID_POSTFIX = "-flow-rules";
private NacosConfigUtil() {}
}
FlowRuleNacosProvider 类
package com.alibaba.csp.sentinel.dashboard.rule.nacos;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity;
import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.csp.sentinel.util.StringUtil;
import com.alibaba.nacos.api.config.ConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component("flowRuleNacosProvider")
public class FlowRuleNacosProvider implements DynamicRuleProvider<List<FlowRuleEntity>> {
@Autowired
private ConfigService configService;
@Autowired
private Converter<String, List<FlowRuleEntity>> converter;
@Override
public List<FlowRuleEntity> getRules(String appName) throws Exception {
String rules = configService.getConfig(appName + NacosConfigUtil.FLOW_DATA_ID_POSTFIX,
NacosConfigUtil.GROUP_ID, 3000);
if (StringUtil.isEmpty(rules)) {
return new ArrayList<>();
}
return converter.convert(rules);
}
}
FlowRuleNacosPublisher类
package com.alibaba.csp.sentinel.dashboard.rule.nacos;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity;
import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.csp.sentinel.util.AssertUtil;
import com.alibaba.nacos.api.config.ConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
@Component("flowRuleNacosPublisher")
public class FlowRuleNacosPublisher implements DynamicRulePublisher<List<FlowRuleEntity>> {
@Autowired
private ConfigService configService;
@Autowired
private Converter<List<FlowRuleEntity>, String> converter;
@Override
public void publish(String app, List<FlowRuleEntity> rules) throws Exception {
AssertUtil.notEmpty(app, "app name cannot be empty");
if (rules == null) {
return;
}
configService.publishConfig(app + NacosConfigUtil.FLOW_DATA_ID_POSTFIX,
NacosConfigUtil.GROUP_ID, converter.convert(rules));
}
}
修改FlowControllerV2类
@Autowired
@Qualifier("flowRuleDefaultProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
@Qualifier("flowRuleDefaultPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;
改为@Autowired
@Qualifier("flowRuleNacosProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
@Qualifier("flowRuleNacosPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;
修改webapp/resources/app/scripts/directives/sidebar下sidebar.html文件,放开42行流控规则 v1菜单,原来为注释
<li ui-sref-active="active" ng-if="entry.appType==0">
<a ui-sref="dashboard.flow({app: entry.app})">
<i class="glyphicon glyphicon-filter"></i> 流控规则 V1</a>
</li>
注释51行流控规则,原来为放开的,并把下面的dashboard.flowV1改为dashboard.flow<!-- <li ui-sref-active="active" ng-if="entry.isGateway">-->
<!-- <a ui-sref="dashboard.gatewayFlow({app: entry.app})">-->
<!-- <i class="glyphicon glyphicon-filter"></i> 流控规则</a>-->
<!-- </li>-->
<li ui-sref-active="active" ng-if="!entry.isGateway">
<a ui-sref="dashboard.flow({app: entry.app})">
<i class="glyphicon glyphicon-filter"></i> 流控规则</a>
</li>
此时就差不多完成了.接下来我们在dashboard启动参数上新增
-Dserver.port=8090
-Dcsp.sentinel.dashboard.server=localhost:8090
#不配置,dashboar上不会有dashboar模块显示
-Dproject.name=sentinel-dashboard
-Dsentinel.dashboard.auth.username=admin
-Dsentinel.dashboard.auth.password=admin
-Dsentinel.config.nacos.host=127.0.0.1:31844
-Dsentinel.config.nacos.namespace=1d6f66a8-b9f2-401f-9af0-781a1fa4ddce
启动项目及DashBoard
此时我们访问下biz的接口,刷新dashboard就会看到biz模块,sentinel-dashboar模块不配置name此处不会显示
此时我们给captcha接口新增一条流控规则后,此时我们在流控规则v1出就可以新增的规则了
然后我们在看看这条规则是否有存储在nacos里面呢?
这时我们就可以在nacos对应的命名空间下新增了一个flow-rules结尾的配置文件,点击编辑进去看到的就是我们配置的流控规则,然后我们请求下验证码接口看看是否生效了呢?
通过请求我们可以看到,我们设置的0线程生效了.然后您还可以试下在dashboar删除这条流控规则,看看流控是否解除了限制呢。
对ip及用户名限流
此时新增流控规则那的针对来源默认是default类型,也就是限制所有,通过在springboot项目中编写代码,我们还可以做到具体配置限制哪个ip,限制哪个用户,新增配置类如下:
package com.common.sentinel.config;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.csp.sentinel.adapter.servlet.callback.RequestOriginParser;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.alibaba.fastjson.JSONObject;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Objects;
@Slf4j
@Configuration
public class SentinelConfig {
@Value("${spring.application.name}")
private String applicationName;
@Bean("accountRequestOriginParser")
public RequestOriginParser accountRequestOriginParser() {
return (httpServletRequest -> {
List<FlowRule> flowRuleList = FlowRuleManager.getRules();
if (CollectionUtil.isNotEmpty(flowRuleList)) {
for (FlowRule rule : flowRuleList) {
//规则如果包含点或者冒号就表示限制ip,如果不是就表示限制用户,配置为default,不会走此
if (rule.getLimitApp().contains(":") || rule.getLimitApp().contains(".")) {
return judgeIp(httpServletRequest);
}else{
return judgeAccount(httpServletRequest);
}
}
}
return "";
});
}
private String judgeIp(HttpServletRequest httpServletRequest) {
try {
String ip = 获取ip方法
return ip;
} catch (Exception e) {
log.error("[sentinel限流]获取ip异常",e);
}
return "";
}
private String judgeAccount(HttpServletRequest httpServletRequest) {
try{
//获取用户方法
String userName = //获取用户
} catch (Exception e) {
log.error("[sentinel限流]获取用户失败",e);
}
}
return userName;
}
}
到此,整个持久化及限流过程就完成了.有兴趣的朋友还可以思考下,如何通过某些机制实现对接口的自动化限流.源码为二次开发后sentinel-dashboard
关注公众号,发送:"限流" 即可获取源码