一、什麼是Watcher機制
Zookeeper可以理解爲是一種提供持久化功能的內存樹狀結構存儲,樹的每一個節點被稱爲znode,當一個zonde節點執行了更新操作,我們稱之爲事件,而一個監視點表示一個與之關聯的znode節點和事件類型組成的一個單次觸發器,當一個監視點被一個事件觸發時,就會產生一個通知,通知是註冊了監視點的應用客戶端收到事件的報告信息。當客戶端註冊了一個監視點來接收通知,匹配該監視點條件的第一個事件會觸發監視點的通知並且最多隻觸發一次。Watcher機制採用Push/Pull的方式進行數據的更新,當zonde發生變化時,ZK服務器會產生一個WatchedEvent事件通知客戶端(Push),客戶端根據事件類型從ZK服務器獲取數據進行下一步的處理(Pull)。
二、Watcher的類文件結構
Watcher在Zookeeper中是一個接口,客戶端根據自己的需要實現該接口並設置Watcher。下圖是該接口的類文件UML
1、Zookeeper狀態
- Disconnected:客戶端是斷開連接的狀態,不能連接服務集合中的任意一個
- SyncConnected:客戶端是連接狀態,連接其中的一個服務
- AuthFailed:鑑權失敗
- ConnectedReadOnly:客戶端連接只讀的服務器
- SaslAuthenticated:SASL認證
- Expired:服務器已經過期了該客戶端的Session
2、Zookeeper事件類型
- None:無
- NodeCreated:節點創建
- NodeDeleted:節點刪除
- NodeDataChanged:節點數據改變
- NodeChildrenChanged:子節點改變(添加/刪除)
三、設置Watcher
Watcher是由一系列讀取操作所設置的一次性觸發器,每一個Watcher由一個特定的操作觸發。這些操作主要包括:
- getData:獲取數據
- exists:節點是否存在
- getChildren:獲取子節點
當客戶端進行這些操作,或者在創建Zookeeper客戶端時,可以顯式的設置一個Watcher。在Zookeeper客戶端,與服務器的連接通過一個ClientCnxn對象進行維護,ClientCnxn中通過ClientWatchManager去管理客戶端註冊的Watcher,當進行上述操作時,客戶端會將ClientWatchManager維護的Watcher同步給ZK服務器
void primeConnection() throws IOException {
LOG.info("Socket connection established to "
+ clientCnxnSocket.getRemoteSocketAddress()
+ ", initiating session");
isFirstConnect = false;
long sessId = (seenRwServerBefore) ? sessionId : 0;
ConnectRequest conReq = new ConnectRequest(0, lastZxid,
sessionTimeout, sessId, sessionPasswd);
synchronized (outgoingQueue) {
// We add backwards since we are pushing into the front
// Only send if there's a pending watch
// TODO: here we have the only remaining use of zooKeeper in
// this class. It's to be eliminated!
//獲取Zookeeper客戶端註冊的Watcher並設置到待發送Packet中
if (!disableAutoWatchReset) {
List<String> dataWatches = zooKeeper.getDataWatches();
List<String> existWatches = zooKeeper.getExistWatches();
List<String> childWatches = zooKeeper.getChildWatches();
if (!dataWatches.isEmpty()
|| !existWatches.isEmpty() || !childWatches.isEmpty()) {
SetWatches sw = new SetWatches(lastZxid,
prependChroot(dataWatches),
prependChroot(existWatches),
prependChroot(childWatches));
RequestHeader h = new RequestHeader();
h.setType(ZooDefs.OpCode.setWatches);
h.setXid(-8);
Packet packet = new Packet(h, new ReplyHeader(), sw, null, null);
outgoingQueue.addFirst(packet);
}
}
for (AuthData id : authInfo) {
outgoingQueue.addFirst(new Packet(new RequestHeader(-4,
OpCode.auth), null, new AuthPacket(0, id.scheme,
id.data), null, null));
}
outgoingQueue.addFirst(new Packet(null, null, conReq,
null, null, readOnly));
}
clientCnxnSocket.enableReadWriteOnly();
if (LOG.isDebugEnabled()) {
LOG.debug("Session establishment request sent on "
+ clientCnxnSocket.getRemoteSocketAddress());
}
}
服務器在DataTree結構中,通過WatchManager維護客戶端設置的Watcher,並負責觸發他們。當服務器接收到客戶端的請求時,會將客戶端設置的Watcher放入WatchManager結構中。服務端的Watcher是一個ServerCnxn。
//getData
public byte[] getData(String path, Stat stat, Watcher watcher)
throws KeeperException.NoNodeException {
DataNode n = nodes.get(path);
if (n == null) {
throw new KeeperException.NoNodeException();
}
synchronized (n) {
n.copyStat(stat);
if (watcher != null) {
dataWatches.addWatch(path, watcher);
}
return n.data;
}
}
//exists
public Stat statNode(String path, Watcher watcher)
throws KeeperException.NoNodeException {
Stat stat = new Stat();
DataNode n = nodes.get(path);
if (watcher != null) {
dataWatches.addWatch(path, watcher);
}
if (n == null) {
throw new KeeperException.NoNodeException();
}
synchronized (n) {
n.copyStat(stat);
return stat;
}
}
//getChildren
public List<String> getChildren(String path, Stat stat, Watcher watcher)
throws KeeperException.NoNodeException {
DataNode n = nodes.get(path);
if (n == null) {
throw new KeeperException.NoNodeException();
}
synchronized (n) {
if (stat != null) {
n.copyStat(stat);
}
ArrayList<String> children;
Set<String> childs = n.getChildren();
if (childs != null) {
children = new ArrayList<String>(childs.size());
children.addAll(childs);
} else {
children = new ArrayList<String>(0);
}
if (watcher != null) {
childWatches.addWatch(path, watcher);
}
return children;
}
}
四、觸發Watcher
Watcher是一個特定操作的一次性觸發器,最多觸發一次。當服務器進行能夠產生znode更新操作時,會觸發與該操作相關聯Watcher
//創建節點時會產生事件
public String createNode(String path, byte data[], List<ACL> acl,
long ephemeralOwner, int parentCVersion, long zxid, long time)
throws KeeperException.NoNodeException,
KeeperException.NodeExistsException {
//...
dataWatches.triggerWatch(path, Event.EventType.NodeCreated);
childWatches.triggerWatch(parentName.equals("") ? "/" : parentName,
Event.EventType.NodeChildrenChanged);
return path;
}
//查詢與該操作相關聯的Watcher並刪除
public Set<Watcher> triggerWatch(String path, EventType type, Set<Watcher> supress) {
WatchedEvent e = new WatchedEvent(type,
KeeperState.SyncConnected, path);
HashSet<Watcher> watchers;
synchronized (this) {
watchers = watchTable.remove(path);
if (watchers == null || watchers.isEmpty()) {
if (LOG.isTraceEnabled()) {
ZooTrace.logTraceMessage(LOG,
ZooTrace.EVENT_DELIVERY_TRACE_MASK,
"No watchers for " + path);
}
return null;
}
for (Watcher w : watchers) {
HashSet<String> paths = watch2Paths.get(w);
if (paths != null) {
paths.remove(path);
}
}
}
for (Watcher w : watchers) {
if (supress != null && supress.contains(w)) {
continue;
}
w.process(e);
}
return watchers;
}
//向客戶端發送通知
synchronized public void process(WatchedEvent event) {
ReplyHeader h = new ReplyHeader(-1, -1L, 0);
if (LOG.isTraceEnabled()) {
ZooTrace.logTraceMessage(LOG, ZooTrace.EVENT_DELIVERY_TRACE_MASK,
"Deliver event " + event + " to 0x"
+ Long.toHexString(this.sessionId)
+ " through " + this);
}
// Convert WatchedEvent to a type that can be sent over the wire
WatcherEvent e = event.getWrapper();
//發送通知
sendResponse(h, e, "notification");
}
客戶端在收到通知後,會查詢與該操作關聯的具體Watcher,進行觸發
//查詢關聯的Watcher集合
public Set<Watcher> materialize(Watcher.Event.KeeperState state,
Watcher.Event.EventType type,
String clientPath)
{
Set<Watcher> result = new HashSet<Watcher>();
switch (type) {
case None:
result.add(defaultWatcher);
boolean clear = ClientCnxn.getDisableAutoResetWatch() &&
state != Watcher.Event.KeeperState.SyncConnected;
synchronized(dataWatches) {
for(Set<Watcher> ws: dataWatches.values()) {
result.addAll(ws);
}
if (clear) {
dataWatches.clear();
}
}
synchronized(existWatches) {
for(Set<Watcher> ws: existWatches.values()) {
result.addAll(ws);
}
if (clear) {
existWatches.clear();
}
}
synchronized(childWatches) {
for(Set<Watcher> ws: childWatches.values()) {
result.addAll(ws);
}
if (clear) {
childWatches.clear();
}
}
return result;
case NodeDataChanged:
case NodeCreated:
synchronized (dataWatches) {
addTo(dataWatches.remove(clientPath), result);
}
synchronized (existWatches) {
addTo(existWatches.remove(clientPath), result);
}
break;
case NodeChildrenChanged:
synchronized (childWatches) {
addTo(childWatches.remove(clientPath), result);
}
break;
case NodeDeleted:
synchronized (dataWatches) {
addTo(dataWatches.remove(clientPath), result);
}
// XXX This shouldn't be needed, but just in case
synchronized (existWatches) {
Set<Watcher> list = existWatches.remove(clientPath);
if (list != null) {
addTo(existWatches.remove(clientPath), result);
LOG.warn("We are triggering an exists watch for delete! Shouldn't happen!");
}
}
synchronized (childWatches) {
addTo(childWatches.remove(clientPath), result);
}
break;
default:
String msg = "Unhandled watch event type " + type
+ " with state " + state + " on path " + clientPath;
LOG.error(msg);
throw new RuntimeException(msg);
}
return result;
}
//觸發Watcher
private void processEvent(Object event) {
try {
if (event instanceof WatcherSetEventPair) {
// each watcher will process the event
WatcherSetEventPair pair = (WatcherSetEventPair) event;
for (Watcher watcher : pair.watchers) {
try {
watcher.process(pair.event);
} catch (Throwable t) {
LOG.error("Error while calling watcher ", t);
}
}
//...
}
五:刪除Watcher
- 觸發Watcher:觸發Watcher後,Watcher會自動刪除
- 關閉會話:客戶端設置的每個監視點與會話相關聯如果會話過期等待中的監視器將會被刪除。需要注意,監視點可以跨越不同服務端的連接而保持,當客戶端與服務器斷開連接後連接到集羣中的另一臺服務器,客戶端會將未發送的監視點列表和最新的事務ID發給服務端。
- 刪除WatcherApi
六:Watcher機制的整體流程
七:Watcher使用的注意事項
- Watcher是一次觸發器,假如需要持續監聽數據變更,需要在每次獲取時設置Watcher
- 會話過期:當客戶端會話過期時,該客戶端註冊的Watcher會失效
- 事件丟失:在接收通知和註冊監視點之間,可能會丟失事件,但Zookeeper的狀態變更和數據變化,都會記錄在狀態元數據信息和ZK數據節點上,所以能夠獲取最終一致的ZK信息狀態
- 避免Watcher過多:服務器會對每一個註冊Watcher事件的客戶端發送通知,通知通過Socket連接的方式發送,當Watcher過多時,會產生一個尖峯的通知
本文對Zookeeper的Watcher機制做了一個簡單介紹,目前還在學習的過程中,如有問題歡迎指正。