【zookeeper原始碼】啟動流程詳解

清幽之地發表於2019-03-03

zookeeper啟動類的位置在org.apache.zookeeper.server.ZooKeeperServerMain,沒錯,找到它,並執行Main方法,即可啟動zookeeper伺服器。

請注意,在筆者的環境中只啟動了一個zookeeper伺服器,所以它並不是一個叢集環境。

一、載入配置

第一步就是要載入配置檔案,我們來看initializeAndRun方法。

protected void initializeAndRun(String[] args)throws ConfigException, IOException{
	ServerConfig config = new ServerConfig();
	if (args.length == 1) {
		config.parse(args[0]);
	} else {
		config.parse(conf);
	}
	runFromConfig(config);
}
複製程式碼

這裡主要就是把zoo.cfg中的配置載入到ServerConfig物件中,過程比較簡單,不再贅述。我們先看幾個簡單的配置項含義。

配置 含義
clientPort 對外服務埠,一般2181
dataDir 儲存快照檔案的目錄,預設情況下,事務日誌檔案也會放在這
tickTime ZK中的一個時間單元。ZK中所有時間都是以這個時間單元為基礎,進行整數倍配置
minSessionTimeout maxSessionTimeout Session超時時間,預設是2tickTime ~ 20tickTime 之間
preAllocSize 預先開闢磁碟空間,用於後續寫入事務日誌,預設64M
snapCount 每進行snapCount次事務日誌輸出後,觸發一次快照,預設是100,000
maxClientCnxns 最大併發客戶端數,預設是60

二、啟動服務

我們接著往下看,來到runFromConfig方法。

public void runFromConfig(ServerConfig config) throws IOException {
	LOG.info("Starting server");
	FileTxnSnapLog txnLog = null;
	try {
		final ZooKeeperServer zkServer = new ZooKeeperServer();
		final CountDownLatch shutdownLatch = new CountDownLatch(1);
		
		//註冊伺服器關閉事件
		zkServer.registerServerShutdownHandler(
				new ZooKeeperServerShutdownHandler(shutdownLatch));
	
		//操作事務日誌和快照日誌檔案類
		txnLog = new FileTxnSnapLog(new File(config.dataLogDir), new File(
				config.dataDir));
		txnLog.setServerStats(zkServer.serverStats());
		
		//設定配置屬性
		zkServer.setTxnLogFactory(txnLog);
		zkServer.setTickTime(config.tickTime);
		zkServer.setMinSessionTimeout(config.minSessionTimeout);
		zkServer.setMaxSessionTimeout(config.maxSessionTimeout);
		
		//例項化ServerCnxnFactory抽象類
		cnxnFactory = ServerCnxnFactory.createFactory();
		cnxnFactory.configure(config.getClientPortAddress(),
				config.getMaxClientCnxns());
		cnxnFactory.startup(zkServer);
		shutdownLatch.await();
		shutdown();
		cnxnFactory.join();
		if (zkServer.canShutdown()) {
			zkServer.shutdown(true);
		}
	} catch (InterruptedException e) {
		LOG.warn("Server interrupted", e);
	} finally {
		if (txnLog != null) {
			txnLog.close();
		}
	}
}
複製程式碼

以上程式碼就是zookeeper伺服器從啟動到關閉的流程。我們拆分來看。

1、服務關閉事件

我們看到給zkServer註冊了伺服器關閉的處理類。

final ZooKeeperServer zkServer = new ZooKeeperServer();
final CountDownLatch shutdownLatch = new CountDownLatch(1);
zkServer.registerServerShutdownHandler(
		new ZooKeeperServerShutdownHandler(shutdownLatch));
複製程式碼

首先,我們應該知道zookeeper伺服器是有狀態的。

protected enum State {
	INITIAL, RUNNING, SHUTDOWN, ERROR;
}
複製程式碼

那麼,在狀態發生變化的時候,就會呼叫到setState方法。

