zookeeper 實現分散式鎖安全用法
zookeeper 實現分散式鎖安全用法
標籤: zookeeper sessionExpire connectionLoss 分散式鎖
- 背景
- ConnectionLoss 連結丟失
- SessionExpired 會話過期
- 繞開 zookeeper broker 進行狀態通知
- leader 選舉與zkNode 斷開
- 做好冪等
- 靜態擴容、動態擴容
背景
分散式鎖現在用的越來越多,通常用來協調多個併發任務。在一般的應用場景中存在一定的不安全用法,不安全用法會帶來多個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實現的一套獨立的分散式系統需要的工作會更多點。
作者:王清培 (滬江集團資深架構師)