Hadoop 之 ZooKeeper (一)

Hadoop 之 ZooKeeper
-----------------------------------------------------------------------------------------------------------------------------------------
本文介绍使用 Hadoop 的分布式协调服务构建通用的分布式应用 —— ZooKeeper。 ZooKeeper 是 Hadoop 分布式协调服务。

写分布式应用是比较难的,主要是因为部分失败(partial failure). 当一条消息通过网络在两个节点间发送时,如果发生网络错误,发送者无法知道接受者
是否接收到了这条消息。接收者可能在发生网络错误之前已经收到了这条消息,也可能没有收到,又或者接收者的进程已经死掉。发送者能获知到底发生了什
么事的唯一途径是重新连接接收者,并询问它。这就是部分失败:我们甚至不知道一个操作是否失败了。

因为部分失败是分布式系统的固有特征,因此 ZooKeeper 并不能避免部分失败,当然它也不能隐藏部分失败。但 ZooKeeper 为我们提供了一系列工具来构建
分布式应用来安全地处理部分失败。


    Apache ZooKeeper
    -------------------------------------------------------------------------------------------------------------------------------------
    ZooKeeper 是 Apache 软件基金会旗下的一个独立开源系统,它是 Google 公司为了解决 BigTable 中问题而提出的 Chubby 算法的开源实现。它提供了
    类似文件系统的访问目录和文件(称为 znode)的功能。通常分布式系统利用它协调所有权、注册服务、监听更新。
    
    
ZooKeeper 具有如下特征:

    ● ZooKeeper 是简单的: ZooKeeper 核心是一个精简的文件系统,对外提供一些简单的操作和一些额外的抽象,例如排序和通知。
    ● ZooKeeper 是富有表现力的 :ZooKeeper 原生组件 (ZooKeeper primitives) 是一组构件块(building blocks),可用于构建各种类型数据结构协调和
    协议。相关例子包括分布式队列,分布式锁,以及一组节点中的领导者选举(leader election).
    ● ZooKeeper 是高可用的:ZooKeeper 运行于一组机器之上,并设计为高可用性,因此应用程序可以依赖于它。ZooKeeper 可以帮助系统避免出现单点故障,
    因此可以构建一个可靠的应用。
    ● ZooKeeper 采用松耦合的交互方式:ZooKeeper 交互支持的交互过程中,参与者之间不需要彼此了解。例如, ZooKeeper 可被用作一个汇集机制,这样
    进程在不知道对方存在(或网络状况)的情况下能够彼此发现并进行信息交互。参与的各方甚至可以不必同时存在,因为一个进程可以在 ZooKeeper 中留下
    一条消息,而另外一个进程即使在之前的那个进程退出后也可以读取这条消息。
    
    ● ZooKeeper 是一个资源库 (library) : ZooKeeper 提供了一个开源的,共享的通用协调模式实现仓库。使个人程序员免于自己编写这类通用协议(通常很
    难写好)。随着时间的推移,ZooKeeper 社区不断地增加和改进资源库,每个人都会从中受益。


ZooKeeper 也是高性能的。在 Yahoo! , ZooKeeper 诞生的地方,对于由几百个客户端产生的以写操作为主的工作负载来说,ZooKeeper 集群基准测试吞吐量
已经超过每秒 10000 个操作;对于以读操作为主的工作负载来说,吞吐量更是高出好几倍。



1. 安装和运行 ZooKeeper (Installing and Running ZooKeeper)
-----------------------------------------------------------------------------------------------------------------------------------------
首次尝试使用 ZooKeeper 时,最简单的方法是在一台 ZooKeeper 服务器上以独立模式(standalone mode)运行。可以在一台用于开发的机器上尝试运行。
ZooKeeper 要求 Java 运行,因此首先要确保已经安装好 Java .

下载 :http://zookeeper.apache.org/releases.html
解包 :
    % tar xzf zookeeper-x.y.z.tar.gz

设置环境变量:

    % export ZOOKEEPER_HOME=~/sw/zookeeper-x.y.z
    % export PATH=$PATH:$ZOOKEEPER_HOME/bin

配置文件:conf/zoo.cfg,   也可保存到 /etc/zookeeper 子目录中, 如果设置环境变量 ZOOCFGDIR, 也可以设置到该关键变量指定的目录内。
    cd conf
    cp zoo_sample.cfg zoo.cfg
    
    vi conf/zoo.cfg
最低配置:
    tickTime=2000
    dataDir=/Users/tom/zookeeper
    clientPort=2181

运行:
    % zkServer.sh start


