ZooKeeper學習總結 第二篇:ZooKeeper深入探討


其實zookeeper系列的學習總結很早就寫完了,這段時間在准備找工作的事情,就一直沒有更新了。下邊給大家送上,文中如有不恰當的地方,歡迎給予指證,不勝感謝!。

1. 數據模型

1.1. 只適合存儲小數據

Zk維護着一個邏輯上的樹形層次結構,樹中的節點稱為znode,個znode都有一個ACL(權限控制)。Zookeeper是被設計用來協調服務的,因此znode里存儲的都是小數據,而不是大容量的數據,數據容量一般在1MB范圍內。

1.2. 操作的原子性

Znode的數據讀寫是原子的,要么讀或寫了完整的數據,要么就失敗,不會出現只讀或寫了部分數據。

1.3. Znode的路徑

和Unix中的文件系統路徑格式很想,但是只支持絕對路徑,不支持相對路徑,也不支持點號(”.”和”..”)。

1.4. 短暫的znode和持久的znode

Znode有兩種類型:短暫的和持久的。短暫的znode生命周期僅限創建它的客戶端與服務器端之間的連接沒有斷開,客戶端斷開連接后,znode將會被刪除。

1.5. 順序znode

名稱中包含Zookeeper指定順序號的znode。若在創建znode時設置了順序標識,那么該znode被創建后,名字的后邊將會附加一串數字,該數字是由一個單調遞增的計數器來生成的。例如,創建節點時傳入的path是”/aa/bb”,創建后的則可能是”/aa/bb0002”,再次創建后是”/aa/bb0003”。

Znode的創建模式CreateMode有四種,分別是:EPHEMERAL(短暫的znode)、EPHEMERAL_SEQUENTIAL(短暫的順序znode)、PERSISTENT(持久的znode)和PERSISTENT_SEQUENTIAL(持久的順序znode)。如果您已經看過了上篇博文,那么這里的api調用應該是很好理解的,見:http://www.cnblogs.com/leocook/p/zk_0.html。

1.6. 觀察

這部分在上篇博文中已經做了詳細的說明,包括連接的觀察和znode的觀察,這部分在構建一個穩定的zookeeper應用中有着很重要的作用,具體會在下邊說到。

2.  ACL

即:Access Control List(訪問控制列表)。Znode被創建時帶有一個ACL列表,zk提供了下邊三種身份驗證模式

  • digest

用戶名+密碼驗證。

  • host

客戶端主機名hostname驗證。

  • ip

客戶端的IP驗證。

  • auth

使用sessionID驗證

  • world

無驗證,默認是無任何權限。該模式較為特殊,在zk連接添加ACL中會說到

ACL權限對應如下表:

在設置ACL時,可以給zk客戶端和服務器端的連接設置ACL,也可以在創建znode時,給znode設置ACL,在創建了znode后,如果有zk客戶端來操作znode,只有滿足權限要求時,才能完成相對應的操作:

2.1. 給ZK連接添加ACL

可使用zk對象的addAuthInfo()方法來添加驗證模式,如使用digest模式進行身份驗證:zk.addAuthInfo(“digest”,”username:passwd”.getBytes());

在zookeeper對象被創建時,初始化會被添加world驗證模式。world身份驗證模式的驗證id是”anyone”。

若該連接創建了znode,那么他將會被添加auth身份驗證模式的驗證id是””,即空字符串,這里將使用sessionID進行驗證。

2.2. 給znode設置ACL

  • 自己創建ACL

創建ACL對象時,可用ACL類的構造方法ACL(int perms, Id id)

其中參數perms表示權限,在接口org.apache.zookeeper.ZooDefs.Perms中有相關的常量:READ、WRITE、CREATE、DELETE、ALL和ADMIN,它們值如下表:

Id參數是驗證模式,可用構造方法Id(String scheme, String id)來創建。參數scheme是驗證模式,digest、host或ip,id是對應的驗證,digest對應用戶名和密碼對,如“user:passwd”;host對應主機名,如”localhost”;ip對應ip地址,如”192.168.1.120”。

  • 使用api中預設的ACL

