Zookeeper原始碼分析(三) ----- 單機模式(standalone)執行

擁抱心中的夢想發表於2018-05-10

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
複製程式碼

下面先看看單機啟動時序圖:

Zookeeper原始碼分析(三) ----- 單機模式(standalone)執行

1、首先執行main方法 2、解析傳進來的配置檔案路徑,預設會去${baseDir}\conf\zoo.cfg找配置檔案 3、建立NIOServerCnxnFactory進行監聽客戶端的連線請求,在Zookeeper中有兩種ServerCnxnFactory,一種是NIOServerCnxnFactory,另一種是NettyServerCnxnFactory,前者為預設工廠,後者除非你在啟動main方法時指定Systemzookeeper.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 followerleader之間同步的世間量 不配置 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是如何判斷何種模式啟動伺服器的?

因為ZookeeperZkServer.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));
}
複製程式碼

如果配置了,那麼serverssize>0,解析完成之後,回到QuorumPeerMaininitializeAndRun()方法:

 // 如果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();
        }
    }
}
複製程式碼

瞭解完上面的程式碼,我們明白單機啟動ZooKeeperServerZK做了什麼工作,主要點在zk建立的是哪種工廠,至於NIOServerCnxnFactory的程式碼,我就不說了,大家有興趣可以去看看。

迴歸正題,讓我們進入NIOServerCnxnFactoryrun()方法中看看:

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實現。

謝謝閱讀~~

相關文章