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物件。它包含一個父級節點和子節點集合、許可權資訊、節點資料內容、統計資訊,都在此類中表示。
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