zookeeper簡單介紹及API使用 1.1 zookeeper簡介 zookeeper是一個針對大型分散式系統的可靠的協調系統,提供的功能包括配置維護、名字服務、分散式同步、組服務等。zookeeper可以集群複製,集群間通過zab協議來保持數據的一致性。該協議包括兩個階段:leader ele ...
zookeeper簡單介紹及API使用
1.1 zookeeper簡介
zookeeper是一個針對大型分散式系統的可靠的協調系統,提供的功能包括配置維護、名字服務、分散式同步、組服務等。zookeeper可以集群複製,集群間通過zab協議來保持數據的一致性。該協議包括兩個階段:leader election階段和Atomic broadcas階段。
leader election階段:集群間選舉出一個leader,其他的機器則稱為follower,所有的寫操作都被傳送給leader,並通過broadcas將所有的更新告訴follower,當leader崩潰或leader失去大多數的follower時,需要重新選舉出一個新的leader,讓所有的伺服器都恢復到一個正確的狀態。當leader被選舉出來且大多數伺服器完成了和leader的狀態同步後,leader election過程結束,進入Atomic broadcas階段。
Atomic broadcas階段:Atomic broadcas同步leader和follower之間的信息,保證二者具有相同的系統狀態。
zookeeper的協作過程簡化了鬆散耦合系統之間的交互,即使參與者彼此不知道對方的存在,也能夠相互發現並且完成交互。
1.2 zookeeper API簡單使用
可以認為zookeeper是一個小型的、精簡的文件系統,它的每個節點稱為znode,znode除了本身能夠包含一部分數據之外,還能擁有子節點,當節點或子節點數據發生變化時,基於watcher機制,會發出相應的通知給訂閱其狀態變化的客戶端。
1.2.1 zookeeper節點創建
maven項目中引入模塊:
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.6</version>
</dependency>
創建zookeeper對象和節點:
1 public static void main(String[] args) throws Exception { 2 /* 3 * 127.0.0.1:2181:伺服器地址 4 * 10:超時時間 5 * watcher:若包含boolean watch的讀方法中傳入true,則將預設watcher註冊為所關註事件的watcher 6 * 若傳入false,則不註冊任何watcher。此處暫且定為空 7 */ 8 ZooKeeper zookeeper = new ZooKeeper("127.0.0.1:2181", 10, null); 9 /* 10 * 若創建的節點已經存在,則會拋出異常 11 * /root:節點路徑 ; root data:路徑包含的位元組數據 12 * Ids.OPEN_ACL_UNSAFE:訪問許可權 13 * CreateMode.PERSISTENT:節點類型 14 */ 15 zookeeper.create("/root", "root data".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); 16 /* 17 * 設置節點數據 18 * -1:版本號;若匹配不到響應的節點則會拋出異常 19 */ 20 zookeeper.setData("/root", "hello".getBytes(), -1); 21 /* 22 * 讀取節點數據 23 * stat是節點狀態參數,讀取時會傳出該節點當前狀態信息 24 */ 25 Stat stat = new Stat(); 26 byte[] data = zookeeper.getData("/root", false, stat); 27 System.out.println(new String(data)); 28 /* 29 * 添加子節點,若父節點不存在會拋出異常 30 */ 31 zookeeper.create("/root/child", "child data".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); 32 /* 33 * 判斷節點是否存在,不存在則返回的stat為null 34 */ 35 Stat existsStat = zookeeper.exists("/root/child", false); 36 System.out.println(existsStat); 37 /* 38 * /root/child:刪除節點路徑 39 * -1:節點的版本號;若設置為-1,則匹配所有版本,zookeeper會比較刪除的版本和伺服器版本是否一致,不一致會拋出異常 40 */ 41 zookeeper.delete("/root/child", -1); 42 }
實際運行中最常出現這個錯誤:
Exception in thread "main" org.apache.zookeeper.KeeperException$ConnectionLossException: KeeperErrorCode = ConnectionLoss for /root
at org.apache.zookeeper.KeeperException.create(KeeperException.java:90)
at org.apache.zookeeper.KeeperException.create(KeeperException.java:42)
at org.apache.zookeeper.ZooKeeper.create(ZooKeeper.java:643)
at com.project.soa.zookeeper.ZookeeperDemo.main(ZookeeperDemo.java:12)
這是因為還未連接上zookeeper就開始添加、刪除節點等操作,為避免這種情況發生,可以在做操作時對連接狀態做判斷:
1 ZooKeeper zookeeper = new ZooKeeper("127.0.0.1:2181", 10, null); 2 if (zookeeper.getState() == States.CONNECTED) { 3 4 }
1.2.2 watcher的實現
節點的狀態發生變化,可以通過zookeeper的watcher機制讓客戶端取得通知。watcher的實現較為簡單,只需實現org.apache.ZooKeeper.Watcher介面即可,其中節點的狀態變化包含以下幾種狀態:
註意:watcher機制是一次性的,每次處理完狀態變化事件之後需重新註冊watcher。這也導致在處理事件和重新加上watcher這段時間發生的節點狀態無法被感知。
1.2.3 zkClient的使用
zkClient解決了watcher的一次性註冊問題,將znode的事件重新定義為子節點的變化、數據的變化、連接及狀態的變化三類,watcher執行後重新讀取數據的同時再註冊相同的watcher。在異常發生時zkClient會自動創建新的zookeeper實例進行重連,此時原來的watcher節點都將失效,可在zkClient定義的連接狀態變化的介面中進行相應處理。同時zkClient還提供了序列化和反序列化介面ZkSerializer,簡化了znode上對象的存儲。
maven中引入zkClient模塊:
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
<version>0.1</version>
</dependency>
簡單事例:
1 public static void main(String[] args) { 2 ZkClient zkClient = new ZkClient("192.168.146.132:2181"); 3 String path = "/root"; 4 zkClient.createPersistent(path); 5 zkClient.create(path + "/child", "znode child", CreateMode.EPHEMERAL); 6 List<String> children = zkClient.getChildren(path); 7 System.out.println(children); 8 int countChildren = zkClient.countChildren(path); 9 System.out.println(countChildren); 10 System.out.println(zkClient.exists(path)); 11 zkClient.writeData(path + "/child", "hello everyone"); 12 Object data = zkClient.readData(path + "/child"); 13 System.out.println(data); 14 zkClient.delete(path + "/child"); 15 16 //訂閱數據的變化 17 zkClient.subscribeDataChanges(path, new IZkDataListener() { 18 19 public void handleDataDeleted(String arg0) throws Exception { 20 21 } 22 23 public void handleDataChange(String arg0, Object arg1) throws Exception { 24 25 } 26 }); 27 28 //訂閱子節點的變化 29 zkClient.subscribeChildChanges(path, new IZkChildListener() { 30 31 public void handleChildChange(String arg0, List<String> arg1) throws Exception { 32 33 } 34 }); 35 36 zkClient.subscribeStateChanges(new IZkStateListener() { 37 38 public void handleStateChanged(KeeperState arg0) throws Exception { 39 40 } 41 42 public void handleNewSession() throws Exception { 43 // 在這裡可以進行異常發生時節點失效的容錯處理 44 45 } 46 }); 47 }
1.2.4 路由和負載均衡
當服務規模變大時,服務之間的依賴變得十分複雜,這時我們不僅需要瞭解服務提供方,還需要瞭解服務消費方以瞭解服務的調用情況,可以以此作為服務擴容或下線的依據。
服務消費者獲取服務提供者地址列表的部分代碼為:
1 List<String> serverList; 2 3 public List<String> getServerList() { 4 serverList = new ArrayList<String>(); 5 String serviceName = "server - A"; 6 String serviceString = "127.0.0.1:2181"; 7 String path = "/config/" + serviceName; 8 ZkClient zkClient = new ZkClient(serviceString); 9 if (zkClient.exists(path)) {//服務存在則取地址列表 10 serverList = zkClient.getChildren(path); 11 } else { 12 throw new RuntimeException(); 13 } 14 // 註冊監聽事件 15 zkClient.subscribeChildChanges(path, new IZkChildListener() { 16 17 public void handleChildChange(String s, List<String> list) throws Exception { 18 serverList = list; 19 } 20 }); 21 return serverList; 22 }
先取得服務上所註冊的包含服務提供者地址的子節點,取得伺服器地址列表後便可根據負載均衡演算法選取調用伺服器,伺服器列表還存在本地以降低網路開銷。註冊監聽器來感知伺服器上線、下線和宕機事件,若發生節點改動,則將監聽方法中取得的最新子節點賦給當前的serverList。
服務提供者向zookeeper註冊服務:
1 String path = "/config"; 2 String serverList = "127.0.0.1:2181"; 3 String serverName = "server"; 4 ZkClient zkClient = new ZkClient(serverList); 5 if (!zkClient.exists(path)) { 6 zkClient.createPersistent(path);//創建根節點 7 } 8 if (zkClient.exists(path + "/" + serverName)) { 9 zkClient.createPersistent(path + "/" + serverName);//創建服務節點 10 } 11 //註冊當前伺服器 12 InetAddress addr = InetAddress.getLocalHost(); 13 //取得本機ip 14 String ip = addr.getHostAddress().toString(); 15 //創建當前伺服器節點 16 zkClient.createPersistent(path + "/" + serverName + "/" + ip);
這樣只有當配置信息更新時服務消費者才會去獲取最新的服務地址列表,其他時候使用本地緩存即可,這樣能大大降低配置中心的壓力。
1.3 HTTP服務網關
移動互聯網的崛起出現了多平臺的現狀,同樣的功能廠商需根據不同平臺開發不同的APP,使得開發成本增高。而由於客戶端APP、第三方ISV(獨立軟體開發商)應用都必須經過公共網路來發起客戶端請求,網關(gateway)作用得以凸顯。gateway接收外部各種APP的請求,經過一系列許可權與安全校驗等,根據服務名到對應配置中心選取伺服器列表,再由負載均衡演算法選取一臺伺服器進行調用,將結果返回給客戶端。
gateway可以攔截一系列惡意請求,而且能使不同的平臺共用重覆的邏輯,降低開發和運維成本。但由於gateway是整個網路的核心節點,一旦失效,依賴它的所有外部APP都將無法使用,因此在設計之初應該考慮到系統流量的監控和容量的規劃,以便在達到峰值時能夠快速進行系統擴容。
上圖是一種網關集群的架構方案,一組對等的伺服器組成網關集群接收外部HTTP請求,當流量達到警戒值,能方便地增加機器進行擴容。網關前有兩台負載均衡設備負責對網關集群進行負載均衡,設備間進行心跳檢測,一旦其中一臺宕機,另一臺則變更自己的地址接管宕機設備,平時這兩台機器均對外提供服務。