在創建znode時可以設置該znode的ACL列表。接口org.apache.zookeeper.ZooDefs.Ids中有一些已經設置好的權限常量,例如:

(1)、OPEN_ACL_UNSAFE:完全開放。事實上這里是采用了world驗證模式,由於每個zk連接都有world驗證模式,所以znode在設置了OPEN_ACL_UNSAFE時,是對所有的連接開放。

(2)、CREATOR_ALL_ACL:給創建該znode連接所有權限。事實上這里是采用了auth驗證模式,使用sessionID做驗證。所以設置了CREATOR_ALL_ACL時,創建該znode的連接可以對該znode做任何修改。

(3)、READ_ACL_UNSAFE:所有的客戶端都可讀。事實上這里是采用了world驗證模式,由於每個zk連接都有world驗證模式,所以znode在設置了READ_ACL_UNSAFE時,所有的連接都可以讀該znode。

注:紅色部分是本人閱讀源碼的一些研究,auth和world的相關描述經供參考。

3. 運行模式

Zookeeper有兩種運行模式:獨立模式(standalone mode)和復制模式(replicated mode)。

3.1. 獨立模式

只有一個zookeeper服務實例,不可保證高可靠性和恢復性,可在測試環境中使用,生產環境不建議使用。

3.2. 復制模式

復制模式也就是集群模式,有多個zookeeper實例在運行,建議多個zk實例是在不同的服務器上。集群中不同zookeeper實例之間數據不停的同步。有半數以上的實例保持正常運行,zk服務就能正常運行,例如:有5個zk實例,掛了2個,還剩3個,依然可以正常工作;如有6個zk實例,掛了3個,則不能正常工作。

每個znode的修改都會被復制到超過半數的機器上,這樣就會保證至少有一台機器會保存最新的狀態,其余的副本最終都會跟新到這個狀態。Zookeeper為實現這個功能,使用了Zab協議,該協議有兩個可以無限重復的階段:

  • 選舉領導

集群中所有的zk實例會選舉出來一個“領導實例”(leader),其它實例稱之為“隨從實例”(follower)。如果leader出現故障,其余的實例會選出一台leader,並一起提供服務,若之前的leader恢復正常,便成為follower。選舉follower是一個很快的過程,性能影響不明顯。

Leader主要功能是協調所有實例實現寫操作的原子性,即:所有的寫操作都會轉發給leader,然后leader會將更新廣播給所有的follower,當半數以上的實例都完成寫操作后,leader才會提交這個寫操作,隨后客戶端會收到寫操作執行成功的響應。

  • 原子廣播

上邊已經說到:所有的寫操作都會轉發給leader,然后leader會將更新廣播給所有的follower,當半數以上的實例都完成寫操作后,leader才會提交這個寫操作,隨后客戶端會收到寫操作執行成功的響應。這么來的話,就實現了客戶端的寫操作的原子性,每個寫操作要么成功要么失敗。邏輯和數據庫的兩階段提交協議很像。

3.3. 復制模式下的數據一致性

Znode的每次寫操作都相當於數據庫里的一次事務提交,每個寫操作都有個全局唯一的ID,稱為:zxid(ZooKeeper Transaction)。ZooKeeper會根據寫操作的zxid大小來對操作進行排序,zxid小的操作會先執行。zk下邊的這些特性保證了它的數據一致性:

  • 順序一致性

任意客戶端的寫操作都會按其發送的順序被提交。如果一個客戶端把某znode的值改為a,然后又把值改為b(后面沒有其它任何修改),那么任何客戶端在讀到值為b之后都不會再讀到a。

  • 原子性

這一點再前面已經說了,寫操作只有成功和失敗兩種狀態,不存在只寫了百分之多少這么一說。

  • 單一系統映像

