翻译- O'Reilly ZooKeeper-第三章 ZooKeeper API入门

第二部分 ZooKeeper编程

         这部分内容帮助开发者学习ZooKeeper编程技巧,掌握在分布式程序中使用ZooKeeper进行协同的正确方法。ZooKeeper API包括Java和C版本。两个版本的基本结构和方法签名都是类似的。Java版最流行,最易于使用,所以我们用它讲解示例(第七章介绍C版本)。Master-Worker例子的源码位于GitHub Repository。

第三章 ZooKeeperAPI入门

         前一章使用zkCli介绍了ZooKeeper的基本操作。本章开始在应用程序中使用ZooKeeper API,我们会介绍通过API创建会话和设置wather的方法,也会开始编码实现Master-Worker示例。

设置ZooKeeper ClASSPATH

         运行和编译ZooKeeper的Java代码都需要正确设置CLASSPATH。除了ZooKeeper自身Jar包,ZooKeeper还使用了一系列第三方库。为了减少输入,增加可读性,我们用环境变量CLASSPATH代表这些库。ZooKeeper分发包bin目录下的zkEnv.sh脚本中设置了CLASSPATH环境变量,执行:

ZOOBINDIR="/bin"
source "$ZOOBINDIR"/zkEnv.sh

创建ZooKeeper会话

         ZooKeeperAPI都围绕着ZooKeeper句柄,句柄代表一次ZooKeeper会话。如图3-1所示,当连接中断时,和某台ZooKeeper服务器建立的会话会自动重连到另外的服务器上。只要会话还存活,这个句柄就有效,而且ZooKeeper客户端库会持续维护和服务器的活动连接来保证会话的存活性。如果句柄被关闭,ZooKeeper客户端库会告诉服务器终止会话。如果ZooKeeper服务端认为客户端已经挂掉,它也会废弃会话。如果客户端再使用这个废弃会话对应的句柄重新连接服务器,服务器会告知客户端会话已经被废弃,所有的句柄操作都返回错误。

翻译- O'Reilly ZooKeeper-第三章 ZooKeeper API入门_第1张图片

Figure 3-1. Sessionmigration between two servers

创建一个ZooKeeper句柄的构造函数是:

ZooKeeper(
    String connectString,
    int sessionTimeout,
    Watcher watcher)
其中:

connectString:

         ZooKeeper服务器的主机名和端口号。之前我们用zkCli连接ZooKeeper服务时列出了服务器信息。

sessionTimeout:

         ZooKeeper声明会话废弃之前没有接收到客户端消息的时间,以毫秒计算。我们使用15000,也就是15s。这意味着如果ZooKeeper超过15s都没有连接上客户端,它就会终止客户端会话。注意这个时间偏大,适用于我们的实验性程序。典型的ZooKeeper会话超时时间是5-10s。

watcher:

         watcher对象用于接收会话事件。因为Watcher是接口,所以我们需要实现它,并将实例化对象传递给ZooKeeper构造函数。客户端使用这个watcher监控会话状态,和某台ZooKeeper服务器连接建立或连接丢失时都会触发事件。Watcher也能用于监控ZooKeeper数据变化。最重要的是,当会话过期时,客户端会收到通知事件。

实现Watcher

         实现watcher,就能收到ZooKeeper发出的通知事件。进一步查看Watcher接口:

public interface Watcher {
    void process(WatchedEvent event);
}

         接口很简单。我们以后会重度使用它,现在仅仅打印event。开始实现我们的示例Master:        

import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.Watcher;
public class Master implements Watcher {
    ZooKeeper zk;
    String hostPort;
    Master(String hostPort) {
        this.hostPort= hostPort;1
    }
    void startZK(){
        zk = new ZooKeeper(hostPort, 15000, this);2
    }
    public void process(WatchedEvent e) {
        System.out.println(e);3
    }
    public static void main(String args[])throws Exception {
        Master m = new Master(args[0]);
        m.startZK();
        // wait for a bit
        Thread.sleep(60000);4
    }
}

1.        构造函数中没有实例化ZooKeeper对象,仅仅保存了端口号。Java的最佳实践是在某个对象的构造函数完成之前,不应该去调用这个对象的其他方法。因为Master实现了Watcher接口,而实例化ZooKeeper对象时,Watcher的回调方法会被调用,所以我们必须在Mater构造函数返回之后再构造ZooKeeper对象。

