前文閱讀:
【ZooKeeper系列】1.ZooKeeper單機版、偽叢集和叢集環境搭建
【ZooKeeper系列】2.用Java實現ZooKeeper API的呼叫

在系列的前兩篇文章中,介紹了ZooKeeper環境的搭建(包括單機版、偽叢集和叢集),對建立、刪除、修改節點等場景用命令列的方式進行了測試,讓大家對ZooKeeper環境搭建及常用命令列有初步的認識,也為搭建ZooKeeper的開發環境、生產環境起到了拋磚引玉的作用。也介紹了用Java來實現API的呼叫,包括節點的增、刪、改、查。通過對這兩篇的學習,讓大家對ZooKeeper的使用有了初步認識,也可用於實現系列後面篇章要介紹的命名服務、叢集管理、分散式鎖、負載均衡、分散式佇列等。

在前兩篇中,強調了閱讀英文文件的重要性,也帶領大家解讀了部分官方文件,想傳達出的理念是ZooKeeper沒有想象中的那麼難,閱讀官方文件也沒那麼難。後面的篇章中,結合官方文件,在實戰演練和解讀原始碼的基礎上加深理解。

上聯:說你行你就行不行也行
下聯:說不行就不行行也不行
橫批:不服不行
閱讀原始碼就跟這個對聯一模一樣,就看你選上聯,還是下聯了!

這一篇開始原始碼環境的搭建,here we go

很多老鐵留言說很想研讀些github上的開源專案,但程式碼clone下來後總出現這樣或那樣奇奇怪怪的問題,很影響學習的積極性。學習ZooKeeper的原始碼尤其如此,很多人clone程式碼後,報各種錯,提示少各種包。問了下度娘ZooKeeper原始碼環境,搜出來的文章真的差強人意,有些文章錯的竟然非常離譜。這裡我重新搭建了一遍,也會介紹遇到的一些坑。

很多老鐵上來一堆猛操作,從github上下載了ZooKeeper原始碼後,按常規方式匯入IDEA,最後發現少各種包。起初我也是這樣弄的,以為ZooKeeper是用Maven來構建的,仔細去了解了下ZooKeeper的版本歷史,其實是用的Ant。如今一般用的Maven或Gradle,很少見到Ant的專案了,這裡不對Ant多做介紹。

1 Ant環境搭建

Ant官網地址:https://ant.apache.org/bindownload.cgi

下載解壓後,跟配置jdk一樣配置幾個環境變數:

//修改為自己本地安裝的目錄
ANT_HOMT=D:\apache-ant-1.10.7 
PATH=%ANT_HOME%/bin
CLASSPATH=%ANT_HOME%/lib

配置好後,測試下Ant是否安裝成功。ant -version,得到如下資訊則代表安裝成功:

Apache Ant(TM) version 1.10.7 compiled on September 1 2019

Ant的安裝跟JDK的安裝和配置非常相似,這裡不做過多介紹。

2 下載ZooKeeper原始碼

原始碼地址:https://github.com/apache/zookeeper

猿人谷在寫本篇文章時,releases列表裡的最新版本為release-3.5.6,我們以此版本來進行原始碼環境的搭建。

3 編譯ZooKeeper原始碼

切換到原始碼所在目錄,執行ant eclipse將專案編譯並轉成eclipse的專案結構。

這個編譯過程會比較長,差不多等了7分鐘。如果編譯成功,會出現如下結果:

4 匯入IDEA

上面已經將專案編譯並轉成eclipse的專案結構,按eclipse的形式匯入專案。

5 特別說明

將原始碼匯入IDEA後在org.apache.zookeeper.Version中發現很多紅色警告,很明顯少了org.apache.zookeeper.version.Info類。

查詢原始碼得知是用來發布的時候生成版本用的,我們只是研讀原始碼,又不釋出版本所以直接寫死就ok了。

即新增Info類:

package org.apache.zookeeper.version;

public interface Info {
    int MAJOR = 3;
    int MINOR = 5;
    int MICRO = 6;
    String QUALIFIER = null;
    String REVISION_HASH = "c11b7e26bc554b8523dc929761dd28808913f091";
    String BUILD_DATE = "10/08/2019 20:18 GMT";
}

6 啟動zookeeper

