翻譯:steve sun 部落格地址:http://sundiontheway.github.io/blog
本文假設你已經具有一定分散式計算的基礎知識。你將在第一部分看到以下內容:
- ZooKeeper資料模型
- ZooKeeper Sessions
- ZooKeeper Watches
- 一致性保證(Consistency Guarantees)
- 建立模組——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"為保留字元
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不使用時鐘時間。
存在於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子節點數
客戶端通過建立一個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:
- 'connected' : session正確建立,客戶端和服務叢集正常連線
- .... 客戶端從伺服器叢集斷開
- 'disconnected' : 客戶端失去和伺服器叢集的連線
- .... 過了一段時間, 超過了叢集判定session過期的超時時間, 客戶端並沒有發覺自己和服務叢集斷開了連線
- .... 又過一段時間, 客戶端恢復了同叢集的網路連線
- 'expired' : 最終客戶端重新連上叢集,然後被通知已經到期
客戶端傳送請求會使session保持活動狀態。客戶端會傳送ping包(譯者注:心跳包)以保持session不會超時。Ping包不僅讓服務端 知道客戶端仍然活動,而且讓客戶端也知道和服務端的連線沒有中斷。Ping包傳送時間正好可以判斷是否連線中斷或是重新啟動一個新的伺服器連線。
和伺服器的連線建立成功,當一個同步或非同步操作執行後,有兩種情況會讓客戶端庫判斷失去連線:
- 應用在session上請求了一個已經失效的操作
- zookeeper伺服器有一個等待中的操作時,客戶端會從那臺伺服器斷開連線。即伺服器有等待的非同步呼叫。
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(因為沒有了子節點)。
ZooKeeper如何保證watch可靠性
zookeeper有如下方式:
- watch與其他事件、watch、非同步回覆保持有序,Zookeeper客戶端庫確保任何分發都是有序的。
- 客戶端會在某個監視的znode資料更新之前看到這個znode的watch事件。
- watch事件的順序由Zookeeper服務端觀察到的更新順序決定。
- watch是一次性觸發的;如果你收到watch事件後還想繼續得到後續更改的通知,你需要再生成(設定)一個watch。
- 由於watch是一次性觸發,你在獲取某事件和傳送新的請求來得到watch這個操作之間,無法確保觀察到Zookeeper中那個節點在這期間 的所有修改。你要準備好應付這種情況出現:znode會在收到事件和再次設定新事件(譯者注:對節點的操作)之間發生了多次修改。(你可能並不關心,但是 必須瞭解這可能發生)
- watch物件,或是function/context對,只會在得到通知時觸發一次。例如,如果一個watch物件同時用來監控某個目標檔案是否存在和監聽getData(),之後那個檔案被刪除了。那麼這個watch物件只會觸發一次檔案刪除事件通知。
- 如果你斷開了同伺服器的連線(例如伺服器掛了),你在重新連上之前得不到任何watch。出於這種原因,session event會被髮送給所有重要的watch handler。可以使用session事件進入安全模式:當斷開連線時你收不到任何事件,這樣你的程式可以在那種模式下穩健地執行。(譯者注:可以通過 傳送session event使客戶端進入安全模式(偽斷開連線狀態),在安全模式你可以修改程式碼而不用擔心程式收到事件通知)
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:允許設定許可權
- 你想要A獲得操作zookeeper上某個znode的許可權,但是不可以對其子節點進行CREATE和DELETE。
- 只CREATE不DELETE:某個客戶端在上一級目錄上通過傳送建立請求建立了一個zookeeper節點。你希望所有客戶端都可以在這個節點上新增,但是隻有建立者可以刪除。(這就類似於檔案的APPEND許可權)
內建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認證
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有資料一致性的保證:
順序一致性 客戶端的更新會按照它們傳送的次序排序。
原子性 更新的失敗或成功,都不會出現半個結果。
單獨系統映象 不管客戶端連哪個伺服器,它看來都是同一個。
可靠性 一旦更新生效,它就會一直儲存到下一次客戶端更新。這就有兩個推論:
- 如果客戶端得到成功的返回值,說明更新生效了。在一些錯誤情況下(連線錯誤,超時等)客戶端不會知道更新是否生效。雖然我們使失敗的機率最小化,但是也只能保證成功的返回值情況。(這就叫Paxos演算法的單調性條件)
- 客戶端能看到的更新,即使是渡請求或成功更新,在伺服器失敗時也不會回滾。
用這些一致性保證可以在客戶端中構造出更高階的程式如 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()方法。程式設計介面
常見問題和故障
下面是一些常見的陷阱:
- 如果你使用watch,你必須監控好已經連線的watch事件。當ZooKeeper客戶端斷開和伺服器的連線,直到重新連線上這段時間你都收不到任何通知。如果你正在監視znode是否存在,那麼你在斷開連線期間收不到它建立和銷燬的通知。
- 你必須測試ZooKeeper故障的情況。在大多數伺服器都可用的情況下,ZooKeeper是可以維持工作的。關鍵問題是你的客戶端程式是否能 察覺到。在實際情況下,客戶端與ZooKeeper的連線有可能中斷(多數時候是因為Zookeeper故障或網路中斷)。Zookeeper的客戶端庫 關注於如何讓你重新連線並且知道發生了什麼。但是同時你也必須確保能夠恢復你的狀態和傳送失敗的請求。努力在測試庫裡測出這些問題,而不是在產品裡——用 幾臺伺服器組成的zookeeper叢集測試這個問題,嘗試讓它們重啟。
- 客戶端維護的伺服器列表必須和現有的伺服器列表一致。如果客戶端的列表是現有伺服器列表的子集,還可以在非最佳狀態工作,但是如果客戶端列表裡的伺服器不在現有叢集裡你就悲劇了。
- 注意存放事務日誌的位置。效能評測最重要的部分就是日誌,ZooKeeper會在回覆響應之前先把日誌同步到磁碟上。為了達到最佳效能,首選專用 的磁碟來存日誌。把日誌放在繁忙的磁碟上會降低效率。如果你只有一個磁碟,就把記錄檔案放在NFS上然後增加SnapshotCount。這樣雖然無法完 全解決問題,但能緩解一些。
- 正確地設定你java的堆空間大小。這是避免頻繁交換的有效措施。無用的訪問磁碟會讓你的效率大打折扣。記住,在ZooKeeper中,一切都是有序的,如果一個伺服器訪問了磁碟,所有的伺服器都會同步這個操作。
來自:開源中國
相關閱讀
評論(1)