2.        使用Master对象作为Watcher构造ZooKeeper对象。

3.        这个简单示例不做复杂事件处理,仅仅打印出事件。

4.        连接上ZooKeeper后,会话由一个后台线程维持。这个线程是守护线程,如果程序退出,线程也会立刻退出。我们调用sleep方法,才能在程序退出之前观察到发生的事件。

编译程序:

         $ javac -cp $CLASSPATH Master.java

运行编译好的Master类:                 

 $ java -cp $CLASSPATH Master 127.0.0.1:2181
... - INFO [...] - Client environment:zookeeper.version=3.4.5-1392090, ...1
...
... - INFO [...] - Initiating client connection,
connectString=127.0.0.1:2181 ...
... - INFO [...] - Opening socket connection to server
localhost/127.0.0.1:2181. ...2
... - INFO [...] - Socket connection established tolocalhost/127.0.0.1:2181,
initiating session
... - INFO [...] - Session establishment complete on server
localhost/127.0.0.1:2181, ...3
WatchedEvent state:SyncConnected type:Nonepath:null 4

ZooKeeper客户端API会输出各种日志消息帮助用户理解正在发生的事情。尽管日志相当冗长,可以通过修改配置文件停用,但这些日志在开发过程中是非常有用的,甚至在部署到正式环境之后,如果出现异常,也非常有价值。

1.        一开始的少量日志输出ZooKeeper客户端版本和环境变量。

2.        这部分日志产生于客户端连接上某台ZooKeeper服务器,包括初始连接和失败重连。

3.        这部分日志产生于连接建立之后,含有客户端连接上的主机和端口,以及协商后的会话超时时间。如果客户端设置的会话超时时间太短或太长,服务器端都会作出调整。

4.        最后一行不是来自于ZooKeeper库,而是我们在Wacher.process(WatchedEvent e)中打印的WatchEvent对象。

这个例子假设所有需要的库都在lib目录下,log4j.conf配置文件在conf目录下。如果你在日志中发现:

log4j:WARN No appenders could be foundfor logger
(org.apache.zookeeper.ZooKeeper).
log4j:WARN Please initialize the log4jsystem properly.

       这代表log4j.conf文件没有被放在CLASSPATH下。

运行Watcher示例

         让我们试试看在没有启动ZooKeeper服务的情况下运行Master会发生什么?先停止服务,然后运行Master。之前日志中输出的WatchedEvent事件没有出现,因为ZooKeeper库连接不上服务器。

         现在启动ZooKeeper服务,运行Master,然后在Master运行时停止服务。你会在SyncConnected事件之后发现Disconnected事件。

         当一些开发者看到Disconnected事件,认为需要再创建一个ZooKeeper句柄重新连接服务。千万不要这样做!再次启动ZooKeeper服务,运行Master,然后在保持Master运行时停止服务,仍然是SyncConnected事件之后出现Disconnected事件,不仅如此,还会再出现一次SyncConnected事件,因为ZooKeeper客户端库会自动重连。虽然断网和服务器崩溃时时有发生,但ZooKeeper通常都能处理这些异常。

         需要重点关注的是,ZooKeeper服务本身也会出现异常,比如ZooKeeper服务器会崩溃或者断网,这和我们模拟的停止服务场景效果类似。只要ZooKeeper服务由三台以上的服务器组成,一台服务器的崩溃就不会导致整个服务停止。客户端会观察到Disconnected事件和紧随其后的SycConnected事件。

         表面上看起来,客户端除了sleeping之外没有太多其他行为,但从我们观察到的event事件来看,幕后发生了不少事情。可以通过ZooKeeper提供的两个主要管理接口:JMX和四字母命令(four-letter words)来查看服务端情况。这些接口将在第十章深入讨论。我们先试用下stat和dump。

         使用这些命令,首先要telnet远程登陆到2181端口,然后输入命令回车运行。比如,如果我们运行Master,使用stat命令:        

$ telnet 127.0.0.1 2181
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
stat
ZooKeeper version: 3.4.5-1392090, built on 09/30/2012 17:52 GMT
Clients:
/127.0.0.1:39470[1](queued=0,recved=3,sent=3)
/127.0.0.1:39471[0](queued=0,recved=1,sent=0)
Latency min/avg/max: 0/5/48
Received: 34
Sent: 33
Connections: 2
Outstanding: 0
Zxid: 0x17
Mode: standalone
Node count: 4
Connection closed by foreign host.

         从输出中可以看到,两个客户端连接上了ZooKeeper。其中之一是Master,另外一个是Telnet连接本身。

         使用dump命令:

$ telnet 127.0.0.1 2181
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
dump
SessionTracker dump:
Session Sets (3):
0 expire at Wed Nov 28 20:34:00 PST2012:
0 expire at Wed Nov 28 20:34:02 PST2012:
1 expire at Wed Nov 28 20:34:04 PST 2012:
0x13b4a4d22070006
ephemeral nodes dump:
Sessions with Ephemerals (0):
Connection closed by foreign host.

         可以看到其中有一个属于Master的活动会话,还能够看到该会话的过期时间,这个过期时间基于创建ZooKeeper对象时指定的会话超时时间计算得来。

         终止Master进程,然后反复用dump观察活动会话。你会发现一段时间之后会话才消失。这是因为服务端在会话超时之前不会终止会话。另外,只要客户端还维护着和ZooKeeper服务器之间的活动连接,就会不停延后会话的过期时间。

         当Master结束,最好通过调用ZooKeeper.close()方法,立即停止会话。调用close方法后,ZooKeeper对象代表的会话被清理。

         在示例程序中加入close方法:        

void stopZK() throws Exception { zk.close();}
public static void main(String args[])throws Exception {
    Master m = new Master(args[0]);
    m.startZK();
    // wait for a bit
    Thread.sleep(60000);
    m.stopZK();
}

         再次运行Master,然后使用dump命令来观察会话是否还处于活动状态。因为Master显式关闭了会话,所以ZooKeeper不必等到会话超时才关闭它。

获得控制权

         Master创建会话后,需要获取控制权。务必要小心的是,只能同时存在一个活动Master。而且还需要有多个备用Master进程运行,在活动Master崩溃时能接管控制权。

         为了确保在同一时间只有一个活动Master进程,我们使用第二章“Master角色”中描述的简单Leader选举算法:所有潜在的Master都试图创建/master节点,只有唯一的一个进程能够成功,成为活动Master。

         创建/master节点需要做两件事情。首先是初始化节点数据,通常初始化数据中都包含Master进程的相关信息。我们用随机的ServerID标记每个进程,用作初始化数据。其次,我们需要指定新节点的ACL(访问控制列表)。ZooKeeper一般运行在可信环境中,可以使用开放的ACL。

ZooDefs.Ids.OPEN_ACL_UNSAFE常量表示任何用户都对节点拥有所有的权限。(顾名思义,在非可信环境下,这是极不安全的。)

ZooKeeper提供了节点粒度的可插拔式ACL认证机制,如果有必要,可以具体限定到谁能访问某个节点,能对某个节点做什么类型的操作。在我们的简单例子中,使用OPEN_ACL_UNSAFE就足够了。 我们还希望如果活动Master挂掉,/master节点也被删除。在第二章“持久化和临时节点”章节中介绍了临时节点。将/master节点设置成临时节点,这样会话被关闭或者会话失效后,/master节点会被ZooKeeper自动删除。

增加以下代码:

String serverId = Integer.toHexString(random.nextInt());
void runForMaster() {
    zk.create("/master",1
    serverId.getBytes(),2
    OPEN_ACL_UNSAFE,3
    CreateMode.EPHEMERAL);4
}

1.        如果/master节点已经存在,create操作会失败。我们还把代表服务器的唯一ID存储为/master节点的数据。

2.        节点数据只能是字节数组类型,所以把整数转换成字节数据。

3.        如前所述,使用开放ACL。

4.        创建临时节点。

我们还需要处理create操作抛出的两种异常:KeeperException和InterrupttedException。特别是要正确处理ConnectionLossException(KeepException的子类)和InterrupttedException。对于其他异常,可以选择放弃该操作或继续,但在这两种异常情形下,create操作可能是成功的,实际上我们已经成为Master。

ConnectionLossException发生在客户端和ZooKeeper服务器失去连接情况下。通常都是由网络错误引起的,比如网络分割或某台ZooKeeper服务器异常。当异常发生时,客户端不知道请求丢失是发生在ZooKeeper服务器处理之前,还是处理之后但客户端没有收到响应。前面提到过,ZooKeeper客户端库会为后续请求重建连接,但是进程必须判断未决请求是否已经被处理,是否需要重新发起请求。