测试:
    % echo ruok | nc localhost 2181
    imok

    
    ZooKeeper commands: the four-letter words
    +===============+===========+===========================================================================
    | 分类            | Command    | 描述
    +---------------+-----------+---------------------------------------------------------------------------
    | Server status    | ruok        | Prints imok if the server is running and not in an error state.
    +---------------+-----------+---------------------------------------------------------------------------
    |                | conf        | Prints the server configuration (from zoo.cfg).
    +---------------+-----------+---------------------------------------------------------------------------
    |                | envi        | Prints the server environment, including ZooKeeper version, Java version,
    |                |            | and other system properties.
    +---------------+-----------+---------------------------------------------------------------------------
    |                | srvr        | Prints server statistics, including latency statistics, the number of
    |                |            | znodes, and the server mode (standalone, leader, or follower).
    +---------------+-----------+---------------------------------------------------------------------------
    |                | stat        | Prints server statistics and connected clients.
    +---------------+-----------+---------------------------------------------------------------------------
    |                | srst        | Resets server statistics.
    +---------------+-----------+---------------------------------------------------------------------------
    |                | isro        | Shows whether the server is in read-only (ro) mode (due to a network
    |                |            | partition) or read/write mode (rw).
    +---------------+-----------+---------------------------------------------------------------------------
    | Client         | dump        | Lists all the sessions and ephemeral znodes for the ensemble. You must
    | connections    |            | connect to the leader (see srvr) for this command.
    +---------------+-----------+---------------------------------------------------------------------------
    |                | cons        | Lists connection statistics for all the server’s clients.
    +---------------+-----------+---------------------------------------------------------------------------
    |                | crst        | Resets connection statistics.
    +---------------+-----------+---------------------------------------------------------------------------
    | Watches        | wchs        | Lists summary information for the server’s watches.
    +---------------+-----------+---------------------------------------------------------------------------
    |                | wchc        | Lists all the server’s watches by connection. Caution: may impact server
    |                |            | performance for a large number of watches.
    +---------------+-----------+---------------------------------------------------------------------------
    |                | wchp        | Lists all the server’s watches by znode path. Caution: may impact server
    |                |            | performance for a large number of watches.
    +---------------+-----------+---------------------------------------------------------------------------
    | Monitoring    | mntr        | Lists server statistics in Java properties format, suitable as a source
    |                |            | for monitoring systems such as Ganglia and Nagios.
    +---------------+-----------+---------------------------------------------------------------------------


除了 mntr 命令外,ZooKeeper 还可通过 JMX 显示统计信息,更详细的内容查看 ZooKeeper 文档。src/contrib 目录内还有一些监视工具。

从 ZooKeeper v3.5.0 之后,有一个内置的 web server 提供与 four-letter 命令相同信息。访问 http://localhost:8080/commands 获得命令列表。



2. 案例 (An Example)
-----------------------------------------------------------------------------------------------------------------------------------------
假设有一组服务器用于为客户端提供某种服务。我们希望每个客户端都能找到其中的一台服务器,这样就可以使用这项服务。在这个例子中,其中一个挑战
是维护这组服务器成员列表。

这组服务器的成员关系列表显然不能存储在网络中的单个节点上,因为那个节点故障将意味着整个系统失效(我们希望整个成员列表是高度可用的)。我们先
假设已经有了一种可靠的方法来解决列表的存储问题。仍然存在的问题是,如果其中一台服务器出现故障,如何从列表中移除这台服务器。某些进程需要负责
移除失效的服务器,但注意不能是故障服务器本身,因为它已不再运行。

我们所描述的不是一个被动的分布式数据结构,而是一个主动的,并且能够在某些外部事件发生时可以修改数据项状态的数据结构。 ZooKeeper 提供了这种
服务,接下来让我们看看如何使用它来实现组成员管理应用。



2.1 ZooKeeper 中的组成员关系 (Group Membership in ZooKeeper)
-----------------------------------------------------------------------------------------------------------------------------------------
理解 ZooKeeper 的一种方法是将他理解为一个提供高可用性的文件系统。它没有文件和目录,而是统一使用节点(node)概念,称为 znode . znode 即可以
作为保存数据的容忍(如同文件),也可以作为保存其他 znode 的容器(如同目录)。所有 znode 构成了一个层次化的命名空间,一个自然的构建成员列表的
方法是使用组名称创建一个父 znode, 使用组成员名称(服务器名称)作为节点名称创建子 znodes . 如下图所示:


                                +---------------+
                                |                |
                                |        /        |
                                |                |
                                +---------------+
                                    
                                +---------------+
                                |                |
                                |    /zoo        |
                                |                |
                                +---------------+


            +---------------+    +---------------+    +---------------+
            |                |    |                |    |                |
            |    /zoo/duck    |    |    /zoo/goat    |    |    /zoo/cow    |
            |                |    |                |    |                |
            +---------------+    +---------------+    +---------------+



这个示例中,没有在任何 znode 节点中存储数据,但在实际的应用中,可以想象将成员相关的数据存储在它们的 znode 中,例如主机名。





2.2 创建组 (Creating the Group)
-----------------------------------------------------------------------------------------------------------------------------------------
使用 ZooKeeper 的 Java API, 为组创建节点,名称为 /zoo

//A program to create a znode representing a group in ZooKeeper
public class CreateGroup implements Watcher {

    private static final int SESSION_TIMEOUT = 5000;
    private ZooKeeper zk;
    private CountDownLatch connectedSignal = new CountDownLatch(1);
    
    public void connect(String hosts) throws IOException, InterruptedException {
        zk = new ZooKeeper(hosts, SESSION_TIMEOUT, this);
        connectedSignal.await();
    }
    
    @Override
    public void process(WatchedEvent event) { // Watcher interface
        if (event.getState() == KeeperState.SyncConnected) {
            connectedSignal.countDown();
        }
    }
    
    public void create(String groupName) throws KeeperException,
    InterruptedException {
        String path = "/" + groupName;
        String createdPath = zk.create(path, null/*data*/, Ids.OPEN_ACL_UNSAFE,
        CreateMode.PERSISTENT);
        System.out.println("Created " + createdPath);
    }
    
    public void close() throws InterruptedException {
        zk.close();
    }
    
    public static void main(String[] args) throws Exception {
        CreateGroup createGroup = new CreateGroup();
        createGroup.connect(args[0]);
        createGroup.create(args[1]);
        createGroup.close();
    }
}

