Zookeeper學習筆記

JavaDog發表於2019-03-24

簡介

Zookeeper 是一個分散式的服務框架,主要用來解決分散式叢集中應用系統的協調和一致性問題,它能提供基於類似於檔案系統的目錄節點樹方式的資料儲存,但是 Zookeeper 並不是用來專門儲存資料的,它的作用主要是用來維護和監控你儲存的資料的狀態變化。如:統一命名服務、狀態同步服務、叢集管理、分散式應用配置管理等。

它能夠為分散式應用提供高效能和可靠地協調服務,使用ZooKeeper可以大大簡化分散式協調服務的實現,為開發分散式應用極大地降低了成本。

架構和原理

整體架構

1441798625403

Zookeeper叢集是由一組Server節點組成,這一組Server節點中存在一個角色為Leader的節點,其他節點都為Follower。客戶端可以和叢集中的任一Server建立連線,當讀請求時,所有Server都可以直接返回結果;當請求為資料變更請求時,Follower會將請求轉發給Leader節點,Leader節點接收到資料變更請求後,首先會將變更寫入本地磁碟,以作恢復,當持久化完畢後才會將變更寫入記憶體,並將變更後的資料同步到各個Follower。
Zookeeper中一共有以下角色:
(1) Leader,負責進行投票的發起和決議,更新狀態和資料
(2) Follower,用於接收客戶端請求並向客戶端返回結果,在選擇Leader時會進行投票
(3) Observer,一種功能和Follower相同,但是它不參與投票過程。它主要是為了擴充套件系統,提高讀取速度
(4) Client,客戶端用來發起請求,嚴格說不屬於Zookeeper叢集

核心資料結構

Zookeeper的核心資料結構是如下圖的樹形結構:

1441799725106

樹中的每個節點稱為ZNode,簡單的說它包含一個路徑和與之相關的後設資料,以及它的孩子節點。比較類似樹形的檔案系統,但Zookeeper的資料儲存在記憶體中,所以擁有分散式同步服務的高吞吐和低延遲的特點。Zookeeper主要通過對樹形資料結構ZNode節點的監聽和變更來完成不同分散式環境中各個程式的協調和同步。
(1)節點ZNode儲存同步、協調相關的資料,資料量比較小,比如狀態資訊、配置內容、位置資訊等。
(2)ZNode中存有狀態資訊,包括版本號、ACL變更、時間戳等, 每次變更版本號都會遞增。這樣一方面可以基於版本號檢索狀態;另一方面可以實現分散式的樂觀鎖。
(3)ZNode都有ACL,可以限制ZNode的訪問許可權
(4)ZNode上資料的讀寫都是原子的
(5)客戶端可以在ZNode上設定Watcher監聽,一但該ZNode有資料變更,就會通知客戶端,觸發回撥方法。【這個地方需要注意,Watcher都是一次性,觸發一次後就失效,持續監聽需要重新註冊】
(6)客戶端和Zookeeper連線建立後就是一次session,Zookeeper支援臨時節點,它和一次session關聯,一但session關閉,節點就被刪除。【可以用臨時節點來實現連通性的檢測】

工作機制

