本文介绍下zookeeper中leader选举机制的基本用法和关键知识点。
一、 选项设置 提到Leader选举,先需要重点介绍下创建znode时的Flag选项。
ZOO_EPHEMERAL,用来标记当创建这个znode的节点和Zookeeper失去连接后,这个znode将不再存在在Zookeeper里,Zookeeper使用Watcher察觉事件信息。当客户端接收到事件信息,比如连接超时、节点数据改变、子节点改变,可以调用相应的行为来处理数据。
ZOO_SEQUENCE 用来标识节点命名具有递增的后缀序号(一般是节点名称后填充 10 位字符的序号,如 /xyz0000000000, /xyz0000000001, /xyz0000000002, ...),如下便所示,直接在/app_watch/下创建节点。
[zk: localhost:2181(CONNECTED) 42] ls /app_watch [0000000017, 0000000018]
同样地,ZOO_EPHEMERAL, ZOO_SEQUENCE 可以组合使用,下面的示例中就用到了序列号的特性。
二、监视机制
以下面的示例中,可以看到在client的回调中,每监视到一次事件通知,需要再次调用触发监视,这里就需要具体说明下zookeeper和 watch机制。
Zookeeper 中最有特色且最不容易理解的是监视(Watches)。Zookeeper 所有的读操作——getData(), getChildren(), 和 exists() 都 可以设置监视(watch),监视事件可以理解为一次性的触发器, 官方定义如下: a watch event is one-time trigger, sent to the client that set the watch, which occurs when the data for which the watch was set changes。对此需要作出如下理解:
当设置监视的数据发生改变时,该监视事件会被发送到客户端,例如,如果客户端调用了 getData("/znode1", true) 并且稍后 /znode1 节点上的数据发生了改变或者被删除了,客户端将会获取到 /znode1 发生变化的监视事件,而如果 /znode1 再一次发生了变化,除非客户端再次对 /znode1 设置监视,否则客户端不会收到事件通知。
Zookeeper 客户端和服务端是通过 socket 进行通信的,由于网络存在故障,所以监视事件很有可能不会成功地到达客户端,监视事件是异步发送至监视者的,Zookeeper 本身提供了保序性(ordering guarantee):即客户端只有首先看到了监视事件后,才会感知到它所设置监视的 znode 发生了变化(a client will never see a change for which it has set a watch until it first sees the watch event). 网络延迟或者其他因素可能导致不同的客户端在不同的时刻感知某一监视事件,但是不同的客户端所看到的一切具有一致的顺序。
这意味着 znode 节点本身具有不同的改变方式。你也可以想象 Zookeeper 维护了两条监视链表:数据监视和子节点监视(data watches and child watches) getData() and exists() 设置数据监视,getChildren() 设置子节点监视。 或者,你也可以想象 Zookeeper 设置的不同监视返回不同的数据,getData() 和 exists() 返回 znode 节点的相关信息,而 getChildren() 返回子节点列表。因此, setData() 会触发设置在某一节点上所设置的数据监视(假定数据设置成功),而一次成功的 create() 操作则会出发当前节点上所设置的数据监视以及父节点的子节点监视。一次成功的 delete() 操作将会触发当前节点的数据监视和子节点监视事件,同时也会触发该节点父节点的child watch。 Zookeeper 中的监视是轻量级的,因此容易设置、维护和分发。当客户端与 Zookeeper 服务器端失去联系时,客户端并不会收到监视事件的通知,只有当客户端重新连接后,若在必要的情况下,以前注册的监视会重新被注册并触发,对于开发人员来说 这通常是透明的。只有一种情况会导致监视事件的丢失,即:通过 exists() 设置了某个 znode 节点的监视,但是如果某个客户端在此 znode 节点被创建和删除的时间间隔内与 zookeeper 服务器失去了联系,该客户端即使稍后重新连接 zookeeper服务器后也得不到事件通知。
三、示例
在这个示例中,服务器启动会在/app_watch下注册自己的信息,并使用自增序列号的机制,客户端同样监听/app_watch下的节点信息变化,并打印出来,如果需要选举leader的话,直接把child里的序列号最小的选为leader就好了。
客户端代码如下:
#include <zookeeper.h>
#include <zookeeper_log.h>
#include <iostream>
#include <string.h>
using namespace std;
const char* host = "127.0.0.1:2181";
const int timeout = 2000;
zhandle_t* zkhandle = NULL;
bool is_connect = false;
char path[] = "/app_watch";
int ret = 0;
char buffer[1024];
int buff_len;
Stat stat;
void clients_watcher_g(zhandle_t * zh, int type, int state, const char* path, void* watcherCtx) {
printf("global watcher - type:%d,state:%d\n", type, state);
if ( type==ZOO_SESSION_EVENT ) {
if ( state==ZOO_CONNECTED_STATE ) {
printf("connected to zookeeper service successfully!\n");
printf("timeout:%d\n", zoo_recv_timeout(zh));
is_connect = true;
}
else if ( state==ZOO_EXPIRED_SESSION_STATE ) {
printf("zookeeper session expired!\n");
}
}
else {
printf("other type:%d\n", type);
}
}
void child_watch_cb(zhandle_t* zh, int type, int state, const char* path, void* watcher) {
printf("call child watch cb\n");
}
void watch_cb(zhandle_t* zh, int type, int state, const char* path, void* watcher) {
printf("call watch cb\n");
// watch path
buff_len = sizeof(buffer);
bzero(buffer, buff_len);
ret = zoo_wget(zkhandle, path, watch_cb, NULL, buffer, &buff_len, &stat);
printf("ret=%d stat=%d len=%d buffer=%s\n", ret, stat.ctime, buff_len, buffer);
}
int main(int argc, char* argv[])
{
//Init and connect
zoo_set_debug_level(ZOO_LOG_LEVEL_WARN);
if (zkhandle)
zookeeper_close(zkhandle);
zkhandle = zookeeper_init(host, clients_watcher_g, timeout, 0, NULL, 0);
if (NULL == zkhandle) {
printf("zookeeper init error\n");
return 0;
}
// watch path
buff_len = sizeof(buffer);
bzero(buffer, buff_len);
ret = zoo_wget(zkhandle, path, watch_cb, NULL, buffer, &buff_len, &stat);
printf("ret=%d stat=%d len=%d buffer=%s\n", ret, stat.ctime, buff_len, buffer);
while(1) {
struct String_vector str_vec;
ret = zoo_wget_children(zkhandle, path, child_watch_cb, NULL, &str_vec);
if (ZOK != ret) {
printf("zookeeper wget error\n");
return 0;
}
for (int i = 0; i < str_vec.count; i++) {
printf("data[%d] data:[%s]\n", i, str_vec.data[i]);
}
printf("sleep...\n");
sleep(5);
}
zookeeper_close(zkhandle);
}
服务器代码如下:
#include <zookeeper.h>
#include <zookeeper_log.h>
#include <iostream>
#include <string.h>
using namespace std;
const char* host = "127.0.0.1:2181";
const int timeout = 2000;
zhandle_t* zkhandle = NULL;
bool is_connect = false;
void clients_watcher_g(zhandle_t * zh, int type, int state, const char* path, void* watcherCtx) {
printf("global watcher - type:%d,state:%d\n", type, state);
if ( type==ZOO_SESSION_EVENT ) {
if ( state==ZOO_CONNECTED_STATE ) {
printf("connected to zookeeper service successfully!\n");
printf("timeout:%d\n", zoo_recv_timeout(zh));
is_connect = true;
}
else if ( state==ZOO_EXPIRED_SESSION_STATE ) {
printf("zookeeper session expired!\n");
}
}
else {
printf("other type:%d\n", type);
}
}
int main(int argc, char* argv[])
{
//Init and connect
zoo_set_debug_level(ZOO_LOG_LEVEL_WARN);
if (zkhandle)
zookeeper_close(zkhandle);
zkhandle = zookeeper_init(host, clients_watcher_g, timeout, 0, NULL, 0);
if (NULL == zkhandle) {
printf("zookeeper init error\n");
return 0;
}
// init
char path[] = "/app_watch";
Stat stat;
int ret = 0;
// create
ret = zoo_create(zkhandle, path, NULL, -1, &ZOO_OPEN_ACL_UNSAFE, ZOO_EPHEMERAL|ZOO_SEQUENCE, NULL, 0);
printf("create [%s] ret=%d\n", path, ret);
char buffer[1024];
int buff_len;
char child_path[1024];
//snprintf(child_path, 1023, "%s", path, getpid());
snprintf(child_path, 1023, "%s/", path, getpid());
// create
buff_len = sizeof(buffer);
snprintf(buffer, buff_len, "10.101.1.101:%d", getpid());
buff_len = strlen(buffer);
printf("buff:%s len:%d\n", buffer, buff_len);
ret = zoo_create(zkhandle, child_path, buffer, buff_len, &ZOO_OPEN_ACL_UNSAFE, ZOO_EPHEMERAL|ZOO_SEQUENCE, NULL, 0);
printf("create [%s] ret=%d\n", child_path, ret);
while(1) sleep(1200);
zookeeper_close(zkhandle);
}
执行一下客户端,并启动一个服务器实例,查看如下:
[zk: localhost:2181(CONNECTED) 11] ls /app_watch
[0000000026]
[zk: localhost:2181(CONNECTED) 12] get /app_watch/0000000026
10.101.1.101:20594
看到 app_watch下已经注册了一个节点,id是26,查看client的日志如下:
[root@SH-todo-1412181717 /home/derrywang/zookeeper/zk_demo2]# ./watch_client
global watcher - type:-1,state:3
ret=0 stat=-1597853671 len=18 buffer=10.101.1.101:29413
connected to zookeeper service successfully!
timeout:4000
data[0] data:[0000000026]
sleep...
再启动一个服务器实例,查看如下:
[zk: localhost:2181(CONNECTED) 13] ls /app_watch
[0000000026, 0000000027]
[zk: localhost:2181(CONNECTED) 14] get /app_watch/0000000027
10.101.1.101:20871
看到app_watch下已经注册了两个节点了,id是27, 查看client的日志如下:
call child watch cb
data[0] data:[0000000026]
data[1] data:[0000000027]
sleep...
已经触发了回调,并获了一个一个新的节点,这里把第一个实例停掉,过一段时间client日志如下:
call child watch cb
data[0] data:[0000000027]
sleep...
leader已死,只有一个节点了,这里可以触发切换了。
上面的例子说明了zookeeper leader选举的核心机制了,了解了这个机制,再做路由配置等都是应用层的实现了。