[ZooKeeper之七] 使用 Watcher 监视机制处理状态变化

一、轮询与通知

  在分布式系统中,检测系统状态是非常重要的工作,只有实时检测到节点故障和故障原因,才能采取相应的处理方案,保证整个系统对外提供服务上的高可用。

  检测手段上,可以是定时通信,假如没有收到响应(某些场景会有重试次数)可以认为服务器发生故障了,在 ZooKeeper 集群内部,集群中的每个节点都要跟主节点建立通信连接,方便从主节点同步事务到从节点;当主节点崩溃时,会触发群首选举,此时集群中的所有节点互相之间也有通信,直到选出群首。这种通信是构建 ZooKeeper 服务的基础,每时每刻都在进行,一般会定时发起一个请求,检查通信是否正常,一般定时的周期是由配置参数控制的,发起请求时不可能一直等待,所以一般也会有个配置参数控制的超时时间。

  对于应用程序来说,轮询却往往不是高效的方式,特别是期望的变化发生的频率很低时。比如主-从模型中,多个节点选出主节点后,如果采用轮询的方式,备用主节点得定期检查代表主节点的znode /master 正常,假如主节点崩溃的频率很低,则这种轮询检测都是在做无用功,为了减小轮询的消耗,可以将周期调大一点,但是这样一来假如主节点故障又会导致发现故障时间和故障持续时间变长。

  针对基于 ZooKeeper 的应用程序,ZooKeeper 提供了针对处理变化的 Watcher 监视机制,应用程序可以针对某个znode注册一个监视点(watch),设置监视事件(比如节点存在状态、子节点数量、节点数据)变化,变化发生时就会收到一个单次的通知,如果处理完之后还想持续监视,就需要注册新的监视点。

zookeeper监视点的注册和变化通知

Q1:为什么监视点事件变化是单次触发的?是否会丢失事件?

  ZooKeeper API中对监视点事件变化通知的触发是一次性的,如果想要持续监视和收到通知,就需要在收到通知处理完之后重新设置监视点。在 [ZooKeeper之二] ZooKeeper 基础和架构 中介绍过为什么监视通知只通知一次,是为了保障全局有序,但是这样有可能会错过连续变化,因为可能事件在通知之后新的监视点设置之前又发生了,如图所示:


(1)客户端C2读取任务列表,其初始值为空,并设置监视点
(2)客户端C1创建了一个任务,通知C1
(3)客户端C1创建了第二个任务
(4)客户端C2读取任务列表,获取task-1、task-2

  这时需要客户端主动查询读取 ZooKeeper 节点数据和状态,这样就不会错误所有变化了。从性能角度考虑,在高频率的事件变化中,通过通知可客户端主动查询,可以提高通知的效率,假设有10个事件变化,在第1个和第6个变化时推送了通知,第2到第5个变化之在两个通知间隙客户端主动查询获取的,第7到第10个变化是在第三次设置监视点之前客户端主动获取的,这样平均每个事件只产生了0.2个通知,虽然产生了主动查询的开销,但是从总体上看10个变化开销了两个通知和两个主动查询,比如每个变化都推送通知需要开销10个通知还是很划算的。

  但是对于开发者来说,ZooKeeper API 还是太底层了,要花费很多精力处理底层通讯细节,所以实际开发中,使用的是各种 ZooKeeper API 的高级封装库,比如Java的 zkClientcurator 还有其他编程语言的封装库,它们大都封装了对监视点通知后重新设置新的监视点的操作。

Q2:被动通知和主动查询是否会漏掉一些节点变化产生ABA问题?ZooKeeper API是如何解决的?

  有时候,收到通知后主动查询并重设监视点不一定能察觉到收到通知到设置新的监视点这段时间的变化,如下图所示:


(1)客户端1对 /config 设置了数据变化的监视点
(2)客户端2修改了数据
(3)触发 ZooKeeper 对客户端1的通知
(4)客户端2将数据设置为3
(5)客户端2将数据设置为2
(6)客户端1为了查询是否有新的变化和设置监视点,发现数据还是它收到通知时的2

  虽然客户端1重设监视点之前检查到数据还是跟收到通知时一样,觉得中间没有变化,但实际上已经变化了,节点数据2不再是原来触发通知时的那个2了,产生了ABA问题。

  ZooKeeper 为了保证全局有序,每个znode都有一个版本号,当对节点进行写操作时,版本号就会加1,利用 ZooKeeper 的版本特性,可以识别出中间是否变化发生,ZooKeeper API中的 Stat 对象保存了操作节点的元数据包括版本号,所以可以通过 Stat 来获取版本号,同时 ZooKeeper API 中也有一些带版本参数的方法,比如:

 public void setData(final String path, byte[] data, int version, StatCallback cb, Object ctx)

  这是设置节点数据的方法,只有传入的版本号跟节点的当前版本号一致时,才会执行写操作。


二、设置监视点

  使用监视点,需要实现 Watcher 接口,并实现 process 方法

void process(WatchedEvent event);

  ZooKeeper 中所有的写操作:getDatagetChildrenexists 都可以设置监视点,WatchedEvent 对象的数据结构包括:

    private final KeeperState keeperState;
    private final EventType eventType;
    private String path;

KeeperState:会话状态,包括UnknownDisconnectedNoSyncConnectedSyncConnectedAuthFailedConnectedReadOnlySaslAuthenticatedExpiredClosed
EventType:事件类型,包括NoneNodeCreatedNodeDeletedNodeDataChangedNodeChildrenChangedDataWatchRemovedChildWatchRemovedPersistentWatchRemoved
path:节点路径。

  监视点有两种:数据监视点和子节点监视点。getDataexists 可以设置数据监视点,当节点数据被修改或者节点创建删除时,都会触发数据监视点;getChildren 可以设置子节点监视点,当新增或删除子节点时会触发,修改子节点数据则不会。所以每种时间类型可以设置的监视点如下:

事件类型 监视点
NodeCreated exists
NodeDeleted exists/getData
NodeDataChanged exists/getData
NodeChildrenChanged getChildren

  ZooKeeper 为所有可以设置监视点的方法都提供了同步和异步的方法,同步方法只能选择使用 ZooKeeper 对象定义时传入的 Watcher 监视器,如果传入的watch参数值为false,则不使用监视器,如下所示:

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

  如果不想使用 ZooKeeper 对象默认的监视器,则可以使用下面的方法

public byte[] getData(final String path, Watcher watcher, Stat stat)

  更多时候,开发者使用回调加自定义监视器的组合

public void getData(final String path, Watcher watcher, DataCallback cb, Object ctx)

  当然也可以使用回调加默认监视器

public void getData(String path, boolean watch, DataCallback cb, Object ctx) 



三、完善主-从模型

  在 [ZooKeeper之六] ZooKeeper API基本使用 中使用 ZooKeeper API 构建了主-从模型,但只实现了部分功能,主节点、工作节点、客户端之间还不能有效结合起来处理任务,在完善主-从模型之前,先分别梳理出主节点、从节点和客户端所要处理的变化,从而利用监视机制实现。

主节点(备用主节点)

1、主节点选举
2、动态感知从节点变化
3、动态感知客户端提交任务

工作节点

1、等待主节点分配任务

客户端

1、等待任务执行结果


1、主节点

(1)主节点选举

  主节点选举实际上就是抢锁创建 /master 的过程,如果创建成功表示当前节点当选为主节点,否则成为备用主节点,需要设置 /master 是否存在的数据监视点。

  在抢锁过程中,如果发生连接丢失,当前节点无法确认是什么原因导致,有可能此时已经有节点当选为主节点,并且该节点有可能是当前节点,所以连接丢失时要先获取 /master 数据。获取数据时连接丢失则接着获取;如果返回码表示节点不存在,则重新投入选举;如果 /master 已存在,则判断存储节点唯一标识的数据是否跟当前节点一致,如果一致表示当前节点成功当选主节点,否则当前节点成为备用主节点,并对 /master 设置监视点。

  在抢锁过程中,如果返回节点已存在,表示已经有其他节点当选为主节点,此时当前节点成为备用主节点,并对 /master 设置监视点。

  在抢锁的过程中,如果返回操作成功,表示当前节点当选为主节点,接下来初始化 /workers/tasks/assign/status相关znode。

  相关代码已经在 [ZooKeeper之六] ZooKeeper API基本使用 中贴出,这里不再重复。