InterrupttedException是被客户端线程调用Thread.interrupt触发。这通常是程序关闭动作中的一部分,但也可能不是,这取决于应用的具体实现。异常会中断本地客户端请求处理过程,使请求处于未知状态。

因为这两种异常都打断了正常的请求处理过程,开发者不能对请求处理状态作出任何假设。在处理异常时,必须先确定系统当前状态。比如在Leader选举中,不能实际上已经被选中却不自知。也就是说,如果进程create操作成功了,但是它自身却不知道已经取得控制权,那么除非该进程死亡,其他进程都不能成为Master,这回导致没有进程充当Master角色。

处理ConnectionLossException时,先要找出是否已经有进程成功创建了/master节点。如果该进程碰巧是自己,就开始充当Leader。我们使用getData方法:

byte[] getData(
    String path,
    bool watch,
    Stat stat)

其中:

path

         和大部分ZooKeeper方法一样,第一个参数都是节点路径,我们从这个节点查询数据。

watch

         表示是否监视节点数据变化。如果设置为true,会通过创建ZooKeeper句柄时设置的Watcher对象接收事件。getData还有一个支持设置Watcher对象参数的重载方法。在下一章中我们会介绍如何监视数据变化,现在仅仅想获取当前数据,所以将watch参数设置为false。

stat

         用于填充节点的元数据

返回值

         如果返回成功(没有抛出异常),节点数据以字节数组格式返回。

在runForMaster方法代码中加入异常处理片段:

String serverId = Integer.toString(Random.nextLong());
boolean isLeader = false;
// returns true if thereis a master
boolean checkMaster() {
    while (true) {
        try {
            Stat stat = new Stat();
            byte data[] = zk.getData("/master", false, stat);1
            isLeader = new String(data).equals(serverId));2
            return true;
        } catch (NoNodeException e) {
           // no master, so trycreate again
            return false;
        } catch (ConnectionLossException e) {
        }
    }
}
void runForMaster() throws InterruptedException {3
    while (true) {
        try {4
            zk.create("/master", serverId.getBytes(),
                      OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);5
            isLeader = true;
            break;
        } catch (NodeExistsException e) {
            isLeader = false;
            break;
        } catch (ConnectionLossException e) {6
        }
        if (checkMaster()) break;7
    }
}

         4.使用try…catch块捕获zk.create抛出的ConnectionLossException。

         5.如果create操作成功,进程成为Master

         6.catch到ConnectionLossException异常后不做任何处理,继续运行下一行代码

         7.检查是否存在活动master,如果不存在,重试create操作。

         1.获取/master节点数据,检查当前活动Master。

         2.这行代码解释了我们为什么在创建/master节点时使用serverID数据:如果/master节点存在,通过查看其数据就可以判断谁是Leader。如果当前进程触发了ConnectionLossException,它可能实际上已经成为Master;可能create操作成功了,丢失的只是响应消息。

         3.直接抛出InterruptedException异常给调用者。

         在这个例子中,直接将InterruptedException抛给调用者,并继续往上传递。不幸的是,Java中对如何处理线程中断、中断代表的含义都没有明确的指导原则。有时候,中断用来向线程发送信号,告诉它需要在程序关闭之前进行清理动作。另外一些场景下,中断用来控制某个具体线程的动作,而程序继续保持运行。

         如何处理InterruptedException异常取决于上下文。如果InterruptedException沿堆栈往上传递,所有清理动作正常进行,最终关闭zk句柄,是可行的。如果zk句柄没有被关闭,我们需要在重新抛出异常或者继续异步操作之前,判断进程是否已经是Master。这种情况比较棘手,要求仔细设计处理好。

Master类的main方法修改为:

public static void main(String args[])throws Exception {
    Master m = new Master(args[0]);
    m.startZK();
    m.runForMaster();1
    if (isLeader) {
        System.out.println("I'm the leader");
        // wait for a bit
        Thread.sleep(60000);
    } else {
        System.out.println("Someone else is the leader");2
    }
    m.stopZK();
}

1.        调用之前实现的runForMaster方法,结果要么是当前进程成为Master,要么是其他进程已经是Master。

2.        如果已经实现了Master的应用逻辑,那么在此开始运行。现在只能先发表“胜利宣言”自我满足一下,sleep60s之后退出main方法。

