您好,登錄后才能下訂單哦!
本篇內容主要講解“ZooKeeper集群的數據同步過程是什么”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“ZooKeeper集群的數據同步過程是什么”吧!
經歷了選舉之后,我們的馬果果榮耀當選當前辦事處集群的 Leader,所以現在假設各個辦事處的關系圖是這樣:
我們現在就來說說馬小云和馬小騰是如何同馬果果進行數據同步的。
結束了累人的選舉后,馬小云和馬小騰以微弱的優勢輸掉了競爭,只能委屈成為 Follower。整理完各自的情緒后,他們要做的第一件事情就是通過話務員上報自己的信息給馬果果,使用了專門的暗號 FOLLOWERINFO, 數據主要有自己的 epoch 和 myid:
然后是馬果果這邊,他收到 FOLLOWERINFO 之后也會進行統計,直到達到半數以上后,綜合各個 Follower 給的信息會計算出新的 epoch,然后將這個新的 epoch 隨著暗號 LEADERINFO 回發給其他 Follower
然后再回到馬小云和馬小騰這邊,收到 LEADERINFO 之后將新的 epoch 記錄下來,然后回復給馬果果一個 ACKEPOCH 暗號并帶上自己這邊的最大 zxid,表示剛剛的 LEADERINFO 收到了
然后馬果果這邊也會等待半數以上的 ACKEPOCH 的通知,收到之后會根據各個 Follower 的信息給出不同的同步策略。關于不同的同步策略,這里我先入為主的給大家介紹一下:
DIFF,如果 Follower 的記錄和 Leader 的記錄相差的不多,使用增量同步的方式將一個一個寫請求發送給 Follower
TRUNC,這個情況的出現代表 Follower 的 zxid 是領先于當前的 Leader 的(可能是以前的 Leader),需要 Follower 自行把多余的部分給截斷,降級到和 Leader 一致
SNAP,如果 Follower 的記錄和當前 Leader 相差太多,Leader 直接將自己的整個內存數據發送給 Follower
至于采用哪一種策略,是如何進行判斷的,接下來一一進行講解。
每一個 ZK 節點在收到寫請求后,會維護一個寫請求隊列(默認是 500 大小,通過 zookeeper.commitLogCount
配置),將寫請求記錄在其中,這個隊列中的最早進入的寫請求當時的 zxid 就是 minZxid(以下簡稱 min),最后一個進入的寫請求的 zxid 就是 maxZxid(以下簡稱 max),達到上限后,會移除最早進入的寫請求,知道了這兩個值之后,我們來看看 DIFF 是怎么判斷的。
一種情況就是如果當 Follower 通過 ACKEPOCH 上報的 zxid 是在 min 和 max 之間的話,就采用 DIFF 策略進行數據同步。
我們的例子中 Leader 的 zxid 是 99,說明這個存儲 500 個寫請求的隊列根本沒有放滿,所以 min 是 1 max 是 99,很顯然 77 以及 88 是在這個區間內的,那馬果果就會為另外兩位 Follower 找到他們各自所需要的區間,先發送一個 DIFF 給 Follower,然后將一條條的寫請求包裝成 PROPOSAL 和 COMMIT 的順序發給他們
另一種情況是如果 Follower 的 zxid 不在 min 和 max 的區間內時,但當 zookeeper.snapshotSizeFactor
配置大于 0 的話(默認是 0.33),會嘗試使用 log 進行 DIFF,但是需要同步的 log 文件的總大小不能超過當前最新的 snapshot 文件大小的三分之一(以默認 0.33 為例)的話,才可以通過讀取 log 文件中的寫請求記錄進行 DIFF 同步。同步的方法也和上面一樣,先發送一個 DIFF 給 Follower 然后從 log 文件中找到該 Follower 的區間,再一條條的發送 PROPOSAL 和 COMMIT。
而 Follower 收到 PROPOSAL 的暗號消息后,就會像處理客戶端請求那樣去一條條處理,慢慢就會將數據恢復成和 Leader 是一致的。
假設現在三個辦事處是這樣的
馬果果的寫請求隊列在默認配置下記錄了 277 至 777 的寫請求,又假設現在的場景不滿足上面 1.1.2 的情況,馬果果就知道當前需要通過 SNAP 的情況進行同步了。
馬果果會先發送一個 SNAP 的請求給馬小云和馬小騰讓他們準備起來
緊接著就會當前內存中的數據整個序列化(和 snapshot 文件是一樣的)然后一起發送給馬小云和馬小騰。
而馬小云和馬小騰收到馬果果發來的整個 snapshot 之后會先清空自己當前的數據庫的所有信息,接著直接將收到的 snapshot 反序列化就完成了整個內存數據的恢復。
最后一種策略的場景假設是這樣:
假設馬小騰是上一個 Leader,但是經歷了停電以后恢復重新以 Follower 的身份加入集群,但是他的 zxid 要比 max 還大,這個時候馬果果就會給馬小騰發送 TRUNC,(至于圖中為什么馬小云不舉例為 TRUNC,因為如果馬小云的 zxid 也比馬果果要大的話,馬果果在當前場景下就不可能當選 Leader 了)。
馬果果就會發送 TRUNC 給馬小騰(這里忽略馬小云)
假設馬小騰的本地 log 文件目錄下是這樣的:
/tmp └── zookeeper └── log └── version-2 └── log.0 └── log.500 └── log.800
而馬小騰收到 TRUNC 之后,會找到本地 log 文件中所有大于 777 的 log 文件刪除,即這里的 log.800
,然后會在 log.500
這個文件找到 777 這個 zxid 記錄并且把當前文件的讀寫指針修改至 777 的位置,之后針對該文件的讀寫操作就會從 777 開始,這樣就會把之后的那些記錄給覆蓋了。
而馬果果這邊當判斷完同步策略并發送給另外兩馬之后,便會發送一個 NEWLEADER 的信息給他們
而馬小云和馬小騰在收到 NEWLEADER 之后,若之前是通過 SNAP 方式同步數據的話,這里會強制快照一份新的 snapshot 文件在自己這里。然后會回復給馬果果一個 ACK 的消息,告訴他自己的同步數據已經完成了
然后馬果果同樣會等待半數一樣的 ACK 接收完成后,再發送一個 UPTODATE 給其他兩馬,告訴他們現在辦事處數據已經都一致了,可以開始對外提供服務了
然后馬小云和馬果果收到 UPTODATE 之后會再回復一個 ACK 給馬果果,但是這次馬果果收到這次的 ACK 之后不會做處理,所以在 UPTODATE 之后,各個辦事處就已經算可以正式對外提供服務了。
上面說了這么多,但是馬小云和馬小騰都是 Follower,如果是 Observer 呢?怎么用上面的步驟同步呢?
區別就在第一步,Follower 發送的是 FOLLOWERINFO,而 Observer 發送的是 OBSERVERINFO 除此之外沒有任何區別,和 Follower 是一樣的步驟進行數據同步。
現在把其中的一些細節再用猿話說明一下,三種不同的數據同步策略,Leader 在發送 Follower 的時候采用的具體方法是不太相同的
如果采用的是 DIFF 或者 TRUNC 的同步方法的話,Leader 其實不是在找到有差異數據的時候發送過去的,而是按照順序先放入一個隊列,最后再統一啟動一個線程去一個個發送的
DIFF :
TRUNC:
但是以 SNAP 方式同步的話就不會放入該隊列,無論是 SNAP 消息還是之后整個序列化后的內存快照 snapshot 都會直接通過服務端間的 socket 直接寫入。
讓我們把三種策略消息交互的全過程再看一遍,這里就以馬小云舉例了
可以看到首尾是一樣的,就是中間的請求根據不同的策略會有不同的請求發送。差不多到這里關于 Follower 或 Observer 是如何同 Leader 同步消息,整體的邏輯都介紹完了。
Follower 和 Observer 同步數據的方式一共有三種:DIFF、SNAP、TRUNC
DIFF 需要 Follower 或 Observer 和 Leader 的數據相差在 min 和 max 范圍內,或者配置了允許從 log 文件中恢復
TRUNC 是當 Follower 或 Observer 的 zxid 比 Leader 還要大的時候,該節點需要主動刪除多余 zxid 相關的數據,降級至 Leader 一致
SNAP 作為最后的數據同步手段,由 Leader 直接將內存數據整個序列化完并發送給 Follower 或 Observer,以達到恢復數據的目的
我看了下文章的字數還行,決定加一點料,開一個小篇講一下 ACL,這個我拖了很久沒解釋的坑。
先帶大家重拾記憶,之前創建節點代碼片段中的 ZooDefs.Ids.OPEN_ACL_UNSAFE
就是 ACL 的參數
client.create("/更新視頻/跳舞/20201101", "這是Data,既可以記錄一些業務數據也可以隨便寫".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
首先如果配置了 zookeeper.skipACL
該參數為 yes
(注意大小寫),表示當前節點放棄 ACL 校驗,默認是 no
那這個 ACL 是怎么規定的,有哪些權限,又是怎么在服務端體現的呢?首先 ACL 整體分為 Permission 和 Scheme 兩部分,Permission 是針對操作的權限,而 Scheme 是指定使用哪一種鑒權模式,下面我們一起來了解下。
首先 ZK 將權限分為 5 種:
READ(以下簡稱 R),獲取節點數據或者獲取子節點列表
WRITE(以下簡稱 W),設置節點數據
CREATE(以下簡稱 C),創建節點
DELETE(以下簡稱 D),刪除節點
ADMIN(以下簡稱 A),設置節點的 ACL 權限
然后該 5 種權限在代碼層面就是簡單的 int 數據,而判斷是否有權限只需要用 & 操作即可,和目標權限 & 完結果只要不等于 0 就說明擁有該權限,細節如下:
int binary R 1 00001 W 2 00010 C 4 00100 D 8 01000 A 16 10000
假設現在的客戶端權限為 RWC,對應的數值就是各個權限相加 1 + 2 + 4 = 7
int binary RWC 7 00111
對任意有 R、W、C 權限需求的節點,求 & 的結果都不為 0,所以就能判斷該客戶端是擁有 RWC 這 3 個權限的。
但是如果當該客戶端對目標節點進行刪除時,做 & 判斷權限的話,可以得到結果為 0,表示該客戶端不具備刪除的權限,就會返回給客戶端權限錯誤
int binary RWC 7 00111 D 8 & 01000 ------------------ 結果 0 00000
Scheme 有 4 種,分別是 ip
、world
、digest
、super
,但是其實就是兩大類,一種是針對 IP 地址的 ip,另一種是使用類似“用戶名:密碼”的 world
、digest
、super
。其實整個 ACL 是分三個部分的,scheme:id:perms
,id 的取值取決于 scheme 的種類,這里是 ip 所以 id 的取值就是具體的 IP 地址,而 perms 則是我上一小節介紹的 RWCDA。
這三部分的前兩部分 scheme:id
相當于告訴服務端 “我是誰?”,而最后的部分 perms
則是代表了 “我能做什么?”,這兩個問題,任意一個問題出錯都會導致服務端拋出 NoAuthException
的異常,告訴客戶端權限不夠。
我們先來直接看一段代碼,其中的 IP 10.11.12.13
我是隨便寫的
ZooKeeper client = new ZooKeeper("127.0.0.1:2181", 3000, null); List<acl> aclList = new ArrayList<>(); aclList.add(new ACL(ZooDefs.Perms.ALL, new Id("ip", "10.11.12.13"))); String path = client.create("/abc", "test".getBytes(), aclList, CreateMode.PERSISTENT); System.out.println(path); // 輸出 /abc client.close();
可以看到 /abc
是可以被正確輸出的,而且通過查看 /
的子節點列表是可以看到 /abc
節點的
ZooKeeper client = new ZooKeeper("127.0.0.1:2181", 3000, null); List<string> children = client.getChildren("/", false); System.out.println(children); // 輸出 [abc, zookeeper] client.close();
但是現在如果去訪問該節點的數據的話就會得到報錯
ZooKeeper client = new ZooKeeper("127.0.0.1:2181", 3000, null); byte[] data = client.getData("/abc", false, null); System.out.println(new String(data)); client.close();
Exception in thread "main" org.apache.zookeeper.KeeperException$NoAuthException: KeeperErrorCode = NoAuth for /abc
讀者可以試試把上面的 IP 改成 127.0.0.1
重新創建節點,之后就能正常訪問了,一般生產環境中 IP 模式用的不多(也可能是我用的不多),如果要用 IP 控制訪問的話,通過防火墻白名單之類的手段即可,這個層面我認為不需要 ZK 去管。
這個模式應該是最常用的(手動狗頭)
我們還是來看一段代碼
ZooKeeper client = new ZooKeeper("127.0.0.1:2181", 3000, null); List<acl> aclList = new ArrayList<>(); aclList.add(new ACL(ZooDefs.Perms.READ, new Id("world", "anyone"))); // 區別是這行 String path = client.create("/abc", "test".getBytes(), aclList, CreateMode.PERSISTENT); System.out.println(path); // 輸出 /abc client.close();
我把 scheme 改成了 World 模式,而 World 模式的 id 取值就是固定的 anyone 不能用其他值,而且我還設置了 perms 為 R,所以這個節點只能讀數據,但不能做其他操作,如果使用 setData
對其進行數據修改的話也會得到權限的錯誤
ZooKeeper client = new ZooKeeper("127.0.0.1:2181", 3000, null); Stat stat = client.setData("/abc", "newData".getBytes(), -1); // NoAuth for /abc
現在再回頭看之前的 ZooDefs.Ids.OPEN_ACL_UNSAFE
,其實就是 ZK 提供的常用的靜態常量,代表不校驗權限
Id ANYONE_ID_UNSAFE = new Id("world", "anyone"); ArrayList<acl> OPEN_ACL_UNSAFE = new ArrayList<acl>(Collections.singletonList(new ACL(Perms.ALL, ANYONE_ID_UNSAFE)));
這個就是我們熟悉的用戶名密碼了,還是先上代碼
ZooKeeper client = new ZooKeeper("127.0.0.1:2181", 3000, null); List<acl> aclList = new ArrayList<>(); aclList.add(new ACL(ZooDefs.Perms.ALL, new Id("digest", DigestAuthenticationProvider.generateDigest("laoxun:kaixin")))); // 1 String path = client.create("/abc", "test".getBytes(), aclList, CreateMode.PERSISTENT); System.out.println(path); client.close();
這個寫法中必須要注意的是 1 處的 username:password
的字符串必須通過 DigestAuthenticationProvider.generateDigest
的方法包裝一下,用這個方法會對傳入的字符串進行編碼。
包裝完后 laoxun:kaixin
其實變成了 laoxun:/xQjqfEf7WHKtjj2csJh2/aEee8=
,這個過程如下:
laoxun:kaixin
對整個字符串先進行 SHA1 加密
對加密后的結果進行 Base64 編碼
將用戶名和編碼后的結果拼接
上面的代碼還有一種寫法如下,使用 addAuthInfo
在客戶端上下文中添加權限信息
ZooKeeper client = new ZooKeeper("127.0.0.1:2181", 3000, null); client.addAuthInfo("digest", "laoxun:kaixin".getBytes()); // 1. List<acl> aclList = new ArrayList<>(); aclList.add(new ACL(ZooDefs.Perms.ALL, new Id("auth", ""))); // 2. 這里的 Id 是固定寫法 String path = client.create("/abc", "test".getBytes(), aclList, CreateMode.PERSISTENT); System.out.println(path); client.close();
這里有兩個改動,在 1 處使用 addAuthInfo
的方法可以在當前客戶端的會話中添加 auth 信息,Digest 的 id 取值為 username:password
直接用明文即可,無論是 username 還是 password 都是自定義的。
然后是查詢代碼
ZooKeeper client = new ZooKeeper("127.0.0.1:2181", 3000, null); client.addAuthInfo("digest", "laoxun:kaixin".getBytes()); // 這行如果注釋的話就會報錯 byte[] data = client.getData("/abc", false, null); System.out.println(new String(data)); // test
不管創建的時候是何種寫法,查詢的時候都要使用 addAuthInfo
在會話中添加權限信息,才能對該節點進行查詢
聽名字就知道這個模式是管理員的模式了,因為之前創建的那些節點,如果設置了用戶名密碼,其他客戶端是無法訪問的,如果該客戶端自己退出了,這些節點就無法去操作了,所以需要管理員這一個角色來對其進行降維打擊。
首先 Super 模式是要開啟的,我這里假設管理員的用戶名為 HelloZooKeeper,密碼為 niubi,經過編碼后就是 HelloZooKeeper:PT8Sb6Exg9YyPCS7fYraLCsqzR8=
, 然后需要在服務端啟動的環境中指定 zookeeper.DigestAuthenticationProvider.superDigest
配置,參數就是 HelloZooKeeper:PT8Sb6Exg9YyPCS7fYraLCsqzR8=
即可。
創建節點假設還是以 laoxun:kaixin
的模式,然后通過管理員的密碼也能進行正常的訪問
ZooKeeper client = new ZooKeeper("127.0.0.1:2181", 3000, null); client.addAuthInfo("digest", "HelloZooKeeper:niubi".getBytes()); // 1. byte[] data = client.getData("/abc", false, null); System.out.println(new String(data)); // test client.close();
這里可以看到 1 處的 Super 模式本質上還是 Digest,指定的 scheme 為 digest,然后之后的 id 取值采用的是明文,而非編碼后的格式,切記!
我這里列出大部分服務端提供的操作對應的 Permission 權限:
操作 | 所需權限 | 描述 |
---|---|---|
create | 父節點的 CREATE | 創建節點 |
create2 | 父節點的 CREATE | 創建節點,同時返回節點數據 |
createContainer | 父節點的 CREATE | 創建容器節點 |
createTTL | 父節點的 CREATE | 創建帶超時時間的節點 |
delete | 父節點的 DELETE | 刪除節點 |
setData | 當前節點的 WRITE | 設置節點數據 |
setACL | 當前節點的 ADMIN | 設置節點的權限信息 |
reconfig | 當前節點的 WRITE | 重新設置一些配置(之后有機會介紹) |
getData | 當前節點的 READ | 查詢節點數據 |
getChildren | 當前節點的 READ | 獲取子節點列表 |
getChildren2 | 當前節點的 READ | 獲取子節點列表 |
getAllChildrenNumber | 當前節點的 READ | 獲取所有子節點(包含孫子節點)數量 |
getACL | 當前節點的 ADMIN 或 READ | 獲取節點的權限信息 |
可以看到刪除和創建節點看的是父節點的權限,只有讀寫才是看的自己本身的權限。另外如果表格中沒有出現的操作可以認為不需要 ACL 權限校驗,其他要么是只需要客戶端是一個合法的 session 或者本身是一些比較特殊的功能,例如:createSession、closeSession 等。至于關于 session 的更多內容,留到下一篇再講吧~哈哈
我們剛剛花了一點篇幅介紹了 ACL 是什么,怎么用?現在深入了解下 ACL 在 ZK 的服務端底層是怎么去實現的吧~為了節約篇幅,這次就直接進入猿話講解了。
首先祭出之前的一張圖,喚醒下大家的記憶
圖中權限部分(藍色字體)之前的文章直接省略跳過了,沒有進行解釋,今天我們就好好講講這個權限字段。
從圖上也能看到權限這個字段是直接以數字(long 類型,64 位的整型數字)的方式保存在服務端的節點中的,而 -1 是一個特殊的值代表不進行權限的校驗對應的就是之前的 OPEN_ACL_UNSAFE
常量。
而 ACL 權限無論是創建節點時提供的(ACL 參數是一個 List),還是通過 addAuth
方法提供的(這個方法可以被調用多次),這兩種設計都表示一個客戶端是可以擁有多種權限的,比如:多個用戶名密碼,多個 IP 地址等等。
ACL 我之前講過是由 3 個部分組成的,即 scheme:id:perms
為了簡潔的表示我會在之后使用該形式去表示一個 ACL。
服務端會使用兩個哈希表把目前接收到的 ACL 列表和其對應的數字雙向的關系保存起來,類似這樣(圖中的 ACL 取值是我隨意編造的):
ZK 服務端會維護一個從 1 開始的數字,收到一個新的 ACL 會同時放入這兩個哈希表(源碼中對應的就是兩個 Map
,一個是 Map<list<acl>, Long>
,一個是 Map<long, list<acl>>
),除了這兩個哈希表以外,ZK 服務端還為每一個客戶端都維護了一個會話中的權限信息,該權限信息就是客戶端通過 addAuth
添加的,但是這個客戶端的權限信息只保存了 scheme:id
部分,所以結合以下三個信息就可以對客戶端的本次操作進行權限校驗了:
兩個哈希表表示的節點的信息 scheme:id:perms
,可以有多個
客戶端會話上下文中的權限信息僅 id:perms
,可以有多個
本次操作對應的權限要求,即 3.3 表格中列出的所需權限
校驗的流程如下:
這里額外提一下,校驗器是可以自定義的,用戶可以自定義自己的 scheme 以及自己的校驗邏輯,需要在服務端的環境變量中配置以 zookeeper.authProvider.
開頭的配置,對應的值則對應一個 class 類全路徑,這個類必須實現 org.apache.zookeeper.server.auth.AuthenticationProvider
接口,而且這個類必須能被 ZK 服務端加載到,這樣就可以解析自定義的 scheme 控制整個校驗邏輯了,這個功能比較高級,我也沒用過,大家就當補充知識了解下~
到此,相信大家對“ZooKeeper集群的數據同步過程是什么”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。