ZooKeeper解惑
今年年初的時候,寫了一篇ZooKeeper的入門文章《初識ZooKeeper》,一直到這一周,才有時間將ZooKeeper整個源碼通讀了一遍。不能說完全理解了ZooKeeper的工作原理與細節,但是之前心中一直關于ZooKeeper的疑問都得到了解釋。
現在網上關于ZooKeeper的文章很多,有介紹Leader選舉算法的,有介紹ZooKeeper Server內部原理的,還有介紹ZooKeeper Client的。本文不打算再寫類似的內容,而專注與解答讀者對ZooKeeper的相關疑問。
ZooKeeper在客戶端究竟做了什么事情
使用過ZooKeeper的讀者都知道,初始化客戶端的代碼如下:
System.out.println("Starting ZK:"); zk = new ZooKeeper(address, 3000, this); System.out.println("Finished starting ZK: " + zk);
完成客戶段的初始化之后,就可以對ZooKeeper進行相應的操作了:
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"); } }
雖然上面的代碼看起來簡單明了,但是ZooKeeper的客戶端在后臺默默做了許多事情:
1 與ZooKeeper服務端進行通信,包括:連接,發送消息,接受消息。
2 發送心跳信息,保持與ZooKeeper服務端的有效連接與Session的有效性。
3 錯誤處理,如果客戶端當前連接的ZooKeeper服務端失效,自動切換到另一臺有效的ZooKeeper服務端。
4 管理Watcher,處理異常調用和Watcher。
Watcher的事件通知機制是如何實現的
看過Google的分布式鎖機制Chubby論文會發現,ZooKeeper中多了一個事件訂閱機制:Watcher。那么Watcher內部究竟是如何實現的呢?其實,在ZooKeeper客戶端中,有一個成員變量(ZKWatchManager)專門負責管理所有的Watcher,當用戶使用如下代碼時:
List<String> list = zk.getChildren(path, watcher);
ZooKeeper會將這個Watcher存儲在ZKWatchManager中,同時通知ZooKeeper服務器記錄該Client對應的Session中的Path下注冊的事件類型。當ZooKeeper服務器發生了指定的事件后,ZooKeeper服務器將通知ZooKeeper客戶端,ZooKeeper客戶端再從ZKWatchManager中找到對應的回調函數,并予以執行。
整個過程中,客戶端存儲事件的信息和Watcher的執行邏輯,服務端只存儲事件的信息。
如何用好ZooKeeper客戶端
每實例化一個ZooKeeper客戶端,就開啟了一個Session。ZooKeeper客戶端是線程安全的,也可以認為它實現了連接池。
因此,每一個應用只需要實例化一個ZooKeeper客戶端即可,同一個ZooKeeper客戶端實例可以在不同的線程中使用。除非你想同一個應用中開啟多個Session,使用不同的Watcher,在這種情況下,才需要實例化多個ZooKeeper客戶端。
ZooKeeper是否對ZNode有大小限制
如果你仔細看過ZooKeeper的文檔,會發現文檔中對ZNode的大小做了限制,最大不能超過1M。這個1M的大小限制在ZooKeeper的客戶端和服務端都有限制:
客戶端:
packetLen = Integer.getInteger("jute.maxbuffer", 4096 * 1024); int len = incomingBuffer.getInt(); if (len < 0 || len >= packetLen) { throw new IOException("Packet len" + len + " is out of range!"); }
服務端:
static public final int maxBuffer = determineMaxBuffer(); private static int determineMaxBuffer() { String maxBufferString = System.getProperty("jute.maxbuffer"); try { return Integer.parseInt(maxBufferString); } catch(Exception e) { return 0xfffff; } } if (len < 0 || len > maxBuffer) { throw new IOException("Unreasonable length = " + len); }
可以看出,ZooKeeper確實對數據的大小有限制,默認就是1M,如果希望傳輸超過1M的數據,可以修改環境變量“jute.maxbuffer”即可。
為什么要限制ZooKeeper中ZNode的大小
ZooKeeper是一套高吞吐量的系統,為了提高系統的讀取速度,ZooKeeper不允許從文件中讀取需要的數據,而是直接從內存中查找。還句話說,ZooKeeper集群中每一臺服務器都包含全量的數據,并且這些數據都會加載到內存中。同時ZNode的數據并支持Append操作,全部都是Replace。
所以從上面分析可以看出,如果ZNode的過大,那么讀寫某一個ZNode將造成不確定的延時;同時ZNode過大,將過快地耗盡ZooKeeper服務器的內存。這也是為什么ZooKeeper不適合存儲大量的數據的原因。
如何提升ZooKeeper集群的性能
我們說性能,可以從兩個方面去考慮:寫入的性能與讀取的性能。由于ZooKeeper的寫入首先需要通過Leader,然后這個寫入的消息需要傳播到半數以上的Fellower通過才能完成整個寫入。所以整個集群寫入的性能無法通過增加服務器的數量達到目的,相反,整個集群中Fellower數量越多,整個集群寫入的性能越差。
ZooKeeper集群中的每一臺服務器都可以提供數據的讀取服務,所以整個集群中服務器的數量越多,讀取的性能就越好。但是Fellower增加又會降低整個集群的寫入性能。為了避免這個問題,可以將ZooKeeper集群中部分服務器指定為Observer。
更多關于ZooKeeper的文章請參考:http://www.cnblogs.com/gpcuster/tag/ZooKeeper/