1、背景

当前项目没有接入配置中心、只针对两三个对外三方接口做熔断降级,最初的预想是尽量简单化实现,所以在考虑的时候想着尽量把服务端dashboard剥离出来,监控指标可以后续再调研是否可以暴露给grafana,抱着这个目的开始了一番调研。

2、实施

sentinel规则一共有三种模式

推送模式说明优点缺点
原始模式API 将规则推送至客户端并直接更新到内存中,扩展写数据源(WritableDataSource简单,无任何依赖不保证一致性;规则保存在内存中,重启即消失。严重不建议用于生产环境
Pull模式扩展写数据源(WritableDataSource), 客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件 等简单,无任何依赖;规则持久化不保证一致性;实时性不保证,拉取过于频繁也可能会有性能问题。
Push模式扩展读数据源(ReadableDataSource),规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 Nacos、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。生产环境下一般采用 push 模式的数据源。规则持久化;一致性;快速引入第三方依赖

2.1、原始模式

2.1.1、原始模式下使用dashboard

这个模式是dashboard修改的规则的默认模式,在使用dashboard修改规则后,会调用客户端的api接口同步规则,客户端会讲规则存入内存,也就是说如果重启服务,规则就会荡然无存。所以,这种模式只适合在测试或者写demo的时候使用。

img

2.1.2、原始模式下不使用dashboard

离开dashboard,可以手动写死或者实现WritableDataSource(具体介绍参考2.2),不过个人认为原始模式就是追求简单,与其实现WritableDataSource再配置,不如直接写死来的简单。 代码如下,不过会带来另一个问题:无法动态修改,所以依旧是一个缺陷很多的模式。

private void initRules() {
        //=============================规则1=========================
        FlowRule rule1 = new FlowRule();
        rule1.setResource("rule1");//规则名称
        rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);//如果设置0则按照线程数限流,如果设置1则按照QPS(每秒查询率)限流
        rule1.setCount(100);   // 每秒调用最大次数为 100 次
        rule1.setControlBehavior(0);//0快速失败,1预警,2排队等候
        rule1.setMaxQueueingTimeMs(1000);//排队超时阈值
 
        //=============================规则2=========================
        FlowRule rule2 = new FlowRule();
        rule2.setResource("rule2");
        rule2.setGrade(RuleConstant.FLOW_GRADE_QPS);
        rule2.setCount(10);   // 每秒调用最大次数为 10 次
 
        List<FlowRule> rules = new ArrayList<>();
        rules.add(rule1);
        rules.add(rule2);
        FlowRuleManager.loadRules(rules);
    }

2.2 Pull模式

这种模式相较于原始模式有了很大的改变,服务在启动的时候会将对应的读数据源注册至对应的 RuleManager,将写数据源注册至 transport 的 WritableDataSourceRegistry 中,即加载到内存里。同时在写入后,服务端也会定时去轮询查询最新的规则并同步到内存。如果使用dashboard修改规则后,dashboard会调用客户端api来推送规则,接着客户端会将规则写入内存,并持久化。

img

以本地文件数据源为例

public class FileDataSourceInit implements InitFunc {

    @Override
    public void init() throws Exception {
        String flowRulePath = "xxx";

        ReadableDataSource<String, List<FlowRule>> ds = new FileRefreshableDataSource<>(
            flowRulePath, source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {})
        );
        // 将可读数据源注册至 FlowRuleManager.
        FlowRuleManager.register2Property(ds.getProperty());

        WritableDataSource<List<FlowRule>> wds = new FileWritableDataSource<>(flowRulePath, this::encodeJson);
        // 将可写数据源注册至 transport 模块的 WritableDataSourceRegistry 中.
        // 这样收到控制台推送的规则时,Sentinel 会先更新到内存,然后将规则写入到文件中.
        WritableDataSourceRegistry.registerFlowDataSource(wds);
    }

    private <T> String encodeJson(T t) {
        return JSON.toJSONString(t);
    }
}

有两个接口需要注意,一个是ReadableDataSource,一个是WritableDataSource,分别对应规则的读取以及规则的写入。

ReadableDataSource

FileRefreshableDataSource的继承树:FileRefreshableDataSource->AutoRefreshDataSource->AbstractDataSource->ReadableDataSource

实际上FileRefreshableDataSource最终也是实现了ReadableDataSource,先看看ReadableDataSource的作用

public interface ReadableDataSource<S, T> {
	  //加载配置
    T loadConfig() throws Exception;
    //数据源读取配置,数据源可以是 yaml 配置文件,可以是 MySQL、Redis 等,由实现类决定从哪种数据源读取配置。
    S readSource() throws Exception;
    //获取 SentinelProperty
    SentinelProperty<T> getProperty();
    //用于关闭数据源,例如使用文件存储配置时,可在此方法实现关闭文件输入流等
    void close() throws Exception;
}

如果动态数据源提供 SentinelProperty,则可以调用 getProperty 方法获取动态数据源SentinelProperty,将 SentinelProperty 注册给规则管理器,动态数据源在读取到配置时就可以调用自身 SentinelProperty 的 updateValue 方法通知规则管理器更新规则。

AutoRefreshDataSource在原有的基础上增加了AutoRefreshDataSource方法,这就是方法主要是用来定时刷新数据源,代码如下。可以看到默认刷新时间是3000ms,可自定义刷新时间,以免刷新间隔比较短对服务有一些额外负载。

public abstract class AutoRefreshDataSource<S, T> extends AbstractDataSource<S, T> {
    private ScheduledExecutorService service;
    protected long recommendRefreshMs = 3000L;

