1. 程式人生 > >zookeeper 實現分布式鎖安全用法

zookeeper 實現分布式鎖安全用法

() 之間 void 寫入 機房 串行化 client .data 無法

  • 背景
  • 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實現的一套獨立的分布式系統需要的工作會更多點。

    作者:王清培 (滬江集團資深架構師)

    zookeeper 實現分布式鎖安全用法