rpc系列-ZooKeeper

一.簡介

Zookeeper是一個分佈式協調服務,就是爲用戶的分佈式應用程序提供協調服務。

Zookeeper本身就是一個分佈式程序(只要有半數以上節點存活,zk就能正常服務)。

Zookeeper所提供的服務涵蓋:主從協調、服務器節點動態上下線、統一配置管理、分佈式共享鎖、統一名稱服務……

 

雖然說可以提供各種服務,但是zookeeper在底層其實只提供了兩個功能:

管理(存儲,讀取)用戶程序提交的數據;

併爲用戶程序提供數據節點監聽服務;

 

 

 

Zookeeper集羣的角色:  Leader 和  follower  (Observer)

只要集羣中有半數以上節點存活,集羣就能提供服務

 

二.結構

特性

1.Zookeeper:一個leader,多個follower組成的集羣

2.全局數據一致:每個server保存一份相同的數據副本,client無論連接到哪個server,數據都是一致的

3.分佈式讀寫,更新請求轉發,由leader實施

4.更新請求順序進行,來自同一個client的更新請求按其發送順序依次執行

5.數據更新原子性,一次數據更新要麼成功,要麼失敗

6.實時性,在一定時間範圍內,client能讀到最新數據

 

結構

1.層次化的目錄結構,命名符合常規文件系統規範(見下圖)

2.每個節點在zookeeper中叫做znode,並且其有一個唯一的路徑標識

3.節點Znode可以包含數據和子節點(但是EPHEMERAL類型的節點不能有子節點)

4.客戶端應用可以在節點上設置監視器

 

節點類型

1.Znode有兩種類型:

短暫(ephemeral)(斷開連接自己刪除)

持久(persistent)(斷開連接不刪除)

2.Znode有四種形式的目錄節點(默認是persistent )

PERSISTENT

PERSISTENT_SEQUENTIAL(持久序列/test0000000019 )

EPHEMERAL

EPHEMERAL_SEQUENTIAL

3.創建znode時設置順序標識,znode名稱後會附加一個值,順序號是一個單調遞增的計數器,由父節點維護。

4.在分佈式系統中,順序號可以被用於爲所有的事件進行全局排序,這樣客戶端可以通過順序號推斷事件的順序。

 

 

三.原理

zookeeper的選舉機制(集羣paxos)

以一個簡單的例子來說明整個選舉的過程.
假設有五臺服務器組成的zookeeper集羣,它們的id從1-5,同時它們都是最新啓動的,也就是沒有歷史數據,在存放數據量這一點上,都是一樣的.假設這些服務器依序啓動,來看看會發生什麼.
1. 服務器1啓動,此時只有它一臺服務器啓動了,它發出去的報沒有任何響應,所以它的選舉狀態一直是LOOKING狀態。


2.服務器2啓動,它與最開始啓動的服務器1進行通信,互相交換自己的選舉結果,由於兩者都沒有歷史數據,所以id值較大的服務器2勝出,但是由於沒有達到超過半數以上的服務器都同意選舉它(這個例子中的半數以上是3),所以服務器1,2還是繼續保持LOOKING狀態。

3.服務器3啓動,根據前面的理論分析,服務器3成爲服務器1,2,3中的老大,而與上面不同的是,此時有三臺服務器選舉了它,所以它成爲了這次選舉的leader。

4. 服務器4啓動,根據前面的分析,理論上服務器4應該是服務器1,2,3,4中最大的,但是由於前面已經有半數以上的服務器選舉了服務器3,所以它只能接收當小弟的命了。
5.服務器5啓動,同4一樣,當小弟.

 

非全新集羣的選舉機制(數據恢復)

那麼,初始化的時候,是按照上述的說明進行選舉的,但是當zookeeper運行了一段時間之後,有機器down掉,重新選舉時,選舉過程就相對複雜了。

需要加入數據id、leader id和邏輯時鐘。

數據id:數據新的id就大,數據每次更新都會更新id。

Leader id:就是我們配置的myid中的值,每個機器一個。

邏輯時鐘:這個值從0開始遞增,每次選舉對應一個值,也就是說:  如果在同一次選舉中,那麼這個值應該是一致的 ;  邏輯時鐘值越大,說明這一次選舉leader的進程更新。

 

選舉的標準就變成:

1.邏輯時鐘小的選舉結果被忽略,重新投票

2.統一邏輯時鐘後,數據id大的勝出

3.數據id相同的情況下,leader id大的勝出

根據這個規則選出leader。

 

四.示例

Zookeeper的監聽器工作機制

監聽器是一個接口,我們的代碼中可以實現Wather這個接口,實現其中的process方法,方法中即我們自己的業務邏輯

監聽器的註冊是在獲取數據的操作中實現:

getData(path,watch)監聽的事件是:節點數據變化事件

getChildren(path,watch)監聽的事件是:節點下的子節點增減變化事件

 

服務端

public class DistributedServer {
    private static final String host = "localhost:2181";
    private static final int sessionTimeout = 2000;
    private static final String parentNode = "/servers/";

    private ZooKeeper zk = null;

