文章出處

本文假設你已經具有一定分布式計算的基礎知識。你將在第一部分看到以下內容:

  • ZooKeeper數據模型

  • ZooKeeper Sessions

  • ZooKeeper Watches

  • 一致性保證(Consistency Guarantees)

接下來的4小節講述了程序開發的實際應用:

  • 創建模塊——ZooKeeper操作指引

  • 編程語言接口

  • 簡單示例演示程序的結構

  • 常見問題和故障

本文的附錄中包含和ZooKeeper相關的有用信息。

ZooKeeper的數據模型

ZooKeeper有一個類似分布式文件系統的命名體系。區別在于Zookeeper每個一個節點或子節點都可以擁有數據。節點路徑是一個由斜線分開的絕對路徑,注意沒有相對路徑。只要滿足下面要求的unicode字符都可以作為節點路徑:

  • 空字符不能出現在路徑名

  • 不能出現以下字符: \u0001 - \u0019 and \u007F - \u009F

  • 以下字符不允許使用: \ud800 -uF8FFF, \uFFF0-uFFFF, \uXFFFE - \uXFFFF (where X is a digit 1 - E), \uF0000 - \uFFFFF

  • 字符"."可以作為一個名字的一部分, 但是"."和".."不能單獨作為相對路徑使用, 以下用法都是無效的: "/a/b/./c"或者"/a/b/../c"

  • "zookeeper"為保留字符

ZNodes

ZooKeeper樹結構中的節點被稱為znode。各個znode維護著一組用來標記數據和訪問權限發生變化的版本號。這些版本號組成的狀態結構 具有時間戳。Zookeeper使用版本號和時間戳來驗證緩存狀態,調整更新。 每次znode中的數據發生變化,znode的版本號增加。例如,每當一個客戶端恢復數據時,它就接收這個版本的數據,而當一個客戶端提交了更新或刪除記 錄,它必須同時提供這個znode當前正在發生變化的數據的版本。如果這個版本和目前真實的版本不匹配,則提交無效。 __提示,在分布式程序中,一個字節點可以代表一個通用的主機,服務器,集群中的一員,客戶端程序等。但是在Zookeeper中,znode代表數據節 點,Servers代表組成了Zookeeper服務的機器; quorum peers refer to the servers that  make up an ensemble; 客戶端代表任何使用ZooKeeper服務的主機或程序。

znode作為對程序開發來說最重要的信息,有幾個特性需要特別關注下:

Watches 客戶端可以在znode上設置Watch。znode發生的變化會觸發watch然后清除watch。當一個watch被觸發,Zookeeper給客戶端發送一個通知。更多關于watch的內容請查看ZooKeeper Watches一節。

數據存取 命名空間中每個znode中的數據讀寫是原子操作。讀操作讀取znode中的所有數據位,寫操作則替換所有數據。每個節點都有一個訪問權限控制表 (ACL)來標記誰可以做什么。 zookeeper不是設計成普通的數據庫或大型對象存儲的。它是用來管理coordination data。coordination  data包括配置文件、狀態信息、rendezvous等。這些數據結構的一個共同特點就是相對較小——以千字節為準。Zookeeper的客戶端和服務 會檢查確保每個znode上的數據小于1M,實際平均數據要遠遠小于1M。 大規模數據的操作會引發一些潛在的問題并且延長在網絡和介質之間傳輸的時間。如果確實需要大型數據的存儲,那么可以采用如NFS或HDFS之類的大型數據 存儲系統,亦或是在zookeeper中存儲指向存儲位置的指針。

臨時節點(Ephemeral Nodes) zookeeper還有臨時節點的概念,這些節點的生命周期依賴于創建它們的session是否活躍。session結束時節點即被銷毀。也由于這種特性,臨時節點不允許有子節點。

