Zookeeper:分布式程序的基石


一、目錄

1、zookeeper是什么?

2、安裝、配置、啟動、監控

3、javaApi基礎用法

4、應用場景

5、CAP理論/paxos算法 

二、zookeeper簡介

官方版:zookeeper是 一個分布式的,開放源碼的分布式應用程序協調服務,它包含一個簡單的原語集,分布式應用程序可以基於它實現同步服務,配置維護和命名服務等。

概括版:zookeeper是“一致、 有頭、數據樹”。

一致:數據一致性(核心)。例如,有一個1000台機器的集群,我想修改1000台機器上相同的配置文件,那么我們是一台一台去改嗎?顯然這種解決辦法並不可靠,所以把配置信息注冊到zookeeper,集群機器就成為了觀察者,當配置信息改變的時候,通知集群所有的機器改變。從而保證了集群的配置文件一致性。

有頭:有頭領。為什么要有頭領?例如,有1000太機器的集群,客戶端發送請求修改配置文件,那么誰去處理?有人可能說,我想指定那台機器,我就指定它ip唄?這樣的話,那怎么實現負載均衡,手動控制嗎?所以,選擇一個頭領,當客戶端請求來的時候,由leader(頭領)去控制,把任務分配給follower(跟班)。

數據樹:綁定數據的樹狀結構。簡單理解,它也是一個數據庫,只不過它是存儲於內存的樹狀結構的數據庫。

三、安裝-配置-啟動-監控

1、物理架構

  

     搭建zookeeper集群,zookeeper規定是總共集群的機器是奇數,而集群正常運行的條件是:存活的機器大於集群總數的二分之一。

2、安裝配置(注意:集群的所有機器都需要設置)

  • 下載zookeeper-3.4.10.tar.gz

  • 解壓:tar xvf zookeeper-3.4.10.tar.gz

  • 配置  

    • 切換到/zookeeper/conf目錄:/usr/local/zookeeper-3.4.10/conf (我的路徑)
    • 拷貝:cp zoo_sample.cfg zoo.cfg
    • 修改zoo.cfg:vim zoo.cfg
      • dataDir=/tmp/zookeeper (數據存儲位置,生產環境需要修改,這個是linux的臨時目錄,可能會被刪除)
      • 在配置文件底部添加一下內容:我這個配置了域名,如果沒有配置域名就用ip。
      • server.1=master:2888:3888
      • server.2=slave2:2888:3888
      • server.3=slave3:2888:3888
    • 修改數據文件
      • 切換/tmp目錄:cd /tmp
      • 創建zookeeper目錄:mkdir zookeeper
      • 切換至zookeeper目錄:cd /tmp/zookeeper
      • 創建myid文件:vim myid
      • master上,輸入1保存;slave2,輸入2保存;slave3,輸入3保存。
  • 啟動、觀測

    • 切換至/zookeeper/bin目錄:/usr/local/zookeeper-3.4.10/bin
    • 服務端
      • 啟動:./zkServer.sh start
      • 查看:./zkServer.sh status
      • 停止:./zkServer.sh stop
    • jps(查看狀態)
      • 2289 QuorumPeerMain
      • 2302 Jps
    • 客戶端  .
      • ./zkCli.sh -server master:2181
      • create /tank tankservers
      • create /tank/server1 server1info
      • create /tank/server2 server2info
      • create /tank/server3 server3info
      • ls /tank
      • get /tank
      • set /tank tankserversinfo
      • get /tank
      • delete /tank/server3

四、javaApi基礎用法

