Zookeeper官網文檔—第二章 2.概述-概述

介紹

該文檔是指在指導開發者利用Zookeeper協同服務開發分佈式服務應用.它包含許多概念和實用的知識.

該指南的前四部分主要闡述了Zookeeper許多高級概念.這能夠幫助開發者理解Zookeeper是如何工作的和怎樣利用Zookeeper去工作.文檔中不包含源代碼,但是它假定你對分佈式系統的問題比較熟悉. 圍繞四個方面:

接下來的四個方面提供實際的編程知識.有這些:

這本書的最後是一個附錄,包含一些有用的連接和Zookeeper的相關信息.

本文檔的大多數信息都是作爲獨立的參考資料而編寫的. 但是,在啓動你得第一個Zookeeper應用之前,你應該至少閱讀Zookeeper數據模型ZooKeeper基本操作 的相關概念. 並且, 簡單的應用實例 [tbd]可以幫助你來理解Zookeeper客戶端應用的基本結構.

ZooKeeper數據模型

ZooKeeper有一個分層命名空間,類似於一個分佈式文件系統.唯一的不同是命名空間中的每一個節點能夠存儲數據,子節點也是. 它看起來像是文件系統,並且允許文件也是一個目錄. 一個規範的、絕對的、斜槓分隔的路徑來表示一個節點路徑. 不存在相對路徑. 任何符號只要符合以下約束條件都可以被使用:

  • null字符(\u000)不能在一個路徑名稱被使用.(這會導致C語言編程的問題.)

  • 下列字符不能被使用,因爲他們不能被很好的被展示:\u0001 - \u0019 and \u007F - \u009F.

  • 下列字符不允許使用: \ud800 -uF8FFF, \uFFF0 - uFFFF.

  • "."字符可以和其他字符組合使用,但是 "." and ".."不能被單獨作爲一個節點和路徑使用,因爲Zookeeper不能使用相對路徑.下面是一些無效的例子: "/a/b/./c" 或者 "/a/b/../c".

  • 這個標識"zookeeper"是保留的.

ZNodes

在Zookeeper數中每一個節點都被稱爲znode. Znodes包含一個stat數據結構,包含版本號、acl改變. sta數據結構也有時間戳. 通過版本號和時間戳Zookeeper能夠檢查緩存和協調更新. 每次zode的數據改變,版本號會增加. 例如,每當客戶端獲取數據時,會同時獲取數據的版本. 當客戶端執行一個更新或刪除時,它必須提供要改變節點數據的版本號. 如果它提供的版本號不等於真實數據的版本號,更新將失敗. (這個行爲也可以被重寫,更多信息看...)[tbd...]

注意

在分佈式應用中,node這個詞可以用來表示一臺主機,一個服務,一個集羣中的成員,一個客戶端進程,etc. 在Zookeeper文檔中,znodes表示一個數據節點. 服務表示組成Zookeeper服務的機器; quorum peers表示組成集羣的機器; 客戶端表示任何使用Zookeper服務的主機或進程.

一個znode是需要程序員熟悉的重要抽象概念. Znodes有幾個值得注意的特徵.

觀察者

客戶端能夠爲znodes設置觀察者. zonde節點發生改變時會觸發觀察者並在之後移除觀察者. 當觀察者被觸發,Zookeeper會給客戶端發送一個通知.關於觀察者更多的內容,可以查看ZooKeeper Watches部分.

數據訪問

在命名空間的每個znode的數據存儲操作讀和寫都是原子性的. 讀操作讀取與這個znode節點相關的全部數據,寫操作更新znode的全部數據. 每個節點都有一個訪問控制清單(ACL)約束了誰能進行操作.

ZooKeeper不是爲了設計成一般數據庫或者爲大對象數據存儲而做的. 相反,他是用來管理協調數據.  數據能夠以配置信息、狀態信息、集合點等形式存在. 各式各樣分佈式數據的共同點是它們都比較小. 以千字節爲標準. Zookeeper客戶端和服務端有一個健康檢查來確保znodes節點的數據小於1M, 然而數據通常情況下會更小. 操作大的數據會導致操作花費更多的時間,會影響操作的延遲,因爲需要在網絡和存儲媒介中傳輸數據需要消耗額外的時間. 處理該數據的通常模式是將該數據存儲在一個大容量的存儲系統,例如NFS和HDFS,然後在Zookeeper中去存儲數據的保存位置.

臨時節點

ZooKeeper也有臨時節點的概念. 這些znodes存活的時間和創建這個節點的會話有效期是一樣的. 當會話結束,znode會被刪除. 因爲這個原因,所以臨時的znodes是不允許有孩子節點的.

順序節點 -- 唯一名稱

當創建一個znode時,你可以請求Zookeeper在路徑的末尾添加一個自增計數器. 對於父節點來說這個計數器是獨一無二的. 計數器的格式是%010d -- 這是一個十位數.(客戶端以這種格式化來簡化排序), i.e. "<path>0000000001". 看 Queue Recipe 關於這個特徵使用的例子. 注意:這個計數器用來存儲下一個序列號是一個4字節的數,當增加到2147483647 之後,計數器會溢出.

Zookeeper中的時間