序列節點——命名不唯一 當你創建節點的時候,你會需要zookeeper提供一組單調遞增的計數來作為路徑結尾。這個計數對父znode是唯一的。用%010d的格式——用0來填充的10位數(計數如此命名是為了簡單排序)。例如"0000000001",注意計數器是有符號整型,超過表示范圍會溢出。

ZooKeeper中的時間

zookeeper有很多記錄時間的方式:

  • Zxid(ZooKeeper Transaction Id): zookeeper每次發生改動都會增加zxid,zxid越大,發生的時間越靠后。

  • Version numbers: 對znode的改動會增加版本號。版本號包括version (znode上數據的修改數), cversion (znode的子節點的修改數), aversion (znode上ACL(權限)的修改數)。

  • Ticks : 多個server構成zookeeper服務時,各個server用ticks來標記如狀態上報、連接超時等事件。ticks  time還間接反映了session超時的最小值(兩次tick time);如果客戶端請求的最小session  timeout低于這個最小值,服務端會通知客戶端最小超時置為這個最小值。

  • Real time : 除了每次znode創建或改動時候將時間戳記錄到狀態結構中外,zookeeper不使用時鐘時間。

ZooKeeper狀態結構(Stat Structure)

存在于znode中的狀態結構,由以下各個部分組成:

  • czxid - znode創建產生的zxid

  • mzxid - znode最后一次修改的zxid

  • ctime - znode創建的時間的絕對毫秒數

  • mtime - znode最后一次修改的絕對毫秒數

  • version - znode上數據的修改數

  • cversion - 子節點修改數

  • aversion - znode的ACL修改數

  • ephemeralOwner - 臨時節點的所有者的session id。如果此節點非臨時節點,該值為0

  • dataLength - znode的數據長度

  • numChildren - znode子節點數

ZooKeeper Sessions

客戶端通過創建一個handle和服務端建立session連接。一旦創建完成,handle就進入了CONNECTING狀態,客戶端庫嘗試連接一臺構成zookeeper的server,屆時進入CONNECTED狀態。通常情況下操作會介于這兩種狀態之間。 一旦出現了不可恢復的錯誤:如session中止,鑒權失敗或者應用直接結束handle,則handle會進入到CLOSED狀態。下圖是客戶端的狀態轉換圖:

應用在創建客戶端session時必須提供一串逗號分隔的主機號:端口號,每對主機端口號對應一個ZooKeeper的 server(如:"127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002"),客戶端庫會嘗試連接任意一臺服務 器,如果連接失敗或是客戶端主動斷開連接,客戶端會自動繼續與下一臺服務器連接,直到連接成功。

3.2.0版本新增內容:  一個新的操作“chroot”可以添加在連接字符串的尾部,用來指明客戶端命令運行的根目錄地址。類似unix的chroot命令,例如: "127.0.0.1:4545/app/a" or  "127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002/app/a",說明客戶端會以"/app/a"為根目 錄,所有路徑都相對于根目錄來設置,如"/foo/bar"的操作會運行在"/app/a/foo/bar"。 這一特性在多用戶環境下非常好用,每個使用zookeeper服務的用戶可以設置不同的根目錄。

當客戶端獲得和zookeeper服務連接的handle時,zookeeper會創建一個Zookeeper  session分配給客戶端,用一個64-bit數字表示。一旦客戶端連接了其他服務器,客戶端必須把這個session  id也作為連接握手的一部分發送。出于安全目的,zookeeper給session id創建一個密碼,任何zookeeper服務器都可以驗證密碼。 當客戶端創建session時密碼和session id一起發送到客戶端來,當客戶端重新連接其他服務器時,同時要發送密碼和session id。

