Zookeeper源碼分析:集羣模式啓動概述

參考資料

<<從PAXOS到ZOOKEEPER分佈式一致性原理與實踐>>
zookeeper-3.0.0

Zookeeper概述

Zookeeper是一個分佈式的,開放源碼的分佈式應用程序協調服務。致力於提供一個高性能、高可用,具有嚴格的順序訪問控制能力(寫操作嚴格順序)的分佈式協調服務。

Zookeeper集羣啓動

集羣啓動方法與配置文件

查看目錄bin下的zkServer.sh內容;

ZOOBIN=`readlink -f "$0"`
ZOOBINDIR=`dirname "$ZOOBIN"`

. $ZOOBINDIR/zkEnv.sh                                                                               # 設置運行的環境變量

case $1 in
start) 
    echo -n "Starting zookeeper ... "
    java  "-Dzookeeper.log.dir=${ZOO_LOG_DIR}" "-Dzookeeper.root.logger=${ZOO_LOG4J_PROP}" \
    -cp $CLASSPATH $JVMFLAGS org.apache.zookeeper.server.quorum.QuorumPeerMain $ZOOCFG &            # 啓動 zookeeper 啓動類爲 QuorumPeerMain
    echo STARTED
    ;;
stop) 
    echo -n "Stopping zookeeper ... " 
    echo kill | nc localhost $(grep clientPort $ZOOCFG | sed -e 's/.*=//')                          # 殺死進程
    echo STOPPED
    ;;
upgrade)
    shift
    echo "upgrading the servers to 3.*"
    java "-Dzookeeper.log.dir=${ZOO_LOG_DIR}" "-Dzookeeper.root.logger=${ZOO_LOG4J_PROP}" \
    -cp $CLASSPATH $JVMFLAGS org.apache.zookeeper.server.upgrade.UpgradeMain ${@}            
    echo "Upgrading ... "
    ;;
restart)
    shift
    $0 stop ${@}
    sleep 3
    $0 start ${@}                                                                                   # 重啓就是先殺死進程 然後再啓動
    ;;
status)
    STAT=`echo stat | nc localhost $(grep clientPort $ZOOCFG | sed -e 's/.*=//') 2> /dev/null| grep Mode`
    if [ "x$STAT" = "x" ]
    then
        echo "Error contacting service. It is probably not running."                               # 檢查狀態
    else
        echo $STAT
    fi
    ;;
*)
    echo "Usage: $0 {start|stop|restart|status}" >&2

esac

在中斷中輸入zkServer.sh start就可以啓動Zookeeper集羣,啓動的配置文件爲默認的zoo_sample.cfg,如果是集羣啓動,需要修改該配置文件,文件中需要加入多臺集羣的IP信息,並且集羣啓動的時候的配置文件需要相同。參考配置文件如下;

# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial 
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between 
# sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored.
dataDir=/export/crawlspace/mahadev/zookeeper/server1/data
# the port at which the clients will connect
clientPort=2181

#設置集羣信息
server.1=192.168.0.1:2888:3888
server.2=192.168.0.2:2888:3888
server.3=192.168.0.3:2888:3888

在啓動的參數解析過程中可以依次查看各個參數的用途。

Zookeeper集羣啓動流程
public class QuorumPeerMain {
    
    private static final Logger LOG = Logger.getLogger(QuorumPeerMain.class);

    /**
     * To start the replicated server specify the configuration file name on the
     * command line.
     * @param args command line
     */
    public static void main(String[] args) {
        if (args.length == 2) {
            ZooKeeperServerMain.main(args);                                                         // 如果是參數啓動則直接啓動 默認爲單節點啓動
            return;
        }
        QuorumPeerConfig.parse(args);
        if (!QuorumPeerConfig.isStandalone()) {
            runPeer(new QuorumPeer.Factory() {                                                      // 繼承自QuorumPeer.Factory 並實現了其中的接口方法 create 和 createConnectionFactory
                public QuorumPeer create(NIOServerCnxn.Factory cnxnFactory) throws IOException {
                    QuorumPeer peer = new QuorumPeer();                                             // 生成實例
                    peer.setClientPort(ServerConfig.getClientPort());                               // 獲取實例監聽的客戶端端口
                    peer.setTxnFactory(new FileTxnSnapLog(
                                new File(QuorumPeerConfig.getDataLogDir()), 
                                new File(QuorumPeerConfig.getDataDir())));                   
                    peer.setQuorumPeers(QuorumPeerConfig.getServers());                             // 設置Servers配置信息
                    peer.setElectionType(QuorumPeerConfig.getElectionAlg());                        // 設置選舉類型
                    peer.setMyid(QuorumPeerConfig.getServerId());                                   // 設置Serverid 
                    peer.setTickTime(QuorumPeerConfig.getTickTime());                               
                    peer.setInitLimit(QuorumPeerConfig.getInitLimit());
                    peer.setSyncLimit(QuorumPeerConfig.getSyncLimit());
                    peer.setCnxnFactory(cnxnFactory);                                               // 設置網絡客戶端請求處理的框架
                    return peer;
                }
                public NIOServerCnxn.Factory createConnectionFactory() throws IOException {
                    return new NIOServerCnxn.Factory(getClientPort());                              // 找到IO複用的工廠方法
                }
            });
        }else{
            // there is only server in the quorum -- run as standalone
            ZooKeeperServerMain.main(args); 
        }
    }
    
