1. 概述
Zookeeper是一個開源的分佈式的,爲分佈式應用提供協調服務的Apache項目。
Zookeeper從設計模式角度來理解:是一個基於觀察者模式設計的分佈式服務管理框架,它負責存儲和管理大家都關心的數據,然後接受觀察者的註冊,一旦這些數據的狀態發生變化,Zookeeper就將負責通知已經在Zookeeper上註冊的那些觀察者做出相應的反應。
所以可以大概理解成:Zookeeper=文件系統+通知機制。
官網下載:https://zookeeper.apache.org/
2. 特點
- Zookeeper:一個領導者(Leader),多個跟隨者(Follower)組成的集羣
- 集羣中只要有半數以上節點存活,Zookeeper集羣就能正常服務。
- 全局數據一致:每個Server保存一份相同的數據副本,Client無論連接到哪個Server,數據都是一致的。
- 更新請求順序進行,來自同一個Client的更新請求按其發送順序依次執行。
- 數據更新原子性,一次數據更新要麼成功,要麼失敗。
- 實時性,在一定時間範圍內,Client能讀到最新數據。
3. 數據結構
ZooKeeper數據模型的結構與Unix文件系統很類似,整體上可以看作是一棵樹,每個節點稱做一個ZNode。每一個ZNode默認能夠存儲1MB的數據,每個ZNode都可以通過其路徑唯一標識。且節點的名稱都是唯一的,不允許重複。
4. 應用場景
統一命名服務、統一配置管理、統一集羣管理、服務器節點動態上下線、軟負載均衡,分佈式鎖等。
- 統一命名:在分佈式系統中,經常需要對應用/服務進行統一命名,便於識別,和ip和域名類似,而zk的樹形結構有個特點就是節點名稱不允許重複,所以可以使用這個特性來實現命名服務。
- 配置管理:集羣環境下,一般將應用的統一配置集中管理,即寫到zookeeper中的Znode上,然後客戶端通過監聽該節點數據,配置一變更,Zookeeper就立即通知監聽該節點的各個服務器
- 統一集羣管理:集羣中的各個應用都將自身信息註冊到Zookeeper上,然後通過監聽節點獲取他們的實時狀態變化
- 服務器節點動態上下線:集羣中,將服務器自身註冊爲Zookeeper上的一個節點,當服務器宕機,節點自動刪除,此時,客戶端就能即使響應,當下次請求時就能避免訪問宕機的服務器
- 軟負載均衡:服務器作爲節點註冊到Zookeeper上,然後Zookeeper記錄每臺服務器被訪問的次數,讓訪問數最少的服務器區處理最新的客戶端請求
- 分佈式鎖:可實現分佈式鎖。
5.Zookeeper內部原理
5.1 選舉機制(重要)
- 半數機制:集羣中半數以上機器存活,集羣可用。所以Zookeeper適合安裝奇數臺服務器。
- Zookeeper雖然在配置文件中並沒有指定Master和Slave。但是,Zookeeper工作時,是有一個節點爲Leader,其他則爲Follower,Leader是通過內部的選舉機制臨時產生的。
舉例說明:
假設有五臺服務器組成的Zookeeper集羣,它們的id從1-5(通過myid文件設置id),同時它們都是最新啓動的,也就是沒有歷史數據,在存放數據量這一點上,都是一樣的。假設這些服務器依序啓動,來看看會發生什麼?
注意:一開始都是會選舉自己的,但是如果選舉自己後沒當上Leader的話,下次其他服務器啓動,就會投票給其他的zk server
(1)服務器1啓動,此時只有它一臺服務器啓動了,它發出去的報文沒有任何響應,所以它的選舉狀態一直是LOOKING狀態。
(2)服務器2啓動,它與最開始啓動的服務器1進行通信,互相交換自己的選舉結果,由於兩者都沒有歷史數據,所以id值較大的服務器2勝出,但是由於沒有達到超過半數以上的服務器都同意選舉它(這個例子中的半數以上是3),所以服務器1、2還是繼續保持LOOKING狀態。
(3)服務器3啓動,根據前面的理論分析,服務器3成爲服務器1、2、3中的老大,而與上面不同的是,此時有三臺服務器選舉了它,所以它成爲了這次選舉的Leader。
(4)服務器4啓動,根據前面的分析,理論上服務器4的id應該是服務器1、2、3、4中最大的,但是由於前面已經有半數以上的服務器選舉了服務器3,所以它只能當Follow。
(5)服務器5啓動,同4一樣當小弟。
5.2 節點類型
- 持久型目錄節點:客戶端與Zookeeper斷開連接後,該節點依舊存在
- 持久性順序編號目錄節點:和持久型目錄節點不同的是,Zookeeper會給該節點名稱進行順序編號。
- 臨時目錄節點:客戶端與Zookeeper斷開連接後,該節點被刪除
- 臨時順序編號目錄節點:和臨時目錄節點不同的是,Zookeeper會給該節點名稱進行順序編號。
說明:創建znode時設置順序標識,znode名稱後會附加一個值,順序號是一個單調遞增的計數器,由父節點維護。如設置的節點名爲user,那麼添加順序標識後,生成的就編程user,user001,user002
5.3 Stat結構體
屬性有很多,需要記住的就是:
dataLength- znode的數據長度
numChildren - znode子節點數量
5.4 監聽器使用(重要)
常見的監聽器:
- 監聽節點數據的變化:get path [watch]
- 監聽子節點增減的變化:ls path [watch]
監聽注意點:
監聽只能監聽一次節點變化,如果想再次監聽,那就必須再次發送監聽請求,即再次調用get path watch命令。
在代碼中實現循環監聽有兩種方式:
- 對某個路徑在本次監聽完後主動用簡單(exist()方法)的調用再次註冊監聽
- 使用addWatch方法設定AddWatchMode屬性
實現監聽:
- 首先要有一個main()線程
- 在main線程中創建Zookeeper客戶端,這時就會創建兩個線程,一個負責網絡連接通信(connet),一個負責監聽(listener)。
- 通過connect線程將註冊的監聽事件發送給Zookeeper。
- 在Zookeeper的註冊監聽器列表中將註冊的監聽事件添加到列表中。
- Zookeeper監聽到有數據或路徑變化,就會將這個消息發送給listener線程。
- listener線程內部調用了process()方法。
5.5 寫數據流程
6. 單節點集羣安裝
6.1 安裝前準備
(1)安裝Jdk
(2)拷貝Zookeeper安裝包到Linux系統下
(3)解壓到指定目錄
tar -zxvf zookeeper-3.4.10.tar.gz -C /opt/module/
6.2 修改配置
(1)將/opt/module/zookeeper-3.4.10/conf這個路徑下的zoo_sample.cfg修改爲zoo.cfg;
mv zoo_sample.cfg zoo.cfg
(2)打開zoo.cfg文件,修改dataDir路徑(保存數據到本地系統,便於數據管理):
vim zoo.cfg
dataDir=/opt/module/zookeeper-3.4.10/zkData
(3)在/opt/module/zookeeper-3.4.10/這個目錄上創建zkData文件夾
mkdir zkData
6.3 操作Zookeeper
(1)啓動Zookeeper服務器端
bin/zkServer.sh start
(2)查看進程是否啓動
jps
(3)查看狀態:
bin/zkServer.sh status
(4)啓動客戶端:
bin/zkCli.sh
(5)退出客戶端:
quit
(6)停止Zookeeper
bin/zkServer.sh stop
6.4 配置文件參數解讀
Zookeeper中的配置文件zoo.cfg中參數含義解讀如下:
1.tickTime =2000:服務器之間或客戶端與服務器之間維持心跳的時間間隔,也就是每個tickTime時間就會發送一個心跳,時間單位爲毫秒。
它用於心跳機制,並且設置最小的session超時時間爲兩倍心跳時間。(session的最小超時時間是2*tickTime)
2.initLimit =10:Follower和Leader初始通信時限
集羣中的Follower跟隨者服務器與Leader領導者服務器之間初始連接時能容忍的最多心跳數(tickTime的數量)。像這樣設置的話,啓動Zookeeper時Leader和Follow最大的連接時間爲2000*10=20s
3.syncLimit =5:Leader和Follower初始連接成功後,集羣中Leader與Follower之間的最大響應時間單位。假如響應超過syncLimit * tickTime,Leader認爲Follwer死掉,從服務器列表中刪除Follwer。這裏的話就是10秒
4.dataDir:數據文件目錄+數據持久化路徑,主要用於保存Zookeeper中的數據。
5.clientPort =2181:客戶端連接服務器端時的端口
7. 分佈式安裝部署
部署在3臺機器上
7.1 解壓安裝
解壓zookeeper的安裝包到3臺機器上
7.2 配置服務器編號
(1)在/opt/module/zookeeper-3.4.10/這個目錄下創建zkData
(2)在/opt/module/zookeeper-3.4.10/zkData目錄下創建一個myid的文件,並添加id
touch myid
vi myid
#在 myid文件中添加與server對應的編號2
2
添加myid文件,注意一定要在linux裏面創建,在notepad++裏面很可能亂碼
(3)在其他兩臺機器上都添加myid文件,並修改文件內容爲3、4
7.3 配置zoo.cfg文件
(1) 重命名/opt/module/zookeeper-3.4.10/conf這個目錄下的zoo_sample.cfg爲zoo.cfg
(2) 打開zoo.cfg文件,修改數據存儲地址
dataDir=/opt/module/zookeeper-3.4.10/zkData
再添加如下配置
增加如下配置
#######################cluster##########################
server.2=192.168.24.200:2888:3888
server.3=192.168.24.201:2888:3888
server.4=192.168.24.202:2888:3888
(3)配置參數解讀
server.A=B:C:D
A是一個數字,表示這個是第幾號服務器,即設置在myid中的值
集羣模式下配置一個文件myid,這個文件在dataDir目錄下,這個文件裏面有一個數據就是A的值,Zookeeper啓動時讀取此文件,拿到裏面的數據與zoo.cfg裏面的配置信息比較從而判斷到底是哪個server。
B是這個服務器的ip地址;
C是這個服務器與集羣中的Leader服務器交換數據的端口;
D是萬一集羣中的Leader服務器掛了,需要一個端口來重新進行選舉,選出一個新的Leader,而這個端口就是用來執行選舉時服務器相互通信的端口。
7.4 啓動服務器
分別啓動3臺機器的zk server
bin/zkServer.sh start
8. 客戶端命令行操作
- ls path [watch]:使用 ls 命令來查看當前znode中所包含的內容
- ls2 path [watch]:看當前節點數據並能看到更新次數等數據
- create:普通創建 (-s 含有序列,-e 臨時節點)
- get path [watch]:獲得節點的值
- set:設置節點的具體值
- stat:查看節點狀態
- delete:刪除節點
- rmr:遞歸刪除節點
1.啓動客戶端
bin/zkCli.sh
2.創建2個普通節點
create /sanguo "jinlian"
create /sanguo/shuguo "liubei"
3.創建短暫節點
create -e /sanguo/wuguo "zhouyu"
4.創建帶序號的節點
create -s /sanguo/weiguo/xiaoqiao "jinlian"
create -s /sanguo/weiguo/daqiao "jinlian"
create -s /sanguo/weiguo/daqiao "jinlian"
結果:
Created /sanguo/weiguo/xiaoqiao0000000000
Created /sanguo/weiguo/daqiao0000000001
Created /sanguo/weiguo/diaocan0000000002
如果原來沒有序號節點,序號從0開始依次遞增。如果原節點下已有2個節點,則再排序時從2開始,以此類推。
5.獲得節點的值
get /sanguo
6.節點的值變化監聽,在命令行可添加watch(上面的命令列表查看)的條件下,給命令最後添加watch,可以實現監聽節點值的變化
get /sanguo watch
7. 監聽某節點的子節點路徑變化
ls /sanguo watch
9. JAVA 操作zookeeper
9.1 引入依賴包
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.6.1</version>
</dependency>
9.2 代碼展示
public class MyZkClient {
// 集羣連接地址,多個以逗號分隔
private static final String CONNECT_ADDRES = "127.0.0.1:2181,127.0.0.1:2182";
// 會話超時時間
private static final int SESSIONTIME = 2000;
// 信號量,讓zk在連接之前等待,連接成功後才能往下走.
private static final CountDownLatch countDownLatch = new CountDownLatch(1);
private static String LOG_MAIN = "【main】 ";
private static ZooKeeper zk;
public void createConnection(String connectAddres, int sessionTimeOut) {
try {
zk = new ZooKeeper(connectAddres, sessionTimeOut, new Watcher() {
public void process(WatchedEvent watchedEvent) {
System.out.println("事件來了");
// 獲取事件狀態
Watcher.Event.KeeperState keeperState = watchedEvent.getState();
// 獲取事件類型
Watcher.Event.EventType eventType = watchedEvent.getType();
// zk 路徑
String path = watchedEvent.getPath();
System.out.println("進入到 process() keeperState:" + keeperState + ", eventType:" + eventType + ", path:" + path);
// 判斷是否建立連接
if (Watcher.Event.KeeperState.SyncConnected == keeperState) {
if (Watcher.Event.EventType.None == eventType) {
// 如果建立建立成功,讓後程序往下走
System.out.println(LOG_MAIN + "zk 建立連接成功!");
countDownLatch.countDown();
} else if (Watcher.Event.EventType.NodeCreated == eventType) {
System.out.println(LOG_MAIN + "事件通知,新增node節點" + path);
} else if (Watcher.Event.EventType.NodeDataChanged == eventType) {
System.out.println(LOG_MAIN + "事件通知,當前node節點" + path + "被修改....");
}
else if (Watcher.Event.EventType.NodeDeleted == eventType) {
System.out.println(LOG_MAIN + "事件通知,當前node節點" + path + "被刪除....");
}
}
System.out.println("--------------------------------------------------------");
}
});
System.out.println(LOG_MAIN + "zk 開始啓動連接服務器....");
countDownLatch.await();
} catch (Exception e) {
e.printStackTrace();
}
}
public boolean createPath(String path, String data) {
try {
this.exists(path, true);
this.zk.create(path, data.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
System.out.println(LOG_MAIN + "節點創建成功, Path:" + path + ",data:" + data);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 判斷指定節點是否存在
*
* @param path
* 節點路徑
*/
public Stat exists(String path, boolean needWatch) {
try {
return this.zk.exists(path, needWatch);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public boolean updateNode(String path,String data) throws KeeperException, InterruptedException {
exists(path, true);
this.zk.setData(path, data.getBytes(), -1);
return false;
}
public static void main(String[] args) throws KeeperException, InterruptedException {
MyZkClient zkClientWatcher = new MyZkClient();
zkClientWatcher.createConnection(CONNECT_ADDRES, SESSIONTIME);
boolean createResult = zkClientWatcher.createPath("/p15", "pa-644064");
zkClientWatcher.updateNode("/p15","7894561");
// 一次性監聽器
zk.getData("/p15", new Watcher() {
public void process(WatchedEvent watchedEvent) {
System.out.println("haha");
}
}, null);
//添加自定義監聽器
//AddWatchMode.PERSISTENT表示循環監聽指定節點,能監聽到子節點的新增和刪除,監聽不了子節點的更新。監聽不了孫節點的任何操作
//AddWatchMode.PERSISTENT_RECURSIVE 表示循環監聽指定節點及其子孫節點的增刪改操作
zk.addWatch("/p15", new Watcher() {
public void process(WatchedEvent watchedEvent) {
System.out.println("xixi");
}
}, AddWatchMode.PERSISTENT);
Thread.sleep(Long.MAX_VALUE);
}
}
代碼說明:
1. 使用CountDownLatch的作用是讓zk client連接服務器後再去做新增,修改那些操作,因爲可以看到,我們在createConnection後立即就調了createPath創建節點了,如果zk client都還沒連上zk server,就去做新增,修改,那肯定是會出問題的
2.在createConnection方法中可看到我們在new ZooKeeper時傳入了一個Watcher的一個匿名內部類,重寫了process方法,這裏其實就是添加了一個默認的事件監聽器。該默認的事件監聽器默認只會監聽一次,且監聽的觸發是在zk client連接上zk server。
3.創建節點時調用ZooKeeper類的create方法,且通過CreateMode參數來改變節點的類型(暫時性和持久性)
4.在updateNode()和createPath()方法中可以看到調用了this.exists(path, true)方法,true就表示給path節點再次添加事件監聽。通過次方法來實現簡單的循環監聽。
因爲上面說過了,在zk client連接上zk server後,事件就已經監聽過一次了,而zk 的事件監聽也只能是一次,如果想再次監聽,必須再次發送監聽請求。而this.exists(path, true)方法不僅判斷了節點是否存在,還會爲節點再次添加事件監聽,當監聽到事件後,會調用new ZooKeeper()中實現的Watch監聽器。
5.如果不想使用new ZooKeeper()中實現的Watch監聽器,就自己再寫個監聽器,然後調用exists的重載方法 exists(String path, Watcher watcher),這樣監聽到後就會執行新的監聽器代碼
6.可以看到我的main方法中有端代碼:zk.getData。。。,這個事件就是自己重新定義了一個監聽器,沒有再使用那個默認的監聽器。
9.3 自定義註冊事件及循環監聽
(1)自定義監聽器:實現Watcher接口,實現process方法
(2)註冊事件:zk.addWatch(),其中有個參數AddWatchMode。它有兩個值:
AddWatchMode.PERSISTENT:循環監聽指定節點,能監聽到子節點的新增和刪除,監聽不了子節點的更新。監聽不了孫節點的任何操作
AddWatchMode.PERSISTENT_RECURSIVE:循環監聽指定節點及其子孫節點的增刪改操作
使用addWatch()監聽的節點即使被刪除,也會持續監聽。將節點刪除後再次添加,會發現依然是可以監聽到節點的新增的
9.4 WatchedEvent
WatchedEvent表示監聽器中process方法中的回調參數:WatchedEvent watchedEvent
該參數定義了事件通知相關的邏輯,包含KeeperState和EventType兩個枚舉類,分別代表了通知狀態和事件類型