zookeeper
原始碼分析系列文章:
- Zookeeper原始碼分析(一) —– 原始碼執行環境搭建
- Zookeeper原始碼分析(二) —– zookeeper日誌
- Zookeeper原始碼分析(三) —– 單機模式(standalone)執行
原創部落格,純手敲,轉載請註明出處,謝謝!
如你所知,zk的執行方式有兩種,獨立模式和複製模式。很顯然複製模式是用來搭建zk叢集的,因此我把複製模式稱為叢集模式。在之前的文章中我們已經對獨立模式下執行zk的原始碼進行相關分析,接下來我們一起來研究研究Zk叢集模式下的原始碼。
叢集模式下的除錯不像獨立模式那麼簡單,也許你可能會問,那是否需要多臺物理機來搭建一個zk叢集呢?其實也不需要,單臺物理機也是可以模擬叢集執行的。因此,下文我們將按照以下目錄開展討論:
一、zk叢集搭建及相關配置
zk配置叢集其實非常簡單,在上篇部落格中講到,zk在解析配置檔案時會判斷你配置檔案中是否有類似server.
的配置項,如果沒有類似server.
的配置項,則預設以獨立模式執行zk。相反,叢集模式下就要求你進行相應的配置了。下面將一步一步對搭建環境進行講解:
- 1、在zk的conf目錄中增加3個配置檔案,名字分別為
zoo1.cfg
、zoo2.cfg
和zoo3.cfg
其內容分別如下:
zoo1.cfg
tickTime=200000
initLimit=10
syncLimit=5
dataDir=E:\resources\Zookeeper\zookeeper-3.4.11\conf\data\1
dataLogDir=E:\resources\Zookeeper\zookeeper-3.4.11\conf\log\1
maxClientCnxns=2
# 伺服器監聽客戶端連線的埠
clientPort=2181
server.1=127.0.0.1:2887:3887
server.2=127.0.0.1:2888:3888
server.3=127.0.0.1:2889:3889
複製程式碼
zoo2.cfg
tickTime=200000
initLimit=10
syncLimit=5
dataDir=E:\resources\Zookeeper\zookeeper-3.4.11\conf\data\2
dataLogDir=E:\resources\Zookeeper\zookeeper-3.4.11\conf\log\2
maxClientCnxns=2
# 伺服器監聽客戶端連線的埠
clientPort=2182
server.1=127.0.0.1:2887:3887
server.2=127.0.0.1:2888:3888
server.3=127.0.0.1:2889:3889
複製程式碼
zoo3.cfg
tickTime=200000
initLimit=10
syncLimit=5
dataDir=E:\resources\Zookeeper\zookeeper-3.4.11\conf\data\3
dataLogDir=E:\resources\Zookeeper\zookeeper-3.4.11\conf\log\3
maxClientCnxns=2
# 伺服器監聽客戶端連線的埠
clientPort=2183
server.1=127.0.0.1:2887:3887
server.2=127.0.0.1:2888:3888
server.3=127.0.0.1:2889:3889
複製程式碼
上面相關通用的配置項在此處我就不做一一解釋,相關含義在上篇文章中都有提到。下面我們重點關注下clientPort
屬性和server.x
屬性。
clientPort
代表伺服器監聽客戶端連線的埠,換句話說就是客戶端連線到伺服器的那個埠。該屬性的預設配置一般都是2181
,那為什麼我們這裡要寫成2181
,2182
,2183
呢?其實原因很簡單,因為我們的叢集式搭建在單臺物理機上面,為了防止埠衝突,我們設定3臺zk伺服器分別監聽不同的埠。
至於server.x
屬性,用於配置參與叢集的每臺伺服器的地址和埠號。其格式為:
server.x addressIP:port1:port2
其中x
表示zk節點的唯一編號,也就是我們常說的sid的值,下面講到zk選舉的時候將會進一步講解。你可能會很好奇port1
和port2
之間有什麼區別,在zk中,port1
表示fllowers
連線到leader
的埠,port2
表示當前結點參與選舉的埠。之所以要這麼設計,其實我覺得在ZAB
協議中,當客戶端發出的寫操作在伺服器端執行完畢時,leader
節點必須將狀態同步給所有的fllowers
,leader
和fllowers
之間需要進行通訊嘛!另外一種是所有節點進行快速選舉時,各個節點之間需要進行投票,投票選出完一個leader
節點之後需要通知其他節點。所以說,明白埠含義即可,它們就是區別作用罷了。
- 2、建立3個
myid
檔案
zk在叢集模式下執行時會讀取位於dataDir
目錄下的myid
檔案,如果沒有找到,則會報錯。因此,下面我們將分別在對應的dataDir
下新建myid
檔案,該檔案的內容填寫當前伺服器的編號,也就是我們上面說到的server.x
中的x
值。
E:\resources\Zookeeper\zookeeper-3.4.11\conf\data\1
下建立該檔案,檔案內容為序號1
E:\resources\Zookeeper\zookeeper-3.4.11\conf\data\2
下建立該檔案,檔案內容為序號2
E:\resources\Zookeeper\zookeeper-3.4.11\conf\data\3
下建立該檔案,檔案內容為序號3
- 3、分別採用不同的配置檔案執行
QuorumPeerMain
的main()
方法即可。配置檔案路徑可以這樣傳給eclipse
,如下圖:
上面講的內容似乎和原始碼打不上邊,嗯,彆著急,下面就講原始碼。
首先我們看看zk是如何解析server.x
標籤的,進入QuorumPeerConfig
類的parseProperties()
方法,你將看到如下程式碼片段:
// 判斷屬性是否以server.開始
if (key.startsWith("server.")) {
int dot = key.indexOf(`.`);
// 獲取sid的值,也就是我們server.x中的x值
long sid = Long.parseLong(key.substring(dot + 1));
// 將配置值拆分為陣列,格式為[addressIP,port1,port2]
String parts[] = splitWithLeadingHostname(value);
if ((parts.length != 2) && (parts.length != 3) && (parts.length != 4)) {
LOG.error(value + " does not have the form host:port or host:port:port "
+ " or host:port:port:type");
}
// 代表當前結點的型別,可以是觀察者型別(不需要參與投票),也可以是PARTICIPANT(表示該節點後期可能成為follower和leader)
LearnerType type = null;
String hostname = parts[0];
Integer port = Integer.parseInt(parts[1]);
Integer electionPort = null;
if (parts.length > 2) {
electionPort = Integer.parseInt(parts[2]);
}
}
複製程式碼
上面原始碼將會根據你的配置解析每一個server
配置,原始碼也不是很複雜,接下來我們將看看zk如何讀取dataDir
目錄下的myid
檔案,繼續在QuorumPeerConfig
的parseProperties()
方法中,找到如下程式碼片段:
File myIdFile = new File(dataDir, "myid");
// 必須在快照目錄下建立myid檔案,否則報錯
if (!myIdFile.exists()) {
throw new IllegalArgumentException(myIdFile.toString() + " file is missing");
}
// 讀取myid的值
BufferedReader br = new BufferedReader(new FileReader(myIdFile));
String myIdString;
try {
myIdString = br.readLine();
} finally {
br.close();// 注意,優秀的人都不會丟三落四,對於開啟的各種io流,不用的時候記得關閉,不要浪費資源
}
複製程式碼
二、zk叢集模式下的初始化
在zk中,無論是獨立模式執行還是複製模式執行,其初始化的步驟都可以歸為:
- 1、解析配置檔案
- 2、初始化執行伺服器
對於配置檔案的解析,我們在上一篇文章和本文上節已做出相關分析。我們重點看下zk叢集模式執行的相關原始碼,讓我們進入QuormPeerMain
類的runFromConfig()
方法,原始碼如下:
/**
* 載入配置執行伺服器
* @param config
* @throws IOException
*/
public void runFromConfig(QuorumPeerConfig config) throws IOException {
LOG.info("Starting quorum peer");
try {
// 建立一個ServerCnxnFactory,預設為NIOServerCnxnFactory
ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory();
// 對ServerCnxnFactory進行相關配置
cnxnFactory.configure(config.getClientPortAddress(), config.getMaxClientCnxns());
// 初始化QuorumPeer,代表伺服器節點server執行時的各種資訊,如節點狀態state,哪些伺服器server參與競選了,我們 可以將它理解為叢集模式下執行的容器
quorumPeer = getQuorumPeer();
// 設定參與競選的所有伺服器
quorumPeer.setQuorumPeers(config.getServers());
// 設定事務日誌和資料快照工廠
quorumPeer.setTxnFactory(
new FileTxnSnapLog(new File(config.getDataDir()), new File(config.getDataLogDir())));
// 設定選舉的演算法
quorumPeer.setElectionType(config.getElectionAlg());
// 設定當前伺服器的id,也就是在data目錄下的myid檔案
quorumPeer.setMyid(config.getServerId());
// 設定心跳時間
quorumPeer.setTickTime(config.getTickTime());
// 設定允許follower同步和連線到leader的時間總量,以ticket為單位
quorumPeer.setInitLimit(config.getInitLimit());
// 設定follower與leader之間同步的時間量
quorumPeer.setSyncLimit(config.getSyncLimit());
// 當設定為true時,ZooKeeper伺服器將偵聽來自所有可用IP地址的對等端的連線,而不僅僅是在配置檔案的伺服器列表中配置的地址(即叢集中配置的server.1,server.2。。。。)。 它會影響處理ZAB協議和Fast Leader Election協議的連線。 預設值為false
quorumPeer.setQuorumListenOnAllIPs(config.getQuorumListenOnAllIPs());
// 設定工廠,預設是NIO工廠
quorumPeer.setCnxnFactory(cnxnFactory);
// 設定叢集數量驗證器,預設為半數原則
quorumPeer.setQuorumVerifier(config.getQuorumVerifier());
// 設定客戶端連線的伺服器ip地址
quorumPeer.setClientPortAddress(config.getClientPortAddress());
// 設定最小Session過期時間,預設是2*ticket
quorumPeer.setMinSessionTimeout(config.getMinSessionTimeout());
// 設定最大Session過期時間,預設是20*ticket
quorumPeer.setMaxSessionTimeout(config.getMaxSessionTimeout());
// 設定zkDataBase
quorumPeer.setZKDatabase(new ZKDatabase(quorumPeer.getTxnFactory()));
quorumPeer.setLearnerType(config.getPeerType());
quorumPeer.setSyncEnabled(config.getSyncEnabled());
// 設定NIO處理連結的執行緒數
quorumPeer.setQuorumCnxnThreadsSize(config.quorumCnxnThreadsSize);
quorumPeer.start();
quorumPeer.join();
} catch (InterruptedException e) {
// warn, but generally this is ok
LOG.warn("Quorum Peer interrupted", e);
}
}
複製程式碼
在處理客戶端請求方面,叢集模式和獨立模式都是使用ServerCnxnFactory
的相關子類實現,預設採用基於NIO的NIOServerCnxnFactory
,對於QuormPeer
類,你可以把它想象成一個容器或者上下文,它包含著叢集模式下當前結點的所有配置資訊,如哪些伺服器參與選舉,每個節點的狀態等等。當該方法執行至quorumPeer.join();
時,當前執行緒將阻塞,直到其他所有執行緒退出為止。
讓我們進入quorumPeer.start()
方法,看看它做了什麼動作:
public synchronized void start() {
// 初始化是記憶體資料庫
loadDataBase();
// 用於處理程式為捕獲的異常和處理客戶端請求
cnxnFactory.start();
// 選舉前相關配置
startLeaderElection();
// 執行緒呼叫本類的run()方法,實施選舉
super.start();
}
複製程式碼
zk本身執行時會在記憶體中維護一個目錄樹,也就是一個記憶體資料庫,初始化伺服器時,zk會從本地配置檔案中裝載資料近記憶體資料庫,如果沒有本地記錄,則建立一個空的記憶體資料庫,同時,快照資料的儲存也是基於記憶體資料庫完成的。
三、zk叢集模式下如何進行選舉?
小編目測了程式碼之後發現zk應該是採用JMX來管理選舉功能,但由於小編對JMX暫時不熟悉,因此,此部分將不結合原始碼進行解釋,直接說明zk中選舉流程。
首先每個伺服器啟動之後將進入LOOKING
狀態,開始選舉一個新的群首或者查詢已經存在的群首,如果群首存在,其他伺服器就會通知這個新啟動的伺服器,告知那個伺服器是群首,於此同時,新的伺服器會與群首建立連結,以確保自己的狀態和群首一致。
對於群首選舉時傳送的訊息,我們稱之為通知訊息。當伺服器進入LOOKING
狀態時,會想叢集中所有其他節點傳送一個通知,該同志包括了自己的投票資訊vote,vote的資料結構很簡單,一般由sid和zxid組成,sid表示當前伺服器的編號,zxid表示當前伺服器最大的事務編號,投票資訊的交換規則如下:
- 1、如果voteZxid > myzxid 或者 (voteZxid = myZxid 且 voteId > mySid ) ,保留當前的投票資訊
- 2、否則修改自己的投票資訊,將voteZxid賦值給myZxid,將voteId賦值給mySid
總之就是先比較事務ID,如果相等,再比較伺服器編號Sid。如果一個伺服器接收到的所有通知都一樣時,則表示群首選舉成功(zxid最大或者sid最大)
四、為什麼說組成zk叢集的節點數最好為奇數,且建議為3個節點?
Zk叢集建議伺服器的數量為奇數個,其內部採用多數原則,因為這樣能使得整個叢集更加高可用。當然這也是由zk選舉演算法決定的,一個節點雖然可以為外界提供服務,但只有一個節點的zk還能算作是叢集嗎?很明顯不是,只能說是獨立模式執行zk。
假設我們配置的機器有5臺,那麼我們認為只要超過一半(即3)的伺服器是可用的,那麼整個叢集就是可用的,至於為什麼一定要數量的半數,這是由於zk中採用多數原則決定的,具體可以檢視QuorumMaj
類,該類有個校驗多數原則的方法,程式碼如下:
/**
* 這個類實現了對法定伺服器的校驗
* This class implements a validator for majority quorums. The
* implementation is straightforward.
*/
public class QuorumMaj implements QuorumVerifier {
private static final Logger LOG = LoggerFactory.getLogger(QuorumMaj.class);
// 一半的伺服器數量,如果是5,則half=2,只要可用的伺服器數量大於2,則整個zk就是可用的
int half;
/**
* Defines a majority to avoid computing it every time.
*/
public QuorumMaj(int n) {
this.half = n / 2;
}
/**
* Returns weight of 1 by default.權重
*/
public long getWeight(long id) {
return (long) 1;
}
/**
* Verifies if a set is a majority.
*/
public boolean containsQuorum(HashSet<Long> set) {
return (set.size() > half);//傳入一組伺服器id,校驗必須大於半數才能正常提供服務
}
}
複製程式碼
我們再來看看QuormPeerConfig
類中的parseProperties()
方法中的程式碼片段:
// 只有2臺伺服器server
if (servers.size() == 2) {
// 列印日誌,警告至少需要3臺伺服器,但不會報錯
LOG.warn("No server failure will be tolerated. " + "You need at least 3 servers.");
} else if (servers.size() % 2 == 0) {
LOG.warn("Non-optimial configuration, consider an odd number of servers.");
}
複製程式碼
該程式碼片段對你配置檔案中配置的伺服器數量進行校驗,如果是偶數或者等於2,則會發出諸如“該配置不是推薦配置”的警告,如果伺服器數量等於2,則不能容忍哪怕1臺伺服器崩潰。
為了加深印象,我們來看看為什麼zk推薦使用奇數臺伺服器。
- 如果配置3臺伺服器,那麼當一臺掛了以後,3臺伺服器中的2臺票數過半,可以選出一臺leader;
- 如果配置4臺,那麼允許1臺掛掉,這和只有3臺伺服器是一樣的,為節省成本,何不選擇3臺,但是當4臺中2臺掛了之後,那麼4臺中可用的2臺票數沒過半無法選擇出leader