ZooKeeper跟蹤時間的多種方式:

  • Zxid

    每次改變Zookeeper的狀態都會接收一個zxid形式的標誌.(ZooKeeper事務Id). 這裏展示了所有Zookeeper變更順序.每次改變都會獲得一個獨一無二的zxid,如果zxid1小於zxid2,那麼zid1產生在zxid2之前.

  • version

    每次改變節點都會使節點的版本號之一的增加. 這三個版本號是:version (一個節點數據的變化次數), cversion (一個節點的孩子節點的變化次數), 和 aversion (一個節點ALC的變化次數).

  • Ticks

    當使用集羣Zookeeper時,服務使用ticks作爲事件的定義時間,例如狀態更新、session超時、集羣節點間連接超時時間. 這個tick時間僅僅間接通過最小session超時時間暴露出來. (2個tick的時間); 如果一個客戶端請求session超時時間是小於最小超時時間,這個服務會告訴客戶端的超時時間值等於最小超時時間.

  • Real time

    ZooKeeper不使用real time時間或者時鐘時間,除了在創建或修改znode時將該時間放入stat結構中.

ZooKeeper Stat Structure

每個Zookeeper的znode的stat結構由以下這些字段組成:

  • czxid

    znode節點創建時的事務ID.

  • mzxid

    znode最後修改時間的事務ID

  • pzxid

    znode孩子節點最後修改時的事務ID.

  • ctime

    znode被創建時的時間,以毫秒爲時間單位.

  • mtime

    znode被修改時的時間,以毫秒爲時間單位.

  • version

    znode節點數據改變的次數

  • cversion

    znode孩子節點改變的次數

  • aversion

    znode節點ACL變化的次數

  • ephemeralOwner

    如果這個節點是臨時節點,那麼這個標識znode的所屬會話ID. 如果不是臨時節點,那麼會是0.

  • dataLength

    znode數據的長度

  • numChildren

    znode孩子節點的數量

ZooKeeper會話

通過一種使用一種開發語言可以使客戶端與服務端創建一個句柄而與Zokkeeper服務建立會話. 一旦建立連接,處理狀態會處於CONNECTINS狀態,客戶端庫會嘗試和組成Zookeeper服務的其中一臺服務建立連接,這時處理狀態會轉變爲CONNECTEDz狀態. 在正常操作時,將會處於這兩個狀態之一的狀態.  如果遇到不可恢復的錯誤,例如會話超時、身份驗證失敗、應用處理句柄關閉,句柄狀態會變爲CLOSED狀態. 下面圖標顯示Zokeeper客戶單可能的狀態變化:

創建客戶端會話的代碼字符串應該包含一個由逗號分隔的host:port組成,每一個單元相當於一個Zookeeper服務. (e.g. "127.0.0.1:4545" 或 "127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002"). ZooKeeper客戶端庫會選擇任意的服務嘗試連接. 如果連接失敗,或者客戶端和服務端因爲任何原因斷開連接,客戶端會自動選擇列表中下一個服務自動連接,直到建立連接爲止.

Added in 3.2.0: 一個可選的"chroot"後綴也支持追加連接字符串. 之後運行的客戶端命令都會與這個根節點相關(類似於unix chroot命令). 使用起來可以像這樣: "127.0.0.1:4545/app/a" 或 "127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002/app/a"在這裏客戶端的根節點應該是"/app/a",所有節點都會與這個根節點相關聯 - ie getting/setting/etc... 在這個操作中"/foo/bar"實際結果是運行在"/app/a/foo/bar" (從服務器角度). 這個特徵在多租戶環境中是特別有用的,Zokkeeper服務的每個用戶都使用不同的根節點. 這讓重用變得更加簡單,每個用戶都能在好像在根節點"/"編寫程序,而真實的地址(say /app/a)在部署時確定.

當客戶端從Zokeeper服務獲得一個句柄,Zokeeper會創建一個Zookeeper會話,標識爲一個64位的數字,它會被分配給客戶端. 如果客端戶連接不同的Zookeeper服務,它會將會話ID作爲連接握手的一部分一同發送.  作爲一個安全保障,服務端會創建一個讓任意Zookeeper服務都能夠驗證的seesion id 密碼. 這個密碼會在客戶端與會話建立連接後與session id 一同發送. 每當客戶端與一個新服務建立會話時,它會和會話ID一同發送.

Zookeeper客戶端庫和Zookeeper會話創建連接時,有一個參數是以毫秒爲單位的會話超時時間. 客戶端發送一個請求超時時間,而服務端則回覆客戶端一個超時時間. 當前實際的超時時間最小是tick時間的兩倍,最大是tick時間的20倍.  可以使用Zookeeper客戶端API來協商設定該值.

當一個客戶單(會話)從Zokeeper集羣中分掉線時,它會查詢當會話創建時的服務列表. 最終,當客戶端和其中的一個服務建立連接,會話會轉爲爲"connected" 狀態 (如果在會話超時時間內回覆連接)或者會轉變爲"expired"狀態(如果在會話超時時間之後回覆連接). 在斷開連接時不應創建一個新的會話對象(在C中是一個新的Zookeeper類或者Zookeeper句柄). Zookeeper客戶端庫會爲你處理句柄重連.尤其是我們內置構建的客戶端庫會處理類似"羊羣效應"這樣的問題(從衆心理)等...  只有當通知你會話過期時,才應創建一個新的連接.