由于没有直接处理InterruptedException,当异常发生后,程序会退出(ZooKeeper句柄会被自动关闭)。当然,现在Master在关闭之前什么都没干,我们先关注其他部分。在下一章中,Master会实现管理系统中的任务队列。

异步获得控制权

         ZooKeeper为所有的同步操作都提供了对应的异步实现版本。这有利于实现在单线程程序中发起多次请求,能够简化程序实现。本节用异步方法调用重新实现获得上一节的例子。

         以下是create方法的异步版本:

void create(String path,
            byte[] data,
            Listacl,
            CreateMode createMode,
            AsyncCallback.StringCallback cb,1
            Object ctx)2

         异步版本的create方法比同步版本增加了两个参数:

1.        回调方法对象

2.        用户自定义上下文(这个对象作为参数传递给回调方法)

异步调用通常在create请求传递给服务器之前就立即返回了。在回调方法中通常会使用我们传递的ctx上下文参数。当create请求收到服务器的响应时,ctx参数传递给被调用的回调方法。

注意create方法不会抛出异常,这能够简化编程。因为方法调用不用等待操作完成之前就返回了,所以不用担心InterruptedException;而且因为请求异常被编码为回调方法的第一个参数,所以也不用担心KeepException。

回调对象需要实现StringCallback方法:

void processResult(int rc, String path, Object ctx, String name)

         异步方法请求排成队列,通过另外一个单独线程传输到服务端。客户端收到的响应,也被一个专门的回调线程处理。单独的回调线程能保证收到的回调被顺序处理。

         rc

         异步调用的返回结果。OK或者是和各种KeepException对应的错误码。

         path

         create操作中使用的path路径参数。

         ctx

         create操作中使用的ctx上下文参数。

         name

         被创建的节点名称。

         如果创建成功,path和name参数相等,除非创建的是CreateMode.SEQUENTIAL顺序节点。

         回调处理

         因为一个独立线程处理所有回调,如果某次回调阻塞,随后的回调都会被阻塞。所以你最好不要在回调方法中做密集操作或阻塞操作。通常情况下都应该避免在回调方法中调用同步API,这样后续的回调操作才能被迅速处理。

         开始用异步方法实现Master功能。创建一个masterCreateCallback对象接收create操作的结果:

static boolean isLeader;
static StringCallback masterCreateCallback = new StringCallback() {
    void processResult(int rc, String path, Object ctx, String name) {
    switch(Code.get(rc)) {1
    case CONNECTIONLOSS:2
        checkMaster();
        return;
    case OK:3
        isLeader = true;
        break;
    default:4
        isLeader = false;
    }
    System.out.println("I'm " + (isLeader? "": "not") +
                       "the leader");
    }
};
void runForMaster(){
    zk.create("/master", serverId.getBytes(), OPEN_ACL_UNSAFE,
              CreateMode.EPHEMERAL, masterCreateCallback, null);5
}

1.        得到create操作的结果rc,转换成Code枚举变量。不等于0的rc值代表不同的KeeperException异常。

2.        如果由于连接丢失造成create操作失败,会得到CONNECTIONLOSS错误码而不是抛出ConnectionLossException异常。当连接丢失,需要检查系统状态,找出要做的恢复动作,这些操作通过后面实现的checkMaster方法来完成。

3.        Woohoo!我们成为了Leader,将isLeader设置为true。

4.        如果发生其他问题,竞争Leader失败。

5.        在runForMaster启动方法中,将masterCreateCallback对象传递给create方法。ctx参数设置为null,因为我们不需要从runForMaster方法传递信息到masterCreateCallback.processResult方法。

现在实现checkMaster方法。这个方法看起来和同步版本有一些区别,我们从调用getData异步方法开始,把原来checkMaster中的一连串处理逻辑都挪到了回调方法中。当getData操作返回后,会在DataCallback回调方法中继续进行下一步处理:                 

DataCallbackmasterCheckCallback = new DataCallback() {
    void processResult(int rc, String path, Object ctx, byte[] data,
                       Statstat) {
        switch(Code.get(rc)) {
        case CONNECTIONLOSS:
            checkMaster();
            return;
        case NONODE:
            runForMaster();
            return;
        }
    }
}
void checkMaster() {
    zk.getData("/master", false, masterCheckCallback, null);
}

异步版本的基本逻辑和同步版一致,在异步版中, while循环被回调方法以及新的异步操作所取代。

