學習zookeeper,看看這一篇

zookeeper學習總結

  • zookeeper是一個服務於分佈式應用程序的中間件協調服務

單體架構到微服務架構

  • 單體架構下,一個完整的流程比如"購物",可以在單體架構中完成整個流程。
  • 分佈式架構下,服務進行細化,拆分,涉及到服務之間的通信。

SpringBoot+RestTemplate

  • SpringBoot提供RestTemplate支持遠程通信,其簡化了http服務的通信
  • 使用RestTemplate,只需要使用者提供URL
  • RestTemplate本質上是對現有技術進行了封裝,如JDK、Apache HttpClient、OkHttp等

分佈式一致性問題

  • 在一個分佈式系統中,有多個節點,每個節點都會提出一個請求,
  • 但是所有節點中只有一個請求會被通過
  • 被通過的請求是所有節點達成一致的結果
  • 所謂一致性:提出的所有請求中能夠選出最終一個確定的請求,並且選出之後,所有節點都要知道

分佈式鎖服務

  • zookeeper是作爲分佈式鎖服務的一設計
  • 註冊中心是zookeeper是其能夠實現的功能之一

zookeeper集羣部署

  • 進入zk安裝的conf目錄下,複製zoo.sample.cfg文件爲zoo.cfg文件,並配置dirData目錄存儲相關數據
  • 在配置的dirData路徑下創建myid文件,文件內容自定義,爲每一臺zk服務器的標識
  • 在conf目錄下的zoo.cfg配置文件下配置集羣
    • zk啓動的時候會根據myid的內容與配置文件中的配置相匹配
  • 集羣啓動後,會進行leader選舉,選出leader節點
#2181:訪問zookeeper的端口     3888:重新選舉後leadeer的端口
server.1=192.168.1.1:2181:3888
server.2=192.168.1.1:2181:3888
server.3=192.168.1.1:2181:3888
#解包
tar -zxvf z..
#啓動ZK服務
./zkServer.sh start
#查看zk服務狀態
./zkServer.sh status
#停止zk服務
./zkServer.sh stop
#重啓zk服務
./zkServer.sh restart
#連接zk
sh zkCli.sh

問題記錄

  • 記錄創建zookeeper集羣時遇到的坑
  • 本地虛擬機同時啓動三臺總有一臺無法運行——暫未解決
  • zk啓動之後查看啓動狀態和日誌發現,節點之間未建立連接
    • 通過修改服務器上的hosts文件,去掉ip解決

zookeeper集羣角色

  • 在zookeeper集羣中一共有三種角色:Leader、follower、Observer
  • leader節點是被選舉出的主節點,這裏稱爲leader選舉
  • follow節點參與leader選舉,選舉失敗爲follower節點
  • Observer爲觀察者節點,不參與選舉

zookeeper數據結構

  • zk的數據結構類似於標準的文件系統,其內部維護了一系列的節點稱爲zNode
  • 每個節點上都可以保存數據,數據形式是以key-value的形式存儲的
  • 節點上都可以保存數據以及掛載子節點,構成層次化的樹形結構

節點類型

  • 持久化節點:PERSISTENT 創建完成後一直存在zk服務器上,直到主動刪除
  • 持久有序節點:PERSISTENT_SEQUENTIAL 會爲其子節點維護一個順序
  • 臨時節點:EPHEMERAL 生命週期與會話綁定在一起,當客戶端失效,自動清理
  • 臨時有序節點:在臨時節點的基礎上多了一個有序性

zookeeper會話

  • 初始化或連接未建立的狀態是connecting

  • 建立連接的狀態時connected

  • 關閉連接的狀態是closed

zookeeper節點狀態信息

  • zk的每個節點除了存儲數據內容之外,還存儲了節點本身的狀態信息,通過get命令可獲得詳細內容
