Sentinel 动态数据源深度解析:实现规则的热更新与动态配置

图片[1]-Sentinel 动态数据源深度解析:实现规则的热更新与动态配置 - 速优课-速优课

本文导读

在前面的文章中,我们学习了 Sentinel 的各种规则配置方式,都是通过 loadRules API 以硬编码的方式加载规则。这种方式在生产环境中有明显的缺点:

  • 修改规则需要重新发布应用
  • 无法根据线上流量动态调整阈值
  • 应急响应慢,出问题时不能快速调整

为了解决这些问题,Sentinel 提供了动态数据源(Dynamic DataSource)机制,支持规则的动态配置和热更新。

本文将从以下几个方面深入探讨 Sentinel 的动态数据源:

  1. SentinelProperty:规则属性的监听器模式
  2. ReadableDataSource:动态数据源的核心接口
  3. AbstractDataSource:数据源的抽象模板
  4. 实战案例:基于 Spring Cloud 动态配置实现规则动态更新

通过本文的学习,你将彻底掌握 Sentinel 动态数据源的实现原理,并能够根据业务需求自定义动态数据源。


一、动态数据源概述

1.1 为什么需要动态数据源

在微服务架构中,限流、熔断等规则的动态调整是刚需。典型的场景包括:

场景 需求
大促活动前 需要调高核心接口的限流阈值
服务出问题时 需要快速调低阈值或开启熔断
日常运营中 根据业务变化动态调整规则
应急响应时 快速开关某些功能降级

如果每次调整规则都需要重新发布应用,不仅效率低下,还可能引入新的风险。动态数据源就是为了解决这个问题而设计的。

1.2 Sentinel 动态数据源的模块结构

动态数据源作为扩展功能放在 sentinel-extension 模块下。以 Sentinel 1.7.1 版本为例,该模块下的子模块包括:

子模块 作用
sentinel-datasource-extension 定义动态数据源接口、提供抽象类(核心)
sentinel-datasource-redis 基于 Redis 实现的动态数据源
sentinel-datasource-zookeeper 基于 ZooKeeper 实现的动态数据源
sentinel-datasource-nacos 基于 Nacos 实现的动态数据源
sentinel-datasource-apollo 基于 Apollo 实现的动态数据源
sentinel-parameter-flow-control 热点参数限流模块
sentinel-annotation-aspectj 注解支持模块

其中,sentinel-datasource-extension 是核心,定义了动态数据源的整体框架。其他模块都是基于这个框架的具体实现。

实际上,很多团队并不会直接使用 Sentinel 提供的动态数据源实现,而是根据自己的技术栈自定义实现。比如使用 Spring Cloud Config、Kubernetes ConfigMap 等。


二、SentinelProperty:监听器模式的实现

2.1 什么是 SentinelProperty

SentinelProperty 是 Sentinel 提供的一个属性接口,它是动态数据源能够工作的基础。你可以把它理解为一个”可观察的属性容器”:

  • 可以给它添加监听器(Listener)
  • 当配置改变时,调用 updateValue 方法通知所有监听器
  • 监听器收到通知后,执行相应的更新操作

SentinelProperty 并非定义在 sentinel-datasource-extension 模块中,而是定义在 sentinel-core 模块中,因为它是 Sentinel 核心的一部分。

接口定义如下:

public interface SentinelProperty<T> {
    // 添加监听器
    void addListener(PropertyListener<T> listener);
    // 移除监听器
    void removeListener(PropertyListener<T> listener);
    // 更新值,通知所有监听器
    boolean updateValue(T newValue);
}

2.2 默认实现:DynamicSentinelProperty

SentinelProperty 的默认实现类是 DynamicSentinelProperty,源码如下(有删减):

public class DynamicSentinelProperty<T> implements SentinelProperty<T> {
    // 存储注册的监听器
    protected Set<PropertyListener<T>> listeners = Collections.synchronizedSet(new HashSet<PropertyListener<T>>());
    