运行:
    % export CLASSPATH=ch21-zk/target/classes/:$ZOOKEEPER_HOME/*:\
    $ZOOKEEPER_HOME/lib/*:$ZOOKEEPER_HOME/conf
    % java CreateGroup localhost zoo
    Created /zoo




2.3 加入组 (Joining a Group)
-----------------------------------------------------------------------------------------------------------------------------------------
这个应用的下一部分是一段用于注册组成员的程序。每个组成员将作为一个程序运行,并且加入到组中。当程序退出时,这个组成员应当从组中被移除。为了
实现这一点,我们在 ZooKeeper 的命名空间中使用短暂的 znode 来代表一个组成员。


//A program that joins a group
public class JoinGroup extends ConnectionWatcher {
    public void join(String groupName, String memberName) throws KeeperException,
    InterruptedException {
        String path = "/" + groupName + "/" + memberName;
        String createdPath = zk.create(path, null/*data*/, Ids.OPEN_ACL_UNSAFE,
        CreateMode.EPHEMERAL);
        System.out.println("Created " + createdPath);
    }
    
    public static void main(String[] args) throws Exception {
        JoinGroup joinGroup = new JoinGroup();
        joinGroup.connect(args[0]);
        joinGroup.join(args[1], args[2]);
        // stay alive until process is killed or thread is interrupted
        Thread.sleep(Long.MAX_VALUE);
    }
}


//A helper class that waits for the ZooKeeper connection to be established
public class ConnectionWatcher implements Watcher {
    private static final int SESSION_TIMEOUT = 5000;
    protected ZooKeeper zk;
    private CountDownLatch connectedSignal = new CountDownLatch(1);
    
    public void connect(String hosts) throws IOException, InterruptedException {
        zk = new ZooKeeper(hosts, SESSION_TIMEOUT, this);
        connectedSignal.await();
    }
    
    @Override
    public void process(WatchedEvent event) {
        if (event.getState() == KeeperState.SyncConnected) {
            connectedSignal.countDown();
        }
    }
    
    public void close() throws InterruptedException {
        zk.close();
    }
}


2.4 列出组成员 (Listing Members in a Group)
-----------------------------------------------------------------------------------------------------------------------------------------

//A program to list the members in a group
public class ListGroup extends ConnectionWatcher {

    public void list(String groupName) throws KeeperException,
        InterruptedException {
        String path = "/" + groupName;
        try {
            List<String> children = zk.getChildren(path, false);
            if (children.isEmpty()) {
            System.out.printf("No members in group %s\n", groupName);
            System.exit(1);
            }
            for (String child : children) {
                System.out.println(child);
            }
        } catch (KeeperException.NoNodeException e) {
            System.out.printf("Group %s does not exist\n", groupName);
            System.exit(1);
        }
    }
    public static void main(String[] args) throws Exception {
        ListGroup listGroup = new ListGroup();
        listGroup.connect(args[0]);
        listGroup.list(args[1]);
        listGroup.close();
    }
}


运行:
    % java ListGroup localhost zoo
    No members in group zoo


    % java JoinGroup localhost zoo duck &
    % java JoinGroup localhost zoo cow &
    % java JoinGroup localhost zoo goat &
    % goat_pid=$!

    % java ListGroup localhost zoo
    goat
    duck
    cow
    
    % kill $goat_pid

    % java ListGroup localhost zoo
    duck
    cow

    
    ZooKeeper 命令行工具
    -----------------------------------------------------------------------------
    % zkCli.sh -server localhost ls /zoo
    [cow, duck]


2.5 删除组 (Deleting a Group)
-----------------------------------------------------------------------------------------------------------------------------------------


//A program to delete a group and its members
public class DeleteGroup extends ConnectionWatcher {
    public void delete(String groupName) throws KeeperException,
    InterruptedException {
        
    String path = "/" + groupName;
    try {
            List<String> children = zk.getChildren(path, false);
            for (String child : children) {
                    zk.delete(path + "/" + child, -1);
            }
            zk.delete(path, -1);
        } catch (KeeperException.NoNodeException e) {
            System.out.printf("Group %s does not exist\n", groupName);
            System.exit(1);
        }
    }
    
    public static void main(String[] args) throws Exception {
        DeleteGroup deleteGroup = new DeleteGroup();
        deleteGroup.connect(args[0]);
        deleteGroup.delete(args[1]);
        deleteGroup.close();
    }

}


运行:

% java DeleteGroup localhost zoo
% java ListGroup localhost zoo
Group zoo does not exist



3. ZooKeeper 服务 (The ZooKeeper Service)
-----------------------------------------------------------------------------------------------------------------------------------------
ZooKeeper 是高可用的,高性能的协调服务。本节,我们将从三个方面了解这个服务:模型、操作和实现




3.1 数据模型 (Data Model)
-----------------------------------------------------------------------------------------------------------------------------------------
ZooKeeper 维护一个树形层次结构,树中的节点称为 znode. znode 可以用于存储数据,并且有一个与之关联的 ACL. ZooKeeper 设计用于实现协调服务(这
类服务通常使用小数据文件), 而不是用于大容量数据存储,因此一个 znode 能存储的数据被限制在 1 MB 以内。

ZooKeeper 的数据访问是具有原子性。客户端读取存储在一个 znode 中的数据永远不会直接收到一部分数据;要么获取所有数据,要么读取失败。类似地,
一次写入会替换掉一个 znode 相关的所有数据。ZooKeeper 保证写入或者成功或者失败;不会出现部分写入状况。ZooKeeper 不支持追加操作。这个特征与
HDFS 不同,HDFS 设计用于大规模数据存储,支持流式数据访问和追加操作。