zookeeper客戶端庫里有一個創建zookeeper session的參數,叫做session  timeout(超時),用毫秒表示。客戶端發送請求超時,服務端在超時范圍內響應客戶端。session超時最小為2個ticktime,最大為20個 ticktime。zookeeper客戶端API可以協調超時時間。 當客戶端和zookeeper服務器集群斷開時,它會搜索session創建時的服務器列表。最后,當至少一個服務器和客戶端重新建立連 接,session或被重新置為"connected"狀態(超時時間內重新連接),或被置為"expired(過期)"狀態(超出超時時間)。不建議在 斷開連接后重新創建session。ZK客戶端庫會幫你重新連接。特別地,我們將啟發式學習模式植入客戶的庫中來處理類似“羊群效應”等問題。只有當你的 session過期時才重新創建(托管的)。 session過期的狀態轉換圖示例同過期session的watcher:

  1. 'connected' : session正確創建,客戶端和服務集群正常連接

  2. .... 客戶端從服務器集群斷開

  3. 'disconnected' : 客戶端失去和服務器集群的連接

  4. .... 過了一段時間, 超過了集群判定session過期的超時時間, 客戶端并沒有發覺自己和服務集群斷開了連接

  5. .... 又過一段時間, 客戶端恢復了同集群的網絡連接

  6. 'expired' : 最終客戶端重新連上集群,然后被通知已經到期

另一個session建立時zookeeper需要的參數是默認watcher(監視者)。在客戶端發生任何變化時,watcher都會發出通知。 例如客戶端失去和服務器的連接、客戶端session到期等。watcher默認的初始狀態是disconnected。(也就是說任何狀態改變事件都由 客戶端庫發送到watcher)。當新建一個連接時,第一個發送給watcher的事件通常就是session連接事件。

客戶端發送請求會使session保持活動狀態。客戶端會發送ping包(譯者注:心跳包)以保持session不會超時。Ping包不僅讓服務端 知道客戶端仍然活動,而且讓客戶端也知道和服務端的連接沒有中斷。Ping包發送時間正好可以判斷是否連接中斷或是重新啟動一個新的服務器連接。

和服務器的連接建立成功,當一個同步或異步操作執行后,有兩種情況會讓客戶端庫判斷失去連接:

  1. 應用在已經失效的session上調用了一個操作時

  2. zookeeper服務器有未完成的操作,客戶端這時會斷開連接。即服務器有未完成的異步調用時

3.2.0版本新增內容 —— SessionMovedException  一個客戶端無法查看的內部異常SessionMovedException。這個異常發生在服務端收到一個請求,這個請求的session已經在另一個服 務器上重新連接。發生這種情況的原因通常是客戶端發送完請求后,由于網絡延時,客戶端超時重新和其他服務器建立連接,當請求包到達第一臺服務器時,服務器 發現session已經移除并關閉了和客戶端的連接。客戶端一般不用理會這個問題,但是有一種情況值得注意,當兩臺客戶端使用事先存儲的session  id和密碼試圖創建同一個連接時,第一臺客戶端重建連接,第二臺則會被中斷。

ZooKeeper Watches

所有zookeeper的讀操作——getData(), getChildren(),  exists()——都可以設置一個watch。Zookeeper的watch的定義是:watch事件是一次性觸發的,發送到客戶端的。在監視的數據 發生變化時產生watch事件。以下三點是watch(事件)定義的關鍵點:

  • 一次性觸發: 當數據發生變化時,一個watch事件被發送給客戶端。例如,如果一個客戶端做了一次getData("/znode1", true)然后節點/znode1發生數據變化或刪除,這個客戶端將收到/znode1的watch事件。如果/znode1繼續發生改變,不會再有watch發送,除非客戶端又做了其他讀操作產生了新的watch。

  • 發送給客戶端: 這就意味著,事件在發往客戶端的過程中,可能無法在修改操作成功的返回值到達客戶端之前到達客戶端。watch是異步發送給watchers的。 zookeeper提供一種保證順序的方法:客戶端在第一次看到某個watch事件之前不可能看到產生watch的修改的返回值。網絡延時或其他因素可能 導致不同客戶端看到watch并返回不同時間更新的返回值。關鍵的一點是,不同的客戶端看到發生的一切都必須是按照相同順序的。

  • watch依附的數據: 這是說改變一個節點有不通方式。用好理解的話說,zookeeper維護兩組watch:data watch和child  watch。getData()和exists()產生data watch。getChildren()引起child  watch。watch根據數據返回的種類不同而不同。getData()和exists()返回關于節點的數據信息,而getChildren()返回 子節點列表。因此setData()觸發某個znode的data  watch(假設事件成功)。create()成功會觸發被創建的znode上的data watch和在它父節點上的child  watch。delete()成功會觸發data watch和child watch(因為沒有了子節點)。