现在来看,同步版的实现要比异步版简单,但是在下一章会看到,如果程序被异步通知消息驱动,那么把所有操作都修改为异步调用会更简洁一些。异步调用还不会阻塞程序,其他动作,包括新的ZooKeeper操作都能同时进行。

设置元数据

         继续使用异步API来设置元数据目录。Master-Worker示例依赖三个目录:/tasks,/assign和/workers。可以依赖某种初始化机制来确保这几个目录在系统运行之前准备好,或者Master在每次启动时负责创建这些目录。接下去的代码片段展示了创建这些目录的过程,除了处理连接丢失之外其他错误都忽略掉了:

public void bootstrap(){
    createParent("/workers", new byte[0]);1
    createParent("/assign", new byte[0]);
    createParent("/tasks", new byte[0]);
    createParent("/status", new byte[0]);
}
void createParent(String path, byte[] data) {
    zk.create(path,
              data,
              Ids.OPEN_ACL_UNSAFE,
              CreateMode.PERSISTENT,
              createParentCallback,
              data);2
}
StringCallback createParentCallback = new StringCallback() {
    public void processResult(int rc, String path, Object ctx, String name) {
        switch (Code.get(rc)) {
        case CONNECTIONLOSS:
            createParent(path, (byte[]) ctx);3
            break;
        case OK:
            LOG.info("Parentcreated");
            break;
        case NODEEXISTS:
            LOG.warn("Parent alreadyregistered: " + path);
            break;
        default:
            LOG.error("Something wentwrong: ",
                      KeeperException.create(Code.get(rc), path));
        }
    }
};

1.        这些节点都不用包含数据,所以使用空字节数组。

2.        一般情况下,回调方法中使用ctx上下文对象来跟踪被创建节点的数据。看起来有点奇怪,create方法的第二个参数和第四个参数都是data数据对象,其实第二个参数用于创建节点,第四个参数则在createParentCallback回调方法中使用。

3.        如果遇上CONNECTIONLOSS返回码,回调函数直接调用createParent方法进行重试。因为ctx上下文对象和回调对象是独立的,而create操作中的第四个ctx上下文参数正好是节点的数据,所以一个回调方法可以处理所有的create操作。

可以注意到,ZooKeeper中的文件(含有数据的节点)和目录(含有子节点的节点)没有什么区别,一个节点可以同时是文件和目录。

注册Worker

         Master启动后,需要注册好worker才能发号施令。根据设计,每个worker都在/work节点下创建一个临时子节点。使用以下代码轻松实现,并用节点中的数据来代表worker的状态:

import java.util.*;
import org.apache.zookeeper.AsyncCallback.DataCallback;
import org.apache.zookeeper.AsyncCallback.StringCallback;
import org.apache.zookeeper.AsyncCallback.VoidCallback;
import org.apache.zookeeper.*;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.AsyncCallback.ChildrenCallback;
import org.apache.zookeeper.KeeperException.Code;
import org.apache.zookeeper.data.Stat;
import org.slf4j.*;
public class Worker implements Watcher {
    private static final Logger LOG = LoggerFactory.getLogger(Worker.class);
    ZooKeeper zk;
    String hostPort;
    String serverId = Integer.toHexString(random.nextInt());
    Worker(String hostPort) {
        this.hostPort= hostPort;
    }
    void startZK() throws IOException {
        zk = new ZooKeeper(hostPort, 15000, this);
    }
    public void process(WatchedEvent e) {
        LOG.info(e.toString() + "," + hostPort);
    }
    void register(){
        zk.create("/workers/worker-" + serverId,
                  "Idle".getBytes(),1
                  Ids.OPEN_ACL_UNSAFE,
                  CreateMode.EPHEMERAL,2
                  createWorkerCallback, null);
    }
    StringCallback createWorkerCallback = new StringCallback() {
        public void processResult(int rc, String path, Object ctx,
                                  String name) {
            switch (Code.get(rc)) {
            case CONNECTIONLOSS:
                register();3
                break;
            case OK:
                LOG.info("Registered successfully: " + serverId);
                break;
            case NODEEXISTS:
                LOG.warn("Already registered: " + serverId);
                break;
            default:
                LOG.error("Something went wrong: "
                          + KeeperException.create(Code.get(rc), path));
            }
        }
    };
    public static void main(String args[]) throws Exception {
        Worker w = new Worker(args[0]);
        w.startZK();
        w.register();
        Thread.sleep(30000);
    }
}

