1. Zookeeper 入門
1.1 概述
Zookeeper 是一個開源的分佈式的,爲分佈式應用提供協調服務的 Apache 項目。
Zookeeper 從設計模式角度來理解:是一個基於觀案者模式設計的分佈式服務管理框架,它負責存儲和管理大家都關心的數據,然後接受觀察者的註冊,一旦這些數據的狀態發生變化,Zookeeper 就將負責通知已經在 Zookeeper 上註冊的那些觀察者做出相應的反應。
1.2 特點
- Zookeeper:一個領導者(Leader) ,多個跟隨者(Follower)組成的集羣。
- 集羣中只要有半數以上節點存活,Zookeeper 集羣就能正常服務。
- 全局數據一致:每個 Server 保存一份相同的數據副本,Client 無論連接到哪個 Server,數據都是一致的。
- 更新請求順序進行,來自同一個 Client 的更新請求按其發送順序依次執行。
- 數據更新原子性,一次數據更新要麼成功,要麼失敗。
- 實時性,在一定時間範圍內,Client 能讀到最新數據。
1.3 數據結構
ZooKeeper 數據模型的結構與 Unix 文件系統很類似,整體上可以看作是一棵樹,每個節點稱做一個 ZNode。每一個 ZNode 默認能夠存儲 1 MB 的數據,每個 ZNode 都可以通過其路徑唯一標識。
1.4 應用場景
提供的服務包括:統一命名服務、統一配置管理、統一集羣管理、服務器節點動態上下線、軟負載均衡等。
-
統一命名服務
在分佈式環境下,經常需要對應用/服務進行統一命名 ,便於識別。例如:IP 不容易記住,而域名容易記住。
-
統一配置管理
(1)分佈式環境下,配置文件同步非常常見。
① 一般要求一個集羣中,所有節點的配置信息是一致的,比如 Kafka 集羣。
② 對配置文件修改後,希望能夠快速同步到各個節點上。(2)配置管理可交由 ZooKeeper 實現。
① 可將配置信息寫入 ZooKeeper 上的一個 Znode 。
② 各個客戶端服務器監聽這個 Znode。
③ 一旦 Znode 中的數據被修改,ZooKeeper 將通知各個客戶端服務器。
-
統一集羣管理
(1)分佈式環境中,實時掌握每個節點的狀態是必要的。
可根據節點實時狀態做出一些調整。
(2)ZooKeeper 可以實現實時監控節點狀態變化
① 可將節點信息寫入Z ooKeeper 上的一個 ZNode。
② 監聽這個 ZNode 可獲取它的實時狀態變化。
-
服務器動態上下線
-
軟負載均衡
在 Zookeeper 中記錄每臺服務器的訪問數,讓訪問數最少的服務器去處理最新的客戶端請求。
2. Zookeeper 安裝
2.1 下載地址
2.2 本地模式安裝部署
-
準備工作
① 拷貝 Zookeeper 安裝包到 Linux 系統下(apache-zookeeper-3.5.6-bin.tar.gz)
② 解壓到指定目錄
tar -zxvf apache-zookeeper-3.5.6-bin.tar.gz -C /hadoop/
③ 重命名
mv apache-zookeeper-3.5.6-bin/ zookeeper-3.5.6
-
修改配置
① 將 /hadoop/zookeeper-3.5.6/conf 這個路徑下的 zoo_sample.cfg 修改爲 zoo.cfg
mv zoo_sample.cfg zoo.cfg
② 打開 zoo.cfg 文件,修改 dataDir 路徑
dataDir=/hadoop/zookeeper-3.5.6/zkData
③ 在 /hadoop/zookeeper-3.5.6/ 這個目錄上創建 zkData 文件夾
mkdir zkData
-
修改環境變量
① 打開配置文件
vim /etc/profile
② 在配置文件中添加以下內容
#ZOOKEEPER
export ZOOKEEPER_HOME=/hadoop/zookeeper-3.5.6
export PATH=$PATH:$ZOOKEEPER_HOME/bin
③ 使配置文件生效
source /etc/profile
-
操作 Zookeeper
① 啓動 Zookeeper
zkServer.sh start
② 查看進程是否啓動
jps
③ 查看狀態
zkServer.sh status
④ 啓動客戶端
zkCli.sh
⑤ 退出客戶端
quit
⑥ 停止 Zookeeper
zkServer.sh stop
2.3 分佈式安裝部署
-
集羣規劃
在 master、slave1 和 slave2 三個節點上部署 Zookeeper。
-
解壓安裝
① 解壓 Zookeeper 安裝包到 /hadoop/ 目錄下
tar -zxvf apache-zookeeper-3.5.6-bin.tar.gz -C /hadoop/
③ 重命名
mv apache-zookeeper-3.5.6-bin/ zookeeper-3.5.6
③ 同步 /hadoop/zookeeper-3.5.6 目錄內容到 slave1、slave2
xsync zookeeper-3.5.6/
-
配置服務器編號
① 在 /hadoop/zookeeper-3.5.6/ 這個目錄下創建 zkData
mkdir zkData
② /hadoop/zookeeper-3.5.6/zkData 目錄下創建一個 myid 的文件
touch myid
③ 編輯 myid 文件
vim myid
在文件中添加與 server 對應的編號:
0
④ 分發到其他機器上
xsync myid
並分別在 slave1、slave2 上修改 myid 文件中內容爲 1、2
-
配置 zoo.cfg 文件
① 將 /hadoop/zookeeper-3.5.6/conf 這個路徑下的 zoo_sample.cfg 修改爲 zoo.cfg
mv zoo_sample.cfg zoo.cfg
② 打開 zoo.cfg 文件,修改 dataDir 路徑
dataDir=/hadoop/zookeeper-3.5.6/zkData
增加如下配置
server.0=master:2888:3888
server.1=slave1:2888:3888
server.2=slave2:2888:3888
③ 同步 zoo.cfg 配置文件
xsync zoo.cfg
-
修改環境變量
① 打開配置文件
vim /etc/profile
② 在配置文件中添加以下內容
#ZOOKEEPER
export ZOOKEEPER_HOME=/hadoop/zookeeper-3.5.6
export PATH=$PATH:$ZOOKEEPER_HOME/bin
③ 同步配置文件
xsync /etc/profile
④ 使配置文件生效(三臺機器)
source /etc/profile
-
集羣操作
① 三臺機器分別啓動 Zookeeper
zkServer.sh start
② 三臺機器分別關閉 Zookeeper
zkServer.sh stop
-
編寫 Zookeeper 的羣起羣關腳本
① 在 /usr/local/bin 目錄下創建 zk 文件
vim zk
在文件中輸入以下內容:
#!/bin/bash
case $1 in
"start"){
for i in master slave1 slave2
do
echo "****************** $i *********************"
ssh $i "source /etc/profile && zkServer.sh start"
done
};;
"stop"){
for i in master slave1 slave2
do
echo "****************** $i *********************"
ssh $i "source /etc/profile && zkServer.sh stop"
done
};;
esac
② 修改腳本 zk 具有執行權限
chmod 777 zk
③ 調用腳本形式:zk start 或 zk stop
2.4 配置參數解讀
Zookeeper 中的配置文件 zoo.cfg 中參數含義解讀如下:
-
tickTime =2000:通信心跳數,Zookeeper 服務器與客戶端心跳時間,單位毫秒
Zookeeper 使用的基本時間,服務器之間或客戶端與服務器之間維持心跳的時間間隔,也就是每個tickTime 時間就會發送一個心跳,時間單位爲毫秒。它用於心跳機制,並且設置最小的 session 超時時間爲兩倍心跳時間。(session 的最小超時時間是 2*tickTime)
-
initLimit =10:LF 初始通信時限
集羣中的 Follower 跟隨者服務器與 Leader 領導者服務器之間初始連接時能容忍的最多心跳數(tickTime的數量),用它來限定集羣中的 Zookeeper 服務器連接到 Leader 的時限。
-
syncLimit =5:LF 同步通信時限
集羣中 Leader 與 Follower 之間的最大響應時間單位,假如響應超過 syncLimit * tickTime,Leader 認爲 Follwer 死掉,從服務器列表中刪除 Follwer。
-
dataDir:數據文件目錄+數據持久化路徑
主要用於保存 Zookeeper 中的數據。
-
clientPort =2181:客戶端連接端口
監聽客戶端連接的端口。
-
server.A=B:C:D
A 是一個數字,表示這個是第幾號服務器;集羣模式下配置一個文件 myid,這個文件在 dataDir 目錄下,這個文件裏面有一個數據就是 A 的值,Zookeeper 啓動時讀取此文件,拿到裏面的數據與 zoo.cfg 裏面的配置信息比較從而判斷到底是哪個server。
B 是這個服務器的 ip 地址;
C 是這個服務器與集羣中的 Leader 服務器交換信息的端口;
D 是萬一集羣中的 Leader 服務器掛了,需要一個端口來重新進行選舉,選出一個新的 Leader,而這個端口就是用來執行選舉時服務器相互通信的端口。
3. Zookeeper 內部原理
3.1 選舉機制
-
半數機制
集羣中半數以上機器存活,集羣可用。所以 Zookeeper 適合安裝奇數臺服務器。
-
Zookeeper 雖然在配置文件中並沒有指定 Master 和 Slave。但是,Zookeeper 工作時,是有一個節點爲 Leader,其他則爲 Follower,Leader 是通過內部的選舉機制臨時產生的。
-
選舉過程例子
假設有五臺服務器組成的 Zookeeper 集羣,它們的 id 從1-5,同時它們都是最新啓動的,也就是沒有歷史數據,在存放數據量這一點上,都是一樣的。假設這些服務器依序啓動。
① 服務器 1 啓動,此時只有它一臺服務器啓動了,它發出去的報文沒有任何響應,所以它的選舉狀態一直是 LOOKING 狀態。② 服務器 2 啓動,它與最開始啓動的服務器 1 進行通信,互相交換自己的選舉結果,由於兩者都沒有歷史數據,所以 id 值較大的服務器 2 勝出,但是由於沒有達到超過半數以上的服務器都同意選舉它(這個例子中的半數以上是 3),所以服務器 1、2 還是繼續保持 LOOKING 狀態。
③ 服務器 3 啓動,根據前面的理論分析,服務器 3 成爲服務器 1、2、3 中的老大,而與上面不同的是,此時有三臺服務器選舉了它,所以它成爲了這次選舉的 Leader。
④ 服務器 4 啓動,根據前面的分析,理論上服務器4應該是服務器 1、2、3、4 中最大的,但是由於前面已經有半數以上的服務器選舉了服務器 3,所以它只能接收當小弟的命了。
⑤ 服務器 5 啓動,同 4 一樣當小弟。
3.2 節點類型
-
持久(Persistent)
客戶端和服務器端斷開連接後,創建的節點不刪除
-
短暫(Ephemeral)
客戶端和服務器端斷開連接後,創建的節點自己刪除
-
節點類型
① 持久化目錄節點客戶端與 Zookeeper 斷開連接後,該節點依舊存在。
② 持久化順序編號目錄節點
客戶端與 Zookeeper 斷開連接後,該節點依舊存在,只是 Zookeeper 給該節點名稱進行順序編號
③ 臨時目錄節點
客戶端與 Zookeeper 斷開連接後,該節點被刪除
④ 臨時順序編號目錄節點
客戶端與 Zookeeper 斷開連接後,該節點被刪除,只是 Zookeeper 給該節點名稱進行順序編號。
說明: 創建 znode 時設置順序標識,znode 名稱後會附加一個值,順序號是一個單調遞增的計數器,由父節點維護。
注意: 在分佈式系統中,順序號可以被用於爲所有的事件進行全局排序,這樣客戶端可以通過順序號推斷事件的順序。
3.3 Stat 結構體
-
czxid: 創建節點的事務 zxid
每次修改 ZooKeeper 狀態都會收到一個 zxid 形式的時間戳,也就是 ZooKeepe r事務 ID。
事務 ID 是 ZooKeeper 中所有修改總的次序。每個修改都有唯一的 zxid,若 zxid1 小於 zxid2,那麼 zxid1 在 zxid2 之前發生。 -
ctime: znode 被創建的毫秒數(從 1970 年開始)
-
mzxid: znode 最後更新的事務 zxid
-
mtime: znode 最後修改的毫秒數(從 1970 年開始)
-
pZxid: znode 最後更新的子節點 zxid
-
cversion : znode 子節點變化號,znode 子節點修改次數
-
dataversion: znode 數據變化號
-
aclVersion: znode 訪問控制列表的變化號
-
ephemeralOwner: 如果是臨時節點,這個是 znode 擁有者的 session id。如果不是臨時節點則是 0。
-
dataLength: znode 的數據長度
-
numChildren: znode 子節點數量
3.4 監聽器原理
-
監聽原理詳解:
① 首先要有一個 main() 線程
② 在 main 線程中創建 Zokeeper 客戶端,這時就會創建兩個線程,一個負責網絡連接通信(connet),一個負責監聽(listener) 。
③ 通過 connect 線程將註冊的監聽事件發送給 Zookeeper。
④ 在 Zookeeper 的註冊監聽器列表中將註冊的監聽事件添加到列表中。
⑤ Zookeeper 監聽到有數據或路徑變化,就會將這個消息發送給 listener 線程。
⑥ listener 線程內部調用了 process() 方法。 -
常見的監聽
① 監聽節點數據的變化
get -w path
② 監聽子節點增減的變化
ls -w path
3.5 寫數據流程
4. Zookeeper 實戰
4.1 客戶端命令行操作
- 啓動客戶端
zkCli.sh
- 顯示所有操作命令
help
- 查看當前 znode 中所包含的內容
ls /
- 查看當前節點詳細數據
ls -s /
- 分別創建 2 個普通節點
create /animals "dog"
create /animals/small "ant"
- 獲得節點的值
get /animals
get /animals/small
- 創建短暫節點
create -e /animals/big "elephant"
- 創建帶序號的節點
create -s /animals/middle "hourse"
- 修改節點數據值
set /animals/small "bug"
- 節點的值變化監聽
① 在 slave1 主機上註冊監聽 /animals 節點數據變化
get -w /animals
② 在 slave2 主機上修改 /animals 節點的數據
set /animals "cat"
③ 觀察 slave1 主機收到子節點變化的監聽
- 節點的子節點變化監聽(路徑變化)
① 在 slave1 主機上註冊監聽 /animals 節點的子節點變化
ls -w /animals
② 在 slave2 主機 /animals 節點上創建子節點
create /animals/mini "fly"
③ 觀察 slave1 主機收到子節點變化的監聽
- 刪除節點
delete /animals/big
- 遞歸刪除節點
deleteall /animals/mini
- 查看節點狀態
stat /animals
4.2 API 操作
4.3.1 IDEA 環境搭建
- 創建一個 Maven 工程
- 在 pom 文件中添加依賴
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.8.2</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.5.6</version>
</dependency>
</dependencies>
- 在項目的 src/main/resources 目錄下,新建一個文件,命名爲 “log4j.properties”,在文件中填入:
log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n
log4j.appender.logfile=org.apache.log4j.FileAppender
log4j.appender.logfile.File=target/spring.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n
4.3.2 創建 ZooKeeper 客戶端
package zookeeper;
import org.apache.zookeeper.*;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
public class TestZookeeper {
private String connectString = "master:2181,slave1:2181,slave2:2181";
private int sessionTimeout = 2000;
private ZooKeeper zkClient;
@Test
public void init() throws IOException {
zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
public void process(WatchedEvent watchedEvent) {
}
});
}
4.3.3 創建子節點
先將上面的 init() 方法前面的註解 @Test 改爲 @Before
// 創建子節點
@Test
public void createNode() throws Exception {
// 參數1:要創建的節點的路徑; 參數2:節點數據 ; 參數3:節點權限 ;參數4:節點的類型
String path = zkClient.create("/demo", "hello".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
System.out.println(path);
}
4.3.4 獲取子節點並監聽節點變化
// 獲取子節點並監聽節點變化
@Test
public void getChildrenAndWatch() throws Exception {
List<String> children = zkClient.getChildren("/", true);
for (String child : children) {
System.out.println(child);
}
// 延時阻塞
Thread.sleep(Long.MAX_VALUE);
}
@Before
public void init() throws IOException {
zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
public void process(WatchedEvent watchedEvent) {
List<String> children = null;
try {
children = zkClient.getChildren("/", true);
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (String child : children) {
System.out.println(child);
}
}
});
}
4.3.5 判斷 Znode 是否存在
// 判斷znode是否存在
@Test
public void exist() throws Exception {
Stat stat = zkClient.exists("/animals", false);
System.out.println(stat == null ? "not exist" : "exist");
}
4.3 監聽服務器節點動態上下線案例
-
需求
某分佈式系統中,主節點可以有多臺,可以動態上下線,任意一臺客戶端都能實時感知到主節點服務器的上下線。
-
需求分析
-
代碼實現
① 先在集羣上創建 /servers 節點
create /servers "servers"
② 服務器端向 Zookeeper 註冊代碼
package zookeeper;
import org.apache.zookeeper.*;
import java.io.IOException;
public class DistributeServer {
private String connectString = "master:2181,slave1:2181,slave2:2181";
private int sessionTimeout = 2000;
private ZooKeeper zkClient;
public static void main(String[] args) throws Exception {
args = new String[]{"slave1"};
DistributeServer server = new DistributeServer();
// 1.連接zookeeper集羣
server.getConnect();
// 2.註冊節點
server.register(args[0]);
// 3.業務邏輯處理
server.business();
}
private void getConnect() throws IOException {
zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
public void process(WatchedEvent event) {
}
});
}
private void register(String hostname) throws KeeperException, InterruptedException {
String path = zkClient.create("/servers/server", hostname.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(hostname + " is online");
}
private void business() throws InterruptedException {
Thread.sleep(Long.MAX_VALUE);
}
}
③ 客戶端代碼
package zookeeper;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class DistributeClient {
private String connectString = "master:2181,slave1:2181,slave2:2181";
private int sessionTimeout = 2000;
private ZooKeeper zkClient;
public static void main(String[] args) throws Exception {
DistributeClient client = new DistributeClient();
// 1.連接zookeeper集羣
client.getConnect();
// 2.註冊監聽
client.getChildren();
// 3.業務邏輯處理
client.business();
}
private void getConnect() throws IOException {
zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
public void process(WatchedEvent event) {
try {
getChildren();
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
private void getChildren() throws KeeperException, InterruptedException {
List<String> children = zkClient.getChildren("/servers", true);
// 存儲服務器節點主機名稱集合
ArrayList<String> hosts = new ArrayList<String>();
for (String child : children) {
byte[] data = zkClient.getData("/servers/" + child, false, null);
hosts.add(new String(data));
}
System.out.println(hosts);
}
private void business() throws InterruptedException {
Thread.sleep(Long.MAX_VALUE);
}
}