watch在客戶端已連接上的服務器里維護,這樣可以保證watch輕量便于設置,維護和分發。當客戶端連接了一臺新的服務器,watch會在任何 session事件時觸發。當斷開和服務器的連接時,watch不會觸發。當客戶端重新連接上時,任何之前注冊過的watch都會重新注冊并在需要的時候 被觸發。一般來說這一切都是透明的。只有一種可能會丟失watch:當一個znode在斷開和服務器連接時被創建或刪除,那么判斷這個znode存在的 watch因未創建而找不到。

ZooKeeper如何保證watch可靠性

zookeeper有如下方式:

  • watch與其他事件、watch、異步回復保持有序,Zookeeper客戶端庫確保任何分發都是有序的。

  • 客戶端會在某個監視的znode數據更新之前看到這個znode的watch事件。

  • watch事件的順序由Zookeeper服務端觀察到的更新順序決定。

watch注意事項

  • watch是一次性觸發的;如果你收到watch事件后還想繼續得到后續更改的通知,你需要再生成(設置)一個watch。

  • 由于watch是一次性觸發,你在獲取某事件和發送新的請求來得到watch這個操作之間,無法確保觀察到Zookeeper中那個節點在這期間 的所有修改。你要準備好應付這種情況出現:znode會在收到事件和再次設置新事件(譯者注:對節點的操作)之間發生了多次修改。(你可能并不關心,但是 必須了解這可能發生)

  • watch對象,或是function/context對,只會在得到通知時觸發一次。例如,如果一個watch對象同時用來監控某個目標文件是否存在和監聽getData(),之后那個文件被刪除了。那么這個watch對象只會觸發一次文件刪除事件通知。

  • 如果你斷開了同服務器的連接(例如服務器掛了),你在重新連上之前得不到任何watch。出于這種原因,session  event會被發送給所有重要的watch  handler。可以使用session事件進入安全模式:當斷開連接時你收不到任何事件,這樣你的進程可以在那種模式下穩健地執行。(譯者注:可以通過 發送session event使客戶端進入安全模式(偽斷開連接狀態),在安全模式你可以修改代碼而不用擔心程序收到事件通知)

使用ACL控制ZooKeeper訪問權限

zookeeper使用ACL來控制對znode(zookeeper的數據節點)的訪問權限。ACL的實現方式和unix的文件權限類似:用不同 位來代表不同的操作限制和組限制。與標準unix權限不同的是,zookeeper的節點沒有三種域——用戶,組,其他。zookeeper里沒有節點的 所有者的概念。取而代之的是,一個由ACL指定的id集合和其相關聯的權限。 注意,一個ACL只從屬于一個特定的znode。對這個znode子節點也是無效的。例如,如果/app只有被ip172.16.16.1的讀權限,/app/status有被所有人讀的權限,那么/app/status可以被所有人讀,ACL權限不具有遞歸性。 zookeeper支持插件式認證方式,id使用scheme:id的形式。scheme是id對應的類型方式,例如ip:172.16.16.1就是一個地址為172.16.16.1的主機id。 當客戶端連接zookeeper并且認證自己,zookeeper就在這個與客戶端的連接中關聯所有與客戶端一致的id。當客戶端訪問某個znode時,znode的ACL會重新檢查這些id。ACL的表達式為(scheme:expression,perms)expression就是特殊的scheme,例如,(ip:19.22.0.0/16, READ)就是把任何以19.22開頭的ip地址的客戶端賦予讀權限。