    public AutoRefreshDataSource(Converter<S, T> configParser) {
        super(configParser);
        this.startTimerService();
    }

    public AutoRefreshDataSource(Converter<S, T> configParser, long recommendRefreshMs) {
        super(configParser);
        if (recommendRefreshMs <= 0L) {
            throw new IllegalArgumentException("recommendRefreshMs must > 0, but " + recommendRefreshMs + " get");
        } else {
            this.recommendRefreshMs = recommendRefreshMs;
            this.startTimerService();
        }
    }

    private void startTimerService() {
        this.service = Executors.newScheduledThreadPool(1, new NamedThreadFactory("sentinel-datasource-auto-refresh-task", true));
        this.service.scheduleAtFixedRate(new Runnable() {
            public void run() {
                try {
                    if (!AutoRefreshDataSource.this.isModified()) {
                        return;
                    }

                    T newValue = AutoRefreshDataSource.this.loadConfig();
                    AutoRefreshDataSource.this.getProperty().updateValue(newValue);
                } catch (Throwable var2) {
                    RecordLog.info("loadConfig exception", var2);
                }

            }
        }, this.recommendRefreshMs, this.recommendRefreshMs, TimeUnit.MILLISECONDS);
    }
}
WritableDataSource
public interface WritableDataSource<T> {
	//写入持久化
    void write(T var1) throws Exception;
  //关闭文件流等
    void close() throws Exception;
}

WritableDataSource的方法作用相对简单点,主要就是写入持久化的规则,比如FileWritableDataSource中就是讲规则写入文件内。

public void write(T value) throws Exception {
        this.lock.lock();

        try {
            String convertResult = (String)this.configEncoder.convert(value);
            FileOutputStream outputStream = null;

            try {
                outputStream = new FileOutputStream(this.file);
                byte[] bytesArray = convertResult.getBytes(this.charset);
                RecordLog.info(String.format("[FileWritableDataSource] Writing to file %s: %s", this.file.toString(), convertResult), new Object[0]);
                outputStream.write(bytesArray);
                outputStream.flush();
            } finally {
                if (outputStream != null) {
                    try {
                        outputStream.close();
                    } catch (Exception var16) {
                    }
                }

            }
        } finally {
            this.lock.unlock();
        }

    }

实际上在这种模式下,如果不使用dashboard,可以在客户端暴露接口,手动实现WritableDataSource更新规则。多节点的情况下还得使用redis的发布订阅来保证每个节点都更新规则,有这个时间成本不如部署一套dashboard。

2.3 Push模式

一般生产环境使用的是push模式,就目前的开源sentinel代码存在一个问题。

sentinel dashboard推送规则后,服务端节点重启,规则消失

那么以redis为例,要修改需要怎么做?如图中所述,需要修改sentinel dashboard源码,在修改规则或者新增的时候,推送规则到redis,并且使用发布订阅通知服务端的节点,保证数据一致性。或者在不使用dashboard的情况下,服务端开放一个接口修改redis中sentinel规则。
img

如何修改sentinel dashboard端的代码?

  • 实现DynamicRuleProvider接口,根据应用名称查询redis流控规则;
  • 实现DynamicRulePublisher接口,保存流控规则、发布更新消息;
  • 修改FlowControllerV1中查询流控规则的方法,改为调用DynamicRuleProvider从redis获取;
  • 修改FlowControllerV1中新增、修改、删除时的发布方法,改为调用DynamicRulePublisher保存到redis并发送更新消息。

以FlowRule为例

public class FlowRuleRedisProvider implements DynamicRuleProvider<List<FlowRuleEntity>> {

    private final Logger logger = LoggerFactory.getLogger(FlowRuleRedisProvider.class);

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    public List<FlowRuleEntity> getRules(String appName) throws Exception {
        String key = RULE_FLOW_PREFIX + appName;
        String ruleStr = (String)redisTemplate.opsForValue().get(key);
        if(StringUtils.isEmpty(ruleStr)) {
            return Collections.emptyList();
        }
        List<FlowRuleEntity> rules = JSON.parseArray(ruleStr, FlowRuleEntity.class);
        return rules;
    }
}
public class FlowRuleRedisPublisher implements DynamicRulePublisher<List<FlowRuleEntity>> {

    private final Logger logger = LoggerFactory.getLogger(FlowRuleRedisPublisher.class);

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Value("${sentinel.channel.enabled:true}")
    private boolean channelEnabled;

    @Override
    public void publish(String app, List<FlowRuleEntity> rules) throws Exception {
        redisTemplate.multi();
        String ruleKey = RULE_FLOW_PREFIX + app;
        String ruleStr = JSON.toJSONString(rules);
        redisTemplate.opsForValue().set(ruleKey, ruleStr);
        if (channelEnabled) {
            String channelKey = RULE_FLOW_CHANNEL_PREFIX + app;
            redisTemplate.convertAndSend(channelKey, rules.size());
        }
        redisTemplate.exec();
    }
}

客户端只需引入官方维护好的代码即可

       <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-redis</artifactId>
        </dependency>

3、总结

1.接入配置中心的情况下,优先整合配置中心,规则方便看。
2.如果规则很少改动或者对于实时性不高,可以选择pull模式,实现简单改动代码量较少,相反就选择push模式。dashboard属于可以选配的一环,毕竟也要不了多少服务器成本。

参考:

sentinel代码 https://github.com/alibaba/Sentinel/tree/master

官方文档 https://github.com/alibaba/Sentinel/wiki

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