在应用程序中,很多时候需要知道ZooKeeper集合的状态。例如,备份主节点需要知道主要主节点已经崩溃,从节点需要知道任务分配给了自己,甚至ZooKeeper的客户端会定时轮询ZooKeeper集合,检查系统状态是否发生了变化。然而轮询方式并非高效的方式,尤其是在期望的变化发生频率很低时。
例如,在主要主节点崩溃时,备份主节点需要知道这一情况, 以便它们可以进行故障处理。为了减少主节点崩溃后的恢复时间,我们需要频繁轮询,如每50毫秒,那每秒就产生20次查询。如果结点很多,这些消息开销就很大,假如将监听时间拉长,就可能导致处理故障不及时。
为此,可以通过ZooKeeper通知客户端感兴趣的具体事件来避免轮询的调优和轮询流量。ZooKeeper提供了处理变化的重要机制——监视点(watch)。客户端可以对指定的znode节点注册一个通知请求,在发生变化时就会收到一个单次的通知。例如,我们的主节点创建了一个临时性的znode节点来标识主节点锁,而备份节点注册一个监视点来监视这个主节点锁是否存在,如果主节点崩溃,主节点锁自动被删除,并通知所有备份主节点。一旦备份主节点收到通知, 它们就可以开始进行主节点选举,例如通过尝试创建一个临时的znode节点来标识主节点锁。
监视点和通知形成了一个通用机制,使客户端可以观察变化情况, 而不用不断地轮询ZooKeeper,该机制还适用于很多情况。
先明确一个概念,我们所说的事件 (event)表示一个znode节点执行了更新操作,也就是对结点进行了增删改。而一个监视点(watch) 表示一个znode节点和与之关联的事件类型组成的,也称为单次触发器。当一个监视点被一个事件触发时,就会产生一个通知(notification)。通知也就是应用客户端收到的事件报告的消息。
客户端设置的每个监视点与会话关联,如果会话过期,等待中的监视点将会被删除。
单次触发是否会丢失事件呢?会的,不过丢失事件通常并不是问题,因为任何在接收通知与注册新监视点之间的变化情况,均可以通过重新读取ZooKeeper的状态信息来获得。假设一个从节点接收到一个新任务分配给它的通知。为了接收新任 务,从节点读取任务列表,如果在通知接收后,又给这个从节点分配了 更多的任务,在通过getChildren调用获取任务列表时会返回所有的任务。同时调用getChildren时也可以设置新的监视点,从而保证从节点不 会丢失任务。
1 如何设置监视点
ZooKeeper的API中的所有读操作:getData、getChildren和exists, 均可以选择在读取的znode节点上设置监视点。使用监视点机制,我们 需要实现Watcher接口类,实现其中的process方法,这个方法在我们前面的演示中都有,不过只是打印了一下:
public void process(WatchedEvent event);
WatchedEvent数据结构包括以下信息:
-
ZooKeeper会话状态(KeeperState):Disconnected、 SyncConnected、AuthFailed、ConnectedReadOnly、SaslAuthenticated和 Expired。
-
事件类型(EventType):NodeCreated、NodeDeleted、 NodeDataChanged、NodeChildrenChanged和None。
-
如果事件类型不是None时,返回一个znode路径。
其中前三个事件类型只涉及单个znode节点,第四个事件类型涉及 监视的znode节点的子节点。我们使用None表示无事件发生,而是 ZooKeeper的会话状态发生了变化。
监视点有两种类型:数据监视点和子节点监视点。创建、删除或设置一个znode节点的数据都会触发数据监视点,exists和getData这两个操 作可以设置数据监视点。只有getChildren操作可以设置子节点监视点, 这种监视点只有在znode子节点创建或删除时才被触发。对于每种事件 类型,我们通过以下调用设置监视点:
NodeCreated:通过exists调用设置一个监视点。
NodeDeleted :通过exists或getData调用设置监视点。
NodeDataChanged :通过exists或getData调用设置监视点。
NodeChildrenChanged :通过getChildren调用设置监视点。
当创建一个ZooKeeper对象(见第3章),我们需要传递一个默认的 Watcher对象,ZooKeeper客户端使用这个监视点来通知应用ZooKeeper 状态的变化情况,如会话状态的变化。对于ZooKeeper节点的事件的通 知,你可以使用默认的监视点,也可以单独实现一个。例如,getData调 用有两种方式设置监视点:
public byte[] getData(final String path, Watcher watcher, Stat stat);
public byte[] getData(String path, boolean watch, Stat stat);
两个方法第一个参数均为znode节点,第一个方法传递一个新的 Watcher对象(我们已经创建完毕),第二个方法则告诉客户端使用默认的监视点,我们只需要在调用时将第二个参数传递true。
stat入参为Stat类型的实例化对象,ZooKeeper使用该对象返回指定 的path参数的znode节点信息。Stat结构包括znode节点的属性信息,如该 znode节点的上次更新(zxid)的时间戳,以及该znode节点的子节点 数。
对于监视点的一个重要问题是,一旦设置监视点就无法移除。要想移除一个监视点,只有两个方法,一是触发这个监视点,二是使其会话 被关闭或过期。在未来版本中可能会改变这个特性,这是因为开发社区致力于在版本3.5.0中提供该功能。
ZooKeeper监控状态的通用模型是:
1.进行调用异步。
2.实现回调对象,并传入异步调用函数中。
3.如果操作需要设置监视点,实现一个Watcher对象,并传入异步调用函数中。
以下为exists的异步调用的示例代码:
zk.exists("/myZnode", // 异步方式调用exists
myWatcher,
existsCallback,
null);
Watcher myWatcher = new Watcher() {//2 Watcher的实现
public void process(WatchedEvent e) {
// Process the watch event
} }
StatCallback existsCallback = new StatCallback() {//3 exists的回调对象
public void processResult(int rc, String path, Object ctx, Stat stat) {
// Process the result of the exists call
} };
2 主从模式的例子
现在,我们通过主-从模式的例子详细研究如何处理状态的变化。通常,一个组件需要等待处理的情况至少包含以下几个:管理权变化、主节点等待从节点列表的变化、主节点等待新任务进行分配、从节点等待分配新任务和客户端等待任务的执行结果。
该例子来自《ZooKeeper-分布式过程协同技术详解》一书,案例设计的是master充当主结点,worker模拟要执行的任务,client用于提交任务。通过整个例子,我们会感觉到虽然代码整体上是上一文的组合,但是基于原生zookeeper开发还是非常复杂,要自己实现是非常困难的。我们只以”管理权变化“为例,看一下核心的代码如何做的。
应用客户端通过创建/master节点来推选自 己为主节点(我们称为“主节点竞选”),如果znode节点已经存在,应用客户端确认自己不是主要主节点并返回,然而,如果主要主节点崩溃,备份主节点并不知道,因此我们需要在/master上设置监视点,在节点删除时(无论是显式关闭还是因为主要主节点的会话过期),ZooKeeper会通知客户端。
为了设置监视点,我们新建一个新的监视点对象,命名为 masterExistsWatcher,并传入exists方法中。一旦/master删除就会发出通 知,就会调用在masterExistsWatcher定义的process函数,并调用 runForMaster方法。
StringCallback masterCreateCallback = new StringCallback() {
public void processResult(int rc, String path, Object ctx, String name) {
switch (Code.get(rc)) {
case CONNECTIONLOSS:
//在连接丢失事件发生的情况下,客户端检查/master节点是否存 在,因为客户端并不知道是否能够创建这个节点。
checkMaster();
break;
case OK:
state = MasterStates.ELECTED;
//如果返回OK,那么开始行使领导权
takeLeadership();
break;
case NODEEXISTS:
state = MasterStates.NOTELECTED;
//如果其他进程已经创建了这个znode节点,客户端需要监视该节点。
masterExists();
break;
default:
state = MasterStates.NOTELECTED;
LOG.error("Something went wrong when running for master.",
KeeperException.create(Code.get(rc), path));
}
LOG.info("I'm " + (state == MasterStates.ELECTED ? "" : "not ") + "the leader " + serverId);
}
};
void masterExists() {
zk.exists("/master", //通过exists调用在/master节点上设置了监视点
masterExistsWatcher,
masterExistsCallback,
null);
}
Watcher masterExistsWatcher = new Watcher(){
public void process(WatchedEvent e) {
if(e.getType() == EventType.NodeDeleted) {
assert "/master".equals( e.getPath() );
//如果/master节点删除了,那么再次竞选主节点。
runForMaster();
}
}
};
下面继续采用我们在前面所讨论的异步方式,我们同样需要为 exists调用创建一个回调函数,以便在回调函数中关注某些情况。首先,在发生连接丢失的事件时,因为需要在/master节点上设置监视点, 所以需要再次调用exists操作;其次,在create的回调方法执行和exists操 作执行之间发生了/master节点被删除的情况,因此在exists返回操作成功后(返回OK),我们需要检查返回的stat对象是否为空,因为当节点不存在时,stat为null;最后,如果返回的结果不是OK或 CONNECTIONLOSS,我们通过获取节点数据来检查/master节点。加入客户端的会话过期,在这种情况下,获得/master数据的回调方法会记录 一个错误信息并退出。以下为我们的exists回调方法的代码:
StatCallback masterExistsCallback = new StatCallback() {
public void processResult(int rc, String path, Object ctx, Stat stat){
switch (Code.get(rc)) {
case CONNECTIONLOSS:
//连接丢失的情况下重试。
masterExists();
break;
case OK:
state = MasterStates.RUNNING;
runForMaster();
case NONODE:
state = MasterStates.RUNNING;
runForMaster();
LOG.info("It sounds like the previous master is gone, " +
"so let's run for master again.");
break;
default:
checkMaster();
break;
}
}
};
其他代码请参考原书,为了简化zk开发的难度,我们一般使用Curator来进行,Curator对zk进行了大量的二次封装,开发更为方便。
想看我的更多新文章,就关注我吧。