ACL權限

ZooKeeper支持下列權限:

  • CREATE:允許創建子節點

  • READ:允許獲得節點數據并列出所有子節點

  • WRITE:允許設置節點上的數據

  • DELETE:允許刪除子節點

  • ADMIN:允許設置權限

CREATE和DELETE操作是更細的粒度上的WRITE操作。有一種特殊的情況:

  • 你想要A獲得操作zookeeper上某個znode的權限,但是不可以對其子節點進行CREATE和DELETE。

  • 只CREATE不DELETE:某個客戶端在上一級目錄上通過發送創建請求創建了一個zookeeper節點。你希望所有客戶端都可以在這個節點上添加,但是只有創建者可以刪除。(這就類似于文件的APPEND權限)

zookeeper沒有文件所有者的概念,但有ADMIN權限。在某種意義上說,ADMIN權限指定了所謂的所有者。zookeeper雖然不支持 查找權限(在目錄上的執行權限雖然不能列出目錄內容,卻可以查找),但每個客戶端都隱含著擁有查找權限。這樣你可以查看節點狀態,但僅此而已。(這有個問 題,如果你在不存在的節點上調用了zoo_exists(),你將無權查看)

內建ACL模式

ZooKeeper有下列內建模式:

  • world  有獨立id,anyone,代表任何用戶。

  • auth 不使用任何id,代表任何已經認證過的用戶

  • digest 之前使用了格式為username:pathasowrd的字符串來生成一個MD5哈希表作為ACL ID標識。在空文檔中發送username:password來完成認證。現在的ACL表達式格式為username:base64, 用SHA1編碼密碼。

  • ip 用客戶端的ip作為ACL ID標識。ACL表達式的格式為addr/bits,addr中最有效的位匹配上主機ip最有效的位。

ZooKeeper C client API

插件式ZooKeeper認證

zookeeper運行于復雜的環境下,有各種不同的認證方式。因此zookeeper擁有一套插件式的認證框架。內建認證scheme也是使用這 套框架。 為了便于理解認證框架的工作方式,你首先要了解兩種主要的認證操作。框架首先必須認證客戶端。這步操作通常在客戶端連接服務器的同時完成并且將從客戶端發 過來的(或從客戶端收集來的)認證信息關聯此次連接。認證框架的第二步操作是在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判斷哪個id適用于該scheme。 當客戶端發送與連接關聯的認證信息時,handleAuthentication被調用。客戶端指定和認證信息相應的模式。zookeeper把信息傳給認證插件,認證插件的getScheme匹配scheme。實現handleAuthentication的方法通常在判斷信息錯誤后返回一個error,或者在確認連接后使用cnxn.getAuthInfo().add(new Id(getScheme(), data))

認證插件在設置和ACL中都有涉及。當對某個節點設置ACL時,zookeeper服務器會傳那個條目的id給isValid(String id)方法。插件需要判斷id的連接來源。例如,ip:172.16.0.0/16是有效id,ip:host.com是無效id。如果新的ACL包括一個"auth"條目,就用isAuthenticated判斷該scheme的認證信息是否關聯了連接,是否可以被添加到ACL中。一些scheme不會被包含到auth中。例如,如果auth已經指定,客戶端的ip地址就不作為id添加到ACL中。 在檢查ACL時zookeeper有一個matches(String id, String aclExpr)方法。ACL的條目需要和認證信息相匹配。為了找到和客戶端對應的條目,zookeeper服務器尋找每個條目的scheme,如果對某個scheme有那個客戶端的認證信息,matches(String id, String aclExpr)會被調用并傳入兩個值,分別是事先由handleAuthentication 加入連接信息中認證信息的id,和設置到ACL條目id的aclExpr。認證插件用自己的邏輯匹配scheme來判斷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,只有一個會被使用。同樣,所有服務器都必須統一插件定義,否則客戶端用插件提供的認證schemes連接服務器時會出錯。

