zookeeper
原始碼分析系列文章:
原創部落格,純手敲,轉載請註明出處,謝謝!
一、Zookeeper
單機啟動原理
Zookeeper
屬於C/S
架構,也就是傳統的客戶端-伺服器模式,客戶端傳送請求,伺服器響應請求。這和高效能網路框架Netty
是一樣的,因此我們也可以猜想到它的啟動方式無非就是從main()
方法開始,客戶端和伺服器各有一個main()
方法。
那我們先來看看Zookeeper
伺服器端的啟動過程,當你開啟Zookeeper
目錄下/bin
目錄中zkServer.cmd
檔案你就會發現,其實Zookeeper
的啟動入口為org.apache.zookeeper.server.quorum.QuorumPeerMain
類的main
方法,無論你是單機模式啟動Zookeeper
還是複製模式啟動Zookeeper
,執行入口都是這個類,至於如何區別是哪種模式啟動,該類會根據你配置檔案的配置進行判斷,具體的判斷接下來將會詳細講解。
zkServer.cmd
詳細原始碼:
setlocal
call "%~dp0zkEnv.cmd"
set ZOOMAIN=org.apache.zookeeper.server.quorum.QuorumPeerMain // 設定主類入口
echo on
call %JAVA% "-Dzookeeper.log.dir=%ZOO_LOG_DIR%" "-Dzookeeper.root.logger=%ZOO_LOG4J_PROP%" -cp "%CLASSPATH%" %ZOOMAIN% "%ZOOCFG%" %* // 執行該類的main()方法
endlocal
複製程式碼
下面先看看單機啟動時序圖:
1、首先執行main
方法
2、解析傳進來的配置檔案路徑,預設會去${baseDir}\conf\zoo.cfg
找配置檔案
3、建立NIOServerCnxnFactory
進行監聽客戶端的連線請求,在Zookeeper
中有兩種ServerCnxnFactory
,一種是NIOServerCnxnFactory
,另一種是NettyServerCnxnFactory
,前者為預設工廠,後者除非你在啟動main
方法時指定System
的zookeeper.serverCnxnFactory
屬性值為NettyServerCnxnFactory
。
下面將詳細深入原始碼分析各個階段是如何實現以及工作的。
二、Zookeeper
單機模式(standalone
)啟動
- 1、
Zookeeper
是如何解析配置檔案的?
zk的屬性配置分為兩種:
1、
Java System property
:Java系統環境變數,也就是System.setProperty()
設定的引數2、
No Java system property
配置檔案屬性,也就是你在配置檔案中配置的屬性
配置檔案的解析原理很簡單,無非就是解析一些.properties
檔案中的鍵值對,其實Java
已經提供了Properties
類來代表.properties
檔案中所有鍵值對集合,我們可以使用Properties
物件的load()
方法將一個配置檔案裝載進記憶體,然後對該物件進行遍歷就得到我們鎖配置的屬性值集合了。
說到Zookeeper
中的配置檔案解析,原理也和上面差不多,只不過是在變數鍵值對的時候多了一些Zookeeper
自身的邏輯判斷。ZooKeeper
中的配置檔案解析從QuorumPeerConfig
類的parse()
方法說起,原始碼如下:
/**
* Parse a ZooKeeper configuration file 解析一個配置檔案
* @param path the patch of the configuration file
* @throws ConfigException error processing configuration
*/
public void parse(String path) throws ConfigException {
File configFile = new File(path);
LOG.info("Reading configuration from: " + configFile);
try {
if (!configFile.exists()) {
throw new IllegalArgumentException(configFile.toString() + " file is missing");
}
// 宣告一個Properties物件
Properties cfg = new Properties();
FileInputStream in = new FileInputStream(configFile);
try {
// 傳入一個配置檔案輸入流即可裝載所有配置
cfg.load(in);
} finally {
// 涉及到流的操作記得最後將流關閉
in.close();
}
// 此處是zk自身的邏輯處理
parseProperties(cfg);
} catch (IOException e) {
throw new ConfigException("Error processing " + path, e);
} catch (IllegalArgumentException e) {
throw new ConfigException("Error processing " + path, e);
}
}
複製程式碼
接下來我們來看看上面的parseProperties(cfg)
方法,該方法太長了,硬著頭皮啃完:
/**
* Parse config from a Properties.
* @param zkProp Properties to parse from.
* @throws IOException
* @throws ConfigException
*/
public void parseProperties(Properties zkProp) throws IOException, ConfigException {
int clientPort = 0;
String clientPortAddress = null;
// 遍歷所有的key-value鍵值對
for (Entry<Object, Object> entry : zkProp.entrySet()) {
// 注意這裡要把首尾空格去掉
String key = entry.getKey().toString().trim();
String value = entry.getValue().toString().trim();
// 儲存快照檔案snapshot的目錄配置
if (key.equals("dataDir")) {
dataDir = value;
// 事務日誌儲存目錄
} else if (key.equals("dataLogDir")) {
dataLogDir = value;
// 客戶端連線server的埠,zk啟動總得有個埠吧!如果你沒有配置,則會報錯!一般我們會將埠配置為2181
} else if (key.equals("clientPort")) {
clientPort = Integer.parseInt(value);
// 伺服器IP地址
} else if (key.equals("clientPortAddress")) {
clientPortAddress = value.trim();
// zk中的基本事件單位,用於心跳和session最小過期時間為2*tickTime
} else if (key.equals("tickTime")) {
tickTime = Integer.parseInt(value);
// 客戶端併發連線數量,注意是一個客戶端跟一臺伺服器的併發連線數量,也就是說,假設值為3,那麼某個客戶端不能同時併發連線3次到同一臺伺服器(併發嘛!),否則會出現下面錯誤too many connections from /127.0.0.1 - max is 3
} else if (key.equals("maxClientCnxns")) {
maxClientCnxns = Integer.parseInt(value);
} else if (key.equals("minSessionTimeout")) {
minSessionTimeout = Integer.parseInt(value);
} else if (key.equals("maxSessionTimeout")) {
maxSessionTimeout = Integer.parseInt(value);
} else if (key.equals("initLimit")) {
initLimit = Integer.parseInt(value);
} else if (key.equals("syncLimit")) {
syncLimit = Integer.parseInt(value);
} else if (key.equals("electionAlg")) {
electionAlg = Integer.parseInt(value);
} else if (key.equals("quorumListenOnAllIPs")) {
quorumListenOnAllIPs = Boolean.parseBoolean(value);
} else if (key.equals("peerType")) {
if (value.toLowerCase().equals("observer")) {
peerType = LearnerType.OBSERVER;
} else if (value.toLowerCase().equals("participant")) {
peerType = LearnerType.PARTICIPANT;
} else {
throw new ConfigException("Unrecognised peertype: " + value);
}
......
複製程式碼
下面對解析的所有配置項用表格總結下: 所有的配置項都可以在官網查詢到。
下面我們一起看看Zookkeeper
的配置檔案屬性:
配置項 | 說明 | 異常情況 | 是否報錯? | 錯誤 or 備註 |
---|---|---|---|---|
clientPort |
服務端server監聽客戶端連線的埠 | 不配置 | 是 | clientPort is not set |
clientPortAddress |
客戶端連線的伺服器ip地址 | 不配置 | 否 | 預設使用網路卡的地址 |
dataDir |
資料快照目錄 | 不配置 | 是 | dataDir is not set |
dataLogDir |
事務日誌存放目錄 | 不配置 | 否 | 預設跟dataDir 目錄相同 |
tickTime |
ZK基本時間單元(毫秒),用於心跳和超時.minSessionTimeout 預設是兩倍ticket |
不配置 | 是 | tickTime is not set |
maxClientCnxns |
同一ip地址最大併發連線數(也就是說同一個ip最多可以同時維持與伺服器連結的個數) | 不配置 | 否 | 預設最大連線數為60,設定為0則無限制 |
minSessionTimeout |
最小會話超時時間,預設2*ticket | 不配置 | 否,若minSessionTimeout > maxSessionTimeout ,則報錯 |
minSessionTimeout must not be larger than maxSessionTimeout |
maxSessionTimeout |
最大會話超時時間,預設20*ticket | 不配置 | 否 | 不能小於minSessionTimeout |
initLimit |
允許follower 同步和連線到leader 的時間總量,以ticket 為單位 |
不配置 | 是 | initLimit is not set ,如果zk管理的資料量特別大,則辭職應該調大 |
syncLimit |
follower 與leader 之間同步的世間量 |
不配置 | 是 | syncLimit is not set |
electionAlg |
zk選舉演算法選擇,預設值為3,表示採用快速選舉演算法 |
不配置 | 否 | 如果沒有配置選舉地址server ,則拋Missing election port for server: serverid |
quorumListenOnAllIPs |
當設定為true時,ZooKeeper伺服器將偵聽來自所有可用IP地址的對等端的連線,而不僅僅是在配置檔案的伺服器列表中配置的地址(即叢集中配置的server.1,server.2。。。。)。 它會影響處理ZAB協議和Fast Leader Election協議的連線。 預設值為false | 不配置 | 否 | |
peerType |
伺服器的角色,是觀察者observer 還是參與選舉或成為leader ,預設為PARTICIPANT |
不配置 | 否 | 若配置了不知支援的角色,則報Unrecognised peertype: |
autopurge.snapRetainCount |
資料快照保留個數,預設是3,最小也是3 | 不配置 | 否 | |
autopurge.purgeInterval |
執行日誌、快照清除任務的時間間隔(小時) | 不配置 | 否 | 預設是 0 |
server.x=[hostname]:nnnnn[:nnnnn] |
叢集伺服器配置 | 不配置 | 單機:否;叢集:是 | zk叢集啟動將載入該該配置,每臺zk伺服器必須有一個myid檔案,裡邊存放伺服器的id,該id值必須匹配server.x中的x ; 第一個埠表示與leader 連線的埠,第二個埠表示用於選舉的埠,第二個埠是可選的 |
- 2、
Zookeeper
是如何判斷何種模式啟動伺服器的?
因為Zookeeper
的ZkServer.cmd
啟動檔案指定的統一入口為org.apache.zookeeper.server.quorum.QuorumPeerMain
,那麼我們就要問了,那ZK
是怎麼判斷我要單機模式啟動還是叢集方式啟動呢?答案是明顯的,也就是取決於你在配置檔案zoo.cfg
中是否有配置server.x=hostname:port1:port2
,以上的配置項表明我們想讓ZK
以叢集模式執行,那麼在程式碼中是如何體現的呢?
上面講到ZK
解析配置檔案的原理,我們依舊走進parseProperties()
方法,看看如下程式碼:
.....
// 此處解析配置檔案以server.開頭的配置
} else if (key.startsWith("server.")) {
// server.3
int dot = key.indexOf('.');
long sid = Long.parseLong(key.substring(dot + 1));
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");
}
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]);
}
if (parts.length > 3) {
if (parts[3].toLowerCase().equals("observer")) {
type = LearnerType.OBSERVER;
} else if (parts[3].toLowerCase().equals("participant")) {
type = LearnerType.PARTICIPANT;
} else {
throw new ConfigException("Unrecognised peertype: " + value);
}
}
if (type == LearnerType.OBSERVER) {
observers.put(Long.valueOf(sid),
new QuorumServer(sid, hostname, port, electionPort, type));
} else {
// 如果配置了,那麼就加進servers中,其中servers是一個本地快取Map,用於儲存配置的ip地址
servers.put(Long.valueOf(sid), new QuorumServer(sid, hostname, port, electionPort, type));
}
複製程式碼
如果配置了,那麼servers
的size>0
,解析完成之後,回到QuorumPeerMain
的initializeAndRun()
方法:
// 如果servers長度大於0,則叢集方式啟動,否則,單機啟動
if (args.length == 1 && config.servers.size() > 0) {
runFromConfig(config);
} else {
LOG.warn("Either no config or no quorum defined in config, running "
+ " in standalone mode");
// there is only server in the quorum -- run as standalone
ZooKeeperServerMain.main(args);
}
複製程式碼
從上面可以看出,單機啟動的入口為ZooKeeperServerMain
類,而統一的入口類為QuorumPeerMain
,所以,在ZK
中,伺服器端的啟動類就只有這兩個了。
- 3、單機模式下,
Zookeeper
是如何處理客戶端請求的?
無論是哪種方式啟動Zookeeper
,它都必須對客戶端的請求進行處理,那麼ZK
是如何處理客戶端請求的呢?讓我們一起來看看原始碼是怎麼寫的!
上面說到,Zk
單機啟動的入口類為ZooKeeperServerMain
,我們一起看下其runFromConfig()
方法:
/**
* Run from a ServerConfig.
* @param config ServerConfig to use.
* @throws IOException
*/
public void runFromConfig(ServerConfig config) throws IOException {
LOG.info("Starting server");
FileTxnSnapLog txnLog = null;
try {
// 建立一個ZooKeeperServer,ZooKeeperServer代表具體執行的zk伺服器,包含監聽客戶端請求
final ZooKeeperServer zkServer = new ZooKeeperServer();
// 這個是表明上面建立的ZooKeeperServer執行緒執行完之後,當前主執行緒才結束,類似Thread的join()方法
final CountDownLatch shutdownLatch = new CountDownLatch(1);
// 關閉伺服器時的回撥處理器
zkServer.registerServerShutdownHandler(
new ZooKeeperServerShutdownHandler(shutdownLatch));
// 執行快照資料,日誌的定時儲存操作,指定儲存路徑
txnLog = new FileTxnSnapLog(new File(config.dataLogDir), new File(
config.dataDir));
zkServer.setTxnLogFactory(txnLog);
zkServer.setTickTime(config.tickTime);
zkServer.setMinSessionTimeout(config.minSessionTimeout);
zkServer.setMaxSessionTimeout(config.maxSessionTimeout);
// 建立ServerCnxnFactory,預設實現為NIOServerCnxnFactory,也可以指定為NettyServerCnxnFactory
cnxnFactory = ServerCnxnFactory.createFactory();
cnxnFactory.configure(config.getClientPortAddress(),
config.getMaxClientCnxns());
// 啟動伺服器,將一個伺服器zkServer丟給工廠,然後啟動
cnxnFactory.startup(zkServer);
// 這裡將會等待,除非呼叫shutdown()方法
shutdownLatch.await();
shutdown();
// 這裡會等待直到zkServer執行緒完成
cnxnFactory.join();
if (zkServer.canShutdown()) {
zkServer.shutdown(true);
}
} catch (InterruptedException e) {
// warn, but generally this is ok
LOG.warn("Server interrupted", e);
} finally {
if (txnLog != null) {
txnLog.close();
}
}
}
複製程式碼
瞭解完上面的程式碼,我們明白單機啟動ZooKeeperServer
時ZK
做了什麼工作,主要點在zk
建立的是哪種工廠,至於NIOServerCnxnFactory
的程式碼,我就不說了,大家有興趣可以去看看。
迴歸正題,讓我們進入NIOServerCnxnFactory
的run()
方法中看看:
public void run() {
while (!ss.socket().isClosed()) {
try {
// 每一秒輪詢一次
selector.select(1000);
Set<SelectionKey> selected;
synchronized (this) {
selected = selector.selectedKeys();
}
ArrayList<SelectionKey> selectedList = new ArrayList<SelectionKey>(selected);
Collections.shuffle(selectedList);
for (SelectionKey k : selectedList) {
// 如果有讀請求或者連線請求,則接收請求
if ((k.readyOps() & SelectionKey.OP_ACCEPT) != 0) {
SocketChannel sc = ((ServerSocketChannel) k.channel()).accept();
InetAddress ia = sc.socket().getInetAddress();
int cnxncount = getClientCnxnCount(ia);
// 這裡對maxClientCnxns做出判斷,防止DOS攻擊
if (maxClientCnxns > 0 && cnxncount >= maxClientCnxns) {
LOG.warn("Too many connections from " + ia + " - max is " + maxClientCnxns);
sc.close();
} else {
LOG.info("Accepted socket connection from " + sc.socket().getRemoteSocketAddress());
sc.configureBlocking(false);
SelectionKey sk = sc.register(selector, SelectionKey.OP_READ);
NIOServerCnxn cnxn = createConnection(sc, sk);
sk.attach(cnxn);
addCnxn(cnxn);
}
// 如果有讀請求且客戶端之前有連線過的,則直接處理
} else if ((k.readyOps() & (SelectionKey.OP_READ | SelectionKey.OP_WRITE)) != 0) {
NIOServerCnxn c = (NIOServerCnxn) k.attachment();
c.doIO(k);
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("Unexpected ops in select " + k.readyOps());
}
}
}
selected.clear();
} catch (RuntimeException e) {
LOG.warn("Ignoring unexpected runtime exception", e);
} catch (Exception e) {
LOG.warn("Ignoring exception", e);
}
}
closeAll();
LOG.info("NIOServerCnxn factory exited run method");
}
複製程式碼
看到這,我覺得對於Zk
如何監聽處理客戶端的請求就清晰多了,上面的程式碼主要採用輪詢機制,每一秒輪詢一次,通過selector.select(1000)
方法指定,這裡的監聽方式和傳統的BIO不同,傳統的網路監聽採用阻塞的accept()
方法,zk採用java的nio實現。
謝謝閱讀~~