(2)动态感知从节点变化

  当主节点选举出来并完成相关目录的初始化后,从节点就可以加入进来了,这里创建 getWorker(),在初始化代码里调用,如下:

    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);
                    if ("/workers".equals(path)) {
                        LOG.info("Init create '/workers' successfully, find workers now!");
                        getWorkers();
                    }                   
                    break;
                    
                case NODEEXISTS:
                    LOG.warn("Parent already registered: " + path);
                    if ("/workers".equals(path)) {
                        LOG.info("Find exist znode '/workers' successfully, find workers now!");
                        getWorkers();
                    }
                    break;
                    
                default:
                    LOG.error("Something went wrong: ", KeeperException.create(Code.get(rc), path));
            }
        }
    };

  每个从节点都会在 /workers 下创建一个znode,形式如:/workers/worker-id,主节点为了动态感知从节点变化,需要设置对 /workers 的子节点监视器,当新的从节点加入时,就会收到通知,代码如下:

    void getWorkers() {
        zk.getChildren("/workers", 
                       workersChangeWatcher, 
                       workersGetChildrenCallback, 
                       null);
    }
    
    Watcher workersChangeWatcher = new Watcher() {
        @Override
        public void process(WatchedEvent event) {
            if (event.getType() == EventType.NodeChildrenChanged) {
                assert "/workers".equals(event.getPath());
                getWorkers();
            }
        }
    };
    
    ChildrenCallback workersGetChildrenCallback = new ChildrenCallback() {      
        @Override
        public void processResult(int rc, String path, Object ctx, List children) {
            switch (Code.get(rc)) {
                case CONNECTIONLOSS:
                    getWorkers();
                    break;
                    
                case OK:
                    LOG.info("Successfully got a list of workers: " + children.size() + " workers");
                    // 为从节点列表创建对应的znode "/assign/worker-id"
                    reassignAndSet(children);
                    break;
                    
                default:
                    LOG.error("getChildren failed", KeeperException.create(Code.get(rc), path));
            }           
        }
    };

  当主节点收到从节点变化的通知时,同时返回从节点列表,我们需要为从节点列表创建对象的znode,路径为 /assign/worker-id(为了避免分配任务信息丢失,znode应该是持久节点),在 reassignAndSet 方法中完成处理。由于返回的从节点列表是全量版本,这意味着对于已经创建对应znode的worker可能重复创建 /assign/worker-id,为了避免不必要的重复创建,可以在主节点设置一个当前活跃已初始化(即创建了对应的 /assign/worker-id)工作节点缓存,缓存类 ChildrenCache 代码如下:

/**
 * 缓存活跃的工作从节点信息
 */
public class ChildrenCache {
    private List activeWorkerCache;
    private List newWorker = new ArrayList<>();         // 为第一次加入的工作节点创建 '/assign/worker-id' 节点
    
    public ChildrenCache(List workers) {
        activeWorkerCache = workers;
    }
    
    // 从旧的缓存中找出已经死掉的从节点,用于重新分配分配给这些节点但还未完成的任务,并更新缓存
    public List removeAndSet(List workers) {
        List deadWorkers = new ArrayList<>();
        for (String w : activeWorkerCache) {
            if (!workers.contains(w)) {
                deadWorkers.add(w);             
            }
        }   
        
        newWorker = new ArrayList<>();
        for (String w : workers) {
            if (!activeWorkerCache.contains(w)) {
                newWorker.add(w);
            }
        }
        activeWorkerCache = workers;        
        return deadWorkers;
    }
    
    public int size() {
        return activeWorkerCache.size();
    }
    
    public String get(int index) {
        return activeWorkerCache.get(index);
    }