客戶端只會連接host列表中狀態最新的那些實例。如果正在連接到的實例掛了,客戶端會嘗試重新連接到集群中的其他實例,那么此時滯后於故障實例的其它實例都不會接收該連接請求,只有和故障實例版本相同或更新的實例才接收該連接請求。

  • 持久性

寫操作完成之后將會被持久化存儲,不受服務器故障影響。

  • 及時性

在對某個znode進行讀操作時,應該先執行sync方法,使得讀操作的連接所連的zk實例能與leader進行同步,從而能讀到最新的類容。

注意:sync調用是異步的,無需等待調用的返回,zk服務器會保證所有后續的操作會在sync操作完成之后才執行,哪怕這些操作是在執行sync之前被提交的。

4. 提高ZooKeeper應用的容錯

分布式環境是很復雜的,網絡的不可靠、單點故障等問題都是經常發生的。那么在構建一個分布式應用程序時,這些問題都是需要慎重考慮的。因此,如何構建一個可復原的分布式應用將成為一個值得討論的話題。Java api中每個異常都對應一類故障模式,下邊我們將會以Java api中的異常為例來討論ZooKeeper應用程序中可能會出現的一些故障。

4.1. Java API中的一些常見異常

  • InterruptedException異常

若客戶端的某操作被中斷,則會拋出InterruptedException異常。拋出該異常時,不一定是出現故障,只能表明某個zookeeper操作被中斷而已。

  • KeeperException異常

服務器發出錯誤信號或是服務器存在通信故障。該類現在共有21個子類, 分為3大類:

 (1)、狀態異常

當一個客戶端對zk的某操作失敗時,就會出現狀態異常。例如:更新數據時所指定的版本號不正確就會拋出異常BadVersionException、若在短暫的znode下創建子節點則會拋出異常NoChildrenForEphemeralsException。

(2)、可恢復的異常

那些在zk會話中可以恢復的異常叫可恢復的異常。當丟失zk連接時就會拋出異常ConnectionLossException,這時zk會自動嘗試重新連接,以確保會話的完整性。Zk無法判斷ConnectionLossException異常相關的操作是否成功執行,有可能出現只完成部分,那么是否重新執行剛才的操作就得知道該操作是否是等冪的。

等冪操作是指一次或多次執行都會產生相同結果的操作;非等冪操作是指一次或多次執行會產生不相同結果的操作。非等冪操作就不能盲目操作了。

寫操作里有創建、刪除、修改。在一個分布式環境中,刪除zk里的znode或是修改znode的數據是等冪的,只有創建znode可能不是等冪的,創建順序znode就是一個非等冪的操作。

那么怎么樣才能避免創建順序znode不會出現重復創建呢?下面我來展開討論:

假設的場景:

客戶端:客戶端任務是在連接到zk服務端時會只創建一個順序znode;

ConnectionLossException拋出ConnectionLossException異常重新連接后會話沒有失效, 但是zk無法判斷創建znode的操作是否成功。

我們知道順序znode的節點名稱格式是形如”znodeName<sequentialNumber>”,zk客戶端和服務器端的會話有個全局唯一的sessionID,我們可以把sessionID加入znode的名稱中,形如:” znodeName<sessionID><sequentialNumber>”,sequentialNumber是相對於父znode唯一的。這樣我們在創建某個znode之前先判斷一下父znode下有無名稱形如” znodeName<sessionID>“這樣字符開頭的子znode,就能確保每個客戶端連接只創建一個znode。

這種場景會在什么時候會遇到呢?在我們要實現一個分布式鎖的時候,核心思想之一就在這里。那么問題來了,什么是分布式鎖呢?后面會有獨立的博文來講解關於它的代碼實現。

(3)、不可恢復的異常

不可恢復的異常發生時,所有的短暫znode都將會丟失,只有程序中顯示的重建zk連接,並重建znode的狀態。例如:會話過期會拋出異常SessionExpiredException,身份驗證失敗會拋出異常AuthFailedException。