會話過期由Zookeeper集羣進行管理,而不由客戶端進行管理. 當客戶端與集羣建立連接時,會提供一個上面所說的超時時間. 這個值被用來確定客戶端會話合適過期. 當客戶端與集羣在制定的超時時間內沒有心跳,超時會發生.(i.e. no heartbeat).  在會話超時時集羣會刪除所有屬於這個會話的臨時節點,並立刻通知所有客戶端這個改變. (任何管着這些znodes的客戶端). 此刻會話過期的客戶端與集羣依然是斷開連接的,他不會收到過期通知,除非它能夠與集羣重新建立連接. 這個客戶端會一直停留在斷開連接的狀態,直到TCP連接能夠與集羣重新建立連接. 此時過期Session的觀察者會收到"會話過期"的消息.

對於一個過期會話的觀察者,其觀察者的狀態變化:

  1. 'connected' : 會話和集羣是連接和通信的 (client/server運行正常)

  2. .... 客戶端與集羣斷開連接

  3. 'disconnected' : 客戶單與集羣失去通信

  4. 轉瞬間,在超時時間過後,集羣失效這個會話,客戶單不能收到任何通知,因爲它此時已經與集羣斷開連接.

  5. 轉瞬間,客戶單恢復與集羣的網絡連通性.

  6. 'expired' : 最後客戶端與集羣恢復連接,客戶端會收到過期消息.

Zookeeper建立會話連接的另一個參數是默認觀察者. 當客戶端發生任何狀態改變時觀察者都會收到通知. 例如如果客戶端失去與服務器連接,客戶端會收到通知,或者如果客戶端會話超時也一樣. 這個監聽者應該考慮初始狀態到斷開狀態.(i.e. 在任何狀態變動時間之前通過客戶端庫發送給觀察者). 對於一個新的連接,首要事件應該是通知觀察者會話連接事件.

客戶端通過發送請求使會話保持存活. 如果會話在一段時間是空閒的,會導致會話超時,客戶端會發送一個PING請求去保持會話的活躍. 這個PING請求不僅僅讓Zookeeper服務端知道客戶端是存活的,還允許客戶端確認連接的Zookeeper服務是活躍的. PING的時間是足夠保守的合理時間來確保發現一個死掉的連接和恢復一個新的服務.

一旦與服務端建立連接成功,當客戶單發生connectionloss異常時(在C中是異常代碼,在Java中是異常,詳細情況查看API文檔)一般有兩種情況,同步或異步操作導致:

  1. 當執行一個操作時,會話已經長時間失效.

  2. 當等待服務器端操作時,Zookeeper客戶端斷開連接,例如:等待一個異步操作.

Added in 3.2.0 -- SessionMovedException. 用一個通常不被客戶單發現的異常被稱爲SessionMovedException. 這個異常的原因是一個已經建立連接的會話重新與一臺不同的服務建立連接並接受請求. 通常導致這個異常的原因是客戶端發送一個請求到一個服務器,但是網絡數據延遲,所以客戶端超時並連接到一臺新的服務器.  當延遲的數據發送到第一個服務器,舊的服務器會發現會話已經移除,客戶端連接被關閉. 客戶端通常情況下不會發現這個錯誤,因爲客戶端不會從哪些老的連接中讀取數據.(老的連接通常是關閉的). 另一種情況也會發生這種情況,當兩個客戶端都嘗試使用同一個session id和密碼恢復同一個連接時. 其中一臺客戶單會恢復連接,而另一臺客戶端將會斷開連接.(原因是它會一直重新嘗試連接會話).

ZooKeeper觀察者

在Zookeeper所有讀取操作 - getData(), getChildren(), 和 exists() - 可以選着設置一個watch作爲一個監聽者. 這是Zokkeeper種watch的定義 :一個監聽事件只會觸發一次,當監聽到數據的改變,會發送給設置了監聽者的客戶端. 關於觀察者的定義,有三個點需要思考:

  • 觸發一次

    當數據發生變化時,一個監聽事件會發送給客戶端. 例如,如果一個客戶端一個客戶端執行了getData("/znode1",true),之後對/znode1節點執行修改和刪除操作,客戶端將會觸發一個關於/znode1的觀察者事件.如果/znode1節點又一次改變,不會再次觸發觀察者事件,除非再次通過另外一個getData("/znode1",true)設置一個新的觀察者.

  • 發送客戶端

    意味着事件可能在送達客戶端的路上,但是在操作成功的返回碼到達發起這個變更操作的客戶端之前,事件可能還沒到達監聽的客戶端. 觀察者會異步發送到觀察者. Zookeeper提供一個順序保障:客戶端從來不會看到它設置的監聽者改變,知道第一個發現監聽事件.網絡延遲或者其他的因素可能導致不同的客戶端能夠在不同時間發現觀察者並獲得返回碼.關鍵的一點是每一個客戶端看到每一個事件有一個始終如一的順序.

  • 被設置監聽的數據

    這是指一個節點能夠變化的不同方式. 可以認爲Zokkeeper有兩個監聽列表: 數據監聽和孩子節點監聽. getData() 和 exists() 設置數據監聽器. getChildren()設置孩子觀察者. 二者選一,可以根據返回數據的類型來設置觀察者. getData() and exists()會返回關於節點數據的信息, 而 getChildren()會返回一個孩子列表. 因此, setData()會觸發znode設置的數據觀察者. 一個成功的 create()會觸發一個數據監聽者. 一個成功的delete()會同時觸發數據監聽者和一個子節點監聽者(因爲可能沒有更多的子節點).

