Zookeeper源碼分析(四) ----- 集羣模式(replicated)運行

zookeeper源碼分析系列文章:算法

原創博客,純手敲,轉載請註明出處,謝謝!數據庫

如你所知,zk的運行方式有兩種,獨立模式和複製模式。很顯然複製模式是用來搭建zk集羣的,所以我把複製模式稱爲集羣模式。在以前的文章中咱們已經對獨立模式下運行zk的源碼進行相關分析,接下來咱們一塊兒來研究研究Zk集羣模式下的源碼。數組

集羣模式下的調試不像獨立模式那麼簡單,也許你可能會問,那是否須要多臺物理機來搭建一個zk集羣呢?其實也不須要,單臺物理機也是能夠模擬集羣運行的。所以,下文咱們將按照如下目錄開展討論:bash

1、zk集羣搭建及相關配置

zk配置集羣其實很是簡單,在上篇博客中講到,zk在解析配置文件時會判斷你配置文件中是否有相似server.的配置項,若是沒有相似server.的配置項,則默認以獨立模式運行zk。相反,集羣模式下就要求你進行相應的配置了。下面將一步一步對搭建環境進行講解:服務器

  • 一、在zk的conf目錄中增長3個配置文件,名字分別爲zoo1.cfgzoo2.cfgzoo3.cfg

其內容分別以下:數據結構

zoo1.cfgeclipse

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.cfgide

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,那爲何咱們這裏要寫成218121822183呢?其實緣由很簡單,由於咱們的集羣式搭建在單臺物理機上面,爲了防止端口衝突,咱們設置3臺zk服務器分別監聽不一樣的端口。

至於server.x屬性,用於配置參與集羣的每臺服務器的地址和端口號。其格式爲:

server.x addressIP:port1:port2

其中x表示zk節點的惟一編號,也就是咱們常說的sid的值,下面講到zk選舉的時候將會進一步講解。你可能會很好奇port1port2之間有什麼區別,在zk中,port1表示fllowers鏈接到leader的端口,port2表示當前結點參與選舉的端口。之因此要這麼設計,其實我以爲在ZAB協議中,當客戶端發出的寫操做在服務器端執行完畢時,leader節點必須將狀態同步給全部的fllowersleaderfllowers之間須要進行通訊嘛!另一種是全部節點進行快速選舉時,各個節點之間須要進行投票,投票選出完一個leader節點以後須要通知其餘節點。因此說,明白端口含義便可,它們就是區別做用罷了。

  • 二、建立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

  • 三、分別採用不一樣的配置文件運行QuorumPeerMainmain()方法便可。配置文件路徑能夠這樣傳給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文件,繼續在QuorumPeerConfigparseProperties()方法中,找到以下代碼片斷:

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流,不用的時候記得關閉,不要浪費資源
}
複製代碼

2、zk集羣模式下的初始化

在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會從本地配置文件中裝載數據近內存數據庫,若是沒有本地記錄,則建立一個空的內存數據庫,同時,快照數據的保存也是基於內存數據庫完成的。

3、zk集羣模式下如何進行選舉?

小編目測了代碼以後發現zk應該是採用JMX來管理選舉功能,但因爲小編對JMX暫時不熟悉,所以,此部分將不結合源碼進行解釋,直接說明zk中選舉流程。

首先每一個服務器啓動以後將進入LOOKING狀態,開始選舉一個新的羣首或者查找已經存在的羣首,若是羣首存在,其餘服務器就會通知這個新啓動的服務器,告知那個服務器是羣首,於此同時,新的服務器會與羣首創建連接,以確保本身的狀態和羣首一致。

對於羣首選舉時發送的消息,咱們稱之爲通知消息。當服務器進入LOOKING狀態時,會想集羣中全部其餘節點發送一個通知,該同志包括了本身的投票信息vote,vote的數據結構很簡單,通常由sid和zxid組成,sid表示當前服務器的編號,zxid表示當前服務器最大的事務編號,投票信息的交換規則以下:

  • 一、若是voteZxid > myzxid 或者 (voteZxid = myZxid 且 voteId > mySid ) ,保留當前的投票信息
  • 二、不然修改本身的投票信息,將voteZxid賦值給myZxid,將voteId賦值給mySid

總之就是先比較事務ID,若是相等,再比較服務器編號Sid。若是一個服務器接收到的全部通知都同樣時,則表示羣首選舉成功(zxid最大或者sid最大)

4、爲何說組成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
相關文章
相關標籤/搜索