一致性保證

ZooKeeper是一個高性能,可擴展的服務。讀和寫操作都非常快速。之所以如此,全因為zookeeper有數據一致性的保證:

順序一致性 客戶端的更新會按照它們發送的次序排序。

原子性 更新的失敗或成功,都不會出現半個結果。

單獨系統鏡像 不管客戶端連哪個服務器,它看來都是同一個。

可靠性 一旦更新生效,它就會一直保存到下一次客戶端更新。這就有兩個推論:

  1. 如果客戶端得到成功的返回值,說明更新生效了。在一些錯誤情況下(連接錯誤,超時等)客戶端不會知道更新是否生效。雖然我們使失敗的幾率最小化,但是也只能保證成功的返回值情況。(這就叫Paxos算法的單調性條件)

  2. 客戶端能看到的更新,即使是渡請求或成功更新,在服務器失敗時也不會回滾。

時效性 客戶端看到的系統狀態在某個時間范圍內是最新的(幾十秒內),任何系統更改都會在該時間范圍內被客戶端發現。否則客戶端會檢測到斷開服務。

用這些一致性保證可以在客戶端中構造出更高級的程序如 leader election, barriers, queues, read/write revocable locks(無須在zookeeper中附加任何東西)。更多信息Recipes and Solutions

zookeeper不存在的一致性保證: 多客戶端同一時刻看到的內容相同 zookeeper不可能保證兩臺客戶端在同一時間看到的內容總是一樣,由于網絡延遲等原因。假設這樣一個場景,A和B是兩個客戶端,A設置節點/a下的 值從0變為1,然后讓B讀/a,B可能讀到舊的數據0。如果想讓A和B讀到同樣的內容,B必須在讀之前調用zookeeper接口中的sync()方法。

編程接口

常見問題和故障

下面是一些常見的陷阱:

  1. 如果你使用watch,你必須監控好已經連接的watch事件。當ZooKeeper客戶端斷開和服務器的連接,直到重新連接上這段時間你都收不到任何通知。如果你正在監視znode是否存在,那么你在斷開連接期間收不到它創建和銷毀的通知。

  2. 你必須測試ZooKeeper故障的情況。在大多數服務器都可用的情況下,ZooKeeper是可以維持工作的。關鍵問題是你的客戶端程序是否能 察覺到。在實際情況下,客戶端與ZooKeeper的連接有可能中斷(多數時候是因為Zookeeper故障或網絡中斷)。Zookeeper的客戶端庫 關注于如何讓你重新連接并且知道發生了什么。但是同時你也必須確保能夠恢復你的狀態和發送失敗的請求。努力在測試庫里測出這些問題,而不是在產品里——用 幾臺服務器組成的zookeeper集群測試這個問題,嘗試讓它們重啟。

  3. 客戶端維護的服務器列表必須和現有的服務器列表一致。如果客戶端的列表是現有服務器列表的子集,還可以在非最佳狀態工作,但是如果客戶端列表里的服務器不在現有集群里你就悲劇了。

  4. 注意存放事務日志的位置。性能評測最重要的部分就是日志,ZooKeeper會在回復響應之前先把日志同步到磁盤上。為了達到最佳性能,首選專用 的磁盤來存日志。把日志放在繁忙的磁盤上會降低效率。如果你只有一個磁盤,就把記錄文件放在NFS上然后增加SnapshotCount。這樣雖然無法完 全解決問題,但能緩解一些。

  5. 正確地設置你java的堆空間大小。這是避免頻繁交換的有效措施。無用的訪問磁盤會讓你的效率大打折扣。記住,在ZooKeeper中,一切都是有序的,如果一個服務器訪問了磁盤,所有的服務器都會同步這個操作。

其他資料鏈接請自行官網查看。


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()