天天看點

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

<a href="http://my.oschina.net/pingpangkuangmo/blog/484955">zookeeper源碼研究系列(1)源碼環境搭建</a>

<a href="http://my.oschina.net/pingpangkuangmo/blog/486780">zookeeper源碼研究系列(2)用戶端建立連接配接過程分析</a>

<a href="http://my.oschina.net/pingpangkuangmo/blog/491673">zookeeper源碼研究系列(3)單機版伺服器介紹</a>

<a href="http://my.oschina.net/pingpangkuangmo/blog/495311">zookeeper源碼研究系列(4)叢集版伺服器介紹</a>

啟動類是org.apache.zookeeper.server.quorum.quorumpeermain,啟動參數就是配置檔案的位址

來看下一個簡單的配置檔案内容:

ticktime值,機關ms,預設3000

用途1:用于指定session檢查的間隔

伺服器會每隔一段時間檢查一次連接配接它的用戶端的session是否過期。該間隔就是ticktime。

用途2:用于給出預設的minsessiontimeout和maxsessiontimeout

如果沒有給出maxsessiontimeout和minsessiontimeout(為-1),則minsessiontimeout和maxsessiontimeout的取值如下:

minsessiontimeout == -1 ? ticktime 2 : minsessiontimeout; maxsessiontimeout == -1 ? ticktime 20 : maxsessiontimeout;

分别是ticktime的2倍和20倍。

用戶端代碼在建立zookeeper對象的時候會給出一個sessiontimeout時間,而上述的minsessiontimeout和maxsessiontimeout就是用來限制用戶端的sessiontimeout

用途3:作為initlimit和synclimit時間的基數,見下面

initlimit:在初始化階段和leader的通信的讀取逾時時間,即當調用socket的inputstream的read方法時最大阻塞時間不能超過initlimit*ticktime。設定如下:

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

initlimit還會作為初始化階段收集相關響應的總時間,一旦超過該時間,還沒有過半的機器進行響應,則抛出interruptedexception的timeout異常

synclimit:在初始化階段之後的請求階段和leader通信的讀取逾時時間,即對leader的一次請求到響應的總時間不能超過synclimit*ticktime時間。follower和leader之間的socket的逾時時間初始化階段是前者,當初始化完畢又設定到後者時間上。設定如下:

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

synclimit還會作為與leader的連接配接逾時時間,如下:

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

datadir:用于存儲資料快照的目錄

datalogdir:用于存儲事務日志的目錄,如果沒有指定,則和datadir保持一緻

clientport:對用戶端暴漏的連接配接端口

maxclientcnxns值,用于指定伺服器端最大的連接配接數。

叢集的server配置,一種格式為server.a=b:c:d,還有其他格式,具體可以去看quorumpeerconfig源碼解析這一塊

a:即為叢集中server的id标示,很多地方用到它,如選舉過程中,就是通過id來識别是哪台伺服器的投票。如初始化sessionid的時候,也用到id來防止sessionid出現重複。

b:即該伺服器的host位址或者ip位址。

c:一旦leader選舉成功後,非leader伺服器都要和leader伺服器建立tcp連接配接進行通信。該通信端口即為c

d:在leader選舉階段,每個伺服器之間互相連接配接(上述serverid大的會主動連接配接serverid小的server),進行投票選舉的事宜,d即為投票選舉時的通信端口

上述配置是每台伺服器都要知道的叢集配置,同時要求在datadir目錄中建立一個myid檔案,裡面寫上上述serverid中的一個id值,即表明某台伺服器所屬的編号

我們由前一篇文章知道了,單機版的伺服器啟動,就是建立了一個zookeeperserver對象。我們需要再次熟悉下zookeeperserver的類圖,如下:

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

可見leader伺服器要使用leaderzookeeperserver,follower伺服器要使用followerzookeeperserver。而叢集版伺服器啟動後,可能是leader或者follower。在運作過程中角色還會進行自動更換,即自動更換使用不同的zookeeperserver子類。此時就需要一個代理對象,用于角色的更換、所使用的zookeeperserver的子類的更換。這就是quorumpeer,如下圖

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

這裡面很多的配置屬性都交給了quorumpeer,由它傳遞給底層所使用的zookeeperserver子類。

來詳細看看這些配置屬性:

servercnxnfactory cnxnfactory:負責和用戶端建立連接配接和通信

filetxnsnaplog logfactory:通過datadir和datalogdir目錄,用于事務日志記錄和記憶體datatree和session資料的快照。

