阿里开源分布式事务组件 seata : 配置机制简析

seata 的客户端代码和服务端代码逻辑里,读取配置时统一采用的以下这种 API

ConfigurationFactory.getInstance().getString()

seata 目前(0.8.0)支持以下几种配置方式
1. 本地文件方式
2. zookeeper
3. nacos
4. apollo
5. consul
6. etcd
在分析 seata 的配置解析细节之前,先看看 seata 对于配置解析机制的设计
具体来说,seata 的配置项的命名风格都是类似于 computer.apple.macbookpro 这种的文本扁平化风格。
它本质上还是一个结构形的配置方式,即每个具体配置项都有父节点,举个例子:

computer.apple {

    macbookpro = 12000

    macbookair = 8000

}

这种配置方式与普通的 xml 配置文件在结构上没有什么大的区别,但与 xml 相比 还是有不少的好处和优点。
1. 简洁明了,xml 本质上还是个标记型语言,引入了许多不必要的复杂标记,间接增加了解析成本
2. 解析阶段,xml 定位到某个配置项的逻辑更加繁琐
3. 配置更新时如果需要更新文件,xml 的文件的更新动作不够轻量级,如果依赖一些第三方实现,还会造成代码入侵,可扩展性差。
相比之下,目前主流的配置中心例如 zookeeper 或者 apollo,这些软件设计之初对数据结构的选型就与 computer.apple.macbookpro 这样的配置形态很相称。
例如,zookeeper 的数据结构是类似于文件系统的树状风格,所以 zookeeper 之前是很适合拿来做公共配置中心的,直到后面更优秀的配置中心出现,zookeeper 才慢慢淡出配置界,毕竟 zookeeper 它更擅长做的事情是分布式一致性的协调。
正是 seata 采用了这种配置风格,所以 seata 在配置中心的支持这一块,就很方便地与当前主流的配置中心做集成。
试想一下,如果要把一个 xml 配置文件存到 zookeeper 上做全局管理。大概有这么两种方式吧:
1. 把整个 xml 文本存到一个 znode 上
2. 把 xml 的结构解析成树状结构,再一个一个对应地到 zookeeper 上创建节点
第一种方式,显然很 low 哦。干脆存到数据库算了。
第二种方式,会带来额外的解析成本,并且不容易做配置变更这样的逻辑,因为一个配置项的变更,意味着要重新在内存里生成整个 xml 文本,然后再写进本地配置文件里面。
因此,一个 seata 相关的进程启动的时候,必然要从本地某个地方知道要用什么配置中心。
而实际上,seata 是先读取本地的一个配置文件 registry.conf,再决定要用什么样的配置方式的。

这个逻辑在我们上面提到的 API ConfigurationFactory.getInstance()
的源码里可以看到具体的细节。
下面是 buildConfiguration 方法:

    private static Configuration buildConfiguration() {

        ConfigType configType = null;

        String configTypeName = null;

        try {

            // 这里读取的是本地 registry.conf 配置文件,获取配置中心的类型

            configTypeName = CURRENT_FILE_INSTANCE.getConfig(

                ConfigurationKeys.FILE_ROOT_CONFIG + ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR

                    + ConfigurationKeys.FILE_ROOT_TYPE);  

            configType = ConfigType.getType(configTypeName);

        } catch (Exception e) {

            throw new NotSupportYetException("not support register type: " + configTypeName, e);

        }

        if (ConfigType.File == configType) {

            String pathDataId = ConfigurationKeys.FILE_ROOT_CONFIG + ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR

                + FILE_TYPE + ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR

                + NAME_KEY;

            String name = CURRENT_FILE_INSTANCE.getConfig(pathDataId);

            return new FileConfiguration(name);

        } else {

            return EnhancedServiceLoader.load(ConfigurationProvider.class, Objects.requireNonNull(configType).name())

                .provide();

        }

    }

可以看到,seata 先读取本地的一个配置文件,默认是读取 registry.conf 文件,再从这个文件里面读取具体的配置类型。
那么 registry.conf 里面关于配置中心的配置是怎么样的呢,大概像这样:

config {

  # file、nacos 、apollo、zk、consul、etcd3

  type = "file"


nacos { serverAddr = "localhost" namespace = "public" cluster = "default" } consul { serverAddr = "127.0.0.1:8500" } apollo { app.id = "seata-server" apollo.meta = "http://192.168.1.204:8801" } zk { serverAddr = "127.0.0.1:2181" session.timeout = 6000 connect.timeout = 2000 } etcd3 { serverAddr = "http://localhost:2379" } file { name = "file.conf" } }

