ZooKeeper源碼學習筆記(2)--Standalone模式下的ZooKeeper

來源:互聯網
上載者:User
Server入口

Server的啟動代碼位於 zkServer.sh 檔案中。

zkServer.sh指令碼同 /etc/init.d/ 中的啟動指令碼比較類似,都是通過shell的case命令解析指令執行。具體指令如下:
1. start: 通過nohup後台啟動org.apache.zookeeper.server.quorum.QuorumPeerMain
2. start-foreground: 前台運行org.apache.zookeeper.server.quorum.QuorumPeerMain
3. stop: 殺死通過start啟動的進程
4. restart: 先後調用stop和start,起到重啟的作用
5. status: 通過org.apache.zookeeper.client.FourLetterWordMain查看運行情況
6. upgrade: 通過org.apache.zookeeper.server.upgrade.UpgradeMain 進行線上更新
7. print-cmd: 輸出啟動start的命令 啟動邏輯

從zkServer.sh中看到,ZooKeeper Server的入口類是QuorumPeerMain 。

在入口函數中,根據在 zoo.cfg 檔案中配置的server個數,決定啟動Standalone(單機)模式或是Cluster(叢集)模式

如果在 zoo.cfg 檔案中沒有配置 server 則預設作為 Standalone 模式啟動,並將啟動參數傳遞給 ZooKeeperServerMain::main ,否則作為 Cluster 模式進行啟動。

在本節中,暫時不考慮Cluster模式,只關心 Standalone 模式下的 Server 運行邏輯。 Standalone 模式的啟動流程

如上所言,在Standalone模式下,QuorumPeerMain會將啟動參數傳遞給ZooKeeperServerMain::main。

ZooKeeperServerMain::main裡,ZooKeeper在解析完成config檔案後,調用runFromConfig初始化Server。