public class ZooKeeperServer{
	//當zookeeper伺服器狀態發生變化時候呼叫此方法
	protected void setState(State state) {
		this.state = state;
		if (zkShutdownHandler != null) {
			zkShutdownHandler.handle(state);
		} else {
			LOG.debug("ZKShutdownHandler is not registered, so ZooKeeper server "
					+ "won't take any action on ERROR or SHUTDOWN server state changes");
		}
	}
}
複製程式碼

然後在這裡就會呼叫到註冊的處理器。在處理器中,如果發現狀態不對,shutdownLatch.await方法就會被喚醒。

class ZooKeeperServerShutdownHandler {
	void handle(State state) {
        if (state == State.ERROR || state == State.SHUTDOWN) {
            shutdownLatch.countDown();
        }
    }
}
複製程式碼

當它被喚醒,事情就變得簡單了。關閉、清理各種資源。

2、日誌檔案

事務日誌檔案和快照檔案的操作,分別對應著兩個實現類,在這裡就是為了建立檔案路徑和建立類例項。

public FileTxnSnapLog(File dataDir, File snapDir) throws IOException {
	LOG.debug("Opening datadir:{} snapDir:{}", dataDir, snapDir);

	this.dataDir = new File(dataDir, version + VERSION);
	this.snapDir = new File(snapDir, version + VERSION);
	if (!this.dataDir.exists()) {
		if (!this.dataDir.mkdirs()) {
			throw new IOException("Unable to create data directory "
					+ this.dataDir);
		}
	}
	if (!this.dataDir.canWrite()) {
		throw new IOException("Cannot write to data directory " + this.dataDir);
	}
	if (!this.snapDir.exists()) {
		if (!this.snapDir.mkdirs()) {
			throw new IOException("Unable to create snap directory "
					+ this.snapDir);
		}
	}
	if (!this.snapDir.canWrite()) {
		throw new IOException("Cannot write to snap directory " + this.snapDir);
	}
	if(!this.dataDir.getPath().equals(this.snapDir.getPath())){
		checkLogDir();
		checkSnapDir();
	}

	txnLog = new FileTxnLog(this.dataDir);
	snapLog = new FileSnap(this.snapDir);
}
複製程式碼

上面的好理解,如果檔案不存在就去建立,並檢查是否擁有寫入許可權。

中間還有個判斷很有意思,如果兩個檔案路徑不相同,還要呼叫checkLogDir、checkSnapDir去檢查。檢查什麼呢?就是不能放在一起。

事務日誌檔案目錄下,不能包含快照檔案。 快照檔案目錄下,也不能包含事務日誌檔案。

最後,就是初始化兩個實現類,把建立後的檔案物件告訴它們。

3、啟動服務

伺服器的啟動對應著兩個實現:NIO伺服器和Netty伺服器。所以一開始要呼叫createFactory來選擇例項化一個實現類。

static public ServerCnxnFactory createFactory() throws IOException {
	String serverCnxnFactoryName =
		System.getProperty("zookeeper.serverCnxnFactory");
	if (serverCnxnFactoryName == null) {
		serverCnxnFactoryName = NIOServerCnxnFactory.class.getName();
	}
	try {
		ServerCnxnFactory serverCnxnFactory = Class.forName(serverCnxnFactoryName)
				.getDeclaredConstructor().newInstance();
		return serverCnxnFactory;
	} catch (Exception e) {
		IOException ioe = new IOException("Couldn't instantiate "
				+ serverCnxnFactoryName);
		ioe.initCause(e);
		throw ioe;
	}
}
複製程式碼

先獲取zookeeper.serverCnxnFactory屬性值,如果它為空,預設建立的就是NIOServerCnxnFactory例項。

所以,如果我們希望用Netty啟動,就可以這樣設定: System.setProperty("zookeeper.serverCnxnFactory", NettyServerCnxnFactory.class.getName());

最後通過反射獲取它們的構造器並例項化。然後呼叫它們的方法來繫結埠,啟動服務。兩者差異不大,在這裡我們們以Netty為例看一下。

  • 建構函式
