16-Sentinel动态数据源:规则动态配置

原创 吴就业 145 0 2020-09-22

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/62dc033d699544019cbb846133797dab

作者:吴就业
链接:https://wujiuye.com/article/62dc033d699544019cbb846133797dab
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

深入理解Sentinel

经过前面的学习,我们知道,为资源配置各种规则可使用Sentinel提供的各种规则对应的loadRules API,但这种以编码的方式配置规则很难实现动态修改。但基于Sentinel提供的各种规则对应的loadRules API,我们可以自己实现规则的动态更新,而这一功能几乎在每个需要使用Sentinel的微服务项目中都需要实现一遍。Sentinel也考虑到了这点,所以提供了动态数据源接口,并且提供了多种动态数据源的实现,尽管我们可能不会用到。

动态数据源作为扩展功能放在sentinel-extension模块下,前面我们学习的热点参数限流模块sentinel-parameter-flow-control也是在该模块下。在1.7.1版本,sentinel-extension模块下的子模块除sentinel-parameter-flow-control、sentinel-annotation-aspectj之外,其余子模块都是实现动态数据源的模块。

显然,sentinel-datasource-extension模块才是我们主要研究的模块,这是Sentinel实现动态数据源的核心。

SentinelProperty

SentinelProperty是Sentinel提供的一个接口,可注册到Sentinel提供的各种规则的Manager,例如FlowRuleManager,并且可以给SentinelProperty添加监听器,在配置改变时,你可以调用SentinelProperty#updateValue方法,由它负责调用监听器去更新规则,而不需要调用FlowRuleManager#loadRules方法。同时,你也可以注册额外的监听器,在配置改变时做些别的事情。

SentinelProperty并非sentinel-datasource-extension模块中定义的接口,而是sentinel-core定义的接口,其源码如下。

public interface SentinelProperty<T> {
    void addListener(PropertyListener<T> listener);
    void removeListener(PropertyListener<T> listener);
    boolean updateValue(T newValue);
}

默认使用的实现类是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;
    }
}

可见,DynamicSentinelProperty使用Set存储已注册的监听器,updateValue负责通知所有监听器,调用监听器的configUpdate方法。

在前面分析FlowRuleManager时,我们只关注了其loadRules方法,除了使用loadRules方法加载规则配置之外,FlowRuleManager还提供registerProperty API,用于注册SentinelProperty。

使用SentinelProperty实现加载FlowRule的步骤如下:

FlowRuleManager支持使用SentinelProperty加载或更新限流规则的实现源码如下。

public class FlowRuleManager {
    // 缓存限流规则
    private static final Map<String, List<FlowRule>> flowRules = new ConcurrentHashMap<String, List<FlowRule>>();
    // PropertyListener监听器
    private static final FlowPropertyListener LISTENER = new FlowPropertyListener();
    // SentinelProperty
    private static SentinelProperty<List<FlowRule>> currentProperty 
       // 提供默认的 SentinelProperty
       = new DynamicSentinelProperty<List<FlowRule>>();
 
  static {
       // 给默认的SentinelProperty注册监听器(FlowPropertyListener)
        currentProperty.addListener(LISTENER);
  }
  
  // 注册SentinelProperty
  public static void register2Property(SentinelProperty<List<FlowRule>> property) {
        synchronized (LISTENER) {
            currentProperty.removeListener(LISTENER);
            // 注册监听器
            property.addListener(LISTENER);
            currentProperty = property;
        }
  }
}

实现更新限流规则缓存的FlowPropertyListener是FlowRuleManager的一个内部类,其源码如下。

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);
            }
        }
}

PropertyListener接口定义的两个方法:

所以,现在我们有两种方法更新限流规则:

ReadableDataSource

Sentinel将读和写数据源抽离成两个接口,一开始只有读接口,写接口是后面才加的功能,目前来看,写接口只在热点参数限流模块中使用到。事实上,使用读接口就已经满足我们的需求。ReadableDataSource接口的定义如下。

public interface ReadableDataSource<S, T> {
    T loadConfig() throws Exception;
    S readSource() throws Exception;
    SentinelProperty<T> getProperty();
    void close() throws Exception;
}

ReadableDataSource是一个泛型接口,参数化类型S代表用于装载从数据源读取的配置的类型,参数化类型T代表对应Sentinel中的规则类型。例如,我们可以定义一个FlowRuleProps类,用于装载从yml配置文件中读取的限流规则配置,然后再将FlowRuleProps转为FlowRule,所以S可以替换为FlowRuleProps,T可以替换为List。

ReadableDataSource接口定义的方法解释说明如下:

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

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

