zookeeper
源碼分析系列文章:算法
原創博客,純手敲,轉載請註明出處,謝謝!數據庫
如你所知,zk的運行方式有兩種,獨立模式和複製模式。很顯然複製模式是用來搭建zk集羣的,所以我把複製模式稱爲集羣模式。在以前的文章中咱們已經對獨立模式下運行zk的源碼進行相關分析,接下來咱們一塊兒來研究研究Zk集羣模式下的源碼。數組
集羣模式下的調試不像獨立模式那麼簡單,也許你可能會問,那是否須要多臺物理機來搭建一個zk集羣呢?其實也不須要,單臺物理機也是能夠模擬集羣運行的。所以,下文咱們將按照如下目錄開展討論:bash
zk配置集羣其實很是簡單,在上篇博客中講到,zk在解析配置文件時會判斷你配置文件中是否有相似server.
的配置項,若是沒有相似server.
的配置項,則默認以獨立模式運行zk。相反,集羣模式下就要求你進行相應的配置了。下面將一步一步對搭建環境進行講解:服務器
zoo1.cfg
、zoo2.cfg
和zoo3.cfg
其內容分別以下:數據結構
zoo1.cfg
eclipse
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
ide
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
屬性。post
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
節點以後須要通知其餘節點。因此說,明白端口含義便可,它們就是區別做用罷了。
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
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集羣模式運行的相關源碼,讓咱們進入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應該是採用JMX來管理選舉功能,但因爲小編對JMX暫時不熟悉,所以,此部分將不結合源碼進行解釋,直接說明zk中選舉流程。
首先每一個服務器啓動以後將進入LOOKING
狀態,開始選舉一個新的羣首或者查找已經存在的羣首,若是羣首存在,其餘服務器就會通知這個新啓動的服務器,告知那個服務器是羣首,於此同時,新的服務器會與羣首創建連接,以確保本身的狀態和羣首一致。
對於羣首選舉時發送的消息,咱們稱之爲通知消息。當服務器進入LOOKING
狀態時,會想集羣中全部其餘節點發送一個通知,該同志包括了本身的投票信息vote,vote的數據結構很簡單,通常由sid和zxid組成,sid表示當前服務器的編號,zxid表示當前服務器最大的事務編號,投票信息的交換規則以下:
總之就是先比較事務ID,若是相等,再比較服務器編號Sid。若是一個服務器接收到的全部通知都同樣時,則表示羣首選舉成功(zxid最大或者sid最大)
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推薦使用奇數臺服務器。