[ZooKeeper之六] ZooKeeper API基本使用

一、ZooKeeper 会话

  zkCli 只能作为一个调试工具,实际开发中用的最底层的是 ZooKeeper 原生API,有很多客户端库比如Java语言的 zkClientcurator 都是对 ZooKeeper API 的封装扩展。

  ZooKeeper API 围绕 ZooKeeper 的句柄而构建,每个API调用都需要传递该句柄,这个句柄代表了客户端与 ZooKeeper 之间的一个会话。当客户端要跟 ZooKeeper 服务端连接时,需要先通过句柄创建一个会话,连接到集群中的某台服务器上去,当这台服务器使用中发生故障,则会发生故障转移,客户端尝试连接集群中其他服务器,并将会话转移到新的服务器上去。

会话在两个服务器之间发生迁移

  在 ZooKeeper API 中,ZooKeeper对象表示一个句柄,其最基础的构造函数如下:

public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher)

connectString:zookeeper 连接串,可以是独立模式下的一台服务器和端口,也可以是仲裁模式下的集群地址,eg: 127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183
sessionTimeout:会话超时时间,如果客户端跟服务器失去连接的时间超过该值,则 ZooKeeper 会终止该会话。
watcher:用于接收会话时间的监视器对象,可以监视节点状态(删除、新增节点/子节点)和数据的变化等。



二、使用 Watcher

  ZooKeeper API中,Watcher 是一个接口,定义了 process 方法,要实现一个监视器,需要实现该接口,下面的代码简单演示了创建一个 ZooKeeper 句柄和监视器:

public class Master implements Watcher {

    ZooKeeper zk;
    String hostPort;
    
    public Master(String hostPort) {
        this.hostPort = hostPort;
    }
    
    @Override
    public void process(WatchedEvent event) {
        System.out.println(event);      
    }
    
    void start() throws IOException {
        zk = new ZooKeeper(hostPort, 15000, this);
    }
    
    void stop() throws InterruptedException {
        zk.close();
    }

    public static void main(String[] args) throws Exception {
        Master m = new Master(args[0]);
        m.start();
        
        Thread.sleep(60000);
    }   
}

  Master 实现了 Watcher 接口,在重载的 process 方法中,简单输出事件信息;为了在对象初始化完毕之后才开始创建句柄,使用 start 方法初始化句柄,设置的超时事件为15000毫秒即15秒;句柄代表了一个会话连接,除了会话超时时服务端会关闭会话,客户端显式关闭句柄也可以关闭会话,这里封装了 close 方法;main 方法传入一个连接串,这里我连接的是本地的 127.0.0.1:2181

  启动 zookeeper 服务端,执行程序,日志如下,先初始化客户端连接,然后打开与 zk server 的连接,socket 连接建立后初始化会话,最后完成会话创建,由于设置了监视器,这时会收到一个同步连接时间的通知。


  如果没有打印日志,可能是没有加上日志配置文件,可从 zookeeper 工具包的 conf 目录下的 log4j.properties 文件加到工程的资源目录中。


  当服务端连接服务器发生故障,客户端会尝试连接集群中其他机器,由于本地服务端只有一个服务节点,也就是独立模式,所以客户端会不断发起socket连接。




三、竞争主节点

1、同步竞争

  主节点的竞争是一个抢锁的过程,假设锁对应的节点是 /master,抢锁就是各个节点进程尝试去创建 /master 的过程,ZooKeeper 对象提供了一系列重载的创建节点的 create 方法,最简单的重载方法方法头如下:

public String create(final String path, byte[] data, List acl, CreateMode createMode)

path:要创建的节点路径,比如在主节点是 /master
data:节点存储数据,这里我们要区分出是哪个节点创建了 /master,因此节点存储服务节点的唯一标识。
acl:接入控制列表,可以对节点的访问权限用 ZooKeeper 提供的ACL策略来控制,这里用最简单的 Ids.OPEN_ACL_UNSAFE,这是不安全的,生产可以根据需要设置。
createMode:创建模式,指定节点类型,为了让主节点故障时自动释放锁,/master 应该创建为临时节点 CreateMode.EPHEMERAL

  为了表示节点唯一标识,定义一个 serverId 的成员,这里使用随机数的哈希来初始化

