05_Sentinel规则持久化之推模式
推模式
由拉模式改造流程来改造推模式思路:
该模式存在的问题:
控制台修改的规则要经过微服务端推送,才能同步到nacos配置中心,增加了微服务端的负载。
解决思路:
读数据源思路:
可以参考sentinel官方提供的Demo:com.alibaba.csp.sentinel.demo.datasource.nacos.NacosDataSourceDemo
private static void loadRules() { ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>(remoteAddress, groupId, dataId, source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {})); FlowRuleManager.register2Property(flowRuleDataSource.getProperty()); }
在构造函数com.alibaba.csp.sentinel.datasource.nacos.NacosDataSource#NacosDataSource中,会通过Nacos提供的API添加一个监听器用来监听配置变更:
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); // Update the new value to the property. getProperty().updateValue(newValue); } }; initNacosListener(); loadInitialConfig(); }
com.alibaba.csp.sentinel.datasource.nacos.NacosDataSource#initNacosListener
private void initNacosListener() { try { this.configService = NacosFactory.createConfigService(this.properties); // Add config listener. configService.addListener(dataId, groupId, configListener); } catch (Exception e) { RecordLog.warn("[NacosDataSource] Error occurred when initializing Nacos data source", e); e.printStackTrace(); } }
com.alibaba.csp.sentinel.datasource.nacos.NacosDataSource#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); } }
通过demo提供Api com.alibaba.csp.sentinel.demo.datasource.nacos.NacosConfigSender创建配置:
public static void main(String[] args) throws Exception { final String remoteAddress = "localhost:8848"; final String groupId = "Sentinel_Demo"; final String dataId = "com.alibaba.csp.sentinel.demo.flow.rule"; final String rule = "[\n" + " {\n" + " \"resource\": \"TestResource\",\n" + " \"controlBehavior\": 0,\n" + " \"count\": 5.0,\n" + " \"grade\": 1,\n" + " \"limitApp\": \"default\",\n" + " \"strategy\": 0\n" + " }\n" + "]"; ConfigService configService = NacosFactory.createConfigService(remoteAddress); System.out.println(configService.publishConfig(dataId, groupId, rule)); }
配置中心增加流控配置信息:
修改配置中心流控规则信息,限流阈值修改为10.0:
查看流控效果阈值已经变为10:
接下来就是整合以下核心代码到Spring Cloud中:
private static void loadRules() { ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>(remoteAddress, groupId, dataId, source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {})); FlowRuleManager.register2Property(flowRuleDataSource.getProperty()); }
可以通过spring扩展点,sentinel的spi机制等。
官方已经提供了整合,使用方式:
引入依赖:
<!--sentinel持久化 --> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency>
yml中增加配置信息:
sentinel: transport: # 添加sentinel的控制台地址 dashboard: 127.0.0.1:8080 # 指定应用与Sentinel控制台交互的端口,应用本地会起一个该端口占用的HttpServer #port: 8719 datasource: ds1: #名称自定义,唯一 nacos: server-addr: 127.0.0.1:8848 dataId: ${spring.application.name}-flow groupId: DEFAULT_GROUP data-type: json rule-type: flow
通过以上配置即完成了对从nacos中拉取sentinel配置规则信息的整合。
整合过程源码:
切入点在com.alibaba.csp.sentinel.datasource.nacos.NacosDataSource#NacosDataSource加载的构造函数中断点查看调用链路:
com.alibaba.cloud.sentinel.custom.SentinelDataSourceHandler利用Spring扩展点SmartInitializingSingleton进行加载,那么SentinelDataSourceHandler实在什么时候加载的?
在com.alibaba.cloud.sentinel.custom.SentinelAutoConfiguration的自动装配类中:
@Bean @ConditionalOnMissingBean public SentinelDataSourceHandler sentinelDataSourceHandler(DefaultListableBeanFactory beanFactory, SentinelProperties sentinelProperties, Environment env) { return new SentinelDataSourceHandler(beanFactory, sentinelProperties, env); }
自动注入SentinelProperties相关配置信息,即yml中配置的信息:
@Autowired private SentinelProperties properties;
@ConfigurationProperties( prefix = "spring.cloud.sentinel" ) @Validated public class SentinelProperties { private boolean eager = false; private boolean enabled = true; private String blockPage; private Map<String, DataSourcePropertiesConfiguration> datasource; private SentinelProperties.Transport transport; private SentinelProperties.Metric metric; private SentinelProperties.Servlet servlet; private SentinelProperties.Filter filter; private SentinelProperties.Flow flow; private SentinelProperties.Log log; private Boolean httpMethodSpecify; private Boolean webContextUnify; public SentinelProperties() { this.datasource = new TreeMap(String.CASE_INSENSITIVE_ORDER); this.transport = new SentinelProperties.Transport(); this.metric = new SentinelProperties.Metric(); this.servlet = new SentinelProperties.Servlet(); this.filter = new SentinelProperties.Filter(); this.flow = new SentinelProperties.Flow(); this.log = new SentinelProperties.Log(); this.httpMethodSpecify = false; this.webContextUnify = true; } ... }
在扩展点SmartInitializingSingleton的方法#afterSingletonsInstantiated中注册registerBeanDefinition名称为dataSourceName + "-sentinel-" + (String)validFields.get(0) + "-datasource":
public void afterSingletonsInstantiated() { this.sentinelProperties.getDatasource().forEach((dataSourceName, dataSourceProperties) -> { try { List<String> validFields = dataSourceProperties.getValidField(); if (validFields.size() != 1) { log.error("[Sentinel Starter] DataSource " + dataSourceName + " multi datasource active and won't loaded: " + dataSourceProperties.getValidField()); return; } AbstractDataSourceProperties abstractDataSourceProperties = dataSourceProperties.getValidDataSourceProperties(); abstractDataSourceProperties.setEnv(this.env); abstractDataSourceProperties.preCheck(dataSourceName); this.registerBean(abstractDataSourceProperties, dataSourceName + "-sentinel-" + (String)validFields.get(0) + "-datasource"); } catch (Exception var5) { log.error("[Sentinel Starter] DataSource " + dataSourceName + " build error: " + var5.getMessage(), var5); } }); }
核心代码#registerBean根据dataSourceName注册beanClass为NacosDataSourceFactoryBean的BeanDefinition:
private void registerBean(final AbstractDataSourceProperties dataSourceProperties, String dataSourceName) { BeanDefinitionBuilder builder = this.parseBeanDefinition(dataSourceProperties, dataSourceName); this.beanFactory.registerBeanDefinition(dataSourceName, builder.getBeanDefinition()); AbstractDataSource newDataSource = (AbstractDataSource)this.beanFactory.getBean(dataSourceName); dataSourceProperties.postRegister(newDataSource); }
核心代码:dataSourceProperties.postRegister(newDataSource);根据配置信息中配置的不同类型的rule-type来注册到对应的RuleManager中:
public void postRegister(AbstractDataSource dataSource) { switch(this.getRuleType()) { case FLOW: FlowRuleManager.register2Property(dataSource.getProperty()); break; case DEGRADE: DegradeRuleManager.register2Property(dataSource.getProperty()); break; case PARAM_FLOW: ParamFlowRuleManager.register2Property(dataSource.getProperty()); break; case SYSTEM: SystemRuleManager.register2Property(dataSource.getProperty()); break; case AUTHORITY: AuthorityRuleManager.register2Property(dataSource.getProperty()); break; case GW_FLOW: GatewayRuleManager.register2Property(dataSource.getProperty()); break; case GW_API_GROUP: GatewayApiDefinitionManager.register2Property(dataSource.getProperty()); } }
Spring在利用factoryBean创建bean时,便会加载对应的构造方法添加了nacos对应规则的监听器,执行上述的核心代码,至此springCloud便完成了对从nacos读取配置的整合。
对sentinel控制台进行改造:
获取规则改造:
//从客户端内存获取规则配置 // List<FlowRuleEntity> rules = sentinelApiClient.fetchFlowRuleOfMachine(app, ip, port); //从远程配置中心获取规则配置 List<FlowRuleEntity> rules = ruleProvider.getRules(app,ip,port);
发布规则到配置中心改造:
/** * 发布规则到远程配置中心 */ private void publishRules(/*@NonNull*/ String app) throws Exception { List<FlowRuleEntity> rules = repository.findAllByApp(app); rulePublisher.publish(app, rules); }
// 发布规则到客户端内存中 // publishRules(entity.getApp(), entity.getIp(), entity.getPort()).get(5000, TimeUnit.MILLISECONDS); // 发布规则到远程配置中心 publishRules(entity.getApp());
更新规则改造:
// 发布规则到客户端内存中 // publishRules(entity.getApp(), entity.getIp(), entity.getPort()).get(5000, TimeUnit.MILLISECONDS); // 发布规则到远程配置中心 publishRules(entity.getApp());
删除规则改造:
//publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort()).get(5000, TimeUnit.MILLISECONDS); publishRules(oldEntity.getApp());
微服务端接入:
引入依赖:
<!--sentinel持久化 --> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency>
增加配置信息:
flow-rules: nacos: server-addr: 127.0.0.1:8848 dataId: ${spring.application.name}-flow-rules groupId: SENTINEL_GROUP # 注意groupId对应Sentinel Dashboard中的定义 data-type: json rule-type: flow degrade-rules: nacos: server-addr: 127.0.0.1:8848 dataId: ${spring.application.name}-degrade-rules groupId: SENTINEL_GROUP data-type: json rule-type: degrade param-flow-rules: nacos: server-addr: 127.0.0.1:8848 dataId: ${spring.application.name}-param-flow-rules groupId: SENTINEL_GROUP data-type: json rule-type: param-flow authority-rules: nacos: server-addr: 127.0.0.1:8848 dataId: ${spring.application.name}-authority-rules groupId: SENTINEL_GROUP data-type: json rule-type: authority system-rules: nacos: server-addr: 127.0.0.1:8848 dataId: ${spring.application.name}-system-rules groupId: SENTINEL_GROUP data-type: json rule-type: system
启动改造后的dashbord控制台进行测试即可。
@Override public List<ParamFlowRuleEntity> getRules(String appName,String ip,Integer port) throws Exception { String rules = configService.getConfig(appName + NacosConfigUtil.PARAM_FLOW_DATA_ID_POSTFIX, NacosConfigUtil.GROUP_ID, NacosConfigUtil.READ_TIMEOUT); if (StringUtil.isEmpty(rules)) { return new ArrayList<>(); } List<ParamFlowRule> list = JSON.parseArray(rules, ParamFlowRule.class); return list.stream().map(rule -> ParamFlowRuleEntity.fromParamFlowRule(appName, ip, port, rule)) .collect(Collectors.toList()); }
@Override public void publish(String app, List<ParamFlowRuleEntity> rules) throws Exception { AssertUtil.notEmpty(app, "app name cannot be empty"); if (rules == null) { return; } configService.publishConfig(app + NacosConfigUtil.PARAM_FLOW_DATA_ID_POSTFIX, NacosConfigUtil.GROUP_ID, NacosConfigUtil.convertToRule(rules)); }