在客戶端連接服務端時,監聽者會被保存在本地. 這使得觀察者可以輕量級、可維護、可分派. 當客戶端連接一個新的服務,監聽器會觸發一些會話事件. 當與服務器端斷開連接,觀察者將不會收到通知. 當客戶端重連,如果需要所有之前註冊的監聽者將會重新註冊並且觸發. 這是顯而易見的. 這裏有可能導致監聽者丟失:如果在斷網期間,znode被創建或刪除,一個已創建的節點監聽器沒有創建,將會丟失.

監聽者的含義

我們可以用讀取Zookeeper狀態的三個調用方法來設置觀察者:exists, getData, 和 getChildren. 下面是一個觀察者能夠觸發和調用的事件列表:

  • Created event:

    激活調用exitsts.

  • Deleted event:

    激活調用exitsts,getData和getChildren.

  • Changed event:

    激活調用exists和getData.

  • Child event:

    激活調用getChildren

關於觀察者的Zookeeper保障是什麼.

關於觀察者,Zookeeper提供這些保障:

  • 觀察者是根據其他事件、其他觀察者、異步應答而確定的.Zookeeper客戶端庫確保事件都能順序被分發.

  • 客戶端會在看到znode節點對應數據之前發現觀察者事件.

  • Zookeeper服務的觀察者事件的順序對應Zookeeper服務更新的順序.

關於觀察者要記住的事情

  • 觀察者只能被觸發一次;如果你想獲得其未來的改變通知,那你必須設置另一個觀察者.

  • 因爲觀察者只能被觸發一次,你不能發現在獲得事件和發送一個請求設置一個新的觀察這間的Zookeeper的任何變化. 準備處理這種在獲得觀察者事件和再次設置觀察者事件之間的znode節點多次改變的情況. (你可能不關注,但是至少應該認識到它可能發生)

  • 一個觀察者對象,或者方法/上下文對,爲一個通知只會被觸發一次. 例如,如果相同的觀察者在exitsts和getData調用中註冊到了相同的文件,這個文件被刪除,觀察者對象對於這個文件只會觸發一次刪除通知.

  • 當你從服務斷開連接(例如,服務異常),在你重連之前觀察者不會收到任何觀察者. 由於這個原因會話事件會被髮送給所有未處理的監聽器. 使用會話事件應該進入安全模式:你將不會在斷開期間接受到任何事件,所以你在該模式下應該保持謹慎.

ZooKeeper使用ACLs控制訪問

ZooKeeper使用ACL來控制訪問znodes(Zookeeper數據樹上的數據節點). ACL的實現與UNIX的文件訪問權限十分相似: 它使用權限bit來允許/拒絕一個節點和比特許可範圍的各種操作. 不同於傳統的UNITX權限系統,一個Zookeeper節點沒有三個傳統作用域的限制:user(文件所屬用戶)、group,和world(其他). ZooKeeper沒有節點所有者的概念. 而是ACL指定集合,與該權限相關的ID集合.

也要注意ACL僅適用於一個特定的znode. 它不適用於其子節點.例如,如果/appp僅僅被ip爲172.16.16.1的讀取,/app/status是有開放全部讀取權限的,任何人都可以讀取/app/status;ACLs不是遞歸的.

Zookeeer支持可插拔式的插件認證方案. Ids指定使用這個形式scheme:id,shceme是ID對應的認證方案. 例如, ip:172.16.16.1是一個主機地址爲172.16.16.1的ID.

當客戶端連接Zookeeper並進行認證,Zookeeper將與符合這個客戶端的所有ids關聯起來. 當客戶端試圖訪問一個節點時,這些ids會檢查znodes節點的ALC.  ACLs是由成對組成的(scheme:expression,perms). ACLs are made up of pairs of (scheme:expression, perms). 表達式的格式指定了權限. 例如,這個鍵值對給所有19.22開頭的客戶端體哦概念股了讀取權限.

ACL權限

ZooKeeper支持如下權限:

  • CREATE: 你可以創建一個子節點.

  • READ: 你可以讀取節點的數據和獲得子節點列表.

  • WRITE: 你可以設置節點的數據

  • DELETE: 你可以刪除一個子節點

  • ADMIN: 你可以設置權限

CREATE和DELETE權限從WRITE權限中分離開來,爲了更好的訪問控制. CREATEDELETE的情況如下:

你想要A有權限設置節點數據,而不能CREATEDELETE子節點.

CREATE而無DELETE: 客戶端在父目錄發起創建Zookeeper節點的請求.你想要所有客戶端都能夠添加,但是隻有發起該請求的能夠刪除.(這個情況類似與追加文件權限)

而且,ADMIN權限存在是因爲Zookeeper沒有文件所有者的概念. 某種意義上,ADMIN權限就相當於文件所有者. ZooKeeper不支持LOOKUP權限(目錄上執行權限位允許你查看,即使你不能列出目錄). 所有人都隱含LOOKUP權限. 允許你查看節點狀態,但是不能進行其他操作.(有些問題,在一個不存在的節點上執行調用zoo_exists(),沒有權限進行安全檢查.)