針對單機版本和叢集版本,分別對應兩個啟動類:

  • 單機:ZooKeeperServerMain
  • 叢集:QuorumPeerMain

這裡我們只做單機版的測試。

在conf目錄裡有個zoo_sample.cfg,複製一份重新命名為zoo.cfg。

zoo.cfg裡的內容做點修改(也可以不做修改),方便日誌查詢。dataDir和dataLogDir根據自己的情況設定。

dataDir=E:\\02private\\1opensource\\zk\\zookeeper\\dataDir
dataLogDir=E:\\02private\\1opensource\\zk\\zookeeper\\dataLogDir

執行主類 org.apache.zookeeper.server.ZooKeeperServerMain,將zoo.cfg的完整路徑配置在Program arguments。

執行ZooKeeperServerMain,得到的結果如下:

Connected to the target VM, address: '127.0.0.1:0', transport: 'socket'
log4j:WARN No appenders could be found for logger (org.apache.zookeeper.jmx.ManagedUtil).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.

告知日誌無法輸出,日誌檔案配置有誤。這裡需要指定日誌檔案log4j.properties。

在VM options配置,即指定到conf目錄下的log4j.properties:

-Dlog4j.configuration=file:E:/02private/1opensource/zk/zookeeper/conf/log4j.properties

配置後重新執行ZooKeeperServerMain,輸出日誌如下,

可以得知單機版啟動成功,單機版服務端地址為127.0.0.1:2181。

7 啟動客戶端

通過執行ZooKeeperServerMain得到的日誌,可以得知ZooKeeper服務端已經啟動,服務的地址為127.0.0.1:2181。啟動客戶端來進行連線測試。

客戶端的啟動類為org.apache.zookeeper.ZooKeeperMain,進行如下配置:

即客戶端連線127.0.0.1:2181,獲取節點/yuanrengu的資訊。

下面帶領大家一起看看客戶端啟動的原始碼(org.apache.zookeeper.ZooKeeperMain)。這裡要給大家說下我閱讀原始碼的習慣,很多老鐵以為閱讀原始碼就是順著程式碼看,這樣也沒啥不對,只是很多開源專案程式碼量驚人,這麼個幹看法,容易注意力分散也容易看花眼。我一般是基於某個功能點,從入口開始debug跑一遍,弄清這個功能的“程式碼線”,就像跑馬圈塊地兒一樣,弄清楚功能有關的程式碼,瞭解引數傳遞的過程,這樣看程式碼時就更有針對性,也能排除很多幹擾程式碼。

7.1 main

main裡就兩行程式碼,通過debug得知args裡包含的資訊就是上面我們配置在Program arguments裡的資訊:

7.1.1 ZooKeeperMain

    public ZooKeeperMain(String args[]) throws IOException, InterruptedException {
        // 用於解析引數裡的命令列的
        cl.parseOptions(args);
        System.out.println("Connecting to " + cl.getOption("server"));
        // 用於連線ZooKeeper服務端
        connectToZK(cl.getOption("server"));
    }

通過下圖可以看出,解析引數後,就嘗試連線127.0.0.1:2181,即ZooKeeper服務端。cl.getOption("server")得到的就是127.0.0.1:2181。

7.1.2 parseOptions


可以很清楚的得知解析args的過程,主要從"-server","-timeout","-r","-"這幾個維度來進行解析。

7.1.3 connectToZK

    protected void connectToZK(String newHost) throws InterruptedException, IOException {
        // 用於判斷現在ZooKeeper連線是否還有效
        // zk.getState().isAlive() 注意這個會話是否有效的判斷,客戶端與 Zookeeper連線斷開不一定會話失效
        if (zk != null && zk.getState().isAlive()) {
            zk.close();
        }

        // 此時newHost為127.0.0.1:2181
        host = newHost;
        // 判斷是否為只讀模式,關於只讀模式的概念在前一篇文章中有介紹
        boolean readOnly = cl.getOption("readonly") != null;
        // 用於判斷是否建立安全連線
        if (cl.getOption("secure") != null) {
            System.setProperty(ZKClientConfig.SECURE_CLIENT, "true");
            System.out.println("Secure connection is enabled");
        }
        zk = new ZooKeeperAdmin(host, Integer.parseInt(cl.getOption("timeout")), new MyWatcher(), readOnly);
    }

