使用Node.js實現一個簡單的ZooKeeper客戶端

什麼是ZooKeeper

Zookeeper 是一個分佈式的、開源的協調服務,用在分佈式應用程序中。它提出了一組簡單的原語,分佈式應用程序可以基於這些原語之上構建更高層的分佈式服務用於實現同步、配置管理、分組和命名等。Zookeeper 設計的容易進行編程,它使用一種類似於文件系統的目錄樹結構的數據模型,以 java 方式運行,有 java 和 c 的綁定(binding)。

分佈式系統中的協調服務總所周知地難於正確實現,尤其容易產生諸如爭用條件 (race conditions)、死鎖(deadlock) 等錯誤。Zookeeper 背後的動機就是減輕分佈式應用程序從頭做起實現協調服務的難度。

數據模型

Zookeeper 會維護一個具有層次關係的數據結構,它非常類似於一個標準的文件系統,如下圖所示:

Zookeeper 數據結構

Zookeeper 這種數據結構有如下這些特點:
1. 每個子目錄項如 NameService 都被稱作爲 znode,這個 znode 是被它所在的路徑唯一標識,如 Server1 這個 znode 的標識爲 /NameService/Server1
2. znode 可以有子節點目錄,並且每個 znode 可以存儲數據,注意 EPHEMERAL 類型的目錄節點不能有子節點目錄
3. znode 是有版本的,每個 znode 中存儲的數據可以有多個版本,也就是一個訪問路徑中可以存儲多份數據
4. znode 可以是臨時節點,一旦創建這個 znode 的客戶端與服務器失去聯繫,這個 znode 也將自動刪除,Zookeeper 的客戶端和服務器通信採用長連接方式,每個客戶端和服務器通過心跳來保持連接,這個連接狀態稱爲 session,如果 znode 是臨時節點,這個 session 失效,znode 也就刪除了
5. znode 的目錄名可以自動編號,如 App1 已經存在,再創建的話,將會自動命名爲 App2
6. znode 可以被監控,包括這個目錄節點中存儲的數據的修改,子節點目錄的變化等,一旦變化可以通知設置監控的客戶端,這個是 Zookeeper 的核心特性,Zookeeper 的很多功能都是基於這個特性實現的。

簡潔的API

Zookeeper 的設計目標之一就是提供簡單的編程接口。於是,它只提供了以下的操作:

  • create : 在(命名空間)樹的一個特定地址上創建一個節點
  • delete : 刪除一個節點
  • exists : 檢測在一個地址上是否存在節點
  • get data : 從節點讀取數據
  • set data :將數據寫入節點
  • get children :檢索子節點列表
  • sync : 等待數據傳播完成

誰在用?

如小米公司的米聊,其後臺就採用了ZooKeeper作爲分佈式服務的統一協作系統。而阿里公司的開發人員也廣泛使用ZooKeeper,並對其進行了適當修改,開源了一款TaoKeeper軟件,以適應自身業務需要。另外還包括Apache HBase、Apache Kafka、Facebook Message等產品也都使用了ZooKeeper。

應用場景

  • 數據量比較小,但對數據可靠性要求很高的場景,比如管理分佈式應用的協作數據。

不能做什麼

  • ZooKeeper不合適做海量存儲,因爲它主要用來管理分佈式應用協作的關鍵數據。對於海量數據,不同的應用有不同的需求,如對一致性和持久性的不同需求,所以在設計應用時,最佳實踐應該將應用數據和協作數據分開,況且對於海量數據我們的選擇很多,如數據庫或者分佈式文件系統等。
  • 不要讓ZooKeeper Server來管理應用程序的緩存,而應該把這些任務交給ZooKeeper客戶端,因爲這樣會導致ZooKeeper的設計更加複雜。比如,讓ZooKeeper來管理緩存失效,可能會導致ZooKeeper在運行時,停滯在等待客戶端確認一個緩存失效的請求上,因爲在進行所有寫操作之前,都需要確認對應的緩存數據是否失效。

Node.js應用與ZooKeeper Server進行通信

那麼當Node.js應用作爲整個異構分佈式系統中的一環,需要作爲客戶端去操作ZooKeeper Server上的znode時,應該如何實現?
說實話,上文介紹了這麼多ZooKeeper的原理,其實作爲客戶端只需要單純的把znode作爲文件來操作就好,並且可以監聽znode的改變,十分方便。本文只描述怎樣使用Node.js實現ZooKeeper客戶端角色。

node-zookeeper

node-zookeeper是ZooKeeper的一個Node.js客戶端實現,這個模塊是基於ZooKeeper原生提供的C API來實現的。
下載
npm install zookeeper
栗子