內置的ACL方案

ZooKeeeper有如下內置構建:

  • world 有一個獨立ID, 代表任意一個人.

  • auth 不使用任何ID,代表任何權限使用者.

  • digest 使用用戶名:密碼字符串生成一個MD5,當作時ACL ID. 認證時通過發送用戶名:密碼的明文來進行的. 當使用ACL時,表達式將會使用用戶名:base64,base64時SHA1密碼加密.

  • ip 使用客戶端IP作爲一個ACL ID身份. 這個ACL表達式的格式爲addr/bits ,此時addr中的有效位與客戶端addr中的有效位進行比對.

ZooKeeper C client API

The following constants are provided by the ZooKeeper C library:

  • const int ZOO_PERM_READ; //can read node’s value and list its children

  • const int ZOO_PERM_WRITE;// can set the node’s value

  • const int ZOO_PERM_CREATE; //can create children

  • const int ZOO_PERM_DELETE;// can delete children

  • const int ZOO_PERM_ADMIN; //can execute set_acl()

  • const int ZOO_PERM_ALL;// all of the above flags OR’d together

The following are the standard ACL IDs:

  • struct Id ZOO_ANYONE_ID_UNSAFE; //(‘world’,’anyone’)

  • struct Id ZOO_AUTH_IDS;// (‘auth’,’’)

ZOO_AUTH_IDS empty identity string should be interpreted as “the identity of the creator”.

ZooKeeper client comes with three standard ACLs:

  • struct ACL_vector ZOO_OPEN_ACL_UNSAFE; //(ZOO_PERM_ALL,ZOO_ANYONE_ID_UNSAFE)

  • struct ACL_vector ZOO_READ_ACL_UNSAFE;// (ZOO_PERM_READ, ZOO_ANYONE_ID_UNSAFE)

  • struct ACL_vector ZOO_CREATOR_ALL_ACL; //(ZOO_PERM_ALL,ZOO_AUTH_IDS)

The ZOO_OPEN_ACL_UNSAFE is completely open free for all ACL: any application can execute any operation on the node and can create, list and delete its children. The ZOO_READ_ACL_UNSAFE is read-only access for any application. CREATE_ALL_ACL grants all permissions to the creator of the node. The creator must have been authenticated by the server (for example, using “digest” scheme) before it can create nodes with this ACL.

The following ZooKeeper operations deal with ACLs:

  • int zoo_add_auth (zhandle_t *zh,const char* scheme,const char* cert, int certLen, void_completion_t completion, const void *data);

The application uses the zoo_add_auth function to authenticate itself to the server. The function can be called multiple times if the application wants to authenticate using different schemes and/or identities.

  • int zoo_create (zhandle_t *zh, const char *path, const char *value,int valuelen, const struct ACL_vector *acl, int flags,char *realpath, int max_realpath_len);

zoo_create(...) operation creates a new node. The acl parameter is a list of ACLs associated with the node. The parent node must have the CREATE permission bit set.

  • int zoo_get_acl (zhandle_t *zh, const char *path,struct ACL_vector *acl, struct Stat *stat);

This operation returns a node’s ACL info.

  • int zoo_set_acl (zhandle_t *zh, const char *path, int version,const struct ACL_vector *acl);

This function replaces node’s ACL list with a new one. The node must have the ADMIN permission set.

Here is a sample code that makes use of the above APIs to authenticate itself using the “foo” scheme and create an ephemeral node “/xyz” with create-only permissions.

Note

This is a very simple example which is intended to show how to interact with ZooKeeper ACLs specifically. See .../trunk/src/c/src/cli.c for an example of a C client implementation

#include <string.h>
#include <errno.h>

#include "zookeeper.h"

static zhandle_t *zh;

/**
 * In this example this method gets the cert for your
 *   environment -- you must provide
 */
char *foo_get_cert_once(char* id) { return 0; }

/** Watcher function -- empty for this example, not something you should
 * do in real code */
void watcher(zhandle_t *zzh, int type, int state, const char *path,
             void *watcherCtx) {}

int main(int argc, char argv) {
  char buffer[512];
  char p[2048];
  char *cert=0;
  char appId[64];

  strcpy(appId, "example.foo_test");
  cert = foo_get_cert_once(appId);
  if(cert!=0) {
    fprintf(stderr,
            "Certificate for appid [%s] is [%s]\n",appId,cert);
    strncpy(p,cert, sizeof(p)-1);
    free(cert);
  } else {
    fprintf(stderr, "Certificate for appid [%s] not found\n",appId);
    strcpy(p, "dummy");
  }

  zoo_set_debug_level(ZOO_LOG_LEVEL_DEBUG);

  zh = zookeeper_init("localhost:3181", watcher, 10000, 0, 0, 0);
  if (!zh) {
    return errno;
  }
  if(zoo_add_auth(zh,"foo",p,strlen(p),0,0)!=ZOK)
    return 2;

  struct ACL CREATE_ONLY_ACL[] = {{ZOO_PERM_CREATE, ZOO_AUTH_IDS}};
  struct ACL_vector CREATE_ONLY = {1, CREATE_ONLY_ACL};
  int rc = zoo_create(zh,"/xyz","value", 5, &CREATE_ONLY, ZOO_EPHEMERAL,
                      buffer, sizeof(buffer)-1);

  /** this operation will fail with a ZNOAUTH error */
  int buflen= sizeof(buffer);
  struct Stat stat;
  rc = zoo_get(zh, "/xyz", 0, buffer, &buflen, &stat);
  if (rc) {
    fprintf(stderr, "Error %d for %s\n", rc, __LINE__);
  }

  zookeeper_close(zh);
  return 0;
}
      