serverId = Integer.toHexString(random.nextInt());

  用最简单的 create 方法创建节点时,可能会抛出异常,主要是 KeeperExceptionInterruptedException

  其中 KeeperException 是 ZooKeeper API 中所有异常类的父类,ConnectionLossException 是其子类,常发生于客户端与 ZooKeeper 服务端失去连接,原因可能是网络分区、响应超时、服务器故障等,这是我们在使用 ZooKeeper API 操作节点时经常需要处理的异常,发生连接丢失时,客户端不知道连接是在请求被处理前丢失的还是处理完返回处理结果丢失的,因为发生该异常时,通常都需要去检查操作的节点状态数据,并重新发起请求。

  InterruptedException 通常发生在客户端线程,或者说使用 ZooKeeper 服务的应用程序的主动或被动部分中断,但还在被程序中的其他部分所使用导致,同样应用程序发生中断时不知道操作是否被完成,需要检查或重新发起请求。

  回到主节点的竞争中来,当一个节点在抢锁时,该锁可能已经被其他节点锁占有,这样创建 /master 时可能抛出 NodeExistsException,这也是 KeeperException 的子类,表示该节点已存在,这时候当前节点就要放弃竞争,代码如下所示:

    void runForMaster() throws KeeperException, InterruptedException {
        while (true) {
            try {
                zk.create("/master", serverId.getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
            } catch (NodeExistsException e) {
                LOG.warn("The master node already exists");
                isLeader = false;
                break;              
            } catch (ConnectionLossException e) {}
            LOG.warn("Try to run for master again after check master.");
        }
    }

  当 /master 已存在时,结束创建 /master 的尝试;当发生连接丢失时,不断尝试创建 /master,这里我们在类中定义一个布尔类型的成员 isLeader,用来表示当前节点是否成为主节点。

  这里考虑一个问题,当连接丢失时,有可能当前节点已经成功创建 /master 了,只是返回结果的响应丢失了,因此这里可以优化,在重新发起请求(while循环控制)前,先检查是否存在 /master 节点,如果存在并且存储的表示节点唯一标识的 serverId 数据跟当前节点一致,则可以确定当前节点成为主节点,否则是从节点,退出竞争;如果不存在 /master,继续竞争,返回的布尔值表示是否要继续循环,这里通过 getData 方法达到上述目的,代码如下:

    boolean checkMaster() throws KeeperException, InterruptedException {
        while (true) {
            try {
                Stat stat = new Stat();
                byte[] data = zk.getData("/master", false, stat);
                isLeader = new String(data).equals(serverId);
                return true;
            } catch (NoNodeException e) {
                return false;
            } catch (ConnectionLossException e) {           
            }
        }
    }

  getData 方法的方法头如下:

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

path:要获取数据对应的 znode 路径。
watch:是否要监视,如果设为 true,则节点数据更新时,会通过 ZooKeeper 句柄创建时传入的监视器通知当前节点。
stat:假如znode存在,其元数据会填充到该对象中。
  综上所述,完整的代码如下:

public class SyncMaster implements Watcher {
    private static final Logger LOG = LoggerFactory.getLogger(SyncMaster.class);
    
    ZooKeeper zk;
    String hostPort;
    Random random = new Random();
    String serverId;
    boolean isLeader = false;
    
    public SyncMaster(String hostPort) {
        this.hostPort = hostPort;
        serverId = Integer.toHexString(random.nextInt());
        LOG.info("serverId = " + serverId);
    }
    
    @Override
    public void process(WatchedEvent event) {
        System.out.println(event);      
    }
    
    void start() throws IOException {
        zk = new ZooKeeper(hostPort, 15000, this);
    }
    
    void stop() throws InterruptedException {
        zk.close();
    }

    void runForMaster() throws KeeperException, InterruptedException {
        while (true) {
            try {
                zk.create("/master", serverId.getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
            } catch (NodeExistsException e) {
                LOG.warn("The master node already exists");
                isLeader = false;
                break;              
            } catch (ConnectionLossException e) {}
            LOG.warn("Try to run for master again after check master.");
            if (checkMaster()) {
                if (isLeader) {
                    LOG.warn("The master node already exists, this server is the master!");
                } else {
                    LOG.warn("The master node already exists, this server is not the master!");
                }
                break;
            }
        }
    }
    
    boolean checkMaster() throws KeeperException, InterruptedException {
        while (true) {
            try {
                Stat stat = new Stat();
                byte[] data = zk.getData("/master", false, stat);
                isLeader = new String(data).equals(serverId);
                return true;
            } catch (NoNodeException e) {
                return false;
            } catch (ConnectionLossException e) {           
            }
        }
    }
    
    public static void main(String[] args) throws Exception {
        SyncMaster m = new SyncMaster(args[0]);
        m.start();
        m.runForMaster();
        
        if (m.isLeader) {
            LOG.warn("This server is the master, serverId = " + m.serverId);            
        } else {
            LOG.warn("The master node already exists.");
        }
        
        Thread.currentThread().join();
    }
    
}

  启动两个线程模拟竞争主节点的两个节点,运行结果如下:

第一个节点日志

  从第一个节点的日志可以看出,尝试创建 /master 时发生了 ConnectionLossException,在重新尝试创建之前,先去获取 /master znode数据,结果发生其实创建成功了,可以确定该节点成为主节点了。

第二个节点日志

  从第二个节点日志可以看出,第一次尝试去竞争时就抛出节点已存在异常,可以确定已经有其他节点成为主节点。

2、异步竞争

  ZooKeeper API 中所有操作 znode 的方法,都提供了异步处理的版本,这也是实际应用中常用的方式。

Q1:使用异步方式操作znode有什么好处?

  在Java中,可以使用线程池来达到异步回调的效果,将任务的提交和实际处理分离,提交任务的线程不需要阻塞等待任务执行完,实际任务处理时,由执行器去线程池中获取空闲线程去执行任务,这样做可以避免请求阻塞,提高任务处理的速度,有点像消息队列。

  同样的,ZooKeeper API 中设计了异步操作的模式,有利于客户端快速处理大量请求,避免阻塞等待请求处理结果,而是通过回调函数异步地等待服务端通知,不过这里要特别注意的是,对于一个客户端来说,对回调函数的处理是单线程的,所以如果在一个回调函数中阻塞或者存在耗时的操作,会导致其他通知回调的处理迟迟无法得到处理,因此要避免在回调函数中集中处理或者阻塞

  在同步处理的时候,我们要处理各种奇奇怪怪的异常,而在异步处理中,会变成一个 return code 被回调函数所处理,这使得代码处理上更加优雅,当然 return code 也包含了非异常的其他情况。

  这里做个实验来验证回调单线程处理,代码如下:

public class AsyncCallback implements Watcher {
    
    ZooKeeper zk;
    String hostPort;
    CountDownLatch latch = new CountDownLatch(1);
    
    public AsyncCallback(String hostPort) {
        this.hostPort = hostPort;
    }
    
    @Override
    public void process(WatchedEvent event) {
        System.out.println(event);      
    }

    void start() throws IOException {
        zk = new ZooKeeper(hostPort, 5000, this);
    }
    
    void testCallback() {
        zk.getData("/node1", this, node1Callback, null);
        zk.getData("/node2", this, node2Callback, null);
    }
    
    DataCallback node1Callback = new DataCallback() {       
        @Override
        public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {
            System.out.println("/node1's callback");
            try {
                // 阻塞
                latch.await();
                System.out.println("阻塞结束");
            } catch (InterruptedException e) {              
                e.printStackTrace();
            }
        }
    };
    
    DataCallback node2Callback = new DataCallback() {       
        @Override
        public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {
            System.out.println("/node2's callback");
        }
    };  
    
    public static void main(String[] args) throws Exception {
        AsyncCallback cb = new AsyncCallback("127.0.0.1:2181");
        cb.start();
        cb.testCallback();
        
        Thread.sleep(10000);   // 休眠10秒,之后闩减
        cb.latch.countDown();
    }
}

  执行结果如下,很明显,第一个回调被阻塞时,第二个回调一直没办法处理,直到第一个回调处理完,才触发对第二个回调的处理,验证了客户端处理回调是单线程的。



Q2:客户端单线程处理回调可以怎样优化?

  回调函数只用来接收回调通知,具体操作封装成任务提交给线程池异步去异步执行,这样可以避免单线程阻塞,同时提高回调处理效率。



对于创建znode的操作,其异步处理的方法方法头如下:

public void create(final String path, byte[] data, List acl, CreateMode createMode, StringCallback cb, Object ctx)

cb:回调函数,参数类型是一个接口,开发人员可以根据自己需求继承该接口定制
ctx:存储节点或操作信息的上下文,通常用于重新发起操作或检查节点,使用很灵活

  下面将主节点竞争改写成异步处理的形式,isElecting 表示当前节点是否已结束竞争。

    void runForMaster() {
        zk.create("/master", serverId.getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL, masterCreateCallback, null);
    }
    
    StringCallback masterCreateCallback = new StringCallback() {        
        @Override
        public void processResult(int rc, String path, Object ctx, String name) {
            switch (Code.get(rc)) {
                case CONNECTIONLOSS:
                    checkMaster();
                    break;
    
                case NODEEXISTS:
                    LOG.warn("The '/master' znode already exists, set '/master' exist watcher!");
                    isLeader = false;
                    isElecting = false;
                    //masterExists();   // 优化点:没抢锁成功则设置 /master 的监视器
                    break;
                    
                case OK:
                    LOG.info("Create '/master' znode successfully, serverId = " + serverId);
                    isLeader = true;
                    isElecting = false;     
                    break;
                    
                default:
                    LOG.error("Error when trying to create '/master' znode.", KeeperException.create(Code.get(rc), path));
                    isElecting = false;
            }           
        }
    };

  这里有个优化的点,前面同步处理实现时,只是一次性的竞争,当主节点发生故障时,并不能发起一次新的选举,因此异步处理这里可以完善,当一个节点竞争失败时,应该设置对 /master 的监视器,这样当主节点故障时,就能收到通知,东山再起,这里使用带回调对象和监视器的 exists 方法来实现。

    // 监听主节点变化,方便备用主节点在主节点挂掉时竞争成为新的主节点
    void masterExists() {
        zk.exists("/master", masterExistsWatcher, masterExistsCallback, null);      
    }
    
    Watcher masterExistsWatcher = new Watcher() {       
        @Override
        public void process(WatchedEvent e) {
            LOG.info("masterExistsWatcher receive event notify: " + e);
            if (e.getType() == EventType.NodeDeleted) {
                assert "/master".equals(e.getPath());
                runForMaster();
            }           
        }
    };
    
    StatCallback masterExistsCallback = new StatCallback() {
        @Override
        public void processResult(int rc, String path, Object ctx, Stat stat) {
            LOG.info("masterExistsCallback start!");
            switch (Code.get(rc)) {
                // 连接丢失时重试
                case CONNECTIONLOSS:
                    masterExists();
                    break;
    
                // 如果返回OK,判断 znode 节点是否存在,不存在就竞选主节点
                case OK:
                    if (stat == null) {
                        LOG.info("return OK but stat is null, so call runForMaster()");
                        state = MasterStates.RUNNING;                       
                        runForMaster();
                    }                   
                    break;
                    
                // 如果发生意外情况,通过获取节点数据来检查 /master 是否存在
                default:
                    LOG.info("Error when trying to judge wether '/master' exist or not, so call checkMaster()");
                    checkMaster();
                    break;
            }
        }       
    };


3、元数据设置

  顺着主从模式实现的思路,当主节点选举好了,就要创建相关的znode来放任务、工作节点和任务分配,分别是 /tasks/workers/assign,这里还专门创建一个 /status,用来存放任务执行结果,比如任务 task-0000000001 执行结果后,将结果放在 /status/task-0000000001 下。在 masterCreateCallbackOK 分支中调用初始化方法 bootstrap(),该方法代码如下:

    void bootstrap() {
        createParent("/workers", new byte[0]);
        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, parentCreateCallback, data);
    }   
    
    StringCallback parentCreateCallback = new StringCallback() {        
        @Override
        public void processResult(int rc, String path, Object ctx, String name) {
            switch (Code.get(rc)) {
                case CONNECTIONLOSS:
                    createParent(path, (byte[]) ctx);
                    break;
    
                case OK:
                    LOG.info("Parent created: " + path);
                    break;
                    
                case NODEEXISTS:
                    LOG.warn("Parent already registered: " + path);
                    break;
                    
                default:
                    LOG.error("Something went wrong: ", KeeperException.create(Code.get(rc), path));
            }
        }
    };

  如果在回调函数中得到 CONNECTIONLOSS 的返回码,则调用 createParent 方法重试;正常情况下会创建成功,当主节点故障,其他节点当选主节点时,也需要调用 bootstrap 方法,可能相关znode已经被上一任主节点创建好了,所以可能得到 NODEEXISTS 的返回码,其他情况简单处理为发生错误或异常。


四、注册从节点

  注册从节点时,为每个从节点在 zookeeper 上创建一个对应的临时znode,这样当从节点故障,该临时znode自动删除,主节点通过监视 /workers 子节点的变化,来动态检测有哪些正常的从节点,代码如下,serverId 是每个从节点的唯一标识,连接丢失时重试,如果注册成功或发现znode已存在,则获取分配的任务列表。

    void register() {
        zk.create("/workers/worker-" + serverId, status.getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL, createWorkerCallback, null);
    }
    
    StringCallback createWorkerCallback = new StringCallback() {        
        @Override
        public void processResult(int rc, String path, Object ctx, String name) {
            switch (Code.get(rc)) {
                case CONNECTIONLOSS:
                    register();
                    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));
            }
            
        }
    };



