【轉】Zookeeper的前世今生

原文鏈接:https://www.imooc.com/article/45213

 

到底發生了什麼?

  在電商架構中,早期是單體架構,可以很快的解決交互問題和產品初期的迭代。但是隨着架構的發展,後端無法支撐大流量。一開始的解決辦法是增加服務器等垂直解決方法,但是這樣的效率太低並且成本太高。因此開發者開始考慮水平伸縮來提高整體的性能。

  首先是對產品的拆分:按照類型拆分成不同的模塊,那麼模塊之間的交互就需要實現遠程調用,比如webservice。這樣其實就簡單的形成了一個分佈式架構。服務越來越多,我們就拆分的越來越細,隨着流量不斷提升,後端規模越來越大。舉個例子:用戶調用訂單服務,它是通過http協議來調用,那麼首先訂單服務那必須給用戶一個地址。如果說訂單系統是一個大的集羣,那麼可能我們就需要維護多個這樣的地址。那麼應該如何解決大規模地址管理?集羣裏的地址如何去轉發?如果其中一個節點down機了怎麼辦,怎麼去管理服務動態上下線感知?

  那我們就通過解決以上三個問題來引出本文的主角---Zookeeper

  我們可以考慮設置一箇中間件,它的存在可以讓我們的服務發佈的時候註冊上去,充當一個電話本,記住你所有的地址,並且及時瞭解你是不是斷開。用戶服務只需要拿到中間件的地址,就可以獲得對應的相關的調用的目標服務的信息,拿到這個信息,根據負載均衡算法,就可以做一個轉發。

  Zookeeper是一個什麼東西呢?它是一個文件存儲類似的樹型結構,entry是key-value。每個子節點由父節點管理,子節點是父節點詳細的分類。比如說,對於訂單服務系統,它的子節點存放着各種地址。Zookeeper適不適合作爲一個註冊中心?很多人說不是很合適,但是目前大部分企業仍然用它來做註冊中心的功能。Zookeeper的學術名稱爲分佈式協調服務,它的本意是解決分佈式鎖的,比如說幾個服務訪問共享資源,就會出現資源競爭的問題,這時候就會需要一個協調者來解決這個問題,Zookeeper就是用來解決這個問題的,可以看作一個交警。這樣一來共享資源就變成了一個單點訪問資源,你先來我中間件裏來,我再判斷讓不讓你去訪問。當然爲了保持單點的特點,Zookeeper一般是以集羣出現,在滿足單點的功能,提高其可用性。集羣的出現帶來的問題衆所周知,那就是數據同步。Zookeeper內部角色分爲Leader、Follower、observer,數據提交方式基於二階提交,寫數據寫在follower上,其他的follower去同步數據。請求命令放在leader上,然後讓其他的節點知道,這裏滿足一個CAP原則。

  Zookeeper作爲一個分佈式協調服務,目標是爲了解決分佈式架構中一致性問題。

  Zookeeper客戶端可以提供增刪改查節點的功能,刪除的時候必須一層一層的刪除。而且節點具有唯一性,可以參考電腦文件結構。同時節點還分爲臨時節點 -e和持久化節點、有序節點 -s和無序節點。

Zookeeper應用場景

  註冊中心、配置中心(和註冊中心大同小異,類似於application.properties,用來統一維護配置信息)、負載均衡(知道機器的狀態以及選舉leader)和分佈式鎖。

[zk: localhost:2181(CONNECTED) 0] create /userservice 0Created /userservice
[zk: localhost:2181(CONNECTED) 2] ls /[zookeeper, userservice]
[zk: localhost:2181(CONNECTED) 3] ls /userservice
[]

   配置zoo.cfg文件,將三個服務器的ip、訪問Zookeeper的端口以及選舉的端口配置好

server.1=10.10.101.7:2888:3888server.2=10.10.101.104:2888:3888server.3=10.10.101.108:2888:3888
server.n=ip:prot:prot

其中n在 /tmp/zookeeper/myid裏配置

  除了Leader和Follower之外,還有一個Observer的節點,它的作用是用來監控整個集羣的狀態。

Zookeeper特性分析

ACL

  ACL屬於一種權限控制,控制你創建的文件夾(節點)的0訪問權限。它提供了create、write、read、delete和admin五種權限。

角色

  leader用來處理事務請求的,就是所有的添加修改刪除都會去leader那,非事務請求(查詢)可能會落到任一節點上。

數據模型

  數據模型是Zookeeper裏較爲核心的東西,它的結構類似於樹,也類似於電腦的文件管理系統。節點特性分爲持久化和有序性,每個znode可以保存少量數據。

會話

  客戶端和服務端連接會建立會話

進階