Zookeeper的核心是Zab(Zookeeper Atomic Broadcast)協議。Zab協議有兩種模式,它們分別是恢復模式和廣播模式。
恢復模式:
當Zookeeper叢集啟動或Leader崩潰時,就進入到該模式。該模式需要選舉出新的Leader,選舉演算法基於paxos或fastpaxos【先mark一下,具體需要去看一下論文】:
(1)每個Server啟動以後都會詢問其它的Server投票給誰
(2)對於其他Server的詢問,Server每次根據自己的狀態回覆自己推薦Leader的id和該Server最後處理事務的zxid【zookeeper中的每次變更(事務)都會被賦予一個順序遞增的zxid,zxid越大說明變更越新】【Server剛啟動時都會選擇自己】
(3)收到Server的回覆後,就計算出zxid最大的那個Server,將該Server的資訊設定成下次要投票的Server【如果zxid同樣大,就選擇Server id大的】
(4)計算獲得票數最多的Server,如果該Server的得票數超過半數,則該Server當選Leader,否則繼續投票直到Leader選舉出來。
假設Zookeeper叢集有5臺機器,ServerId分別為1,2,3,4,5,啟動順序按照1,2,3,4,5依次啟動:
a) Server 1啟動,此時它選擇自己為leader,同時向外發出投票報文,但收不到任何回覆,選票不過半,啟動機器數不超過叢集的一半【不能正常工作】
b) Server2啟動,此時由於沒有歷史資料,Server1和Server2會選擇ServerId較大的2位leader,但選票不超過一半,啟動的機器數不超過叢集的一半【不能正常工作】
c) Server3啟動,此時情況與b)類似,Server3會被選為Leader,但不同的是此時得票過半,並且啟動的機器數超過叢集的一半,所以叢集可以正常工作,Server3被選為Leader
d) Server4啟動,由於此時Server3已經被選為Leader,所以Server4只能作為Follower
e) Server5啟動,與d)同理,Server5也只能作為Follower
Leader崩潰恢復時,類似不同的是,有了資料主要依據zxid的大小進行選舉。

廣播模式:
Leader選舉完畢後,Leader需要與Follower進行資料同步:
a) leader會開始等待server連線
b) Follower連線leader,將最大的zxid傳送給leader
c) Leader根據follower的zxid確定同步點
d) 完成同步後通知follower 已經成為uptodate狀態
e) Follower收到uptodate訊息後,就可以重新接受client的請求進行服務了。

典型應用場景

(1) 分散式Barrier
1441801130557

Java中可以使用CyclicBarrier這個類實現多個執行緒的Barrier機制。在分散式環境中可以使用Zookeeper實現這一機制:可以在Zookeeper上建立一個ZNode作為Barrier的實體,分散式環境中需要被Barrier通過的各個任務呼叫exists()方法檢測ZNode是否存在【並註冊Watcher】,當需要開啟Barrier時,就刪除掉這個ZNode,Zookeeper會通知到各個任務開始執行。

(2) 分式佇列
可以使用一個ZNode來表示佇列,然後用它的子節點來表示佇列中的節點。Zookeeper的create方法有順序遞增的模式,會自動地在name後面加上一個遞增的數字來插入新元素。offer的時候使用create方法,take的時候按照子節點的順序刪除第一個即可。

(3) 分散式鎖
結合上面兩種機制,Zookeeper可以實現分散式鎖,比如建立一個ZNode節點表示鎖,當一個客戶端去拿鎖時,會在這個節點下建立一個自增的子節點,然後通過getChildren()方式來檢查自己建立的子節點是不是最靠前的,如果是則拿到鎖,否則就呼叫exist()來檢查第二靠前的子節點,並加上watch來監視。當拿到鎖的子節點執行完後歸還鎖,歸還鎖僅僅需要刪除自己建立的子節點,這時watch機制會通知到所有沒有拿到鎖的客戶端,這些客戶端就會重複上述的過程來檢查是否能夠拿到鎖。

(4) 配置管理

1441801922154

如圖所示,Zookeeper還可以用於統一配置管理,通過建立一個ZNode存放統一的配置資訊,然後每一個客戶端都去Watcher這個節點,一但節點的資料發生變化也就是配置發生變化,Zookeeper會通過Watcher通知到各個客戶端,客戶端可以做出相應變更。

(5) Storm&JStorm
使用ZooKeeper來協調整個計算叢集,Storm計算叢集存在Nimbus和Supervisor兩類節點。Nimbus負責分配任務(Topology),將任務資訊寫入ZooKeeper儲存,然後Supervisor從ZooKeeper中讀取任務資訊。另外,Nimbus也監控叢集中的計算任務節點,Supervisor也會傳送心跳資訊(包括狀態資訊)到ZooKeeper中,使得Nimbus可以實現狀態的監控,任何計算節點出現故障,只要重新啟動之後,繼續從ZooKeeper中獲取資料即可繼續執行計算任務。