ZooKeeper 通过路径引用,路径在 ZooKeeper 中表示为斜线分隔的(slash-delimited) Unicode 字符串, 类似 Unix 中文件系统路径。路径必须是绝对的,
因此,它们必须以 / 字符开始。此外,所有的路径表示必须是规范的,即每个路径只有唯一的一种表示方式,不支持路径解析。例如, 在 Unix 中,一个具
有路径 /a/b 的文件可以通过路径 /a/./b 表示,原因在于 . 在 Unix 路径中表示为当前目录(.. 表示为当前目录的上一级目录). 在 ZooKeeper 中,"."
不具有这种特殊含义,这样表示的路径名是不合法的。

在 ZooKeeper 中,路径是由 Unicode 字符串构成,并且有一些限制。字符串 "zookeeper" 是一个保留词,不能将它作为路径表示中的一部分。需要特别指出
的是,ZooKeeper 使用 /zookeeper 子树来保存管理信息,例如,关于配额的信息。

注意, ZooKeeper 的路径与 URI 不同,前者在 Java API 中通过 java.lang.String 来使用,而后者是通过 Hadoop Path 类(或 java.net.URI 类)使用。

znode 有一些属性对分布式应用非常有用,下面几节分别讨论。



    ■ 短暂 znodes(Ephemeral znodes)
    -----------------------------------------------------------------------------------------------------------------------------------------
    正如所看到的,znode 可以是两种类型之一:短暂的(ephemeral)或持久的(persistent)。一个 znode 的类型在创建时设置并且之后不可改变。一个短暂的
    znode 在创建它的客户端会话结束时由 ZooKeeper 删除。相反,持久类型的 znode 不与客户端会话关联,并且只有当一个客户端(不一定是创建它的那个客
    户端)明确删除时它才被删除。短暂 znode 没有子 znode,即便是短暂 znode 也没有.

    虽然短暂 znode 绑定到一个客户端会话(client session), 但它们对所有客户端是可见的(当然受 ACL 策略控制).

    短暂 znode 对于构建需要知道特定时刻有哪些分布式资源可用的应用来说,是一种理想的选择。本章之前的案例使用短暂 znode 实现组成员服务,因此,
    任何进程在任何特定时间都能发现哪些组成员可用。



    ■ 顺序号(Sequence numbers)
    -----------------------------------------------------------------------------------------------------------------------------------------
    顺序的 znode (sequential znode) 由 ZooKeeper 给出一个顺序号(a sequence number)作为其名称的一部分。如果一个 znode 创建时设置了顺序标志(
    sequential flag), 那么一个单步递增的计数器(由父节点维护)就会追加到它名称之后。
    
    例如,如果一个客户端请求使用名称 /a/b- 创建一个顺序的 znode, 创建的 znode 实际名称可能为 /a/b-3. 之后,如果另一个顺序 znode 节点使用 /a/b-
    创建了,它会使用一个更大的计数器值给定一个唯一的名称,例如 /a/b-5. 在 Java API 中,给顺序 znode 的实际路径是作为调用 create() 的返回值返回
    给客户端的。
    
    顺序号可用于分布式系统发生事件时进行全局排序,并用于客户端推断事件顺序。
    
    
    ■ 观察(Watches)
    -----------------------------------------------------------------------------------------------------------------------------------------
    当一个 znode 以某种方式发生变化时,观察允许客户端得到通知。观察由 ZooKeeper 服务上的操作设置,并由服务上的其他操作触发。例如,一个客户端
    可以在一个 znode 上调用 exists 操作,同时将一个 watch 置于其上。如果这个 znode 不存在了, exists 操作会返回 false. 如果,在之后的某个时间,
    这个 znode 由另一客户端创建了,这个 watch 被触发,通知前一个客户端这个 znode 的创建。
    
    Watchers 只被触发一次。要接收多个通知,客户端需要重新注册这个 watch. 因此,如果前一个例子中的客户端希望接收这个 znode 的存在与否的更多通知
    (例如,被删除时的通知), 它需要再次调用 exists 操作来设置一个新的 watch.
    
    
    
    
    
3.2 操作 (Operations)
-----------------------------------------------------------------------------------------------------------------------------------------
ZooKeeper 中有 9 种基本操作,如下表所示:

        
            Operations in the ZooKeeper service
    +===================+==============================================================
    |    操作            |    描述
    +-------------------+--------------------------------------------------------------
    | create            | Creates a znode (the parent znode must already exist)
    +-------------------+---------------------------------------------------------------
    | delete            | Deletes a znode (the znode must not have any children)
    +-------------------+--------------------------------------------------------------
    | exists            | Tests whether a znode exists and retrieves its metadata
    +-------------------+---------------------------------------------------------------
    | getACL, setACL    | Gets/sets the ACL for a znode
    +-------------------+--------------------------------------------------------------
    | getChildren        | Gets a list of the children of a znode
    +-------------------+--------------------------------------------------------------
    | getData, setData    | Gets/sets the data associated with a znode
    +-------------------+-------------------------------------------------------------
    | sync                | Synchronizes a client’s view of a znode with ZooKeeper
    +-------------------+---------------------------------------------------------------


    