buildConfiguration() 方法会先读取 type,再根据 type 继续读取这个 type 所对应的具体配置。
比如说,type 如果是 file 类型,那么就会读取 config.file.name,获取到本地配置文件的名字,然后再去读取具体的配置文件。
如果 type 是 zk,那么就会读取 config.zk.* ,获取到 zk 的地址信息,然后通过请求 zk 的方式获取存放在 zk 上的信息。
读取配置时,主要是通过 Configuration 接口去获取,Configuration 接口有许多不同的实现,例如 FileConfiguration 或者 ZooKeeperConfiguration,分别代表不同的配置中心。
通过 buildConfiguration() 代码的逻辑可以看出,除了本地文件配置方式,其它配置方式都是采用 SPI 的服务发现机制进行扩展的。
不过这个 SPI 并不是直接用的 JDK 本身自带的 SPI 机制,而是 seata 自己实现的一种比 JDK SPI 功能更多的 SPI 机制,不过两者的主要思想是一样的。
阿里系的很多中间件软件都很喜欢用 SPI 机制,比如说 Dubbo,也是疯狂地 SPI。
这种自己实现的 SPI 机制的好处在于灵活性比较强,比如说可以自定义注解来标识实现类的优先级。
Configuration 接口定义的方法基本上是很基本的 getInt 或者 getString 这类方法,例如:

   /**

     * Gets int.

     *

     * @param dataId the data id

     * @param defaultValue the default value

     * @param timeoutMills the timeout mills

     * @return the int

     */

    int getInt(String dataId, int defaultValue, long timeoutMills);


/** * Gets int. * * @param dataId the data id * @param defaultValue the default value * @return the int */ int getInt(String dataId, int defaultValue);

这里有个需要注意的地方是,这些方法都有两个不同的参数列表,多了一个 timeoutMillis。
因为读取配置已经不能假设在本地文件读取来,而是有可能通过网络去某个注册中心读取,因为需要经过网络,那么必然会有读取不到的情况,这个参数是用来限制配置读取的超时时间。

接下来以 FileConfiguration 这个实现来看看 seata 的一些配置解析细节。
首先,本地配置文件的读取, seata 引用的是 typesafe 公司的一个解析库叫做 Config,这个解析库支持的配置风格,就是包括上文展示的 registry.conf 的这种类似于 json 的风格。 
其官方介绍是这样的:纯Java写成、零外部依赖、代码精简、功能灵活、API友好。支持Java properties、JSON、JSON超集格式HOCON以及环境变量。 

不过,它有一个重要的功能未实现,那就是配置文件的写入,所以目前 seata 也没有配置变更后更新本地配置文件的功能。 
可以看一下 ConfigOperateRunnable 的 run 方法:

        @Override

        public void run() {

            if (null != configFuture) {

                if (configFuture.isTimeout()) {

                    setFailResult(configFuture);

                    return;

                }

                try {

                    if (configFuture.getOperation() == ConfigOperation.GET) {

                        String result = fileConfig.getString(configFuture.getDataId());

                        configFuture.setResult(result);

                    } else if (configFuture.getOperation() == ConfigOperation.PUT) {

                        //todo

                        configFuture.setResult(Boolean.TRUE);

                    } else if (configFuture.getOperation() == ConfigOperation.PUTIFABSENT) {

                        //todo

                        configFuture.setResult(Boolean.TRUE);

                    } else if (configFuture.getOperation() == ConfigOperation.REMOVE) {

                        //todo

                        configFuture.setResult(Boolean.TRUE);

                    }

                } catch (Exception e) {

                    setFailResult(configFuture);

                    LOGGER.warn("Could not found property {}, try to use default value instead.",

                        configFuture.getDataId());

                }

            }

        }

赫然写着 TODO。
前面提到来读取配置有超时,那 seata 读取的时候怎么样做到检测超时呢,从下面这个方法入手:

    @Override

    public String getConfig(String dataId, String defaultValue, long timeoutMills) {

        String value;

        if ((value = getConfigFromSysPro(dataId)) != null) {

            return value;

        }

        ConfigFuture configFuture = new ConfigFuture(dataId, defaultValue, ConfigOperation.GET, timeoutMills);

        configOperateExecutor.submit(new ConfigOperateRunnable(configFuture));

        return (String)configFuture.get();

    }

每次读取配置时,都会读取封装成一个任务,扔给一个指定的线程池,再通过 ConfigFuture 去获取结果,ConfigFuture 是支持超时时间的设置的,只不过这里 FileConfiguration 没有在乎这个超时时间,毕竟超时是用来针对需要通过网络访问的第三方配置中心的。
这里用到了一个“异步转同步”的思想,也比较好地运用了 Future 机制。

在我的另一篇文章中有专门讲到了 seata 的这种机制 阿里开源分布式事务组件 seata :seata server 通信层解析

此外,Configuration 接口还定义了配置变更监听器相关的接口,允许监听某个配置项的变更。不过这块实现还不是很完整,比如说 zookeeper 的监听器目前还没有任何一种实现。
本文完。