利用ZooKeeper開發(fā)服務(wù)器上下線感知程序

What is ZooKeeper

ZooKeeper是一個(gè)分布式的分布式應(yīng)用程序協(xié)調(diào)服務(wù)。簡(jiǎn)單地來說,就是用于協(xié)調(diào)管理多個(gè)分布式應(yīng)用程序的一個(gè)工具,扮演著一個(gè)第三方管理者的角色。

問題背景分析

假設(shè)現(xiàn)在有10個(gè)應(yīng)用程序(App#0 - App#9),運(yùn)行在由10臺(tái)服務(wù)器(Server#0 - Server#9)組成的集群上(假設(shè)平均分配,每臺(tái)服務(wù)器上運(yùn)行一個(gè)程序)。此時(shí)由于某個(gè)熱門線上活動(dòng)的開始(如搶票or低價(jià)秒殺等),突然間有數(shù)以百萬計(jì)的用戶訪問服務(wù)器上的資源,等待服務(wù)器處理并應(yīng)答(如下圖所示)。

server.png

很不幸10臺(tái)服務(wù)器中有K臺(tái)受不住負(fù)載壓力,導(dǎo)致服務(wù)器崩潰。在這種情況下,如果客戶端無法感知服務(wù)器的狀態(tài)(在線/離線),部分向已經(jīng)崩潰的服務(wù)器發(fā)送請(qǐng)求的客戶端將會(huì)有長(zhǎng)時(shí)間無法獲得應(yīng)答,它們只能一直重復(fù)地向已經(jīng)崩潰的服務(wù)器地址重發(fā)請(qǐng)求,無法切換至另外(10-K)臺(tái)完好的服務(wù)器進(jìn)行交互。

breakdown.png

其實(shí)在這種場(chǎng)景下,如果客戶端能夠及時(shí)地感知到集群中哪些節(jié)點(diǎn)已經(jīng)崩潰,哪些節(jié)點(diǎn)仍然完好,是可以切換至完好的節(jié)點(diǎn)并向其發(fā)送請(qǐng)求的。理論上只要集群中仍有1個(gè)節(jié)點(diǎn)是完好的,它即能向客戶端提供服務(wù)。

所以整個(gè)問題的癥結(jié)就在于,如何讓客戶端感知到服務(wù)器上下線狀態(tài),以便切換請(qǐng)求發(fā)送的地址。

zk.png

重新參考ZooKeeper的功能描述,ZooKeeper可以用來協(xié)調(diào)管理多個(gè)分布式應(yīng)用程序,那其實(shí)可以用于管理我們的分布式機(jī)器集群。如上圖所示,在用戶和服務(wù)器集群中間可設(shè)置ZooKeeper層,讓ZooKeeper實(shí)時(shí)感知每一個(gè)節(jié)點(diǎn)的狀態(tài),然后客戶端并不直接向具體節(jié)點(diǎn)發(fā)起請(qǐng)求,而應(yīng)先向ZooKeeper詢問當(dāng)前仍然存活的服務(wù)器節(jié)點(diǎn),然后再從中挑選一個(gè)負(fù)載較低的服務(wù)器節(jié)點(diǎn)進(jìn)行交互。由于ZooKeeper本身的高可用性(本身也可拓展為分布式架構(gòu)),所以就能大大地提高整個(gè)系統(tǒng)的可用性。

ZooKeeper數(shù)據(jù)結(jié)構(gòu)

ZooKeeper數(shù)據(jù)結(jié)構(gòu)采用了樹狀結(jié)構(gòu)(在文件系統(tǒng)中被廣泛使用),且不是簡(jiǎn)單的二叉樹,而是多叉樹。在ZooKeeper的樹結(jié)構(gòu)中,每一個(gè)節(jié)點(diǎn)被稱為znode,可通過控制臺(tái)命令或者Java的SDK對(duì)內(nèi)部數(shù)據(jù)進(jìn)行管理。

znode的類型有2*2=4種,分別是:

  • PERSISTENT
  • PERSISTENT_SEQUENTIAL
  • EPHEMERAL
  • EPHEMERAL_SEQUENTIAL

其中PERSISTENTEPHEMERAL的區(qū)別正如其名,在無外力影響下PERSISTENT節(jié)點(diǎn)不會(huì)被改變和刪除,而EPHEMERAL節(jié)點(diǎn)在創(chuàng)建節(jié)點(diǎn)的session結(jié)束后會(huì)自動(dòng)從樹中刪除。至于SEQUENTIAL非SEQUENTIAL則影響了節(jié)點(diǎn)id自增,SEQUENTIAL節(jié)點(diǎn)的id會(huì)自動(dòng)遵循父節(jié)點(diǎn)下的自增規(guī)則進(jìn)行命名。

zkds.png

如圖所示,在本問題中我們可以把一臺(tái)服務(wù)器看作樹中的一個(gè)節(jié)點(diǎn),我們可以利用EPHEMERAL節(jié)點(diǎn)的這一特性進(jìn)行服務(wù)器狀態(tài)的監(jiān)聽。服務(wù)器上線時(shí)創(chuàng)建與zk之間的session并向zk注冊(cè)節(jié)點(diǎn),只要服務(wù)器不崩潰,session便不會(huì)結(jié)束,即EPHEMERAL節(jié)點(diǎn)會(huì)一直存在,可被客戶端感知;當(dāng)服務(wù)器崩潰時(shí),其與zk之間保持的session自然也會(huì)結(jié)束,EPHEMERAL節(jié)點(diǎn)會(huì)自動(dòng)被刪除,客戶端查詢服務(wù)器列表時(shí)絕對(duì)無法獲得已刪除的節(jié)點(diǎn)信息。

Demo程序

  • Server.java (服務(wù)器端代碼)
package my.bigdata.zk;

import org.apache.zookeeper.*;

public class Server {

    private static final String HOST_ADDRESS = "localhost:2181";
    private static final int DEFAULT_TIMEOUT = 2000;
    private static final String DEFAULT_SERVER_PARENT = "/servers";

    private ZooKeeper zkConnect = null;

    /**
     * 連接至ZooKeeper
     * @throws Exception
     */
    public void connect() throws Exception{
        zkConnect = new ZooKeeper(HOST_ADDRESS, DEFAULT_TIMEOUT, new Watcher() {
            @Override
            public void process(WatchedEvent watchedEvent) {
                System.out.println("Type:" + watchedEvent.getType()
                        + " Path:" + watchedEvent.getPath());
            }
        });
    }

    /**
     * 向ZooKeeper注冊(cè)本服務(wù)器節(jié)點(diǎn)
     * @param data 服務(wù)器信息
     * @throws Exception
     */
    public void register(String data) throws Exception{
        String create = zkConnect.create(DEFAULT_SERVER_PARENT + "/server",
                                            data.getBytes(),
                                            ZooDefs.Ids.OPEN_ACL_UNSAFE,
                                            CreateMode.EPHEMERAL_SEQUENTIAL);   // 注冊(cè)成ephemeral節(jié)點(diǎn)以便自動(dòng)在zk上注銷
        System.out.println(create + " is registered!");
    }

    /**
     * 通過sleep模擬服務(wù)器在線
     */
    public void sleep() {
        try {
            Thread.sleep(20000);
        } catch (Exception e) {
            System.out.println(e.toString());
        }
    }


    public static void main(String[] args) throws Exception {

        //連接至zk
        Server server = new Server();
        server.connect();

        //向zk注冊(cè)服務(wù)器信息
        String data = args[0];
        server.register(data);

        server.sleep();
    }
}

服務(wù)器端的重點(diǎn)在于,程序啟動(dòng)時(shí)向ZooKeeper的指定節(jié)點(diǎn)下注冊(cè)服務(wù)器信息,相當(dāng)于通知ZooKeeper這個(gè)第三方:“服務(wù)器已上線”。其次,注冊(cè)的節(jié)點(diǎn)類型必須是ephemeral節(jié)點(diǎn),為了實(shí)現(xiàn)節(jié)點(diǎn)id自增(auto-increment)還可以使用ephemeral_sequential節(jié)點(diǎn)。

  • Client.java (客戶端代碼)
package my.bigdata.zk;

import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Client {

    private static final String HOST_ADDRESS = "localhost:2181";
    private static final int DEFAULT_TIMEOUT = 2000;
    private static final String DEFAULT_SERVER_PARENT = "/servers";

    private ZooKeeper zkConnect = null;
    private List<String> availableServers;

    /**
     * 連接至ZooKeeper
     * @throws Exception
     */
    public void connect() throws Exception {
        zkConnect = new ZooKeeper(HOST_ADDRESS, DEFAULT_TIMEOUT, new Watcher() {
            @Override
            public void process(WatchedEvent watchedEvent) {
                try {
                    updateServerCondition();    // 重復(fù)注冊(cè)
                } catch (Exception e) {
                    System.out.println(e.toString());
                }
            }
        });
    }

    /**
     * 向zk查詢服務(wù)器情況, 并update本地服務(wù)器列表
     * @throws Exception
     */
    public void updateServerCondition() throws Exception {
        List<String> children = zkConnect.getChildren(DEFAULT_SERVER_PARENT, true);
        List<String> servers = new ArrayList<>();
        for(String child : children) {
            byte[] data = zkConnect.getData(DEFAULT_SERVER_PARENT + "/" + child,
                                        false,
                                        null);
            servers.add(new String(data));
        }
        availableServers = servers;
        System.out.println(Arrays.toString(servers.toArray(new String[0])));
    }

    /**
     * 通過sleep讓客戶端持續(xù)運(yùn)行,模擬"監(jiān)聽"
     */
    public void sleep() throws Exception{
        System.out.println("client is working");
        Thread.sleep(Long.MAX_VALUE);
    }

    public static void main(String[] args) throws Exception {

        // 連接zk
        Client client = new Client();
        client.connect();

        // 獲取servers節(jié)點(diǎn)信息(并監(jiān)聽),從中獲取服務(wù)器信息列表
        client.updateServerCondition();

        client.sleep();
    }
}

客戶端的重點(diǎn)在于,它不斷地向ZooKeeper某個(gè)特定節(jié)點(diǎn)(此處是servers節(jié)點(diǎn))注冊(cè)了一個(gè)Watcher,那么一旦該節(jié)點(diǎn)下的結(jié)構(gòu)發(fā)生改變,ZooKeeper會(huì)向注冊(cè)了Watcher的客戶端發(fā)送“狀態(tài)變化”的消息,那么客戶端即可動(dòng)態(tài)地從ZooKeeper中獲取最新的服務(wù)器節(jié)點(diǎn)信息,甚至無需“主動(dòng)”詢問。

當(dāng)然,ZooKeeper的應(yīng)用場(chǎng)景還有很多,考慮到它本身也可拓展為一個(gè)分布式應(yīng)用,在這種高可用性保證下它簡(jiǎn)直就是多個(gè)分布式應(yīng)用的萬能管家和協(xié)調(diào)者??。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容