ZooKeeper 中的更新操作是有条件的(are conditional). 一次 delete 或 setData 操作必须指定要更新的 znode 的版本号(可以通过调用 exists 获得).
如果版本号不匹配,更新会失败。更新是非阻塞操作,因此,客户端更新失败(由于其他进程同时在更新该 znode)可以决定是否再次更新或执行一些其他操作,
不会因此而阻塞其他进程的执行进度。

虽然 ZooKeeper 可以被被看作是一个文件系统,但处于简单性的需要,一些文件系统的基本操作被它摈弃了,因为 ZooKeeper 文件很小,并且整体写入或整体
读出,没有必要提供 open, close, or seek 这些操作。    




    ■ 批次更新 (Multiupdate)
    -----------------------------------------------------------------------------------------------------------------------------------------
    有另一个 ZooKeeper 操作,称为 multi, 将多个基本操作批次聚集为一个操作单元,要么整体成功,要么整体失败。某些基本操作成功而某些失败的情景
    永远不会发生。

    Multiupdate 对于在 ZooKeeper 中构建维护一些全局一致的数据结构非常有用。例如构建一个无向图。

    
    
    ■ APIs
    -----------------------------------------------------------------------------------------------------------------------------------------    
    有两个核心语言绑定 ZooKeeper 客户端,一个是 Java, 一个是 C; 也有一个 contrib 构建用于 Perl, Python, 和 REST 客户端。对于每种构建,都有执行
    同步操作或异步操作的选择。下面是 ZooKeeper 类的 exists 操作的签名,或者返回一个 Stat 对象封装 znode 的元数据,或者 null 如果 znode 不存在:

        public Stat exists(String path, Watcher watcher) throws KeeperException, InterruptedException
    
    异步方法:
    
        public void exists(String path, Watcher watcher, StatCallback cb, Object ctx)
    
    在 Java API 中,所有的异步方法都有 void 返回类型,因为操作的结果是通过一个回调(callback)传递。调用者传递一个回调实现,当从 ZooKeeper 接收到
    响应时,它的方法会被调用。本例中,回调是 StatCallback 接口,具有如下方法:
    
        public void processResult(int rc, String path, Object ctx, Stat stat);
        
    rc 参数是返回代码,对应到 KeeperException 中定义的代码。非零值表示一个异常,这种情况下 stat 参数返回 null. path 和 ctx 参数对应 客户端调用
    exists() 方法传递进来的参数,并可以用于识别这个回调是响应哪个请求。 ctx 参数可以是任意对象,可被客户端用于当 path 对象不足以消除请求歧义时
    来识别请求。如果不需要,可以设置为 null .
    
    有两个 C shared 库。单线程的库,zookeeper_st, 只支持异步 API, 是针对 pthread 不可用准备的。大多数开发者会使用多线程库(multithreaded library)
    zookeeper_mt, 它支持同步的和异步的 API. 对于如何构建和使用 C API, 参考 ZooKeeper 分发包 src/c 目录的 README 文件。
    
    
    
    ■ Watch 触发器 (Watch triggers)
    -----------------------------------------------------------------------------------------------------------------------------------------    
    读取操作 exists, getChildren, and getData 可以设置 watch, 并且 watch 由写操作触发: create, delete, and setData. ACL 操作不参与到观察中。
    当一个 watch 触发时,一个 watch 事件生成,该 watch event 的类型取决于 watch 和触发它的操作:
    
        ● exists 操作上设置的 watch 会在被观察的 znode 创建,删除,或其数据更新时触发。
        ● getData 操作上设置的 watch 会在被观察的 znode 被删除或有数据更新时触发。创建时不放生触发是因为该 znode 必须存在 getData 操作才能成功。
        ● getChildren 操作上设置的 watch 会在被观察的 znode 的子节点被创建或删除时,或该 znode 被删除时触发。可以通过查看 watch 的事件类型得知
        是该 znode 还是其子节点被删除: NodeDeleted 表明是该 znode 被删除,而 NodeChildrenChanged 指明是它的一个子节点被删除。
        
        
                Watch creation operations and their corresponding triggers
            +===================+===============+=======================+===============+=======================+===================+
            | Watch creation    | create znode    | create child            | delete znode    |                        | setData            |
            +-------------------+---------------+-----------------------+---------------+-----------------------+-------------------+
            | exists            | NodeCreated    |                        | NodeDeleted    |                        | NodeDataChanged    |
            +-------------------+---------------+-----------------------+---------------+-----------------------+-------------------+
            | getData            |                |                        | NodeDeleted    |                        | NodeDataChanged    |
            +-------------------+---------------+-----------------------+---------------+-----------------------+-------------------+
            | getChildren        |                | NodeChildrenChanged    | NodeDeleted    | NodeChildrenChanged    |                    |
            +-------------------+---------------+-----------------------+---------------+-----------------------+-------------------+
    
        一个 watch event 包括与该 event 相关的 znode 的 path, 因此对于 NodeCreated 和 NodeDeleted 事件,可以简单地通过观察 path 分辨出哪个节点
        被创建或被删除。在 NodeChildrenChanged 事件之后确定是哪个子节点放生了变化,需要再次调用 getChildren 来获取新的子节点列表。类似地,对于
        为了发现 NodeDataChanged 事件的新数据,需要调用 getData. 在这两种情况下, znode 在接收 watch event 和执行读取操作时, znode 可能发生变化,
        因此在编写应用时应牢记这一点。
    
    
    ■ ACLs
    -----------------------------------------------------------------------------------------------------------------------------------------        
    每个 znode 被创建时都会带有一个 ACL 列表,用于决定谁可以对它执行何种操作。
    
    ACL 依赖于验证机制,客户端向 ZooKeeper 标明其自身的过程。 ZooKeeper 提供了几种验证方式(authentication scheme):
    
        ● digest    : 客户端通过用户名和密码验证
        ● sasl        : 客户端通过 Kerberos 验证
        ● ip        : 客户端通过 IP 地址验证
    
    客户端可以在建立一个 ZooKeeper 会话之后对自己进行身份验证。虽然 znode 的 ACL 列表要求客户端是通过身份验证的,但 ZooKeeper 的身份验证过程是
    可选的。客户端必须自己进行身份验证来支持对 znode 的访问。这里有一个使用用户名和密码的 digest 方式进行身份验证的例子:
    
        zk.addAuthInfo("digest", "tom:secret".getBytes());
    
    一个 ACL 是身份验证方式(authentication scheme)、符合该方式的一个身份和一组权限的组合。例如,如果想给一个 IP 地址为 10.0.0.1 的客户端,读访问
    一个 znode, 应该在该 znode 上设置 ip 验证方式的 ACL, ID 为 10.0.0.1, READ 许可权限。在 Java 中,应该如下创建一个 ACL 对象:
    
        new ACL(Perms.READ, new Id("ip", "10.0.0.1"));
    
    下表列出完整的权限。注意, exist 操作并不受 ACL 权限的控制,因此任何客户端可以调用 exists 来检索一个 znode 的 Stat 对象并查询一个 znode 事实
    上是否存在。
    
    
            ACL permissions
    
        +===================+===========================================
        | ACL permission    | Permitted operations                        
        +-------------------+-------------------------------------------
        | CREATE            | create (a child znode)                    
        +-------------------+-------------------------------------------
        | READ                | getChildren
        |                    +-------------------------------------------
        |                    | getData                                    
        +-------------------+-------------------------------------------
        | WRITE                | setData                                    
        +-------------------+-------------------------------------------
        | DELETE            | delete (a child znode)                    
        +-------------------+-------------------------------------------
        | ADMIN                | setACL                                    
        +-------------------+-------------------------------------------
    
    
    在 ZooDefs.Ids class 中有一些预定义的 ACL, 包括 OPEN_ACL_UNSAFE, 它将所有的权限(除了 ADMIN 权限)授予所有人。
    
    此外, ZooKeeper 有一个插入式的身份验证机制(a pluggable authentication mechanism), 如果需要的话,可以集成第三方身份验证系统。
    
    

    
