其實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權威指南》