var ZooKeeper = require ("zookeeper");
var zk = new ZooKeeper({
  connect: "localhost:8888" // zk server的服務器地址和監聽的端口號
 ,timeout: 200000 // 以毫秒爲單位
 ,debug_level: ZooKeeper.ZOO_LOG_LEVEL_WARN 
 ,host_order_deterministic: false 
});
zk.connect(function (err) {
    if(err) throw err;
    console.log ("zk session established, id=%s", zk.client_id);
    zk.a_create ("/node.js1", "some value", ZooKeeper.ZOO_SEQUENCE | ZooKeeper.ZOO_EPHEMERAL, function (rc, error, path)  {
        if (rc != 0) {
            console.log ("zk node create result: %d, error: '%s', path=%s", rc, error, path);
        } else {
            console.log ("created zk node %s", path);
            process.nextTick(function () {
                zk.close ();
            });
        }
    });
});

其中:

  • connect:
    • 包含主機名和ZooKeeper服務器的端口。
  • timeout:
    • 以毫秒爲單位,表示ZooKeeper等待客戶端通信的最長時間,之後會聲明會話已死亡。ZooKeeper的會話一般設置超時時間5-10秒。
  • debug_level:
    • 設置日誌的輸出級別,有四種級別:ZOO_LOG_LEVEL_ERROR, ZOO_LOG_LEVEL_WARN, ZOO_LOG_LEVEL_INFO, ZOO_LOG_LEVEL_DEBUG
  • host_order_deterministic:
    • 初始化zk客戶端實例後,該實例是否是按確定順序去連接ZooKeeper Server集羣中的主機,直到連接成功,或者該會話被斷開。

常見API:

  • connect():連接ZooKeeper Server
  • a_create (path, data, flags, path_cb): 創建一個znode,並賦值,可以決定這個znode的節點類型(永久、臨時、永久有序、臨時有序)
  • a_get(path, watch, data_cb)
    • path: 我們想要獲取數據的zonde節點路徑
    • watch: 表示我們是否想要監聽該節點後續的數據變更。
    • data_cb(rc ,error, stat, data):
      • rc:return code,0爲成功
      • error:錯誤
      • stat:znode的元數據信息
      • data: znode中的數據
  • a_set( path, data, version, stat_cb ): 需要注意的是,ZooKeeper並不允許局部寫入或讀取znode的數據,當設置一個znode節點的數據或讀取時,znode節點的內容或被整個替換或全部讀取出來。
    • path: 我們想要設置數據的zonde節點路徑
    • data:我們想要設置的數據,一個znode節點可以包含任何數據,數據存儲爲字節數組(byte array)。字節數組的具體格式特定於每個應用的實現,ZooKeeper不直接提供解析的支持,用戶可以使用如Protobuf、Thrift、Avro或MessagePack等序列化協議來處理保存在znode中的數據格式,一般UTF-8編碼的字符串就夠用了。
    • version:znode的version,從stat中抽取出來的。
    • data_cb(rc, error, stat): 設置數據的回調
  • close(): 關閉客戶端連接
  • a_exists(path, watch, stat_cb): 判斷znode是否存在
  • a_delete_( path, version, void_cb ):刪除znode,結尾加上”“是爲了不和保留字”_delete“衝突。。。

實現對指定znode節點數據進行CURD的ORM

'use strict'

const ZooKeeper = require('zookeeper');
const logger = require('../logger/index.js'); // 打日誌的工具
const Promise = require('bluebird');
const _ = require('lodash');
let node_env = process.env.NODE_ENV ? process.env.NODE_ENV: 'development';
let connect = node_env === 'development' ? 'zktest.imweb.com:8888' : 'zk.imweb.oa.com:8888';
let timeout = 200000; // 單位毫秒
let path = node_env === 'development' ? '/zk_test/blackList' : '/zk/blackList';
let debug_level = ZooKeeper.ZOO_LOG_LEVEL_WARN;
let host_order_deterministic = false;
let defaultInitOpt = {
    connect,
    timeout,
    debug_level,
    host_order_deterministic
};

class ZK {
    constructor(opt) {
        this.opt = opt;
        this._initZook();
    }

    _initZook() {
        this.zookeeper = new ZooKeeper(this.opt.initOpt || defaultInitOpt);
    }

    /**
     * [get zookeeper blackList]
     * @return {[type]}            [description]
     */
    get() {
        return new Promise((resolve, reject) => {
            let self = this;
            self.zookeeper.connect(function(error) {
                if (error) {
                    reject(error);
                    return;
                }
                console.log('zk session established, id=%s', self.zookeeper.client_id);

                self.zookeeper.a_get(path, null, function(rc, error, stat, data) {
                    if (rc !== 0) {
                        console.log('zk node get result: %d, error: "%s", stat=%s, data=%s', rc, error, stat, data);
                        reject(err);
                    } else {
                        logger.info('get zk node: ' + data)
                        resolve(data);                        
                    }
                    process.nextTick(() => {self.zookeeper.close();});
                })
            });
        });
    }