1.        使用节点数据表示相应Worker的状态。

2.        如果节点对应的Worker进程挂掉,临时节点会被清理。所以只要查看/workers的子节点就知道有哪些woker可用。

3.        因为只有当前进程负责创建临时节点,所以如果发生CONNECTIONLOSS,简单重试即可。

如前所述,如果Worker进程挂掉,注册的临时节点会被清理,这就实现了Worker端的组成员管理。

同时,我们把Worker的状态信息存储在节点中,查询ZooKeeper就可以知道Worker的状态。Worker的初始状态为idle,执行任务后,会被设置成其他状态值。

以下是setStatus的实现。这个方法的工作机制与之前的其他方法略有不同。为了不延迟其他操作,我们通过异步方法设置状态:

StatCallbackstatusUpdateCallback = new StatCallback() {
    public void processResult(int rc, String path, Object ctx, Stat stat) {
        switch(Code.get(rc)) {
        case CONNECTIONLOSS:
            updateStatus((String)ctx);1
            return;
        }
    }
};
synchronized private void updateStatus(String status) {
    if (status == this.status) {2
        zk.setData("/workers/" + name, status.getBytes(), -1,
                   statusUpdateCallback, status);3
    }
}
public void setStatus(String status) {
    this.status = status;4
    updateStatus(status);5
}

4.本地缓存status值,用于更新失败时重试。

5.在setStatus中不直接进行更新操作,封装成updateStatus方法,以便于处理失败重试逻辑

2.处理连接丢失时发起重试有个潜在问题:破坏系统的有序性。ZooKeeper在保证请求和响应的有序性方面做得相当好,但失败重试是则不尽然,因为发起的是一次新的请求。所以我们做了两点:a)检查更新操作是否与当前保存的状态相当。b)在同步方法中检查和重试。

3.执行无条件更新操作(第三个参数为-1,禁用版本检查),将status作为上下文参数传递给回调方法。

1.如果发生CONNECTIONLOSS,简单重试updateStatus就可以了。updateStaus还会负责检查状态竞争条件。

考虑以下场景,进一步理解CONNECTIONLOSS问题:

1.        Worker运行task-1任务,所以状态为“working”。

2.        客户端尝试执行setData操作时遇到网络问题

3.        客户端判断发生了CONNECTIONLOSS,然后在statusUpdateCallback调用之前,Worker完成了task-1任务,状态变为空闲。

4.        Worker执行setData,尝试设置状态为“idle”。

5.        客户端开始处理CONNECTIONLOSS,如果updateStatus不检查当前状态的话,会尝试把状态设置成“working”。

6.        连接重建,客户端库会老老实实地按照先后顺序发送这两次setData操作,最终Worker的状态变为“working”。

通过在setData操作之前检查当前状态,可以避免以上场景。

任务队列

         系统最后一个组件是负责提交任务队列的客户端。客户端在/tasks下增加任务节点。任务使用顺序节点,这有两方面好处:首先,序列号代表任务排队顺序。其次,序列号简化了创建唯一任务的工作量。客户端代码如下所示:

import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.Watcher;
public class Client implements Watcher {
    ZooKeeper zk;
    String hostPort;
    Client(String hostPort) {this.hostPort= hostPort; }
    void startZK() throws Exception {
        zk = new ZooKeeper(hostPort, 15000, this);
    }
    String queueCommand(Stringcommand) throws KeeperException {
        while (true) {
            try {
                String name = zk.create("/tasks/task-",1
                                        command.getBytes(), OPEN_ACL_UNSAFE,
                                        CreateMode.SEQUENTIAL);2
                return name;3
                break;
            } catch (NodeExistsExceptione) {
                throw new Exception(name + " already appears to be running");
            } catch (ConnectionLossExceptione) {4
            }
    }}
    public void process(WatchedEvent e) { System.out.println(e); }
    public static void main(String args[]) throws Exception {
        Client c = new Client(args[0]);
        c.start();
        String name = c.queueCommand(args[1]);
        System.out.println("Created " + name);
    }
}

1.        在/tasks下创建任务节点,任务名称前缀是task-。

2.        使用CreateMode.SEQUENTIAL,任务名称后缀是单调递增的序列号。ZooKeeper保证了任务名称唯一性和任务有序性。