map&lt;long, quorumserver&gt; quorumpeers:quorumserver包含ip、和leader通信端口、選舉端口即上述server.a=b:c:d的内容。而這裡的key則是a,即server的id。這裡的server不包含observers,即這裡的server都是要參與投票的。

int electiontype:選舉算法的類型。預設是3,采用的是fastleaderelection選舉算法。如下圖

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

目前前三種選舉算法都被标記為過時了,隻保留了最後一種選舉算法。具體的選舉過程,後面單獨拿出一篇部落格來分析。目前的首要目标是把叢集的啟動過程簡單弄清楚,然後了解在叢集時,如何來處理請求的整個過程。

long myid:就是本機器配置的id,即myid檔案中寫入的數字。

int ticktime、minsessiontimeout、maxsessiontimeout:這幾個參數在單機版的時候都講過了。

int initlimit、synclimit:上面已經較長的描述過了

quorumverifier quorumconfig:用于驗證是否過半機器已經認同了。預設采用的是quorummaj,即最簡單的數量過半即可,不考慮權重問題

zkdatabase zkdb:即該伺服器的記憶體資料庫,最終還是會傳遞給zookeeperserver的子類。

learnertype:就兩種,participant, observer。participant參與投票,可能成為follower,也可能成為leader。observer不參與投票,角色不會改變。

然後就是啟動quorumpeer,之後阻塞主線程,啟動過程如下:

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

主要分成4大步:

loaddatabase():從事務日志目錄datalogdir和資料快照目錄datadir中恢複出datatree資料

cnxnfactory.start():開啟對用戶端的連接配接端口

startleaderelection():建立出選舉算法

super.start():啟動quorumpeer線程,在該線程中進行伺服器狀态的檢查

不再重點說明選舉過程,後面會專門抽出一篇部落格來詳細說明選舉過程。

quorumpeer本身繼承了thread,在run方法中不斷的檢測目前伺服器的狀态,即quorumpeer的serverstate state屬性。serverstate枚舉内容如下:

looking:即該伺服器處于leader選舉階段

following:即該伺服器作為一個follower

leading:即該伺服器作為一個leader

observing:即該伺服器作為一個observer

在quorumpeer的線程中操作如下:

伺服器的狀态是looking,則根據之前建立的選舉算法,執行選舉過程

選舉過程一直阻塞,直到完成選舉。完成選舉後,各自的伺服器根據投票結果判定自己是不是被選舉成leader了,如果不是則狀态改變為following,如果是leader,則狀态改變為leading。

伺服器的狀态是leading:則會建立出leaderzookeeperserver伺服器,然後封裝成leader,調用leader的lead()方法,也是阻塞方法,隻有當該leader挂了之後,才去執行下setleader(null)并重新回到looking的狀态

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

伺服器的狀态是following:則會建立出followerzookeeperserver伺服器,然後封裝成follower,調用follower的followleader()方法,也是阻塞方法,隻有當該叢集中的leader挂了之後,才去執行下setfollower(null)并重新回到looking的狀态

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

下面就來詳細的看看各個角色的啟動過程:

首先是根據已有的配置資訊建立出leaderzookeeperserver:

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

然後就是封裝成leader對象

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

leader和leaderzookeeperserver各自的職責是什麼呢?

我們知道單機版使用的zookeeperserver不需要處理叢集版中follower與leader之間的通信。zookeeperserver最主要的就是requestprocessor處理器鍊、zkdatabase、sessiontracker(隻是實作不一樣)。這幾部分是單機版和叢集版伺服器都共通的,主要不同的地方就是requestprocessor處理器鍊的不同。是以leaderzookeeperserver、followerzookeeperserver和zookeeperserver最主要的差別就是requestprocessor處理器鍊。

叢集版還要負責處理follower與leader之間的通信,是以需要在leaderzookeeperserver和followerzookeeperserver之外加入這部分内容。是以就有了leader對leaderzookeeperserver等封裝,follower對followerzookeeperserver的封裝。前者加上加入serversocket負責等待follower的socket連接配接,後者加入socket負責去連接配接leader。

看下leader處理socket連接配接的過程:

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

可以看到每來一個其他zookeeper伺服器的socket連接配接,就會建立一個learnerhandler,具體的處理邏輯就全部交給learnerhandler了

然後在learnerhandler中就開始了leader和follower或者observer的初始化同步過程,這個過程之後詳細講解。完成同步之後,learnerhandler就進行循環過程,不斷的讀取來自follower或者observer的資料包,如下:

learnerhandler會接收來自follower或者observer的ping、request請求等。ping請求,則需要重新計算所傳遞過來的sessionid的過期時間。事務請求則需要follower或者observer轉發給leader,該事務請求就是leader.request類型。