五、客户端

  主-从模式中的主节点和工作节点,对客户端来说,都是服务端,客户端相当于使用服务的应用程序,向服务端提交任务,提交任务就是在 /tasks 下创建对应的节点,为了多个客户端提交的任务名不重复,且任务提交后,即使系统发生故障也不应该丢失任务数据,所以znode类型为持久顺序节点,代码如下:

    String queueCommand(String command) throws KeeperException, InterruptedException {
        while (true) {
            try {
                String name = zk.create("/tasks/task-", command.getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);
                return name;                
            } catch (NodeExistsException e) {
                LOG.warn("The node already exists!");
                return null;
            } catch (ConnectionLossException e) {               
            }
        }
    }

  如果任务提交处理失败,会进入循环处理重新提交,这里存在一个问题,当任务提交失败抛出异常时,假设是连接丢失导致,这时可能已经成功在 /tasks 上创建了一个节点,简单重复提交,可能会导致为了提交一个任务,实际上在 /tasks 下创建了两个节点,如果任务是可以被重复执行,不考虑性能问题,可以简单重试,如果任务重复执行会导致应用程序不一致,则需要自行定义任务名,并且使用持久节点


六、完整代码

1、竞争主节点

  使用异步竞争的主节点完整代码如下:

package zookeeper.master;