3.3 实现 (Implementation)
-----------------------------------------------------------------------------------------------------------------------------------------    
ZooKeeper 服务可运行于两种不同的模式。独立模式(standalone mode)下,只有一个 ZooKeeper 服务器,因其简单性,比较适合于测试环境(甚至可嵌入到
单元测试, embedded in unit tests), 但不能确保高可用性和可恢复性。在生产环境中,ZooKeeper 在一个集群的机器上运行于复制模式(replicated mode)
这个计算机集群称为集合体(an ensemble)。ZooKeeper 通过复制来实现高可用性,并且只要集合体内多数机器可用就可以提供服务。例如,在一个有 5 个节
点的集合体中,任何两个节点故障服务仍然可以工作,因为还保有三个的多数节点可用。注意,有 6 个节点的集合体也只能容忍 2 台机器出现故障,因为如
果 3 台机器出现故障,剩下的 3 台机器没有超过集合体的半数。出于这个原因,一个集合体通常包含奇数台机器。

从概念上来说,ZooKeeper 是非常简单的:它所做的就是确保对 znode 树所做每一个修改都会被复制到集合体中超过半数的机器上。如果少数机器发生故障,
那么最少会有一部机器会保持最新状态,其他剩下的复本最终也会更新到这个状态。

然而,这个简单想法的实现却不简单。ZooKeeper 使用一个称为 Zab 的协议,这个协议分为两个阶段运行,可以无限重复:



    ■ 第一阶段:领导者选举 (Phase 1: Leader election)
    -------------------------------------------------------------------------------------------------------------------------------------
    集合体中的所有机器通过一个进程选择出一个卓越的成员(electing a distinguished member),称为领导者(leader). 其他机器称为跟班(followers).
    一旦半数以上的(或法定数量的) followers 将它们的状态与 leader 同步了,则这个阶段结束。
    
    
    ■ 第而阶段:原子广播 (Phase 2: Atomic broadcast)
    -------------------------------------------------------------------------------------------------------------------------------------
    所有的写入请求都会被转发给 leader, 再由 leader 将更新广播给 followers. 当半数以上的跟班已持久化这个变化,领导者提交更新,然后客户端才
    收到响应说明更新已成功。这个完成一致性的协议设计为原子性的,因此一个改变(change)要么成功要么失败。
    
    
如果领导者出现故障,其余的机器会选出另外一个 leader, 使用这个新的 leader 继续像之前一样提供服务。如果之前的 leader 之后恢复运行,它作为一个
follower 启动。leader 选举是非常快的,根据一个已公布的结果,大约 200 毫秒,因此选举过程不会感觉到性能的降低。
    
集合体内所有机器在更新它们内存中的 znode 树之前将更新写入到磁盘。读请求可以由任何机器提供服务,并且因为只涉及内存查询,因此非常块。    
    
    
    