    public static void runPeer(QuorumPeer.Factory qpFactory) {
        try {
            QuorumStats.registerAsConcrete();
            QuorumPeer self = qpFactory.create(qpFactory.createConnectionFactory());                // 創建實例
            self.start();                                                                           // 啓動線程執行
            self.join();                                                                            // 阻塞直到線程退出
        } catch (Exception e) {
            LOG.fatal("Unexpected exception",e);
        }
        System.exit(2);
    }

}

啓動的簡單邏輯流程就是,首先判斷是否是集羣模式啓動,如果是集羣模式啓動,則首先調用QuorumPeerConfig解析配置參數,通過解析參數來判斷是否在配置文件中是否是集羣模式,如果配置中是集羣模式,則調用runPeer方法,該方法主要就是接受一個QuorumPeer.Factory參數,然後調用create方法,然後就調用start方法啓動線程並阻塞等待經常結束。

Created with Raphaël 2.2.0啓動命令行參數長度是否爲2Yes or No?解析配置文件解析是否單機啓動集羣啓動單節點啓動yesnoyesno
配置文件類QuorumPeerConfig
public class QuorumPeerConfig extends ServerConfig {                                        // 繼承自ServerConfig 該類實現了一個配置實例單例模式
    private static final Logger LOG = Logger.getLogger(QuorumPeerConfig.class);

    private int tickTime;
    private int initLimit;
    private int syncLimit;
    private int electionAlg;
    private int electionPort;
    private HashMap<Long,QuorumServer> servers = null;
    private long serverId;

    private QuorumPeerConfig(int port, String dataDir, String dataLogDir) {                // 調用父類的構造方法
        super(port, dataDir, dataLogDir);
    }

