简介
diamond是淘宝内部使用的一个管理持久配置的系统,它的特点是简单、可靠、易用,目前淘宝内部绝大多数系统的配置,由diamond来进行统一管理。
diamond为应用系统提供了获取配置的服务,应用不仅可以在启动时从diamond获取相关的配置,而且可以在运行中对配置数据的变化进行感知并获取变化后的配置数据。
diamond的特点是简单、可靠、易用:
- 简单:整体结构非常简单,从而减少了出错的可能性。
- 可靠:应用方在任何情况下都可以启动,在承载淘宝核心系统并正常运行一年多以来,没有出现过任何重大故障。
- 易用:客户端使用只需要两行代码,暴露的接口都非常简单,易于理解。
github地址 https://github.com/takeseem/diamond
整体架构
- 作为一个配置中心,diamond的功能分为发布和订阅两部分。因为diamond存放的是持久数据,这些数据的变化频率不会很高,甚至很低,所以发布采用手工的形式,通过diamond后台管理界面发布;订阅是diamond的核心功能,订阅通过diamond-client的API进行。
- diamond服务端采用mysql加本地文件的形式存放配置数据。发布数据时,数据先写到mysql,再写到本地文件;订阅数据时,直接获取本地文件,不查询数据库,这样可以最大程度减少对数据库的压力。
- diamond服务端是一个集群,集群中的每台机器连接同一个mysql,集群之间的数据同步通过两种方式进行,一是每台server定时去mysqldump数据到本地文件,二是某一台server接收发布数据请求,在更新完mysql和本机的本地文件后,发送一个HTTP请求(通知)到集群中的其他几台server,其他server收到通知,去mysql中将刚刚更新的数据dump到本地文件。
- 每一台server前端都有一个nginx,用来做流量控制。
- 地址服务器是一台有域名的机器,上面运行有一个HTTPserver,其中有一个静态文件,存放着diamond服务器的地址列表。客户端启动时,根据自身的域名绑定,连接到地址服务器,取回diamond服务器的地址列表,从中随机选择一台diamond服务器进行连接。
可以看到,整个diamond的架构非常简单,使用的都是最常用的一些技术以及产品,它之所以表现得非常稳定,跟其架构简单是分不开的,当然,稳定的另一个主要原因是它具备一套比较完善的容灾机制,容灾机制将在下一篇文章中讲述。
服务端安装
- 检出源码,修改配置文件 jdbc.properties 中的数据库连接信息,完成之后maven打包
- 数据库执行初始化sql
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17create database diamond;
grant all on diamond.* to CK@'%' identified by 'abc';
use diamond
create table config_info (
`id` bigint(64) unsigned NOT NULL auto_increment,
`data_id` varchar(255) NOT NULL default ' ',
`group_id` varchar(128) NOT NULL default ' ',
`content` longtext NOT NULL,
`md5` varchar(32) NOT NULL default ' ',
`gmt_create` datetime NOT NULL default '2010-05-05 00:00:00',
`gmt_modified` datetime NOT NULL default '2010-05-05 00:00:00',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_config_datagroup` (`data_id`,`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ; - 将打好的包 diamond-server.war 放到tomcat工作目录,启动。启动成功之后,访问 http://localhost:8090/diamond-server/
- 发布数据,账号密码是user.properties中配置的,默认是 abc=123。登录后进入后台管理界面,然后点击“配置信息管理”—— “添加配置信息”,在输入框中输入dataId、group、内容,最后点击“提交”即可。成功后,可以在“配置信息管理”中查询到发布的数据。
- 集群安装。修改node.properties,格式为 ip\:port ,这里面的冒号,一定要通过\转义一下,要不然获取地址不对。当存在node节点的配置,发布修改数据后会通知其他节点更新。
- 每台diamond-server 前建议增加nginx转发,方便限流,而且客户端默认请求80端口
- 其他配置: system.properties中的dump_config_interval 是多久去更新一次本地缓存的数据 默认是 600秒
客户端安装
客户端获取数据方法:1
2
3
4
5
6
7
8
9
10DiamondManager manager = new DefaultDiamondManager(group, dataId, new ManagerListener() {
public Executor getExecutor() {
return null;
}
public void receiveConfigInfo(String configInfo) {
// 客户端处理数据的逻辑
}
});
集成思路:重写PropertyPlaceholderConfigurer,将diamond管理的配置交个spring,spring的xml可以直接使用${}来查询数据,增加工具类PropertiesUtils.java 方便查询diamond管理的数据。具体代码1
2
3
4
5
6
7
8
9
10
11<!-- 引入依赖diamond -->
<dependency>
<groupId>com.taobao.diamond</groupId>
<artifactId>diamond-client</artifactId>
<version>2.0.5.4.taocode-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.taobao.diamond</groupId>
<artifactId>diamond-utils</artifactId>
<version>2.0.5.4.taocode-SNAPSHOT</version>
</dependency>
重写PropertyPlaceholderConfigurer1
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
32
33
34
35
36
37
38
39
40
41
42
43package com.zyx.demo.common.spring;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
/**
* <p>重写PropertyPlaceholderConfigurer,将diamond配置信息交给spring</p>
*/
public class SpringPropertyPlaceholderConfigurer extends PropertyPlaceholderConfigurer {
private List<String> diamondList;
public List<String> getDiamondList() {
return diamondList;
}
public void setDiamondList(List<String> diamondList) {
this.diamondList = diamondList;
}
protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, Properties props) throws BeansException {
Properties properties = PropertiesUtils.getProperties(diamondList);
if (properties == null) {
String diamondFilePath = PropertiesUtils.DIAMOND_FILEPATH;//System.getProperty("user.home") + System.getProperty("file.separator") + ".diamond.domain";
throw new RuntimeException("从diamond获取配置为空(dataId和group是" + diamondList + "),请检查diamond要连接的环境:" + diamondFilePath);
}
this.setProperties(properties);
for (Iterator<Object> iterator = properties.keySet().iterator(); iterator.hasNext();) {
String key = (String) iterator.next();
String value = (String) properties.get(key);
props.setProperty(key, value);
}
super.processProperties(beanFactoryToProcess, properties);
}
}
PropertiesUtils.java工具类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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182package com.zyx.demo.common.spring;
import com.taobao.diamond.manager.ManagerListener;
import com.taobao.diamond.manager.ManagerListenerAdapter;
import com.taobao.diamond.manager.impl.DefaultDiamondManager;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.Executor;
/**
* <p>工具类,获取diamond配置</p>
*/
public class PropertiesUtils {
public static Properties properties;
private static Logger logger = Logger.getLogger(PropertiesUtils.class);
private static final long TIME_OUT = 5000L;
private static String diamondIpList;
private static List<String> diamondIdgroupList;
protected static final String DIAMOND_FILEPATH="diamond.data";
public static Properties getProperties(List<String> diamondList) {
diamondIdgroupList = diamondList;
if (null == properties) {
init();
}
return properties;
}
public static Properties getProperties() {
if (null == properties) {
init();
}
return properties;
}
/**
* 根据key从map中取值
*/
public static Object getValueByKey(String key) {
if (null == properties) {
init();
}
return properties.get(key);
}
public static String getStringValueByKey(String key) {
return (String) getValueByKey(key);
}
public static int getIntValueByKey(String key) {
return Integer.parseInt((String) getValueByKey(key));
}
public static double getDoubleValueByKey(String key) {
return Double.parseDouble((String) getValueByKey(key));
}
public static boolean getBooleanValueByKey(String key) {
return Boolean.parseBoolean((String) (getValueByKey(key)));
}
public static String getStringValueByKey(String key, String defaultV) {
Object value = getValueByKey(key);
if (value == null) {
return defaultV;
}
return (String) value;
}
public static int getIntValueByKey(String key, int defaultV) {
Object value = getValueByKey(key);
if (value == null) {
return defaultV;
}
return Integer.parseInt((String) value);
}
public static double getDoubleValueByKey(String key, double defaultV) {
Object value = getValueByKey(key);
if (value == null) {
return defaultV;
}
return Double.parseDouble((String) value);
}
public static boolean getBooleanValueByKey(String key, boolean defaultV) {
Object value = getValueByKey(key);
if (value == null) {
return defaultV;
}
return Boolean.parseBoolean((String) (value));
}
/**
* init(读取多个dataId 与 groupId )*/
private static void init() {
String diamondFilePath = PropertiesUtils.class.getClassLoader().getResource(DIAMOND_FILEPATH).getPath() ;//System.getProperty("user.home") + "/.diamond.domain";
try {
List<String> contentList = FileUtils.readLines(new File(diamondFilePath), "UTF-8");
for (String ipList : contentList) {
if (!ipList.contains("#")) {
diamondIpList = ipList.trim();
break;
}
}
} catch (Exception e) {
logger.error("获取diamond文件内容失败:" + e.getMessage(), e);
}
logger.info("diaond-->filePath:" + diamondFilePath + " change diamondIpList:" + diamondIpList);
if (diamondIdgroupList != null && diamondIpList != null) {
for (String str : diamondIdgroupList) {
// dataid
String dataId = "";
String groupId = "";
if (str.indexOf(":") > -1) {
dataId = str.substring(0, str.indexOf(":"));
}
if (str.lastIndexOf(":") > -1) {
groupId = str.substring(str.indexOf(":") + 1,str.length());
}
if (!StringUtils.isEmpty(dataId) && !StringUtils.isEmpty(groupId)) {
DefaultDiamondManager manager = new DefaultDiamondManager(dataId, groupId, new ManagerListenerAdapter() {
public void receiveConfigInfo(String configInfo) {
//数据发生变更时,更新数据
putAndUpdateProperties(configInfo);
}
}, diamondIpList);
String configInfo = manager.getAvailableConfigureInfomation(TIME_OUT);
logger.debug("从diamond取到的数据是:" + configInfo);
putAndUpdateProperties(configInfo);
} else {
logger.error("diamond数据配置properties异常: DataId:" + dataId + ",Group:" + groupId);
}
}
} else {
logger.error("diamond数据配置properties异常: diamondBeanList is null or diamondIpList is null");
}
}
/**
* 更新properties中数据*/
public static void putAndUpdateProperties(String configInfo) {
if (StringUtils.isNotEmpty(configInfo)) {
if (properties == null) {
properties = new Properties();
}
try {
properties.load(new ByteArrayInputStream(configInfo.getBytes()));
} catch (IOException e) {
logger.error("根据diamond数据流转成properties异常" + e.getMessage(), e);
}
} else {
logger.error("从diamond取出的数据为空,请检查配置");
}
}
public static List<String> getDiamondIdgroupList() {
return diamondIdgroupList;
}
public static void setDiamondIdgroupList(List<String> diamondIdgroupList) {
PropertiesUtils.diamondIdgroupList = diamondIdgroupList;
}
public String getDiamondIpList() {
return diamondIpList;
}
}
spring配置1
2
3
4
5
6
7
8<!-- diamond管理配置文件 -->
<bean id = "propertyConfigurer" class="com.zyx.demo.common.spring.SpringPropertyPlaceholderConfigurer">
<property name="diamondList">
<list>
<value>com-zyx-demo:com-zyx-demo</value>
</list>
</property>
</bean>
容灾机制
diamond具有一套完备的容灾机制,容灾机制涉及到client和server两部分,主要包括以下几个方面:
- server存储数据的方式。
server存储数据是“数据库+本地文件”的方式,集群间的数据同步我们在之前的文章中讲过(请参考专题二的原理部分),client订阅数据时,访问的是本地文件,不查询数据库,这样即使数据库出问题了,仍然不影响client的订阅。 - server是一个集群。
这是一个基本的容灾机制,集群中的一台server不可用了,client发现后可以自动切换到其他server上进行访问,自动切换在client内部实现。 - client保存snapshot
client每次从server获取到数据后,都会将数据保存在本地文件系统,diamond称之为snapshot,即数据快照。当client下次启动发现在超时时间内所有server均不可用(可能是网络故障),它会使用snapshot中的数据快照进行启动。 - client校验MD5
client每次从server获取到数据后,都会进行MD5校验(数据保存在responsebody,MD5保存在responseheader),以防止因网络故障造成的数据不完整,MD5校验不通过直接抛出异常。 - client与server分离
client可以和server完全分离,单独使用,diamond定义了一个“容灾目录”的概念,client在启动时会创建这个目录,每次主动获取数据(即调用getAvailableConfigInfomation()方法),都会优先从“容灾目录”获取数据,如果client按照一个固定的规则,在“容灾目录”下配置了需要的数据,那么client直接获取到数据返回,不再通过网络从diamond-server获取数据。同样的,在每次轮询时,都会优先轮询“容灾目录”,如果发现配置还存在于其中,则不再向server发出轮询请求。以上的情形,会持续到“容灾目录”的配置数据被删除为止。
根据以上的容灾机制,我们可以总结一下diamond整个系统完全不可用的条件:
- 数据库不可用。
- 所有server均不可用。
- client主动删除了snapshot
- client没有备份配置数据,导致其不能配置“容灾目录”。
同时满足以上4个条件的概率,在生产环境中是极小的。以上就是diamond的容灾机制
其他相关
disconf是来自百度的分布式配置管理平台。
包括百度、滴滴出行、银联、网易、拉勾网、苏宁易购、顺丰科技 等知名互联网公司正在使用!
github地址 https://github.com/knightliao/disconf