zookeeper 實現分布式鎖安全用法
背景
分布式鎖現在用的越來越多,通常用來協調多個並發任務。在一般的應用場景中存在一定的不安全用法,不安全用法會帶來多個master在並行執行,業務或數據可能存在重復計算帶來的副作用,在沒有拿到lock的情況下扮演者master等諸如此類。
要想準確的拿到分布式鎖,並且準確的捕獲在分布式情況下鎖的動態轉移狀態,需要處理網絡變化帶來的連鎖反應。比如常見的 session expire、connectionLoss,在設置lock狀態的時候我們如何保證準確拿到lock。
在設計任務的時候我們需要具有 stop point 的策略,這個策略是用來在感知到lock丟失後能夠交付執行權的機制。但是是否需要這麽嚴肅的處理這個問題還取決於業務場景,比如下遊的任務已經做好冪等也無所謂重復計算。 但是在有些情況下確實需要嚴肅精準控制。
ConnectionLoss 鏈接丟失
先說第一個場景,connectionLoss事件,此事件表示提交的commit有可能執行成功也有可能執行失敗,成功是指在zookeeper broker 中執行成功但是返回的時候tcp斷開了,導致未能拿到返回的狀態。失敗是指根本就沒有提交到zookeper broker中鏈接就斷開了。
所以在我們獲取lock的時候需要做 connectionLoss 事件處理,我們看個例子。
protected void runForMaster() { logger.info("master:run for master."); AsyncCallback.StringCallback createCallback = (rc, path, ctx, name) -> { switch (KeeperException.Code.get(rc)) { case CONNECTIONLOSS: checkMaster();//鏈接失效檢查znode設置是否成功 return; case OK: isLeader = true; logger.info("master:I‘m the leader serverId:" + serverId); addMasterWatcher();//監控 master znode this.takeLeadership();//執行leader權利 break; case NODEEXISTS: isLeader = false; String serverId = this.getMasterServerId(); this.takeBackup(serverId); break; } }; zk.create(rootPath + "/master", serverId.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL, createCallback, null);//創建master節點 } /** * check master 循環檢查 */ private void checkMaster() { AsyncCallback.DataCallback masterCheckCallback = (rc, path, ctx, data[], stat) -> { switch (KeeperException.Code.get(rc)) { case CONNECTIONLOSS: checkMaster(); return; case NONODE: runForMaster(); return; default: { String serverId = this.getMasterServerId(); isLeader = serverId.equals(this.serverId); if (BooleanUtils.isNotTrue(isLeader)) { this.takeBackup(serverId); } else { this.takeLeadership(); } } return; } }; zk.getData(masterZnode, false, masterCheckCallback, null); }
這裏的master表示具有執行權,只有成功拿到master 角色才能履行master權利。
runForMaster 方法一旦發現有connectionLoss就發起checkMaster進行檢查,同時checkMaster方法中也進行connectinLoss檢查,直到拿到明確的狀態為止。在此時有可能有另外的節點獲取到了master角色,那麽當前節點就做好backup等待機會。
我們需要捕獲zookeeper所有的狀態變化,要知道master什麽時候失效做好申請準備,當自己是master時候會話失效需要釋放master權利。
/**
* 監控 master znode 做 master/slave 切換
*/
private void addMasterWatcher() {
AsyncCallback.StatCallback addMasterWatcher = (rc, path, ctx, stat) -> {
switch (KeeperException.Code.get(rc)) {
case CONNECTIONLOSS:
addMasterWatcher();
break;
case OK:
if (stat == null) {
runForMaster();//master 已經不存在
} else {
logger.info("master:watcher master znode ok.");
}
break;
case NONODE:
logger.info("master:master znode delete.");
runForMaster();
break;
}
};
zk.exists(masterZnode, MasterExistsWatcher, addMasterWatcher, null);
}
通過zookeeper watcher 機制來進行狀態監聽,保持與網絡、zookeeper狀態變化聯動。
SessionExpired 會話過期
我們在來看第二個問題,第一個問題是獲取lock的時候如何保證一定可以準確拿到狀態,這裏狀態是指master角色或者backup角色。
當我們成功與zookeeper broker建立鏈接,成功獲取到master角色並且正在履行master義務時突然zookeeper通知session過期,SessionExpired事件表示zookeeper將會刪除所有當前會話創建的臨時znode,也就意味這master znode將會被其他會話創建。
此時我們需要將自己的master 權利交出去,也就是我們必須放下目前手上執行的任務,這個停止的狀態必須能夠反應到全局。此時最容易出現到問題就是,我們已經不是master了但是還在偷偷到執行master權利,通過dashboard會看到很奇怪的問題,不是master的服務器還在執行。
case SESSIONEXPIRED:
//執行 stop point 通知
this.stopPoint();
break;
所以這裏需要我們在設計任務時有stop point 策略,類似jvm的safe point,隨時響應全局停止。
繞開 zookeeper broker 進行狀態通知
還有一種常見的使用方式是繞開zookeeper 來做狀態通知。
我們都知道zookeeper cluster 是由多臺實例組成,每個實例都在全國甚至全球的不同地方,leader到這些節點之間都有很大的同步延遲差異,zookeeper內部采用法定人數的兩階段提交的方式來完成一次commit。
比如有7個實例構成一套zookeeper cluster ,當一次client 寫入 commit只需要集群中有超過半數完成寫入就算這次commit提交成功了。但是cleint得到這個提交成功的響應之後立馬執行接下來的任務,這個任務可能是讀取某個znode下的所有狀態數據,此時有可能無法讀取到這個狀態。
如果是分布式鎖的話很有可能是鎖在zk集群中的轉移無法和client集群保持一直。所以只要是基於zookeeper做集群調度就要完全原來zookeeper來做狀態通知,不可以繞開zookeeper來自行調度。
leader 選舉與zkNode 斷開
zookeeper leader 是所有狀態變更的串行化器,add、update、delete都需要leader來處理,然後傳播給所有follower、observer節點。
所有的session是保存在leader中的,所有的watcher是保存在client鏈接的zookeper node中的,這裏兩個場景都會導致狀態遷移的通知不準時。
如果zookeeper是由多數據中心構成的一套集群,存在異地同步延遲的問題,leader是肯定會放在寫入的數據中心中,同時zid應該是最大的,甚至是一組高zid的機器都在寫入的數據中心中,這樣保證leader宕機也不會輕易導致leader選舉到其他數據中心。
但是follower、observer都會有client在使用,也會有在這些節點進行協調的分布式集群。
先說leader選舉導致異地節點延遲感知問題,比如當前 zookeeper cluster 有7臺機器構成:
dataCenter shanghai:zid=100、zid=80、zid=50
dataCenter beijing: zid=10、zid=20
dataCenter shenzhen:zid=30、zid=40
由於網絡問題集群發生leader選舉,zid=100暫時脫離集群,zid=80成為leader,這裏不考慮日誌新舊問題,優先使用zid進行選舉。
由於集群中所有的session是保存在原來zid=100的機器中的,新leader沒有任何session信息,所以將導致所有session丟失。
session的保持時間是取決於我們設置的sessinoTimeout時間來的,client通過ping來將心跳傳播到所鏈接的zkNode,這個zkNode可能是任意角色的node,然後zkNode在與zkleaderNode進行心跳來保持會話,同時zkNode也會通過ping來保持會話超時時間。
此時當原有當client在重新鏈接上zkNode時會被告知sessionExpired。sessionExpired 是由zkNode通知出來的,當會話丟失或者過期,client在去嘗試鏈接zkNode時候會被zkNode告知會話過期。
如果client只捕獲了sessionExpired顯然會出現多個master運行情況,因為當你與zkNode斷開到時候,當時還沒有收到sessionExpired事件時,已經有另外client成功創建master拿到權利。
這種情況在zkNode出現脫離集群當時候也會出現,當zkNode斷開之後也會出現sessionExpired延遲通知問題。所有的watcher都是需要在新的zkNode上創建才會收到新的事件。
靜態擴容、動態擴容
在極端情況下靜態擴容可能會導致zookeeper集群出現嚴重的數據不一致問題,比如現有集群:A、B、C,現在需要進行靜態擴容,停止ABC實例,拉入DE實例,此時如果C實例是ABC中最滯後的實例,如果AB啟動的速度沒有C快就會導致CDE組成新的集群,新的紀元號會覆蓋原來的AB日誌。當然現在基本上不會接受靜態擴容,基本上都是動態擴容。
動態擴容在極端情況下也會出現類似問題,比如現在有三個機房,1、2、3,1機房方leader zid=200、100,2機房zid=80、50,3機房zid=40,假設上次的commit是在zid=200、100、50之間提交的,此時機房1出現斷網,2機房zid=80、50與3機房zid=40開始組成新的集群,新的紀元在zid=50上產生。
做好冪等
在使用zookeeper來實現分布式鎖或者集群調度的時候會出現很多分布式下的問題,為了保證這些問題的出現不會帶來業務系統或者業務數據的不一致,我們還是在這些任務上做好冪等性考慮。
比如進行數據的計算,做個時間檢查,版本檢查之類的。如果本身是基於zookeeper實現的一套獨立的分布式系統需要的工作會更多點。
作者:王清培 (滬江集團資深架構師)
zookeeper 實現分布式鎖安全用法