    public static void parse(String[] args) {                                             // 解析配置文件參數
        if(instance!=null)
            return;

        try {
            if (args.length != 1) {                                                      // 確保輸入的唯一參數就是配置文件的文件路徑
                System.err.println("USAGE: configFile");
                System.exit(2);
            }
            File zooCfgFile = new File(args[0]);                                        // 生成配置文件類型
            if (!zooCfgFile.exists()) {                                                 // 檢查輸入的配置文件是否存在
                LOG.error(zooCfgFile.toString() + " file is missing");
                System.exit(2);
            }
            Properties cfg = new Properties();     
            FileInputStream zooCfgStream = new FileInputStream(zooCfgFile);             // 讀文件
            try {
                cfg.load(zooCfgStream);
            } finally {
                zooCfgStream.close();
            }
            HashMap<Long,QuorumServer> servers = new HashMap<Long,QuorumServer>();      // 保存集羣機器的信息
            String dataDir = null;
            String dataLogDir = null;
            int clientPort = 0;
            int tickTime = 0;
            int initLimit = 0;
            int syncLimit = 0;
            int electionAlg = 3;
            int electionPort = 2182;
            for (Entry<Object, Object> entry : cfg.entrySet()) {                        // 獲取解析的文件的配置參數
                String key = entry.getKey().toString();                                 // 轉爲string類型
                String value = entry.getValue().toString();
                if (key.equals("dataDir")) {                                            // 文件目錄
                    dataDir = value;
                } else if (key.equals("dataLogDir")) {                                  // 日誌目錄
                    dataLogDir = value;
                } else if (key.equals("clientPort")) {                                  // 客戶端連接端口
                    clientPort = Integer.parseInt(value);
                } else if (key.equals("tickTime")) {                                    // 基本時間間隔
                    tickTime = 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.startsWith("server.")) {                                 // 解析配置集羣IP端口信息
                    int dot = key.indexOf('.');
                    long sid = Long.parseLong(key.substring(dot + 1));                  // 獲取server配置的第一個id 
                    String parts[] = value.split(":");                                  // 獲取  ip  port
                    if ((parts.length != 2) &&                                      
                            (parts.length != 3)){
                        LOG.error(value
                                + " does not have the form host:port or host:port:port");
                    }
                    InetSocketAddress addr = new InetSocketAddress(parts[0],
                            Integer.parseInt(parts[1]));                                // 配置IP Port
                    if(parts.length == 2)
                        servers.put(Long.valueOf(sid), new QuorumServer(sid, addr));
                    else if(parts.length == 3){
                        InetSocketAddress electionAddr = new InetSocketAddress(parts[0],
                                Integer.parseInt(parts[2]));                            // 通信接口監聽
                        servers.put(Long.valueOf(sid), new QuorumServer(sid, addr, electionAddr));   // 壓入server
                    }
                } else {
                    System.setProperty("zookeeper." + key, value);                      // 其他屬性直接設置到類上
                }
            }
            if (dataDir == null) {                                                      // 檢查參數  是否爲空 如果爲空 則報錯
                LOG.error("dataDir is not set");
                System.exit(2);
            }
            if (dataLogDir == null) {
                dataLogDir = dataDir;
            } else {
                if (!new File(dataLogDir).isDirectory()) {
                    LOG.error("dataLogDir " + dataLogDir+ " is missing.");
                    System.exit(2);
                }
            }
            if (clientPort == 0) {
                LOG.error("clientPort is not set");
                System.exit(2);
            }
            if (tickTime == 0) {
                LOG.error("tickTime is not set");
                System.exit(2);
            }
            if (servers.size() > 1 && initLimit == 0) {
                LOG.error("initLimit is not set");
                System.exit(2);
            }
            if (servers.size() > 1 && syncLimit == 0) {
                LOG.error("syncLimit is not set");
                System.exit(2);
            }
            QuorumPeerConfig conf = new QuorumPeerConfig(clientPort, dataDir,
                    dataLogDir);                                                    // 生成一個實例 並設置參數
            conf.tickTime = tickTime;
            conf.initLimit = initLimit;
            conf.syncLimit = syncLimit;
            conf.electionAlg = electionAlg;
            conf.servers = servers;
            if (servers.size() > 1) {                                               // 如果是多個server
                /*
                 * If using FLE, then every server requires a separate election port.
                 */
                if(electionAlg != 0){
                   for(QuorumServer s : servers.values()){
                       if(s.electionAddr == null)
                           LOG.error("Missing election port for server: " + s.id);
                   }
                }
                
                File myIdFile = new File(dataDir, "myid");                        // 檢查myid文件是否存在 該文件包含該實例的id信息
                if (!myIdFile.exists()) {
                    LOG.error(myIdFile.toString() + " file is missing");
                    System.exit(2);
                }
                BufferedReader br = new BufferedReader(new FileReader(myIdFile));       // 獲取server id
                String myIdString;
                try {
                    myIdString = br.readLine();
                } finally {
                    br.close();
                }
                try {
                    conf.serverId = Long.parseLong(myIdString); 
                } catch (NumberFormatException e) {
                    LOG.error(myIdString + " is not a number");
                    System.exit(2);
                }
            }
            instance=conf;                                                      // 將解析好的數據設置到instance 中, 後續的實例信息都是從該實例獲取
        } catch (Exception e) {
            LOG.error("FIXMSG",e);
            System.exit(2);
        }
    }
    ...
}

繼承的父類就是ServerConfig,主要查看該類的parse方法。

    public static int getClientPort(){
        assert instance!=null;
        return instance.clientPort;
    }
    public static String getDataDir(){
        assert instance!=null;
        return instance.dataDir;
    }
    public static String getDataLogDir(){
        assert instance!=null;
        return instance.dataLogDir;
    }
    public static boolean isStandalone(){
        assert instance!=null;
        return instance.isStandaloneServer();
    }
    
    protected static ServerConfig instance=null;
    
    public static void parse(String[] args) {                                              // 解析的時候生成單例
        if(instance!=null)
            return;
        if (args.length != 2) {                                                            // 如果輸入參數長度不爲2
            System.err.println("USAGE: ZooKeeperServer port datadir\n");
            System.exit(2);
        }
        try {
              instance=new ServerConfig(Integer.parseInt(args[0]),args[1],args[1]);
        } catch (NumberFormatException e) {
            System.err.println(args[0] + " is not a valid port number");
            System.exit(2);
        }
    }