public abstract class AbstractDataSource<S, T> implements ReadableDataSource<S, T> {
    protected final Converter<S, T> parser;
    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;
    }
}

从源码可以看出:

Converter接口的定义如下:

public interface Converter<S, T> {
    T convert(S source);
}

基于Spring Cloud动态配置实现规则动态配置

我们项目中并未使用Sentinel提供的任何一种动态数据源的实现,而是选择自己实现数据源,因为我们项目是部署在Kubernetes集群上的,我们可以利用ConfigMap资源存储限流、熔断降级等规则。Spring Cloud Kubernetes提供了Spring Cloud动态配置接口的实现,因此,我们不需要关心怎么去读取ConfigMap资源。就相当于基于Spring Cloud动态配置实现Sentinel规则动态数据源。Spring Cloud动态配置如何使用这里不做介绍。

以实现FlowRule动态配置为例,其步骤如下:

第一步:定义一个用于装载动态配置的类,如FlowRuleProps所示。

@Component
@RefreshScope
@ConfigurationProperties(prefix = "sentinel.flow-rules")
public class FlowRuleProps{
  //....省略
}

第二步:创建数据转换器,实现将FlowRuleProps转为List,如FlowRuleConverter所示。

public class FlowRuleConverter implements Converter<FlowRuleProps, List<FlowRule>>{
    
    @Override
    public List<FlowRule> convert(FlowRuleProps source){
       //....省略
    }
}

第三步:创建FlowRuleDataSource,继承AbstractDataSource,实现readSource方法。readSource方法只需要获取FlowRuleProps返回即可,代码如下。

@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 {
    }
}

第四步:增强FlowRuleDataSource,让FlowRuleDataSource能够监听到FlowRuleProps配置改变。

@Component
public class FlowRuleDataSource extends AbstractDataSource<FlowRuleProps, List<FlowRule>>
   implements ApplicationListener<RefreshScopeRefreshedEvent>,
        InitializingBean{
       // .....
    @Override
    public void onApplicationEvent(RefreshScopeRefreshedEvent event) {
        getProperty().updateValue(loadConfig());
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        onApplicationEvent(new RefreshScopeRefreshedEvent());
    }
}

第五步:写一个ApplicationRunner类,在Spring容器刷新完成后, 将数据源(FlowRuleDataSource)的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());
    }
}

在调用FlowRuleManager#register2Property方法将FlowRuleDataSource动态数据源的SentinelProperty注册给FlowRuleManager时,FlowRuleManager会自动给该SentinelProperty注册一个监听器(FlowPropertyListener)。

到此,一个基于Spring Cloud动态配置的限流规则动态数据源就已经完成,整个调用链路如下:

总结:

了解Sentinel实现动态数据源的原理后,我们可以灵活的自定义规则动态数据源,例如本篇介绍的,利用Kubernetes的ConfigMap资源存储规则配置,并通过Spring Cloud动态配置实现Sentinel的规则动态数据源。不仅如此,Sentinel实现动态数据源的整体框架的设计也是值得我们学习的,如数据转换器、监听器。

#后端

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

Spring Data R2DBC快速上手指南

本篇内容介绍如何使用r2dbc-mysql驱动程序包与mysql数据库建立连接、使用r2dbc-pool获取数据库连接、Spring-Data-R2DBC增删改查API、事务的使用,以及R2DBC Repository。

使用Spring WebFlux + R2DBC搭建消息推送服务

消息推送服务主要是处理同步给用户推送短信通知或是异步推送短信通知、微信模板消息通知等。本篇介绍如何使用Spring WebFlux + R2DBC搭建消息推送服务。

教你如何编写一个IDEA插件,并掌握核心知识点PSI

IDEA有着极强的扩展功能,它提供插件扩展支持,让开发者能够参与到IDEA生态建设中,为更多开发者提供便利、提高开发效率。我们常用的插件有Lombok、Mybatis插件,这些插件都大大提高了我们的开发效率。即便IDEA功能已经很强大,并且也已有很多的插件,但也不可能面面俱到,有时候我们需要自给自足。

Spring Boot实现加载自定义配置文件

本篇将介绍两种加载自定义配置文件的实现方式,并通过分析源码了解SpringBoot加载配置文件的流程,从而加深理解。

设计模式那些模糊不清的概念

23种设计模式属于结构型模式,而mvc模式等属于架构型模式。本篇要讨论的设计模式指的是结构型设计模式。

实现一个分布式调用链路追踪Java探针你可能会遇到的问题

Instrumentation之所以难驾驭,在于需要了解Java类加载机制以及字节码,一不小心就能遇到各种陌生的Exception。笔者在实现Java探针时就踩过不少坑,其中一类就是类加载相关的问题,也是本篇所要跟大家分享的。