一、Zookeeper架構
雲計算越來越流行的今天,單一機器處理能力已經不能滿足我們的需求,不得不采用大量的服務集群。服務集群對外提供服務的過程中,有很多的配置需要隨時更新,服務間需要協調工作,這些信息如何推送到各個節點?並且保證信息的一致性和可靠性?
眾所周知,分布式協調服務很難正確無誤的實現,它們很容易在競爭條件和死鎖上犯錯誤。如何在這方面節省力氣?Zookeeper是一個不錯的選擇。 Zookeeper背后的動機就是解除分布式應用在實現協調服務上的痛苦。本文在介紹Zookeeper的基本理論基礎上,用Zookeeper實現了一 個配置管理中心,利用Zookeeper將配置信息分發到各個服務節點上,並保證信息的正確性和一致性。
Zookeeper是什么?
引用官方的說法:“Zookeeper是一個高性能,分布式的,開源分布式應用協調服務。它提供了簡單原始的功能,分布式應用可以基於它實現更高級 的服務,比如同步,配置管理,集群管理,名空間。它被設計為易於編程,使用文件系統目錄樹作為數據模型。服務端跑在java上,提供java和C的客戶端 API”。
Zookeeper總體結構
Zookeeper服務自身組成一個集群(2n+1個服務允許n個失效)。Zookeeper服務有兩個角色,一個是leader,負責寫服務和數據同步,剩下的是follower,提供讀服務,leader失效后會在follower中重新選舉新的leader。
Zookeeper邏輯圖如下,

- 客戶端可以連接到每個server,每個server的數據完全相同。
- 每個follower都和leader有連接,接受leader的數據更新操作。
- Server記錄事務日志和快照到持久存儲。
- 大多數server可用,整體服務就可用。
Zookeeper 特點
- 順序一致性:按照客戶端發送請求的順序更新數據。
- 原子性:更新要么成功,要么失敗,不會出現部分更新。
- 單一性 :無論客戶端連接哪個server,都會看到同一個視圖。
- 可靠性:一旦數據更新成功,將一直保持,直到新的更新。
- 及時性:客戶端會在一個確定的時間內得到最新的數據。
二、Zookeeper數據模型
ZooKeeper的數據結構, 與普通的文件系統極為類似. 見下圖:

圖片引用自developerworks
圖中的每個節點稱為一個znode. 每個znode由3部分組成:
- stat. 此為狀態信息, 描述該znode的版本, 權限等信息.
- data. 與該znode關聯的數據.
- children. 該znode下的子節點.
三、Zookeeper應用場景
1、統一命名服務(Name Service)
分布式應用中,通常需要有一套完整的命名規則,既能夠產生唯一的名稱又便於人識別和記住,通常情況下用樹形的名稱結構是一個理想的選擇,樹形的名稱結構是一個有層次的目錄結構,既對人友好又不會重復。說到這里你可能想到了 JNDI,沒錯 Zookeeper 的 Name Service 與 JNDI 能夠完成的功能是差不多的,它們都是將有層次的目錄結構關聯到一定資源上,但是 Zookeeper 的 Name Service 更加是廣泛意義上的關聯,也許你並不需要將名稱關聯到特定資源上,你可能只需要一個不會重復名稱,就像數據庫中產生一個唯一的數字主鍵一樣。
Name Service 是 Zookeeper 內置的功能,你只要調用 Zookeeper 的 API 就能實現。如調用 create 接口就可以很容易創建一個目錄節點。
以上摘自IMB Bluemix的分布式服務框架 Zookeeper – 管理分布式環境中的數據文章中,我作個總結:
1)提供類似JNDI的服務,這也是Zookeeper的基礎,整個Zookeeper的功能就是圍繞樹形結構的內容進行展開的
2)提供臨時類型(EPHEMERAL)的目錄
3)提供順序自動編號類型(SEQUENTIAL)的目錄
關於2)、3)點會在后面作詳細介紹,集群管理、共享鎖就是基於以上特性進行實現的
2、統一配置管理(Configuration Management)
配置的管理在分布式應用環境中很常見,例如同一個應用系統需要多台 PC Server 運行,但是它們運行的應用系統的某些配置項是相同的,如果要修改這些相同的配置項,那么就必須同時修改每台運行這個應用系統的 PC Server,這樣非常麻煩而且容易出錯。
像這樣的配置信息完全可以交給 Zookeeper 來管理,將配置信息保存在 Zookeeper 的某個目錄節點中,然后將所有需要修改的應用機器監控配置信息的狀態,一旦配置信息發生變化,每台應用機器就會收到 Zookeeper 的通知,然后從 Zookeeper 獲取新的配置信息應用到系統中。
以上摘自IMB Bluemix的分布式服務框架 Zookeeper – 管理分布式環境中的數據文章中。
在我們使用JVM內存進行數據緩存的場景下,可以采用ZK的這種方式進行數據更新。有人說可以直接使用memcached、redis進行統一配置管理,這樣直接修改redis中的數據就可以了,在對緩存數據的訪問量不大的前提下,該設計是沒有問題的,當數據訪問量極大的時候該設計存在一個問題,就是頻繁的訪問memcached、redis造成大量的網絡開銷,進而影響系統性能。因此將數據緩存至JVM更合適一些。這里只是舉了一個簡單的例子,關於緩存的使用之后寫一篇文章進行詳細的分析。
3、集群管理(Cluster Management)
Zookeeper 能夠很容易的實現集群管理的功能,如有多台 Server 組成一個服務集群,那么必須要一個“總管”知道當前集群中每台機器的服務狀態,一旦有機器不能提供服務,集群中其它集群必須知道,從而做出調整重新分配服務策略。同樣當增加集群的服務能力時,就會增加一台或多台 Server,同樣也必須讓“總管”知道。
Zookeeper 不僅能夠幫你維護當前的集群中機器的服務狀態,而且能夠幫你選出一個“總管”,讓這個總管來管理集群,這就是 Zookeeper 的另一個功能 Leader Election。
它們的實現方式都是在 Zookeeper 上創建一個 EPHEMERAL 類型的目錄節點,然后每個 Server 在它們創建目錄節點的父目錄節點上調用 getChildren(String path, boolean watch) 方法並設置 watch 為 true,由於是 EPHEMERAL 目錄節點,當創建它的 Server 死去,這個目錄節點也隨之被刪除,所以 Children 將會變化,這時 getChildren上的 Watch 將會被調用,所以其它 Server 就知道已經有某台 Server 死去了。新增 Server 也是同樣的原理。
Zookeeper 如何實現 Leader Election,也就是選出一個 Master Server。和前面的一樣每台 Server 創建一個 EPHEMERAL 目錄節點,不同的是它還是一個 SEQUENTIAL 目錄節點,所以它是個 EPHEMERAL_SEQUENTIAL 目錄節點。之所以它是 EPHEMERAL_SEQUENTIAL 目錄節點,是因為我們可以給每台 Server 編號,我們可以選擇當前是最小編號的 Server 為 Master,假如這個最小編號的 Server 死去,由於是 EPHEMERAL 節點,死去的 Server 對應的節點也被刪除,所以當前的節點列表中又出現一個最小編號的節點,我們就選擇這個節點為當前 Master。這樣就實現了動態選擇 Master,避免了傳統意義上單 Master 容易出現單點故障的問題。
以上摘自IMB Bluemix的分布式服務框架 Zookeeper – 管理分布式環境中的數據文章中。
4、共享鎖(Locks)
共享鎖在同一個進程中很容易實現,但是在跨進程或者在不同 Server 之間就不好實現了。Zookeeper 卻很容易實現這個功能,實現方式也是需要獲得鎖的 Server 創建一個 EPHEMERAL_SEQUENTIAL 目錄節點,然后調用 getChildren方法獲取當前的目錄節點列表中最小的目錄節點是不是就是自己創建的目錄節點,如果正是自己創建的,那么它就獲得了這個鎖,如果不是那么它就調用 exists(String path, boolean watch) 方法並監控 Zookeeper 上目錄節點列表的變化,一直到自己創建的節點是列表中最小編號的目錄節點,從而獲得鎖,釋放鎖很簡單,只要刪除前面它自己所創建的目錄節點就行了。
以上摘自IMB Bluemix的分布式服務框架 Zookeeper – 管理分布式環境中的數據文章中。
五、隊列管理(Queue Management)
Zookeeper 可以處理兩種類型的隊列:
當一個隊列的成員都聚齊時,這個隊列才可用,否則一直等待所有成員到達,這種是同步隊列。
隊列按照 FIFO 方式進行入隊和出隊操作,例如實現生產者和消費者模型。
1)同步隊列用 Zookeeper 實現的實現思路如下:
創建一個父目錄 /synchronizing,每個成員都監控標志(Set Watch)位目錄 /synchronizing/start 是否存在,然后每個成員都加入這個隊列,加入隊列的方式就是創建 /synchronizing/member_i 的臨時目錄節點,然后每個成員獲取 / synchronizing 目錄的所有目錄節點,也就是 member_i。判斷 i 的值是否已經是成員的個數,如果小於成員個數等待 /synchronizing/start 的出現,如果已經相等就創建 /synchronizing/start。
2)FIFO 隊列用 Zookeeper 實現思路如下:
實現的思路也非常簡單,就是在特定的目錄下創建 SEQUENTIAL 類型的子目錄 /queue_i,這樣就能保證所有成員加入隊列時都是有編號的,出隊列時通過 getChildren( ) 方法可以返回當前所有的隊列中的元素,然后消費其中最小的一個,這樣就能保證 FIFO。
以上摘自IMB Bluemix的分布式服務框架 Zookeeper – 管理分布式環境中的數據文章中。
四、Zookeeper安裝與使用
安裝環境為centos6.5
172.16.80.177
172.16.80.178
1. 配置機器名。
vi /etc/hosts
172.16.80.177 zookeeper1 172.16.80.178 zookeeper2
2. 安裝JDK並配置環境變量(JAVA_HOME、CLASSPATH、PATH)。
3、下載Zookeeper安裝包
https://mirrors.tuna.tsinghua.edu.cn/apache/zookeeper/