/** * .測試zookeeper的基本操作 * Creator:邱勇Aaron * DateTime:2017/6/25 14:51 */
public class ZookeeperBasicOperator { public static String connectString="192.168.0.100,192.168.0.102,192.168.0.103:2181"; private ZooKeeper zk; 
</span><span style="color: #0000ff;">public</span><span style="color: #000000;"> ZookeeperBasicOperator(){
    </span><span style="color: #0000ff;">this</span>(connectString,2000,<span style="color: #0000ff;">null</span><span style="color: #000000;">);
}

</span><span style="color: #0000ff;">public</span> ZookeeperBasicOperator(String connectString,<span style="color: #0000ff;">int</span><span style="color: #000000;"> sessionTimeout,Watcher watcher){
    </span><span style="color: #0000ff;">try</span><span style="color: #000000;"> {
        zk</span>=<span style="color: #0000ff;">new</span><span style="color: #000000;"> ZooKeeper(connectString,sessionTimeout,watcher);
    }</span><span style="color: #0000ff;">catch</span><span style="color: #000000;"> (IOException e){
        e.printStackTrace();
    }
}
</span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">static</span> <span style="color: #0000ff;">void</span> main(String [] args) <span style="color: #0000ff;">throws</span><span style="color: #000000;"> Exception {
    connectString</span>="192.168.0.100,192.168.0.102,192.168.0.103:2181"<span style="color: #000000;">;
    ZookeeperBasicOperator zkOperator</span>=<span style="color: #0000ff;">new</span> ZookeeperBasicOperator(connectString,2000,<span style="color: #0000ff;">new</span><span style="color: #000000;"> MyWatcher());
    </span><span style="color: #008000;">//</span><span style="color: #008000;">創建目錄節點</span>
    zkOperator.create("/testRootPath","testRootData"<span style="color: #000000;">,CreateMode.PERSISTENT);
    </span><span style="color: #008000;">//</span><span style="color: #008000;">查看目錄節點</span>
    zkOperator.getData("/testRootPath"<span style="color: #000000;">);

    </span><span style="color: #008000;">//</span><span style="color: #008000;">創建目錄子節點</span>
    zkOperator.create("/testRootPath/testRootChild","testRootChild"<span style="color: #000000;">,CreateMode.PERSISTENT);
    </span><span style="color: #008000;">//</span><span style="color: #008000;">查看目錄節點的子節點目錄;</span>
    zkOperator.getChildern("/testRootPath"<span style="color: #000000;">);

    </span><span style="color: #008000;">//</span><span style="color: #008000;">重新設置目錄節點的數據</span>
    zkOperator.setData("/testRootPath","helloTestRootData"<span style="color: #000000;">);
    </span><span style="color: #008000;">//</span><span style="color: #008000;">查看目錄節點的數據</span>
    zkOperator.getData("/testRootPath"<span style="color: #000000;">);

    </span><span style="color: #008000;">//</span><span style="color: #008000;">刪除目錄節點的子數據節點</span>
    zkOperator.delete("/testRootPath/testRootChild",-1<span style="color: #000000;">);
    </span><span style="color: #008000;">//</span><span style="color: #008000;">刪除目錄節點</span>
    zkOperator.delete("/testRootPath",-1<span style="color: #000000;">);

    </span><span style="color: #008000;">//</span><span style="color: #008000;">關閉zookeeper</span>

zkOperator.closeZookeeper();

}

</span><span style="color: #008000;">//</span><span style="color: #008000;">創建目錄節點、孩子節點</span>
<span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span> create(String path,String bindingData,CreateMode mode) <span style="color: #0000ff;">throws</span><span style="color: #000000;"> KeeperException, InterruptedException {
    zk.create(path,bindingData.getBytes(),ZooDefs.Ids.OPEN_ACL_UNSAFE,mode);
    System.out.println();
}

</span><span style="color: #008000;">//</span><span style="color: #008000;">修改目錄節點的數據</span>
<span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span> setData(String path,String bindingData,<span style="color: #0000ff;">int</span> version) <span style="color: #0000ff;">throws</span><span style="color: #000000;"> KeeperException, InterruptedException {
    zk.setData(path,bindingData.getBytes(),version);
}

</span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span> setData(String path,String bindingData) <span style="color: #0000ff;">throws</span><span style="color: #000000;"> KeeperException, InterruptedException {
    </span><span style="color: #0000ff;">this</span>.setData(path,bindingData,-1<span style="color: #000000;">);
}

</span><span style="color: #008000;">//</span><span style="color: #008000;">刪除節點</span>
<span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span> delete(String path,<span style="color: #0000ff;">int</span> version) <span style="color: #0000ff;">throws</span><span style="color: #000000;"> KeeperException, InterruptedException {
    zk.delete(path,version);
}

</span><span style="color: #008000;">//</span><span style="color: #008000;">獲得目錄節點數據</span>
<span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span> getData(String path) <span style="color: #0000ff;">throws</span><span style="color: #000000;"> KeeperException, InterruptedException {
    System.out.println(</span><span style="color: #0000ff;">new</span> String(zk.getData(path,<span style="color: #0000ff;">false</span>,<span style="color: #0000ff;">null</span><span style="color: #000000;">)));
}

