当sentinel dashboard配置规则后推送给微服务,微服务会存在内存中,但是一旦重启 规则就会消失,这种方式并不建议在生产环境中使用,为此我们需要对sentinel的源码进行改造,让他支持持久化。
Sentinel持久化模式 Sentinel一共有3种推送模式:
推送模式
说明
优点
缺点
原始模式
API 将规则推送至客户端并直接更新到内存中,扩展写数据源(WritableDataSource)
简单,无任何依赖
不保证一致性;规则保存在内存中,重启即消失。严重不建议用于生产环境
Pull 模式
扩展写数据源(WritableDataSource), 客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件 等
简单,无任何依赖;规则持久化
不保证一致性;实时性不保证,拉取过于频繁也可能会有性能问题。
Push 模式
扩展读数据源(ReadableDataSource),规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 Nacos、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。生产环境下一般采用 push 模式的数据源。
规则持久化;一致性;快速
引入第三方依赖
一句话总结:生产环境建议使用Push 推送模式,而客户端通过监听器来监听配置中心的配置的变化而更新sentinel规则。
1)原始模式 Dashboard 的推送规则方式是通过 API 将规则推送至客户端,而客户端会更新到内存中:
缺点:由于规则是保存在内存中并没有进行持久化,当客户端重启后,规则就会丢失,该模式强烈不建议在生产环境中使用。
2)pull 拉模式
首先 Sentinel 控制台通过 API 将规则推送至客户端并更新到内存中,接着注册的写数据源会将新的规则保存到本地的文件中。使用 pull 模式的数据源时一般不需要对 Sentinel 控制台进行改造。这种实现方法好处是简单,坏处是无法保证监控数据的一致性。
持久化流程
用户通过控制台添加规则
sentinel dashboard通过通信模块,推送给微服务模块的sentinel client
sentinel client更新到内存中并且 通过实现WritableDataSource接口,把规则保存到本地文件中
微服务A会有一个监控现场去监听本地文件的变化,当本地文件MD5发生变化后,就会把 最新的本地文件覆盖到内存中
2.1 pull模式核心源码 FileRefreshableDataSource
会周期性(默认3秒)的去获取策略文件并解析规则,如果发现文件发现了变动,就会将规则更新到内存中
父类方法会创建一个定时任务,每3秒去检测一次 本地文件是否更新,如果更新的话就加载最新的配置文件到内存中。
读取配置方法
2.2 改造的扩展点 在改造pull模式之前,我们可以根据不同的框架来决定采用哪个扩展点进行 改造,不同的框架扩展点不同,具体如下:
spi
这里我们通过sentinel自己的spi接口来实现持久化
spring
beanPostProcessor
beanFactoryPostProcessor
SmartInitializingSingleton
ApplicationListener
FactoryBean
springBoot
2.3 改造pull模式 引入pom依赖
1 2 3 4 5 6 7 <dependencies > <dependency > <groupId > com.javaxing</groupId > <artifactId > sentinel-datasource-extension-pull</artifactId > <version > 1.0.0</version > </dependency > </dependencies >
引入私人仓库
上面的依赖,主要是实现了对pull的持久化改造,我们给他打包jar包上传到了私人的maven仓库,所以要在项目中引入该仓库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <repositories > <repository > <id > nexus-xu</id > <name > Nexus Central</name > <url > https://maven.javaxing.com/repository/sentinel/</url > <layout > default</layout > <releases > <enabled > true</enabled > </releases > <snapshots > <enabled > true</enabled > </snapshots > </repository > </repositories >
测试改造效果
我们通过sentinel设置策略,QPS 阈值为1 ,1秒内访问2次就会被限流:
重启服务端
一般情况下,当我们 java服务端重启后,再次访问该controller后,sentinel dashboard的流控规则会丢失。但是由于我们引入了pull改造依赖包,当重启后,流控规则不会消失。
再次访问controller:
sentinel dashboard控制台
控制台的流控规则依然存在,因为当服务重启后会从本地文件中拉取规则到内存中,并创建一个定时任务去监听该本地文件,如果该文件发生了变动的话会马上更新到内存中。如果控制台会从服务中的内存拉取规则到控制台。
2.4 pull拉模式改造细节 2.3 我们引入了jar包后,直接可以实现pull模式的持久化,但是具体是如何实现的呢,下面我们来大致的分析一下实现的代码。
实现InitFunc接口,在init中处理DataSource初始化逻辑,并利用spi机制实现加载。
核心实现代码
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 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 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 package com.javaxing.sentinel.extension.filepull;import com.alibaba.csp.sentinel.command.handler.ModifyParamFlowRulesCommandHandler;import com.alibaba.csp.sentinel.datasource.FileRefreshableDataSource;import com.alibaba.csp.sentinel.datasource.FileWritableDataSource;import com.alibaba.csp.sentinel.datasource.ReadableDataSource;import com.alibaba.csp.sentinel.datasource.WritableDataSource;import com.alibaba.csp.sentinel.init.InitFunc;import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule;import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRuleManager;import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule;import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRuleManager;import com.alibaba.csp.sentinel.slots.system.SystemRule;import com.alibaba.csp.sentinel.slots.system.SystemRuleManager;import com.alibaba.csp.sentinel.transport.util.WritableDataSourceRegistry;import java.io.FileNotFoundException;import java.util.List;public class FileDataSourceInit implements InitFunc { @Override public void init () throws Exception { RuleFileUtils.mkdirIfNotExits(PersistenceRuleConstant.storePath); RuleFileUtils.createFileIfNotExits(PersistenceRuleConstant.rulesMap); dealFlowRules(); dealDegradeRules(); dealSystemRules(); dealParamFlowRules(); dealAuthRules(); } private void dealFlowRules () throws FileNotFoundException { String ruleFilePath = PersistenceRuleConstant.rulesMap.get(PersistenceRuleConstant.FLOW_RULE_PATH).toString(); ReadableDataSource<String, List<FlowRule>> flowRuleRDS = new FileRefreshableDataSource ( ruleFilePath, RuleListConverterUtils.flowRuleListParser ); FlowRuleManager.register2Property(flowRuleRDS.getProperty()); WritableDataSource<List<FlowRule>> flowRuleWDS = new FileWritableDataSource <List<FlowRule>>( ruleFilePath, RuleListConverterUtils.flowFuleEnCoding ); WritableDataSourceRegistry.registerFlowDataSource(flowRuleWDS); } private void dealDegradeRules () throws FileNotFoundException { String degradeRuleFilePath = PersistenceRuleConstant.rulesMap.get(PersistenceRuleConstant.DEGRAGE_RULE_PATH).toString(); ReadableDataSource<String, List<DegradeRule>> degradeRuleRDS = new FileRefreshableDataSource ( degradeRuleFilePath, RuleListConverterUtils.degradeRuleListParse ); DegradeRuleManager.register2Property(degradeRuleRDS.getProperty()); WritableDataSource<List<DegradeRule>> degradeRuleWDS = new FileWritableDataSource <>( degradeRuleFilePath, RuleListConverterUtils.degradeRuleEnCoding ); WritableDataSourceRegistry.registerDegradeDataSource(degradeRuleWDS); } private void dealSystemRules () throws FileNotFoundException { String systemRuleFilePath = PersistenceRuleConstant.rulesMap.get(PersistenceRuleConstant.SYSTEM_RULE_PATH).toString(); ReadableDataSource<String, List<SystemRule>> systemRuleRDS = new FileRefreshableDataSource ( systemRuleFilePath, RuleListConverterUtils.sysRuleListParse ); SystemRuleManager.register2Property(systemRuleRDS.getProperty()); WritableDataSource<List<SystemRule>> systemRuleWDS = new FileWritableDataSource <>( systemRuleFilePath, RuleListConverterUtils.sysRuleEnCoding ); WritableDataSourceRegistry.registerSystemDataSource(systemRuleWDS); } private void dealParamFlowRules () throws FileNotFoundException { String paramFlowRuleFilePath = PersistenceRuleConstant.rulesMap.get(PersistenceRuleConstant.HOT_PARAM_RULE).toString(); ReadableDataSource<String, List<ParamFlowRule>> paramFlowRuleRDS = new FileRefreshableDataSource ( paramFlowRuleFilePath, RuleListConverterUtils.paramFlowRuleListParse ); ParamFlowRuleManager.register2Property(paramFlowRuleRDS.getProperty()); WritableDataSource<List<ParamFlowRule>> paramFlowRuleWDS = new FileWritableDataSource <>( paramFlowRuleFilePath, RuleListConverterUtils.paramRuleEnCoding ); ModifyParamFlowRulesCommandHandler.setWritableDataSource(paramFlowRuleWDS); } private void dealAuthRules () throws FileNotFoundException { String authFilePath = PersistenceRuleConstant.rulesMap.get(PersistenceRuleConstant.AUTH_RULE_PATH).toString(); ReadableDataSource<String, List<AuthorityRule>> authRuleRDS = new FileRefreshableDataSource ( authFilePath, RuleListConverterUtils.authorityRuleParse ); AuthorityRuleManager.register2Property(authRuleRDS.getProperty()); WritableDataSource<List<AuthorityRule>> authRuleWDS = new FileWritableDataSource <>( authFilePath, RuleListConverterUtils.authorityEncoding ); WritableDataSourceRegistry.registerAuthorityDataSource(authRuleWDS); } }
3)push 推模式 3.1 pull模式的缺点 1、限流策略的实时性
由于我们pull模式会在本地创建一个线程,该线程会创建一个定时任务 每3秒去检测一下本地策略文件是否发生变更,变更的话就把文件更新到内存中。
定时任务是每3秒检测一次,如果我们推送了策略后,就会存在2秒的真空期,无法做到策略的实时性。
2、多服务推送问题
如果服务B和服务A都提供相同的业务,而用户访问controller时,落到了服务A上,sentinel dashboard 创建某个流控策略时,就会推送给服务A,而服务B 的本地文件则不会有该策略,导致服务B无法达到流控,两台服务之间的流控策略会存在不一致的情况。
3.2 Sentinel Nacos更新内存源码
sentinel client 会去nacos中拉取数据后,更新到内存中。
注意:下面的代码并没有实现更新到内存的代码,而是从nacos中读取配置数据,并且监听nacos的配置文件变化。
而更新内存的代码,是通过继承AbstractDataSource这个方法,这个方法会去实现ReadableDataSrouce 读数据源接口,AbstractDataSource会把流控策略更新到内存中。
nacos构造方法
构造方法的父类方法会获取到peroperty
从peroperty获取nacos中的数据,并创建一个监听器,监听文件ID的数据变化,如果文件数据发生变化就会更新到内存中
初始化监听器
加载配置数据
完整代码
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 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 132 133 134 135 136 137 package com.alibaba.csp.sentinel.datasource.nacos;import com.alibaba.csp.sentinel.concurrent.NamedThreadFactory;import com.alibaba.csp.sentinel.datasource.AbstractDataSource;import com.alibaba.csp.sentinel.datasource.Converter;import com.alibaba.csp.sentinel.log.RecordLog;import com.alibaba.csp.sentinel.util.AssertUtil;import com.alibaba.csp.sentinel.util.StringUtil;import com.alibaba.nacos.api.NacosFactory;import com.alibaba.nacos.api.PropertyKeyConst;import com.alibaba.nacos.api.config.ConfigService;import com.alibaba.nacos.api.config.listener.Listener;import java.util.Properties;import java.util.concurrent.*;public class NacosDataSource <T> extends AbstractDataSource <String, T> { private static final int DEFAULT_TIMEOUT = 3000 ; private final ExecutorService pool = new ThreadPoolExecutor (1 , 1 , 0 , TimeUnit.MILLISECONDS, new ArrayBlockingQueue <Runnable>(1 ), new NamedThreadFactory ("sentinel-nacos-ds-update" , true ), new ThreadPoolExecutor .DiscardOldestPolicy()); private final Listener configListener; private final String groupId; private final String dataId; private final Properties properties; private ConfigService configService = null ; public NacosDataSource (final String serverAddr, final String groupId, final String dataId, Converter<String, T> parser) { this (NacosDataSource.buildProperties(serverAddr), groupId, dataId, parser); } public NacosDataSource (final Properties properties, final String groupId, final String dataId, Converter<String, T> parser) { super (parser); if (StringUtil.isBlank(groupId) || StringUtil.isBlank(dataId)) { throw new IllegalArgumentException (String.format("Bad argument: groupId=[%s], dataId=[%s]" , groupId, dataId)); } AssertUtil.notNull(properties, "Nacos properties must not be null, you could put some keys from PropertyKeyConst" ); this .groupId = groupId; this .dataId = dataId; this .properties = properties; this .configListener = new Listener () { @Override public Executor getExecutor () { return pool; } @Override public void receiveConfigInfo (final String configInfo) { RecordLog.info("[NacosDataSource] New property value received for (properties: {}) (dataId: {}, groupId: {}): {}" , properties, dataId, groupId, configInfo); T newValue = NacosDataSource.this .parser.convert(configInfo); getProperty().updateValue(newValue); } }; initNacosListener(); loadInitialConfig(); } private void loadInitialConfig () { try { T newValue = loadConfig(); if (newValue == null ) { RecordLog.warn("[NacosDataSource] WARN: initial config is null, you may have to check your data source" ); } getProperty().updateValue(newValue); } catch (Exception ex) { RecordLog.warn("[NacosDataSource] Error when loading initial config" , ex); } } private void initNacosListener () { try { this .configService = NacosFactory.createConfigService(this .properties); configService.addListener(dataId, groupId, configListener); } catch (Exception e) { RecordLog.warn("[NacosDataSource] Error occurred when initializing Nacos data source" , e); e.printStackTrace(); } } @Override public String readSource () throws Exception { if (configService == null ) { throw new IllegalStateException ("Nacos config service has not been initialized or error occurred" ); } return configService.getConfig(dataId, groupId, DEFAULT_TIMEOUT); } @Override public void close () { if (configService != null ) { configService.removeListener(dataId, groupId, configListener); } pool.shutdownNow(); } private static Properties buildProperties (String serverAddr) { Properties properties = new Properties (); properties.setProperty(PropertyKeyConst.SERVER_ADDR, serverAddr); return properties; } }
实现Push推送模式 生产环境下,一般都使用push推送模式的形式来做持久化,pull模式明显是非常不可取的。
而pull模式和push的区别在于,pull模式是由 微服务客户端主动去本地拉取数据并推送给sentinel dashboard,这明显是不合理的,推送的动作应该由 Nacos(配置中心)或sentinel dashboard来做。
正确做法:sentinel dashboard/nacos控制台推送给配置中心(nacos),nacos推送给 微服务客户端,微服务收到后更新rules到内存。
1)基于Nacos控制台实现推送
用户通过nacos控制台 添加规则
nacos控制台 把规则推送到 nacos(配置中心)
nacos会对该配置进行持久化(看具体的配置,一般建议存入mysql中)
由于微服务启动时,nacos client会监听配置中心的文件ID变化,如果配置文件发生了变化,会触发监听器的回调方法,把配置更新到内存中
源码/工具
springBoot集成sentinel实现nacos push推送模式源码:https://files.javaxing.com/sentinel/SpringSentinelDemo-nacos-push.zip
1.1 引入依赖 1 2 3 4 5 <dependency > <groupId > com.alibaba.csp</groupId > <artifactId > sentinel-datasource-nacos</artifactId > </dependency >
1.2 nacos配置中心配置策略
1 2 3 4 5 6 7 8 9 10 [ { "resource" : "/springBootSentinelSuccess" , "controlBehavior" : 0 , "count" : 1.0 , "grade" : 1 , "limitApp" : "default" , "strategy" : 0 } ]
1.3 配置yml 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 spring: application: name: sentinel cloud: nacos: config: server-addr: 10.211 .55 .12 :8848 discovery: server-addr: 10.211 .55 .12 :8848 sentinel: transport: dashboard: 10.211 .55 .12 :9991 web-context-unify: false datasource: ds1: nacos: server-addr: 10.211 .55 .12 :8848 dataId: ${spring.application.name}-flow groupId: DEFAULT_GROUP data-type: json rule-type: flow management: endpoints: web: exposure: include: '*'
1.4 sentinel dashboard
nacos配置策略后,会推送给微服务。微服务收到策略后会更新到内存中,而sentinel dashboard 会从微服务内存中获取数据。
1.5 该方法缺点 直接在Sentinel Dashboard中修改规则配置 ,配置中心的配置不会发生变化。
因为我们sentinel dashboard的数据来自微服务内存,sentinel dash修改策略后,也只会推送给微服务内存,并不会推送给nacos。
为了解决这个问题,我们得采用第二种办法,修改sentinel dashboard源码,让dashboard修改策略后推送给nacos。
2)基于Sentinel控制台实现推送(推荐) 除了nacos外,我们也可以通过sentinel dashboard来实现推送,思路为:
用户通过sentinel dashboard 添加规则
sentinel dashboard把规则推送到 nacos(配置中心)
nacos会对该配置进行持久化(看具体的配置,一般建议存入mysql中)
由于微服务启动时,nacos client会监听配置中心的文件ID变化,如果配置文件发生了变化,会触发监听器的回调方法,把配置更新到内存中
从 Sentinel 1.4.0 开始,Sentinel 控制台提供 DynamicRulePublisher 和 DynamicRuleProvider 接口用于实现应用维度的规则推送和拉取:
DynamicRuleProvider: 拉取规则
DynamicRulePublisher: 推送规则
源码/工具
springBoot集成sentinel实现dashboard push推送模式源码:https://files.javaxing.com/sentinel/SpringSentinelDemo.zip
sentinel dashboard改造后的jar包:https://files.javaxing.com/sentinel/sentinel-dashboard-push.jar
2.1 规则拉取和规则推送实现方法 在com.alibaba.csp.sentinel.dashboard.rule包下创建nacos包,然后把各种场景的配置规则拉取和推送的实现类写到此包下:
从Nacos拉取规则
推送规则到Nacos
2.2 控制台从nacos拉取策略
我们修改控制台获取策略的controller,让其从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 42 43 44 45 46 47 @Autowired @Qualifier("flowRuleNacosProvider") private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider; @Autowired @Qualifier("flowRuleNacosPublisher") private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher; @GetMapping("/rules") @AuthAction(PrivilegeType.READ_RULE) public Result<List<FlowRuleEntity>> apiQueryMachineRules (@RequestParam String app, @RequestParam String ip, @RequestParam Integer port) { if (StringUtil.isEmpty(app)) { return Result.ofFail(-1 , "app can't be null or empty" ); } if (StringUtil.isEmpty(ip)) { return Result.ofFail(-1 , "ip can't be null or empty" ); } if (port == null ) { return Result.ofFail(-1 , "port can't be null" ); } try { List<FlowRuleEntity> rules = ruleProvider.getRules(app,ip,port); if (rules != null && !rules.isEmpty()) { for (FlowRuleEntity entity : rules) { entity.setApp(app); if (entity.getClusterConfig() != null && entity.getClusterConfig().getFlowId() != null ) { entity.setId(entity.getClusterConfig().getFlowId()); } } } rules = repository.saveAll(rules); return Result.ofSuccess(rules); } catch (Throwable throwable) { logger.error("Error when querying flow rules" , throwable); return Result.ofThrowable(-1 , throwable); } }
2.3 控制台推送策略到nacos
我们修改控制台新增策略的controller,让其收到策略后推送到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 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 @PostMapping("/rule") @AuthAction(PrivilegeType.WRITE_RULE) public Result<FlowRuleEntity> apiAddFlowRule (@RequestBody FlowRuleEntity entity) { Result<FlowRuleEntity> checkResult = checkEntityInternal(entity); if (checkResult != null ) { return checkResult; } entity.setId(null ); Date date = new Date (); entity.setGmtCreate(date); entity.setGmtModified(date); entity.setLimitApp(entity.getLimitApp().trim()); entity.setResource(entity.getResource().trim()); try { entity = repository.save(entity); publishRules(entity.getApp()); return Result.ofSuccess(entity); } catch (Throwable t) { Throwable e = t instanceof ExecutionException ? t.getCause() : t; logger.error("Failed to add new flow rule, app={}, ip={}" , entity.getApp(), entity.getIp(), e); return Result.ofFail(-1 , e.getMessage()); } } private <R> Result<R> checkEntityInternal (FlowRuleEntity entity) { if (StringUtil.isBlank(entity.getApp())) { return Result.ofFail(-1 , "app can't be null or empty" ); } if (StringUtil.isBlank(entity.getIp())) { return Result.ofFail(-1 , "ip can't be null or empty" ); } if (entity.getPort() == null ) { return Result.ofFail(-1 , "port can't be null" ); } if (StringUtil.isBlank(entity.getLimitApp())) { return Result.ofFail(-1 , "limitApp can't be null or empty" ); } if (StringUtil.isBlank(entity.getResource())) { return Result.ofFail(-1 , "resource can't be null or empty" ); } if (entity.getGrade() == null ) { return Result.ofFail(-1 , "grade can't be null" ); } if (entity.getGrade() != 0 && entity.getGrade() != 1 ) { return Result.ofFail(-1 , "grade must be 0 or 1, but " + entity.getGrade() + " got" ); } if (entity.getCount() == null || entity.getCount() < 0 ) { return Result.ofFail(-1 , "count should be at lease zero" ); } if (entity.getStrategy() == null ) { return Result.ofFail(-1 , "strategy can't be null" ); } if (entity.getStrategy() != 0 && StringUtil.isBlank(entity.getRefResource())) { return Result.ofFail(-1 , "refResource can't be null or empty when strategy!=0" ); } if (entity.getControlBehavior() == null ) { return Result.ofFail(-1 , "controlBehavior can't be null" ); } int controlBehavior = entity.getControlBehavior(); if (controlBehavior == 1 && entity.getWarmUpPeriodSec() == null ) { return Result.ofFail(-1 , "warmUpPeriodSec can't be null when controlBehavior==1" ); } if (controlBehavior == 2 && entity.getMaxQueueingTimeMs() == null ) { return Result.ofFail(-1 , "maxQueueingTimeMs can't be null when controlBehavior==2" ); } if (entity.isClusterMode() && entity.getClusterConfig() == null ) { return Result.ofFail(-1 , "cluster config should be valid" ); } return null ; } private void publishRules ( String app) throws Exception { List<FlowRuleEntity> rules = repository.findAllByApp(app); rulePublisher.publish(app, rules); }
2.4 微服务接入新的sentinel dashboard 引入依赖
引入sentinel nacos数据源的依赖
1 2 3 4 5 <dependency > <groupId > com.alibaba.csp</groupId > <artifactId > sentinel-datasource-nacos</artifactId > </dependency >
修改yml配置
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 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 spring: application: name: sentinel cloud: nacos: config: server-addr: 10.211 .55 .12 :8848 discovery: server-addr: 10.211 .55 .12 :8848 sentinel: transport: dashboard: 10.211 .55 .12 :9991 web-context-unify: false datasource: flow-rules: nacos: server-addr: 10.211 .55 .12 :8848 dataId: ${spring.application.name}-flow-rules groupId: SENTINEL_GROUP data-type: json rule-type: flow degrade-rules: nacos: server-addr: 10.211 .55 .12 :8848 dataId: ${spring.application.name}-degrade-rules groupId: SENTINEL_GROUP data-type: json rule-type: degrade param-flow-rules: nacos: server-addr: 10.211 .55 .12 :8848 dataId: ${spring.application.name}-param-flow-rules groupId: SENTINEL_GROUP data-type: json rule-type: param-flow authority-rules: nacos: server-addr: 10.211 .55 .12 :8848 dataId: ${spring.application.name}-authority-rules groupId: SENTINEL_GROUP data-type: json rule-type: authority system-rules: nacos: server-addr: 10.211 .55 .12 :8848 dataId: ${spring.application.name}-system-rules groupId: SENTINEL_GROUP data-type: json rule-type: system management: endpoints: web: exposure: include: '*'
需要指定流控策略,热点参数,熔断策略等 数据源的策略来源,sentinel.transport.datasource.flow-rules
sentinel dashboard添加流控策略
Nacos查看配置文件
当我们在sentinel dashboard配置流控策略后,会推送到nacos,所以我们在nacos中是可以看到对应的策略的。
测试接口
2.5 热点参数策略失效问题 注意:控制台改造后有可能出现规则不生效的情况,比如热点参数规则因为Converter解析json错误的原因会导致不生效。
参见源码:com.alibaba.csp.sentinel.datasource.AbstractDataSource#loadConfig(S) 会解析配置规则。
原因是:改造dashboard,提交到nacos配置中心的数据是ParamFlowRuleEntity类型,微服务拉取配置要解析的是ParamFlowRule类型,会导致规则解析丢失数据,造成热点规则不生效。 其他的规则原理也是一样,存在失效的风险。
2.6 解决热点参数策略失效 解决方案一 自定义解析器 自定义一个解析热点规则配置的解析器FlowParamJsonConverter,继承JsonConverter,重写convert方法。然后利用后置处理器替换beanName为”param-flow-rules-sentinel-nacos-datasource”的converter属性,注入FlowParamJsonConverter。
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 42 43 44 45 46 47 48 @Configuration public class ConverterConfig { @Bean("sentinel-json-param-flow-converter2") @Primary public JsonConverter jsonParamFlowConverter () { return new FlowParamJsonConverter (new ObjectMapper (), ParamFlowRule.class); } } @Component public class FlowParamConverterBeanPostProcessor implements BeanPostProcessor { @Autowired private JsonConverter jsonParamFlowConverter; @Override public Object postProcessBeforeInitialization (Object bean, String beanName) throws BeansException { if (beanName.equals("param-flow-rules-sentinel-nacos-datasource" )) { NacosDataSourceFactoryBean nacosDataSourceFactoryBean = (NacosDataSourceFactoryBean) bean; nacosDataSourceFactoryBean.setConverter(jsonParamFlowConverter); return bean; } return bean; } } public class FlowParamJsonConverter extends JsonConverter { Class ruleClass; public FlowParamJsonConverter (ObjectMapper objectMapper, Class ruleClass) { super (objectMapper, ruleClass); this .ruleClass = ruleClass; } @Override public Collection<Object> convert (String source) { List<Object> list = new ArrayList <>(); JSONArray jsonArray = JSON.parseArray(source); for (int i = 0 ; i < jsonArray.size(); i++) { JSONObject jsonObject = (JSONObject) jsonArray.getJSONObject(i).get("rule" ); Object object = JSON.toJavaObject(jsonObject, ruleClass); list.add(object); } return list; } }
解决方案二 实体类转换(推荐) 改造Sentinel Dashboard控制台,发布配置时将ParamFlowRuleEntity转成ParamFlowRule类型 ,再发布到Nacos配置中心。从配置中心拉取配置后将ParamFlowRule转成ParamFlowRuleEntity。
从nacos拉取规则策略
从配置中心拉取配置到控制台时,FlowRule转换为FlowRuleEntity
推送规则到Nacos
将参数的类型从 FlowRuleEntity转成FlowRule类型(RuleEntity)