基於zookeeper的主備切換方法
zookeeper的ZOO_EPHEMERAL節點(如果ZOO_EPHEMERAL滿足不了需求,可以考慮和ZOO_SEQUENCE結合使用),在會話關閉或過期時,會自動刪除,利用這一特性可以實現兩個或多節點間的主備切換。
實現方法:
1)在進程啟動時調用zookeeper_init()初始化:
bool X::init_zookeeper()
{
// 第一次調用時_clientid總是為NULL,
// 狀態為ZOO_EXPIRED_SESSION_STATE時,需要重新調用zookeeper_init,
// 這個時候可傳入的_clientid為前一次zookeeper_init()產生的_clientid
// 請注意zookeeper_init()是一個異步調用,返回非NULL並不表示會話建立成功,
// 只有當zk_watcher中的type為ZOO_SESSION_EVENT和state為ZOO_CONNECTED_STATE時,
// 才真正表示會話建立成功。
_zhandle = zookeeper_init(zk_hosts, zk_watcher, 5000, _clientid, this, 0);
if (NULL == _zhandle)
{
MYLOG_ERROR("init zookeeper failed: %s\n", zerror(errno));
return false;
}
MYLOG_INFO("init zookeeper(%s) successfully\n", zk_hosts);
return true;
}
2)進入工作之前,先嘗試切換成主,只有成功切換成主後才進入work
bool X::run()
{
while (true)
{
int num_items = 0;
// 備機最簡單的方法是每隔一定時間,如1秒就嘗試轉成master,
// 如果不使用輪詢,則可以采用監視_zk_path的方式
mooon::sys::CUtils::millisleep(1000);
// 如果不是master,則嘗試轉成master,如果轉成不成功則繼續下一次嘗試
if (!is_master() && !change_to_master())
continue;
do_work();
}
}
bool X::is_master() const
{
return _is_master;
}
bool X::change_to_master()
{
static uint64_t log_counter = 0; // 打log計數器,備狀態時的日志輸出
// ZOO_EPHEMERAL|ZOO_SEQUENCE
// _myip為本地IP地址,可以通過它來判斷當前誰是master
// _zk_path值示例:/master/test,注意需要先保證/master已存在
int errcode = zoo_create(_zhandle, _zk_path.c_str(), _myip.c_str(), _myip.size()+1, &ZOO_OPEN_ACL_UNSAFE, ZOO_EPHEMERAL, NULL, 0);
// (-4)connection loss,比如為zookeeper_init()指定了無效的hosts(一個有效的host也沒有)
if (errcode != ZOK)
{
_is_master = false;
// 減少為備狀態時的日志輸出
if (0 == log_counter++ % 600)
{
MYLOG_DEBUG("become master[%s] failed: (%d)%s\n", _zk_path.c_str(), errcode, zerror(errcode));
}
return false;
}
else
{
_is_master = true;
log_counter = 0;
MYLOG_INFO("becase master[%s]\n", _zk_path.c_str());
// sleep一下,以便讓原master正在進行的完成
mooon::sys::CUtils::millisleep(2000);
return true;
}
}
3)當zookeeper會話成功建立或過期時均會觸發zk_watcher,可通過type和state來區分
void zk_watcher(zhandle_t *zh, int type, int state, const char *path, void *context)
{
X* x = static_cast<X*>(context);
MYLOG_DEBUG("zh=%p, type=%d, state=%d, context=%p, path=%s\n", zh, type, state, context, path);
// zookeeper_init成功時type為ZOO_SESSION_EVENT,state為ZOO_CONNECTED_STATE
if ((ZOO_SESSION_EVENT == type) && (ZOO_CONNECTED_STATE == state))
{
x->on_zookeeper_connected(path);
}
else if ((ZOO_SESSION_EVENT == type) && (ZOO_EXPIRED_SESSION_STATE == state))
{
// 需要重新調用zookeeper_init(),簡單點可以退出當前進程重啟
x->on_zookeeper_expired();
}
}
附: zookeeper日志
默認情況下zookeeper日志是輸出到stderr,但可以通過zoo_set_log_stream()來定向到自己的日志輸出中,還可以使用zoo_set_debug_level()來控制zookeeper的日志級別。