</span><span style="color: #008000;">//</span><span style="color: #008000;">獲得孩子節點數據</span>
<span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span> getChildern(String path) <span style="color: #0000ff;">throws</span><span style="color: #000000;"> KeeperException, InterruptedException {
    System.out.println(zk.getChildren(path,</span><span style="color: #0000ff;">true</span><span style="color: #000000;">));
}

</span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> closeZookeeper(){
    </span><span style="color: #0000ff;">try</span><span style="color: #000000;"> {
        zk.close();
    } </span><span style="color: #0000ff;">catch</span><span style="color: #000000;"> (InterruptedException e) {
        e.printStackTrace();
    }
}

}

class MyWatcher implements Watcher{
@Override
public void process(WatchedEvent watchedEvent) {
System.out.println(
"已經觸發了:"+watchedEvent.getType()+"事件!");
}
}

五、ZooKeeper 典型的應用場景(轉自:http://www.cnblogs.com/ggjucheng/p/3370359.html)

Zookeeper 從設計模式角度來看,是一個基於觀察者模式設計的分布式服務管理框架,它負責存儲和管理大家都關心的數據,然后接受觀察者的注冊,一旦這些數據的狀態發生變化,Zookeeper 就將負責通知已經在 Zookeeper 上注冊的那些觀察者做出相應的反應,從而實現集群中類似 Master/Slave 管理模式,關於 Zookeeper 的詳細架構等內部細節可以閱讀 Zookeeper 的源碼

下面詳細介紹這些典型的應用場景,也就是 Zookeeper 到底能幫我們解決那些問題?下面將給出答案。

統一命名服務(Name Service)

分布式應用中,通常需要有一套完整的命名規則,既能夠產生唯一的名稱又便於人識別和記住,通常情況下用樹形的名稱結構是一個理想的選擇,樹形的名稱結構是一個有層次的目錄結構,既對人友好又不會重復。說到這里你可能想到了 JNDI,沒錯 Zookeeper 的 Name Service 與 JNDI 能夠完成的功能是差不多的,它們都是將有層次的目錄結構關聯到一定資源上,但是 Zookeeper 的 Name Service 更加是廣泛意義上的關聯,也許你並不需要將名稱關聯到特定資源上,你可能只需要一個不會重復名稱,就像數據庫中產生一個唯一的數字主鍵一樣。

Name Service 已經是 Zookeeper 內置的功能,你只要調用 Zookeeper 的 API 就能實現。如調用 create 接口就可以很容易創建一個目錄節點。
 

配置管理(Configuration Management)

配置的管理在分布式應用環境中很常見,例如同一個應用系統需要多台 PC Server 運行,但是它們運行的應用系統的某些配置項是相同的,如果要修改這些相同的配置項,那么就必須同時修改每台運行這個應用系統的 PC Server,這樣非常麻煩而且容易出錯。

像這樣的配置信息完全可以交給 Zookeeper 來管理,將配置信息保存在 Zookeeper 的某個目錄節點中,然后將所有需要修改的應用機器監控配置信息的狀態,一旦配置信息發生變化,每台應用機器就會收到 Zookeeper 的通知,然后從 Zookeeper 獲取新的配置信息應用到系統中。

圖 2. 配置管理結構圖

 

集群管理(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 容易出現單點故障的問題。

圖 3. 集群管理結構圖

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(); } } } 

共享鎖(Locks)

共享鎖在同一個進程中很容易實現,但是在跨進程或者在不同 Server 之間就不好實現了。Zookeeper 卻很容易實現這個功能,實現方式也是需要獲得鎖的 Server 創建一個 EPHEMERAL_SEQUENTIAL 目錄節點,然后調用 getChildren方法獲取當前的目錄節點列表中最小的目錄節點是不是就是自己創建的目錄節點,如果正是自己創建的,那么它就獲得了這個鎖,如果不是那么它就調用exists(String path, boolean watch) 方法並監控 Zookeeper 上目錄節點列表的變化,一直到自己創建的節點是列表中最小編號的目錄節點,從而獲得鎖,釋放鎖很簡單,只要刪除前面它自己所創建的目錄節點就行了。

圖 4. Zookeeper 實現 Locks 的流程圖
 

