Zookeeper原始碼分析(四) —– 叢集模式(replicated)執行

擁抱心中的夢想發表於2019-03-03

zookeeper原始碼分析系列文章:

原創部落格,純手敲,轉載請註明出處,謝謝!

如你所知,zk的執行方式有兩種,獨立模式和複製模式。很顯然複製模式是用來搭建zk叢集的,因此我把複製模式稱為叢集模式。在之前的文章中我們已經對獨立模式下執行zk的原始碼進行相關分析,接下來我們一起來研究研究Zk叢集模式下的原始碼。

叢集模式下的除錯不像獨立模式那麼簡單,也許你可能會問,那是否需要多臺物理機來搭建一個zk叢集呢?其實也不需要,單臺物理機也是可以模擬叢集執行的。因此,下文我們將按照以下目錄開展討論:

一、zk叢集搭建及相關配置

zk配置叢集其實非常簡單,在上篇部落格中講到,zk在解析配置檔案時會判斷你配置檔案中是否有類似server.的配置項,如果沒有類似server.的配置項,則預設以獨立模式執行zk。相反,叢集模式下就要求你進行相應的配置了。下面將一步一步對搭建環境進行講解:

  • 1、在zk的conf目錄中增加3個配置檔案,名字分別為zoo1.cfgzoo2.cfgzoo3.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,那為什麼我們這裡要寫成218121822183呢?其實原因很簡單,因為我們的叢集式搭建在單臺物理機上面,為了防止埠衝突,我們設定3臺zk伺服器分別監聽不同的埠。

至於server.x屬性,用於配置參與叢集的每臺伺服器的地址和埠號。其格式為:

server.x addressIP:port1:port2

其中x表示zk節點的唯一編號,也就是我們常說的sid的值,下面講到zk選舉的時候將會進一步講解。你可能會很好奇port1port2之間有什麼區別,在zk中,port1表示fllowers連線到leader的埠,port2表示當前結點參與選舉的埠。之所以要這麼設計,其實我覺得在ZAB協議中,當客戶端發出的寫操作在伺服器端執行完畢時,leader節點必須將狀態同步給所有的fllowersleaderfllowers之間需要進行通訊嘛!另外一種是所有節點進行快速選舉時,各個節點之間需要進行投票,投票選出完一個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、分別採用不同的配置檔案執行QuorumPeerMainmain()方法即可。配置檔案路徑可以這樣傳給eclipse,如下圖:
Zookeeper原始碼分析(四) —– 叢集模式(replicated)執行

上面講的內容似乎和原始碼打不上邊,嗯,彆著急,下面就講原始碼。

首先我們看看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流,不用的時候記得關閉,不要浪費資源
}
複製程式碼

二、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

相關文章