Loading

05_Sentinel规则持久化之推模式

推模式

生产环境下一般更常用的是 push 模式的数据源。对于 push 模式的数据源,如远程配置中心(ZooKeeper, Nacos, Apollo等等)。

由拉模式改造流程来改造推模式思路:

该模式存在的问题:

控制台修改的规则要经过微服务端推送,才能同步到nacos配置中心,增加了微服务端的负载。

解决思路:

推送的操作不应由 Sentinel 客户端进行,而应该经控制台统一进行管理,直接进行推送,数据源仅负责获取配置中心推送的配置并更新到本地。
因此推送规则正确做法应该是配置中心控制台/Sentinel 控制台 → 配置中心 → Sentinel 数据源 → Sentinel,而不是经Sentinel 数据源推送至配置中心。

读数据源思路:

可以参考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控制台进行测试即可。

热点参数规则失效和解决思路:
注意:控制台改造后有可能出现规则不生效的情况,比如热点参数规则因为Converter解析json错误的原因会导致不生效。 
参见源码:com.alibaba.csp.sentinel.datasource.AbstractDataSource#loadConfig(S) 会解析配置规则。
原因是:改造dashboard,提交到nacos配置中心的数据是ParamFlowRuleEntity类型,微服务拉取配置要解析的是ParamFlowRule类型,会导致规则解析丢失数据,造成热点规则不生效。 其他的规则原理也是一样,存在失效的风险。
nacos配置中心保存的数据格式: 
我提供两种解决思路:
1. 自定义一个解析热点规则配置的解析器FlowParamJsonConverter,继承JsonConverter,重写convert方法。然后利用后置处理器替换beanName为"param­flow­rules­sentinel­nacos­datasource"的converter属性,注入FlowParamJsonConverter。
2. 改造Sentinel Dashboard控制台,发布配置时将ParamFlowRuleEntity转成ParamFlowRule类型,再发布到Nacos配置中心。从配置中心拉取配置后将ParamFlowRule转成ParamFlowRuleEntity。 
从配置中心拉取配置到控制台时,FlowRule转换为FlowRuleEntity 
    @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());
    }
从控制台发布配置到配置中心时,FlowRuleEntity转换为FlowRule
    @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));
    }

 

posted @ 2023-05-13 17:40  1640808365  阅读(61)  评论(0编辑  收藏  举报