城的灯

集中式配置中心设计

随着公司的发展,团队成员和项目数越来越多,系统架构不可避免的要向系统安全和开发运维的成本等方面倾斜。毕竟过了小米加步枪的高速发展阶段,我们必须要鸟枪换炮,将系统的质量提高一个层级。

需求

绝大多数项目都离不开包含私密信息的配置文件,这些配置信息之前是分散在数以百计的项目中。不但管理起来麻烦,而且配置信息都放在项目中不可避免的存在安全隐患。我们经过分析研究,发现很多项目的配置都存在共性,如数据库的配置,日志的输出,ZK服务器的地址等等。虽然在maven中有阿里的auto-config插件,在安全性方面有所提升。但是由于该插件只能工作在maven,不能用于gradle等构建工具,并且每个项目还要按照要求编写模板。所以我们需要代码和配置完全分离,不依赖于任何的构建工具,项目代码中只有代码逻辑。

由于我们有些项目中的配置还需要实时的变动,并同步到所有机器,写死在配置文件中,那么服务就必须要重新启动,当然通过JMX或者通过服务调用来修改都行,但是如果服务集群较多,就会存在不小的工作量。所以我们需要希望我们的配置有发布订阅的功能。

最后所有的配置信息集中存储,统一管理,各个项目中不再需要配置文件。开发人员只需要关心业务,配置全交给运维人员,并且如果配置如果出现异常,可以快速回滚。

设计

需求明确了之后,我们将所有的项目进行了梳理,所有的配置信息类型可以归纳为下图:

config-type

从上图,我们可以看出其实我们的配置就是一个4层的文件夹。配置的存储,ZooKeeper当然是不二的选择。原因如下:

  1. ZK本来就是我们基础设施中不可获取的一环。
  2. 配置信息的结构和ZK的功能设计完全契合。
  3. 事件通知ZK天生就支持。
  4. ZK的高可用。

在做架构时,我们必须要保证系统能够平滑升级,将老系统升级的风险降到最低。我们发现90%的老系统都使用了spring框架,并且这些系统中90%的配置都是由spring加载。所以代码层面的切入点就非常明确了,分为如下两步:

  1. 包装一个ZK的工具类ZKUtils,实现读取ZK的配置和订阅文件的变化。
  2. 扩展org.springframework.beans.factory.config.PropertyPlaceholderConfigurer,让其能够从ZK读取配置。

技术细节

在技术的实现上,我们遵循Convention Over Configuration(约定优于配置),下面以一个名为Test的项目作为案例来说明。

配置的级别 路径
Global /Global/Properties/Test
Project /Global/Project/Properties/Test
Server /Global/Project/Server/Properties/Test
Process /Global/Project/Server/{进程标识如端口号}/Properties/Test

项目名称也约定配置在classpath下的一个文件中,该项目名称会用在所有的内部框架中,如日志的输出文件名,配置文件的路径,监控中心的名称等等。

ZKUtils的实现就不说了,直接使用Curator稍微封装下,就能够满足需求。

PropertyPlaceholderConfigurer的实现类就直接贴代码了,都很简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* Created by IntelliJ IDEA
* User:杨果
* Date:11/9/23
* Time:下午10:34
*/
public class TestPropertyPlaceholderConfigurer extends PropertyPlaceholderConfigurer {

/**
* spring需要加载的properties文件路径列表
* <br/>
* 文件路径名约定为"{scope:path}"
* <br/>
* 如:项目Test中配置"Global:properties/redis.properties"代表读取"/Global/Properties/Test/properties/redis.properties"文件。
*/
public void setTestProperties(List<String> testProperties) {
if (null == testProperties || testProperties.size() <= 0) throw new RuntimeException("文件路径名不能为空");

List<Properties> propertiesArray = new ArrayList<>();
for (String value : testProperties) {
String[] strs = value.split(":");
if (strs.length != 2)
throw new RuntimeException("文件路径必须是{scope:path}的形式。如:项目Test中配置\"Global:properties/redis.properties\"代表读取\"/Global/Properties/Test/properties/redis.properties\"文件");

propertiesArray.add(ZKUtils.getConfProperties(strs[0], strs[1]));

}
setPropertiesArray(propertiesArray.toArray(new Properties[propertiesArray.size()]));
setIgnoreUnresolvablePlaceholders(true);
}
}

配置的使用如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

<bean
class="info.yangguo.TestPropertyPlaceholderConfigurer">
<property name="testProperties">
<list>
<value>Global:properties/redis.properties</value>
<value>Global:properties/mysql.properties</value>
</list>
</property>
</bean>

<context:component-scan base-package="info.yangguo.redis"/>
<context:component-scan base-package="info.yangguo.dao" />
</beans>

上面就是我在设计这个集中式配置中心的时候的一些想法,当然代码的具体实现比这个复杂多了,我只是捡最重要的说,是想把一个比较复杂的东西简单化。其实里面还有很多的容错机制,比如ZK集群宕机怎么办,是否可以读取本地的缓存等等。