可插拔式的ZooKeeper認證

ZooKeeper在不同的運行環境中採用不同的認證方案運行. 所以它有一個完整的可插拔式認證框架. 即使式內置的認證設計也是使用這一可插拔式認證框架.

理解認證框架是如何運行的,首先你必須理解兩個重要的認證行爲. 框架首先必須進行客戶端認證. 這通常發生在客戶端連接服務端的一瞬間,會發送客戶端的驗證信息或收集的信息,與連接關聯起來. 框架處理的第二個操作是從ACL發現與客戶端對應的條目.ACL條目是<idspec,permissions>對. idspec可以是一個與認證信息相關聯簡單的字符串,或者是是一個表達式,與評估信息進行比較.  它由認證插件來完成匹配. 這裏有一個認證插件必須實現的接口.

public interface AuthenticationProvider {
    String getScheme();
    KeeperException.Code handleAuthentication(ServerCnxn cnxn, byte authData[]);
    boolean isValid(String id);
    boolean matches(String id, String aclExpr);
    boolean isAuthenticated();
}
    

第一個getScheme()方法返回插件標識字符串. 因爲我們支持多個權限認證方法,所以一個權限認證證書或者idspec總是會添加scheme作爲前綴. ZooKeeper服務使用從認證插件返回的scheme來確定shceme適用的ids.

當一個客戶端發送與連接相關的權限認證信息時,handleAuthentication方法被調用. 客戶端指定信息對應的方案. ZooKeeper服務將這個信息傳遞給可插拔認證,這個getScheme需要與客戶端傳遞的scheme相一致. 如果這個信息是錯誤的,handleAuthentication將會返回一個錯誤碼,或者它會使用cnxn.getAuthInfo().add(new Id(getScheme(), data))與信息相關聯.

這個認證插件包含配置和ACLs. 當一個節點設置一個ACL時, ZooKeeper服務將條目中id部分傳遞給isVlid(String id)方法. 由插件來驗證id是否是正確的格式. 例如, ip:172.16.0.0/16一個正確的ID,但是ip:host.com則不是..如果新的ACL包含一個"認證"條目,isAuthenticated被使用來看看將與連接有關聯的認證信息添加到ACL.有些schemes不應該包含認證.例如,如果auth已經確定,則客戶端IP不應被認爲是一個要加入ACL的ID.

當檢查ACL時,ZooKeeper會調用matches(String id, String aclExpr)方法. 它需要匹配含有相關ACL條目的客戶端認證信息. 爲了發現那個應用到客戶端的標識, ZooKeeper將會找出每一個標識的scheme,如果有一個對應scheme的客戶端認證信息,matches(String id, String aclExpr)方法會在使用了提前被添加到handleAuthentication連接的驗證信息的id和用於ACL標識id的aclExpr這兩個參數後被調用.認證插件使用自身邏輯和認證設計去確認id是否屬於aclExpr.

身份認證插件由兩部分組成:ip和digest. 額外的插件可以通過系統屬性添加. 啓動Zookeeper服務時將會查詢以"zookeeper.authProvider"爲前綴的系統屬性和解析這些屬性的值作爲認證插件的類名. 這些屬性可以使用-Dzookeeeper.authProvider.X=com.f.MyAuth的方式設置或者通過在服務配置文件中添加條目的方式實現:

authProvider.1=com.f.MyAuth
authProvider.2=com.f.MyAuth2
    

注意應該確保屬性的後綴名是獨一無二的. 如果有兩個相同的值,例如:-Dzookeeeper.authProvider.X=com.f.MyAuth -Dzookeeper.authProvider.X=com.f.MyAuth2, 只會使用其中一個. 另外所有服務都必須有相同的插件定義,否則使用了插件提供的認證的客戶端會在連接一些服務器時存在問題.

一致性保障

ZooKeeper是一個高性能,可擴展服務. 讀和寫都被設計的十分迅速,雖然讀要比寫更加迅速. 這是因爲在某些讀的情況下,Zookeeper可以使用舊的數據,這得益於Zookeeper的一致性保障:

連續性保障

來自客戶端的更新將會按照它們的發送順序被應用.

原子性

更新要麼成功,要麼失敗 -- 沒有中間結果.

單獨系統視圖

客戶端將會看到與其連接服務器的相同服務視圖.

可靠性

Once an update has been applied, 它將會一直存在,知道一個客戶端再次更新. 這個擔保有兩個推論:

  1. 如果客戶端獲得一個成功的返回碼,這個更新將會被應用. 有些錯誤(通訊錯誤,超時錯誤)客戶端不會知道更新是否被應用.我們儘量將錯誤降到最低,但是保障只能是在返回成功碼時有用(這在Paxos算法中被稱爲單調性情況)

  2. 任何已被客戶端看到的更新,讀請求或者更新成功的,當從服務異常中恢復過來時也不會回滾.