QuorumPeer執行流程

由於QuorumPeer類繼承自Thread,所以調用start方法時,最終會調用QuorumPeer的start方法,然後該方法會執行run函數啓動線程執行。

    @Override
    public synchronized void start() {
        startLeaderElection();                                              // 啓動選舉流程
        super.start();                                                      // 調用Thread的start方法,即最終會調用該類的run方法
    }

此時就調用了startLeaderElection方法來啓動集羣的選舉。

    synchronized public void startLeaderElection() {
        currentVote = new Vote(myid, getLastLoggedZxid());                              // 獲取最後的zxid 並首先投一票給自己
        for (QuorumServer p : quorumPeers.values()) {                                   // 獲取當前自己的id
            if (p.id == myid) {
                myQuorumAddr = p.addr;                                                  // 獲取當前的地址
                break;
            }
        }
        if (myQuorumAddr == null) {                                                     // 如果沒找到則報錯
            throw new RuntimeException("My id " + myid + " not in the peer list");
        }
        if (electionType == 0) {                                                        // 如果選擇策略爲0
            try {
                udpSocket = new DatagramSocket(myQuorumAddr.getPort());                 // 獲取端口 使用UDP進行選舉
                responder = new ResponderThread();                                      // 開啓線程 執行
                responder.start();
            } catch (SocketException e) {
                throw new RuntimeException(e);
            }
        }
        this.electionAlg = createElectionAlgorithm(electionType);                       // 獲取當前的選舉算法
    }

此時開始選舉的使用了UDP來進行選舉。

    class ResponderThread extends Thread {
        ResponderThread() {
            super("ResponderThread");
        }

        volatile boolean running = true;
        
        @Override
        public void run() {
            try {
                byte b[] = new byte[36];
                ByteBuffer responseBuffer = ByteBuffer.wrap(b);
                DatagramPacket packet = new DatagramPacket(b, b.length);
                while (running) {
                    udpSocket.receive(packet);                                          // 接受數據包
                    if (packet.getLength() != 4) {             
                        LOG.warn("Got more than just an xid! Len = "
                                + packet.getLength());
                    } else {
                        responseBuffer.clear();
                        responseBuffer.getInt(); // Skip the xid                        // 跳過 xid
                        responseBuffer.putLong(myid);                                   
                        Vote current = getCurrentVote();                                // 獲取當前選票
                        switch (getPeerState()) {                                   
                        case LOOKING:                                                   // 如果是競選狀態
                            responseBuffer.putLong(current.id);                         // 壓入id 和 zxid
                            responseBuffer.putLong(current.zxid);
                            break;
                        case LEADING:
                            responseBuffer.putLong(myid);                              // 如果是主 則返回當前主的服務器id 
                            try {
                                responseBuffer.putLong(leader.lastProposed);           // 壓入主 最後一次提交的事物
                            } catch (NullPointerException npe) {
                                // This can happen in state transitions,
                                // just ignore the request
                            }
                            break;
                        case FOLLOWING:
                            responseBuffer.putLong(current.id);                         // 壓入當前的id
                            try {
                                responseBuffer.putLong(follower.getZxid());             // 壓入 zxid
                            } catch (NullPointerException npe) {
                                // This can happen in state transitions,
                                // just ignore the request
                            }
                        }
                        packet.setData(b);
                        udpSocket.send(packet);                                        // 將數據發送出去
                    }
                    packet.setLength(b.length);
                }
            } catch (Exception e) {
                LOG.warn("Unexpected exception",e);
            } finally {
                LOG.warn("QuorumPeer responder thread exited");
            }
        }
    }

根據當前的角色進行不同的操作,選舉過程中會傳輸當前的id和事物id來進行數據的統一,有關選舉的詳細內容後文再詳細分析。