public void runFromConfig(ServerConfig config) throws IOException {    final ZooKeeperServer zkServer = new ZooKeeperServer();    // Registers shutdown handler which will be used to know the    // server error or shutdown state changes.    final CountDownLatch shutdownLatch = new CountDownLatch(1);    zkServer.registerServerShutdownHandler( new ZooKeeperServerShutdownHandler(shutdownLatch));    cnxnFactory = ServerCnxnFactory.createFactory();    cnxnFactory.configure(config.getClientPortAddress(),                    config.getMaxClientCnxns());    cnxnFactory.startup(zkServer);    shutdownLatch.await();    shutdown();    cnxnFactory.join();    if (zkServer.canShutdown()) {        zkServer.shutdown();    }}

走讀源碼發現,cnxnFactory.startup方法中啟動了三個線程,分別是NIOServerCnxnFactory(Runnable啟動), PrepRequestProcessor , SyncRequestProcessor。

線程啟動完畢後,進入shutdownLatch.await()的等待狀態, 阻塞主線程,避免程式退出。

退出邏輯在ZooKeeperServerShutdownHandler::handle中可以看到:

 if (state == State.ERROR || state == State.SHUTDOWN) {  shutdownLatch.countDown();}

當ZooKeeperServer處於異常或關閉狀態,shutdownLatch.countDown();之後,shutdownLatch.await()指令完成,主線程進入關閉流程。 Server Sokect

在學習筆記(1)中,我們看到Client端在SendThread中同伺服器保持一個socket長連結,與之對應的,在Server端也會有一個ServerSocket負責接收Client發送過來的請求。

String serverCnxnFactoryName = System.getProperty(ZOOKEEPER_SERVER_CNXN_FACTORY);if (serverCnxnFactoryName == null) {  serverCnxnFactoryName = NIOServerCnxnFactory.class.getName();}

在runFromConfig中構造了一個ServerCnxnFactory對象,這個對象預設是一個NIOServerCnxnFactory,對應Client端中的ClientCnxnSocketNIO類。

@Overridepublic void configure(InetSocketAddress addr, int maxcc) throws IOException {  thread = new ZooKeeperThread(this, "NIOServerCxn.Factory:" + addr);}@Overridepublic void startup(ZooKeeperServer zks) throws IOException,            InterruptedException {  start();  setZooKeeperServer(zks);  zks.startdata();  zks.startup();}

NIOServerCnxnFactory類本身繼承了Runnable介面,在 NIOServerCnxnFactory::startup 中啟動一個Daemon線程響應來自Client的請求資訊 響應socket請求

Client 端會中有一個SendThread 線程專門負責同 Server 的socket 連結。同樣,在Server端的NIOServerCnxnFactory類中也有一個獨立線程,專門負責讀取Client發送來的資料。

如圖,在Client端成功和Server端建立連結之後,Client端的使用者請求會被 ClientCnxnSocketNIO 寫入socket中,當NIOServerCnxnFactory 讀取並處理完畢後,再通過socket進行寫回,得到response。

對於大部分資料請求,會在doIO中逐步解析成一個Packet對象,再擷取Request請求,發送給ZooKeeperServer::submitRequest進行消費,具體的消費路徑會在後面進行講解,這裡只簡單介紹 socket 的通訊邏輯。 Watcher的實現

ServerCnxn實現了Watcher介面,如果判斷request中包含了watcher,則會將ServerCnxn加入監聽列表中,當指定節點發生變化時,回調ServerCnxn的對應方法,通過sendResponse通知Client節點資訊發生改變 ZooKeeper的資料結構

ZooKeeper是一個基於節點模型的分布式協調架構,使用類似檔案路徑的節點進行資料存放區。

在運行過程中,節點資訊都會被全部載入到記憶體中,每個節點都會被構造成一個DataNode 對象,被稱為znode。 三層資料緩衝層

znode節點會由於使用者的讀寫操作頻繁發生變化,為了提升資料的訪問效率,ZooKeeper中有一個三層的資料緩衝層用於存放節點資料。

outstandingChanges

outstandingChanges 位於ZooKeeperServer 中,用於存放剛變更還沒有同步到ZKDatabase中的節點資訊 ZKDatabase

ZKDatabase 用於管理ZooKeeper的中的節點資料。

ZKDatabase中有一個DataTree對象,在DataTree中維護一個叫做nodes的ConcurrentHashMap,用於在記憶體中持有完整的節點資訊。

在cnxnFactory.startup的時候,系統會通過zkDb.loadDatabase()將序列化存放的節點資訊還原到記憶體中 Disk files

Disk file 由兩部分組成,一個是 FileSnap, 一個是FileTxnLog。顧名思義,FileSnap 用於存放基於某個時間點狀態的 ZooKeeper 節點資訊快照,FileTxnLog 用於存放資料對節點資訊的具體更改操作。 ZKDatabase 的資料持久化

ZooKeeper 通過維護節點資訊的一致性來完成分布式應用的協調工作。 關於 Snapshot 和 Transaction

同 Hadoop 類似,在ZooKeeper中同樣存在Snapshot和Transaction的概念。

Snapshot 對應某個時間點資料的完整狀態,Transaction 代表某條對資料的修正指令。

當Snapshot A 執行完指令後,他的資料狀態得到更新,成為 Snapshot B。

當服務端異常退出或重啟時,還原資料節點到指定狀態有兩種方案,一種是再次執行每一條Transaction,另一種是先將資料節點還原到一個正確的Snapshot,再執行從這個Snapshot之後的每一條Transaction。第一種方案需要儲存從初次開機開始的每一條指令,同時已耗用時間隨指令條數線性增長,影響還原效率。因此我們通常都採用第二種方案snapshot+transaction進行資料還原。 資料載入流程

如果當前並非初次開機ZooKeeper,則我們需要將關閉前的ZooKeeper資料進行還原。

根據前一小節,我們知道了Snapshot和Transaction的關係,在返回源碼,我們看到在ZooKeeperServer::loadData()會調用以下代碼

public long loadDataBase() throws IOException {  PlayBackListener listener=new PlayBackListener(){    public void onTxnLoaded(TxnHeader hdr,Record txn){      Request r = new Request(null, 0, hdr.getCxid(),hdr.getType(), null, null);      addCommittedProposal(r);     }  };  long zxid = snapLog.restore(dataTree,sessionsWithTimeouts,listener);  return zxid;}public long restore(DataTree dt, Map<Long, Integer> sessions,             PlayBackListener listener) throws IOException {  snapLog.deserialize(dt, sessions);  FileTxnLog txnLog = new FileTxnLog(dataDir);  TxnIterator itr = txnLog.read(dt.lastProcessedZxid+1);  long highestZxid = dt.lastProcessedZxid;  TxnHeader hdr;  while (true) {    hdr = itr.getHeader();      if (hdr == null) {         return dt.lastProcessedZxid;      }      processTransaction(hdr,dt,sessions, itr.getTxn());      listener.onTxnLoaded(hdr, itr.getTxn());      if (!itr.next())         break;    }  } finally {    if (itr != null) {      itr.close();    }  }  return highestZxid;}

snapLog是一個FileTxnSnapLog類,他由一個FileSnap和一個FileTxnLog組成。

FileSnap 是快照檔案的工具類,擁有serialize,deserialize方法,可以將DataTree對象進行序列化和還原序列化。

FileTxnLog是Transaction 日誌的工具類,通過txnLog.read,我們拿到Snapshot檔案發生後的Transaction 日誌,通過processTransaction將事務應用到DataTree上,還原初態。

通過loadDatabase()我們成功的將磁碟檔案儲存的節點資訊重新載入到了記憶體中,從這個時候開始我們可以對到來的socket進行消費。 處理 session 請求