NettyServerCnxnFactory() {
	bootstrap = new ServerBootstrap(
			new NioServerSocketChannelFactory(
					Executors.newCachedThreadPool(),
					Executors.newCachedThreadPool()));
	bootstrap.setOption("reuseAddress", true);
	bootstrap.setOption("child.tcpNoDelay", true);
	bootstrap.setOption("child.soLinger", -1);
	bootstrap.getPipeline().addLast("servercnxnfactory", channelHandler);
}
複製程式碼

在建構函式中,初始化ServerBootstrap物件,設定TCP引數。我們重點關注的是,它的事件處理器channelHandler。

  • 事件處理器

這裡的channelHandler是一個內部類,繼承自SimpleChannelHandler。它被標註為@Sharable,還是一個共享的處理器。

@Sharable
class CnxnChannelHandler extends SimpleChannelHandler {
	
	//客戶端連線被關閉
	public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e)throws Exception{
		//移除相應的Channel
		allChannels.remove(ctx.getChannel());
	}
	//客戶端連線
	public void channelConnected(ChannelHandlerContext ctx,ChannelStateEvent e) throws Exception{
		allChannels.add(ctx.getChannel());
		NettyServerCnxn cnxn = new NettyServerCnxn(ctx.getChannel(),
				zkServer, NettyServerCnxnFactory.this);
		ctx.setAttachment(cnxn);
		addCnxn(cnxn);
	}
	//連線斷開
	public void channelDisconnected(ChannelHandlerContext ctx,ChannelStateEvent e) throws Exception{
		NettyServerCnxn cnxn = (NettyServerCnxn) ctx.getAttachment();
		if (cnxn != null) {
			cnxn.close();
		}
	}
	//發生異常
	public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e)throws Exception{
		NettyServerCnxn cnxn = (NettyServerCnxn) ctx.getAttachment();
		if (cnxn != null) {
			if (LOG.isDebugEnabled()) {
				LOG.debug("Closing " + cnxn);
			}
			cnxn.close();
		}
	}
	//有訊息可讀
	public void messageReceived(ChannelHandlerContext ctx, MessageEvent e)throws Exception{
		try {
			//找到對應的NettyServerCnxn,呼叫方法處理請求資訊
			NettyServerCnxn cnxn = (NettyServerCnxn)ctx.getAttachment();
			synchronized(cnxn) {
				processMessage(e, cnxn);
			}
		} catch(Exception ex) {
			LOG.error("Unexpected exception in receive", ex);
			throw ex;
		}
	}
	//處理訊息
	private void processMessage(MessageEvent e, NettyServerCnxn cnxn) {
		....省略
	}
}
複製程式碼

這裡面就是處理各種IO事件。比如客戶端連線、斷開連線、可讀訊息...

我們看messageReceived方法。當有訊息請求時,呼叫到此方法。它會找到當前Channel對應的NettyServerCnxn物件,呼叫其receiveMessage方法,來完成具體請求的處理。

  • 繫結埠

初始化完成之後,通過bootstrap.bind來繫結埠,正式開始對外提供服務。

public class NettyServerCnxnFactory extends ServerCnxnFactory {
	public void start() {
		LOG.info("binding to port " + localAddress);
		parentChannel = bootstrap.bind(localAddress);
	}
}
複製程式碼

上面我們呼叫start方法啟動了Netty服務,但整個zookeeper的啟動過程還沒有完成。

public void startup(ZooKeeperServer zks) throws IOException,InterruptedException {
	start();
	setZooKeeperServer(zks);
	zks.startdata();
	zks.startup();
}
複製程式碼

三、載入資料

接著我們看zks.startdata(); 它要從zookeeper資料庫載入資料。

有的同學不禁有疑問,什麼,zk竟然還有資料庫? 不著急,我們慢慢看。

public class ZooKeeperServer implements SessionExpirer, ServerStats.Provider {
	//載入資料
    public void startdata()throws IOException, InterruptedException {	
        //剛啟動的時候,zkDb為空,先去初始化。
        if (zkDb == null) {
            zkDb = new ZKDatabase(this.txnLogFactory);
        }  
		//載入資料
        if (!zkDb.isInitialized()) {
            loadData();
        }
    }
}
複製程式碼

