品味ZooKeeper之Watcher機制

本文思維導圖如下:


image

前言

Watcher機制是zookeeper最重要三大特性數據節點Znode+Watcher機制+ACL權限控制中的其中一個,它是zk很多應用場景的一個前提,比如集群管理、集群配置、發布/訂閱。

Watcher機制涉及到客戶端與服務器(注意,不止一個機器,一般是集群,這里先認為一個整體分析)的兩者數據通信與消息通信,除此之外還涉及到客戶端的watchManager。

下面正式進入主題。

1.watcher原理框架

image

由圖看出,zk的watcher由客戶端,客戶端WatchManager,zk服務器組成。整個過程涉及了消息通信及數據存儲。

  • zk客戶端向zk服務器注冊watcher的同時,會將watcher對象存儲在客戶端的watchManager。
  • Zk服務器觸發watcher事件后,會向客戶端發送通知,客戶端線程從watchManager中回調watcher執行相應的功能。

注意的是server服務器端一般有多臺共同一起對外提供服務的,里面涉及到zk專有的ZAB協議(分布式原子廣播協議)。在這先不分析,后面會有單獨一文來介紹,因為ZAB協議是zookeeper的實現精髓,有了zab協議才能使zk真正落地,真正的高可靠,數據同步,適于商用。

image

有木有看到小紅旗?加入小紅旗是一個watcher,當小紅旗被創建并注冊到node1節點(會有相應的API實現)后,就會監聽node1+node_a+node_b或node_a+node_b。這里兩種情況是因為在創建watcher注冊時會有多種途徑。并且watcher不能監聽到孫節點。注意注意注意,watcher設置后,一旦觸發一次后就會失效,如果要想一直監聽,需要在process回調函數里重新注冊相同的 watcher

2.通知狀態與事件

public class WatcherTest implements Watcher {
    @Override
    public void process(WatchedEvent event) {
        // TODO Auto-generated method stub
        WatcherTest  w = new WatcherTest();
        ZooKeeper zk = new ZooKeeper(wx.getZkpath(),10000, w);  
    }
    
    public static void main(String[] args){
        WatcherTest  w = new WatcherTest();
        ZooKeeper   zk = new ZooKeeper(wx.getZkpath(), 10000, w);
    }
}

上面例子是把異常處理,邏輯處理等都省掉。watcher的應用很簡單,主要有兩步:繼承 Watcher 接口,重寫 process 回調函數。

當然注冊方式有很多,有默認和重新覆蓋方式,可以一次觸發失效也可以一直有效觸發。這些都可以通過代碼實現。

2.1 KeeperStatus通知狀態

KeeperStatus完整的類名是org.apache.zookeeper.Watcher.Event.KeeperState

2.2 EventType事件類型

EventType完整的類名是org.apache.zookeeper.Watcher.Event.EventType

image

此圖是zookeeper常用的通知狀態與對應事件類型的對應關系。除了客戶端與服務器連接狀態下,有多種事件的變化,其他狀態的事件都是None。這也是符合邏輯的,因為沒有連接服務器肯定不能獲取獲取到當前的狀態,也就無法發送對應的事件類型了。

這里重點說下幾個重要而且容易迷惑的事件:

  • NodeDataChanged事件
  • 無論節點數據發生變化還是數據版本發生變化都會觸發
  • 即使被更新數據與新數據一樣,數據版本dataVersion都會發生變化
  • NodeChildrenChanged
  • 新增節點或者刪除節點
  • AuthFailed
  • 重點是客戶端會話沒有權限而是授權失敗

客戶端只能收到服務器發過來的相關事件通知,并不能獲取到對應數據節點的原始數據及變更后的新數據。因此,如果業務需要知道變更前的數據或者變更后的新數據,需要業務保存變更前的數據(本機數據結構、文件等)和調用接口獲取新的數據

3.watcher注冊過程

3.1涉及接口