ZKClientConfig.SECURE_CLIENT已經被標註為deprecation了:

    /**
     * Setting this to "true" will enable encrypted client-server communication.
     */
    @SuppressWarnings("deprecation")
    public static final String SECURE_CLIENT = ZooKeeper.SECURE_CLIENT;

debug檢視關鍵點處的資訊,可以得知這是建立一個ZooKeeper連線的過程(【ZooKeeper系列】2.用Java實現ZooKeeper API的呼叫,這篇文章裡詳細介紹過ZooKeeper建立連線的過程)

下圖看看幾處關鍵資訊:

Integer.parseInt(cl.getOption("timeout"))為30000。

至此完成了ZooKeeperMain main = new ZooKeeperMain(args);的整個過程。簡短點說就是:

  1. 解析Program arguments裡的引數
  2. 連線ZooKeeper服務端

7.2 main.run()

敲黑板,重頭戲來了哦!

一起來看下run()的程式碼:

    void run() throws CliException, IOException, InterruptedException {
        // cl.getCommand()得到的是 “get”,就是上文傳進來的
        if (cl.getCommand() == null) {
            System.out.println("Welcome to ZooKeeper!");

            boolean jlinemissing = false;
            // only use jline if it's in the classpath
            try {
                Class<?> consoleC = Class.forName("jline.console.ConsoleReader");
                Class<?> completorC =
                    Class.forName("org.apache.zookeeper.JLineZNodeCompleter");

                System.out.println("JLine support is enabled");

                Object console =
                    consoleC.getConstructor().newInstance();

                Object completor =
                    completorC.getConstructor(ZooKeeper.class).newInstance(zk);
                Method addCompletor = consoleC.getMethod("addCompleter",
                        Class.forName("jline.console.completer.Completer"));
                addCompletor.invoke(console, completor);

                String line;
                Method readLine = consoleC.getMethod("readLine", String.class);
                while ((line = (String)readLine.invoke(console, getPrompt())) != null) {
                    executeLine(line);
                }
            } catch (ClassNotFoundException e) {
                LOG.debug("Unable to start jline", e);
                jlinemissing = true;
            } catch (NoSuchMethodException e) {
                LOG.debug("Unable to start jline", e);
                jlinemissing = true;
            } catch (InvocationTargetException e) {
                LOG.debug("Unable to start jline", e);
                jlinemissing = true;
            } catch (IllegalAccessException e) {
                LOG.debug("Unable to start jline", e);
                jlinemissing = true;
            } catch (InstantiationException e) {
                LOG.debug("Unable to start jline", e);
                jlinemissing = true;
            }

            if (jlinemissing) {
                System.out.println("JLine support is disabled");
                BufferedReader br =
                    new BufferedReader(new InputStreamReader(System.in));

                String line;
                while ((line = br.readLine()) != null) {
                    executeLine(line);
                }
            }
        } else {
            // 處理傳進來的引數
            processCmd(cl);
        }
        System.exit(exitCode);
    }

通過下圖可以看出processCmd(cl);cl包含的資訊:

debug到processCmd(MyCommandOptions co) 就到了決戰時刻。裡面的processZKCmd(MyCommandOptions co)就是核心了,程式碼太長,只說下processZKCmd裡的重點程式碼,獲取節點/yuanrengu的資訊:

因為我之前沒有建立過/yuanrengu節點,會拋異常org.apache.zookeeper.KeeperException$NoNodeException: KeeperErrorCode = NoNode for /yuanrengu , 如下圖所示:

經過上面的步驟後exitCode為1,執行System.exit(exitCode);退出。

至此帶領大家dubug了一遍org.apache.zookeeper.ZooKeeperMain,上面我說過,閱讀原始碼幹看效果很小,只有debug才能有助於梳理流程和思路,也能清楚引數傳遞的過程發生了什麼變化。

溫馨提示

上面我們介紹了原始碼環境的搭建過程,執行執行主類 org.apache.zookeeper.server.ZooKeeperServerMain 啟動ZooKeeper服務端,執行org.apache.zookeeper.ZooKeeperMain連線服務端。

閱讀原始碼最好能動起來(debug)讀,這樣程式碼才是活的,幹看的話程式碼如死水一樣,容易讓人索然無味!

每個人操作的方式不一樣,有可能遇到的問題也不一樣,搭建過程中遇到什麼問題,大家可以在評論區留言。