(6) HBase
HBase基於Master-Slave模式架構,可以有多個HMaster,HBase使用ZooKeeper實現了Master的選舉;Zookeeper還用於協調RegionServer節點,資料變更會通過ZooKeeper同步複製到其他節點;Hbase的客戶端查詢資料時也是先連線到Zookeeper,因為Zookeeper裡存放了ROOT表的位置資訊。

環境搭建

由於資源有限,只做了Zookeeper的偽分散式的叢集搭建,在一臺機器上啟動5個Zookeeper的程式來模擬叢集環境。

下載解壓

我下載的是zookeeper-3.4.6.tar.gz,解壓並複製出5份:

$tar xvzf zookeeper-3.4.6.tar.gz
....
複製程式碼
$ tree -L 1
|-- zookeeper1
|-- zookeeper2
|-- zookeeper3
|-- zookeeper4
`-- zookeeper5
複製程式碼

修改配置檔案

分別修改5個zookeeper的配置檔案,以zookeeper1和zookeeper2為例,分別將各個zookeeper的conf目錄下的zoo_sample.cfg改名為zoo.cfg:

zookeeper1/conf/zoo.cfg:

# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial 
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between 
# sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just 
# example sakes.
dataDir=/home/xxxx/programs/zoo/zookeeper1/data
# the port at which the clients will connect
clientPort=2181
server.1=127.0.0.1:2888:3888
server.2=127.0.0.1:2889:3889
server.3=127.0.0.1:2890:3890
server.4=127.0.0.1:2891:3891
server.5=127.0.0.1:2892:3892
複製程式碼

zookeeper2/conf/zoo.cfg:

# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial 
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between 
# sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just 
# example sakes.
dataDir=/home/xxxx/programs/zoo/zookeeper2/data
# the port at which the clients will connect
clientPort=2182
server.1=127.0.0.1:2888:3888
server.2=127.0.0.1:2889:3889
server.3=127.0.0.1:2890:3890
server.4=127.0.0.1:2891:3891
server.5=127.0.0.1:2892:3892
複製程式碼

(1) dataDir 設定為各自zookeeper的資料存放目錄
(2) 因為在一臺機器上,所以clientPort要設定為不同的,比如2181—2185對應5臺機器
(3) 同樣的由於在一臺機器上,每臺機器leader_listen_port和quorum_port也要不同,配置格式,leader_listen_port是該伺服器一旦成為leader之後需要監聽的埠,用於接收來自follower的請求;quorum_port是叢集中的每一個伺服器在最開始選舉leader時監聽的埠,用於伺服器互相之間通訊選舉leader:

server.serverid=serverhost:leader_listent_port:quorum_port
複製程式碼

配置serverid

為每臺機器配置serverid,以zookeeper1為例:

$ echo "1" > /home/xxxx/programs/zoo/zookeeper1/data/myid
複製程式碼

這個地方需要注意:
(1) myid檔案必須放在dataDir配置目錄下,否則會出現以下錯誤:

java.lang.IllegalArgumentException: /home/xxxx/programs/zoo/zookeeper1/data/myid file is missing
複製程式碼

(2) myid檔案中的內容對應到配置檔案中相應的serverid,這裡就是1

啟動叢集

$ zookeeper1/bin/zkServer.sh start
$ zookeeper2/bin/zkServer.sh start
$ zookeeper3/bin/zkServer.sh start
$ zookeeper4/bin/zkServer.sh start
$ zookeeper5/bin/zkServer.sh start
複製程式碼

啟動成功後可以使用status檢視狀態:
(1) 如果只啟動了1個或2個zookeeper,檢視status會是以下資訊,因為zookeeper叢集需要超過一半機器可用時才能正常工作

$ zookeeper1/bin/zkServer.sh status
JMX enabled by default
Using config: /home/xxxx/programs/zoo/zookeeper1/bin/../conf/zoo.cfg
Error contacting service. It is probably not running.
複製程式碼

(2) 啟動成功,可以看到每臺機器的角色leader和follower

$ zookeeper3/bin/zkServer.sh status
JMX enabled by default
Using config: /home/xxxx/programs/zoo/zookeeper3/bin/../conf/zoo.cfg
Mode: leader
複製程式碼
$ zookeeper2/bin/zkServer.sh status
JMX enabled by default
Using config: /home/xxxx/programs/zoo/zookeeper2/bin/../conf/zoo.cfg
Mode: follower
複製程式碼

偽叢集搭建成功,關閉、重啟zookeeper命令:

$ zookeeper3/bin/zkServer.sh stop
$ zookeeper3/bin/zkServer.sh restart
複製程式碼

API使用demo

引入jar包

 <dependency>
      <groupId>org.apache.zookeeper</groupId>
       <artifactId>zookeeper</artifactId>
       <version>3.4.6</version>
 </dependency>
複製程式碼

呼叫API

public class ZookeeperTest {

    final public static String IP = "10.2.38.198";
    final public static String CLIENTPORT = "2181";
    final public static int CONNECTION_TIMEOUT = 2000;

    public static void main(String[] args) throws InterruptedException {
        ZooKeeper zk = null;
        try {
            //和zookeeper伺服器叢集建立連線,並設定Watcher
             zk = new ZooKeeper(IP + ":" + CLIENTPORT, CONNECTION_TIMEOUT, new Watcher() {

                @Override
                public void process(WatchedEvent watchedEvent) {

        System.out.println("事件" + watchedEvent.getType() + "發生");
                }
            });
            //建立永久節點
            zk.create("/testPath", "testData".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            //建立永久子節點
            zk.create("/testPath/testChildPath1", "testChildData1".getBytes(),
                    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            //獲取/testPath節點的資料
            System.out.println(new String(zk.getData("/testPath", true, null)));
            //獲取/testPath/testChildPath1節點資料
String(zk.getData("/testPath/testChildPath1", true, null), "utf-8"));
            //修改/testPath/testChildPath1節點資料
            zk.setData("/testPath/testChildPath1", "修改後testChildData1".getBytes("utf-8"), -1);
            //獲取/testPath/testChildPath1節點資料
            System.out.println(new String(zk.getData("/testPath/testChildPath1", true, null), "utf-8"));
            //建立永久子節點
            zk.create("/testPath/testChildPath2", "testChildDataTwo".getBytes(),
                    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            //獲取/testPath/testChildPath2節點資料
            System.out.println(new String(zk.getData("/testPath/testChildPath2", true, null)));
            // 刪除子目錄節點
            zk.delete("/testPath/testChildPath2", -1);
            zk.delete("/testPath/testChildPath1", -1);
            // 刪除父目錄節點
            zk.delete("/testPath", -1);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        } finally {
            if(zk != null) {
                //關閉連線
                zk.close();
            }
        }
    }
}
複製程式碼

(1) 可以看出zookeeper客戶端主要完成對zookeeper節點的操作,也就是依賴這些節點的操作完成對分散式系統的協調等工作。
(2) 在一些寫操作時,會要求傳version這一引數,這個引數的目的是一種樂觀鎖的機制,在資料庫、tair等中都有應用。
(3) zookeeper的Watcher功能是一次性的,在處理完事件後就會失效,所以如果需要繼續得到通知,需要重新進行註冊。zookeeper的原生API有時候用起來可能有些不便,可以使用 Apache Curator: "Guava is to Java what curator is to Zookeeper" + "A Zookeeper Keeper"【這個API目前還沒有研究,可以先Mark一下】。
(4) 至於一些寫操作可以觸發哪些事件以及其他具體的Zookeeper原生API的使用,可以查詢相關資料,這裡不再贅述。

參考資料:

zookeeper.apache.org/
curator.apache.org/
www.cnblogs.com/viviman/arc…
www.cnblogs.com/ggjucheng/p…
blog.fens.me/hadoop-zook…
shiyanjun.cn/archives/49…
blog.csdn.net/cutesource/…
shiyanjun.cn/archives/47…
sishuok.com/forum/blogP…
www.cnblogs.com/lpshou/arch…

Zookeeper學習筆記


相關文章