    @Override
    public void addListener(PropertyListener<T> listener) {
        listeners.add(listener);
        listener.configLoad(value);
    }
    
    @Override
    public void removeListener(PropertyListener<T> listener) {
        listeners.remove(listener);
    }
    
    @Override
    public boolean updateValue(T newValue) {
        for (PropertyListener<T> listener : listeners) {
            listener.configUpdate(newValue);
        }
        return true;
    }
}

代码解读:

  • 使用 Set 存储已注册的监听器,并用 Collections.synchronizedSet 保证线程安全
  • addListener 方法添加监听器后,会立即调用一次 configLoad 方法加载初始配置
  • updateValue 方法遍历所有监听器,调用其 configUpdate 方法通知配置更新

这是典型的观察者模式(Observer Pattern)的应用:SentinelProperty 是被观察者,PropertyListener 是观察者。

2.3 PropertyListener 接口

PropertyListener 是监听器接口,定义了两个方法:

public interface PropertyListener<T> {
    // 配置首次加载时调用
    void configLoad(T value);
    // 配置更新时调用
    void configUpdate(T value);
}
方法 调用时机
configLoad 监听器首次注册时,用于加载初始配置
configUpdate 配置发生变化时,用于更新配置

2.4 FlowRuleManager 中的 SentinelProperty

在前面分析 FlowRuleManager 时,我们只关注了 loadRules 方法。实际上,FlowRuleManager 还提供了 register2Property API,用于注册 SentinelProperty

我们来看完整的实现:

public class FlowRuleManager {
    // 缓存限流规则
    private static final Map<String, List<FlowRule>> flowRules = new ConcurrentHashMap<String, List<FlowRule>>();
    // 规则更新监听器
    private static final FlowPropertyListener LISTENER = new FlowPropertyListener();
    // SentinelProperty,默认创建一个 DynamicSentinelProperty
    private static SentinelProperty<List<FlowRule>> currentProperty 
       = new DynamicSentinelProperty<List<FlowRule>>();

    static {
        // 给默认的 SentinelProperty 注册监听器
        currentProperty.addListener(LISTENER);
    }

    // 注册自定义的 SentinelProperty
    public static void register2Property(SentinelProperty<List<FlowRule>> property) {
        synchronized (LISTENER) {
            // 先从旧的 property 上移除监听器
            currentProperty.removeListener(LISTENER);
            // 再注册到新的 property 上
            property.addListener(LISTENER);
            // 替换为新的 property
            currentProperty = property;
        }
    }
}

工作流程:

  1. FlowRuleManager 内部维护一个 SentinelProperty,默认是 DynamicSentinelProperty
  2. 静态代码块中,给默认的 SentinelProperty 注册 FlowPropertyListener 监听器
  3. 当调用 register2Property 时,会把监听器从旧的 property 转移到新的 property 上
  4. 之后,只要调用新 property 的 updateValue 方法,就会触发监听器更新规则

2.5 FlowPropertyListener 监听器

FlowPropertyListenerFlowRuleManager 的内部类,实现了真正的规则更新逻辑:

private static final class FlowPropertyListener implements PropertyListener<List<FlowRule>> {
    @Override
    public void configUpdate(List<FlowRule> value) {
        Map<String, List<FlowRule>> rules = FlowRuleUtil.buildFlowRuleMap(value);
        if (rules != null) {
            // 先清空缓存再写入
            flowRules.clear();
            flowRules.putAll(rules);
        }
    }
    
    @Override
    public void configLoad(List<FlowRule> conf) {
        Map<String, List<FlowRule>> rules = FlowRuleUtil.buildFlowRuleMap(conf);
        if (rules != null) {
            flowRules.clear();
            flowRules.putAll(rules);
        }
    }
}

可以看到,configUpdateconfigLoad 的实现几乎一样,都是:

  1. 将规则列表转换为以资源名为 key 的 Map
  2. 清空旧的缓存
  3. 写入新的规则

