设置ZooKeeper的CLASSPATH
我们需要设置正确的classpath,以便运行或编译ZooKeeper的Java代码。除了ZooKeeper的JAR包外,ZooKeeper使用了大量的第三方库。为了简化输入和方便阅读,我们使用环境变量CLASSPATH来表示所有必需的库。
ZooKeeper发行包中bin目录下的zkEnv.sh脚本会为我们设置该环境变量。我们需要使用以下方式来编码:
(在Windows上,使用call命令调用,而不是使用zkEnv.cmd脚本。)
一旦运行这个脚本,环境变量CLASSPATH就会正确设置。我们在编译和运行Java程序时用到它。
建立ZooKeeper会话
ZooKeeper的API围绕ZooKeeper的句柄(handle)而构建,每个API调用都需要传递这个句柄。这个句柄代表与ZooKeeper之间的一个会话。
创建ZooKeeper句柄的构造函数如下所示:
- connectString
包含主机名和ZooKeeper服务器的端口。我们之前通过zkCli连接ZooKeeper服务时,已经列出过这些服务器。 - sessionTimeout
以毫秒为单位,表示ZooKeeper等待客户端通信的最长时间,之后会声明会话已死亡。 - watcher
用于接收会话事件的一个对象,这个对象需要我们自己创建。
Wacher被定义为接口,所以我们需要自己实现一个类,然后初始化这个类的实例并传入ZooKeeper的构造函数中。客户端使用Watcher接口来监控与ZooKeeper之间会话的健康情况。与ZooKeeper服务器之间建立或失去连接时就会产生事件。它们同样还能用于监控ZooKeeper数据的变化。最终,如果与ZooKeeper的会话过期,也会通过Watcher接口传递事件来通知客户端的应用。
实现一个Watcher
为了从ZooKeeper接收通知,我们需要实现监视点。首先让我们进一步了解Watcher接口,该接口的定义如下
这个接口没有多少内容,我们不得不自己实现,但现在我们只是简单地输出事件。所以,让我们从一个名为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;①
}
void startZK() {
zk = new ZooKeeper(hostPort, 15000, this);
}
public void process(WatchedEvent e) {
System.out.println(e);②
}
public static void main(String args[])
throws Exception {
Master m = new Master(args[0]);
m.startZK();
// wait for a bit
Thread.sleep(60000);
}
}
①在构造函数中,我们并未实例化ZooKeeper对象,而是先保存hostPort留着后面使用。Java最佳实践告诉我们,一个对象的构造函数没有完成前不要调用这个对象的其他方法。因为这个对象实现了Watcher,并且当我们实例化ZooKeeper对象时,其Watcher的回调函数就会被调用,所以我们需要Master的构造函数返回后再调用ZooKeeper的构造函数。
②这个简单的示例没有提供复杂的事件处理逻辑,而只是将我们收到的事件进行简单的输出。
通过以下方式就可以编译这个简单的例子:
当我们编译完Master.java这个文件后,运行它并查看结果:
ZooKeeper客户端API产生很多日志消息,使用户可以了解发生了什么。
ZooKeeper管理连接
请不要自己试着去管理ZooKeeper客户端连接。ZooKeeper客户端库会监控与服务之间的连接,客户端库不仅告诉我们连接发生问题,还会主动尝试重新建立通信。一般客户端开发库会很快重建会话,以便最小化应用的影响。所以不要关闭会话后再启动一个新的会话,这样会增加系统负载,并导致更长时间的中断。
获取管理权
为了确保同一时间只有一个主节点进程出于活动状态,我们使用ZooKeeper来实现简单的群首选举算法。
ZooKeeper通过插件式的认证方法提供了每个节点的ACL策略功能,因此,如果我们需要,就可以限制某个用户对某个znode节点的哪些权限。
当然,我们希望在主节点死掉后/master节点会消失,我们可以使用ZooKeeper
的临时性znode节点来达到我们的目的。
因此,我们将会在我们的程序中添加以下代码:
String serverId = Integer.toHexString(random.nextInt());
void runForMaster() {
zk.create("/master",①
serverId.getBytes(),②
OPEN_ACL_UNSAFE,③
CreateMode.EPHEMERAL);④
}
①我们试着创建znode节点/master。如果这个znode节点存在,create就会失败。同时我们想在/master节点的数据字段保存对应这个服务器的唯一ID。
②数据字段只能存储字节数组类型的数据,所以我们将int型转换为一个字节数组。
③如之前所提到的,我们使用开放的ACL策略。
④我们创建的节点类型为EPHEMERAL(临时)。
然而,我们这样做还不够,create方法会抛出两种异常:
KeeperException和InterruptedException。我们需要确保我们处理了这两种异常,特别是ConnectionLossException(KeeperException异常的子类)和InterruptedException。对于其他异常,我们可以忽略并继续执行,但对于这两种异常,create方法可能已经成功了,所以如果我们作为主节点就需要捕获并处理它们。
当处理ConnectionLossException异常时,我们需要找出那个进程创建的/master节点,如果进程是自己,就开始成为群首角色。我们通过getData方法来处理:
- path
类似其他ZooKeeper方法一样,第一个参数为我们想要获取数据的znode节点路径。 - watch
表示我们是否想要监听后续的数据变更。如果设置为true,我们就可以通过我们创建ZooKeeper句柄时所设置的Watcher对象得到事件,同时另一个版本的方法提供了以Watcher对象为入参,通过这个传入的对象来接收变更的事件。
现在我们设置这个参数为false,因为我们现在我们只想知道当前的数据是什么。 - stat
最后一个参数类型Stat结构,getData方法会填充znode节点的元数据信息。
让我们按以下代码段来修改代码,在runForMaster方法中引入异常处理:
String serverId = Integer.toString(Random.nextLong());
boolean isLeader = false;
// returns true if there is a master
boolean checkMaster() {
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) {
// no master, so try create again
return false;
} catch (ConnectionLossException e) {
}
}
}
void runForMaster() throws InterruptedException {③
while (true) {
try {④
zk.create("/master", serverId.getBytes(),
OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);⑤
isLeader = true;
break;
} catch (NodeExistsException e) {
isLeader = false;
break;
} catch (ConnectionLossException e) {⑥
}
if (checkMaster()) break;⑦
}
}
①通过获取/master节点的数据来检查活动主节点。
②该行展示了为什么我们需要使用在创建/master节点时保存的数据:如果/master存在,我们使用/master中的数据来确定谁是群首。如果一个进程捕获到ConnectionLossException,这个进程可能就是主节点,因create操作实际上已经处理完,但响应消息却丢失了。
③我们将InterruptedException异常简单地传递给调用者。
④我们将zk.create方法包在try块之中,以便我们捕获并处理ConnectionLossException异常。
⑤这里为create请求,如果成功执行将会成为主节点。
⑥处理ConnectionLossException异常的catch块的代码为空,因为我们并不想中止函数,这样就可以使处理过程继续向下执行。
⑦检查活动主节点是否存在,如果不存在就重试。
现在,我们看一下Master的main主函数:
public static void main(String args[]) throws Exception {
Master m = new Master(args[0]);
m.startZK();
m.runForMaster();①
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");
}
m.stopZK();
}
其他暂略。