狀態屬性 解釋說明
czxid Create ZXID 表示該節點被創建時的事務ID
mzxid Modify ZXID 表示該節點最後一次被更新時的事務ID
ctime Create Time 表示節點被創建的時間
mtime Modify Time 節點最後一次被更新的時間
version 數據節點的版本號
cversion 子節點的版本號
aversion 節點的ACL版本號
ephemeralOwner 創建臨時節點的會話sessionID若爲持久化節點,該屬性值爲0
dataLength 數據內容的長度
numChildren 當前節點的子節點個數
pzxid 表示當前節點的子節點列表最後一次被修改時的事務ID(這裏特指子節點列表變更)

zk-分佈式數據的原子性

  • zk爲數據節點引入了版本的概念,每個數據節點對應三類版本信息
  • 對數據節點的任何更新操作都會引起版本號的變化
  • 通過版本實現分佈式數據的原子性類似於悲觀鎖和樂觀鎖的概念
    • 悲觀鎖、樂觀鎖概念回顧
    • 悲觀鎖是數據庫中一種併發控制策略,當一個事務A正在對數據進行處理
    • 數據將會處於鎖定狀態,這期間其它事務無法對數據進行操作
    • 樂觀鎖,多個事務在處理過程中不受影響,在同時對一個數據進行修改時
    • 在更新請求提交之前,會對數據是否衝突進行檢測,若發生衝突則提交被拒絕
  • zk就是通過version來實現樂觀鎖機制的——”寫入校驗“

watcher

  • zk提供了分佈式數據的發佈/訂閱功能
  • zk允許客戶端向服務端註冊一個watcher監聽,當服務端一些事件觸發了watcher
  • 服務端向客戶端發送一個事件通知
  • watcher通知是一次性的,一旦觸發一次通知後,watcher就會失效,可以通過循環註冊實現持續監聽

zk重試策略

  • 在和zk服務器建立連接的時候,有提供三種連接策略 retryPolicy
RetryOneTime 僅僅重試一次
RetryUntilElapsed 一直重試直到規定時間結束
RetryNTimes 指定最大重試次數

zk節點權限控制

  • zk提供ACL權限控制機制(Access Control List)來保證數據的安全性
權限模式 授權對象
IP IP地址或IP段,被賦予權限的用戶可在指定IP或IP段進行操作
Digest 最常用控制模式,設置的時候需要DigestAuthenticationProvider.generateDigest() SHA- 加 密和 base64 編碼
World 最開放的控制模式,數據訪問權限對所用用戶開放
Surper 超級用戶,可對節點進行任何操作

zk——watcher機制

  • zk提供watcher監聽機制,可對節點數據變更、刪除、子節點狀態變更等事件進行監聽
  • 通過監聽機制,可以基於zookeeper實現分佈式鎖、集羣管理等功能
zookeeper事件 事件含義
EventType.NodeCreated 當節點被創建,事件觸發
EventType.NodeChildrenChanged 監聽當前節點的直接子節點
EventType.NodeDataChanged 節點數據發生變更,事件觸發
EventType.NodeDeleted 節點刪除,事件觸發
EventType.None 客戶端連接狀態變更,事件觸發
  • watcher監聽是一次性的,可通過循環註冊實現永久監聽

  • Curator是對zookeeper的封裝,提供了三種watcher來監聽節點的變化

    • PathChildCache:監視一個路徑下目標子結點的創建、刪除、更新

    • NodeCache:監視當前結點的創建、更新、刪除,並將結點的數據緩存在本地

    • TreeCache:PathChildCache 和 NodeCache 的“合體”,監視路徑下的創建、更新、刪除事件,

      並緩存路徑下所有孩子結點的數據