同步鎖思路:

加鎖: ZooKeeper 將按照如下方式實現加鎖的操作: 1 ) ZooKeeper 調用 create ()方法來創建一個路徑格式為“ _locknode_/lock- ”的節點,此節點類型為sequence (連續)和 ephemeral (臨時)。也就是說,創建的節點為臨時節點,並且所有的節點連續編號,即“ lock-i ”的格式。 2 )在創建的鎖節點上調用 getChildren ()方法,來獲取鎖目錄下的最小編號節點,並且不設置 watch 。 3 )步驟 2 中獲取的節點恰好是步驟 1 中客戶端創建的節點,那么此客戶端獲得此種類型的鎖,然后退出操作。 4 )客戶端在鎖目錄上調用 exists ()方法,並且設置 watch 來監視鎖目錄下比自己小一個的連續臨時節點的狀態。 5 )如果監視節點狀態發生變化,則跳轉到第 2 步,繼續進行后續的操作,直到退出鎖競爭。 

解鎖:
ZooKeeper 解鎖操作非常簡單,客戶端只需要將加鎖操作步驟 1 中創建的臨時節點刪除即可。復制代碼

同步鎖代碼
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 可以處理兩種類型的隊列:

  1. 當一個隊列的成員都聚齊時,這個隊列才可用,否則一直等待所有成員到達,這種是同步隊列。
  2. 隊列按照 FIFO 方式進行入隊和出隊操作,例如實現生產者和消費者模型。

同步隊列用 Zookeeper 實現的實現思路如下:

創建一個父目錄 /synchronizing,每個成員都監控標志(Set Watch)位目錄 /synchronizing/start 是否存在,然后每個成員都加入這個隊列,加入隊列的方式就是創建 /synchronizing/member_i 的臨時目錄節點,然后每個成員獲取 / synchronizing 目錄的所有目錄節點,也就是 member_i。判斷 i 的值是否已經是成員的個數,如果小於成員個數等待 /synchronizing/start 的出現,如果已經相等就創建 /synchronizing/start。

用下面的流程圖更容易理解:

圖 5. 同步隊列流程圖
 

同步隊列

 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; } } } } 
六、CAP理論與Paxos算法

CAP理論:

一致性(Consistency):在分布式系統中的所有數據備份,在同一時刻同樣的值。(等同於所有節點訪問同一份最新的數據副本)
可用性(Availability):在集群中一個節點出現故障,集群整體是否還能響應客戶端的讀寫請求。(對數據更新具備高可用)
分區容錯性(Partition Tolerance): 以實際效果而言,分區相當於對通信的時限要求。系統如果不能在時限內達成數據一致性,就意味着發生了分區的情況,必須就當前操作在C和A之間做出選擇。
整體總結來說,zookeeper集群,只能滿足三個條件的其中兩個,例如保證一致性與可用性,就不能保證分區容錯性,其他同理。
 

Paxos算法(選舉算法):詳情參考http://www.cnblogs.com/yuyijq/p/4116365.html

新集群啟動時候的選舉過程,啟動的時候就是根據zoo.cfg里的配置,向各個節點廣播投票,一般都是選投自己。然后收到投票后就會進行進行判斷。如果某個節點收到的投票數超過一半,那么它就是leader了。 

了解了這個過程,我們來看看另外一個問題:

一個集群有3台機器,掛了一台后的影響是什么?掛了兩台呢? 

掛了一台:掛了一台后就是收不到其中一台的投票,但是有兩台可以參與投票,按照上面的邏輯,它們開始都投給自己,后來按照選舉的原則,兩個人都投票給其中一個,那么就有一個節點獲得的票等於2,2 > (3/2)=1 的,超過了半數,這個時候是能選出leader的。

掛了兩台: 掛了兩台后,怎么弄也只能獲得一張票, 1 不大於 (3/2)=1的,這樣就無法選出一個leader了。

七、版權聲明

  作者:邱勇Aaron

  出處:http://www.cnblogs.com/qiuyong/

  您的支持是對博主深入思考總結的最大鼓勵。

  本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,尊重作者的勞動成果。

  參考:馬士兵zookeeper、zookeeper官方文檔

     選舉算法:http://www.cnblogs.com/yuyijq/p/4116365.html

     應用場景:http://www.cnblogs.com/ggjucheng/p/3370359.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM