簡介
diamond是淘寶內部使用的一個管理持久配置的系統,它的特點是簡單、可靠、易用,目前淘寶內部絕大多數系統的配置,由diamond來進行統一管理。
diamond為應用系統提供了獲取配置的服務,應用不僅可以在啟動時從diamond獲取相關的配置,而且可以在運行中對配置數據的變化進行感知並獲取變化后的配置數據。
持久配置是指配置數據會持久化到磁盤和數據庫中。
diamond的特點是簡單、可靠、易用:
簡單:整體結構非常簡單,從而減少了出錯的可能性。
可靠:應用方在任何情況下都可以啟動,在承載淘寶核心系統並正常運行一年多以來,沒有出現過任何重大故障。
易用:客戶端使用只需要兩行代碼,暴露的接口都非常簡單,易於理解。
1、作為一個配置中心,diamond的功能分為發布和訂閱兩部分。因為diamond存放的是持久數據,這些數據的變化頻率不會很高,甚至很低,所以發布采用手工的形式,通過diamond后台管理界面發布;訂閱是diamond的核心功能,訂閱通過diamond-client的API進行。 2、diamond服務端采用mysql加本地文件的形式存放配置數據。發布數據時,數據先寫到mysql,再寫到本地文件;訂閱數據時,直接獲取本地文件,不查詢數據庫,這樣可以最大程度減少對數據庫的壓力。 3、diamond服務端是一個集群,集群中的每台機器連接同一個mysql,集群之間的數據同步通過兩種方式進行,一是每台server定時去mysqldump數據到本地文件,二是某一台server接收發布數據請求,在更新完mysql和本機的本地文件后,發送一個HTTP請求(通知)到集群中的其他幾台server,其他server收到通知,去mysql中將剛剛更新的數據dump到本地文件。 4、每一台server前端都有一個nginx,用來做流量控制。 5、圖中沒有將地址服務器畫出,地址服務器是一台有域名的機器,上面運行有一個HTTPserver,其中有一個靜態文件,存放着diamond服務器的地址列表。客戶端啟動時,根據自身的域名綁定,連接到地址服務器,取回diamond服務器的地址列表,從中隨機選擇一台diamond服務器進行連接。 可以看到,整個diamond的架構非常簡單,使用的都是最常用的一些技術以及產品,它之所以表現得非常穩定,跟其架構簡單是分不開的,當然,穩定的另一個主要原因是它具備一套比較完善的容災機制,容災機制將在下一篇文章中講述。
源碼地址
https://github.com/takeseem/diamond.git
服務端安裝
- 檢出源碼,修改配置文件 jdbc.properties 中的數據庫連接信息,完成之后maven打包
- 數據庫執行初始化sql
create 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秒
客戶端安裝
客戶端獲取數據方法:
DiamondManager 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管理的數據。具體代碼
<!-- 引入依賴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>
重寫PropertyPlaceholderConfigurer
package 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; } @Override 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工具類
package 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配置
<!-- 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兩部分,主要包括以下幾個方面:
1、server存儲數據的方式。
server存儲數據是“數據庫+本地文件”的方式,集群間的數據同步我們在之前的文章中講過(請參考專題二的原理部分),client訂閱數據時,訪問的是本地文件,不查詢數據庫,這樣即使數據庫出問題了,仍然不影響client的訂閱。
2、server是一個集群。
這是一個基本的容災機制,集群中的一台server不可用了,client發現后可以自動切換到其他server上進行訪問,自動切換在client內部實現。
3、client保存snapshot
client每次從server獲取到數據后,都會將數據保存在本地文件系統,diamond稱之為snapshot,即數據快照。當client下次啟動發現在超時時間內所有server均不可用(可能是網絡故障),它會使用snapshot中的數據快照進行啟動。
4、client校驗MD5
client每次從server獲取到數據后,都會進行MD5校驗(數據保存在responsebody,MD5保存在responseheader),以防止因網絡故障造成的數據不完整,MD5校驗不通過直接拋出異常。
5、client與server分離
client可以和server完全分離,單獨使用,diamond定義了一個“容災目錄”的概念,client在啟動時會創建這個目錄,每次主動獲取數據(即調用getAvailableConfigInfomation()方法),都會優先從“容災目錄”獲取數據,如果client按照一個固定的規則,在“容災目錄”下配置了需要的數據,那么client直接獲取到數據返回,不再通過網絡從diamond-server獲取數據。同樣的,在每次輪詢時,都會優先輪詢“容災目錄”,如果發現配置還存在於其中,則不再向server發出輪詢請求。以上的情形,會持續到“容災目錄”的配置數據被刪除為止。
根據以上的容災機制,我們可以總結一下diamond整個系統完全不可用的條件:
1、數據庫不可用。
2、所有server均不可用。
3、client主動刪除了snapshot
4、client沒有備份配置數據,導致其不能配置“容災目錄”。
同時滿足以上4個條件的概率,在生產環境中是極小的。
以上就是diamond的容災機制
其他相關
Xdiamond
1、基於數據庫做配置存儲
2、相對於diamond增加了權限設計,結合Secret key,保證配置的安全
3、配置緩存在本地,防止應用因為網絡問題而不能啟動
disconf是來自百度的分布式配置管理平台,包括百度、滴滴出行、銀聯、網易、拉勾網、蘇寧易購、順豐科技 等知名互聯網公司正在使用! https://github.com/knightliao/disconf