在響應socket請求的小節中,我們看到在NIOServerCnxnFactory中啟動了一個Daemon線程,並在while迴圈中擷取socket請求資訊,然後分發到doIO中執行。

ZooKeeper將每一個 socket 的連結,都認為是一個session, 並擁有一個逾時時間。請求會被封裝成一個NIOServerCnxn對象,當判斷session是首次connect到ZooKeeperServer的時候,先讀取connect資訊,在SessionTrackerImpl中維護當前存活的session隊列。

SessionTrackerImpl 是一個獨立線程,專門用於檢測 session 的存活狀態。

其他非首次串連的socket資訊會通過readRequest進行消費。 RequestProcessor 任務鏈

在ZooKeeperServer::setupRequestProcessors中建立了三個RequestProcessor對象,分別是 FinalRequestProcessor , SyncRequestProcessor 和 PrepRequestProcessor ,其中PrepRequestProcessor 和 SyncRequestProcessor 類分別繼承自 Thread 類,作為獨立線程運行。

readRequest通過還原序列化Packet類,提取出Request資訊,然後調用ZooKeeperServer::submitRequest進行資料處理。

public void submitRequest(Request si) {  firstProcessor.processRequest(si);}

對於Standalone 模式的ZooKeeperServer,他的firstPrcessor就是PrepRequestProcessor類 PrepRequestProcessor

PrepRequestProcessor是整個任務鏈的起點。

PrepRequestProcessor::submitRequest不會立即處理request請求,而是將request加入運行隊列submittedRequests中,等待執行。

PrepRequestProcessor自身的獨立線程不斷從隊列中拉去Request對象,調用pRequest(request)。在pRequest中,根據Request的不同種類,將Request轉變為不同的Record對象,通過addChangeRecord將ChangeRecord加入ZooKeeperServer.outstandingChanges 中,此時節點資料並沒有同步到DataTree中。

根據節點的三層緩衝模型,在擷取節點資訊時,會首先從outstandingChangesForPath中擷取資訊,當沒有找到對應的節點資訊時,再通過zkDb::getNode擷取。 SyncRequestProcessor

SyncRequestProcessor 作為 PrepRequestProcessor 的下遊消費者,負責將Transaction寫入TxnLog中,並定時構建快照檔案。

zkDatabase.append中會將Request寫入Transaction Log File,如果發現當前的Txn條數超過閾值,則啟動一個快照線程,將DataTree作為快照執行個體到磁碟中。

在takeSnapshot中,通過序列化當前的DataTree結構,將snapShot儲存到磁碟上

當SyncRequestProcessor的處理條數超過閾值1000條時,調用flush()命令,將任務逐個傳遞給下遊的RequestProcessor進行處理。 FinalRequestProcessor

FinalRequestProcessor作為Standalone模式下的任務鏈終點,主要完成以下工作。

while (!zks.outstandingChanges.isEmpty() && zks.outstandingChanges.get(0).zxid <= request.zxid) {    ChangeRecord cr = zks.outstandingChanges.remove(0);    if (cr.zxid < request.zxid) {        LOG.warn("Zxid outstanding " + cr.zxid + " is less than current " + request.zxid);    }    if (zks.outstandingChangesForPath.get(cr.path) == cr) {        zks.outstandingChangesForPath.remove(cr.path);    }}if (request.hdr != null) {    TxnHeader hdr = request.hdr;    Record txn = request.txn;    rc = zks.processTxn(hdr, txn);}
調用zks.processTxn(),將請求資訊合并到DataTree中 清理掉zks.outstandingChanges中的冗餘資料,防止outstandingChanges無限增長。 任務鏈總結

ZooKeeper Server中通過三層的任務鏈實現對請求的處理過程。

第一層負責在outstandingChanges中構建一個臨時的節點對象,便於後續請求能夠快速擷取對應節點最新狀態

第二層負責將請求資料轉為Transaction日誌,並記錄到磁碟中,便於重啟後的節點資料還原。同時還會根據日誌操作定時儲存快照。

第三層負責批量將請求資料合併到DataTree中,同時清除第一層臨時構建的節點對象。 總結

ZooKeeper Server使用DataTree在記憶體中持有所有節點資訊, 在磁碟中通過Snapshot 和 TxnFile 儲存曆史節點資料。

響應請求時,Server 將請求資料分發給一個RequestProcessor任務鏈進行消費。

在任務鏈中,通過一個單線程保證資料的安全執行緒和一致性。

同樣由於在 ZooKeeper 中是通過單線程保證資料安全執行緒,在大訪問量級下的運行效率值得思考,之後可以看看Cluster下的ZooKeeper有沒有對這一塊做出最佳化。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.