文章目录
第三章、使用ZooKeeper进行开发
ZooKeeper提供了Java语言和C语言的API套件,这两个套件拥有相同的基础结构和特性。
一、开始使用ZooKeeper的API
首先介绍 一下如何使用ZooKeeper的API进行开发,展示如何创建会话,实现监视点 (watcher)。我们还是从主-从模式例子开始进行编码。
1、设置ZooKeeper的CLASSPATH
我们需要设置正确的classpath,以便运行或编译ZooKeeper的Java代码。
ZooKeeper发行包中bin目录下的zkEnv.sh脚本会为我们设置该环境变 量。我们需要使用以下方式来编码:
(在Windows上,使用call命令调用,而不是使用zkEnv.cmd脚本。)
2、建立ZooKeeper会话(通过句柄handle)
ZooKeeper的API围绕ZooKeeper的句柄(handle)而构建,每个API调用都需要传递这个句柄。这个句柄代表与ZooKeeper之间的一个会话。
句柄(handle)指的是使用唯一的整数值即一个四字节长的数值来标识应用程序中的不同对象和同类中的不同的实例。但是句柄(handle)不是指针,程序不能通过句柄来直接阅读文件中的信息。
在图 3-1中,与ZooKeeper服务器已经建立的一个会话如果断开,这个会话就会 迁移到另一台ZooKeeper服务器上。只要会话还存活着,这个句柄就仍然有效,ZooKeeper客户端库会持续保持这个活跃连接,以保证与ZooKeeper服务器之间的会话存活。如果句柄关闭,ZooKeeper客户端库会告知 ZooKeeper服务器终止这个会话。如果ZooKeeper发现客户端已经死掉,就 会使这个会话无效。如果客户端之后尝试重新连接到ZooKeeper服务器,使 用之前无效会话对应的那个句柄进行连接,那么ZooKeeper服务器会通知客 户端库,这个会话已失效,使用这个句柄进行的任何操作都会返回错误。
创建ZooKeeper句柄的构造函数如下:
其中:
- connectString
包含主机名和ZooKeeper服务器的端口。 - sessionTimeout
以毫秒为单位,表示ZooKeeper等待客户端通信的最长时间,之后会声明会话已死亡。目前我们使用15000,即15秒。需 要注意,这个值比较高,但对于我们后续的实验会非常有用。ZooKeeper会话一般设置超时时间为5~10秒。 - watcher
用于接收会话事件的一个对象,这个对象需要我们自己创建。因为 Wacher定义为接口,所以我们需要自己实现一个类,然后初始化这个类的 实例并传入ZooKeeper的构造函数中。客户端使用Watcher接口来监控与 ZooKeeper之间会话的健康情况。与ZooKeeper服务器之间建立或失去连接时就会产生事件。它们同样还能用于监控ZooKeeper数据的变化。最终,如 果与ZooKeeper的会话过期,也会通过Watcher接又传递事件来通知客户端 的应用。
1、实现一个Watcher
为了从ZooKeeper接收通知,我们需要实现监视点。首先让我们进一步
了解Watcher接口,该接口的定义如下:
public interface Watcher
{
void process(WatchedEvent event);
}
下面是一个名称为Master的类开始实现示例:
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.Watcher;
import java.io.IOException;
public class Master implements Watcher
{
ZooKeeper zk;
String hostPort;
Master(String hostPort)
{
this.hostPort = hostPort; //1
}
void startZK() throws IOException {
zk = new ZooKeeper(hostPort, 15000, this); //2
}
@Override
public void process(WatchedEvent e)
{
System.out.println(e); //3
}
public static void main(String args[]) throws Exception
{
Master m = new Master("127.0.0.1:2181");
m.startZK();
// wait for a bit
Thread.sleep(60000); //4
}
}
- 1、在构造函数中,我们并未实例化ZooKeeper对象,而是先保存 hostPort留着后面使用。Java最佳实践告诉我们,一个对象的构造函数没有完成前不要调用这个对象的其他方法。因为这个对象实现了Watcher,并 且当我们实例化ZooKeeper对象时,其Watcher的回调函数就会被调用,所 以我们需要Master的构造函数返回后再调用ZooKeeper的构造函数。
- 2、使用Master对象来构造ZooKeeper对象,以便添加Watcher的回调函数。
- 3、这个简单的示例没有提供复杂的事件处理逻辑,而只是将我们收到 的事件进行简单的输出。
- 4、我们连接到ZooKeeper后,后台就会有一个线程来维护这个 ZooKeeper会话。该线程为守护线程,也就是说线程即使处于活跃状态, 程序也可以退出。因此我们在程序退出前休眠一段时间,以便我们可以看 到事件的发生。
编译上面代码
javac -cp $CLASSPATH Master.java
编译完Master.java这个文件之后,运行并查看结果:
$ java -cp $CLASSPATH Master 127.0.0.1:2181
... - INFO [...] - Client environment:zookeeper.version=3.4.5-1392090, ...//1 ...
... - INFO [...] - Initiating client connection,
connectString=127.0.0.1:2181 ...//2
... - INFO [...] - Opening socket connection to server localhost/127.0.0.1:2181. ...
... - INFO [...] - Socket connection established to localhost/127.0.0.1:2181, initiating session
... - INFO [...] - Session establishment complete on server localhost/127.0.0.1:2181, ...//3
WatchedEvent state:SyncConnected type:None path:null //4
ZooKeeper客户端API产生很多日志消息,使用户可以了解发生了什 么。日志非常详细。
- 1、前面几行日志消息描述了ZooKeeper客户端的实现和环境。
- 2、当客户端初始化一个到ZooKeeper服务器的连接时,无论是最初的 连接还是随后的重连接,都会产生这些日志消息。
- 3、这个消息展示了连接建立之后,该连接的信息,其中包括客户端所 连接的主机和端口信息,以及这个会话与服务器协商的超时时间。如果服 务器发现请求的会话超时时间太短或太长,服务器会调整会话超时时间。
- 4、最后这行并不是ZooKeeper库所输出,而是我们实现的 Watcher.process(WatchedEvent e)函数中输出的WatchEvent对象。
这个例子中,假设设运行时所有必需的库均在lib子目录下,同时假设 log4j.conf文件在conf子目录中。你可以在你所使用的ZooKeeper发行包中找 到这两个目录。如果你看到以下信息:
log4j:WARN No appenders could be found for logger (org.apache.zookeeper.ZooKeeper).
log4j:WARN Please initialize the log4j system properly.
表示你还没有将log4j.conf放到classpath下。
2、运行Watcher的示例
我们启动服务器,然后运行Master,之后停止服务器并保持 Master继续运行。你会看到在SyncConnected事件之后发生了Disconnected事 件。
当开发者看到Disconnected事件时,有些人认为需要创建一个新的 ZooKeeper句柄来重新连接服务。不要这么做!当你启动服务器,然后启动 Master,再重启服务器时看一下发生了什么。你看到SyncConnected事件之 后为Disconnected事件,然后又是一个SyncConnected事件。ZooKeeper客户 端库负责为你重新连接服务。当不幸遇到网络中断或服务器故障时, ZooKeeper可以处理这些故障问题。
客户端就像除了休眠外什么都没做一样,而我们通过发生的事件可以看到后台到底发生了什么。我们还可以看看ZooKeeper服务端都发生了什么。ZooKeeper有两种管理接口:JMX和四字母组成的命令。第10章会深入讨论这些接口,现在我们通过stat和dump这两个四字母命令来看看服务器 上发生了什么。
要使用这些命令,需要先通过telnet连接到客户端端又2181,然后输入 这些命令(在命令后输入Enter键)。例如,如果启动Master这个程序后, 使用stat命令,我们会看到以下输出信息:
$ telnet 127.0.0.1 2181
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
stat
ZooKeeper version: 3.4.5-1392090, built on 09/30/2012 17:52 GMT
Clients:
/127.0.0.1:39470[1](queued=0,recved=3,sent=3)
/127.0.0.1:39471[0](queued=0,recved=1,sent=0)
Latency min/avg/max: 0/5/48
Received: 34
Sent: 33
Connections: 2
Outstanding: 0
Zxid: 0x17
Mode: standalone
Node count: 4
Connection closed by foreign host.
我们从输出信息看到有两个客户端连接到ZooKeeper服务器。一个是 Master程序,另一个为Telnet连接。
如果我们启动Master程序后,使用dump命令,我们会看到以下输出信 息:
$ telnet 127.0.0.1 2181
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
dump
SessionTracker dump:
Session Sets (3):
0 expire at Wed Nov 28 20:34:00 PST 2012:
0 expire at Wed Nov 28 20:34:02 PST 2012:
1 expire at Wed Nov 28 20:34:04 PST 2012:
0x13b4a4d22070006
ephemeral nodes dump:
Sessions with Ephemerals (0):
Connection closed by foreign host.
我们从输出信息中看到有一个活动的会话,这个会话属于Master程 序。我们还能看到这个会话还有多长时间会过期。会话超时的过期时间取 决于我们创建ZooKeeper对象时所指定的值。
让我结束Master程序,再次使用dump命令来看一下活动的会话信息。
你会注意到会话过一段时间后才消失。这是因为直到会话超时时间过了以 后,服务器才会结束这个会话。当然,客户端会不断延续与ZooKeeper服务 器的活动连接的到期时间。
当Master结束时,最好的方式是使会话立即消失。这可以通过 ZooKeeper.close()方法来结束。一旦调用close方法后,ZooKeeper对象实 例所表示的会话就会被销毁。
void stopZK() throws Exception
{
zk.close();
}
public static void main(String args[]) throws Exception
{
Master m = new Master(args[0]);
m.startZK();
// wait for a bit
Thread.sleep(60000);
m.stopZK();
}
3、获取管理权
为了确保同一时间只有一个主节点进程出于活动状态,我们使用 ZooKeeper来实现简单的群首选举算法(在2.4.1节中所描述的)。这个算法中,所有潜在的主节点进程尝试创建/master节点,但只有一个成功,这个成功的进程成为主节点。
ACL(Access Control List)访问控制列表
常量ZooDefs.Ids.OPEN_ACL_UNSAFE为所有人提供了所有权限(正如其名所显示的,这个ACL策略在不可信的环境下使用是非常不安全的)。
ZooKeeper通过插件式的认证方法提供了每个节点的ACL策略功能,我们继续使用OPEN_ACL_UNSAFE策略。当然,我们希望在主节点死掉后/master节点会消失。正如我们在2.1.2节中所提到的持久性和临时性znode节点,我们可以使用ZooKeeper的临时性znode节点来达到我们的目的。我们将定义一个EPHEMERAL的znode节点,当创建它的 会话关闭或无效时,ZooKeeper会自动检测到,并删除这个节点。
需要在程序中添加以下代码
String serverId = Integer.toHexString(random.nextInt());
void runForMaster()
{
zk.create("/master", serverId.getBytes(),OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL);4
}
- 1、我们试着创建znode节点/master。如果这个znode节点存在,create 就会失败。同时我们想在/master节点的数据字段保存对应这个服务器的唯 一ID。
- 2、数据字段只能存储字节数组类型的数据,所以我们将int型转换为一 个字节数组。
- 3、如之前所提到的,我们使用开放的ACL策略。
- 4、我们创建的节点类型为EPHEMERAL。
create方法会抛出两种异常: KeeperException和InterruptedException。我们需要确保我们处理了这两种异常,特别是ConnectionLossException(KeeperException异常的子类)和 InterruptedException。对于其他异常,我们可以忽略并继续执行。
ConnectionLossException异常发生于客户端与ZooKeeper服务端失去连接时。一般常常由于网络原因导致,如网络分区或ZooKeeper服务器故障。当这个异常发生时,客户端并不知道是在ZooKeeper服务器处理前丢失了请 求消息,还是在处理后客户端未收到响应消息。如我们之前所描述的, ZooKeeper的客户端库将会为后续请求重新建立连接,但进程必须知道一个 未决请求是否已经处理了还是需要再次发送请求。
InterruptedException异常源于客户端线程调用了Thread.interrupt,通常 这是因为应用程序部分关闭,但还在被其他相关应用的方法使用。从字面来看这个异常,进程会中断本地客户端的请求处理的过程,并使该请求处于未知状态。
这两种请求都会导致正常请求处理过程的中断,开发者不能假设处理 过程中的请求的状态。当我们处理这些异常时,开发者在处理前必须知道系统的状态。如果发生群首选举,在我们没有确认情况之前,我们不希望确定主节点。如果create执行成功了,活动主节点死掉以前,没有任何进程能够成为主节点,如果活动主节点还不知道自己已经获得了管理权,不会有任何进程成为主节点进程。
当处理ConnectionLossException异常时,我们需要找出那个进程创建的/master节点,如果进程是自己,就开始成为群首角色。我们通过getData方法来处理:
byte[] getData(
String path,
bool watch,
Stat stat)
-
path: 第一个参数为我们想要获取数据的 znode节点路径。
-
watch:
表示我们是否想要监听后续的数据变更。如果设置为true,我们就可以通过我们创建ZooKeeper句柄时所设置的Watcher对象得到事件,同时另 一个版本的方法提供了以Watcher对象为入参,通过这个传入的对象来接收 变更的事件。我们在后续章节再讨论如何监视变更情况,现在我们设置这 个参数为false,因为我们现在我们只想知道当前的数据是什么 -
stat
最后一个参数类型Stat结构,getData方法会填充znode节点的元数据信息。 -
返回值
方法返回成功(没有抛出异常),就会得到znode节点数据的字节数 组。
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); //1
isLeader = new String(data).equals(serverId)); //2
return true;
}
catch (NoNodeException e)
{
// no master, so try create again
return false;
}
catch (ConnectionLossException e)
{
}
}
}
void runForMaster() throws InterruptedException
{//3
while (true)
{
try
{//4
zk.create("/master", serverId.getBytes(), OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);//5
isLeader = true;
break;
}
catch (NodeExistsException e)
{
isLeader = false;
break;
}
catch (ConnectionLossException e)
{
//6
}
if (checkMaster())
break;7
}
}
-
1、通过获取/master节点的数据来检查活动主节点。
-
2、该行展示了为什么我们需要使用在创建/master节点时保存的数据: 如果/master存在,我们使用/master中的数据来确定谁是群首。如果一个 进程捕获到ConnectionLossException,这个进程可能就是主节点,因create 操作实际上已经处理完,但响应消息却丢失了。
-
3、我们将InterruptedException异常简单地传递给调用者。
-
4、我们将zk.create方法包在try块之中,以便我们捕获并处理ConnectionLossException异常。
-
5、这里为create请求,如果成功执行将会成为主节点。
-
6、处理ConnectionLossException异常的catch块的代码为空,因为我们 并不想中止函数,这样就可以使处理过程继续向下执行。
-
7、检查活动主节点是否存在,如果不存在就重试。
public static void main(String args[]) throws Exception
{
Master m = new Master(args[0]);
m.startZK();
m.runForMaster(); //1
if (isLeader)
{
System.out.println("I'm the leader");2
// wait for a bit
Thread.sleep(60000);
}
else
{
System.out.println("Someone else is the leader");
}
m.stopZK();
}
- 1、调用我们之前实现的runForMaster函数,当前进程成为主节点或另 一进程成为主节点后返回。
- 2、当我们开发主节点的应用逻辑时,我们在此处开始执行这些逻辑, 现在我们仅仅输出我们成为主节点的信息,然后等待60秒后退出main函 数。
1、异步获取管理权
ZooKeeper中,所有同步调用方法都有对应的异步调用方法。通过异步调用,我们可以在单线程中同时进行多个调用,同时也可以简化我们的实现方式。让我们回顾管理权的例子,修改为异步调用的方式。
以下为create方法的异步调用版本:
void create(String path,
byte[] data,
List<ACL> acl,
CreateMode createMode,
AsyncCallback.StringCallback cb,//1
Object ctx) //2
create方法的异步方法与同步方法非常相似,仅仅多了两个参数:
- 提供回调方法的对象。
- 用户指定上下文信息(回调方法调用是传入的对象实例)。
注意,该create方法不会抛出异常,我们可以简化处理,因为调用返回 前并不会等待create命令完成,所以我们无需关心InterruptedException异 常;同时因请求的所有错误信息通过回调对象会第一个返回,所以我们也 无需关心KeeperException异常。
回调对象实现只有一个方法的StringCallback接口:
void processResult(int rc, String path, Object ctx, String name)
异步方法调用会简单化队列对ZooKeeper服务器的请求,并在另一个线 程中传输请求。当接收到响应信息,这些请求就会在一个专用回调线程中 被处理。为了保持顺序,只会有一个单独的线程按照接收顺序处理响应包。
processResult各个参数的含义如下:
-
rc
返回调用的结果,返回OK或与KeeperException异常对应的编码值。 -
path
我们传给create的path参数值。 -
ctx
我们传给create的上下文参数。 -
name
创建的znode节点名称。
目前,调用成功后,path和name的值一样,但是,如果采用 CreateMode.SEQUENTIAL模式,这两个参数值就不会相等。
注意:
因为只有一个单独的线程处理所有回调调用,如果回调函数阻塞,所有后续回调调用都会被阻塞,也就是说,一般不要在回调函数中集中操作或阻塞操作。
让我们继续完成我们的主节点的功能,我们创建了masterCreateCallback对象,用于接收create命令的结果:
static boolean isLeader;
static StringCallback masterCreateCallback = new StringCallback()
{
void processResult(int rc, String path, Object ctx, String name)
{
switch(Code.get(rc))
{//1
case CONNECTIONLOSS: //2
checkMaster();
return;
case OK: //3
isLeader = true;
break;
default: //4
isLeader = false;
}
System.out.println("I'm " + (isLeader ? "" : "not ") + "the leader");
}
};
void runForMaster()
{
zk.create("/master", serverId.getBytes(),OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL, masterCreateCallback, null); //5
}
- 1、我们从rc参数中获得create请求的结果,并将其转换为Code枚举类型。rc如果不为0,则对应KeeperException异常。
- 2、如果因连接丢失导致create请求失败,我们会得到 CONNECTIONLOSS编码的结果,而不是ConnectionLossException异常。 当连接丢失时,我们需要检查系统当前的状态,并判断我们需要如何恢 复,我们将会在我们后面实现的checkMaster方法中进行处理。
- 3、我们现在成为群首,我们先简单地赋值isLeader为true。
- 4、其他情况,我们并未成为群首。
- 5、在runForMaster方法中,我们将masterCreateCallback传给create方法,传入null作为上下文对象参数,因为在runForMaster方法中,我们现 在不需要向masterCreateCallback.processResult方法传入任何信息。
我们现在需要实现checkMaster方法,这个方法与之前的同步情况不太 一样,我们通过回调方法实现处理逻辑,因此在checkMaster函数中不会看 到一系列的事件,而只有getData方法。getData调用完成后,后续处理将会 在DataCallback对象中继续:
DataCallback masterCheckCallback = new DataCallback()
{
void processResult(int rc, String path, Object ctx, byte[] data,Stat stat)
{
switch(Code.get(rc))
{
case CONNECTIONLOSS:
checkMaster();
return;
case NONODE:
runForMaster();
return;
}
}
}
void checkMaster()
{
zk.getData("/master", false, masterCheckCallback, null); //调用checkMaster之后,如果出现了CONNECTIONLOSS,那么就又会调用checkMaster,这就实现了上面的while循环操作。
}
同步方法和异步方法的处理逻辑是一样的,只是异步方法中,我们没有使用while循环,而是通过异步操作在回调函数中进行错误处理。注意以下,runForMaster那边的类是StringCallback,而checkMaster这边的类是DataCallback。
2、设置元数据
我们将使用异步API方法来设置元数据路径。我们的主从模型设计依赖三个目录:/tasks、/assign和/workers,以下代码段会创建这些路径,例子中除了连接丢失错误的处理外没 有其他错误处理:
public void bootstrap()
{
createParent("/workers", new byte[0]); //1
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, createParentCallback, data); //2
}
StringCallback createParentCallback = new StringCallback()
{
public void processResult(int rc, String path, Object ctx, String name)
{
switch (Code.get(rc))
{
case CONNECTIONLOSS:
createParent(path, (byte[]) ctx); //3
break;
case OK:
LOG.info("Parent created");
break;
case NODEEXISTS:
LOG.warn("Parent already registered: " + path);
break;
default:
LOG.error("Something went wrong: ", KeeperException.create(Code.get(rc), path));
}
}
};
4、注册从节点
现在我们已经有了主节点,我们需要配置从节点,以便主节点可以发号施令。根据我们的设计,每个从节点会在/workers下创建一个临时性的 znode节点,很简单,我们通过以下代码就可以实现。我们将使用znode节 点中的数据,来指示从节点的状态:
import java.util.*;
import org.apache.zookeeper.AsyncCallback.DataCallback; import org.apache.zookeeper.AsyncCallback.StringCallback; import org.apache.zookeeper.AsyncCallback.VoidCallback; import org.apache.zookeeper.*;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.AsyncCallback.ChildrenCallback; import org.apache.zookeeper.KeeperException.Code;
import org.apache.zookeeper.data.Stat;
import org.slf4j.*;
public class Worker implements Watcher
{
private static final Logger LOG = LoggerFactory.getLogger(Worker.class); ZooKeeper zk;
String hostPort;
String serverId = Integer.toHexString(random.nextInt());
Worker(String hostPort)
{
this.hostPort = hostPort;
}
void startZK() throws IOException
{
zk = new ZooKeeper(hostPort, 15000, this);
}
public void process(WatchedEvent e)
{
LOG.info(e.toString() + ", " + hostPort);
}
void register()
{
zk.create("/workers/worker-" + serverId,
"Idle".getBytes(), //1
Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL, //2
createWorkerCallback, null);
}
StringCallback createWorkerCallback = new StringCallback()
{
public void processResult(int rc, String path, Object ctx,String name)
{
switch (Code.get(rc))
{
case CONNECTIONLOSS:
register(); //3
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(args[0]);
w.startZK();
w.register();
Thread.sleep(30000);
}
}
- 2、如果进程死掉,我们希望代表从节点的znode节点得到清理,所以我 们使用了EPHEMERAL标志,这意味着,我们简单地关注/workers就可以 得到有效从节点的列表
- 3、因为这个进程是唯一创建表示该进程的临时性znode节点的进程,如 果创建节点时连接丢失,进程会简单地重试创建过程。
我们将从节点状态信息存入了代表从节点的znode节点,这样我们就可 以通过查询ZooKeeper来获得从节点的状态。当前,我们只有初始化和空闲 状态,但是,一旦从节点开始处理某些事情,我们还需要设置其他状态信息。
以下为setStatus的实现(会调用setData设置数据状态),这个方法与之前我们看到的方法有些不同, 我们希望异步方式来设置状态,以便不会延迟常规流程的操作:
StatCallback statusUpdateCallback = new StatCallback()
{
public void processResult(int rc, String path, Object ctx, Stat stat)
{
switch(Code.get(rc))
{
case CONNECTIONLOSS:
updateStatus((String)ctx);1
return;
}
}
};
synchronized private void updateStatus(String status)
{
if (status == this.status)
{//2
zk.setData("/workers/" + name, status.getBytes(), -1,
statusUpdateCallback, status); //3
}
}
public void setStatus(String status)
{
this.status = status; //4
updateStatus(status); //5
}
- 4、我们将状态信息保存到本地变量中,万一更新失败,我们需要重试。
- 5、我们并未在setStatus进行更新,而是新建了一个updateStatus方法, 我们在setStatus中使用它,并且可以在重试逻辑中使用。
- 6、重新处理异步请求连接丢失时有个小问题:处理流程可能变得无序,因为ZooKeeper对请求和响应都会很好地保持顺序,但如果连接丢失,我们又再发起一个新的请求,就会导致整个时序中出现空隙。因此, 我们进行一个状态更新请求前,需要先获得当前状态,否则就要放弃更 新。我们通过同步方式进行检查和重试操作。
- 3、我们执行无条件更新(第三个参数值为-1,表示禁止版本号检查),通过上下文对象参数传递状态。
- 1、如果我们收到连接丢失的事件,我们需要用我们想要更新的状态再 次调用updateStatus方法(通过setData的上下文参数传递参数),因为在 updateStatus方法中进行了竞态条件的检查,所以我们在这里就不需要再次检查。
注意
顺序和ConnectionLossException异常
ZooKeeper会严格地维护执行顺序,并提供了强有力的有序保障,然而,在多线程下还是需要小心面对顺序问题。多线程下,当回调函数中包括重试逻辑的代码时,一些常见的场景都可能导致错误发生当遇到 ConnectionLossException异常而补发一个请求时,新建立的请求可能排序在其他线程中的请求之后,而实际上其他线程中的请求应该在原来请求之 后。。
5、任务队列化
系统最后的组件为Client应用程序队列化新任务,以便从节点执行这些任务,我们会在/tasks节点下添加子节点来表示需要从节点需要执行的命令。我们将会使用有序节点,这样做有两个好处,
- 第一,序列号指定了任 务被队列化的顺序;
- 第二,可以通过很少的工作为任务创建基于序列号的 唯一路径。
我们的Client代码如下:
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.Watcher;
public class Client implements Watcher
{
ZooKeeper zk;
String hostPort;
Client(String hostPort)
{
this.hostPort = hostPort;
}
void startZK() throws Exception
{
zk = new ZooKeeper(hostPort, 15000, this);
}
String queueCommand(String command) throws KeeperException
{
while (true)
{
try
{
String name = zk.create("/tasks/task-",//1
command.getBytes(), OPEN_ACL_UNSAFE, CreateMode.SEQUENTIAL);//2
return name;//3
break;
}
catch (NodeExistsException e)
{
throw new Exception(name + " already appears to be running");
}
catch (ConnectionLossException e)
{//4
}
}
public void process(WatchedEvent e)
{
System.out.println(e);
}
public static void main(String args[]) throws Exception
{
Client c = new Client(args[0]);
c.start();
String name = c.queueCommand(args[1]);
System.out.println("Created " + name);
}
}
当我们运行Client应用程序并发送一个命令时,/tasks节点下就会创建 一个新的znode节点,该节点并不是临时性节点,因此即使Client程序结束 了,这个节点依然会存在。
6、管理客户端
最后,我们将会写一个简单的AdminClient,通过该程序来展示系统的 运行状态。ZooKeeper优点之一是我们可以通过zkCli工具来查看系统的状 态,但是通常你希望编写你自己的管理客户端,以便更快更简单地管理系 统。在本例中,我们通过getData和getChildren方法来获得主从系统的运行 状态。
这些方法的使用非常简单,因为这些方法不会改变系统的运行状态, 我们仅需要简单地传播我们遇到的错误,而不需要进行任何清理操作。
该示例使用了同步调用的方法,这些方法还有一个watch参数,我们置 为false值,因为我们不需要监视变化情况,只是想获得系统当前的运行状 态。在下一章中我们将会看到如何使用这个参数来跟踪系统的变化情况。 现在,让我们看一下AdminClient的代码:
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.Watcher;
public class AdminClient implements Watcher
{
ZooKeeper zk;
String hostPort;
AdminClient(String hostPort)
{
this.hostPort = hostPort;
}
void start() throws Exception
{
zk = new ZooKeeper(hostPort, 15000, this);
}
void listState() throws KeeperException
{
try
{
Stat stat = new Stat();
byte masterData[] = zk.getData("/master", false, stat);//1
Date startDate = new Date(stat.getCtime());//2
System.out.println("Master: " + new String(masterData) +" since " + startDate);
}
catch (NoNodeException e)
{
System.out.println("No Master");
}
System.out.println("Workers:");
for (String w: zk.getChildren("/workers", false))
{
byte data[] = zk.getData("/workers/" + w, false, null);//3
String state = new String(data);
System.out.println("\t" + w + ": " + state);
}
System.out.println("Tasks:");
for (String t: zk.getChildren("/assign", false))
{
System.out.println("\t" + t);
}
}
public void process(WatchedEvent e)
{
System.out.println(e);
}
public static void main(String args[]) throws Exception
{
AdminClient c = new AdminClient(args[0]);
c.start();
c.listState();
}
}
- 1、我们在/master节点中保存了主节点名称信息,因此我们从/master 中获取数据,以获得当前主节点的名称。因为我们并不关心变化情况,所 以我们将第二个参数置为false。
- 2、我们通过Stat结构,可以获得当前主节点成为主节点的时间信息。 ctime为该znode节点建立时的秒数(系统纪元以来的秒数,即自1970年1月1 日00:00:00UTC的描述),详细信息请查看 java.lang.System.currentTimeMillis()。
7、小结
我们在zkCli工具中使用的命令与我们通过ZooKeeper编程所使用的API 非常接近,因此,zkCli工具在初期调研时非常有用,我们可以通过该工具 尝试不同的应用数据的组织方式。API与zkCli的命令非常接近,通过zkCli 工具调研后,我们就可以快速写出与zkCli命令对应的应用程序。然而还有 一些注意事项。首先,我们常常在一个稳定环境中使用zkCli工具,而不会 发生某些未知故障,而对于需要部署的代码,我们需要处理异常情况使得 我们的代码非常复杂,尤其是ConnectionLossException异常,开发者需要 检查系统状态并合理恢复(ZooKeeper用于协助管理分布式状态,提供了故 障处理的框架,但遗憾的是,它并不能让故障消失)。其次,适应异步 API的开发非常有用,异步API提供了巨大的性能优势,简化了错误恢复工 作。