同時follower也在不斷接收來自leader的資料包,處理如下:

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

leader在開啟與follower或者observer同步的時候,同時在啟動了本身的requestprocessor處理器鍊,如下:

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

preprequestprocessor-》proposalrequestprocessor-》commitprocessor-》tobeappliedrequestprocessor-》finalrequestprocessor

proposalrequestprocessor-》syncrequestprocessor-》ackrequestprocessor

再來看看follower的處理器鍊

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

followerrequestprocessor-》commitprocessor-》finalrequestprocessor

syncrequestprocessor-》sendackrequestprocessor

接下來就是需要詳細的看看這些處理器鍊

先來看下具體的處理過程:

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

對于一個請求,先交給下一個處理器來處理,如果請求是事務請求,還要将該請求轉發給leader。zks.getfollower().request(request)即通過上述leader與follower的tcp連接配接發送給leader,最終會在上述learnerhandler中出現。

由于followerrequestprocessor的下一個處理器是commitprocessor(是一個線程),nextprocessor.processrequest(request)這個操作僅僅是把request放入等待處理的隊列中,然後就傳回了,執行下面的代碼,将事務請求轉發給leader。

followerrequestprocessor把請求交給了commitprocessor,看下commitprocessor的整個處理流程

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

commitprocessor有三個重要屬性:

linkedlist&lt;request&gt; queuedrequests:用于存放followerrequestprocessor送出的請求

linkedlist&lt;request&gt; committedrequests:用于存放leader對該follower下達的commit請求。我們知道一旦是事務請求就會轉發給leader,需要leader把這個請求下發給所有的follower進行投票,如果過半數達成一緻,才認為該請求可以通過,即是可以commit的請求,此時leader又會下發commit指令,讓follower去執行commit。follower就是在上述follower與leader通信子產品中接收到該請求,然後存放至commitprocessor的committedrequests中的。

arraylist&lt;request&gt; toprocess:需要被下一個處理器處理的請求,可以來自committedrequests,如果是非事務請求不會經過committedrequests,直接到達toprocess中。

具體的處理邏輯如下:

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語
ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

先解釋下nextpending:即等待被處理的事務請求,注意一定是事務請求。

第一步:先把toprocess中的請求交給下一個處理器來處理,然後清空toprocess

第二步:如果queuedrequests清單為空,但是目前有等待被處理的事務。即是這樣的場景:followerrequestprocessor向queuedrequests中送出了一個事務請求,然後改事務請求作為了下一個等待被處理的事務請求即nextpending,然後從queuedrequests中删除,同時followerrequestprocessor将該事務請求轉發給leader,但是此時leader還沒有判定該請求是否能夠被送出,即還未向follower發送commit該請求的操作,即commitprocessor中的committedrequests為空,此時follower要做的事情就是等待leader向它的committedrequests中發送判定結果。

第三步:如果commitprocessor中的committedrequests不為空,即leader向該follower發送了相關的送出請求。則拿出第一個需要commit的請求,驗證下目前需要被處理的事務請求是不是和剛才拿出的請求是不是同一個請求,如果是同一個請求,則替換nextpending中的部分資料,同時存放至toprocess,等待被下一個處理器來處理。同時交出nextpending位置,等待下一個事務請求來占用。

第四步:如果和nextpending不是同一個請求,則直接存放至toprocess,等待被下一個處理器來處理。其他一切不變

第五步:判斷目前是否有等待被處理的事務請求,即nextpending是否為空,如果不為空則continue,不執行下面的第6步和第7步操作。是為了保證請求都能夠被順序處理,前面一個沒處理完,後面的請求不能被處理

第六步:能夠走到第6步,說明nextpending已經為null了,然後從queuedrequests中取出一個請求,如果是事務請求,則把nextpending的位置占住

第七步:如果不是事務請求,則直接存放至toprocess,等待被下一個處理器來處理

其實到這裡,一旦是事務請求,就會被阻塞在這裡,等待leader的決定。那接下來我們就去看看leader是如何處理follower轉發過來的請求的。我們就以建立session為例。

上面說過了,leader會為每一個follower建立一個learnerhandler,來處理與該follower的通信,對于follower轉發的請求,處理如下:

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

先還原成一個request,然後為該request設定owner,this即learnerhandler。然後就把該請求交給了leader的請求處理器鍊,leader的第一個請求處理器是preprequestprocessor

這裡需要提前說明下,當用戶端發送請求給follower的時候,這時候先使用follower的learnersessiontracker為該建立session的請求配置設定了sessionid,同時給定了sessiontimeout時間。但是并沒有在follower本地保留任何資訊。

