大數據技術之Zookeeper
第1章 Zookeeper入門
1.1 概述
-
Zookeeper是一個開源的分佈式的,爲分佈式應用提供協調服務的Apache項目。
-
Zookeeper從設計模式角度來理解:是一個基於觀察者模式設計的分佈式服務管理框架,它負責存儲和管理大家都關心的數據,然後接受觀察者的註冊,一旦這些數據的狀態發生變化,Zookeeper就將負責通知已經在Zookeeper上註冊的那些觀察者做出相應的反應。
-
總結:Zookeeper=文件系統+通知機制
1.2 特點
- Zookeeper:一個領導者(Leader),多個跟隨者(Follower)組成的集羣。
- 集羣中只要有半數以上節點存活,Zookeeper集羣就能正常服務 。
- 全局數據一致:每個Server保存一份相同的數據副本,Client無論連接到哪個Server,數據都是一致的 。
- 更新請求順序進行,來自同一個Client的更新請求按其發送順序依次執行。
- 數據更新原子性,一次性數據更新要麼成功,要麼失敗。
- 實時性,在一定時間範圍內,Client能讀到最新數據。
1.3 數據結構
Zookeeper數據模型的結構與Unix文件系統很類似,整體上可以看作是一棵樹,每個節點稱作一個ZNode。每個ZNode默認能夠儲存1MB的數據,每個ZNode都可以通過其路徑唯一標識。
1.4 應用場景
- 提供的服務包括:統一命名服務、統一配置管理、統一集羣管理、服務器節點動態上下線、軟負載均衡等。
-
統一命名服務:
在分佈式環境下,經常需要對應用/服務進行統一命名,便於識別。
-
統一配置管理:
分佈式環境下,配置文件同步非常常見
一般要求一個集羣中,所有節點的配置信息是一致的,比如 Kafka 集羣
對配置文件修改後,希望能夠快速同步到各個節點上
配置管理可交由ZooKeeper實現
可將配置信息寫入ZooKeeper上的一個Znode
各個客戶端服務器監聽這個Znode
一旦Znode中的數據被修改,ZooKeeper將通知各個客戶端服務器
-
服務器動態上下線
客戶端能實時洞察到服務器上下線的變化
-
軟負載均衡
在Zookeeper中記錄每臺服務器的訪問數
讓訪問數最少的服務器去處理最新的客戶端請求
1.5 下載地址
第2章 Zookeeper安裝
2.1 本地模式安裝部署
- 安裝前準備
安裝Jdk
拷貝Zookeeper安裝包到Linux系統下
解壓到指定目錄
[zhangyong@hadoop101 zookeeper-3.4.10]$ tar -zxvf /opt/software/zookeeper-3.4.10.tar.gz -C /opt/module/
2.配置修改
(1)將/opt/module/zookeeper-3.4.10/conf這個路徑下的zoo_sample.cfg修改爲zoo.cfg;
[zhangyong@hadoop101 conf]$ mv zoo_sample.cfg zoo.cfg
(2)打開zoo.cfg文件,修改dataDir路徑:
[zhangyong@hadoop101 conf]$ vim zoo.cfg
修改如下內容:
dataDir=/opt/module/zookeeper-3.4.10/zkData
(3)在/opt/module/zookeeper-3.4.10/這個目錄上創建zkData文件夾
[zhangyong@hadoop101 zookeeper-3.4.10]$ mkdir zkData
3.操作Zookeeper
(1)啓動Zookeeper
[zhangyong@hadoop101 zookeeper-3.4.10]$ bin/zkServer.sh start
(2)查看進程是否啓動
[zhangyong@hadoop101 zookeeper-3.4.10]$ jps
1368 QuorumPeerMain
1390 Jps
(3)查看狀態:
[zhangyong@hadoop101 zookeeper-3.4.10]$ bin/zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /opt/module/zookeeper-3.4.10/bin/../conf/zoo.cfg
Mode: standalone
(4)啓動客戶端:
[zhangyong@hadoop101 zookeeper-3.4.10]$ bin/zkCli.sh
(5)退出客戶端:
[zk: localhost:2181(CONNECTED) 0] quit
(6)停止Zookeeper
[zhangyong@hadoop101 zookeeper-3.4.10]$ bin/zkServer.sh stop
2.2 配置參數解讀
- 配置文件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:客戶端連接端口
監聽客戶端連接的端口。
第3章 Zookeeper實戰
3.1 分佈式安裝部署
- 集羣規劃
在hadoop101、hadoop102和hadoop103三個節點上部署Zookeeper。
- 配置服務器編號
(1)在/opt/module/zookeeper-3.4.10/zkData目錄下創建一個myid的文件
[zhangyong@hadoop101 zkData]$ touch myid
(2)編輯myid文件
[zhangyong@hadoop101 zkData]$ vi myid
在文件中添加與server對應的編號:
1
- 配置zoo.cfg文件
(1)打開zoo.cfg文件
[zhangyong@hadoop101 conf]$ vim zoo.cfg
修改數據存儲路徑配置
dataDir=/opt/module/zookeeper-3.4.10/zkData
##增加如下配置
server.1=hadoop101:2888:3888
server.2=hadoop102:2888:3888
server.3=hadoop103:2888:3888
-
配置/opt/module/zookeeper-3.4.10/bin下的zkEnv.sh文件
(1)配置JDK環境變量(不是必須的)
export JAVA_HOME=/opt/module/jdk1.8.0_181
(2)在57行配置日誌文件地址:
ZOO_LOG_DIR="/opt/module/zookeeper-3.4.10/logs"
-
把配置好的hadoop101分發到hadoop102、hadoop103中,並且更改myid的編號分別爲2、3
[zhangyong@hadoop101 module]$ xsync zookeeper-3.4.10/ [zhangyong@hadoop102 zkData]$ echo 2 > myid [zhangyong@hadoop103 zkData]$ echo 3 > myid
-
配置參數解讀
server.A=B:C:D。
A是一個數字,表示這個是第幾號服務器;
集羣模式下配置一個文件myid,這個文件在dataDir目錄下,這個文件裏面有一個數據就是A的值,Zookeeper啓動時讀取此文件,拿到裏面的數據與zoo.cfg裏面的配置信息比較從而判斷到底是哪個server。
B是這個服務器的地址;
C是這個服務器Follower與集羣中的Leader服務器交換信息的端口;
D是萬一集羣中的Leader服務器掛了,需要一個端口來重新進行選舉,選出一個新的Leader,而這個端口就是用來執行選舉時服務器相互通信的端口。
-
集羣操作
(1)分別啓動Zookeeper:
先啓動一臺並查看狀態:
[zhangyong@hadoop101 zookeeper-3.4.10]$ bin/zkServer.sh start ZooKeeper JMX enabled by default Using config: /opt/module/zookeeper-3.4.10/bin/../conf/zoo.cfg Starting zookeeper ... STARTED [zhangyong@hadoop101 zookeeper-3.4.10]$ bin/zkServer.sh status ZooKeeper JMX enabled by default Using config: /opt/module/zookeeper-3.4.10/bin/../conf/zoo.cfg Error contacting service. It is probably not running. [zhangyong@hadoop101 zookeeper-3.4.10]$ jps 1520 QuorumPeerMain 1576 Jps
由上面結果顯示可知:zookeeper啓動起來了,但是狀態錯誤,因爲我們說過集羣的zookeeper必須啓動臺數的一般以上纔可以。我們繼續啓動其他機子再來查看:
(2)啓動其他的zookeeper:
[zhangyong@hadoop102 zookeeper-3.4.10]$ bin/zkServer.sh start
ZooKeeper JMX enabled by default
Using config: /opt/module/zookeeper-3.4.10/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
[zhangyong@hadoop102 zookeeper-3.4.10]$ bin/zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /opt/module/zookeeper-3.4.10/bin/../conf/zoo.cfg
Mode: leader
[zhangyong@hadoop102 zookeeper-3.4.10]$ jps
1607 QuorumPeerMain
1676 Jps
[zhangyong@hadoop103 zookeeper-3.4.10]$ bin/zkServer.sh start
ZooKeeper JMX enabled by default
Using config: /opt/module/zookeeper-3.4.10/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
[zhangyong@hadoop103 zookeeper-3.4.10]$
[zhangyong@hadoop103 zookeeper-3.4.10]$ bin/zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /opt/module/zookeeper-3.4.10/bin/../conf/zoo.cfg
Mode: follower
[zhangyong@hadoop103 zookeeper-3.4.10]$ jps
1858 Jps
1795 QuorumPeerMain
由此:zookeeper集羣啓動完成。
3.2 常用的客戶端命令行操作(增刪改查)
命令基本語法 | 功能描述 |
---|---|
help | 顯示所有操作命令 |
ls path [watch] | 使用 ls 命令來查看當前znode中所包含的內容 |
ls2 path [watch] | 查看當前節點數據並能看到更新次數等數據 |
create | 普通創建 -s 含有序列 -e 臨時(重啓或者超時消失) |
get path [watch] | 獲得節點的值 |
set | 設置節點的具體值 |
stat | 查看節點狀態 |
delete | 刪除節點 |
rmr | 遞歸刪除節點 |
- 啓動客戶端
[zhangyong@hadoop101 zookeeper-3.4.10]$ bin/zkCli.sh
2.顯示所有操作命令
[zk: localhost:2181(CONNECTED) 1] help
3.查看當前znode中所包含的內容
[zk: localhost:2181(CONNECTED) 2] ls /
[zookeeper]
4.查看當前節點詳細數據
[zk: localhost:2181(CONNECTED) 3] ls2 /
[zookeeper]
cZxid = 0x0
ctime = Thu Jan 01 08:00:00 CST 1970
mZxid = 0x0
mtime = Thu Jan 01 08:00:00 CST 1970
pZxid = 0x0
cversion = -1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
5.分別創建2個普通節點
[zk: localhost:2181(CONNECTED) 3] create /zhangyong "hahaha"
Created /zhangyong
[zk: localhost:2181(CONNECTED) 4] create /zhangyong/zhangsan "lisi"
Created /zhangyong/zhangsan
6.獲得節點的值
[zk: localhost:2181(CONNECTED) 5] get /zhangyong
hahaha
cZxid = 0x100000003
ctime = Wed Aug 29 00:03:23 CST 2018
mZxid = 0x100000003
mtime = Wed Aug 29 00:03:23 CST 2018
pZxid = 0x100000004
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 7
numChildren = 1
[zk: localhost:2181(CONNECTED) 6] get /zhangyong/zhangsan
lisi
cZxid = 0x100000004
ctime = Wed Aug 29 00:04:35 CST 2018
mZxid = 0x100000004
mtime = Wed Aug 29 00:04:35 CST 2018
pZxid = 0x100000004
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 6
numChildren = 0
7.創建短暫節點
[zk: localhost:2181(CONNECTED) 7] create -e /zhangyong/zhangsan "zhang"
Created /zhangyong/zhangsan
(1)在當前客戶端是能查看到的
[zk: localhost:2181(CONNECTED) 3] ls /zhangyong
[zhangsan, zhangsan]
(2)退出當前客戶端然後再重啓客戶端
[zk: localhost:2181(CONNECTED) 12] quit
[zhangyong@hadoop101 zookeeper-3.4.10]$ bin/zkCli.sh
(3)再次查看根目錄下短暫節點已經刪除
[zk: localhost:2181(CONNECTED) 0] ls /zhangyong
[zhangyong]
8.創建帶序號的節點
(1)先創建一個普通的根節點/zhangyong/weiguo
[zk: localhost:2181(CONNECTED) 1] create /zhangyong/weiguo "caoyu"
Created /zhangyong/weiguo
(2)創建帶序號的節點
[zk: localhost:2181(CONNECTED) 2] create -s /sanguo/weiguo/xiaoqiao "jinlian"
Created /sanguo/weiguo/xiaoqiao0000000000
[zk: localhost:2181(CONNECTED) 3] create -s /sanguo/weiguo/daqiao "jinlian"
Created /sanguo/weiguo/daqiao0000000001
[zk: localhost:2181(CONNECTED) 4] create -s /sanguo/weiguo/diaocan "jinlian"
Created /sanguo/weiguo/diaocan0000000002
如果原來沒有序號節點,序號從0開始依次遞增。如果原節點下已有2個節點,則再排序時從2開始,以此類推。
9.修改節點數據值
[zk: localhost:2181(CONNECTED) 6] set /sanguo/weiguo "simayi"
10.節點的值變化監聽
(1)在hadoop104主機上註冊監聽/sanguo節點數據變化
[zk: localhost:2181(CONNECTED) 26] [zk: localhost:2181(CONNECTED) 8] get /sanguo watch
(2)在hadoop103主機上修改/sanguo節點的數據
[zk: localhost:2181(CONNECTED) 1] set /sanguo "xisi"
(3)觀察hadoop103主機收到數據變化的監聽
WATCHER::
WatchedEvent state:SyncConnected type:NodeDataChanged path:/sanguo
11.節點的子節點變化監聽(路徑變化)
(1)在hadoop103主機上註冊監聽/sanguo節點的子節點變化
[zk: localhost:2181(CONNECTED) 1] ls /sanguo watch
[aa0000000001, server101]
(2)在hadoop102主機/sanguo節點上創建子節點
[zk: localhost:2181(CONNECTED) 2] create /sanguo/jin "simayi"
Created /sanguo/jin
(3)觀察hadoop103主機收到子節點變化的監聽
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/sanguo
12.刪除節點
[zk: localhost:2181(CONNECTED) 4] delete /sanguo/jin
13.遞歸刪除節點
[zk: localhost:2181(CONNECTED) 15] rmr /sanguo/shuguo
14.查看節點狀態
[zk: localhost:2181(CONNECTED) 17] stat /sanguo
cZxid = 0x100000003
ctime = Wed Aug 29 00:03:23 CST 2018
mZxid = 0x100000011
mtime = Wed Aug 29 00:21:23 CST 2018
pZxid = 0x100000014
cversion = 9
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 4
numChildren = 1
3.3 API應用
-
IDEA環境搭建一個Maven工程
-
添加pom文件
<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.8.2</version> </dependency> <!-- zookeeper3.4.10 --> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.10</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>compile</scope> </dependency> </dependencies>
-
拷貝log4j.properties文件到項目根目錄
- 需要在項目的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
-
編寫Java代碼
/** * @Author zhangyong * @Date 2020/3/18 0:18 * @Version 1.0 */ public class zkCli { private static String connectString = "hadoop101:2181,hadoop102:2181,hadoop103:2181"; private static int sessionTimeout = 2000; private ZooKeeper zkClient = null; /** * 創建ZooKeeper客戶端 * @throws Exception */ public void init() throws Exception { zkClient = new ZooKeeper (connectString, sessionTimeout, new Watcher () { @Override public void process(WatchedEvent event) { // 收到事件通知後的回調函數(用戶的業務邏輯) System.out.println (event.getType () + "--" + event.getPath ()); // 再次啓動監聽 try { zkClient.getChildren ("/", true); } catch (Exception e) { e.printStackTrace (); } } }); } /** * 創建子節點 * @throws Exception */ @Test public void create() throws Exception { // 參數1:要創建的節點的路徑; 參數2:節點數據 ; 參數3:節點權限 ;參數4:節點的類型 String nodeCreated = zkClient.create ("/zhangyong", "zhangrui".getBytes (), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); } /** * 獲取子節點並監聽節點變化 * @throws Exception */ @Test public void getChildren() throws Exception { List<String> children = zkClient.getChildren ("/", true); for (String child : children) { System.out.println (child); } // 延時阻塞 Thread.sleep (Long.MAX_VALUE); } /** * 判斷znode是否存在 * @throws Exception */ @Test public void exist() throws Exception { Stat stat = zkClient.exists("/zhangyong", false); System.out.println(stat == null ? "not exist" : "exist"); } }
3.4 監聽服務器節點動態上下線案例(擴展)
- 需求
-
某分佈式系統中,主節點可以有多臺,可以動態上下線,任意一臺客戶端都能實時感知到主節點服務器的上下線。
-
客戶端能實時洞察到服務器上下線的變化
- 具體實現
(1)先在集羣上創建/servers節點
[zk: localhost:2181(CONNECTED) 10] create /servers "servers"
Created /servers
(2)服務器端向Zookeeper註冊代碼
package com.zhangyong.zookeeper;
/**
* @Author zhangyong
* @Date 2020/3/18 0:18
* @Version 1.0
*/
public class DistributeServer {
private static String connectString = "hadoop101:2181,hadoop102:2181,hadoop103:2181";
private static int sessionTimeout = 2000;
private ZooKeeper zk = null;
private String parentNode = "/servers";
// 創建到zk的客戶端連接
public void getConnect() throws IOException {
zk = new ZooKeeper (connectString, sessionTimeout, new Watcher () {
@Override
public void process(WatchedEvent event) {
}
});
}
// 註冊服務器
public void registServer(String hostname) throws Exception {
String create = zk.create (parentNode + "/server", hostname.getBytes (), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println (hostname + " is online " + create);
}
// 業務功能
public void business(String hostname) throws Exception {
System.out.println (hostname + " is working ...");
Thread.sleep (Long.MAX_VALUE);
}
public static void main(String[] args) throws Exception {
// 1獲取zk連接
DistributeServer server = new DistributeServer ();
server.getConnect ();
// 2 利用zk連接註冊服務器信息
server.registServer (args[0]);
// 3 啓動業務功能
server.business (args[0]);
}
}
(2)客戶端代碼
/**
* @Author zhangyong
* @Date 2020/3/18 0:18
* @Version 1.0
*/
public class DistributeClient {
private static String connectString = "hadoop101:2181,hadoop102:2181,hadoop103:2181";
private static int sessionTimeout = 2000;
private ZooKeeper zk = null;
private String parentNode = "/servers";
// 創建到zk的客戶端連接
public void getConnect() throws IOException {
zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent event) {
// 再次啓動監聽
try {
getServerList();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
// 獲取服務器列表信息
public void getServerList() throws Exception {
// 1獲取服務器子節點信息,並且對父節點進行監聽
List<String> children = zk.getChildren(parentNode, true);
// 2存儲服務器信息列表
ArrayList<String> servers = new ArrayList<>();
// 3遍歷所有節點,獲取節點中的主機名稱信息
for (String child : children) {
byte[] data = zk.getData(parentNode + "/" + child, false, null);
servers.add(new String(data));
}
// 4打印服務器列表信息
System.out.println(servers);
}
// 業務功能
public void business() throws Exception{
System.out.println("client is working ...");
Thread.sleep(Long.MAX_VALUE);
}
public static void main(String[] args) throws Exception {
// 1獲取zk連接
DistributeClient client = new DistributeClient();
client.getConnect();
// 2獲取servers的子節點信息,從中獲取服務器信息列表
client.getServerList();
// 3業務進程啓動
client.business();
}
}
第4章 Zookeeper內部原理
4.1 節點類型
-
持久(Persistent):客戶端和服務器端斷開連接後,創建的節點不刪除
-
短暫(Ephemeral):客戶端和服務器端斷開連接後,創建的節點自己刪除
-
說明:創建znode時設置順序標識,znode名稱後會附加一個值,順序號是一個單調遞增的計數器,由父節點維護。
-
注意:在分佈式系統中,順序號可以被用於爲所有的事件進行全局排序,這樣客戶端可以通過順序號推斷事件的順序
(1)持久化目錄節點
- 客戶端與Zookeeper斷開連接後,該節點依舊存在
(2)持久化順序編號目錄節點
- 客戶端與Zookeeper斷開連接後,該節點依舊存在,只是Zookeeper給該節點名稱進行順序編號
(3)臨時目錄節點
- 客戶端與Zookeeper斷開連接後,該節點被刪除
(4)臨時順序編號目錄節點
- 客戶端與Zookeeper斷開連接後,該節點被刪除,只是Zookeeper給該節點名稱進行順序編號。
4.2 Stat結構體
-
czxid-創建節點的事務zxid
-
每次修改ZooKeeper狀態都會收到一個zxid形式的時間戳,也就是ZooKeeper事務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子節點數量
4.3 監聽器原理
-
監聽原理詳解:
- 首先要有一個main()線程
- 在main線程中創建Zookeeper客戶端,這時就會創建兩個線程,一個負責網絡連接通信(connet),一個負責監聽(listener)。
- 通過connect線程將註冊的監聽事件發送給Zookeeper。
- 在Zookeeper的註冊監聽器列表中將註冊的監聽事件添加到列表中。
- Zookeeper監聽到有數據或路徑變化,就會將這個消息發送給listener線程。
- listener線程內部調用了process()方法。
-
常見的監聽
- 監聽節點數據的變化 get path [watch]
- 監聽子節點增減的變化 ls path [watch]
4.4 Paxos算法
-
Paxos算法一種基於消息傳遞且具有高度容錯特性的一致性算法。
-
分佈式系統中的節點通信存在兩種模型:共享內存(Shared memory)和消息傳遞(Messages passing)。基於消息傳遞通信模型的分佈式系統,不可避免的會發生以下錯誤:進程可能會慢、被殺死或者重啓,消息可能會延遲、丟失、重複,在基礎 Paxos 場景中,先不考慮可能出現消息篡改即拜占庭錯誤的情況。Paxos 算法解決的問題是在一個可能發生上述異常的分佈式系統中如何就某個值達成一致,保證不論發生以上任何異常,都不會破壞決議的一致性。
-
在一個Paxos系統中,首先將所有節點劃分爲Proposers,Acceptors,和Learners。(注意:每個節點都可以身兼數職)。
-
一個完整的Paxos算法流程分爲三個階段:
Prepare階段
Proposer向Acceptors發出Prepare請求Promise(承諾)。
Acceptors針對收到的Prepare請求進行Promise承諾。
Accept階段
Proposer收到多數Acceptors承諾的Promise後,向Acceptors發出Propose請求
Acceptors針對收到的Propose請求進行Accept處理。
Learn階段:Proposer將形成的決議發送給所有Learners。
-
Paxos算法流程中的每條消息描述如下:
-
Prepare: Proposer生成全局唯一且遞增的Proposal ID (可使用時間戳加Server ID),向所有Acceptors發送Prepare請求,這裏無需攜帶提案內容,只攜帶Proposal ID即可。
-
Promise: Acceptors收到Prepare請求後,做出“兩個承諾,一個應答”。
兩個承諾:
a. 不再接受Proposal ID小於等於(注意:這裏是<= )當前請求的Prepare請求。
b. 不再接受Proposal ID小於(注意:這裏是< )當前請求的Propose請求。
一個應答:
c. 不違背以前做出的承諾下,回覆已經Accept過的提案中Proposal ID最大的那個提案的Value和Proposal ID,沒有則返回空值。
-
Propose: Proposer 收到多數Acceptors的Promise應答後,從應答中選擇Proposal ID最大的提案的Value,作爲本次要發起的提案。如果所有應答的提案Value均爲空值,則可以自己隨意決定提案Value。然後攜帶當前Proposal ID,向所有Acceptors發送Propose請求。
-
Accept: Acceptor收到Propose請求後,在不違背自己之前做出的承諾下,接受並持久化當前Proposal ID和提案Value。
-
Learn: Proposer收到多數Acceptors的Accept後,決議形成,將形成的決議發送給所有Learners。
- Paxos算法缺陷:在網絡複雜的情況下,一個應用Paxos算法的分佈式系統,可能很久無法收斂,甚至陷入活鎖的情況。
4.5 選舉機制
-
半數機制:集羣中半數以上機器存活,集羣可用。所以Zookeeper適合安裝奇數臺服務器。
-
Zookeeper雖然在配置文件中並沒有指定Master和Slave。但是,Zookeeper工作時,是有一個節點爲Leader,其他則爲Follower,Leader是通過內部的選舉機制臨時產生的。
-
以一個簡單的例子來說明整個選舉的過程。
-
假設有五臺服務器組成的Zookeeper集羣,它們的id從1-5,同時它們都是最新啓動的,也就是沒有歷史數據,在存放數據量這一點上,都是一樣的。假設這些服務器依序啓動,來看看會發生什麼.
a. 服務器1啓動,發起一次選舉。服務器1投自己一票。此時服務器1票數一票,不夠半數以上(3票),選舉無法完成,服務器1狀態保持爲LOOKING;
b. 服務器2啓動,再發起一次選舉。服務器1和2分別投自己一票並交換選票信息:此時服務器1發現服務器2的ID比自己目前投票推舉的(服務器1)大,更改選票爲推舉服務器2。此時服務器1票數0票,服務器2票數2票,沒有半數以上結果,選舉無法完成,服務器1,2狀態保持LOOKING
c. 服務器3啓動,發起一次選舉。此時服務器1和2都會更改選票爲服務器3。此次投票結果:服務器1爲0票,服務器2爲0票,服務器3爲3票。此時服務器3的票數已經超過半數,服務器3當選Leader。服務器1,2更改狀態爲FOLLOWING,服務器3更改狀態爲LEADING;
d. 服務器4啓動,發起一次選舉。此時服務器1,2,3已經不是LOOKING狀態,不會更改選票信息。交換選票信息結果:服務器3爲3票,服務器4爲1票。此時服務器4服從多數,更改選票信息爲服務器3,並更改狀態爲FOLLOWING;e. 服務器5啓動,同4一樣當小弟。
4.6 寫數據流程
- Client 向 ZooKeeper 的 Server1 上寫數據,發送一個寫請求。
- 如果Server1不是Leader,那麼Server1 會把接受到的請求進一步轉發給Leader,因爲每個ZooKeeper的Server裏面有一個是Leader。這個Leader 會將寫請求廣播給各個Server,比如Server1和Server2,各個Server會將該寫請求加入待寫隊列,並向Leader發送成功信息。
- 當Leader收到半數以上 Server 的成功信息,說明該寫操作可以執行。Leader會向各個Server 發送提交信息,各個Server收到信息後會落實隊列裏的寫請求,此時寫成功。
- Server1會進一步通知 Client 數據寫成功了,這時就認爲整個寫操作成功。