ZK编程 - 基础教程 - 实现Queue 和 Barrier
ZK编程 - 基础教程
前言
翻译自官网
介绍
这个教程实现了 障栅(barriers) 和 生产-消费者队列(producer-consumer queues). 分别对应代码里面的 Barrier 和 Queue类, 运行这个例子需要至少一个 ZK 服务器.
两个类都使用下面这个基类:
static ZooKeeper zk = null;
static Integer mutex;
String root;
SyncPrimitive(String address) {
if(zk == null){
try {
System.out.println("Starting ZK:");
zk = new ZooKeeper(address, 3000, this);
mutex = new Integer(-1);
mutex.wait();
System.out.println("Finished starting ZK: " + zk);
} catch (IOException e) {
System.out.println(e.toString());
zk = null;
}
}
}
synchronized public void process(WatchedEvent event) {
synchronized (mutex) {
mutex.notify();
}
}
两个类都是继承自 SyncPrimitive. 这种情况下, 我们把通用的步骤提取到 SyncPrimitive 的构造函数里面. 为了让这个例子保持简单, 我们在实例化 barrier 或者 queue 对象的时候创建 ZK 对象, 并且我们声明一个静态变量(zk)来引用它. 后续的 barrier 和 queue 实例会检查 Zk 对象是否存在. 另一种方式就是把 zk 实例从外部传进来.
我们用 process() 方法来处理监听器触发的时间. 在下面的讨论中, 我们会展示设置了监听器的代码. 监听器是 Zk 的内部结构, 用来通知客户端 node 发生的变化. 例如, 如果一个客户端正在等待另一个客户端离开 barrier, 那么它就可以设置一个监听器并等待对应节点的改变, 根据改变来决定是否继续启动执行代码. 这一点我们一看这些例子就明白了。
Barriers
barrier 是一种基础的同步工具, 用来控制一批进程(线程)同步开始或者结束. 实现的思路是, 使用一个znode来作为这些进程(线程)的父节点. 假设这个父节点就是 “/b1”. 每个进程(线程) “pn” 随后创建了一个节点 “/b1/pn”. 只有创建了足够的子节点之后, 这些线程再同时开始执行.
旁注: 因为这这个例子中 pn 使用的是 计算机的 hostname, 所以在相同的机器上如果要执行的话, 就得使用不同的路径名称.
在这个例子中, 每个进程实例化一个 Barrier 对象, 并且其构造函数需要这些参数:
- ZK服务器的地址, 比如 “zoo1.foo.com:2181”
- barrier 节点的路径 (比如 “/b1”)
- 这组进程的数目
Barrier 的构造函数传个ZK的服务器地址到父类中. 父类会在 zk 为 null 的时候创建一个 ZooKeeper 实例. 随后它会创建一个所有进程的父节点作为 barrier , 我们称之为 root. (注意这个 root 不是 ZK 概念中的 “/”.)
/**
* Barrier 构造函数
*
* @param address zk服务器的地址
* @param root 这组进程的父节点名称
* @param size 这一批进程的数量
*/
Barrier(String address, String root, int size) {
super(address);
this.root = root;
this.size = size;
// 创建 barrier 节点
if (zk != null) {
try {
Stat s = zk.exists(root, false);
if (s == null) {
zk.create(root, new byte[0], Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
}
} catch (KeeperException e) {
System.out
.println("Keeper exception when instantiating queue: "
+ e.toString());
} catch (InterruptedException e) {
System.out.println("Interrupted exception");
}
}
// 使用 hostname 作为 子节点的命令
try {
name = new String(InetAddress.getLocalHost().getCanonicalHostName().toString());
} catch (UnknownHostException e) {
System.out.println(e.toString());
}
}
要进入barrier, 进程要调用 enter(). 进程会在 root 下面创建一个子节点来代表自己, 使用的是hostname 作为节点名称. 它会等待足够的进程进入 barrier. 进程会通过检查父节点的子节点(使用 getChildren 方法)数目来检查等待的数量, 如果数量还不足, 就等待通知唤醒自己. 在代码中, 我们使用 2个参数的 getChildren() 方法. 第一个参数指定要读取哪个节点, 第二个参数是个boolean值来开启监听. 在代码里面设置的是 true.
/**
* 进入 barrier
*/
boolean enter() throws KeeperException, InterruptedException{
zk.create(root + "/" + name, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL);
logger.info("创建节点成功: " + root + "/" + name);
while (true) {
synchronized (mutex) {
//监听是true, 一旦有新的节点加入, 就会唤醒 mutex , 重新循环
List<String> list = zk.getChildren(root, true);
//还没凑齐 size 个请求的时候, 等待
if (list.size() < size) {
mutex.wait();
} else {
return true;
}
}
}
}
注意 enter 方法会抛出 KeeperException 和 InterruptedException, 所以应用需要处理这些异常.
当计算完成的时候, 进程应该调用 leave 来离开 barrier. 首先它会删除自己关联的节点, 然后回去父节点的子节点, 如果至少有一个子节点, 那么它会就等待.(注意: getChildren 的 第二个参数是 true, 表示 ZooKeeper 必须设置 root 节点的监听器). 当接收到通知的时候, 它会再次检查 父节点 是否包含任何子节点.
/**
* 等待所有进程离开 barrier
*
*/
boolean leave(String name) throws KeeperException, InterruptedException {
zk.delete(root + "/" + name, 0);
logger.info("删除 " + root + "/" + name + " 节点");
while (true) {
synchronized (mutex) {
List<String> list = zk.getChildren(root, true);
//每次屏障满了之后, 都会清空一次子节点
//子节点的数目大于0 的时候,
//说明其他的任务还在执行, 需要等待子节点清空
if (list.size() > 0) {
logger.info("start wait");
mutex.wait();
} else {
return true;
}
}
}
}
生产者-消费者队列
一个生产者-消费者队列是不同进程用来产生和消费消息的分布式的数据结构,. 生产者进程创建消息并添加到队列. 消费者进程从队列中移除消息并进行处理. 在这个实现中, 消息简单地使用数字. 队列是用 root 节点来表示, 并且为了元素到队列, 生产者进程会创建新的 root 节点的子节点.
下面的代码展示Queue的构造函数. 跟 Barrier 一样, 首先调用父类SyncPrimitive的构造函数, 然后创建了 ZK 对象. 接着验证 Queue 的 root 节点是否存在, 若不存在则创建.
/**
* Constructor of producer-consumer queue
*
* @param address
* @param name
*/
Queue(String address, String name) {
super(address);
this.root = name;
// Create ZK node name
if (zk != null) {
try {
Stat s = zk.exists(root, false);
if (s == null) {
zk.create(root, new byte[0], Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
}
} catch (KeeperException e) {
System.out.println("Keeper exception when instantiating queue: "
+ e.toString());
} catch (InterruptedException e) {
System.out.println("Interrupted exception");
}
}
}
一个生产者进程调用 “produce()” 来添加1个消息到队列, 并传递一个 整数作为参数. 为了添加这个消息, 该方法使用 zk.create() 方法创建一个新的节点并且使用 SEQUENCE 标志来指示 ZK 添加子节点的时候 同时在末尾添加序号. 这样我们就能保证子节点的顺序, 以此来保证FIFO.
/**
* 添加元素到队列
*/
boolean produce(int i) throws KeeperException, InterruptedException{
ByteBuffer b = ByteBuffer.allocate(4);
byte[] value;
// Add child with value i
b.putInt(i);
value = b.array();
zk.create(root + "/element", value, Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT_SEQUENTIAL);
return true;
}
为了消费其中的元素, 消费者进程会去获取 root 节点的所有子节点, 并解析出其中最小的下标值, 并拿到最先入队列的元素.注意, 如果有冲突, 那么两个竞争的进程将没有办法删除该节点并且这个删除操作会抛出异常.
调用 getChildren() 会按照 字典序 返回子节点. 因为字典序和实际上数字下标的序号有可能不一致, 所以我们还是应该获取最小下标的值. 为了决定哪个下标最小, 我们遍历列表并且截取最后的下标部分进行比较.
/**
* 从队列中移除最先的入队列的元素
*/
int consume() throws KeeperException, InterruptedException {
int retvalue = -1;
Stat stat = null;
// Get the first element available
while (true) {
synchronized (mutex) {
List<String> list = zk.getChildren(root, true);
//如果没有元素, 则等待
if (list.size() == 0) {
logger.info("Going to wait");
mutex.wait();
} else {
//获取到的列表顺序是字典序, 而不是添加的顺序
//找到最小的序号
logger.info("elem(0) :" + list.get(0));
Integer min = new Integer(list.get(0).substring(empharmalIndex));
for (String s : list) {
Integer tempValue = new Integer(s.substring(empharmalIndex));
if (tempValue < min) min = tempValue;
}
String indexName = String.format("%010d",min);
logger.info("Temporary value: " + root + elemPath + indexName);
//获取最小序号的子临时节点的数据
byte[] b = zk.getData(root + elemPath + indexName,
false, stat);
//删除子节点
zk.delete(root + elemPath + indexName, 0);
//nio式缓存
ByteBuffer buffer = ByteBuffer.wrap(b);
retvalue = buffer.getInt();
return retvalue;
}
}
}
}
测试
Queue 测试
启动一个创建100个元素生产者
java SyncPrimitive qTest localhost 100 p
启动一个消费100个元素的消费者
java SyncPrimitive qTest localhost 100 c
Barrier 测试
启动两个参与者的 barrier
java SyncPrimitive bTest localhost 2
总结
- ZK 的单个 api 是线程安全的 , 但是为了让代码正常工作, 对于代码块也需要进行同步管理。
- 利用事件的监控来协调整个程序的运行。
源代码
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Random;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.data.Stat;
public class SyncPrimitive implements Watcher {
static ZooKeeper zk = null;
static Integer mutex;
String root;
SyncPrimitive(String address) {
if(zk == null){
try {
System.out.println("Starting ZK:");
zk = new ZooKeeper(address, 3000, this);
mutex = new Integer(-1);
System.out.println("Finished starting ZK: " + zk);
} catch (IOException e) {
System.out.println(e.toString());
zk = null;
}
}
//else mutex = new Integer(-1);
}
synchronized public void process(WatchedEvent event) {
synchronized (mutex) {
//System.out.println("Process: " + event.getType());
mutex.notify();
}
}
/**
* Barrier
*/
static public class Barrier extends SyncPrimitive {
int size;
String name;
/**
* Barrier constructor
*
* @param address
* @param root
* @param size
*/
Barrier(String address, String root, int size) {
super(address);
this.root = root;
this.size = size;
// Create barrier node
if (zk != null) {
try {
Stat s = zk.exists(root, false);
if (s == null) {
zk.create(root, new byte[0], Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
}
} catch (KeeperException e) {
System.out
.println("Keeper exception when instantiating queue: "
+ e.toString());
} catch (InterruptedException e) {
System.out.println("Interrupted exception");
}
}
// My node name
try {
name = new String(InetAddress.getLocalHost().getCanonicalHostName().toString());
} catch (UnknownHostException e) {
System.out.println(e.toString());
}
}
/**
* Join barrier
*
* @return
* @throws KeeperException
* @throws InterruptedException
*/
boolean enter() throws KeeperException, InterruptedException{
zk.create(root + "/" + name, new byte[0], Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
while (true) {
synchronized (mutex) {
List<String> list = zk.getChildren(root, true);
if (list.size() < size) {
mutex.wait();
} else {
return true;
}
}
}
}
/**
* Wait until all reach barrier
*
* @return
* @throws KeeperException
* @throws InterruptedException
*/
boolean leave() throws KeeperException, InterruptedException{
zk.delete(root + "/" + name, 0);
while (true) {
synchronized (mutex) {
List<String> list = zk.getChildren(root, true);
if (list.size() > 0) {
mutex.wait();
} else {
return true;
}
}
}
}
}
/**
* Producer-Consumer queue
*/
static public class Queue extends SyncPrimitive {
/**
* Constructor of producer-consumer queue
*
* @param address
* @param name
*/
Queue(String address, String name) {
super(address);
this.root = name;
// Create ZK node name
if (zk != null) {
try {
Stat s = zk.exists(root, false);
if (s == null) {
zk.create(root, new byte[0], Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
}
} catch (KeeperException e) {
System.out
.println("Keeper exception when instantiating queue: "
+ e.toString());
} catch (InterruptedException e) {
System.out.println("Interrupted exception");
}
}
}
/**
* Add element to the queue.
*
* @param i
* @return
*/
boolean produce(int i) throws KeeperException, InterruptedException{
ByteBuffer b = ByteBuffer.allocate(4);
byte[] value;
// Add child with value i
b.putInt(i);
value = b.array();
zk.create(root + "/element", value, Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT_SEQUENTIAL);
return true;
}
/**
* Remove first element from the queue.
*
* @return
* @throws KeeperException
* @throws InterruptedException
*/
int consume() throws KeeperException, InterruptedException{
int retvalue = -1;
Stat stat = null;
// Get the first element available
while (true) {
synchronized (mutex) {
List<String> list = zk.getChildren(root, true);
if (list.size() == 0) {
System.out.println("Going to wait");
mutex.wait();
} else {
Integer min = new Integer(list.get(0).substring(7));
String minNode = list.get(0);
for(String s : list){
Integer tempValue = new Integer(s.substring(7));
//System.out.println("Temporary value: " + tempValue);
if(tempValue < min) {
min = tempValue;
minNode = s;
}
}
System.out.println("Temporary value: " + root + "/" + minNode);
byte[] b = zk.getData(root + "/" + minNode,
false, stat);
zk.delete(root + "/" + minNode, 0);
ByteBuffer buffer = ByteBuffer.wrap(b);
retvalue = buffer.getInt();
return retvalue;
}
}
}
}
}
public static void main(String args[]) {
if (args[0].equals("qTest"))
queueTest(args);
else
barrierTest(args);
}
public static void queueTest(String args[]) {
Queue q = new Queue(args[1], "/app1");
System.out.println("Input: " + args[1]);
int i;
Integer max = new Integer(args[2]);
if (args[3].equals("p")) {
System.out.println("Producer");
for (i = 0; i < max; i++)
try{
q.produce(10 + i);
} catch (KeeperException e){
} catch (InterruptedException e){
}
} else {
System.out.println("Consumer");
for (i = 0; i < max; i++) {
try{
int r = q.consume();
System.out.println("Item: " + r);
} catch (KeeperException e){
i--;
} catch (InterruptedException e){
}
}
}
}
public static void barrierTest(String args[]) {
Barrier b = new Barrier(args[1], "/b1", new Integer(args[2]));
try{
boolean flag = b.enter();
System.out.println("Entered barrier: " + args[2]);
if(!flag) System.out.println("Error when entering the barrier");
} catch (KeeperException e){
} catch (InterruptedException e){
}
// Generate random integer
Random rand = new Random();
int r = rand.nextInt(100);
// Loop for rand iterations
for (int i = 0; i < r; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
try{
b.leave();
} catch (KeeperException e){
} catch (InterruptedException e){
}
System.out.println("Left barrier");
}
}