我下載3.4.8版本
4. 安裝並配置(兩台機器上都要做同樣的配置)。
將zookeeper-3.4.8.tar.gz放到root目錄下
mkdir -p /opt/app
tar zxvf zookeeper-3.4.8.tar.gz -C /opt/app/ cd /opt/app/zookeeper-3.4.8/ mkdir data/ logs/
cd /conf
mv zoo_sample.cfg zoo.cfg vi zoo.cfg # 集群每台機器的zoo.cfg配置必須一致。
tickTime=2000 dataDir=/opt/app/zookeeper-3.4.8/data/ dataLogDir=/opt/app/zookeeper-3.4.8/logs/ clientPort=2181 initLimit=5 syncLimit=2 server.1=zookeeper1:2888:3888 # 每台機器都要感知集群的機器組成,配置格式為“server.id=host:port:port”。id范圍1~255。 server.2=zookeeper2:2888:3888
# 在data目錄創建myid文件。根據zoo.cfg配置,id應與機器對應。如zookeeper1的id為1,zookeeper2的id為2. echo 1 > data/myid echo 2 > data/myid
echo 1是在機器1上執行,echo 2是要在機器2上執行
5. 啟動、關閉。
/opt/app/zookeeper-3.4.8/bin/zkServer.sh start /opt/app/zookeeper-3.4.8/bin/zkServer.sh stop /opt/app/zookeeper-3.4.8/bin/zkServer.sh status
[root@host-172-16-80-178 bin]# /opt/app/zookeeper-3.4.8/bin/zkServer.sh status ZooKeeper JMX enabled by default Using config: /opt/app/zookeeper-3.4.8/bin/../conf/zoo.cfg Mode: leader
[root@host-172-16-80-177 zookeeper-3.4.8]# /opt/app/zookeeper-3.4.8/bin/zkServer.sh status ZooKeeper JMX enabled by default Using config: /opt/app/zookeeper-3.4.8/bin/../conf/zoo.cfg Mode: follower
6、連接zookeeper
[root@host-172-16-80-177 zookeeper-3.4.8]# /opt/app/zookeeper-3.4.8/bin/zkCli.sh -server zookeeper2:2181 Connecting to zookeeper1:2181 2017-10-19 04:29:07,456 [myid:] - INFO [main:Environment@100] - Client environment:zookeeper.version=3.4.8--1, built on 02/06/2016 03:18 GMT 2017-10-19 04:29:07,460 [myid:] - INFO [main:Environment@100] - Client environment:host.name=<NA> 2017-10-19 04:29:07,460 [myid:] - INFO [main:Environment@100] - Client environment:java.version=1.7.0_80 2017-10-19 04:29:07,463 [myid:] - INFO [main:Environment@100] - Client environment:java.vendor=Oracle Corporation 2017-10-19 04:29:07,463 [myid:] - INFO [main:Environment@100] - Client environment:java.home=/usr/java/jdk1.7.0_80/jre 2017-10-19 04:29:07,463 [myid:] - INFO [main:Environment@100] - Client environment:java.class.path=/opt/app/zookeeper-3.4.8/bin/../build/classes:/opt/app/zookeeper-3.4.8/bin/../build/lib/*.jar:/opt/app/zookeeper-3.4.8/bin/../lib/slf4j-log4j12-1.6.1.jar:/opt/app/zookeeper-3.4.8/bin/../lib/slf4j-api-1.6.1.jar:/opt/app/zookeeper-3.4.8/bin/../lib/netty-3.7.0.Final.jar:/opt/app/zookeeper-3.4.8/bin/../lib/log4j-1.2.16.jar:/opt/app/zookeeper-3.4.8/bin/../lib/jline-0.9.94.jar:/opt/app/zookeeper-3.4.8/bin/../zookeeper-3.4.8.jar:/opt/app/zookeeper-3.4.8/bin/../src/java/lib/*.jar:/opt/app/zookeeper-3.4.8/bin/../conf: 2017-10-19 04:29:07,463 [myid:] - INFO [main:Environment@100] - Client environment:java.library.path=/usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib 2017-10-19 04:29:07,463 [myid:] - INFO [main:Environment@100] - Client environment:java.io.tmpdir=/tmp 2017-10-19 04:29:07,463 [myid:] - INFO [main:Environment@100] - Client environment:java.compiler=<NA> 2017-10-19 04:29:07,464 [myid:] - INFO [main:Environment@100] - Client environment:os.name=Linux 2017-10-19 04:29:07,464 [myid:] - INFO [main:Environment@100] - Client environment:os.arch=amd64 2017-10-19 04:29:07,464 [myid:] - INFO [main:Environment@100] - Client environment:os.version=2.6.32-573.el6.x86_64 2017-10-19 04:29:07,464 [myid:] - INFO [main:Environment@100] - Client environment:user.name=root 2017-10-19 04:29:07,464 [myid:] - INFO [main:Environment@100] - Client environment:user.home=/root 2017-10-19 04:29:07,464 [myid:] - INFO [main:Environment@100] - Client environment:user.dir=/opt/app/zookeeper-3.4.8 2017-10-19 04:29:07,466 [myid:] - INFO [main:ZooKeeper@438] - Initiating client connection, connectString=zookeeper1:2181 sessionTimeout=30000 watcher=org.apache.zookeeper.ZooKeeperMain$MyWatcher@594b7042 Welcome to ZooKeeper! 2017-10-19 04:29:07,501 [myid:] - INFO [main-SendThread(zookeeper1:2181):ClientCnxn$SendThread@1032] - Opening socket connection to server zookeeper1/172.16.80.177:2181. Will not attempt to authenticate using SASL (unknown error) 2017-10-19 04:29:07,515 [myid:] - INFO [main-SendThread(zookeeper1:2181):ClientCnxn$SendThread@876] - Socket connection established to zookeeper1/172.16.80.177:2181, initiating session JLine support is enabled 2017-10-19 04:29:07,552 [myid:] - INFO [main-SendThread(zookeeper1:2181):ClientCnxn$SendThread@1299] - Session establishment complete on server zookeeper1/172.16.80.177:2181, sessionid = 0x15f33b62ee10001, negotiated timeout = 30000 WATCHER:: WatchedEvent state:SyncConnected type:None path:null
[zk: zookeeper1:2181(CONNECTED) 0] ls /
[zookeeper]
[zk: zookeeper1:2181(CONNECTED) 1] create /helloworld 123
Created /helloworld
[zk: zookeeper1:2181(CONNECTED) 2] ls /
[helloworld, zookeeper]
[zk: zookeeper1:2181(CONNECTED) 3] quit
Quitting...
2017-10-19 04:33:01,671 [myid:] - INFO [main:ZooKeeper@684] - Session: 0x15f33b62ee10002 closed
2017-10-19 04:33:01,675 [myid:] - INFO [main-EventThread:ClientCnxn$EventThread@519] - EventThread shut down for session: 0x15f33b62ee10002
/opt/app/zookeeper-3.4.8/bin/zkCli.sh -server zookeeper2:2181
[zk: zookeeper1:2181(CONNECTED) 0] ls / [helloworld, zookeeper] [zk: zookeeper1:2181(CONNECTED) 1] get /helloworld 123 cZxid = 0x100000005 ctime = Thu Oct 19 04:32:43 EDT 2017 mZxid = 0x100000005 mtime = Thu Oct 19 04:32:43 EDT 2017 pZxid = 0x100000005 cversion = 0 dataVersion = 0 aclVersion = 0 ephemeralOwner = 0x0 dataLength = 3 numChildren = 0 [zk: zookeeper1:2181(CONNECTED) 2]
help命令
顯示客戶所支持的所有命令,如:
ZooKeeper -server host:port cmd args
connecthost:port
getpath [watch]
lspath [watch]
setpath data [version]
rmrpath
delquota[-n|-b] path
quit
printwatcheson|off
create[-s] [-e] path data acl
statpath [watch]
close
ls2path [watch]
history
listquotapath
setAclpath acl
getAclpath
syncpath
redocmdno
addauthscheme auth
deletepath [version]
setquota-n|-b val path
connect命令
連接zk服務端,與close命令配合使用可以連接或者斷開zk服務端。
如connect 127.0.0.1:2181
get命令
獲取節點信息,注意節點的路徑皆為絕對路徑,也就是說必要要從/(根路徑)開始。
如get /
hello world
cZxid = 0x0
ctime = Thu Jan 01 08:00:00 CST 1970
mZxid = 0x5
mtime = Thu Apr 27 15:09:00 CST 2017
pZxid = 0xc
cversion = 1
dataVersion = 2
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 11
numChildren = 1
詳解:
hello world為節點數據信息
cZxid節點創建時的zxid
ctime節點創建時間
mZxid節點最近一次更新時的zxid
mtime節點最近一次更新的時間
cversion子節點數據更新次數
dataVersion本節點數據更新次數
aclVersion節點ACL(授權信息)的更新次數
ephemeralOwner如果該節點為臨時節點,ephemeralOwner值表示與該節點綁定的session id. 如果該節點不是臨時節點,ephemeralOwner值為0
dataLength節點數據長度,本例中為hello world的長度
numChildren子節點個數
ls命令
獲取路徑下的節點信息,注意此路徑為絕對路徑,類似於linux的ls命令。
如ls /zookeeper
set命令
設置節點的數據。
如set /zookeeper "hello world"
rmr命令
刪除節點命令,此命令與delete命令不同的是delete不可刪除有子節點的節點,但是rmr命令可以刪除,注意路徑為絕對路徑。
如rmr /zookeeper/znode
delquota命令
刪除配額,-n為子節點個數,-b為節點數據長度。
如delquota –n 2,請參見listquota和setquota命令。
quit命令
退出。
printwatches命令
設置和顯示監視狀態,on或者off。
如printwatches on
create命令
創建節點,其中-s為順序充點,-e臨時節點。
如create /zookeeper/node1"test_create" world:anyone:cdrwa
其中acl處,請參見getAcl和setAcl命令。
stat命令
查看節點狀態信息。如stat /
cZxid = 0x0
ctime = Thu Jan 01 08:00:00 CST 1970
mZxid = 0x1f
mtime = Thu Apr 27 16:05:14 CST 2017
pZxid = 0xc
cversion = 1
dataVersion = 3
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 1
與get命令大體相同,請參見get命令。
close命令
斷開客戶端與服務端的連接。
ls2命令
ls2為ls命令的擴展,比ls命令多輸出本節點信息。
如 ls /zookeeper
history命令
列出最近的歷史命令。
如history
0 - ls /
1 - ls /
2 - ls2 /
3 - history
4 - listquota /zookeeper
5 – history
基本格式為:命令ID-命令,可以與redo命令配合使用。
listquota命令
顯示配額。
如listquota /zookeeper
absolute path is/zookeeper/quota/zookeeper/zookeeper_limits
Output quota for /zookeepercount=2,bytes=-1
解釋:
/zookeeper節點個數限額為2,長度無限額。
setAcl命令
設置節點Acl。
此處重點說一下acl,acl由大部分組成:1為scheme,2為user,3為permission,一般情況下表示為scheme:id:permissions。
其中scheme和id是相關的,下面將scheme和id一起說明。
scheme和id
world: 它下面只有一個id, 叫anyone, world:anyone代表任何人,zookeeper中對所有人有權限的結點就是屬於world:anyone的
auth: 它不需要id, 只要是通過authentication的user都有權限(zookeeper支持通過kerberos來進行authencation, 也支持username/password形式的authentication)
digest: 它對應的id為username:BASE64(SHA1(password)),它需要先通過username:password形式的authentication
ip: 它對應的id為客戶機的IP地址,設置的時候可以設置一個ip段,比如ip:192.168.1.0/16, 表示匹配前16個bit的IP段
super: 在這種scheme情況下,對應的id擁有超級權限,可以做任何事情(cdrwa)
permissions
CREATE(c): 創建權限,可以在在當前node下創建child node
DELETE(d): 刪除權限,可以刪除當前的node
READ(r): 讀權限,可以獲取當前node的數據,可以list當前node所有的child nodes
WRITE(w): 寫權限,可以向當前node寫數據
ADMIN(a): 管理權限,可以設置當前node的permission
綜上,一個簡單使用setAcl命令,則可以為:
setAcl /zookeeper/node1 world:anyone:cdrw
getAcl命令
獲取節點Acl。
如getAcl /zookeeper/node1
'world,'anyone
: cdrwa
注:可參見setAcl命令。
sync命令
強制同步。
如sync /zookeeper
由於請求在半數以上的zk server上生效就表示此請求生效,那么就會有一些zk server上的數據是舊的。sync命令就是強制同步所有的更新操作。
redo命令
再次執行某命令。
如redo 10
其中10為命令ID,需與history配合使用。
addauth命令
節點認證。
如addauth digest username:password,可參見setAcl命令digest處。
使用方法:
一、通過setAcl設置用戶名和密碼
setAcl pathdigest:username:base64(sha1(password)):crwda
二、認證
addauth digest username:password
delete命令
刪除節點。
如delete /zknode1
setquota命令
設置子節點個數和數據長度配額。
如setquota –n 4 /zookeeper/node 設置/zookeeper/node子節點個數最大為4
setquota –b 100 /zookeeper/node 設置/zookeeper/node節點長度最大為100

