分布式鎖現(xiàn)在用的越來越多,通常用來協(xié)調(diào)多個并發(fā)任務。在一般的應用場景中存在一定的不安全用法,不安全用法會帶來多個master在并行執(zhí)行,業(yè)務或數(shù)據(jù)可能存在重復計算帶來的副作用,在沒有拿到lock的情況下扮演者master等諸如此類。
成都創(chuàng)新互聯(lián)是一家專注于做網(wǎng)站、網(wǎng)站制作與策劃設計,新泰網(wǎng)站建設哪家好?成都創(chuàng)新互聯(lián)做網(wǎng)站,專注于網(wǎng)站建設10余年,網(wǎng)設計領域的專業(yè)建站公司;建站業(yè)務涵蓋:新泰等地區(qū)。新泰做網(wǎng)站價格咨詢:028-86922220
要想準確的拿到分布式鎖,并且準確的捕獲在分布式情況下鎖的動態(tài)轉(zhuǎn)移狀態(tài),需要處理網(wǎng)絡變化帶來的連鎖反應。比如常見的 session expire、connectionLoss,在設置lock狀態(tài)的時候我們?nèi)绾伪WC準確拿到lock。
在設計任務的時候我們需要具有 stop point 的策略,這個策略是用來在感知到lock丟失后能夠交付執(zhí)行權(quán)的機制。但是是否需要這么嚴肅的處理這個問題還取決于業(yè)務場景,比如下游的任務已經(jīng)做好冪等也無所謂重復計算。 但是在有些情況下確實需要嚴肅精準控制。
先說第一個場景,connectionLoss事件,此事件表示提交的commit有可能執(zhí)行成功也有可能執(zhí)行失敗,成功是指在zookeeper broker 中執(zhí)行成功但是返回的時候tcp斷開了,導致未能拿到返回的狀態(tài)。失敗是指根本就沒有提交到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();//監(jiān)控 master znode
this.takeLeadership();//執(zhí)行l(wèi)eader權(quán)利
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);//創(chuàng)建master節(jié)點
}
/**
* check master 循環(huán)檢查
*/
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表示具有執(zhí)行權(quán),只有成功拿到master 角色才能履行master權(quán)利。
runForMaster 方法一旦發(fā)現(xiàn)有connectionLoss就發(fā)起checkMaster進行檢查,同時checkMaster方法中也進行connectinLoss檢查,直到拿到明確的狀態(tài)為止。在此時有可能有另外的節(jié)點獲取到了master角色,那么當前節(jié)點就做好backup等待機會。
我們需要捕獲zookeeper所有的狀態(tài)變化,要知道m(xù)aster什么時候失效做好申請準備,當自己是master時候會話失效需要釋放master權(quán)利。
/**
* 監(jiān)控 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 已經(jīng)不存在
} 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 機制來進行狀態(tài)監(jiān)聽,保持與網(wǎng)絡、zookeeper狀態(tài)變化聯(lián)動。
我們在來看第二個問題,第一個問題是獲取lock的時候如何保證一定可以準確拿到狀態(tài),這里狀態(tài)是指master角色或者backup角色。
當我們成功與zookeeper broker建立鏈接,成功獲取到master角色并且正在履行master義務時突然zookeeper通知session過期,SessionExpired事件表示zookeeper將會刪除所有當前會話創(chuàng)建的臨時znode,也就意味這master znode將會被其他會話創(chuàng)建。
此時我們需要將自己的master 權(quán)利交出去,也就是我們必須放下目前手上執(zhí)行的任務,這個停止的狀態(tài)必須能夠反應到全局。此時最容易出現(xiàn)到問題就是,我們已經(jīng)不是master了但是還在偷偷到執(zhí)行master權(quán)利,通過dashboard會看到很奇怪的問題,不是master的服務器還在執(zhí)行。
case SESSIONEXPIRED:
//執(zhí)行 stop point 通知
this.stopPoint();
break;
所以這里需要我們在設計任務時有stop point 策略,類似jvm的safe point,隨時響應全局停止。
還有一種常見的使用方式是繞開zookeeper 來做狀態(tài)通知。
我們都知道zookeeper cluster 是由多臺實例組成,每個實例都在全國甚至全球的不同地方,leader到這些節(jié)點之間都有很大的同步延遲差異,zookeeper內(nèi)部采用法定人數(shù)的兩階段提交的方式來完成一次commit。
比如有7個實例構(gòu)成一套zookeeper cluster ,當一次client 寫入 commit只需要集群中有超過半數(shù)完成寫入就算這次commit提交成功了。但是cleint得到這個提交成功的響應之后立馬執(zhí)行接下來的任務,這個任務可能是讀取某個znode下的所有狀態(tài)數(shù)據(jù),此時有可能無法讀取到這個狀態(tài)。
如果是分布式鎖的話很有可能是鎖在zk集群中的轉(zhuǎn)移無法和client集群保持一直。所以只要是基于zookeeper做集群調(diào)度就要完全原來zookeeper來做狀態(tài)通知,不可以繞開zookeeper來自行調(diào)度。
zookeeper leader 是所有狀態(tài)變更的串行化器,add、update、delete都需要leader來處理,然后傳播給所有follower、observer節(jié)點。
所有的session是保存在leader中的,所有的watcher是保存在client鏈接的zookeper node中的,這里兩個場景都會導致狀態(tài)遷移的通知不準時。
如果zookeeper是由多數(shù)據(jù)中心構(gòu)成的一套集群,存在異地同步延遲的問題,leader是肯定會放在寫入的數(shù)據(jù)中心中,同時zid應該是最大的,甚至是一組高zid的機器都在寫入的數(shù)據(jù)中心中,這樣保證leader宕機也不會輕易導致leader選舉到其他數(shù)據(jù)中心。
但是follower、observer都會有client在使用,也會有在這些節(jié)點進行協(xié)調(diào)的分布式集群。
先說leader選舉導致異地節(jié)點延遲感知問題,比如當前 zookeeper cluster 有7臺機器構(gòu)成:
dataCenter shanghai:zid=100、zid=80、zid=50
dataCenter beijing: zid=10、zid=20
dataCenter shenzhen:zid=30、zid=40
由于網(wǎng)絡問題集群發(fā)生leader選舉,zid=100暫時脫離集群,zid=80成為leader,這里不考慮日志新舊問題,優(yōu)先使用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顯然會出現(xiàn)多個master運行情況,因為當你與zkNode斷開到時候,當時還沒有收到sessionExpired事件時,已經(jīng)有另外client成功創(chuàng)建master拿到權(quán)利。
這種情況在zkNode出現(xiàn)脫離集群當時候也會出現(xiàn),當zkNode斷開之后也會出現(xiàn)sessionExpired延遲通知問題。所有的watcher都是需要在新的zkNode上創(chuàng)建才會收到新的事件。
在極端情況下靜態(tài)擴容可能會導致zookeeper集群出現(xiàn)嚴重的數(shù)據(jù)不一致問題,比如現(xiàn)有集群:A、B、C,現(xiàn)在需要進行靜態(tài)擴容,停止ABC實例,拉入DE實例,此時如果C實例是ABC中最滯后的實例,如果AB啟動的速度沒有C快就會導致CDE組成新的集群,新的紀元號會覆蓋原來的AB日志。當然現(xiàn)在基本上不會接受靜態(tài)擴容,基本上都是動態(tài)擴容。
動態(tài)擴容在極端情況下也會出現(xiàn)類似問題,比如現(xiàn)在有三個機房,1、2、3,1機房方leader zid=200、100,2機房zid=80、50,3機房zid=40,假設上次的commit是在zid=200、100、50之間提交的,此時機房1出現(xiàn)斷網(wǎng),2機房zid=80、50與3機房zid=40開始組成新的集群,新的紀元在zid=50上產(chǎn)生。
在使用zookeeper來實現(xiàn)分布式鎖或者集群調(diào)度的時候會出現(xiàn)很多分布式下的問題,為了保證這些問題的出現(xiàn)不會帶來業(yè)務系統(tǒng)或者業(yè)務數(shù)據(jù)的不一致,我們還是在這些任務上做好冪等性考慮。
比如進行數(shù)據(jù)的計算,做個時間檢查,版本檢查之類的。如果本身是基于zookeeper實現(xiàn)的一套獨立的分布式系統(tǒng)需要的工作會更多點。
作者:王清培 (滬江集團資深架構(gòu)師)