    public List getActiveWorkerCache() {
        return activeWorkerCache;
    }

    public List getNewWorker() {
        return newWorker;
    }   
}

activeWorkerCache:当前活跃工作节点缓存
newWorker:新加入的工作节点

  除了处理新加入的工作节点,从通知返回的最新全量活跃工作节点列表中和缓存比较还可以发现发生了故障的工作节点,因此这时还需要清理发生故障的工作节点,主要是将分配给故障节点的未完成任务重新分配给新的工作节点,综合上述分析,reassignAndSet 方法代码如下:

    void reassignAndSet(List children) {
        List toProcess;         // 故障工作节点列表
        // 活跃工作节点列表缓存为空,说明是第一次处理,全部都是新增活跃节点
        if (workersCache == null) {
            workersCache = new ChildrenCache(children);
        } else {
            LOG.info("Removing and setting");
            toProcess = workersCache.removeAndSet(children);
            LOG.info("Active Workers list: " + workersCache.getActiveWorkerCache());
            LOG.info("Dead Workers list: " + toProcess);
        }
        
        // 处理故障节点已分配但未完成任务
        if (toProcess != null) {
            for (String worker : toProcess) {
                getAbsentWorkerTasks(worker);
            }
        }
        
        LOG.info("New workers: " + workersCache.getNewWorker());
        // 为第一次加入的工作节点创建 '/assign/worker-id' 节点
        for (String worker : workersCache.getNewWorker()) {
            if (StringUtils.isNotEmpty(worker)) {
                createAssignWorkerNode(worker);
            }
        }
    }
    
    void createAssignWorkerNode(String worker) {
        zk.create("/assign/" + worker, 
                  new byte[0], 
                  Ids.OPEN_ACL_UNSAFE, 
                  CreateMode.PERSISTENT, 
                  assignWorkerCreateCallback, 
                  worker);
    }
    
    StringCallback assignWorkerCreateCallback = new StringCallback() {      
        @Override
        public void processResult(int rc, String path, Object ctx, String name) {
            switch (Code.get(rc)) {
            case CONNECTIONLOSS:
                createAssignWorkerNode((String) ctx);
                break;

            case NODEEXISTS:
                LOG.warn("'" + path + "' already exists.");
                break;
                
            case OK:
                LOG.info("Create '" + path + "' successfully!");
                break;
                
            default:
                LOG.error("Error when trying to create '" + path + "'.", KeeperException.create(Code.get(rc), path));
        }
        }
    };



