在SOA架構設計中。系統對於業務邏輯複用的需求十分強烈,上層業務都想借用已有的底層服務,來快速搭建更多,更豐富的業務。從而下降新業務開展的人力和時間成本,已快速知足瞬息萬變的市場需求。而公共的業務被拆分出來,造成可共用的服務,最大程度地保障了代碼和邏輯的複用,避免重複建設。所以,服務消費者要經過服務名稱,根據服務的路由,在衆多服務中找到要調用的服務的地址列表:node
大體流程圖以下所示:算法
服務消費者 服務列表 地址列表 service1---> server_address1 service2 server_address2 consumer --->service3 server_address3 service4 ....
而對於負載較高的服務來講,每每對應着多臺主機組成的服務集羣。在請求到來時,爲了將請求均衡地分配到後端服務器,負載均衡程序將從服務對應的地址列表中,經過負載均衡算法和規則,選出一臺最佳的服務器進行訪問,這個過程稱爲負載均衡。數據庫
調用者 負載均衡算法 地址列表 round_robin server_address1 consumer --->random ---> server_address2 weight_random server_address3 .... ....
服務規模較小,能夠採用硬編碼的方法將服務地址和配置寫在代碼中,提升編碼解決服務的路由和負載均衡問題,也能夠經過軟硬件負載均衡設備LVS,Nginx等經過相關配置來解決服務的路由和負載均衡問題。
當服務的機器數量在可控範圍內,上述維護成功較低,配置也簡單明瞭,但當服務愈來愈多,規模愈來愈大時,機器數量愈來愈龐大,靠上述維護方案將會愈來愈困難而且單點故障的問題也開始凸顯,一旦服務路由或者負載均衡服務器宕機,全部依賴於它的服務所有不可用。apache
service1--->負載均衡算法-->xxxx(集羣) consumer --->service2--->負載均衡算法-->xxxx(集羣) service3--->負載均衡算法-->xxxx(集羣)
此時須要一個可以動態註冊和獲取服務信息的地方,來統一管理服務名稱和其對應的服務器列表信息,稱之爲服務配置中心。當服務提供者在啓動時,將其提供的服務名稱,服務地址註冊到服務配置中心。服務消費者經過服務配置中心來得到須要調用的服務的機器列表,經過負載均衡算法。選出其中一臺服務器進行調用。當服務器宕機或者下線時,相應的機器須要可以動態的從服務配置中心中移除。並通知相應的服務消費者,不然服務消費者就有可能由於調用到已失效的服務而發生錯誤。在這個過程當中,服務消費者只有第一次調用服務時須要查詢服務配置中心而後將查詢到的信息緩存到本地,後面的調用直接使用本地緩存的服務地址列表信息,而不須要從新發起請求到服務配置中心去獲取相應的服務地址列表。直到服務的地址有變動(機器上線和下線)。這種無中心化的結構解決了以前負載均衡設備所致使的單點故障問題而且大大減輕了服務配置中心的壓力。後端
基於ZooKeeper的持久和非持久的特色,咱們可以近乎實時地感受到後端服務的狀態(上線,下線,宕機)經過其集羣間的zab協議,使得服務配置信息可以保持一致。而zk自己的容錯特性和選舉機制可以保障咱們方便的進行擴容。經過zk來實現服務的動態註冊,機器上線和下線的動態感知,擴容方便容錯性好。且無中心化結構可以解決以前使用負載均衡設備所帶來的單機故障問題,只有當配置信息更新時纔回去zk上獲取最新的服務地址列表,其餘的時候使用本地緩存。設計模式
瞭解了zk以後,咱們來看看zk的使用
關於zk的單機模式安裝與大部分軟件同樣,下載,解壓,修改環境變量。這裏不浪費篇幅了,也不浪費篇幅來寫配置文件的含義了。只介紹幾個zk的關鍵特性和典型的應用場景:緩存
Zookeeper 分佈式服務框架是 Apache Hadoop 的一個子項目,它主要是用來解決分佈式應用中常常遇到的一些數據管理問題,如:統一命名服務、狀態同步服務、集羣管理、分佈式應用配置項的管理等。服務器
Zookeeper 會維護一個具備層次關係的數據結構,它很是相似於一個標準的文件系統,如圖所示:session
Zookeeper 這種數據結構有以下這些特色:數據結構
- 每一個子目錄項如 NameService 都被稱做爲 znode,這個 znode 是被它所在的路徑惟一標識,如 Server1 這個 znode 的標識爲 /NameService/Server1
- znode 能夠有子節點目錄,而且每一個 znode 能夠存儲數據,注意 EPHEMERAL 類型的目錄節點不能有子節點目錄
- znode 是有版本的,每一個 znode 中存儲的數據能夠有多個版本,也就是一個訪問路徑中能夠存儲多份數據
- znode 能夠是臨時節點,一旦建立這個 znode 的客戶端與服務器失去聯繫,這個 znode 也將自動刪除,Zookeeper 的客戶端和服務器通訊採用長鏈接方式,每一個客戶端和服務器經過心跳來保持鏈接,這個鏈接狀態稱爲 session,若是 znode 是臨時節點,這個 session 失效,znode 也就刪除了
- znode 的目錄名能夠自動編號,如 App1 已經存在,再建立的話,將會自動命名爲 App2
- znode 能夠被監控,包括這個目錄節點中存儲的數據的修改,子節點目錄的變化等,一旦變化能夠通知設置監控的客戶端,這個是 Zookeeper 的核心特性,Zookeeper 的不少功能都是基於這個特性實現的,後面在典型的應用場景中會有實例介紹
Zookeeper 做爲一個分佈式的服務框架,主要用來解決分佈式集羣中應用系統的一致性問題,它能提供基於相似於文件系統的目錄節點樹方式的數據存儲,可是 Zookeeper 並非用來專門存儲數據的,它的做用主要是用來維護和監控你存儲的數據的狀態變化。經過監控這些數據狀態的變化,從而能夠達到基於數據的集羣管理,後面將會詳細介紹 Zookeeper 可以解決的一些典型問題,這裏先介紹一下,Zookeeper 的操做接口和簡單使用示例。
基本操做
客戶端要鏈接 Zookeeper 服務器能夠經過建立 org.apache.zookeeper. ZooKeeper 的一個實例對象,而後調用這個類提供的接口來和服務器交互。
前面說了* ZooKeeper 主要是用來維護和監控一個目錄節點樹中存儲的數據的狀態,全部咱們可以操做 ZooKeeper 的也和操做目錄節點樹大致同樣,如建立一個目錄節點,給某個目錄節點設置數據,獲取某個目錄節點的全部子目錄節點,給某個目錄節點設置權限和監控這個目錄節點的狀態變化。*
public class App { public static void main(String[] args) { try { // 建立一個與服務器的鏈接 // 參數三 watch 參數4 canBeReadOnly ZooKeeper zk = new ZooKeeper("localhost:2181", 2000, null, false); // 建立一個目錄節點 /* * 參數1 :節點路徑 參數2 :節點數據 參數3 :訪問權限 Ids.OPEN_ACL_UNSAFE表示無權限 參數4 :節點類型 * CreateMode.PERSISTENT持久節點,當該客戶端斷開連接時不會刪除節點 */ zk.create("/testRootPath", "testRootData".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); // 建立一個子目錄節點 zk.create("/testRootPath/testChildPathOne", "testChildDataOne".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); System.out.println(new String(zk.getData("/testRootPath", false, null))); // 取出子目錄節點列表 System.out.println(zk.getChildren("/testRootPath", true)); // 修改子目錄節點數據 zk.setData("/testRootPath/testChildPathOne", "modifyChildDataOne".getBytes(), -1); System.out.println("目錄節點狀態:[" + zk.exists("/testRootPath", true) + "]"); // 建立另一個子目錄節點 zk.create("/testRootPath/testChildPathTwo", "testChildDataTwo".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); System.out.println(new String(zk.getData("/testRootPath/testChildPathTwo", true, null))); /* * // 刪除子目錄節點 zk.delete("/testRootPath/testChildPathTwo",-1); * zk.delete("/testRootPath/testChildPathOne",-1); // 刪除父目錄節點 * zk.delete("/testRootPath",-1); // 關閉鏈接 zk.close(); */ } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
對於生成ZooKeeper的實例,最後一個參數能夠指定一個Watch:
ZooKeeper zk = new ZooKeeper("localhost:" + CLIENT_PORT, ClientBase.CONNECTION_TIMEOUT, new Watcher() { // 監控全部被觸發的事件 public void process(WatchedEvent event) { System.out.println("已經觸發了" + event.getType() + "事件!"); } });
當對目錄節點監控狀態打開時,一旦目錄節點的狀態發生變化,Watcher 對象的 process 方法就會被調用。
Zookeeper 從設計模式角度來看,是一個基於觀察者模式設計的分佈式服務管理框架,它負責存儲和管理你們都關心的數據,而後接受觀察者的註冊,一旦這些數據的狀態發生變化,Zookeeper 就將負責通知已經在 Zookeeper 上註冊的那些觀察者作出相應的反應,從而實現集羣中相似 Master/Slave 管理模式。
下面詳細介紹這些典型的應用場景,也就是 Zookeeper 到底能幫咱們解決那些問題?下面將給出答案。
分佈式應用中,一般須要有一套完整的命名規則,既可以產生惟一的名稱又便於人識別和記住,一般狀況下用樹形的名稱結構是一個理想的選擇,樹形的名稱結構是一個有層次的目錄結構,既對人友好又不會重複。說到這裏你可能想到了 JNDI,沒錯 Zookeeper 的 Name Service 與 JNDI 可以完成的功能是差很少的,它們都是將有層次的目錄結構關聯到必定資源上,可是 Zookeeper 的 Name Service 更加是普遍意義上的關聯,也許你並不須要將名稱關聯到特定資源上,你可能只須要一個不會重複名稱,就像數據庫中產生一個惟一的數字主鍵同樣。
Name Service 已是 Zookeeper 內置的功能,你只要調用 Zookeeper 的 API 就能實現。如調用 create 接口就能夠很容易建立一個目錄節點。
配置的管理在分佈式應用環境中很常見,例如同一個應用系統須要多臺 PC Server 運行,可是它們運行的應用系統的某些配置項是相同的,若是要修改這些相同的配置項,那麼就必須同時修改每臺運行這個應用系統的 PC Server,這樣很是麻煩並且容易出錯。
像這樣的配置信息徹底能夠交給 Zookeeper 來管理,將配置信息保存在 Zookeeper 的某個目錄節點中,而後將全部須要修改的應用機器監控配置信息的狀態,一旦配置信息發生變化,每臺應用機器就會收到 Zookeeper 的通知,而後從 Zookeeper 獲取新的配置信息應用到系統中。
集羣管理(Group Membership)
Zookeeper 可以很容易的實現集羣管理的功能,若有多臺 Server 組成一個服務集羣,那麼必需要一個「總管」知道當前集羣中每臺機器的服務狀態,一旦有機器不能提供服務,集羣中其它集羣必須知道,從而作出調整從新分配服務策略。一樣當增長集羣的服務能力時,就會增長一臺或多臺 Server,一樣也必須讓「總管」知道。
Zookeeper 不只可以幫你維護當前的集羣中機器的服務狀態,並且可以幫你選出一個「總管」,讓這個總管來管理集羣,這就是 Zookeeper 的另外一個功能 Leader Election。
它們的實現方式都是在 Zookeeper 上建立一個 EPHEMERAL 類型的目錄節點,而後每一個 Server 在它們建立目錄節點的父目錄節點上調用 getChildren(String path, boolean watch) 方法並設置 watch 爲 true,因爲是 EPHEMERAL 目錄節點,當建立它的 Server 死去,這個目錄節點也隨之被刪除,因此 Children 將會變化,這時 getChildren上的 Watch 將會被調用,因此其它 Server 就知道已經有某臺 Server 死去了。新增 Server 也是一樣的原理。
Zookeeper 如何實現 Leader Election,也就是選出一個 Master Server。和前面的同樣每臺 Server 建立一個 EPHEMERAL 目錄節點,不一樣的是它仍是一個 SEQUENTIAL 目錄節點,因此它是個 EPHEMERAL_SEQUENTIAL 目錄節點。之因此它是 EPHEMERAL_SEQUENTIAL 目錄節點,是由於咱們能夠給每臺 Server 編號,咱們能夠選擇當前是最小編號的 Server 爲 Master,假如這個最小編號的 Server 死去,因爲是 EPHEMERAL 節點,死去的 Server 對應的節點也被刪除,因此當前的節點列表中又出現一個最小編號的節點,咱們就選擇這個節點爲當前 Master。這樣就實現了動態選擇 Master,避免了傳統意義上單 Master 容易出現單點故障的問題。
Leader Election 關鍵代碼:
void findLeader() throws InterruptedException { byte[] leader = null; try { leader = zk.getData(root + "/leader", true, null); } catch (Exception e) { logger.error(e); } if (leader != null) { following(); } else { String newLeader = null; try { byte[] localhost = InetAddress.getLocalHost().getAddress(); newLeader = zk.create(root + "/leader", localhost, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); } catch (Exception e) { logger.error(e); } if (newLeader != null) { leading(); } else { mutex.wait(); } } }
共享鎖在同一個進程中很容易實現,可是在跨進程或者在不一樣 Server 之間就很差實現了。Zookeeper 卻很容易實現這個功能,實現方式也是須要得到鎖的 Server 建立一個 EPHEMERAL_SEQUENTIAL 目錄節點,而後調用 getChildren方法獲取當前的目錄節點列表中最小的目錄節點是否是就是本身建立的目錄節點,若是正是本身建立的,那麼它就得到了這個鎖,若是不是那麼它就調用 exists(String path, boolean watch) 方法並監控 Zookeeper 上目錄節點列表的變化,一直到本身建立的節點是列表中最小編號的目錄節點,從而得到鎖,釋放鎖很簡單,只要刪除前面它本身所建立的目錄節點就好了。
同步鎖的關鍵代碼
void getLock() throws KeeperException, InterruptedException{ List<String> list = zk.getChildren(root, false); String[] nodes = list.toArray(new String[list.size()]); Arrays.sort(nodes); if(myZnode.equals(root+"/"+nodes[0])){ doAction(); } else{ waitForLock(nodes[0]); } } void waitForLock(String lower) throws InterruptedException, KeeperException { Stat stat = zk.exists(root + "/" + lower,true); if(stat != null){ mutex.wait(); } else{ getLock(); } }
Zookeeper 能夠處理兩種類型的隊列:
- 當一個隊列的成員都聚齊時,這個隊列纔可用,不然一直等待全部成員到達,這種是同步隊列。
- 隊列按照 FIFO 方式進行入隊和出隊操做,例如實現生產者和消費者模型。
同步隊列用 Zookeeper 實現的實現思路以下:
建立一個父目錄 /synchronizing,每一個成員都監控標誌(Set Watch)位目錄 /synchronizing/start 是否存在,而後每一個成員都加入這個隊列,加入隊列的方式就是建立 /synchronizing/member_i 的臨時目錄節點,而後每一個成員獲取 / synchronizing 目錄的全部目錄節點,也就是 member_i。判斷 i 的值是否已是成員的個數,若是小於成員個數等待 /synchronizing/start 的出現,若是已經相等就建立 /synchronizing/start。
同步隊列
void addQueue() throws KeeperException, InterruptedException{ zk.exists(root + "/start",true); zk.create(root + "/" + name, new byte[0], Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); synchronized (mutex) { List<String> list = zk.getChildren(root, false); if (list.size() < size) { mutex.wait(); } else { zk.create(root + "/start", new byte[0], Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); } } }
當隊列沒盡是進入 wait(),而後會一直等待 Watch 的通知,Watch 的代碼以下:
public void process(WatchedEvent event) { if(event.getPath().equals(root + "/start") && event.getType() == Event.EventType.NodeCreated){ System.out.println("獲得通知"); super.process(event); doAction(); } }
FIFO 隊列用 Zookeeper 實現思路以下:
實現的思路也很是簡單,就是在特定的目錄下建立 SEQUENTIAL 類型的子目錄 /queue_i,這樣就能保證全部成員加入隊列時都是有編號的,出隊列時經過 getChildren( ) 方法能夠返回當前全部的隊列中的元素,而後消費其中最小的一個,這樣就能保證 FIFO。
下面是生產者和消費者這種隊列形式的示例代碼:
生產者代碼
boolean produce(int i) throws KeeperException, InterruptedException{ ByteBuffer b = ByteBuffer.allocate(4); byte[] value; b.putInt(i); value = b.array(); zk.create(root + "/element", value, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL); return true; }
消費者代碼
int consume() throws KeeperException, InterruptedException{ int retvalue = -1; Stat stat = null; while (true) { synchronized (mutex) { List<String> list = zk.getChildren(root, true); if (list.size() == 0) { mutex.wait(); } else { Integer min = new Integer(list.get(0).substring(7)); for(String s : list){ Integer tempValue = new Integer(s.substring(7)); if(tempValue < min) min = tempValue; } byte[] b = zk.getData(root + "/element" + min,false, stat); zk.delete(root + "/element" + min, 0); ByteBuffer buffer = ByteBuffer.wrap(b); retvalue = buffer.getInt(); return retvalue; } } } }
Zookeeper 做爲 Hadoop 項目中的一個子項目,是 Hadoop 集羣管理的一個必不可少的模塊,它主要用來控制集羣中的數據,如它管理 Hadoop 集羣中的 NameNode,還有 Hbase 中 Master Election、Server 之間狀態同步等。 本文介紹的 Zookeeper 的基本知識,以及介紹了幾個典型的應用場景。這些都是 Zookeeper 的基本功能,最重要的是 Zoopkeeper 提供了一套很好的分佈式集羣管理的機制,就是它這種基於層次型的目錄樹的數據結構,並對樹中的節點進行有效管理,從而能夠設計出多種多樣的分佈式的數據管理模型,而不只僅侷限於上面提到的幾個經常使用應用場景。