創建zk客戶端對象實例時注冊:

ZooKeeper(String connectString, int sessionTimeout, Watcher watcher)
ZooKeeper(String connectString, int sessionTimeout, Watcher watcher, boolean 
canBeReadOnly)
ZooKeeper(String connectString, int sessionTimeout, Watcher watcher, long sessionId, byte[] sessionPasswd)
ZooKeeper(String connectString, int sessionTimeout, Watcher watcher, long sessionId, byte[] sessionPasswd, boolean canBeReadOnly)

通過這種方式注冊的watcher將會作為整個zk會話期間的默認watcher,會一直被保存在客戶端ZK WatchManagerdefaultWatcher 中,如果這個被創建的節點在其它時候被創建watcher并注冊,則這個默認的watcher會被覆蓋。注意注意注意,watcher觸發一次就會失效,不管是創建節點時的 watcher 還是以后創建的 watcher

其他注冊watcher的API:

  • getChildren(String path, Watcher watcher)
  • getChildren(String path, boolean watch)
  • Boolean watch表示是否使用上下文中默認的watcher,即創建zk實例時設置的watcher
  • getData(String path, boolean watch, Stat stat)
  • Boolean watch表示是否使用上下文默認的watcher,即創建zk實例時設置的watcher
  • getData(String path, Watcher watcher, AsyncCallback.DataCallback cb, Object ctx)
  • exists(String path, boolean watch)
  • Boolean watch表示是否使用上下文中默認的watcher,即創建zk實例時設置的watcher
  • exists(String path, Watcher watcher)

舉栗子

image

image

image

image

image

這就是watcher的簡單例子,zk的實際應用集群管理,發布訂閱等復雜功能其實就在這個小例子上拓展的。

3.2客戶端注冊

image

這里的客戶端注冊主要是把上面第一點的zookeeper原理框架的注冊步驟展開,簡單來說就是zk客戶端在注冊時會先向zk服務器請求注冊,服務器會返回請求響應,如果響應成功則zk服務端把watcher對象放到客戶端的WatchManager管理并返回響應給客戶端。

3.3服務器端注冊

image
FinalRequestProcessor
/**
 * This Request processor actually applies any transaction associated with a
 * request and services any queries. It is always at the end of a
 * RequestProcessor chain (hence the name), so it does not have a nextProcessor
 * member.
 *
 * This RequestProcessor counts on ZooKeeperServer to populate the
 * outstandingRequests member of ZooKeeperServer.
 */
public class FinalRequestProcessor implements RequestProcessor

由源碼注釋得知,FinalRequestProcessor類實際是任何事務請求和任何查詢的的最終處理類。也就是我們客戶端對節點的set/get/delete/create/exists等操作最終都會運行到這里。

以exists函數為例子:

case OpCode.exists: {
    lastOp = "EXIS";
    // TODO we need to figure out the security requirement for this!
    ExistsRequest existsRequest = new ExistsRequest();
    ByteBufferInputStream.byteBuffer2Record(request.request,
    existsRequest);
    String path = existsRequest.getPath();
    if (path.indexOf('\0') != -1) {
    throw new KeeperException.BadArgumentsException();
    }
    Stat stat = zks.getZKDatabase().statNode(path, existsRequest
    .getWatch() ? cnxn : null);
    rsp = new ExistsResponse(stat);
    break;
}

existsRequest.getWatch() ? cnxn : null此句是在調用exists API時,判斷是否注冊watcher,若是就返回 cnxncnxn是由此句代碼ServerCnxn cnxn = request.cnxn;創建的。

/**
 * Interface to a Server connection - represents a connection from a client
 * to the server.
 */
public abstract class ServerCnxn implements Stats, Watcher

通過ServerCnxn類的源碼注釋得知,ServerCnxn是維持服務器與客戶端的tcp連接與實現了 watcher。總的來說,ServerCnxn類創建的對象cnxn即包含了連接信息又包含watcher信息。