及時性

客戶端的系統視圖在一定時間內是最新的.(在命令的數十秒內). 系統的改變在這個範圍內既不會被客戶端看見,客戶端也不會知道服務的運行中斷。

使用這些一致性保障很容易構造高級的方法,例如:leader選舉、障礙、隊列、在客戶端構造可撤銷的讀寫鎖.(附件不被Zookeeper需要). 更多內容看 Recipes and Solutions.

注意

有時候開發可能會錯誤的以爲Zookeeper有事實上不存在的保障. 比如:

同一時間一致的跨客戶端視圖

ZooKeeper不保障在同一時刻,兩個不同的客戶端會擁有完全相同的數據視圖. 導致這些情況的原因,比如網絡延遲,一個客戶端可能在另一個客戶端獲得改變通知之前執行更新. 考慮到擁有兩個客戶端的情況,A和B. 如果客戶端給znode /a從0賦值爲1,然後告訴B去讀取/a,B可能仍然讀取到舊的值0,取決於它連接的是哪個服務器. 如果客戶端A和客戶端B取得相同的值是十分重要的,那麼客戶端B應該在讀取數據前執行Zookeeper API方法sync().

所有,Zookeeper不能保障所有服務同時發生改變,但是Zookeeper原始(方法)可以用其來構造更加高級的客戶端同步方法. (關於更多信息,參考ZooKeeper Recipes. [tbd:..]).

綁定

Zookeeper客戶端類庫有兩種語言:Java和C. 下面是它們的描述.

Java綁定

有兩個庫來組成Zookeeper Java綁定: org.apache.zookeeperorg.apache.zookeeper.data. 其他包是構成Zookeeper的內部實現或者服務實現的部分. org.apache.zookeeper.data 包主要由生產類組成,這些類可以僅用作容器.

Zookeeper Java客戶端主類是Zookeeper類.  它的兩個構造函數不同僅僅在於可選的session id 和 password. ZooKeeper支持跨進程實例的會話恢復. Java程序能夠將session id和password保存在一個穩定的存儲中,重新啓動,可以恢復會話所使用的早先的進程實例.

當一個Zookeeper對象創建時,兩個線程同時也被創建.  一個是IO線程,另一個用於事件線程. 所有的IO發生在IO線程(使用Java NIO). 所有事件回調發生在事件線程. 會話維持,例如Zookeeper服務重連和維持心跳都在IO線程. 同步應答也在IO線程中處理. 所有異步應答方法和監聽事件處理都在事件線程. 對於這種設計,應注意以下這些事情:

  • 所有異步方法調用和監聽者回調都是順序的,一次一個. 調用者可以進行它們希望的任何處理,但是在這個時間內不會處理任何其他回調.

  • 回調不會阻塞IO線程的處理或者同步調用的處理.

  • 同步調用可能不會按照順序返回. 例如,假設一個客戶端要做以下處理: 一個請求異步讀取節點/a,並且設置監聽器,然後再讀回調完成時,發起一個同步讀.(這也許不是個好辦法,但是不是非法的,這只是個簡單的例子)

    注意,如果在異步讀和同步讀間發生了變化,在同步讀響應之前,客戶端庫會接收到一個關於/a改變的事件,但是因爲異步讀回調是會阻塞事件隊列,在觀察者事件處理之前,同步讀將會返回/a的新值.

最後, 與shutdown相關聯的是清晰的:一旦一個Zookeeper對象關閉或者接收到一個異常事件(SESSION_EXPIRED 和 AUTH_FAILED), Zookeeper對象會變爲無效. 關閉後,其兩個線程也會關閉,任何在Zookeeper句柄上的操作都會變得不可預測,應該避免這種情況.


C綁定有一個單線程庫和多線程庫。多線程庫用起來最簡單,並且與JavaAPI很相似。這個庫將創建一個IO線程和事件分發器線程,後者處理連接維護和回調。單線程庫允許ZooKeeper用在事件驅動應用程序中,此時,它暴露事件循環,這與多線程庫中的一樣。

程序包包含兩個共享庫:zookeeper_st和zookeeper_mt,前者僅提供異步API和回調函數,它們可以整合到應用程序的事件循環中。這個庫存在的唯一理由是它是針對那些不支持pthread庫或pthread庫運行不穩定的平臺(即FreeBSD 4.x)。其他情況下,程序開發者應鏈接zookeeper_mt,它同時支持同步和異步API。


安裝

如果你從Apache庫中通過check-out構建客戶端, 參考以下步驟.如果你是通過從Apache中下載源碼的方式構造,則調到步驟3.

  1. 在Zookeeper的頂級目錄(.../trunk)運行ant compile_jute. 這將會在.../trunk/src/c創建一個名爲"generated"的目錄.

  2. 進入至目錄.../trunk/src/c 運行autoreconf -if 需要引導程序 autoconf, automakelibtool. 確保你已經有 autoconf version 2.59 或者已經安裝. 跳至步驟4.

  3. 如果你通過項目源碼構建,unzip/untar源碼包,cd到zookeeper-x.x.x/src/c目錄.

  4. 運行 ./configure <your-options> 去生成markfile.這裏有一些configure支持的的選項,可以用於這些步驟:

    • --enable-debug

      編譯器選項,運行優化和debug信息. (缺省是不運行的.)

    • --without-syncapi

      不支持同步API;zookeeper_mt庫不構建. (缺省是支持.)

    • --disable-static

      不構建靜態庫. (缺省是支持)

    • --disable-shared

      不構建共享庫 (缺省是支持)

    Note

    關於運行的配置信息,參考INSTALL.

  5. 運行make或make install來構建類庫和安裝它們.

  6. 爲Zookeeper生成doxygen文檔, run make doxygen-doc. 所有文件將會被放置在一個叫docs的新目錄. 默認情況下,這個命令只生成HTML. 關於其他文檔格式信息,運行 ./configure --help

Building Your Own C Client

In order to be able to use the ZooKeeper API in your application you have to remember to

  1. Include ZooKeeper header: #include <zookeeper/zookeeper.h>

  2. If you are building a multithreaded client, compile with -DTHREADED compiler flag to enable the multi-threaded version of the library, and then link against against the zookeeper_mt library. If you are building a single-threaded client, do not compile with -DTHREADED, and be sure to link against the zookeeper_st library.

Note

See .../trunk/src/c/src/cli.c for an example of a C client implementation

構建積木:指導Zookeeper操作.

這部分調查了開發人員對Zookeeper服務的全部操作.  與本手冊前面章節相比,它是更底層的信息,但比ZooKeeperAPI參考信息高,它包含如下主題:

處理錯誤

JAVA和C客戶端都可能報錯.  Java客戶端是通過拋出KeeperException,在異常中調用code()將會返回明確的錯誤碼. C客戶端返回一個明確的ZOO_ERRORS枚舉錯誤碼. 對於這兩種語言,API回調都由結果碼指定結果. 關於更多可能錯誤詳細信息,查看API文檔(java是Javadoc,C是doxygan).

連接ZooKeeper

讀取操作

寫操作

處理觀察者

其他Zookeeper操作

程序結果,簡單的例子

[tbd]

陷阱:常見問題和解決方案

所以你現在知道Zookeeper. 它是快速、簡單、你得應用程序工作,但是等等... 有些事情不對.  這裏有些陷阱會導致Zookeeper用戶錯誤:

  1. 如果你使用觀察者,你必須關注連接觀察者事件. 當一個Zookeeper客戶端與服務器斷線, 在重連之前你不會接收到任何改變的通知. 如果你正在監聽一個znode節點存在, 在你連接斷開期間,如果znode是創建或者刪除的你將會失去該事件.

  2. 你必須測試Zookeeper服務是否失效. ZooKeeper服務只要大多數是有效的,就可以有效的. 這個問題:你得應用是否能處理它? 現實中,一個客戶端連接Zookeeper可能斷開. (對於連接丟失,ZooKeeper服務異常和網絡異常是常見原因.) ZooKeeper客戶端類庫關注恢復你得連接並且讓你知道在何時發生.  但是你必須確保你恢復你的狀態和任何失敗的請求. 在測試環境下而不是正式環境下. 在由多個服務組成的Zookeeper服務商測試,使機器重啓.

  3. 客戶單使用的Zookeeper服務列表必須和每一個Zookeeper服務列表一致.  如果客戶端使用Zookeeper服務真實列表的子集,依然可以工作,但是不是最優, 但是如果客戶端列表有不存在zookeeper集羣中的列表則不會工作.

  4. 小心放置你得事務日誌. Zookeeper性能關鍵部分是事務日誌. Zookeepr在應答之前,必須同步事務到媒體中.  一個專門的事務日誌設備是優秀性能的關鍵. 日誌放置在繁忙的服務將會影響性能. 如果你僅有一個存儲設備,將追蹤文件放置在NFS然後增加snapshotCount; 它不會消除問題,但是會降低它.

  5. 設置合適的Java堆大小. 最重要的是避開交換. 沒必要的磁盤訪問肯定會降低你得性. 記住,在Zookeeper中,每一個事情都是順序的,如果有一個請求訪問了磁盤,隊列中的其他請求也將訪問磁盤.

    爲了避開交換,你應該設置你得堆大小爲物理內存大小減去你得操作系統和緩存大小. 確定你配置堆大小的最佳方式是去運行負載測試. 如果因爲某些原因你不能這樣做,那麼對你的估算保守一些,選擇一個剛好低於內存交換的值.例如,在4G的機器上,選擇3G作爲大小的起點.


除了正式的官方文檔外,還有一些其他的信息源,供ZooKeeper開發者參考。

ZooKeeperWhitepaper [url待定]

它明確地討論了ZooKeeper的設計和性能,由Yahoo!Research編寫

API Reference[url待定]

ZooKeeperAPI的完整參考

ZooKeeper Talkat the Hadoup Summit 2008

Yahoo!Research的Benjamin Reed講的一個介紹ZooKeeper的視頻

Barrier andQueue Tutorial

Theexcellent Java tutorial by Flavio Junqueira編寫的一個優秀的Java教程,用ZooKeeper實現了一個簡單的壁壘(barriers)以及producer-consumer 模式的隊列。

ZooKeeper - AReliable, Scalable Distributed Coordination System

ToddHoff (07/15/2008)寫的一篇文章

ZooKeeperRecipes

採用ZooKeeper來實現的各種同步方案(模擬級別的討論):事件處理,隊列,鎖及兩段提交。

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