一、简介
①主从示例说明
(1)主节点失效
主节点失效时,我们需要有一个备份主节点(backup master)。当
主要主节点(primary master)崩溃时,备份主节点接管主要主节点的角
色,进行故障转移,然而,这并不是简单开始处理进入主节点的请求。新的主要主节点需要能够恢复到旧的主要主节点崩溃时的状态。对于主节点状态的可恢复性,我们不能依靠从已经崩溃的主节点来获取这些信
息,而需要从其他地方获取,也就是通过ZooKeeper来获取。
状态恢复并不是唯一的重要问题。假如主节点有效,备份主节点却
认为主节点已经崩溃。这种错误的假设可能发生在以下情况,例如主节
点负载很高,导致消息任意延迟(关于这部分内容请参见1.1.4节),备
份主节点将会接管成为主节点的角色,执行所有必需的程序,最终可能
以主节点的角色开始执行,成为第二个主要主节点。更糟的是,如果一
些从节点无法与主要主节点通信,如由于网络分区(network partition)
错误导致,这些从节点可能会停止与主要主节点的通信,而与第二个主
要主节点建立主-从关系。针对这个场景中导致的问题,我们一般称之
为脑裂(split-brain):系统中两个或者多个部分开始独立工作,导致整
体行为不一致性。我们需要找出一种方法来处理主节点失效的情况,关
键是我们需要避免发生脑裂的情况。
(2)从节点失效
客户端向主节点提交任务,之后主节点将任务派发到有效的从节点
中。从节点接收到派发的任务,执行完这些任务后会向主节点报告执行
状态。主节点下一步会将执行结果通知给客户端。如果从节点崩溃了,所有已派发给这个从节点且尚未完成的任务需
要重新派发。其中首要需求是让主节点具有检测从节点的崩溃的能力。
主节点必须能够检测到从节点的崩溃,并确定哪些从节点是否有效以便
派发崩溃节点的任务。一个从节点崩溃时,从节点也许执行了部分任
务,也许全部执行完,但没有报告结果。如果整个运算过程产生了其他
作用,我们还有必要执行某些恢复过程来清除之前的状态
(3)通信故障
如果一个从节点与主节点的网络连接断开,比如网络分区
(network partition)导致,重新分配一个任务可能会导致两个从节点执
行相同的任务。如果一个任务允许多次执行,我们在进行任务再分配时
可以不用验证第一个从节点是否完成了该任务。如果一个任务不允许,
那么我们的应用需要适应多个从节点执行相同任务的可能性
通信故障导致的另一个重要问题是对锁等同步原语的影响。因为节
点可能崩溃,而系统也可能网络分区(network partition),锁机制也会
阻止任务的继续执行。因此ZooKeeper也需要实现处理这些情况的机
制。首先,客户端可以告诉ZooKeeper某些数据的状态是临时状态
(ephemeral);其次,同时ZooKeeper需要客户端定时发送是否存活的
通知,如果一个客户端未能及时发送通知,那么所有从属于这个客户端
的临时状态的数据将全部被删除。通过这两个机制,在崩溃或通信故障
发生时,我们就可以预防客户端独立运行而发生的应用宕机。
②任务总结
根据之前描述的这些,我们可以得到以下主-从架构的需求:
主节点选举
这是关键的一步,使得主节点可以给从节点分配任务。
崩溃检测
主节点必须具有检测从节点崩溃或失去连接的能力。
组成员关系管理
主节点必须具有知道哪一个从节点可以执行任务的能力。
元数据管理
主节点和从节点必须具有通过某种可靠的方式来保存分配状态和执
行状态的能力。
二、zookeeper基础
①znode概念
菜谱包括ZooKeeper操作和维护一个小型的数据
节点,这些节点被称为znode,采用类似于文件系统的层级树状结构进
行管理。图2-1描述了一个znode树的结构,根节点包含4个子节点,其
中三个子节点拥有下一级节点,叶子节点存储了数据信息
②ZooKeeper的API暴露了以下方法
create/path data
创建一个名为/path的znode节点,并包含数据data。
delete/path
删除名为/path的znode。
exists/path检查是否存在名为/path的节点。
setData/path data
设置名为/path的znode的数据为data。
getData/path
返回名为/path节点的数据信息。
getChildren/path
返回所有/path节点的所有子节点列表。‘
ls /
查看所有节点
stat /master true
为主节点设置监视点
ls /tasks true
为从节点设置监视点
create -e /workers/worker1.example.com "worker1.example.com:2224"
添加临时节点
create /workers ""
添加主节点
③znode的不同类型
持久节点和临时节点
znode节点可以是持久(persistent)节点,还可以是临时
(ephemeral)节点。持久的znode,如/path,只能通过调用delete来进行
删除。临时的znode与之相反,当创建该节点的客户端崩溃或关闭了与
ZooKeeper的连接时,这个节点就会被删除。
一个临时znode,在以下两种情况下将会被删除:
1.当创建该znode的客户端的会话因超时或主动关闭而中止时。
2.当某个客户端(不一定是创建者)主动删除该节点时。
有序节点
个znode还可以设置为有序(sequential)节点。一个有序znode节
点被分配唯一个单调递增的整数。当创建有序节点时,一个序号会被追
加到路径之后。例如,如果一个客户端创建了一个有序znode节点,其
路径为/tasks/task-,那么ZooKeeper将会分配一个序号,如1,并将这个
数字追加到路径之后,最后该znode节点为/tasks/task-1。有序znode通过
提供了创建具有唯一名称的znode的简单方式。同时也通过这种方式可
以直观地查看znode的创建顺序。
总之,znode一共有4种类型:持久的(persistent)、临时的
(ephemeral)、持久有序的(persistent_sequential)和临时有序的
(ephemeral_sequential)。
④监视与通知
通知(notification)的机制:客户端向ZooKeeper注册需要接收通知的
znode,通过对znode设置监视点(watch)来接收通知。监视点是一个单
次触发的操作,意即监视点会触发一个通知。为了接收多个通知,客户端必须在每次通知后设置一个新的监视点。在图2-3阐述的情况下,当
节点/tasks发生变化时,客户端会收到一个通知,并从ZooKeeper读取一
个新值。
⑤版本
每一个znode都有一个版本号,它随着每次数据变化而自增。两个
API操作可以有条件地执行:setData和delete。这两个调用以版本号作为
转入参数,只有当转入参数的版本号与服务器上的版本号一致时调用才
会成功。当多个ZooKeeper客户端对同一个znode进行操作时,版本的使
用就会显得尤为重要。
三、使用zookeeper进行开发
①建立ZooKeeper会话
(1)简述
ZooKeeper的API围绕ZooKeeper的句柄(handle)而构建,每个API
调用都需要传递这个句柄。这个句柄代表与ZooKeeper之间的一个会
话。在图3-1中,与ZooKeeper服务器已经建立的一个会话如果断开,这
个会话就会迁移到另一台ZooKeeper服务器上。只要会话还存活着,这
个句柄就仍然有效,ZooKeeper客户端库会持续保持这个活跃连接,以
保证与ZooKeeper服务器之间的会话存活。如果句柄关闭,ZooKeeper客
户端库会告知ZooKeeper服务器终止这个会话。如果ZooKeeper发现客户
端已经死掉,就会使这个会话无效。如果客户端之后尝试重新连接到
ZooKeeper服务器,使用之前无效会话对应的那个句柄进行连接,那么
ZooKeeper服务器会通知客户端库,这个会话已失效,使用这个句柄进
行的任何操作都会返回错误。
创建ZooKeeper句柄的构造函数如下所示:
Zoo Keeper(
String connectString,
int sessionTimeout,
Watcher watcher)
其中:
connectString
包含主机名和ZooKeeper服务器的端口。我们之前通过zkCli连接
ZooKeeper服务时,已经列出过这些服务器。
sessionTimeout
以毫秒为单位,表示ZooKeeper等待客户端通信的最长时间,之后
会声明会话已死亡。目前我们使用15000,即15秒。这就是说如果
ZooKeeper与客户端有15秒的时间无法进行通信,ZooKeeper就会终止客
户端的会话。需要注意,这个值比较高,但对于我们后续的实验会非常
有用。ZooKeeper会话一般设置超时时间为5~10秒。
watcher
用于接收会话事件的一个对象,这个对象需要我们自己创建。因为Wacher定义为接口,所以我们需要自己实现一个类,然后初始化这个类
的实例并传入ZooKeeper的构造函数中。客户端使用Watcher接口来监控
与ZooKeeper之间会话的健康情况。与ZooKeeper服务器之间建立或失去
连接时就会产生事件。它们同样还能用于监控ZooKeeper数据的变化。
最终,如果与ZooKeeper的会话过期,也会通过Watcher接口传递事件来
通知客户端的应用。
(2)实现一个Watcher
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);④
}
}
两个运维命令:
stat:查看客户端连接信息
dump:查看会话连接信息
(3)创建主节点以同步和异步的方式
获取管理权
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;⑦
}
}
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();
}
异步获取管理权
static boolean isLeader;
static StringCallback masterCreateCallback = new StringCallback() {
void processResult(int rc, String path, Object ctx, String name) {
switch(Code.get(rc)) {①
case CONNECTIONLOSS:②
checkMaster();
return;
case OK:③
isLeader = true;
break;
default:④
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);⑤ }
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);
}
(4)创建主节点java代码
public void bootstrap() {
createParent("/workers", new byte[0]);①
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);②
}
String Callback createParentCallback = new StringCallback() {
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");
break;
case NODEEXISTS:
LOG.warn("Parent already registered: " + path);
break;
default:
LOG.error("Something went wrong: ",
KeeperException.create(Code.get(rc), path));
}
}
};
(5)创建从节点java代码
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(),①
Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL,②
createWorkerCallback, null);
} StringCallback createWorkerCallback = new StringCallback() {
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));
}
}
};
public static void main(String args[]) throws Exception {
Worker w = new Worker(args[0]);
w.startZK();
w.register();
Thread.sleep(30000);
}
}