import java.io.IOException;
import java.util.Random;

import org.apache.zookeeper.AsyncCallback.DataCallback;
import org.apache.zookeeper.AsyncCallback.StatCallback;
import org.apache.zookeeper.AsyncCallback.StringCallback;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.KeeperException.Code;
import org.apache.zookeeper.Watcher.Event.EventType;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Master implements Watcher {
    private static final Logger LOG = LoggerFactory.getLogger(Master2.class);
    
    ZooKeeper zk;
    String hostPort;
    Random random = new Random(System.currentTimeMillis());
    String serverId;
    boolean isLeader;
    boolean isElecting = true;
    
    public Master(String hostPort) {
        this.hostPort = hostPort;
        // 生成节点唯一ID
        this.serverId = Integer.toHexString(random.nextInt());
        LOG.info("serverId = " + serverId);
    }
    
    @Override
    public void process(WatchedEvent event) {
        System.out.println(event);      
    }
    
    // 初始化 zookeeper 连接句柄对象
    void start() throws IOException {
        zk = new ZooKeeper(hostPort, 5000, this);
    }
    
    void stop() throws InterruptedException {
        zk.close();
    }
    
    // 竞争主节点
    void runForMaster() {
        zk.create("/master", serverId.getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL, masterCreateCallback, null);
    }

    // 竞争主节点回调函数
    StringCallback masterCreateCallback = new StringCallback() {        
        @Override
        public void processResult(int rc, String path, Object ctx, String name) {
            switch (Code.get(rc)) {
                // 连接丢失,简单主节点是否已存在
                case CONNECTIONLOSS:
                    checkMaster();
                    break;
    
                // 主节点已存在,为当前节点设置监视器
                case NODEEXISTS:
                    LOG.warn("The '/master' znode already exists, set '/master' exist watcher!");
                    isLeader = false;
                    isElecting = false;
                    masterExists();
                    break;
                    
                // 成功当选主节点
                case OK:
                    LOG.info("Create '/master' znode successfully, serverId = " + serverId);
                    isLeader = true;
                    isElecting = false;     
                    LOG.info("Starting to bootstrap!");
                    bootstrap();
                    break;
                    
                default:
                    LOG.error("Error when trying to create '/master' znode.", KeeperException.create(Code.get(rc), path));
                    isElecting = false;
            }
            
        }
    };
    
    // 检查主节点是否存在及获取数据
    void checkMaster() {
        zk.getData("/master", false, masterCheckCallback, null);
    }
    
    DataCallback masterCheckCallback = new DataCallback() {     
        @Override
        public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {
            switch (Code.get(rc)) {
                // 连接丢失,重复检查
                case CONNECTIONLOSS:
                    checkMaster();
                    break;
                    
                // 节点不存在,重新竞争主节点
                case NONODE:
                    runForMaster();
                    break;
                    
                // 主节点存在,获取节点数据跟当前节点唯一标识比较判断当前节点是否已当选主节点
                case OK:
                    isLeader = new String(data).equals(serverId);
                    if (isLeader) {
                        LOG.info("The '/master' znode already exists, this server is the master!");
                    } else {
                        LOG.info("The '/master' znode already exists, this server is not the master!");
                    }
                    isElecting = false;
                    break;
    
                default:
                    LOG.error("Error when trying to get data of '/master' znode.", KeeperException.create(Code.get(rc), path));
                    isElecting = false;
            }
            
        }
    };
    
    // 设置主节点监视器
    void masterExists() {
        zk.exists("/master", masterExistsWatcher, masterExistsCallback, null);
    }
    
    // 主节点被删除时,触发本监视器,通知当前节点重新竞选主节点
    Watcher masterExistsWatcher = new Watcher() {       
        @Override
        public void process(WatchedEvent e) {
            LOG.info("masterExistsWatcher receive event notify: " + e);
            if (e.getType() == EventType.NodeDeleted) {
                assert "/master".equals(e.getPath());
                runForMaster();
            }
        }
    };
    
    StatCallback masterExistsCallback = new StatCallback() {
        
        @Override
        public void processResult(int rc, String path, Object ctx, Stat stat) {
            switch (Code.get(rc)) {
                // 连接丢失,重新设置监视器
                case CONNECTIONLOSS:
                    masterExists();
                    break;
                
                // 检查成功,如果Stat对象为空,证明当前不存在主节点,可竞争主节点
                case OK:
                    if (stat == null) {
                        LOG.info("return OK but stat is null, so call runForMaster()");                                         
                        runForMaster();
                    }
                    break;
    
                default:
                    LOG.info("Error when trying to judge wether '/master' exist or not, so call checkMaster()");
                    checkMaster();
                    break;
            }
            
        }
    };
    
    void bootstrap() {
        createParent("/workers", new byte[0]);
        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, parentCreateCallback, data);
    }   
    
    StringCallback parentCreateCallback = new StringCallback() {        
        @Override
        public void processResult(int rc, String path, Object ctx, String name) {
            switch (Code.get(rc)) {
                case CONNECTIONLOSS:
                    createParent(path, (byte[]) ctx);
                    break;
    
                case OK:
                    LOG.info("Parent created: " + path);
                    break;
                    
                case NODEEXISTS:
                    LOG.warn("Parent already registered: " + path);
                    break;
                    
                default:
                    LOG.error("Something went wrong: ", KeeperException.create(Code.get(rc), path));
            }
        }
    };
    
    public static void main(String[] args) throws Exception {
        Master m = new Master("127.0.0.1:2181");
        m.start();
        m.runForMaster();
        
        while (m.isElecting) {
            if (m.isLeader) {
                LOG.info("I'm the master, serverId = " + m.serverId);
            } else {
                LOG.info("The master already exists.");
            }
            
            Thread.sleep(1000);
        }
        LOG.info("The master init election is over!");
        
        Thread.currentThread().join();
    }
}

  执行结果,启动程序的多个实例,这里启动了三个实例





  模拟主节点故障,备用节点竞争转移主节点故障的情况