上面的程式碼中,在剛啟動的時候zkDb為空,所以會進入第一個條件判斷,呼叫構造方法,初始化zkDb。之後,呼叫loadData方法載入資料。

1、ZKDatabase

事實上,zookeeper並沒有資料庫,有的只是ZKDatabase這個類,或者叫它記憶體資料庫。 我們先看看它有哪些屬性。

public class ZKDatabase {    
	//資料樹
    protected DataTree dataTree;
	//Session超時會話
    protected ConcurrentHashMap<Long, Integer> sessionsWithTimeouts;
	//事務、快照Log
    protected FileTxnSnapLog snapLog;
	//最小、最大事務ID
    protected long minCommittedLog, maxCommittedLog;
    public static final int commitLogCount = 500;
    protected static int commitLogBuffer = 700;
	//事務日誌列表,記錄著提案資訊
    protected LinkedList<Proposal> committedLog = new LinkedList<Proposal>();
    protected ReentrantReadWriteLock logLock = new ReentrantReadWriteLock();
	//初始化標記
    volatile private boolean initialized = false;
}
複製程式碼

這裡麵包括會話,資料樹和提交日誌。所有的資料都儲存在DataTree中,它就是資料樹,它儲存所有的節點資料。

public class DataTree {
	//雜湊表提供對資料節點的快速查詢
    private final ConcurrentHashMap<String, DataNode> nodes =
        new ConcurrentHashMap<String, DataNode>();

	//Watcher相關
    private final WatchManager dataWatches = new WatchManager();
    private final WatchManager childWatches = new WatchManager();
	
	//zookeeper預設建立的節點
    private static final String rootZookeeper = "/";
    private static final String procZookeeper = "/zookeeper";
    private static final String procChildZookeeper = procZookeeper.substring(1);
    private static final String quotaZookeeper = "/zookeeper/quota";
    private static final String quotaChildZookeeper = quotaZookeeper
            .substring(procZookeeper.length() + 1);
}
複製程式碼

在我們從zookeeper上查詢節點資料的時候,就是通過DataTree中的方法去獲取。再具體就是通過節點名稱去nodes雜湊表去查詢。比如:

public byte[] getData(String path, Stat stat, Watcher watcher){
	DataNode n = nodes.get(path);
	if (n == null) {
		throw new KeeperException.NoNodeException();
	}
	synchronized (n) {
		n.copyStat(stat);
		if (watcher != null) {
			dataWatches.addWatch(path, watcher);
		}
		return n.data;
	}
}
複製程式碼

那我們也許已經想到了,DataNode才會儲存資料的真正載體。

public class DataNode implements Record {    
	//父級節點
    DataNode parent;
	//節點資料內容
    byte data[];
    //許可權資訊
    Long acl;
    //節點統計資訊
    public StatPersisted stat;
	//子節點集合
    private Set<String> children = null;
	//空Set物件
    private static final Set<String> EMPTY_SET = Collections.emptySet();
}
複製程式碼

在zookeeper中的一個節點就對應一個DataNode物件。它包含一個父級節點和子節點集合、許可權資訊、節點資料內容、統計資訊,都在此類中表示。

【zookeeper原始碼】啟動流程詳解

2、例項化物件

我們接著回過頭來,繼續看程式碼。如果zkDb為空,就要去例項化它。

public ZKDatabase(FileTxnSnapLog snapLog) {
	dataTree = new DataTree();
	sessionsWithTimeouts = new ConcurrentHashMap<Long, Integer>();
	this.snapLog = snapLog;
}
複製程式碼

這裡就是例項化DataTree物件,初始化超時會話的Map,賦值snapLog 物件。

那麼在DataTree的建構函式中,初始化zookeeper預設的節點,就是往nodes雜湊表中新增DataNode物件。

public DataTree() {
	nodes.put("", root);
	nodes.put(rootZookeeper, root);
	root.addChild(procChildZookeeper);
	nodes.put(procZookeeper, procDataNode);
	procDataNode.addChild(quotaChildZookeeper);
	nodes.put(quotaZookeeper, quotaDataNode);
}
複製程式碼