    /**
     * 創建到zk的客戶端連接
     *
     * @throws Exception
     */
    public void getConnect() throws Exception {

        zk = new ZooKeeper(host, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                // 收到事件通知後的回調函數
                System.out.println(event.getType() + "__" + event.getPath());
                try {
                    zk.getChildren("/", true);
                } catch (Exception e) {
                }
            }
        });

    }

    /**
     * 向zk集羣註冊服務器信息
     * ZooDefs.Ids.OPEN_ACL_UNSAFE   默認匿名權限,權限scheme id:'world,'anyone,權限位:31(adcwr)
     * ZooDefs.Ids.READ_ACL_UNSAFE   只讀權限,權限scheme id:'world,'anyone,權限位:1(r)
     *
     * CreateMode
     * 節點類型,類型定義在枚舉CreateMode中:
     * (1)PERSISTENT:持久;
     * (2)PERSISTENT_SEQUENTIAL:持久順序;
     * (3)EPHEMERAL:臨時;
     * (4)EPHEMERAL_SEQUENTIAL:臨時順序。
     * @param data  創建節點初始化內容
     * @throws Exception
     */
    public void registerServer(String data) throws Exception {
        String create = zk.create(parentNode + "test", data.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
        System.out.println(data + " 註冊節點 " + create);
    }

    /**
     * 業務功能
     *
     * @throws InterruptedException
     */
    public void handleBussiness(String data) throws InterruptedException {
        System.out.println(data + "開始handleBussiness");
        Thread.sleep(Long.MAX_VALUE);
    }

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

        // 獲取zk連接
        DistributedServer server = new DistributedServer();
        server.getConnect();

        // 利用zk連接註冊服務器信息
        server.registerServer("test01");

        // 啓動業務功能
        server.handleBussiness("test01");

    }

}

 

分佈式鎖

/**
 *
 *分佈式鎖:幾種實現方式,示例用臨時順序節點實現共享鎖的一般做法
 *
 * 邏輯
 * 1.zk上註冊一個"臨時+序號"的znode,並監聽父節點
 * 2.獲取父節點下所有程序子節點,比較序號大小
 * 3.序號最小的獲取到"鎖",去訪問資源,訪問完後,刪除自己的節點,釋放鎖,重新註冊一個新的子節點
 * 4.其他程序節點會收到事件通知,可以去zk上獲取鎖
 */
public class DistributedClientLock {

    // 會話超時
    private static final int SESSION_TIMEOUT = 2000;
    // zookeeper集羣地址
    private String hosts = "localhost:2181";
    private String groupNode = "servers";
    private String subNode = "test";
    private boolean haveLock = false;

    private ZooKeeper zk;
    /**
     * 記錄自己創建的子節點路徑
     * volatile 不是線程安全的,具有可見性,在一個子內存操作完後,立即刷新回到主內存。
     * 如果不加Volatile,每次調用thisPath,會有副本,修改會有延遲,比如其它線程搶到沒有修改完的數據,就在新的線程繼續執行,造成最後數據值有誤
     * 比如:一個線程寫,其它線程去讀的時候,用的Volatile,比如監聽新節點插入。
     */
    private volatile String thisPath;

    /**
     * 連接zookeeper
     */
    public void connectZookeeper() throws Exception {
        zk = new ZooKeeper(hosts, SESSION_TIMEOUT, new Watcher() {
            public void process(WatchedEvent event) {
                try {
                    System.out.println(event.getType()+"____"+event.getPath());

                    /**
                     * 判斷事件類型,此處只處理子節點變化事件
                     *                          event For “/path”                           event For “/path/child”
                     * create(“/path”)          EventType.NodeCreated                       無
                     * delete(“/path”)          EventType.NodeDeleted       	            無
                     * setData(“/path”)         EventType.NodeDataChanged                   無
                     * create(“/path/child”)    EventType.NodeChildrenChanged(getChild)     EventType.NodeCreated
                     * delete(“/path/child”)    EventType.NodeChildrenChanged(getChild)     EventType.NodeDeleted
                     * setData(“/path/child”)   無                                           EventType.NodeDataChanged
                     */
                    if (event.getType() == Event.EventType.NodeChildrenChanged && event.getPath().equals("/" + groupNode)) {
                        //獲取子節點,並對父節點進行監聽
                        List<String> childrenNodes = zk.getChildren("/" + groupNode, true);
                        String thisNode = thisPath.substring(("/" + groupNode + "/").length());
                        // 去比較是否自己是最小id
                        Collections.sort(childrenNodes);
                        if (childrenNodes.indexOf(thisNode) == 0) {
                            //訪問共享資源處理業務,並且在處理完成之後刪除鎖
                            doSomething();

                            //重新註冊一把新的鎖
                            thisPath = zk.create("/" + groupNode + "/" + subNode, null, ZooDefs.Ids.OPEN_ACL_UNSAFE,
                                    CreateMode.EPHEMERAL_SEQUENTIAL);
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });

        // 程序一進來就先註冊一把鎖到zk上
        thisPath = zk.create("/" + groupNode + "/" + subNode, null, ZooDefs.Ids.OPEN_ACL_UNSAFE,
                CreateMode.EPHEMERAL_SEQUENTIAL);

        // wait一小會,便於觀察
        Thread.sleep(new Random().nextInt(1000));

        // 從zk的鎖父目錄下,獲取所有子節點,並且註冊對父節點的監聽
        List<String> childrenNodes = zk.getChildren("/" + groupNode, true);

        //如果爭搶資源的程序就只有自己,則可以直接去訪問共享資源
        if (childrenNodes.size() == 1) {
            doSomething();
            thisPath = zk.create("/" + groupNode + "/" + subNode, null, ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.EPHEMERAL_SEQUENTIAL);
        }
    }

    /**
     * 處理業務邏輯,並且在最後釋放鎖
     */
    private void doSomething() throws Exception {
        try {
            System.out.println("鎖: " + thisPath);
            Thread.sleep(2000);

        } finally {
            System.out.println("完成: " + thisPath);
            //刪除當前節點
            zk.delete(this.thisPath, -1);
        }
    }

    public static void main(String[] args) throws Exception {
        DistributedClientLock dl = new DistributedClientLock();
        dl.connectZookeeper();
        Thread.sleep(Long.MAX_VALUE);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章