3.4 一致性 (Consistency)
-----------------------------------------------------------------------------------------------------------------------------------------    
理解 ZooKeeper 的实现基本原理有助于理解其服务所提供的一致性保证。对集合体中机器的术语 "leader" 和 "follower" 是恰当的,因为它表明了一个
follower 可能滞后于 leader 几个更新。这就是实际上的一致性,在一个修改被提交之前,只要半数以上的成员而非集合体内全部成员都持久化了变更即可。
对于 ZooKeeper来说,理想的情况是将客户端链接到与领导者状态一致的服务器上。客户端实际上可以被连接到 leader 上,但客户端无法控制,甚至它自己都
不知道是否连接到领导者。

对 znode 树的每个更新都会给定一个全局唯一标识符,称为 zxid(代表 ZooKeeper transaction ID). 更新是有次序的(ordered), 因此如果一个 zxid z1 比
z2 小,那么,根据 ZooKeeper (分布式系统中唯一权威次序, the single authority on ordering in the distributed system), z1 一定在 z2 之前发生。

ZooKeeper 的设计中,以下几点保证了数据的一致性:


    ■ 顺序一致性 (Sequential consistency)
    -------------------------------------------------------------------------------------------------------------------------------------
    来自任意特定客户端的更新都会按其发送顺序被应用。也就是说,如果一个客户端将 znode z 的值更新为 a, 在之后的操作中,它又将 z 的值更新为 b,
    则没有客户端能够在看到 z 的值为 b 之后再看到值 a (如果没有其他对 z 的更新).

    

    ■ 原子性 (Atomicity)
    -------------------------------------------------------------------------------------------------------------------------------------
    更新要么成功要么失败。这意味着如果一个更新失败,没有客户端会看到这个更新。



    ■ 单一系统映像 (Single system image)
    -------------------------------------------------------------------------------------------------------------------------------------
    一个客户端会看到系统的同样的视图,不管它连接到哪个服务器。这意味着,如果一个客户端在同一个会话中连接到一台新的服务器,它所看到的系统
    状态不会比在之前服务器上看到的更陈旧。当一个服务器失效了,客户端尝试连接到集合体中的另一个服务器时,所有状态滞后于故障服务器的服务器都
    不会接受该连接请求,直到它这些服务器将其状态更新至故障服务器的水平。
    
    
    
    ■ 耐久性 (Durability)
    -------------------------------------------------------------------------------------------------------------------------------------
    一个更新一旦成功,其结果就会持久存在并且不会被撤销。这表明更新不会受到服务器故障的影响。
    
    
    
    
    ■ 及时性 (Timeliness)
    -------------------------------------------------------------------------------------------------------------------------------------
    任何客户端的系统视图滞后都是有边界的(bounded),过期不会超过几十秒。这意味着与其允许一个客户端看到非常陈旧的数据,还不如将服务器关闭,
    强迫客户端连接到一个状态更新的服务器。
    
    
出于性能的原因,读取操作从 ZooKeeper 服务器的内存中执行就可以满足,并且不涉及在全局次序中的写操作。这个属性可能导致如果客户端通过 ZooKeeper
以外的机制进行通信,客户端会看到不一致的 ZooKeeper 状态表现。

例如,客户端 A 将 znode z 的值从 a 更新到 a' , A 告知 B 读取 z, 并且 B 读取到的 z 的值是 a 而不是 a' . 这与 ZooKeeper 的一致性保证是完全兼容
的(这种情况称之为“跨客户端视图的同时一致性”, the condition that it does not promise is called “simultaneously consistent cross-client views”)

为了阻止这种情况的发生, B 应该在读取 z 的值之前,在 z 上调用 sync 操作。 sync 操作会强制 B 所连接的 ZooKeeper 服务器赶上 leader 服务器,这样,
当 B 读取 z 的值时,读取到的将会是 A 所更新的(或后来更新的)。
    
    
    NOTE
    -------------------------------------------------------------------------------------------------------------------------------------
    有些令人困惑的是,sync 操作只有作为异步调用(asynchronous call)可用。不需要等待其返回,因为 ZooKeeper 确保这个服务器的任何后续的操作都
    会在 sync 完成之后发生,即便是在 sync 完成之前发出的操作。
    
    
    
    
3.5 会话 (Sessions)
-----------------------------------------------------------------------------------------------------------------------------------------
每个 ZooKeeper 客户端都配置为 ensemble 中的服务器列表。启动时,它尝试连接服务器列表中的一个服务器。如果连接失败,会尝试列表中的另一服务器,
等等,直到它或者成功连接到其中的一个或所有的 ZooKeeper 服务器不可用而失败。

一旦建立了与一个 ZooKeeper server 的连接,server 会为客户端创建一个新的会话(session). 会话有一个超时期限,这个超时期限由创建会话的应用设定。
如果服务器在超时期限内没有收到任何请求,则相应的会话会过期。一旦一个会话过期,就无法重新被打开它,并且任何与该会话相关联的短暂 znode 都会丢
失。虽然会话过期是一个相对罕见的事件,因为会话都是长期存在的,但对于应用程序来说处理会话过期是非常重要的。

会话空闲超过一定的时间,客户端会通过发送 ping 请求(也称为心跳,also known as heartbeats)来保持会话存在。ping 包是有 ZooKeeper client 库自动
发送的,因此代码中不需要考虑如何维护会话。这个时间长度的选择要足够低,以便可以检测出服务器故障(由读超时体现),并且能够在会话超时期间能够重新
连接到另外一个服务器。

故障切换到另一个 ZooKeeper server 是由 ZooKeeper client 自动处理的,并且,至关重要的是,会话(及其关联的短暂 znode)在从一个故障的服务器切换
到另一个服务器之后仍然有效。