(4)、異常捕捉處理

每個子類對應一種異常狀態,且每個子類都對應一個關於錯誤類型的信息代碼,可以通過code方法拿到。 處理該種異常有兩種辦法:

1、通過檢測錯誤代碼(可調用code方法老獲取)來確定是哪種異常,再決定應該采取何種補救措施;

2、通過追捕等價的KeeperException異常,然后再每段捕捉代碼中執行相應的操作。

4.2. 構建可靠的zookeeper應用

上面說到了zk服務器端可能出現一些網絡故障或單點故障登,那么怎么編寫一個可靠的zk客戶端程序來應對可能不穩定的zk實例呢?這里我們向一個znode寫數據為例,來實現它:

/**
 * 顯示配置
 * @throws KeeperException 服務器發出錯誤信號或是服務器存在通信故障。該類現在共有21個子類,
 * 分為3大類:<br/>
 * 1、狀態異常(如:BadVersionException、NoChildrenForEphemeralsException);
 * 2、可恢復的異常(如:ConnectionLossException);
 * 3、不可恢復的異常(如:SessionExpiredException、AuthFailedException)。
 * 每個子類對應一種異常狀態,且每個子類都對應一個關於錯誤類型的信息代碼,可以通過code方法拿到。
 * 處理該種異常有兩種辦法:<br/>
 * 1、通過<b>檢測錯誤代碼</b>來決定應該采取何種補救措施;<br/>
 * 2、通過<b>追捕等價的KeeperException異常</b>,然后再每段捕捉代碼中執行相應的操作。
 * @throws InterruptedException zookeeper操作被中斷。<b>並不一定就是出現故障,只能表明相對應的操作被取消</b>。
 */
public static void write(String path, String value) throws KeeperException, InterruptedException {
    int retries = 0;
    
    while (true) {
        try {
            Stat stat = zk.exists(path, false);
            if(stat == null){
                zk.create(path, value.getBytes(CHARSET), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }else {
                zk.setData(path, value.getBytes(CHARSET), -1);
            }
            break;
        } catch(KeeperException.SessionExpiredException e){
            //TODO 此處會話過期,拋出異常,由上層調用來重新創建zookeeper對象
            throw e;
        }catch(KeeperException.AuthFailedException e){
            //TODO 此處身份驗證時,拋出異常,由上層來終止程序運行
            throw e;
        }catch (KeeperException e) {
            //檢查有沒有超出嘗試的次數
            if(retries == MAXRETRIES){
                throw e;
            }
            retries++;
            TimeUnit.SECONDS.sleep(RETRY_PERIOD_SECONDS);
        }
    }
}

如果您是一名Java開發人員,那么我覺得上面的這些代碼沒什么好解釋的了。下邊看上層調用是怎么處理的:

int flag = 0;

while (true) {
    try {
        write(path, value);
        break;
    } catch (KeeperException.SessionExpiredException e) {
        // TODO: 重新創建、開始一個新的會話
        e.printStackTrace();
        zk = new ZooKeeper(hosts, SESSION_TIMEOUT, this);
    } catch (KeeperException e) {
        // TODO 嘗試了多次,還是出錯,只有退出了
        e.printStackTrace();
        flag = 1;
        break;
    }catch(KeeperException.AuthFailedException e){
        //TODO 此處身份驗證時,終止程序運行
        e.printStackTrace();
        flag = 1;
        break;
    } catch (IOException e) {
        // TODO 創建zookeeper對象失敗,無法連接到zk集群
        e.printStackTrace();
        flag = 1;
        break;
    } 
}

System.exit(flag);

關於編寫一個可恢復的zookeeper應用,這一塊理解了,其它地方應該就是觸類旁通了。

后邊的博客將會更新幾個zookeeper開發實例,例如分布式配置系統、分布式鎖的實現。

 

參考地址:http://zookeeper.apache.org/doc/r3.4.6/

參考書籍:《hadoop權威指南》


免責聲明!

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



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