開始執行
@Override
public void run() {
    setName("QuorumPeer:" + cnxnFactory.getLocalAddress());                 // 設置當前的名稱 該名稱以監聽客戶端的端口結尾

    /*
     * Main loop
     */
    while (running) {
        switch (getPeerState()) {                                           // 獲取當前的狀態
        case LOOKING:
            try {
                LOG.info("LOOKING");
                setCurrentVote(makeLEStrategy().lookForLeader());           // 設置投票並選擇leader
            } catch (Exception e) {
                LOG.warn("Unexpected exception",e);                         // 如果出錯則設置爲LOOKING狀態
                setPeerState(ServerState.LOOKING);
            }
            break;
        case FOLLOWING:
            try {
                LOG.info("FOLLOWING");
                setFollower(makeFollower(logFactory));                      // 如果是FOLLOWING狀態則轉換成follower 跟隨主
                follower.followLeader();
            } catch (Exception e) {
                LOG.warn("Unexpected exception",e);
            } finally {
                follower.shutdown();
                setFollower(null);
                setPeerState(ServerState.LOOKING);
            }
            break;
        case LEADING:
            LOG.info("LEADING");
            try {
                setLeader(makeLeader(logFactory));                          // 設置成主狀態
                leader.lead();                                              // 接聽所有事件請求
                setLeader(null);                                            // 如果失去當前主  則將主設置爲空
            } catch (Exception e) {
                LOG.warn("Unexpected exception",e);
            } finally {
                if (leader != null) {                                       // 設置爲空並重置狀態
                    leader.shutdown("Forcing shutdown");
                    setLeader(null);
                }
                setPeerState(ServerState.LOOKING);
            }
            break;
        }
    }
    LOG.warn("QuorumPeer main thread exited");
}

根據狀態來執行不同的操作,如果是主則接受從的連接,並處理從發送上來的信息。從則會轉發創建信息等到主節點進行處理。後續會詳細的描述整個過程。

客戶端服務器啓動

在創建的過程中也需要創建給客戶端連接請求的服務端口,創建過程就是初始化過程中執行;

new NIOServerCnxn.Factory(getClientPort())

該方法如下;

        public Factory(int port) throws IOException {
            super("NIOServerCxn.Factory:" + port);                          // 獲取服務端連接端口
            setDaemon(true);                                                
            this.ss = ServerSocketChannel.open();                           // 打開連接
            ss.socket().bind(new InetSocketAddress(port));                  // 監聽端口
            ss.configureBlocking(false);                                    // 設置成非阻塞
            ss.register(selector, SelectionKey.OP_ACCEPT);                  // 設置該描述符爲接受請求
            start();                                                        // 開始執行
        }

應該該類也是繼承自Thread,調用start就是執行了該類重寫的run方法。

        public void run() {
            while (!ss.socket().isClosed()) {                                                                       // 檢查連接是否關閉
                try {
                    selector.select(1000);                                                                          // IO複用
                    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();                                                           // 接受新連接
                            sc.configureBlocking(false);                                                            // 設置非阻塞
                            SelectionKey sk = sc.register(selector,
                                    SelectionKey.OP_READ);                                                          // 註冊讀事件
                            NIOServerCnxn cnxn = createConnection(sc, sk);                                          // 初始化一個NIOServerCnxn類
                            sk.attach(cnxn);                                                                        // 添加到列表中
                            addCnxn(cnxn);
                        } else if ((k.readyOps() & (SelectionKey.OP_READ | SelectionKey.OP_WRITE)) != 0) {          // 如果是讀事件或者寫事件則獲取觸發內容
                            NIOServerCnxn c = (NIOServerCnxn) k.attachment();
                            c.doIO(k);                                                                              // 回調執行處理該事件
                        }
                    }
                    selected.clear();                                                                               // 清空
                } catch (Exception e) { 
                    LOG.error("FIXMSG",e);                                                                          // 如果報錯則打印錯誤日誌
                }
            }
            ZooTrace.logTraceMessage(LOG, ZooTrace.getTextTraceLevel(),
                                     "NIOServerCnxn factory exitedloop.");
            clear();
            LOG.error("=====> Goodbye cruel world <======");
            // System.exit(0);
        }

通過查看run函數的執行流程可知,該函數處理過程是一個典型的IO複用的處理過程,客戶端新入的請求都是通過該服務來進行處理的,後續會詳細分析該處理的詳細流程。

總結

本文主要是簡單的概述了一下Zookeeper集羣模式的啓動流程,很粗略的描述了啓動過程中執行的主要內容,首先會開啓一個線程接受客戶端請求的處理,然後打開一個選舉端口進行選舉,接着就會打開一個集羣之間數據處理同步的端口,至此三個端口都提供了不同的服務,完成了主要的Zookeeper集羣的啓動。由於本人才疏學淺,如有錯誤請批評指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章