zk實現分佈式鎖

  • 基於Curator實現分佈式鎖
  • 在分佈式架構下,涉及到多個進程訪問同一個共享資源的情況
  • 這個時候沒有辦法使用synchronized和lock之類的鎖實現數據安全
  • zookeeper實現分佈式鎖
  • zk的同級節點具有唯一性,可利用此特性實現獨佔鎖
  • 即多個進程在zk的指定節點下創建一個名稱相同的節點,只有一個進程能夠創建成功,認爲其獲取了鎖
  • 創建失敗的節點可通過zookeeper的watcher機制來監聽子節點的變化,監聽到刪除事件,觸發所有進程去競爭鎖
  • 通過有序節點實現分佈式鎖,可避免大量進程同時去競爭鎖,每個節點只需要監聽比自己小的節點,避免了資源浪費
  • curator基於zk提供了分佈式鎖的基本使用

Curator 實現分佈式鎖的基本原理

  • 利用path創建臨時有序節點,實現公平鎖核心,1代表了能獲得分佈式鎖的數量,即爲互斥鎖

在這裏插入圖片描述

  • 由LockInternals對象執行分佈式鎖的申請和釋放

在這裏插入圖片描述

  • internaLock(-1, null):不限時等待 internaLock:限時等待

在這裏插入圖片描述

private boolean internalLock(long time, TimeUnit unit) throws Exception
{
    Thread currentThread = Thread.currentThread();
    //  這裏給定的 LockData 實例只能由一個線程操作
    LockData lockData = threadData.get(currentThread);
    if ( lockData != null )
    {
        // 實現可重入,lockData映射表存放線程的鎖信息
        lockData.lockCount.incrementAndGet();
        return true;
    }

    ///  映射表 中沒有對應的鎖信息, ,通過 attempLock獲得鎖    
    String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());
    if ( lockPath != null )
    {
        //  獲取鎖成功記錄  鎖信息到映射表  即  thread
        //  private final ConcurrentMap<Thread, LockData> threadData = Maps.newConcurrentMap();
        LockData newLockData = new LockData(currentThread, lockPath);
        threadData.put(currentThread, newLockData);
        return true;
    }

    return false;
}
  • lockCount:分佈式鎖重入次數

在這裏插入圖片描述

  • attempLock 嘗試獲取鎖,並返回鎖對應的 Zookeeper 臨時順序節點的路徑
String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception
{
    final long      startMillis = System.currentTimeMillis();
    //  無限時等待  millisToWait 爲null
    final Long      millisToWait = (unit != null) ? unit.toMillis(time) : null;
    final byte[]    localLockNodeBytes = (revocable.get() != null) ? new byte[0] : lockNodeBytes;
    int             retryCount = 0;
    //  臨時節點路徑
    String          ourPath = null;
    // 是否已經持有分佈式鎖
    boolean         hasTheLock = false;
    //  是否已經完成獲取鎖操作
    boolean         isDone = false;
    while ( !isDone )
    {
        isDone = true;
        try
        {
            //  在 zk 中創建臨時順序節點
            ourPath = driver.createsTheLock(client, path, localLockNodeBytes);
            //  循環等待來激活分佈式鎖,實現鎖的公平性
            hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath);
        }
        catch ( KeeperException.NoNodeException e )
        {
            // gets thrown by StandardLockInternalsDriver when it can't find the lock node
            // this can happen when the session expires, etc. So, if the retry allows, just try it all again
            if ( client.getZookeeperClient().getRetryPolicy().allowRetry(retryCount++, System.currentTimeMillis() - startMillis, RetryLoop.getDefaultRetrySleeper()) )
            {
                isDone = false;
            }
            else
            {
                throw e;
            }
        }
    }
    if ( hasTheLock )
    {
        // 成功獲得分佈式鎖,返回臨時順序節點的路徑上層將其封裝成鎖信息,記錄在映射表,方便鎖重入
        return ourPath;
    }
    return null;
}
  • 創建臨時順序節點即鎖節點,createsTheLock
