通过ZooKeeper通知客户端感兴趣的时间来避免轮询。
ZooKeeper提供了处理变化的重要机制-监视点(watch)。通过监视点,客户端可以对指定的znode节点注册一个通知请求,在发生变化时就会收到一个单次的通知。
视点和通知形成了一个通用机制,使客户端可以观察变化情况,而不用不断地轮询ZooKeeper。
当应用程序注册了一个监视点来接收通知,匹配该监视点条件的第一个事件会触发监视点的通知,并且最多只触发一次。
客户端设置的每个监视点与会话关联,如果会话过期,等待中的监视点将会被删除。不过监视点可以跨越不同服务端的连接而保持,例如,当一个ZooKeeper客户端与一个ZooKeeper服务端的连接断开后连接到集合中的另一个服务端,客户端会发送未触发的监视点列表,在注册监视点时, 服务端将要检查已监视的znode节点在之前注册监视点之后是否已经变化, 如果znode节点已经发生变化,一个监视点的事件就会被发送给客户端,否 则在新的服务端上注册监视点。
答案是肯定的。一个应用在接收到通知后,注册另一个监视点时,可能会丢失事件。因为可能在这之间znode节点又发生了变化。
实际上,将多个事件分摊到一个通知上具有积极的作用,比如,应用进行高频率的更新操作时,这种通知机制比每个事件都发送通知更加轻量化。举个例子,如果每个通知平均捕获两个事件,我们为每个事件只产生 了0.5个通知,而不是每个事件1个通知。
ZooKeeper的API中的所有读操作:getData、getChildren和exists,均可以选择在读取的znode节点上设置监视点。使用监视点机制,我们需要实现 Watcher接口类,实现其中的process方法:
public void process(WatchedEvent event);
WatchEvent数据结构包含以下信息:
其中前三个事件类型只涉及单个znode节点,第四个事件类型涉及监视 的znode节点的子节点。我们使用None表示无事件发生,而是ZooKeeper的 会话状态发生了变化。
监视点有两种类型:数据监视点和子节点监视点。创建、删除或设置一个znode节点的数据都会触发数据监视点,exists和getData这两个操作可以设置数据监视点。只有getChildren操作可以设置子节点监视点,这种监 视点只有在znode子节点创建或删除时才被触发。对于每种事件类型,我们 通过以下调用设置监视点:
当创建一个ZooKeeper对象(见第3章),我们需要传递一个默认的 Watcher对象,ZooKeeper客户端使用这个监视点来通知应用ZooKeeper状态的变化情况,如会话状态的变化。
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节点的子节点数。
对于监视点的一个重要问题是,一旦设置监视点就无法移除。要想移 除一个监视点,只有两个方法,一是触发这个监视点,二是使其会话被关 闭或过期。
我们进入主-从模式例子的章节之前,先看一些ZooKeeper的应用中使用的通用代码的模型:
zk.exists("/myZnode", //1
myWatcher,existsCallback,null);
Watcher myWatcher = new Watcher()
{//2
public void process(WatchedEvent e)
{
// Process the watch event
}
}
StatCallback existsCallback = new StatCallback()
{ //3
public void processResult(int rc, String path, Object ctx, Stat stat)
{
// Process the result of the exists call
}
};
现在,我们通过主-从模式的例子来看看如何处理状态的变化。以下为一个任务列表,一个组件需要等待处理的变化情况:
应用客户端通过创建/master节点来推选自己为主节点(我们称为“主节点竞选”),如果znode节点已经存在,应用客户端确认自己不是主要主节点并返回。然而,这种实现方式无法容忍主要主节点的崩溃。如果主要主节点崩溃,备份主节点并不知道,因此我们需要在/master上设置监视点,在节点删除时(无论是显式关闭还是因为主要主节点的会话过期),ZooKeeper会通知客户端。
StringCallback masterCreateCallback = new StringCallback()
{
public void processResult(int rc, String path, Object ctx, String name)
{
switch (Code.get(rc))
{
case CONNECTIONLOSS:
checkMaster(); //1
break;
case OK:
state = MasterStates.ELECTED;
takeLeadership(); //2
break;
case NODEEXISTS:
state = MasterStates.NOTELECTED;
masterExists(); //3
break;
default:
state = MasterStates.NOTELECTED;
LOG.error("Something went wrong when running for master.", //4
KeeperException.create(Code.get(rc), path));
}
}
};
void masterExists()
{
zk.exists("/master",//5
masterExistsWatcher,masterExistsCallback,null);
}
Watcher masterExistsWatcher = new Watcher()
{
public void process(WatchedEvent e)
{
if(e.getType() == EventType.NodeDeleted)
{
assert "/master".equals( e.getPath() );
runForMaster(); //6
}
}
};
StatCallback masterExistsCallback = new StatCallback() {
public void processResult(int rc, String path, Object ctx, Stat stat)
{
switch (Code.get(rc))
{
case CONNECTIONLOSS:
masterExists(); //1
break;
case OK:
if(stat == null)
{
state = MasterStates.RUNNING;
runForMaster(); //2
}
break;
default:
checkMaster(); //3
break;
}
}
};
对/master节点执行exists操作,返回结果也许是该znode节点已经被删除,这时因为无法保证监视点的设置是在znode节点删除前,所以客户端需要再次竞选/master节点。如果再次尝试成为主节点失败,那么客户端就知道有其他客户端成功了,之后该客户端就需要再次为/master节点添加监视点。如果收到的是/master节点创建的通知,而不是删除通知,客户端就不 再竞选/master节点,同时,对应的exists操作(设置监视点的操作)会返 回/master节点不存在,然后触发exists回调方法,进行/master节点竞选操 作。
图4-1直观地展示了这些交错的操作。如果竞选主节点成功(图中 a),create操作执行完成,应用客户端不需要做其他事情。如果create操作 失败,则意味着该节点已经存在,客户端就会执行exists操作来设置/master 节点的监视点(图中b)。在竞选主节点和执行exists操作之间,也 许/master节点已经删除了,这时,如果exists调用返回该节点依然存在,客 户端只需要等待通知的到来,否则就需要再次尝试创建/master进行竞选主 节点操作。如果创建/master节点成功,监视点就会被触发,表示znode节点 发生了变化(图中c),不过,这个通知没有什么意义,因为这是客户端自 己引起的变化。如果再次执行create操作失败,我们就会通过执行exists设 置监视点来重新执行这一流程(图中d)。
系统中任何时候都可能发生新的从节点加入进来,或旧的从节点退役 的情况,从节点执行分配给它的任务前也许会崩溃。为了确认某个时间点 可用的从节点信息,我们通过在ZooKeeper中的/workers下添加子节点来注 册新的从节点。当一个从节点崩溃或从系统中被移除,如会话过期等情 况,需要自动将对应的znode节点删除。优雅实现的从节点会显式地关闭其 会话,而不需要ZooKeeper等待会话过期。
Watcher workersChangeWatcher = new Watcher()
{ //1
public void process(WatchedEvent e)
{
if(e.getType() == EventType.NodeChildrenChanged)
{
assert "/workers".equals( e.getPath() ); getWorkers();
}
}
};
void getWorkers()
{
zk.getChildren("/workers", workersChangeWatcher,
workersGetChildrenCallback,
null);
}
ChildrenCallback workersGetChildrenCallback = new ChildrenCallback()
{
public void processResult(int rc, String path, Object ctx,List<String> children)
{
switch (Code.get(rc))
{
case CONNECTIONLOSS:
getWorkerList(); //2
break;
case OK:
LOG.info("Succesfully got a list of workers: " + children.size()+ " workers");
reassignAndSet(children); //3
break;
default:
LOG.error("getChildren failed",KeeperException.create(Code.get(rc), path));
}
}
};
我们从getWorkerList方法开始执行,通过异步方式执行getChildren方法,传入workersGetChildrenCallback参数用于处理操作结果。如果客户端 失去与服务端的连接(CONNECTIONLOSS事件),监视点不会被添加, 我们也不会得到从节点的列表,我们再次执行getWorkerList来设置监视点 并获取从节点列表,如果执行getChildren成功,我们就会调用 reassignAndSet方法,该方法的代码如下:
ChildrenCache workersCache; //1
void reassignAndSet(List<String> children)
{
List<String> toProcess;
if(workersCache == null)
{
workersCache = new ChildrenCache(children); //2
toProcess = null; //3
}
else
{
LOG.info( "Removing and setting" );
toProcess = workersCache.removedAndSet( children ); //4
}
if(toProcess != null)
{
for(String worker : toProcess)
{
getAbsentWorkerTasks(worker); //5
}
}
}