同時仔細看ServerCnxn類里面的源碼,發現有以下這個函數,process函數正是watcher的回調函數啊。

public abstract class ServerCnxn implements Stats, Watcher {
    .
    .
    public abstract void process(WatchedEvent event);
    Stat stat = zks.getZKDatabase().statNode(path, existsRequest.getWatch() ? cnxn : null); 
    //getZKDatabase實際上是獲取是在zookeeper運行時的數據庫。請看下面
    .
    .
}
ZKDatabase
/**
 * This class maintains the in memory database of zookeeper
 * server states that includes the sessions, datatree and the
 * committed logs. It is booted up  after reading the logs
 * and snapshots from the disk.
 */
public class ZKDatabase

通過源碼注釋得知ZKDatabase是在zookeeper運行時的數據庫,在FinalRequestProcessor的case exists中會把existsRequest(exists請求傳遞給ZKDatabase)。

/**
 * the datatree for this zkdatabase
 * @return the datatree for this zkdatabase
 */
public DataTree getDataTree() {
return this.dataTree;
}

ZKDatabase里面有這關鍵的一個函數是從zookeeper運行時展開的節點數型結構中搜索到合適的節點返回。

watchManager
  • Zk服務器端Watcher的管理者

  • 從兩個維度維護watcher

  • watchTable從數據節點的粒度來維護

  • watch2Paths從watcher的粒度來維護

  • 負責watcher事件的觸發

      class WatchManager {
          private final Map<String, Set<Watcher>> watchTable =
          new HashMap<String, Set<Watcher>>();
          
          private final Map<Watcher, Set<String>> watch2Paths = new HashMap<Watcher, Set<String>>();
          Set<Watcher> triggerWatch(String path, EventType type) { return triggerWatch(path, type, null);}
      }
    
watcher觸發
public Stat setData(String path, byte data[], int version, long zxid,long time) throws KeeperException.NoNodeException {
    Stat s = new Stat();
    DataNode n = nodes.get(path);
    if (n == null) {
        throw new KeeperException.NoNodeException();
    }
    byte lastdata[] = null;
    synchronized (n) {
        lastdata = n.data;
        n.data = data;
        n.stat.setMtime(time);
        n.stat.setMzxid(zxid);
        n.stat.setVersion(version);
        n.copyStat(s);
    }
    // now update if the path is in a quota subtree.
    String lastPrefix = getMaxPrefixWithQuota(path);
    if(lastPrefix != null) {
      this.updateBytes(lastPrefix, (data == null ? 0 : data.length)
      - (lastdata == null ? 0 : lastdata.length));
    }
    dataWatches.triggerWatch(path, EventType.NodeDataChanged); //觸發事件
    return s;
}

客戶端回調watcher步驟:

  • 反序列化,將孒節流轉換成WatcherEvent對象。因為在Java中網絡傳輸肯定是使用了序列化的,主要是為了節省網絡IO和提高傳輸效率。
  • 處理chrootPath。獲取節點的根節點路徑,然后再搜索樹而已。
  • 還原watchedEvent:把WatcherEvent對象轉換成WatchedEvent。主要是把zk服務器那邊的WatchedEvent事件變為WatcherEvent,標為已watch觸發。
  • 回調Watcher:把WatchedEvent對象交給EventThread線程。EventThread線程主要是負責從客戶端的ZKWatchManager中取出Watcher,并放入waitingEvents隊列中,然后供客戶端獲取。

4.小結

到此,zookeeper的watcher機制基本告一段落了,watcher機制主要是客戶端、zk服務器和watchManager三者的協調合作完成的。這里只分析了watcher的內容,例如涉及到的ZAB協議等沒有分析,準備把它放在下下文中,下文是zookeeper的ACL訪問控制權限。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,157評論 2 375

推薦閱讀更多精彩內容