到这里,我们就有了两种更新规则的方式:

  1. 直接调用 FlowRuleManager#loadRules 方法
  2. 注册 SentinelProperty,调用 SentinelProperty#updateValue 方法

动态数据源就是基于第二种方式实现的。


三、ReadableDataSource:动态数据源的核心接口

3.1 接口定义

Sentinel 将数据源抽象为读接口和写接口,ReadableDataSource 是读数据源接口,也是动态数据源的核心。

写接口(WritableDataSource)是后来才加的功能,目前主要在热点参数限流模块中使用。大多数场景下,读接口已经足够满足需求。

ReadableDataSource 接口定义如下:

public interface ReadableDataSource<S, T> {
    // 加载配置(转换后的结果)
    T loadConfig() throws Exception;
    // 从数据源读取原始配置
    S readSource() throws Exception;
    // 获取 SentinelProperty
    SentinelProperty<T> getProperty();
    // 关闭数据源
    void close() throws Exception;
}

这是一个泛型接口,两个类型参数的含义:

类型参数 含义 举例
S 从数据源读取的原始配置类型 字符串、JSON对象、配置类
T 转换后的 Sentinel 规则类型 List<FlowRule>

举个例子:

  • 从 Nacos 读取的是 JSON 字符串(S = String)
  • 需要转换成 List<FlowRule>(T = List)
  • 中间需要一个转换器(Converter)

各方法的作用:

方法 作用
loadConfig 加载配置,内部会调用 readSource + 转换器,返回最终的规则对象
readSource 从数据源读取原始配置,具体实现取决于数据源类型
getProperty 获取 SentinelProperty,用于注册到规则管理器
close 关闭数据源,释放资源

3.2 抽象模板类:AbstractDataSource

AbstractDataSource 是一个抽象类,实现了 ReadableDataSource 接口,用于简化具体数据源的实现。子类只需要继承 AbstractDataSource 并实现 readSource 方法即可。

源码如下:

public abstract class AbstractDataSource<S, T> implements ReadableDataSource<S, T> {
    // 数据转换器
    protected final Converter<S, T> parser;
    // SentinelProperty
    protected final SentinelProperty<T> property;

    public AbstractDataSource(Converter<S, T> parser) {
        if (parser == null) {
            throw new IllegalArgumentException("parser can't be null");
        }
        this.parser = parser;
        this.property = new DynamicSentinelProperty<T>();
    }

    @Override
    public T loadConfig() throws Exception {
        return loadConfig(readSource());
    }

    public T loadConfig(S conf) throws Exception {
        T value = parser.convert(conf);
        return value;
    }

    @Override
    public SentinelProperty<T> getProperty() {
        return property;
    }
}

核心设计思想:

  1. 模板方法模式loadConfig 定义了加载配置的骨架流程,readSource 由子类实现
  2. 转换器模式:通过 Converter 将原始配置转换为目标规则类型,实现数据源与规则的解耦
  3. 组合优于继承:内部持有 SentinelProperty,而不是继承它

关键点解读:

  • 构造方法要求传入一个 Converter(数据转换器),不能为 null
  • 在构造方法中创建 DynamicSentinelProperty,子类无需自己创建
  • loadConfig 方法的流程:调用 readSource 获取原始配置 → 调用转换器转换 → 返回规则对象

3.3 Converter 转换器接口

Converter 是数据转换器接口,定义非常简单:

public interface Converter<S, T> {
    T convert(S source);
}
  • S:源类型
  • T:目标类型
  • convert:将源对象转换为目标对象

转换器的设计非常优雅,它将”从哪里读取数据”和”数据如何转换”分离开来:

  • 数据源负责”从哪里读”
  • 转换器负责”怎么转”
  • 两者可以自由组合

四、实战:基于 Spring Cloud 动态配置实现

4.1 背景说明

了解了动态数据源的原理后,我们来看一个实际的案例。

