disconf原理 “入坑”指南
之前有了解過disconf,也知道它是基於zookeeper來做的,但是對於其執行原理不太瞭解,趁著週末,debug下原始碼,也算是不枉費週末大好時光哈 :) 。關於這篇文章,筆者主要是參考disconf原始碼和官方文件,若有不正確地方,感謝評論區指正交流~
disconf是一個分散式配置管理平臺(Distributed Configuration Management Platform),專注於各種 分散式系統配置管理 的通用元件/通用平臺, 提供統一的配置管理服務,是一套完整的基於zookeeper的分散式配置統一解決方案。disconf目前已經被多個公司在使用,包括百度、滴滴出行、銀聯、網易、拉勾網、蘇寧易購、順豐科技 等知名網際網路公司。disconf原始碼地址 https://github.com/knightliao/disconf ,官方文件 https://disconf.readthedocs.io/zh_CN/latest/ 。
目前disconf包含了 客戶端disconf-Client和 管理端disconf-Web兩個模組,均由java實現。服務依賴元件包括Nginx、Tomcat、Mysql、ZooKeeper,nginx提供反向代理(disconf-web是前後端分離的),Tomcat是後端web容器,配置儲存在mysql上,基於zookeeper的wartch模型,實時推送。注意,disconf優先讀取本地檔案,disconf只支援應用對配置的讀操作,通過在disconf-web上更新配置,然後由zookeeper通知到服務例項,最後服務例項去disconf-web端獲取最新配置並更新到本地。
disconf 功能特點:
- 支援配置(配置項/配置檔案)分散式管理
- 配置釋出統一化
- 配置釋出、更新統一化,同一個上線包 無須改動配置 即可在 多個環境中(RD/QA/PRODUCTION) 上線
- 配置更新自動化:使用者在平臺更新配置,使用該配置的系統會自動發現該情況,並應用新配置。特殊地,如果使用者為此配置定義了回撥函式類,則此函式類會被自動呼叫
- 上手簡單,基於註解或者xml配置方式
功能特點描述圖
disconf 架構圖
分析disconf,最好是在本地搭建一個disconf-web環境,方便除錯程式碼,具體步驟可參考官方文件,使用disconf-client,只需要在pom引入依賴即可:
<dependency> <groupId>com.baidu.disconf</groupId> <artifactId>disconf-client</artifactId> <version>2.6.36</version> </dependency>
對於開發人員來說,最多接觸的就是disconf-web配置和disconf-client了,disconf-web配置官方文件已經很詳細了,這裡就來不及解釋了,抓緊上車,去分析disconf-client的實現,disconf-client最重要的內容就是disconf-client初始化流程和配置動態更新機制。disconf的功能是基於Spring的(初始化是在Spring的BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry開始的,配置動態更新也是要更新到Spring IoC中對應的bean),所以使用disconf,專案必須基於Spring。
1 disconf-client 初始化流程
關於disconf-client的初始化,聯想到Spring IoC流程,我們先不看程式碼,可以猜想一下其大致流程,disconf-client首先需要從disconf服務端獲取配置,然後等到IoC流程中建立好對應的bean之後,將對應的配置值設定到bean中,這樣基本上就完成了初始化流程,其實disconf的初始化實現就是這樣的。 disconf-client的初始化開始於BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry(Spring IoC初始化時,對於BeanDefinitionRegistryPostProcessor的實現類,會呼叫其postProcessBeanDefinitionRegistry方法),disconf的DisconfMgrBean類就是BeanDefinitionRegistryPostProcessor的實現類,DisconfMgrBean類的bean配置在哪裡呢?其實就是disconf.xml中的配置,該配置是必須的,示例如下:<!-- 使用disconf必須新增以下配置 --> <bean id="disconfMgrBean" class="com.baidu.disconf.client.DisconfMgrBean" destroy-method="destroy"> <property name="scanPackage" value="com.luo.demo"/> </bean> <bean id="disconfMgrBean2" class="com.baidu.disconf.client.DisconfMgrBeanSecond" init-method="init" destroy-method="destroy"> </bean>
DisconfMgrBean#postProcessBeanDefinitionRegistry方法主要做的3件事就是掃描(firstScan)、註冊DisconfAspectJ 和 bean屬性注入。
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { // scanPackList包括disconf.xml中DisconfMgrBean.scanPackage List<String> scanPackList = StringUtil.parseStringToStringList(scanPackage, SCAN_SPLIT_TOKEN); // 1. 進行掃描 DisconfMgr.getInstance().setApplicationContext(applicationContext); DisconfMgr.getInstance().firstScan(scanPackList); // 2. register java bean registerAspect(registry); }
1.1 firstScan
firstScan操作主要是載入系統配置和使用者配置(disconf.properties),進行包掃描併入庫,然後獲取獲取資料/注入/Watch。protected synchronized void firstScan(List<String> scanPackageList) { // 匯入配置 ConfigMgr.init(); // registry Registry registry = RegistryFactory.getSpringRegistry(applicationContext); // 掃描器 scanMgr = ScanFactory.getScanMgr(registry); // 第一次掃描併入庫 scanMgr.firstScan(scanPackageList); // 獲取資料/注入/Watch disconfCoreMgr = DisconfCoreFactory.getDisconfCoreMgr(registry); disconfCoreMgr.process(); }
進行包掃描是使用Reflections來完成的,獲取路徑下(比如xxx/target/classes)某個包下符合條件(比如com.luo.demo)的資源(reflections),然後從reflections獲取某些符合條件的資源列表,如下:
/** * 掃描基本資訊 */ private ScanStaticModel scanBasicInfo(List<String> packNameList) { ScanStaticModel scanModel = new ScanStaticModel(); // 掃描物件 Reflections reflections = getReflection(packNameList); scanModel.setReflections(reflections); // 獲取DisconfFile class Set<Class<?>> classdata = reflections.getTypesAnnotatedWith(DisconfFile.class); scanModel.setDisconfFileClassSet(classdata); // 獲取DisconfFileItem method Set<Method> af1 = reflections.getMethodsAnnotatedWith(DisconfFileItem.class); scanModel.setDisconfFileItemMethodSet(af1); // 獲取DisconfItem method af1 = reflections.getMethodsAnnotatedWith(DisconfItem.class); scanModel.setDisconfItemMethodSet(af1); // 獲取DisconfActiveBackupService classdata = reflections.getTypesAnnotatedWith(DisconfActiveBackupService.class); scanModel.setDisconfActiveBackupServiceClassSet(classdata); // 獲取DisconfUpdateService classdata = reflections.getTypesAnnotatedWith(DisconfUpdateService.class); scanModel.setDisconfUpdateService(classdata); return scanModel; }View Code 獲取到資源資訊(比如DisconfFile 和DisconfFileItem )之後,讀取DisConfFile類及其對應的DisconfFileItem資訊,將它們放到disconfFileItemMap中,最後將這些資訊儲存到倉庫DisconfCenterStore。這部分邏輯在ScanMgrImpl.firstScan方法中,整體邏輯還是比較清晰的,這裡就不貼程式碼了。 掃描入庫之後,就該獲取資料/注入/Watch(DisconfCoreFactory.getDisconfCoreMgr()中邏輯)了。
public static DisconfCoreMgr getDisconfCoreMgr(Registry registry) throws Exception { FetcherMgr fetcherMgr = FetcherFactory.getFetcherMgr(); // 不開啟disconf,則不要watch了 WatchMgr watchMgr = null; if (DisClientConfig.getInstance().ENABLE_DISCONF) { // Watch 模組 watchMgr = WatchFactory.getWatchMgr(fetcherMgr); } return new DisconfCoreMgrImpl(watchMgr, fetcherMgr, registry); } public static WatchMgr getWatchMgr(FetcherMgr fetcherMgr) throws Exception { synchronized(hostsSync) { // 從disconf-web端獲取 Zoo Hosts資訊,及zookeeper host和zk prefix資訊(預設 /disconf) hosts = fetcherMgr.getValueFromServer(DisconfWebPathMgr.getZooHostsUrl(DisClientSysConfig .getInstance() .CONF_SERVER_ZOO_ACTION)); zooPrefix = fetcherMgr.getValueFromServer(DisconfWebPathMgr.getZooPrefixUrl(DisClientSysConfig .getInstance () .CONF_SERVER_ZOO_ACTION)); /** * 初始化watchMgr,這裡會與zookeeper建立連線,如果/disconf節點不存在會新建 */ WatchMgr watchMgr = new WatchMgrImpl(); watchMgr.init(hosts, zooPrefix, DisClientConfig.getInstance().DEBUG); return watchMgr; } return null; }從disconf-web端獲取zk host和 zk prefix之後,會建立與zk的連線,然後就該從disconf-web端下載配置和watcher了,也就是disconfCoreMgr.process()邏輯。下載配置時disconf-client從disconf-web端獲取配置的全量資料(http連線),並存放到本地,然後解析資料,生成dataMap,dataMap是全量資料。然後將資料注入到倉庫中(DisconfCenterStore.confFileMap,型別為Map<String, DisconfCenterFile>)。注意:這裡還沒有將配置的值設定到bean中,設定bean值是在Spring的finishBeanFactoryInitialization流程做的,準確來說是在初始化bean DisconfMgrBeanSecond(bean配置在disconf.xml中),呼叫其init方法中做的。 在將值設定到倉庫之後,就該監聽對應配置了,這樣才能使用zk的watch機制,在zk上監聽的url格式為 /disconf/boot-demo_1_0_0_0_rd/file/specific.properties ,如果該url對應的node不存在則新建,注意該node是persistent型別的。然後在該node下新建臨時節點,節點名字是discon-client的簽名,格式為host_port_uuid,節點data為針對該配置檔案,disconf-client需要的配置項的json格式資料,比如"{"port":9998,"host":"192.168.1.104"}"。
1.2 註冊DisconfAspectJ
往Spring中註冊一個aspect類DisconfAspectJ,該類會對@DisconfFileItem註解修飾的方法做切面,功能就是當獲取bean屬性值時,如果開啟了DisClientConfig.getInstance().ENABLE_DISCONF,則返回disconf倉庫中對應的屬性值,否則返回bean實際值。注意:目前版本的disconf在更新倉庫中屬性值後會將bean的屬性值也一同更改,所以,目前DisconfAspectJ類作用已不大,不必理會,關於該類的討論可參考issue DisconfAspectJ 攔截的作用?
1.3 bean屬性注入
bean屬性注入是從DisconfMgr.secondScan開始的:
protected synchronized void secondScan() { // 掃描回撥函式,也就是註解@DisconfUpdateService修飾的配置更新回撥類,該類需實現IDisconfUpdate if (scanMgr != null) { scanMgr.secondScan(); } // 注入資料至配置實體中 if (disconfCoreMgr != null) { disconfCoreMgr.inject2DisconfInstance(); } }bean屬性注入通過獲取倉庫中對應的屬性值,然後呼叫setMethod.invoke或者field.set方法來設定,bean對應的boject是通過Spring來獲取的,也就是說,在獲取後bean已經初始化完成,只不過對應的屬性值還不是配置檔案中配置的而已。如果程式中有2個類的@DisconfFile都是同一個配置檔案,那麼這個時候獲取的bean是哪個類的bean呢?關於這個可以點選issue DisconfFile用法諮詢,disconf目前只支援一個配置檔案一個類的方式,不給兩個class同時使用同一個 "resources.properties",否則第二個是不生效的。
2 配置動態更新機制
disconf的配置動態更新藉助於zk的watch機制(watch機制是zk 3大重要內容之一,其餘兩個是zk協議和node儲存模型)實現的,初始化流程會中會對配置檔案註冊watch,這樣當配置檔案更新時,會通知到discnof-client,然後disconf-client再從disconf-web中獲取最新的配置並更新到本地,這樣就完成了配置動態更新。 配置動態更新動作開始於DisconfFileCoreProcessorImpl.updateOneConfAndCallback()方法:/** * 更新訊息: 某個配置檔案 + 回撥 */ @Override public void updateOneConfAndCallback(String key) throws Exception { // 更新 配置 updateOneConf(key); // 回撥 DisconfCoreProcessUtils.callOneConf(disconfStoreProcessor, key); callUpdatePipeline(key); }更新配置時,首先更新倉庫中值,然後更新bean屬性值,配置更新回撥是使用者自定義的回撥方法,也就是@DisconfUpdateService修飾的類。配置更新時流程是: 開發人員在前端更新配置 -> disconf-web儲存資料並更新zookeeper -> zookeeper通知disconf-client -> discnof-client 從 disconf-web下載對應配置 -> 更新倉庫和bean屬性 -> 呼叫回撥 -> 更新配置完成。