1. 基本介紹
在分佈式的環境中,可能會有多個對等的程序讀取同樣的配置文件,程序可以部署在多臺機器上,如果配置採用文件的話,則需要爲部署該程序的機器也部署一個配置文件,一旦要修改配置的時候就會非常麻煩,需要修改多個配置文件,而且容易產生不一致。
集中式配置管理的思路是,將配置數據集中發佈到ZooKeeper的節點上,供訂閱者動態獲取數據。實現配置的集中式管理和動態更新。可以簡單的理解爲配置數據與程序分離。
2. 場景分析
(1).集中式配置管理
通常來說,大部分項目裏面都有約定的配置文件格式,如ini,xml等。一般都會有對應的解析庫類。這種解析庫類的基本工作模式爲:
- 讀取文件(open)
- 解析文件(parse)
- 對外提供參數(get)
如果我們將文件的內容全部放到ZooKeeper的某個節點上.解析類將配置數據全部下載到本地,在完成解析的話,則可以用很小的改動就完成集中式配置管理的需求。
- 讀取Zookeeper上對應路徑的數據(read)
- 解析文件(parse)
- 對外提供參數(get)
(2).動態更新
動態更新是希望不重啓程序就能夠實時獲取更新的配置。在單機環境中,這種配置數據通常會放在數據庫中,修改配置只需要update數據庫就可以了。
使用ZooKeeper的話,需要節點註冊一個watcher,監視配置數據的是否有變化,一定出現變化,則調用新的解析類來重新解析配置數據。
個人認爲這個特徵使用Zookeeper可以實現,但是並不是所有配置都需要這個功能,這種比較適合對配置敏感,需要實時更新配置的情況。
3. 動手實踐
這裏我只實現了集中式配置管理的功能,沒有實現動態更新,有需要的話你可以嘗試自己實現。
由於之前曾經做個一個ini文件的庫類解析,這裏就直接拿過來改了。根據場景的分析,只需要修改open這個函數就ok了。
看下原來的open函數
/*讀取文件名要改爲地址和路徑*/
int IniFile::open(const string &filename)
{
release();
fname_ = filename;
IniSection *section = NULL;
/*讀取數據的方式需要修改*/
FILE *fp = fopen(filename.c_str(),"r");
if(fp == NULL ){
return -1;
}
string line;
string comment;
//增加默認段
section = new IniSection();
sections_[""] = section;
/*獲取行的方式需要修改*/
while(getline(line,fp) > 0){
...//省略單行的解析
}
fclose(fp);
return 0;
}
我們有三個主要需要修改的地方,分別是是入參,fopen和getline。下面是修改後的open函數
/*修改入參,host爲Zookeeper的ip及端口地址,filepath爲配置數據的路徑*/
int IniFile::open2(const string &host,const string &filepath)
{
release();
fname_ = filepath;
IniSection *section = NULL;
char fp[2048]={0};
/*ZooKeeper來讀取*/
zkopen(host,filepath,fp,sizeof(fp));
if(fp[0] == 0){
return -1;
}
string line;
string comment;
//增加默認段
section = new IniSection();
sections_[""] = section;
char *p = fp;
/*調整getline的入參*/
while(getline2(line,p) > 0){
...//省略單行的解析
}
return 0;
}
zkopen從Zookeeper的節點上讀取數據,並保存到fp中。代碼如下:
string zkopen(const string &host,const string &filepath,char *fp,int len)
{
int timeout = 30000;
char path_buffer[512];
int bufferlen=sizeof(path_buffer);
char conf_data[2048];
int conf_len=sizeof(conf_data);
zoo_set_debug_level(ZOO_LOG_LEVEL_WARN); //設置日誌級別,避免出現一些其他信息
zhandle_t* zkhandle = zookeeper_init(host.c_str(),NULL, timeout, 0, (char *)"Monitor Test", 0);
if (zkhandle ==NULL)
{
fprintf(stderr, "Error when connecting to zookeeper servers...\n");
exit(EXIT_FAILURE);
}
int ret = zoo_get(zkhandle,filepath.c_str(),0,conf_data,&conf_len,NULL);
if(ret != ZOK){
fprintf(stderr,"failed to get the data of path %s!\n",filepath.c_str());
conf_data[0] = 0;
}
zookeeper_close(zkhandle);
strncpy(fp,conf_data,len);
return conf_data;
}
接下來在對比下調用的變化。
原來的調用方式:
/** read test **/
IniFile ini;
ini.open(g_filepath);
//獲取指定段的指定項的值
int ret = 0;
string db_name = ini.getStringValue("COMMON","DB",ret);
string db_passwd = ini.getStringValue("COMMON","PASSWD",ret);
現在的調用方式:
/** read test **/
IniFile ini;
ini.open2(g_host,g_filepath);/*僅此處有變化*/
//獲取指定段的指定項的值
int ret = 0;
string db_name = ini.getStringValue("COMMON","DB",ret);
string db_passwd = ini.getStringValue("COMMON","PASSWD",ret);
由上可見,配置的改造還是很容易的,而且對程序的改動很小。
代碼詳見https://github.com/Winnerhust/ZooKeeper-Exam/tree/master/Config
5.小提示
需要注意一點,配置文件中通常有很多換行,而ZooKeeper的客戶端命令行工作不支持字符轉義。比如你要將一個配置文件test.ini的內容保存到Zookeeper上,文件內容如下。
[COMMON]
DB=mysql
PASSWD=root
你可能會在Zookeeper客戶端上輸入:[zk: 172.17.0.36:2181(CONNECTED) 39] create /Conf/test.ini [COMMON]\nDB=mysql\nPASSWD=root\n
結果與我們希望的並不一樣:[zk: 172.17.0.36:2181(CONNECTED) 43] get /Conf/test.ini3[COMMON]\nDB=mysql\nPASSWD=root\n
Zookeeper並沒有將字符串進行轉義,所以不能用ZooKeeper客戶端直接上傳配置文件。因此在代碼裏我還增加了一個上傳配置的功能。只需要將上一個參數-r
就可以了。如將test.ini文件的內容上傳到ZooKeeper:cat test.ini | testcase -r -p/Conf/test.ini -s172.17.0.36:2181