Zookeeper的技術層面由來

  假設分佈式系統中有三個節點作爲一個集羣,在這個集羣裏運行一個任務,所以每個節點都有權限去執行這個任務。那我我有幾個問題想問:

  (1)怎麼保證各個節點數據一致?

  (2)怎麼保證任務只在一個節點執行?

  (3)如果在執行任務的1節點掛了,那麼其他的節點如何發現並接替任務?

  (4)它們對於共享資源是怎麼處理的?

   我們可以先將節點註冊到Zookeeper中,然後因爲節點有順序性,所以說我們第一個看到的節點就認爲他是最具優先權的,那麼它就可以去做這個操作,這就是Zookeeper起到的能夠給集羣節點進行一個協調的作用。

  那麼按照上面的幾個問題,如果我們想設計一箇中間件,那麼應該注意哪些事情呢?

  (1)單點故障 

  存在leader、follower節點。同時也會分擔請求(高可用、高性能)。

  (2)爲啥集羣要有master

  (3)如果集羣中的maser掛了怎麼辦?數據如何恢復?如何選舉?

    Zookeeper選舉使用了ZAB協議

  (4)如何保證數據一致性?(分佈式事務)

  2PC(二階提交):當一個事務涉及多個節點提交,爲了保證進行,引入一個協調者,通過協調者控制整個集羣工作的順利進行。當一個事務開始時,由協調者將請求發給所有節點,然後節點若能執行,則向協調者發起可以執行請求。如果一個參與者失敗,不能進行事務執行,那麼其他節點都將發起回滾提交。否則,所有節點順利提交,這個事務順利完成。

Zookeeper集羣內部成員介紹:

https://img3.sycdn.imooc.com/5b51b57c0001db3909390533.jpg

爲什麼Zookeeper集羣是2*n+1的數量?

  因爲Zookeeper集羣如果正常對外服務,必須有投票機制,集羣內部有過半節點正常服務,保證投票能夠有結果。而且能夠保證對n個服務器的容災處理。

ZAB協議

  它是Zookeeper裏面專門用來處理崩潰恢復的原子廣播協議,依賴ZAB協議實現分佈式數據一致性。如果集羣中leader出現了問題,ZAB協議就會進行恢復模式並選舉產生新的leader,選舉產生之後,並且集羣中有過半節點與leader數據同步之後,ZAB就會退出恢復模式。

  消息廣播:屬於一個簡化的二階提交機制,leader收到請求後,會給事務請求賦予一個zxid,可以通過zxid的大小去比較生成因果有序這個特性。leader會給每個follower給一個FIFO隊列,然後將帶有zxid的消息作爲一個提案分發給所有的leader,follower收到請求後,將提案寫入磁盤,並給leader發送ack。如果leader收到半數以上的ack,就確定這個消息要執行,然後給所有的follower發送commit指令,同時也會在本地執行這個請求。如果沒有通過,所有節點執行回滾。

  崩潰恢復:leader掛掉之後,那麼就需要恢復選舉和數據。當leader失去了過半節點聯繫、leader掛了這兩種情況發生,集羣就會進入崩潰恢復階段。對於數據恢復來說:(1)已經被處理的消息不能丟失,也就是來個栗子:當follower收到commit之前,leader掛掉了,怎麼辦?這時部分節點收到commit,部分節點沒有收到,這時ZAB協議保證已經處理的消息不能被丟失,被丟棄的消息不能再次出現(當leader節點收到事務請求之後,在生成提案時掛了,那麼新選舉的leader節點要跳過這個消息)。

  ZAB協議需要滿足這兩個要求,必須設計出算法。

  ZAB協議中保證選舉出來的leader有着整個集羣zxid最大的提案,這樣第一是保證新的leader之前是正常工作的,第二是因爲zxid是64位的,高32爲epoch編號,每當leader選舉產生一個新的leader,新的leader的epoch號就+1,低32位是消息計數器,每當接受一條消息,就+1。新的leader被選舉之後就會清空。這樣可以保證老的leader掛掉之後,不可能被再次選舉。可以把epoch看做成皇帝的年號,現在統治的事哪個皇帝。

  zxid在上面已經簡單介紹了,下面說一下它的簡單特性:Zookeeper中所有提議在被提出時都會加上zxid。

leader選舉

  基於fast leader選舉,基於幾個方面:zxid最大會設置成leader,epoch;myid(服務器id),myid越大,在leader選舉權重中越大;事務id,事務id越大,表示事務越新;epoch(邏輯時鐘)每一次投票,epoch都會遞增;選舉狀態:LOOKING->LEADING(FOLLOWING、OBSERVING)

  啓動的時候:每個Server都會發起一個投票,每個節點都會先將自己作爲一個leader,並將自己的zxid、myid等信息發給其他節點。其他節點會進行比較:zxid相同就檢查myid,myid大的會作爲leader,之後開始進行統計投票,最後選出leader。

  前一個leader掛掉:所有節點編程looking狀態,然後會查看其他節點的信息,來做出投票。

看一下源碼理解一下

protected void initializeAndRun(String[] args)        throws ConfigException, IOException
    {
        org.apache.zookeeper.server.quorum.QuorumPeerConfig config = new org.apache.zookeeper.server.quorum.QuorumPeerConfig();        if (args.length == 1) {
            config.parse(args[0]);
        }        // Start and schedule the the purge task
        DatadirCleanupManager purgeMgr = new DatadirCleanupManager(config
                .getDataDir(), config.getDataLogDir(), config
                .getSnapRetainCount(), config.getPurgeInterval());
        purgeMgr.start();        //判斷是單機還是集羣模式
        if (args.length == 1 && config.servers.size() > 0) {
            runFromConfig(config);
        } else {
            LOG.warn("Either no config or no quorum defined in config, running "
                    + " in standalone mode");            // there is only server in the quorum -- run as standalone            ZooKeeperServerMain.main(args);
        }
    }

 

  因爲只有集羣模式纔會有選舉,這時候我們會進入到runFromConfig方法中:

public void runFromConfig(org.apache.zookeeper.server.quorum.QuorumPeerConfig config) throws IOException {      try {
          ManagedUtil.registerLog4jMBeans();
      } catch (JMException e) {
          LOG.warn("Unable to register log4j JMX control", e);
      }
  
      LOG.info("Starting quorum peer");      try {
          ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory();
          cnxnFactory.configure(config.getClientPortAddress(),
                                config.getMaxClientCnxns());
  
          quorumPeer = new org.apache.zookeeper.server.quorum.QuorumPeer();
          quorumPeer.setClientPortAddress(config.getClientPortAddress());
          quorumPeer.setTxnFactory(new FileTxnSnapLog(                      new File(config.getDataLogDir()),                      new File(config.getDataDir())));
          quorumPeer.setQuorumPeers(config.getServers());
          quorumPeer.setElectionType(config.getElectionAlg());
          quorumPeer.setMyid(config.getServerId());
          quorumPeer.setTickTime(config.getTickTime());
          quorumPeer.setMinSessionTimeout(config.getMinSessionTimeout());
          quorumPeer.setMaxSessionTimeout(config.getMaxSessionTimeout());
          quorumPeer.setInitLimit(config.getInitLimit());
          quorumPeer.setSyncLimit(config.getSyncLimit());
          quorumPeer.setQuorumVerifier(config.getQuorumVerifier());
          quorumPeer.setCnxnFactory(cnxnFactory);
          quorumPeer.setZKDatabase(new ZKDatabase(quorumPeer.getTxnFactory()));
          quorumPeer.setLearnerType(config.getPeerType());
          quorumPeer.setSyncEnabled(config.getSyncEnabled());
          quorumPeer.setQuorumListenOnAllIPs(config.getQuorumListenOnAllIPs());
  
          quorumPeer.start();
          quorumPeer.join();
      } catch (InterruptedException e) {          // warn, but generally this is ok
          LOG.warn("Quorum Peer interrupted", e);
      }
    }

 

  可以看到,它會從配置文件中加載一些信息,最後啓動start來開始進行選舉。

 

@Override    
public synchronized void start() {
        loadDataBase();
        cnxnFactory.start(); 
        
        //開始進行選舉Leader        startLeaderElection();        super.start();
    }

 

 

    
synchronized public void startLeaderElection() {        try {
            currentVote = new org.apache.zookeeper.server.quorum.Vote(myid, getLastLoggedZxid(), getCurrentEpoch());
        } catch(IOException e) {
            RuntimeException re = new RuntimeException(e.getMessage());
            re.setStackTrace(e.getStackTrace());            throw re;
        }        for (QuorumServer p : getView().values()) {            if (p.id == myid) {
                myQuorumAddr = p.addr;                break;
            }
        }        if (myQuorumAddr == null) {            throw new RuntimeException("My id " + myid + " not in the peer list");
        }        if (electionType == 0) {            try {
                udpSocket = new DatagramSocket(myQuorumAddr.getPort());
                responder = new ResponderThread();
                responder.start();
            } catch (SocketException e) {                throw new RuntimeException(e);
            }
        }        this.electionAlg = createElectionAlgorithm(electionType);
    }

 

  從上段代碼的一開始,表示它會存儲三個信息:myid、zxid和epoch,然後配置選舉類型來使用選舉算法,

 

protected org.apache.zookeeper.server.quorum.Election createElectionAlgorithm(int electionAlgorithm){
        org.apache.zookeeper.server.quorum.Election le=null;                
        //TODO: use a factory rather than a switch
        switch (electionAlgorithm) {        case 0:
            le = new org.apache.zookeeper.server.quorum.LeaderElection(this);            break;        case 1:
            le = new org.apache.zookeeper.server.quorum.AuthFastLeaderElection(this);            break;        case 2:
            le = new org.apache.zookeeper.server.quorum.AuthFastLeaderElection(this, true);            break;        case 3:
            qcm = new org.apache.zookeeper.server.quorum.QuorumCnxManager(this);
            org.apache.zookeeper.server.quorum.QuorumCnxManager.Listener listener = qcm.listener;            if(listener != null){
                listener.start();
                le = new org.apache.zookeeper.server.quorum.FastLeaderElection(this, qcm);
            } else {
                LOG.error("Null listener when initializing cnx manager");
            }            break;        default:            assert false;
        }        return le;
    }

 

  這裏面提供着一些選舉算法,最後會設置一個負責選舉的IO類,然後啓動來進行選舉。

 

 本文思路來源於《從Paxos到Zookeeper:分佈式一致性原理與實踐》一書


作者:慕仙森
鏈接:https://www.imooc.com/article/45213
來源:慕課網

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章