3、載入資料

如果zkDb還沒有被初始化,那就載入資料庫,並設定為已初始化狀態,然後清理一下過期Session。

public class ZooKeeperServer{

	public void loadData() throws IOException, InterruptedException {
		if(zkDb.isInitialized()){
			setZxid(zkDb.getDataTreeLastProcessedZxid());
		}
		else {
			setZxid(zkDb.loadDataBase());
		}
		//清理過期session
		LinkedList<Long> deadSessions = new LinkedList<Long>();
		for (Long session : zkDb.getSessions()) {
			if (zkDb.getSessionWithTimeOuts().get(session) == null) {
				deadSessions.add(session);
			}
		}
		zkDb.setDataTreeInit(true);
		for (long session : deadSessions) {
			killSession(session, zkDb.getDataTreeLastProcessedZxid());
		}
	}
}
複製程式碼

我們看zkDb.loadDataBase()方法。它將從磁碟檔案中載入資料庫。

public class ZKDatabase {

	//從磁碟檔案中載入資料庫,並返回最大事務ID
	public long loadDataBase() throws IOException {
        long zxid = snapLog.restore(dataTree, s
				essionsWithTimeouts, commitProposalPlaybackListener);
        initialized = true;
        return zxid;
    }
}
複製程式碼

既然是磁碟檔案,那麼肯定就是快照檔案和事務日誌檔案。snapLog.restore將證實這一點。

public class FileTxnSnapLog {
	public long restore(DataTree dt, Map<Long, Integer> sessions, 
			PlayBackListener listener) throws IOException {	
		//從快照檔案中載入資料
        snapLog.deserialize(dt, sessions);
		//從事務日誌檔案中載入資料
        long fastForwardFromEdits = fastForwardFromEdits(dt, sessions, listener);
        return fastForwardFromEdits;
    }
}
複製程式碼

載入資料的過程看起來比較複雜,但核心就一點:從檔案流中讀取資料,轉換成DataTree物件,放入zkDb中。在這裡,我們們先不看解析檔案的過程,就看看檔案裡存放的到底是些啥?

快照檔案

我們找到org.apache.zookeeper.server.SnapshotFormatter,它可以幫我們輸出快照檔案內容。在main方法中,設定一下快照檔案的路徑,然後執行它。

public class SnapshotFormatter {
	public static void main(String[] args) throws Exception {		
		//設定快照檔案路徑
		args = new String[1];
		args[0] = "E:\\zookeeper-data\\version-2\\snapshot.6";
		if (args.length != 1) {
			System.err.println("USAGE: SnapshotFormatter snapshot_file");
			System.exit(2);
		}
		new SnapshotFormatter().run(args[0]);
	}
}
複製程式碼

執行這個main方法,在控制檯輸出的就是快照檔案內容。

ZNode Details (count=8):
----
/
  cZxid = 0x00000000000000
  ctime = Thu Jan 01 08:00:00 CST 1970
  mZxid = 0x00000000000000
  mtime = Thu Jan 01 08:00:00 CST 1970
  pZxid = 0x00000000000002
  cversion = 1
  dataVersion = 0
  aclVersion = 0
  ephemeralOwner = 0x00000000000000
  dataLength = 0
----
/zookeeper
  cZxid = 0x00000000000000
  ctime = Thu Jan 01 08:00:00 CST 1970
  mZxid = 0x00000000000000
  mtime = Thu Jan 01 08:00:00 CST 1970
  pZxid = 0x00000000000000
  cversion = 0
  dataVersion = 0
  aclVersion = 0
  ephemeralOwner = 0x00000000000000
  dataLength = 0
----
/zookeeper/quota
  cZxid = 0x00000000000000
  ctime = Thu Jan 01 08:00:00 CST 1970
  mZxid = 0x00000000000000
  mtime = Thu Jan 01 08:00:00 CST 1970
  pZxid = 0x00000000000000
  cversion = 0
  dataVersion = 0
  aclVersion = 0
  ephemeralOwner = 0x00000000000000
  dataLength = 0