2、注册从节点

  完整代码

package zookeeper.slave;

import java.io.IOException;
import java.util.Random;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.AsyncCallback.StringCallback;
import org.apache.zookeeper.KeeperException.Code;
import org.apache.zookeeper.ZooDefs.Ids;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Worker implements Watcher {
    private static final Logger LOG = LoggerFactory.getLogger(Worker2.class);

    ZooKeeper zk;
    String hostPort;
    Random random = new Random();
    String serverId;
    
    public Worker(String hostPort) {
        this.hostPort = hostPort;
        serverId = Integer.toHexString(random.nextInt());
        LOG.info("serverId = " + serverId);
    }
    
    @Override
    public void process(WatchedEvent event) {
        System.out.println(event);          
    }

    void start() throws IOException {
        zk = new ZooKeeper(hostPort, 5000, this);
    }
    
    void stop() throws InterruptedException {
        zk.close();
    }
    
    void register() {
        zk.create("/workers/worker-" + serverId, serverId.getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL, createWorkerCallback, null);
    }
    
    StringCallback createWorkerCallback = new StringCallback() {        
        @Override
        public void processResult(int rc, String path, Object ctx, String name) {
            switch (Code.get(rc)) {
                case CONNECTIONLOSS:
                    register();
                    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("127.0.0.1:2181");
        w.start();
        w.register();
        
        Thread.currentThread().join();
    }
    
}

  执行结果



3、客户端提交任务

  完整代码:

package zookeeper.client;

import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.zookeeper.AsyncCallback.DataCallback;
import org.apache.zookeeper.AsyncCallback.StatCallback;
import org.apache.zookeeper.AsyncCallback.StringCallback;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.KeeperException.Code;
import org.apache.zookeeper.KeeperException.ConnectionLossException;
import org.apache.zookeeper.KeeperException.NodeExistsException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.Watcher.Event.EventType;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.data.Stat;
import org.apache.zookeeper.ZooKeeper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Client implements Watcher {
    private static final Logger LOG = LoggerFactory.getLogger(Client.class);
    
    ZooKeeper zk;
    String hostPort;
    ConcurrentHashMap ctxMap = new ConcurrentHashMap();
    
    public Client(String hostPort) {
        this.hostPort = hostPort;
    }
    
    @Override
    public void process(WatchedEvent event) {
        System.out.println(event);      
    }
    
    void start() throws IOException {
        zk = new ZooKeeper(hostPort, 5000, this);
    }
    
    void stop() throws InterruptedException {
        zk.close();
    }
    
    // 只提交不管执行结果
    String queueCommand(String command) throws KeeperException, InterruptedException {
        while (true) {
            try {
                String name = zk.create("/tasks/task-", command.getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);
                return name;                
            } catch (NodeExistsException e) {
                LOG.warn("The node already exists!");
                return null;
            } catch (ConnectionLossException e) {               
            }
        }
    }

    public static void main(String[] args) throws Exception {
        Client c = new Client("127.0.0.1:2181");
        c.start();
        c.queueCommand("Hello World");
        Thread.currentThread().join();
    }
}

  执行结果


上面演示了程序执行的结果,当然程序是不完善的,没有将各个部分关联起来,不过演示API的基本使用已经足够了,后面会介绍如何完善这一程序。

你可能感兴趣的:([ZooKeeper之六] ZooKeeper API基本使用)