很多项目部署在 Kubernetes 集群上,使用 ConfigMap 资源存储配置。Spring Cloud Kubernetes 提供了 Spring Cloud 动态配置接口的实现,可以自动监听 ConfigMap 的变化。

我们就基于 Spring Cloud 动态配置来实现 Sentinel 规则的动态更新。整个过程不需要关心如何读取 ConfigMap,Spring Cloud 已经帮我们做好了。

注意:以下方法不仅适用于 Kubernetes ConfigMap,也适用于任何支持 Spring Cloud @RefreshScope 的配置中心(如 Nacos Config、Spring Cloud Config 等)。

4.2 实现步骤

以限流规则(FlowRule)的动态配置为例,总共需要五步:

第一步:定义配置装载类

定义一个用于装载动态配置的类,使用 @ConfigurationProperties@RefreshScope 注解:

@Component
@RefreshScope
@ConfigurationProperties(prefix = "sentinel.flow-rules")
public class FlowRuleProps {
    // 配置字段...
    // 例如:private List<FlowRuleConfig> rules;
}
  • @ConfigurationProperties:自动绑定配置文件中的属性
  • @RefreshScope:支持动态刷新,配置变更时会重新创建 Bean

第二步:创建数据转换器

创建一个转换器,实现将 FlowRuleProps 转换为 List<FlowRule>

public class FlowRuleConverter implements Converter<FlowRuleProps, List<FlowRule>> {

    @Override
    public List<FlowRule> convert(FlowRuleProps source) {
        // 将自定义配置转换为 Sentinel 的 FlowRule 列表
        // ...具体转换逻辑省略...
    }
}

第三步:创建动态数据源

创建 FlowRuleDataSource,继承 AbstractDataSource,实现 readSource 方法:

@Component
public class FlowRuleDataSource extends AbstractDataSource<FlowRuleProps, List<FlowRule>> {
    
    @Autowired
    private FlowRuleProps flowRuleProps;

    public FlowRuleDataSource() {
        super(new FlowRuleConverter());
    }
    
    @Override
    public FlowRuleProps readSource() throws Exception {
        return this.flowRuleProps;
    }
    
    @Override
    public void close() throws Exception {
        // 无需关闭资源
    }
}

readSource 方法非常简单,直接返回注入的 flowRuleProps 即可。

第四步:监听配置变更

现在的数据源还不能自动感知配置变化,需要增强一下,让它能够监听配置变更事件:

@Component
public class FlowRuleDataSource extends AbstractDataSource<FlowRuleProps, List<FlowRule>>
   implements ApplicationListener<RefreshScopeRefreshedEvent>,
            InitializingBean {
    
    @Autowired
    private FlowRuleProps flowRuleProps;

    public FlowRuleDataSource() {
        super(new FlowRuleConverter());
    }
    
    @Override
    public FlowRuleProps readSource() throws Exception {
        return this.flowRuleProps;
    }
    
    @Override
    public void close() throws Exception {
    }
    
    // 配置刷新时触发
    @Override
    public void onApplicationEvent(RefreshScopeRefreshedEvent event) {
        getProperty().updateValue(loadConfig());
    }

    // Bean 初始化完成时加载初始配置
    @Override
    public void afterPropertiesSet() throws Exception {
        onApplicationEvent(new RefreshScopeRefreshedEvent());
    }
}

关键点:

  1. 实现 InitializingBean 接口:在 Bean 初始化完成后,首次加载规则配置
  2. 实现 ApplicationListener 接口:监听 RefreshScopeRefreshedEvent 事件,配置变更时自动更新规则
  3. 更新流程:事件触发 → 调用 loadConfig() 加载最新配置 → 调用 updateValue() 通知监听器更新

第五步:注册到规则管理器

最后,在 Spring 容器启动完成后,将数据源的 SentinelProperty 注册到 FlowRuleManager

@Component
public class FlowRuleDataSourceConfiguration implements ApplicationRunner {
    