----
/test
  cZxid = 0x00000000000002
  ctime = Sat Feb 23 19:57:43 CST 2019
  mZxid = 0x00000000000002
  mtime = Sat Feb 23 19:57:43 CST 2019
  pZxid = 0x00000000000005
  cversion = 3
  dataVersion = 0
  aclVersion = 0
  ephemeralOwner = 0x00000000000000
  dataLength = 4
----
/test/t1
  cZxid = 0x00000000000003
  ctime = Sat Feb 23 19:57:53 CST 2019
  mZxid = 0x00000000000003
  mtime = Sat Feb 23 19:57:53 CST 2019
  pZxid = 0x00000000000003
  cversion = 0
  dataVersion = 0
  aclVersion = 0
  ephemeralOwner = 0x00000000000000
  dataLength = 4
----
/test/t2
  cZxid = 0x00000000000004
  ctime = Sat Feb 23 19:57:56 CST 2019
  mZxid = 0x00000000000004
  mtime = Sat Feb 23 19:57:56 CST 2019
  pZxid = 0x00000000000004
  cversion = 0
  dataVersion = 0
  aclVersion = 0
  ephemeralOwner = 0x00000000000000
  dataLength = 4
----
/test/t3
  cZxid = 0x00000000000005
  ctime = Sat Feb 23 19:57:58 CST 2019
  mZxid = 0x00000000000005
  mtime = Sat Feb 23 19:57:58 CST 2019
  pZxid = 0x00000000000005
  cversion = 0
  dataVersion = 0
  aclVersion = 0
  ephemeralOwner = 0x00000000000000
  dataLength = 4
----
Session Details (sid, timeout, ephemeralCount):
0x10013d3939a0000, 99999, 0
0x10013d1adcb0000, 99999, 0
複製程式碼

我們可以看到,格式化後的快照檔案內容,除了開頭的count資訊和結尾的Session資訊,中間每一行就是一個DataNode物件。從節點名稱可以推算出自己的父級節點和子節點,其它的就是此節點的統計資訊物件StatPersisted。

事務日誌檔案

我們找到org.apache.zookeeper.server.LogFormatter這個類,在main方法中設定事務日誌的檔案路徑,然後執行它。在zookeeper中的每一個事務操作,都會被記錄下來。

19-2-23 下午07時57分32秒 session 0x10013d1adcb0000 cxid 0x0 zxid 0x1 createSession 99999

19-2-23 下午07時57分43秒 session 0x10013d1adcb0000 cxid 0x2 zxid 0x2 create '/test,#31323334,v{s{31,s{'world,'anyone}}},F,1

19-2-23 下午07時57分53秒 session 0x10013d1adcb0000 cxid 0x3 zxid 0x3 create '/test/t1,#31323334,v{s{31,s{'world,'anyone}}},F,1

19-2-23 下午07時57分56秒 session 0x10013d1adcb0000 cxid 0x4 zxid 0x4 create '/test/t2,#31323334,v{s{31,s{'world,'anyone}}},F,2

19-2-23 下午07時57分58秒 session 0x10013d1adcb0000 cxid 0x5 zxid 0x5 create '/test/t3,#31323334,v{s{31,s{'world,'anyone}}},F,3

19-2-23 下午07時58分51秒 session 0x10013d3939a0000 cxid 0x0 zxid 0x6 createSession 99999

19-2-23 下午07時59分07秒 session 0x10013d3939a0000 cxid 0x4 zxid 0x7 create '/test/t4,#31323334,v{s{31,s{'world,'anyone}}},F,4
複製程式碼

可以看到,每一個事務對應一行記錄。包含操作時間、sessionId、事務ID、操作型別、節點名稱和許可權資訊等。 需要注意的是,只有變更操作才會被記錄到事務日誌。所以,在這裡我們看不到任何讀取操作請求。

四、會話管理器

會話是Zookeeper中一個重要的抽象。保證請求有序、臨時znode節點、監聽點都和會話密切相關。Zookeeper伺服器的一個重要任務就是跟蹤並維護這些會話。

在zookeeper中,伺服器要負責清理掉過期會話,而客戶端要保持自己的活躍狀態,只能依靠心跳資訊或者一個新的讀寫請求。

而對於過期會話的管理,則依靠“分桶策略”來完成。具體情況是這樣的:

  • 1、zookeeper會為每個會話設定一個過期時間,我們稱它為nextExpirationTime
  • 2、將這個過期時間和相對應的Session集合放入Map中
  • 3、開啟執行緒,不斷輪訓這個Map,取出當前過期點nextExpirationTime的Session集合,然後關閉它們
  • 4、未活躍的Session被關閉;正在活躍的Session會重新計算自己的過期時間,修改自己的過期時間nextExpirationTime,保證不會被執行緒掃描到

簡而言之,還在活躍的Session依靠不斷重置自己的nextExpirationTime時間,就不會被執行緒掃描到,繼而被關閉。

接下來我們看呼叫到的zks.startup();方法,具體是怎麼做的。

public class ZooKeeperServer

	public synchronized void startup() {
		if (sessionTracker == null) {
			createSessionTracker();
		}
		startSessionTracker();
		setupRequestProcessors();
		registerJMX();
		setState(State.RUNNING);
		notifyAll();
	}
}
複製程式碼

我們只關注createSessionTracker、startSessionTracker兩個方法,它們和會話相關。

1、建立會話跟蹤器

建立會話跟蹤器,這裡是一個SessionTrackerImpl物件例項。

protected void createSessionTracker() {
	sessionTracker = new SessionTrackerImpl(this, zkDb.getSessionWithTimeOuts(),
			tickTime, 1, getZooKeeperServerListener());
}
複製程式碼

在構造方法裡,做了一些引數初始化的工作。

public SessionTrackerImpl(SessionExpirer expirer,
		ConcurrentHashMap<Long, Integer> sessionsWithTimeout, int tickTime,
		long sid, ZooKeeperServerListener listener){
		
	super("SessionTracker", listener);
	this.expirer = expirer;
	this.expirationInterval = tickTime;
	this.sessionsWithTimeout = sessionsWithTimeout;
	nextExpirationTime = roundToInterval(Time.currentElapsedTime());
	this.nextSessionId = initializeNextSession(sid);
	for (Entry<Long, Integer> e : sessionsWithTimeout.entrySet()) {
		addSession(e.getKey(), e.getValue());
	}
}
複製程式碼

我們重點關注下一個過期時間nextExpirationTime是怎樣計算出來的。我們來看roundToInterval方法。

private long roundToInterval(long time) {
	return (time / expirationInterval + 1) * expirationInterval;
}
複製程式碼

其中,time是基於當前時間的一個時間戳;expirationInterval是我們配置檔案中的tickTime。如果我們假定time=10,expirationInterval=2,那麼上面計算後的下一個過期時間為(10/2+1)*2=12

這也就是說,當前的Session會被分配到Id為12的分桶中。我們繼續往下看這一過程。 在addSession方法中,先查詢是否有會話Id的SessionImpl,沒有則新建並存入。

synchronized public void addSession(long id, int sessionTimeout) {
	
	sessionsWithTimeout.put(id, sessionTimeout);
	//查詢對應SessionId的Impl類
	if (sessionsById.get(id) == null) {
		SessionImpl s = new SessionImpl(id, sessionTimeout, 0);
		sessionsById.put(id, s);
	} else {
		if (LOG.isTraceEnabled()) {
			ZooTrace.logTraceMessage(LOG, ZooTrace.SESSION_TRACE_MASK,
					"SessionTrackerImpl --- Existing session 0x"
					+ Long.toHexString(id) + " " + sessionTimeout);
		}
	}
	touchSession(id, sessionTimeout);
}
複製程式碼

最後呼叫touchSession來啟用會話。需要注意的是,zookeeper中的每個請求都會呼叫到此方法。它來計算活躍Session的下一個過期時間,並遷移到不同桶中。