來看下preprequestprocessor對于建立session的處理

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

根據請求的sessionid和sessiontimeout時間在leader中使用sessiontrackerimpl建立出一個session。這一部分在單機版的時候已經較長的描述過了,這裡不再說明。然後設定該session的owner是上述提到過的learnerhandler,代表了某個follower。

至此就在leader中建立出了session。

然後繼續下一個處理器proposalrequestprocessor

處理過程如下:

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

第一步:交給下一個處理器來處理,下一個處理器是commitprocessor,我們知道會阻塞在那裡

第二步:如果請求是事務請求的話,則根據該請求建立出一份議案,發給所有的learner(即follower和observer)

第三步:同時記錄該事務請求到事務日志檔案中

來詳細看下第二步如何發出議案:

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

可以看到就是根據request的内容,建構了一個quorumpacket包,然後發送給所有的follower,同時對quorumpacket進行包裝,建立出proposal議案,存至concurrentmap&lt;long, proposal&gt; outstandingproposals結構中,key就是請求的zxid。

follower接收到之後該如何處理呢?

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語
ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

可以看到follower的處理就是把該請求交給了syncrequestprocessor,它會把事務請求記錄到日志中去,同時交給下一個處理器來處理。follower的syncrequestprocessor下一個處理器是sendackrequestprocessor,來看下它又是如何來處理的呢?

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

僅僅就是向leader回複一個leader.ack的響應包,表明本follower已完成事務請求的記錄。

再回到leader的proposalrequestprocessor處理器,然後來詳細看下上述第三步中記錄到事務日志檔案中的内容:

同樣是利用syncrequestprocessor把事務請求記錄到事務日志檔案中,然後交給下一個處理器來處理,leader的下一個處理ackrequestprocessor,如下:

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

其他的follower在記錄完事務請求後,都使用sendackrequestprocessor向leader發送一個應答響應,leader自己在記錄完事務請求後,也需要一個應答,隻是不用發送資料包了,直接調用,響應方法leader.processack,其實leader在接收到其他follower發送的leader.ack的響應包,也會調用該方法進行處理,如下:

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

具體的處理過程就是:

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

第一步:根據zxid從leader的concurrentmap&lt;long, proposal&gt; outstandingproposals取出議案

第二步:記錄該議案的已經響應的follower數量

第三步:然後判決數量是不是已經過半?

第四步:如果過半,則将該決議存至concurrentlinkedqueue&lt;proposal&gt; tobeapplied結構中,作為曆史備份,一旦某個決議被真正執行了,就從中删除。

第五、六步:向所有的follower和observer發送一個commit請求包,因為此時follower和observer都處于阻塞狀态(也不一定)等待leader的commit資料包。兩者的主要差別是,leader之前已經向follower發送過請求的具體内容,這次commit就不需要再完整的發送整個請求内容了,而observer之前沒有收到這個提案,不知道有這個請求,是以需要把整個請求資料全發給observer。

第七步:由于leader本身也阻塞在commitprocessor,是以需要給自己的commitprocessor中加入之前的請求。

對于連接配接follower建立session來說,leader發送的commit請求到達follower之後,該請求就可以在follower中繼續走下去,即走到了follower的最後一個處理器finalrequestprocessor,之前就詳細說過了

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

follower對用戶端建立session的請求執行上述響應,進而整個叢集版的連接配接過程就建立起來了。

再說說具體的細節問題,其他的follower和observer都接收到了leader發來的commit請求,他們該如何來處理呢?對于建立session來說,他們其實都不需要做什麼,那這個request又是如何被處理掉的呢?

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

在finalrequestprocessor中會做這樣的一個判斷,即判斷該請求是否有servercnxn,如果是用戶端連接配接的那台follower,必然會有servercnxn,而其他follower和observer接收到的請求是從leader過來的,就沒有servercnxn,是以就被過濾掉了,不用執行。

ZooKeeper源碼研究系列(4)叢集版伺服器介紹1 系列目錄2 叢集版伺服器啟動過程3 結束語

就是把請求交給下一個處理器即finalrequestprocessor,同時從之前的決議隊列中取出然後删除。

本篇文章貼代碼形式地介紹了leader和follower的請求處理器以及他們通信的過程。下一篇文章就純理論地介紹如下過程

1 連接配接leader建立session關聯的過程,以及session不斷激活的過程

2 連接配接follower建立session關聯的過程,以及session不斷激激活的過程

3 連接配接observer建立session關聯的過程,以及session不斷激激活的過程

4 連接配接leader,setdata的過程

5 連接配接follower,setdata的過程

6 連接配接observer,setdata的過程

繼續閱讀