    /**
     * [set zookeeper black_list]
     * @param {object}   opt: 
     * {
     *     380533076: {
     *         "anchor_uin": 380533076,
     *         "expired_time": 1462876279
     *     },
     *     380533077: {
     *         "anchor_uin": 380533077,
     *         "expired_time": 1462876279
     *     },
     * }
     */
    set(opt) {
        let zkData = null;
        let self = this;
        return new Promise((resolve, reject) => {
            self.zookeeper.connect(function(err) {
                if (err) {
                    reject(err);
                    return;
                }
                console.log('zk session established, id=%s', self.zookeeper.client_id);

                self.zookeeper.a_get(path, null, function(rc, error, stat, data) {
                    if (rc !== 0) {
                        console.log('zk node get result: %d, error: "%s", stat=%s, data=%s', rc, error, stat, data);
                        reject(error);
                    } else {
                        console.log('get zk node %s', data);
                        console.log('stat: ', stat);
                        console.log('data: ', typeof data);
                        try {
                            zkData = JSON.parse(data);
                        } catch (e) {
                            reject(e);
                            return;
                        }

                        zkData.last_update_time = parseInt(new Date().getTime() / 1000, 10);
                        _.extend(zkData.data, opt);
                        let currVersion = stat.version;
                        try {
                            zkData = JSON.stringify(zkData);
                        } catch (e) {
                            reject(e);
                            return;
                        }
                        self.zookeeper.a_set(path, zkData, currVersion, function(rc, error, stat) {
                            if (rc !== 0) {
                                console.log('zk node set result: %d, error: "%s", stat=%s', rc, error, stat);
                                reject(error);
                            } else {
                                logger.info('set zk node succ!');
                                resolve(stat);

                            }
                            process.nextTick(function() {
                                self.zookeeper.close();
                            });
                        })

                    }
                })
            });
        });
    }

    /**
     * [delete zookeeper znode]
     * @param  {array}   keys     [要刪除的黑名單的QQ號]
     * @return {[type]}            [description]
     */
    delete(keys) {
        let zkData = null;
        let self = this;
        return new Promise((resolve, reject) => {
            self.zookeeper.connect(function(err) {
                if (err) {
                    reject(err);
                    return;
                }
                console.log('zk session established, id=%s', self.zookeeper.client_id);

                self.zookeeper.a_get(path, null, function(rc, error, stat, data) {
                    if (rc !== 0) {
                        console.log('zk node get result: %d, error: "%s", stat=%s, data=%s', rc, error, stat, data);
                        reject(error);
                    } else {
                        console.log('get zk node %s', data);
                        console.log('stat: ', stat);
                        console.log('data: ', typeof data);
                        try {
                            zkData = JSON.parse(data);
                        } catch (e) {
                            reject(e);
                            return;
                        }

                        zkData.last_update_time = parseInt(new Date().getTime() / 1000, 10);
                        for (let key of keys) {
                            delete zkData.data[key];
                        }

                        let currVersion = stat.version;
                        try {
                            zkData = JSON.stringify(zkData);
                        } catch (e) {
                            reject(e);
                            return;
                        }
                        self.zookeeper.a_set(path, zkData, currVersion, function(rc, error, stat) {
                            if (rc !== 0) {
                                console.log('zk node set result: %d, error: "%s", stat=%s', rc, error, stat);
                                reject(error);
                            } else {
                                logger.info('set zk node succ!');
                                resolve(stat);
                            }
                            process.nextTick(function() {
                                self.zookeeper.close();
                            });
                        })

                    }
                })
            });
        })

    }

    /**
     * [add description]
     * @param {[type]}   opt      [description]
     */
    add(opt) {
        // zookeeper只能以覆蓋的方式set
        return this.set(opt);
    }

    clear() {
        let zkData = null;
        let self = this;
        return new Promise((resolve, reject) => {
            self.zookeeper.connect(function(err) {
                if (err) {
                    reject(err);
                    return;
                }
                console.log('zk session established, id=%s', self.zookeeper.client_id);

                self.zookeeper.a_get(path, null, function(rc, error, stat, data) {
                    if (rc !== 0) {
                        console.log('zk node get result: %d, error: "%s", stat=%s, data=%s', rc, error, stat, data);
                        reject(error);
                    } else {
                        console.log('stat: ', stat);

                        zkData.last_update_time = parseInt(new Date().getTime() / 1000, 10);
                        zkData.data = '';
                        let currVersion = stat.version;
                        try {
                            zkData = JSON.stringify(zkData);
                        } catch (e) {
                            reject(e);
                            return;
                        }
                        self.zookeeper.a_set(path, zkData, currVersion, function(rc, error, stat) {
                            if (rc !== 0) {
                                console.log('zk node clear result: %d, error: "%s", stat=%s', rc, error, stat);
                                reject(error);
                            } else {
                                logger.info('clear zk node succ!');
                                resolve(stat);
                            }
                            process.nextTick(function() {
                                self.zookeeper.close();
                            });
                        })

                    }
                })
            });
        });
    }
}

module.exports = ZK;
發佈了32 篇原創文章 · 獲贊 51 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章