在故障切换过程中,应用程序将接收到断开连接和连接至服务器的通知。 watch 通知在客户端断开连接时不会被传递,但当客户端成功恢复连接时,watch
通知会被传输。同样,在客户端重新连接到另一个服务器过程中,如果应用程序试图执行一个操作,这个操作将会失败。这充分说明在真实的 ZooKeeper 应用
中,处理连接丢失异常(connection loss exceptions)的重要性。
    
    
    
    ■ 时间 (Time)
    -------------------------------------------------------------------------------------------------------------------------------------    
    ZooKeeper 中有几个时间参数。tick time 是 ZooKeeper 中基本的时间周期,并被集合体内的服务器用来定义相互交互的时间调度。其他的设置都是根据
    tick time 来定义的,或者至少受它的限制。例如,会话超时(The session timeout), 不能低于 2 tick 或大于 20 tick. 如果尝试设置会话超时在这个
    范围之外,它会被修改为这个范围之内。
    
    通常将 tick time 参数设置为 2 秒(2000 毫秒), 对应于允许的会话超时范围是 4 到 40 秒。
    
    在选择会话超时设置时有几点需要考虑。低的会话超时导致更快的机器故障检测。在组成员关系的例子中,会话超时就是用于将故障主机从组中删除的时间。
    但要避免将会话超时时间设得太低,因为繁忙的网络会导致数据包传输延迟,从而可能会无意中导致会话过期。这种情况下,机器可能会出现振荡现象(
    flap):在很短的数据间隔内反复地离开然后又重新加入到组中。
    
    对于那些创建较复杂暂时状态的应用程序来说,更适合设置较长的会话超时,因其重建的代价较高。有些情况下,可以设计应用程序在会话超时周期内重新
    启动,从而避免出现会话过期。这适合于执行维护和升级。服务器会为每个会话分配一个唯一的 ID 和 password,如果在建立连接时把它们传递给
    ZooKeeper,就可能恢复一个会话(只要它没有过期)。应用程序能因此安排优雅的关闭(a graceful shutdown), 通过在进程重启之前将会话 ID 和密码
    存储到稳定的存储器中,重启之后从存储器中获取会话 ID 和 password, 然后恢复会话。
    
    可以将这个特性看做一种用于帮助避免会话过期的优化技术,但不能因此忽略会话过期的处理,如果一部机器意外发生故障这还是可能发生的,或者,
    即使应用程序正常关闭,也可能因任何原因导致它没有在会话过期之前完成重启。
    
    一般的规则是,在较大的 ZooKeeper 集合体中,会话超时的设置应该越大。连接超时、读取超时和 ping 周期都在内部被定义为集合体中服务器数量
    的函数,因此,集合体中服务器数量增多,这些参数的值越小。如果频繁遇到连接丢失的情况,应考虑增大超时值。可以使用 JMX 来监控 ZooKeeper 的
    度量指标,例如请求延时的统计信息。
    
        
    
    
3.6  状态 (States)
-----------------------------------------------------------------------------------------------------------------------------------------
ZooKeeper 对象在其生命周期内会经历几个不同的状态,可以在任何时间通过调用 getState() method 查询其状态。

    public States getState()
    
States 是一个枚举(enum), 表现为 ZooKeeper 对象可能处于的不同状态。一个 ZooKeeper 实例在一个时刻只能处于一种状态。一个新创建的 ZooKeeper
实例在其尝试与 ZooKeeper 服务建立连接时处于 CONNECTING 状态。一旦连接建立,它会进入 CONNECTED 状态。
    
    
                            CONNECTING        
                    
                    
                            CONNECTED
                                                    
                            
                            CLOSED
    
    
客户端使用 ZooKeeper 对象通过注册一个 Watcher 对象能够接收状态转换通知。进入 CONNECTED 状态,Watcher 会接收到一个 WatchedEvent, 它的
KeeperState 的值为 SyncConnected.
    
    
    
    NOTE
    -------------------------------------------------------------------------------------------------------------------------------------    
    一个 ZooKeeper Watcher 对象负有双重责任:一方面它可用于接收 ZooKeeper 状态变化通知(本节所描述的),另一方面可用于接收 znode 变化的通知
    (Watch triggers 中所描述的)。传入 ZooKeeper 对象构造器的 watcher (默认的)用于状态变化,但对于 znode 变化的 watcher, 可以使用一个指定的
    Watcher 实例(通过在向某个合适的读操作传递一个 watcher 实例), 或者共享使用一个默认 watcher,通过读操作中的布尔标识设置。
    
    
    
ZooKeeper 实例可以断开,然后重新连接到 ZooKeeper 服务,状态在 CONNECTED 和 CONNECTING 之间转换。如果断开连接,watcher 会收到 Disconnected
事件。注意,这些状态的变化都是由 ZooKeeper 实例自己发起的,如果连接丢失,它会自动尝试重新连接。

如果调用了 close() method, 或者会话超时,由 KeeperState 的值为 Expired 表明, ZooKeeper 实例可以转换为第三种状态,CLOSED。 一旦进入 CLOSED
状态,ZooKeeper 对象就不再认为是活跃的(可以通过调用 States 上 isAlive() method 测试)并且不能再被使用。要重新连接到 ZooKeeper 服务,客户端必须

构建一个新的 ZooKeeper  实例。


Hadoop 之 ZooKeeper (二)

猜你喜欢

转载自blog.csdn.net/devalone/article/details/80894203