public String createsTheLock(CuratorFramework client, String path, byte[] lockNodeBytes) throws Exception
{
    String ourPath;
    //  lockNodeBytes爲空的話,採用默認(IP地址)創建節點
    if ( lockNodeBytes != null )
    {
         ourPath=client.create()
             .creatingParentContainersIfNeeded()
             .withProtection()
             //  保證臨時節點的有序性
             .withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
             .forPath(path, lockNodeBytes);
    }
    else
    {
        ourPath = client.create()
            .creatingParentContainersIfNeeded()
            .withProtection()
            .withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
            .forPath(path);
    }
    return ourPath;
}
  • internalLockLoop 判斷是否已經持有分佈式鎖:循環等待激活,實現鎖的公平性
private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception
{
    //  是否已經持有鎖
    boolean     haveTheLock = false;
    //  是否刪除子節點
    boolean     doDelete = false;
    try
    {
        if ( revocable.get() != null )
        {
            client.getData().usingWatcher(revocableWatcher).forPath(ourPath);
        }

        while ( (client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock )
        {
            //  獲取排序後的子節點列表
            List<String> children = getSortedChildren();
            // +1 to include the slash  獲取自己創建的臨時子節點的名稱
            String sequenceNodeName = ourPath.substring(basePath.length() + 1); 
            PredicateResults predicateResults = driver
                .getsTheLock(client, children, sequenceNodeName, maxLeases);
            if ( predicateResults.getsTheLock() )
            {
                // 獲得了鎖,中斷循環
                haveTheLock = true;
            }
            else
            {
                //  沒有獲得鎖,,監聽上一臨時順序節點
                String  previousSequencePath = basePath + "/" + predicateResults.getPathToWatch();

                synchronized(this)
                {
                    try 
                    {
    // use getData() instead of exists() to avoid leaving unneeded watchers which is a type of resource leak                         //  使用 getData() 避免資源泄露,exists會創建不必要的監聽器
                        //  這裏 如果監聽到上一順序節點被刪除,,則會獲得鎖
                        client.getData().usingWatcher(watcher).forPath(previousSequencePath);
                        if ( millisToWait != null )
                        {
                            millisToWait -= (System.currentTimeMillis() - startMillis);
                            startMillis = System.currentTimeMillis();
                            if ( millisToWait <= 0 )
                            {
                                //  獲取鎖超時 刪除屬於當前進程的臨時順序節點
                                doDelete = true;    // timed out - delete our node
                                break;
                            }

                            wait(millisToWait);
                        }
                        else
                        {
                            wait();
                        }
                    }
                    catch ( KeeperException.NoNodeException e ) 
                    {
                        // it has been deleted (i.e. lock released). Try to acquire again
                    }
                }
            }
        }
    }
    catch ( Exception e )
    {
        ThreadUtils.checkInterrupted(e);
        doDelete = true;
        throw e;
    }
    finally
    {
        if ( doDelete )
        {
            deleteOurPath(ourPath);
        }
    }
    return haveTheLock;
}
  • getsTheLock獲得鎖的邏輯
    • 獲取臨時有序節點在排序後的列表中的索引
    • 校驗創建的臨時有序節點是否有效
      在這裏插入圖片描述
  • 鎖公平性核心邏輯
    • 已經瞭解到maxLeases爲1,即當ourIndex爲零的時候,線程才能獲得鎖
    • zk臨時有序節點特性,保證了跨多個JVM線程併發創建節點時的順序性,越早創建成功越早激活鎖
    • 如果已經獲取了鎖,無需監聽任何節點,否則需要監聽上一順序節點,,即 ourIndex-1
    • 鎖是公平的,因此無需監聽除了 ourIndex-1 以外的任何節點
    • 如果 ourIndex < 0,則表示會話過期或連接丟失等原因,該線程創建的臨時有序節點被zk刪除,拋出異常
      • 如果在重試策略允許的範圍內重新嘗試獲取鎖,然後重新生成新的臨時有序節點
  • 釋放鎖邏輯
    • newLockCount記錄了鎖重入次數,初始值爲1,當 newLockCount=0,鎖釋放
    • 然後從映射表中移除對應線程的鎖信息

zookeeper的leader選舉原理

  • 在zk的分佈式集羣中,有着三種角色,leader、follower、observer

  • curator提供了兩種選舉 recipe -> Leader Latch 和 Leader Selector

    • Leader Latch:參與選舉的所有節點都會創建一個順序節點最小的節點會被設置爲master節點

      沒有搶佔到Leader節點的節點都會對前一個節點的刪除事件進行監聽,

      當其刪除或master節點手動調用close()或master節點掛掉,後續節點會搶佔master

    • Leader Selector:和Leader Latch的差別在於leader節點在釋放領導權之後會繼續參與競爭

zookeeper數據同步

  • zookeeper通過三種不同的集羣角色(leader、follower、observer),組成整個高性能集羣
  • zk集羣中,客戶端會隨機連接到zookeeper集羣中的一個節點,如果是讀請求,直接從當前節點讀取數據
  • 如果是寫請求,請求將會被轉發給leader提交事務
  • 然後leader會廣播事務,只要有超過半數節點寫入成功,那麼寫請求就會被提交

在這裏插入圖片描述

流程詳解

  • 1.客戶端發出寫事務請求到zk集羣中的follower節點
  • 2.follower節點轉發事務請求到leader節點進行處理
  • 3.leader節點發起proposal事務廣播
  • 4.follower節點發回ack消息確認
  • 5.commit提交事務
  • 6.response返回客戶端

zookeeper——ZAB協議

  • ZAB(zookeeper Atomic Broadcast)協議是爲分佈式協調服務zookeeper專門設計的
  • 支持 崩潰恢復 的原子廣播協議
  • 在zk中,主要依賴ZAB協議來實現分佈式數據的一致性
  • 基於該協議,zk實現了主備模式的系統架構中集羣中各個副本之間的數據一致性
  • zab協議包含兩種基本模式:崩潰恢復和原子廣播

崩潰恢復和原子廣播

  • 當zk集羣啓動後,leader節點出現網絡中斷、崩潰等情況時
  • 基於zab協議zk會自動進入恢復模式並選舉出新的leader
  • 當leader選舉出來之後,並且集羣中有過半機器和leader節點的數據完成同步後
  • zab協議就會退出恢復模式,然後整個集羣進入消息廣播模式
  • 此時當一臺同樣遵守ZAB協議的服務器啓動後加入到集羣中時,如果集羣中存在leader服務器進行消息廣播
  • 新加入的服務器會自動進入數據恢復模式,進行數據同步,然後一起參與到消息廣播流程中去
  • 注意點:如果一個事務proposal在一臺機器上處理成功,那麼該事務應該在所有機器上都被處理成功
  • 即,已經被處理的消息不能丟棄。被丟棄的消息不能再次出現
  • 判斷依據是:消息是否被commit成功

消息廣播實現原理

  • 消息廣播的過程實際上是一個簡化版本的二階段提交過程,類似於分佈式事務的2pc協議
  • 流程分析
  • leader接收到事務請求後,會生成一個全局唯一的64位的自增id,即zxid通過其大小保證消息因果順序
  • leader爲每一個follower節點準備了一個FIFO的隊列(通過TCP協議實現,以實現全局有序的的特點)
  • 將帶有zxid的消息作爲一個提案(Proposal)分發給所有的follower
  • follower節點收到proposal後,會先把proposal寫到磁盤,寫入成功後返回給leader一個ack
  • 當leader接收到合法數量即超過半數節點的ACK之後,leader就會向這些follower發送commit命令,同時在本地執行目標消息
  • follower節點收到commit命令之後,會提交該消息(leader的投票過程不需要Observer的ack,僅做數據同步即可)

數據一致性保證

  • leader選舉算法能夠保證重新選舉出來的leader擁有集羣中所有機器最高編號zxid——zxid最大的事務proposal
  • 那麼就可以保證重新選舉的leader一定具有已經提交的proposal
  • 這是因爲所有提案被commit之前必須有超過半數的follower節點發出ack確認,即擁有消息提案
  • 因此只要有合法數量的節點正常工作,就必然有一個節點保存了所有被commit消息的proposal狀態

關於zxid

  • zxid構成以及作用

  • zxid是一個64位的數據,高32位時epoch編號,每經過一次leader選舉產生一個新的leader,新的leader會將epoch號加1

  • 低32位是消息計數器,每收到一條消息,該值+1,新的leader選舉後,消息被重置爲0

    • 基於此設計,保證了掛掉的leader重新啓動之後不會被選舉爲leader,它的zxid小於當前新的leader
    • 當舊的leader作爲follower接入之後,新的leader會讓其清除所有違背commit的proposal的epoch號
  • zxid說明

  • 爲了保證事務的順序一致性,zk採用了遞增的事務id號(zxid)來標識事務

  • 所有的提案proposal都會被加上zxid

zookeeper的一致性

  • zk集羣內部的數據副本同步是基於半提交策略的,意味着它是最終一致性,而不滿足強一致性要求
  • zookeeper基於zxid以及阻塞隊列的方式實現請求的順序一致性
    • 如果一個client請求連接到一臺follower節點,讀取到最新數據,由於網絡原因連接到其它節點
    • 若是重新連接到的follower節點還沒有完成數據同步,將會讀到舊的數據
    • 針對這種情況,client會記錄自己已經讀取到的最大的zxid,如果重新連接到server
    • 發現client持有的zxid比server的zxid要大,則連接會失敗

zookeeper——leader選舉原理

  • leader選舉存在於兩個階段中,一個是服務器啓動時的leader選舉
  • 其二是運行過程中leader節點宕機導致的leader選舉

重要參數

  • 服務器ID:myid:myid是服務器的編號,編號越大在選擇算法中的權重越大
  • zxid:事務id:值越大說明數據越新,在選舉算法中權重越大
  • 邏輯時鐘也叫投票次數:epoch – logicalclock
    • 在同一輪投票過程中epoch-logicalclock的值是相同的,每次投票結束,該數值會增加
    • 然後接收其它服務器返回的投票信息中的數值進行比較,根據不同的值做出判斷
  • 選舉狀態
LOOKING 競選狀態
FOLLOWING 隨從狀態,同步 leader 狀態,參與投票
OBSERVING 觀察狀態,同步 leader 狀態,不參與投票
LEADING 領導者狀態

服務啓動時的leader選舉

  • 每個節點啓動時的狀態都是LOOKING
  • 在集羣初始化階段,當有一臺服務器server1啓動的時候,無法單獨進行和完成leader選舉
  • 當有多臺服務器啓動的時候,服務器之間此時可以進行相互通信,開始進入leader選舉
|-每個server發起一次投票,首次投票,會將自身作爲Leader服務器來進行投票
|-票據信息中會包含服務器的myid、zxid和epoch,使用vote進行封裝(myid,zxid,epoch)
|-然後各自的投票信息發送給其它服務器
|-收到來自其它服務器的投票信息後,會首先判斷投票是否有效(epoch),是否來自looking狀態的服務器
|-然後進行pk,
——比較epoch
——檢查zxid,zxid較大的服務器優先作爲Leader,zxid相同比較myid
——myid較大的服務器作爲leader服務器
|-投票結束後會統計投票信息,當有過半服務器接受了相同的投票信息,即選出了leader節點
|-確定leader之後,每個服務器會更新自己的狀態,follower->followering   leader->leadering

運行過中的leader選舉

  • 當集羣中的 leader 服務器出現宕機或者不可用的情況時,進入新一輪leader選舉
  • leader宕機之後,剩餘follower服務器,將自己的狀態改變爲looking,然後開始進入leader選舉過程
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章