    @Autowired
    private FlowRuleDataSource flowRuleDataSource;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        // 将数据源的 SentinelProperty 注册给 FlowRuleManager
        FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
    }
}

调用 register2Property 时,FlowRuleManager 会自动给这个 SentinelProperty 注册 FlowPropertyListener 监听器。

4.3 完整调用链路

至此,一个基于 Spring Cloud 动态配置的限流规则动态数据源就完成了。我们来梳理一下完整的调用链路:

配置变更 → Spring Cloud 发出 RefreshScopeRefreshedEvent 事件
    ↓
FlowRuleDataSource.onApplicationEvent() 被调用
    ↓
调用 loadConfig() 加载最新配置
    ↓
loadConfig() 内部调用 readSource() 获取 FlowRuleProps
    ↓
调用 Converter.convert() 将 FlowRuleProps 转为 List<FlowRule>
    ↓
调用 SentinelProperty.updateValue() 通知所有监听器
    ↓
FlowPropertyListener.configUpdate() 被调用
    ↓
更新 FlowRuleManager 中缓存的限流规则
    ↓
后续请求使用新的规则进行限流判断

整个流程非常清晰,每一步的职责都很明确。


五、设计思想总结

Sentinel 动态数据源的设计非常经典,有很多值得学习的地方:

5.1 用到的设计模式

设计模式 应用点
观察者模式 SentinelProperty + PropertyListener,实现配置变更通知
模板方法模式 AbstractDataSource 定义 loadConfig 骨架,子类实现 readSource
转换器模式 Converter 接口,将原始配置转换为目标规则
策略模式 不同的数据源实现(Redis、ZooKeeper、Nacos 等)可以互相替换

5.2 设计亮点

  1. 职责分离:数据源(读)、转换器(转)、属性(通知)各司其职
  2. 易于扩展:新增一种数据源只需要实现 readSource 方法
  3. 灵活性高:转换器可以自由组合,同一数据源可以转换为不同类型的规则
  4. 侵入性低:基于标准接口,不依赖具体实现

总结与思考

本文深入解析了 Sentinel 动态数据源的实现原理,并通过一个实战案例演示了如何基于 Spring Cloud 动态配置实现规则的热更新。

核心要点回顾

1. SentinelProperty

  • 基于观察者模式,支持注册监听器
  • updateValue 方法通知所有监听器配置变更
  • 规则管理器通过注册监听器的方式实现规则更新

2. ReadableDataSource

  • 动态数据源的核心接口,定义了读数据的能力
  • 两个泛型参数:S(原始类型)、T(目标规则类型)
  • 提供 getProperty 方法获取 SentinelProperty

3. AbstractDataSource

  • 抽象模板类,简化数据源实现
  • 内部持有 Converter(转换器)和 SentinelProperty
  • 子类只需实现 readSource 方法

4. Converter

  • 数据转换器接口,实现原始配置到规则的转换
  • 将数据源与规则类型解耦,提高灵活性

实践建议

  1. 优先使用成熟的实现:如果项目已经在使用 Nacos、Apollo 等配置中心,优先使用 Sentinel 官方提供的对应数据源实现
  2. 自定义实现也很简单:如果官方没有提供对应数据源,参照本文的方式自己实现也很容易
  3. 注意配置格式:设计规则配置格式时,要考虑可读性和可维护性
  4. 配置校验:规则更新时建议添加校验逻辑,避免错误配置导致异常
  5. 变更审计:记录规则变更历史,便于追溯问题

掌握了动态数据源的原理,你就可以根据自己的技术栈灵活实现规则的动态配置。下一篇文章我们将介绍 Sentinel 的主流框架适配,看看 Sentinel 是如何与 Spring Cloud、Dubbo 等框架无缝集成的,敬请期待。

© 版权声明
THE END
喜欢就支持一下吧
点赞15
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容

温馨提示:
1、本内容转载于网络,版权归原作者所有!
2、本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
3、本内容若侵犯到你的版权利益,请联系我们,会尽快给予删除处理!