我們一直在說“分桶”,或許難以理解“桶”到底是個什麼東西。在程式碼中,它其實是個HashSet物件。

public class SessionTrackerImpl{
		
	//過期時間和對應Session集合的對映
	HashMap<Long, SessionSet> sessionSets = new HashMap<Long, SessionSet>();	
	//Session集合
	static class SessionSet {
        HashSet<SessionImpl> sessions = new HashSet<SessionImpl>();
    }
	
	synchronized public boolean touchSession(long sessionId, int timeout) {
	
		SessionImpl s = sessionsById.get(sessionId);
		//如果session被刪除或者已經被標記為關閉狀態
		if (s == null || s.isClosing()) {
			return false;
		}
		//計算下一個過期時間
		long expireTime = roundToInterval(Time.currentElapsedTime() + timeout);
		if (s.tickTime >= expireTime) {
			return true;
		}
		
		//獲取Session當前的過期時間
		SessionSet set = sessionSets.get(s.tickTime);
		if (set != null) {
			//從集合中刪除
			set.sessions.remove(s);
		}
		
		//設定新的過期時間並加入到Session集合中
		s.tickTime = expireTime;
		set = sessionSets.get(s.tickTime);
		if (set == null) {
			set = new SessionSet();
			sessionSets.put(expireTime, set);
		}
		set.sessions.add(s);
		return true;
	}
}
複製程式碼

我們回頭看上面那個公式,如果第一次Session請求計算後的過期時間為12。 那麼,對應Session的對映如下: 12=org.apache.zookeeper.server.SessionTrackerImpl$SessionSet@25143a5e 第二次請求,計算後的過期時間為15。就會變成: 15=org.apache.zookeeper.server.SessionTrackerImpl$SessionSet@3045314d

同時,過期時間為12的記錄被刪除。這樣,通過過期時間的變更,不斷遷移這個Session的位置。我們就會想到,如果由於網路原因或者客戶端假死,請求長時間未能到達伺服器,那麼對應Session的過期時間就不會發生變化。 **時代在變化,你不變,就會被拋棄。**這句話,同樣適用於zookeeper中的會話。

我們接著看startSessionTracker();

protected void startSessionTracker() {
	((SessionTrackerImpl)sessionTracker).start();
}
複製程式碼

SessionTrackerImpl繼承自ZooKeeperCriticalThread,所以它本身也是執行緒類。呼叫start方法後開啟執行緒,我們看run方法。

synchronized public void run() {
	try {
		while (running) {
			currentTime = Time.currentElapsedTime();
			if (nextExpirationTime > currentTime) {
				this.wait(nextExpirationTime - currentTime);
				continue;
			}	
			SessionSet set;
			//獲取過期時間對應的Session集合
			set = sessionSets.remove(nextExpirationTime);
			//迴圈Session,關閉它們
			if (set != null) {
				for (SessionImpl s : set.sessions) {
					setSessionClosing(s.sessionId);
					expirer.expire(s);
				}
			}
			nextExpirationTime += expirationInterval;
		}
	} catch (InterruptedException e) {
		handleException(this.getName(), e);
	}
	LOG.info("SessionTrackerImpl exited loop!");
}
複製程式碼

這個方法通過死迴圈的方式,不斷獲取過期時間對應的Session集合。簡直就是發現一起,查處一起 。 這也就解釋了為什麼活躍Session必須要不斷更改自己的過期時間,因為這裡有人在監督。

最後就是註冊了JMX,並設定伺服器的執行狀態。

五、總結

本文主要分析了zookeeper伺服器啟動的具體流程,我們再回顧一下。

  • 配置 zoo.cfg檔案,執行Main方法
  • 註冊zk伺服器關閉事件,清理資源
  • 選擇NIO或者Netty伺服器繫結埠,開啟服務
  • 初始化zkDB,載入磁碟檔案到記憶體
  • 建立會話管理器,監視過期會話並刪除
  • 註冊JMX,設定zk服務狀態為running

相關文章