(3)动态感知客户端提交任务

  当客户端提交任务时,会在 /tasks 节点下创建一个对应的子节点,为了满足避免节点名重复和任务信息不丢失的需求,这里简单使用持久顺序节点创建 /tasks/task-id,设定获取任务列表的方法为 getTasks(),在初始化回调中调用,如下:

    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);
                    if ("/workers".equals(path)) {
                        LOG.info("Init create '/workers' successfully, find workers now!");
                        getWorkers();
                    } else if ("/tasks".equals(path)) {
                        LOG.info("Init create '/tasks' successfully, find tasks now!");
                        getTasks();                     
                    }
                    break;
                    
                case NODEEXISTS:
                    LOG.warn("Parent already registered: " + path);
                    if ("/workers".equals(path)) {
                        LOG.info("Find exist znode '/workers' successfully, find workers now!");
                        getWorkers();
                    } else if ("/tasks".equals(path)) {
                        LOG.info("Find exist znode '/tasks' successfully, find tasks now!");
                        getTasks();
                    }
                    break;
                    
                default:
                    LOG.error("Something went wrong: ", KeeperException.create(Code.get(rc), path));
            }
        }
    };

  获取任务列表的同时设置监视器,如果收到通知就发起查询并重置监视器,回调中获取到最新未分配任务列表后,将其分配给活跃的工作节点执行,代码如下:

    void getTasks() {
        zk.getChildren("/tasks", 
                       tasksChangeWatcher, 
                       tasksGetChildrenCallback, 
                       null);
    }
    
    Watcher tasksChangeWatcher = new Watcher() {        
        @Override
        public void process(WatchedEvent event) {
            if (event.getType() == EventType.NodeChildrenChanged) {
                assert "/tasks".equals(event.getPath());
                getTasks();
            }
        }
    };
    
    ChildrenCallback tasksGetChildrenCallback = new ChildrenCallback() {        
        @Override
        public void processResult(int rc, String path, Object ctx, List children) {
            switch (Code.get(rc)) {
                case CONNECTIONLOSS:
                    getTasks();
                    break;
                    
                case OK:
                    LOG.info("Successfully got new tasks: " + children);
                    if (children != null) {
                        // 将新增任务分配给工作节点执行
                        assignTasks(children);
                    }
                    break;              
    
                default:
                    LOG.error("getChildren failed.", KeeperException.create(Code.get(rc), path));
            }
            
        }
    };

  分配任务的过程实际上是将任务节点转移到 /assign/worker-id 下的过程,所以首先需要获取任务节点数据,再随机选择一台活跃工作节点在其分配目录下创建子节点,当创建成功之后,需要删除对应的 /tasks/task-id,防止同一任务被多次分配到不同的工作节点重复执行,代码如下:

    void assignTasks(List tasks) {
        for (String task : tasks) {
            getTaskData(task);
        }
    }
    
    void getTaskData(String task) {
        zk.getData("/tasks/" + task, 
                   false, 
                   taskDataCallback, 
                   task);
    }
    
    DataCallback taskDataCallback = new DataCallback() {        
        @Override
        public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {
            switch (Code.get(rc)) {
                case CONNECTIONLOSS:
                    getTaskData((String) ctx);
                    break;
                    
                case OK:
                    // 如果任务提交进来的时候还没有活跃的工作节点,则1秒后重试
                    while (workersCache.size() == 0) {
                        LOG.info("There are currently no worker nodes available, try again after 1s.");
                        try {
                            Thread.sleep(1000);                         
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    // 随意挑选一个工作节点来执行任务
                    int worker = random.nextInt(workersCache.size());
                    String designatedWorker = workersCache.get(worker);
                    
                    // 生成分配节点路径
                    String assignmentPath = "/assign/" + designatedWorker + "/" + (String) ctx;
                    createAssignment(assignmentPath, data);
                    break;
                    
                default:
                    LOG.error("Error when trying to get task data.", KeeperException.create(Code.get(rc), path));               
            }
        }
    };

  创建 /assign/worker-id/task-id 和删除 /tasks/task-id 的代码如下:

    void createAssignment(String path, byte[] data) {
        zk.create(path, 
                  data, 
                  Ids.OPEN_ACL_UNSAFE, 
                  CreateMode.PERSISTENT, 
                  assignTaskCallback, 
                  data);    
    }
    
    StringCallback assignTaskCallback = new StringCallback() {      
        @Override
        public void processResult(int rc, String path, Object ctx, String name) {
            switch (Code.get(rc)) {
                case CONNECTIONLOSS:
                    createAssignment(path, (byte[]) ctx);
                    break;
                    
                // 当一个任务被成功分配时,应该将其从 '/tasks/task-id' 中删除
                case OK:
                    LOG.info("Create '" + path + "' successfully, Task assigned correctly: " + name);
                    deleteTask(name.substring(name.lastIndexOf("/") + 1));
                    break;              
                    
                case NODEEXISTS:
                    LOG.warn("Task already assigned: " + path);
                    break;
                    
                default:
                    LOG.error("Error when trying to create '" + path + "'.", KeeperException.create(Code.get(rc), path));                   
            }
        }
    };  
    
    void deleteTask(String task) {
        zk.delete("/tasks/" + task, -1, taskDeleteCallback, task);
    }
    
    VoidCallback taskDeleteCallback = new VoidCallback() {      
        @Override
        public void processResult(int rc, String path, Object ctx) {
            switch (Code.get(rc)) {
                case CONNECTIONLOSS:
                    deleteTask((String) ctx);
                    break;
                    
                case OK:
                    LOG.info("Delete task successfully: " + path);
                    break;
    
                default:
                    LOG.error("Error when trying to delete '" + path +"'.", KeeperException.create(Code.get(rc), path));
            }
            
        }
    };  

  前面 reassignAndSet 方法中清理故障节点重新分配任务的 getAbsentWorkerTasks 方法还没实现,跟分配任务给工作节点是相反的处理过程,具体处理的步骤是先获取故障节点下的任务列表,然后获取每个任务的数据,并为其在 /tasks 下创建对应的子节点,最后清理掉对应故障节点任务的znode /assign/worker-id/task-id,代码如下:

    void getAbsentWorkerTasks(String worker) {
        LOG.info("Absent worker: " + worker);
        // 获取故障节点下未执行任务列表
        zk.getChildren("/assign/" + worker, false, getAbsentTasksCallback, worker);
    }   
    
    ChildrenCallback getAbsentTasksCallback = new ChildrenCallback() {      
        @Override
        public void processResult(int rc, String path, Object ctx, List children) {
            switch (Code.get(rc)) {
                case CONNECTIONLOSS:
                    getAbsentWorkerTasks((String) ctx);
                    break;
                    
                case OK:
                    assignAbsentTasks(path, children);
                    break;
    
                default:
                    LOG.error("Error when trying to get absent task of " + (String) ctx);                   
            }
        }
    };
    
    void assignAbsentTasks(String path, List tasks) {
        for (String task : tasks) {
            assignAbsentTask(path, task);
        }
    } 
    
    // 重新分配任务,先获取数据
    void assignAbsentTask(String path, String task) {
        zk.getData(path + "/" + task, false, dataGetCallback, task);
    }
    
    DataCallback dataGetCallback = new DataCallback() {     
        @Override
        public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {
            switch (Code.get(rc)) {
                case CONNECTIONLOSS:
                    assignAbsentTask(path.substring(0, path.lastIndexOf("/") - 1), (String) ctx);
                    break;
    
                case OK:
                    LOG.info("Get data of '" + path + "' successfully, data: " + new String(data));
                    createTask((String) ctx, data);
                    deleteAssignTask(path);
                    break;
                    
                default:
                    LOG.error("Error when trying to get data of absent task: " + path);
            }           
        }
    };
    
    void createTask(String task, byte[] data) {
        zk.create("/tasks/" + task, data, Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT, taskGetCallback, data);
    }
    
    StringCallback taskGetCallback = new StringCallback() {     
        @Override
        public void processResult(int rc, String path, Object ctx, String name) {
            switch (Code.get(rc)) {
                case CONNECTIONLOSS:
                    createTask(path, (byte[]) ctx);
                    break;
    
                case NODEEXISTS:
                    LOG.warn("Node already exists: " + path);
                    break;
                    
                case OK:
                    LOG.info("Create task node successfully: " + path);
                    
                    break;
                    
                default:
                    LOG.error("Error when trying to create task node: " + path + "," + KeeperException.create(Code.get(rc), path));
            }
            
        }
    };
    
    void deleteAssignTask(String path) {
        zk.delete(path, -1, assignTaskDeleteCallback, path);
    }
    
    VoidCallback assignTaskDeleteCallback = new VoidCallback() {        
        @Override
        public void processResult(int rc, String path, Object ctx) {
            switch (Code.get(rc)) {
                case CONNECTIONLOSS:
                    deleteAssignTask(path);
                    break;
                    
                case NONODE:
                    LOG.warn("Node already delete: " + path);
                    break;
                    
                case OK:
                    LOG.info("Delete node successfully: " + path);
                    break;
    
                default:
                    LOG.error("Error when trying to delete node: " + path + "," + KeeperException.create(Code.get(rc), path));
            }
            
        }
    };


2、工作节点

  首先工作节点连接上 ZooKeeper 之后要在 /workers 下创建一个子节点,节点类型是临时节点,方便主节点对工作节点的故障检测,代码如下:

    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));
            }
            
        }
    };

  为了接收到主节点分配的任务,工作节点需要在注册成功后获取分配任务列表并注册 /assign/worker-id 的子节点监视点,有新任务分配给工作节点时,就会收到一条通知,由于回调处理是单线程的,为了避免通知触发的回调阻塞,任务交给线程池异步处理,代码如下:

    Executor executor = new ThreadPoolExecutor(5, 10, 30, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
    List onGoingTasks = new ArrayList();
    void getTasks() {
        zk.getChildren("/assign/worker-" + serverId, 
                       newTaskWatcher, 
                       tasksGetChildrenCallback, 
                       null);
    }
    
    Watcher newTaskWatcher = new Watcher() {        
        @Override
        public void process(WatchedEvent event) {
            if (event.getType() == EventType.NodeChildrenChanged) {
                assert new String("/assign/worker-" + serverId).equals(event.getPath());
                getTasks();
            }
        }
    };
    
    ChildrenCallback tasksGetChildrenCallback = new ChildrenCallback() {        
        @Override
        public void processResult(int rc, String path, Object ctx, List children) {
            switch (Code.get(rc)) {
                case CONNECTIONLOSS:
                    getTasks();
                    break;
                    
                case OK:
                    if (children != null && !children.isEmpty()) {
                        executor.execute(new Runnable() {                           
                            List children;
                            DataCallback cb;
                            
                            public Runnable init(List children, DataCallback cb) {
                                this.children = children;
                                this.cb = cb;
                                return this;
                            }
                            
                            @Override
                            public void run() {
                                LOG.info("Looping into tasks");
                                synchronized (onGoingTasks) {
                                    for (String task : children) {
                                        if (!onGoingTasks.contains(task)) {
                                            // 防止同一任务被重复处理
                                            onGoingTasks.add(task);
                                            LOG.trace("New task: {}", task);
                                            zk.getData("/assign/worker-" + serverId + "/" + task, 
                                                       false, 
                                                       cb, 
                                                       ctx);
                                        }
                                    }
                                }
                            }
                        }.init(children, taskDataCallback));
                    }
                    break;
                    
                default:
                    LOG.error("getChildren failed: " + KeeperException.create(Code.get(rc), path));
            }           
        }
    };

  获取任务节点数据,执行任务

    void executeTask(String task, Object ctx) {
        zk.getData("/assign/worker-" + serverId + "/" + task, 
                   false, 
                   taskDataCallback, 
                   ctx);
    }

    DataCallback taskDataCallback = new DataCallback() {        
        @Override
        public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {
            switch (Code.get(rc)) {
                case CONNECTIONLOSS:
                    LOG.error("connection loss");
                    executeTask(path.substring(path.lastIndexOf("/") + 1), ctx);
                    break;
            
                case OK:
                    LOG.info("execute task(" + path + ")'s command: " + new String(data));
                    // 执行任务成功之后要清理 '/assign/worker-id/task-id'
                    clearAssignTask(path);
                    // 保存任务执行结果
                    saveResult(path.substring(path.lastIndexOf("/") + 1), "success");
                    break;
    
                default:
                    LOG.error("Error when trying to get Data of '" + path + "', " + KeeperException.create(Code.get(rc), path));
            }           
        }
    };  

  执行完之后,删除 /assign/worker-id/task-id,并保存任务结果到 /status/task-id 中,代码如下:

    void clearAssignTask(String path) {
        zk.delete(path, -1, clearTaskCallback, path);
    }
    
    VoidCallback clearTaskCallback = new VoidCallback() {       
        @Override
        public void processResult(int rc, String path, Object ctx) {
            switch (Code.get(rc)) {
                case CONNECTIONLOSS:
                    clearAssignTask((String) ctx);
                    break;
                    
                case OK:
                    LOG.info("Delete '" + path + "' after executing.");
                    break;
    
                default:
                    LOG.error("Error when trying to clear task: '" + path + "', " + KeeperException.create(Code.get(rc), path));
            }
        }
    };

    void saveResult(String task, String result) {
        zk.create("/status/" + task, result.getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT, saveResultCallback, result);
    }
    
    StringCallback saveResultCallback = new StringCallback() {      
        @Override
        public void processResult(int rc, String path, Object ctx, String name) {
            switch (Code.get(rc)) {
                case CONNECTIONLOSS:
                    saveResult(path.replaceAll("/status/", ""), (String) ctx);
                    break;
                    
                case NODEEXISTS:
                    LOG.warn("Result node already: " + path);
                    break;
                    
                case OK:
                    LOG.info("Save result successfully: " + path);
                    break;
    
                default:
                    LOG.error("Something went wrong when saving result" + KeeperException.create(Code.get(rc), path));
            }
        }
    };


3、客户端

  首先提交任务,提交任务之后设置对 /status/task-id 的监视点,当任务被执行完保存结果在 /status/task-id 时会推送通知给客户端,客户端再从 /status/task-id 中获取任务执行结果,代码如下:

    void submitTask(String task, TaskObject taskCtx) {
        taskCtx.setTask(task);
        zk.create("/tasks/task-", 
                  task.getBytes(), 
                  Ids.OPEN_ACL_UNSAFE, 
                  CreateMode.PERSISTENT_SEQUENTIAL, 
                  createTaskCallback, 
                  taskCtx);
    }
    
    StringCallback createTaskCallback = new StringCallback() {      
        @Override
        public void processResult(int rc, String path, Object ctx, String name) {
            switch (Code.get(rc)) {
                case CONNECTIONLOSS:
                    submitTask(((TaskObject) ctx).getTask(), (TaskObject) ctx);
                    break;
                    
                case OK:
                    LOG.info("My created task name: " + name);
                    ((TaskObject) ctx).setTaskName(name);
                    watchStatus("/status/" + name.replaceAll("/tasks/", ""), ctx);
                    break;
    
                default:
                    LOG.error("Something went wrong" + KeeperException.create(Code.get(rc), path));
            }
        }
    };

  ctxMap 用来保存上下文,设置监视点代码如下:

    ConcurrentHashMap ctxMap = new ConcurrentHashMap();
    void watchStatus(String path, Object ctx) {
        ctxMap.put(path, ctx);
        zk.exists(path, statusWatcher, existsCallback, ctx);
    }
    
    Watcher statusWatcher = new Watcher() {     
        @Override
        public void process(WatchedEvent e) {
            System.out.println(e);
            if (e.getType() == EventType.NodeCreated) {
                assert e.getPath().contains("/status/task-");
                zk.getData(e.getPath(), 
                           false, 
                           getDataCallback, 
                           ctxMap.get(e.getPath()));
            }
        }
    };
    
    StatCallback existsCallback = new StatCallback() {      
        @Override
        public void processResult(int rc, String path, Object ctx, Stat stat) {
            switch (Code.get(rc)) {
                case CONNECTIONLOSS:
                    watchStatus(path, ctx);
                    break;
    
                case OK:
                    if (stat != null) {
                        //zk.getData(path, false, getDataCallback, null);
                        getTaskResult(path);
                    }
                    break;
                    
                case NONODE:
                    LOG.warn("The task has not been completed, so cann't see the result temporarily.");
                    break;
                    
                default:
                    LOG.error("Something went wrong when checking if the status node exists: " + KeeperException.create(Code.get(rc), path));
            }
            
        }
    };

  通知会触发回调,根据回调的 path 参数可以确定哪个任务被执行完了,进而获取任务执行结果,代码如下:

    void getTaskResult(String path) {
        zk.getData(path, false, getDataCallback, null);
    }
    
    DataCallback getDataCallback = new DataCallback() {     
        @Override
        public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {
            switch (Code.get(rc)) {
                case CONNECTIONLOSS:
                    getTaskResult(path);
                    break;
    
                case OK:
                    LOG.info("result: " + new String(data) + ", path: " + path);
                    break;
                    
                default:
                    LOG.error("Something went wrong when get data of path, path: " + path + KeeperException.create(Code.get(rc), path));
            }
            
        }
    };




  基于监视机制,通过上面的代码,我们构建了一个完善的主-从模型。

你可能感兴趣的:([ZooKeeper之七] 使用 Watcher 监视机制处理状态变化)