3.        使用CreateMode.SEQUENTIAL时,无法提前知道任务序列号,需要等待create方法返回新节点名称

4.        如果create操作时发生CONNECTIONLOSS,简单重试可能会导致创建多个任务节点。对于遵循“至少执行一次”策略的应用来说,这可以正常工作。如果应用遵循“至多执行一次”策略,那么需要做一些额外工作:使用唯一ID(比如session ID)创建任务节点。如果发生CONNECTIONLOSS,检查相应任务节点不存在才进行重试。

运行客户端程序,传入命令参数,会在/tasks下创建任务节点。任务节点不是临时类型,所以即使客户端程序结束,它们还会继续存在。

管理端

         最后,我们编写一个简单的AdminClient来展示系统状态。你可以使用zkCli工具来查看ZooKeeper状态,但通常来讲,编写自己的管理端,能更加方便和快捷地管理系统。在下面的例子中,我们使用getData和getChildren方法获取Master-Worker系统的状态。

         这些方法用起来很简单,而且因为它们不会改变系统状态,所以遇到错误时无需清理,抛出错误就行。

         管理端使用同步操作。我们只对系统当前状态感兴趣,不监视系统变化,所以将watch参数设置为false(下一章会介绍使用watch参数跟踪系统变化)。以下是管理端代码:

import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.Watcher;
public class AdminClient implements Watcher {
    ZooKeeper zk;
    String hostPort;
    AdminClient(String hostPort) {this.hostPort= hostPort; }
    void start() throws Exception {
        zk = new ZooKeeper(hostPort, 15000, this);
    }
    void listState() throws KeeperException {
        try {
            Stat stat = new Stat();
            byte masterData[]= zk.getData("/master", false, stat);1
            Date startDate = new Date(stat.getCtime());2
            System.out.println("Master: " + new String(masterData) +
                               " since " + startDate);
        } catch (NoNodeExceptione) {
            System.out.println("NoMaster");
        }
        System.out.println("Workers:");
        for (Stringw: zk.getChildren("/workers", false)) {
            byte data[]= zk.getData("/workers/" + w, false, null);3
            String state = new String(data);
            System.out.println("\t" + w + ": " + state);
        }
        System.out.println("Tasks:");
        for (Stringt: zk.getChildren("/assign", false)) {
            System.out.println("\t"+ t);
        }
    }
    public void process(WatchedEvent e) {System.out.println(e); }
    public static void main(String args[])throws Exception {
        AdminClient c = new AdminClient(args[0]);
        c.start();
        c.listState();
    }
}

1.        获取/master节点的数据,即当前Master名。这里对其变化不感兴趣,所以第二个参数设置为false。

2.        Stat结构中的数据能告诉我们Master运行时间。Ctime是节点从创建到现在的秒数。细节请参考java.lang.System.currentTimeMillis()方法。

3.        这些临时节点包含两部分信息:其存在表示Worker正在运行,其数据代表Worker的状态。

管理端程序非常简单:浏览Master-Worker示例中的ZooKeeper数据。试试启动、停止Master和Worker,运行客户端提交一些任务,管理端会显示出系统状态的变化。

你可能想知道管理端使用异步API会不会更好一些?系统中存在各种延迟,最严重的是硬盘访问和网络传输,为了提高带宽利用率,它们都使用了队列。虽然getData方法不会访问硬盘,但它会使用网络。而且ZooKeeper内部通过流水线式设计能处理数千个并发请求,每个同步方法请求都会走完整个流水线。所以如果你是在一个只有数十个Worker和数百个任务的小型系统环境中,可能还没什么问题,如果增大数量级,那么延迟就很明显了。

假设一次请求的往返时间是1毫秒,如果需要读取10000个节点,会有10秒钟的网络延迟。而使用异步API,延迟时间能缩短到接近1秒。

在本章中,我们开始接触Master-Worker系统,实现了基本的Master、Worker和客户端,但实际上要做的事情还很多:当任务进入队列,Master需要被唤醒,分配任务给Worker;Worker需要找到被分配给它的任务;客户端需要知道任务何时运行完;如果Master崩溃,备用Master需要接管任务;如果Worker崩溃,任务需要被重新分配。接下来的章节中会覆盖这些概念,实现必需功能。

你可能感兴趣的:(Distributed,Programming,~ZooKeeper)