依賴zookeeper元件的一種高可用實踐
背景
電子商務系統大量使用mysql資料庫作為其交易和儲存的系統; 隨著商戶和使用者量的不斷增長,mysql中儲存的資料量會越來越大,這時把所有資料儲存在一張表或者一個數據庫中會極大的影響系統的效能和安全。 分庫分表是業界一個比較通用的方案,並且也比較成熟。
為了進行分庫分表,我們需要為業務表中設定一個唯一的id;舉個商品中心的例子:為了把一個租戶下的所有菜品,菜價,菜品分類放在一下,會在所有這些表上加上一個全域性唯一的租戶id。
全域性id演算法
經過我們前期的調研和討論,我們最終選擇了twitter的snowflake(詳細介紹請參考
未用 | 毫秒數 | datacenterId | workId | 毫秒內序列號 |
---|---|---|---|---|
1bit | 41bit | 5bit | 5bit | 12bit |
該演算法在本地程序執行效率非常高,但datacenterId, 和workId需要在一個叢集中被分配成唯一的;在實際應用中,datacenterId可能沒有,那workId就是10個bit。
下面章節將重點介紹唯一workId的生成過程。
zookeeper生成唯一的workId
workId分配演算法在zookeeper中的節點
0
invoicing標識進銷存服務的節點。
lock是實現分散式鎖的節點,Lock_i是臨時順序節點。
workId節點下儲存每一個機器節點,key=ip1, data=workId1 (演算法保證workId不重複),為永久節點。
zookeeper的節點型別
型別 | 描述 |
---|---|
持久節點(PERSISTENT) | 在節點建立後,就一直存在,直到有刪除操作來主動清除這個節點——不會因為建立該節點的客戶端會話失效而消失 |
持久順序節點(PERSISTENT_SEQUENTIAL) | 這類節點包含持久節點的特性;額外的特性是,每個父節點會為他的第一級子節點維護一份時序,會記錄每個子節點建立的先後順序。在建立此類節點中,ZK會自動為給定節點名加上一個數字字尾,作為新的節點名。這個數字字尾的範圍是整型的最大值。 |
臨時節點(EPHEMERAL) | 和持久節點不同的是,臨時節點的生命週期和客戶端會話繫結。也就是說,如果客戶端會話失效,那麼這個節點就會自動被清除掉。注意,這裡提到的是會話失效,而非連線斷開。另外,在臨時節點下面不能建立子節點。 |
臨時順序節點(EPHEMERAL_SEQUENTIAL) | 臨時自動編號節點;當客戶端和伺服器的session超時後,節點被刪除;在被建立時每個節點被自動的編號。 |
生成唯一workId的流程圖
其中在zookeeper中實現互斥鎖是演算法的難點。
Zookeeper實現互斥鎖的流程圖
zookeeper原始碼中分散式鎖的原始碼分析(/zookeeper-3.5.1-alpha/src/recipes/lock/src/c/src/zoo_lock.c)
主要的邏輯程式碼在zkr_lock_operation()中, 解釋主要邏輯
staticintzkr_lock_operation(zkr_lock_mutex_t *mutex, struct timespec *ts) {
-------------------- //省略部分程式碼
//獲取Locks下所有的節點
ret = retry_getchildren(zh, path, &vectorst, ts, retry);
if(ret != ZOK)
returnret;
struct String_vector *vector = &vectorst;
mutex->id = lookupnode(vector, prefix); // 獲取當前節點的id
if(mutex->id == NULL) {
//當前id不存在,則建立一個臨時順序節點
ret = zoo_create(zh, buf, NULL, 0, mutex->acl,
ZOO_EPHEMERAL|ZOO_SEQUENCE, retbuf, (len+20));
}
if(mutex->id != NULL) {
ret = ZCONNECTIONLOSS;
ret = retry_getchildren(zh, path, vector, ts, retry);
if(ret != ZOK) {
LOG_WARN(("could not connect to server"));
returnret;
}
//sort this list, 按照節點的編號排序,
sort_children(vector);
owner_id = vector->data[0]; //獲取最小編號的節點
mutex->ownerid = strdup(owner_id);
id = mutex->id;
char* lessthanme = child_floor(vector->data, vector->count, id); // 獲取比自己編號小的節點
if(lessthanme != NULL){ //證明當前最小編號的節點不是我自己, 該程式不能獲得鎖
---------- //省略部分程式碼
ret = retry_zoowexists(zh, last_child, &lock_watcher_fn, mutex, | }
&stat, ts, retry); //比自己編號小的節點是一個列表,觀察該列表中編號最大的節點
//這樣比觀察父節點/Locks的變化有優勢,能夠有效的減少“驚群效應”
---------- //省略部分程式碼
} else{
//獲得了該鎖
}
}
zookeeper高可用實踐
Zookeeper中的幾個重要角色
角色名 | 描述 | 參與寫 | 參與讀 |
---|---|---|---|
領導者(Leader) | Leader作為整個ZooKeeper叢集的主節點,負責響應所有對ZooKeeper狀態變更的請求; 領導者負責進行投票的發起和決議,更新系統狀態,處理寫請求。 |
必然參與 | 可以參與 |
跟隨者(Follwer) | 響應本伺服器上的讀請求外,follower還要處理leader的提議,並在leader提交該提議時在本地進行提交。 |
必然參與 | 可以參與 |
觀察者(Observer) | 觀察者可以接收客戶端的讀寫請求,並將寫請求轉發給Leader,但Observer節點不參與投票過程, 只同步leader狀態,Observer的目的是為了,擴充套件系統,提高讀取速度;3.3.0版本以上才有這個角色。 |
不參與 | 主要參與 |
客戶端(Client) | 執行讀寫請求的發起方。 |
|
|
某公司之前的部署模式(在同一機房部署Follow, Leader節點):
缺點:當client讀量增加後,可以通過增加叢集的Follower來提升系統的讀效能;
但隨著Follower節點資料的增加,系統的寫效能會有很大的影響(所有的follower都要參與提議的投票過程,這樣follower節點越多,參與的決議投票的follower就越多);
zookeeper叢集之前有過讀流量和使用者亂用client,導致拖垮主叢集的casestudy。
基礎架構組反饋某部門的讀流量特別小,當前zookeeper叢集按照按這種模式部署的。
美團公司當前的部署模式(優化後):
叢集部署的說明:
型別 | 描述 | 職責 | 是否儲存資料 |
---|---|---|---|
主機房 | 由Leader/Follower構成的投票叢集(對應之前的部署模式) | 負責叢集的讀寫請求 | 儲存 |
機房A | 由Observer構成的ZK叢集 | 負責處理讀請求,轉發client的寫請求到主機房 | 儲存 |
機房B | 由Observer構成的ZK叢集 | 負責處理讀請求,轉發client的寫請求到主機房 | 儲存 |
優點:
客戶端能夠在本機房讀取所需要的資料,減少跨機房的呼叫延遲。
Observer機器發生故障,或者機房之間的鏈路發生故障, 不會影響到zookeeper主叢集的使用
workId生成演算法弱依賴zookeeper的實踐
因為workId生成演算法只在程式初次部署,或者重啟的時候需要訪問zookeeper,並且該配置後續一直都不會更改,可以考慮儲存在zookeeper中的資訊,也在本地檔案或者配置中心中儲存一份。
初始化邏輯:
容錯邏輯:
配置中心在設計的時候就有本地快取(快取檔案),可以直接複用配置中心寫本地檔案的邏輯,而不用額外的寫一個新的本地檔案。
參考文獻:
zookeeper的整體介紹: http://www.ibm.com/developerworks/cn/opensource/os-cn-zookeeper/
zookeeper的部署實踐: http://www.cnblogs.com/sunddenly/p/4143306.html
zookeeper sre: zookeeper叢集架構