或者我們如何為 ZooKeeper、etcd 和 Consul KV 編寫客戶端 C++ 函式庫
在分散式系統的世界中,有許多典型的任務:儲存有關叢集組成的資訊、管理節點的配置、偵測故障節點、選擇領導者
本質上,所有這些系統都是容錯的、可線性化的鍵值儲存。 儘管它們的數據模型存在顯著差異(我們稍後將討論),但它們解決的是相同的實際問題。 顯然,每個使用協調服務的應用程式都與其中一個應用程式綁定,這可能導致需要在一個資料中心支援多個系統,為不同的應用程式解決相同的問題。
解決這個問題的想法起源於澳洲的諮詢機構,然後由我們這個學生小團隊來實施,這就是我要講的。
我們設法創建了一個庫,它提供了與 ZooKeeper、etcd 和 Consul KV 配合使用的通用介面。 該庫是用 C++ 編寫的,但計劃將其移植到其他語言。
資料模型
要為三個不同的系統開發通用接口,您需要了解它們的共同點和不同點。 讓我們弄清楚一下。
動物園管理員
鍵被組織成一棵樹,稱為節點。 因此,對於一個節點,您可以獲得其子節點的清單。 建立 znode (create) 和更改值 (setData) 的操作是分開的:只能讀取和更改現有的鍵。 監視可以附加到檢查節點是否存在、讀取值和取得子節點的操作。 Watch 是一種一次性觸發器,當伺服器上對應資料的版本發生變更時觸發。 臨時節點用於檢測故障。 它們與創建它們的客戶端會話相關聯。 當用戶端關閉會話或停止通知 ZooKeeper 其存在時,這些節點將自動刪除。 支援簡單事務 - 一組操作,如果其中至少一個操作不可能,則要么全部成功,要么全部失敗。
等
該系統的開發人員顯然受到了 ZooKeeper 的啟發,因此所做的一切都不同。 鍵沒有層次結構,但它們形成了按字典順序排序的集合。 您可以取得或刪除屬於某個範圍的所有鍵。 這種結構可能看起來很奇怪,但實際上非常具有表現力,並且可以透過它輕鬆模擬分層視圖。
etcd 沒有標準的比較和設定操作,但它確實有更好的東西:事務。 當然,它們存在於所有三個系統中,但 etcd 事務特別好。 它們由三個區塊組成:檢查、成功、失敗。 第一個區塊包含一組條件,第二個和第三個區塊包含操作。 事務以原子方式執行。 如果所有條件都為真,則執行成功區塊,否則執行失敗區塊。 在 API 3.3 中,成功和失敗區塊可以包含巢狀事務。 也就是說,可以原子地執行幾乎任意嵌套層級的條件構造。 您可以了解有關存在哪些檢查和操作的更多信息
這裡也有手錶,儘管它們稍微複雜一些並且可以重複使用。 也就是說,在某個關鍵範圍上安裝手錶後,您將收到該範圍內的所有更新,直到您取消手錶為止,而不僅僅是第一個更新。 在 etcd 中,ZooKeeper 用戶端會話的類似物是租約。
領事 K.V.
這裡也沒有嚴格的層次結構,但 Consul 可以創建它存在的外觀:您可以取得和刪除具有指定前綴的所有鍵,即使用鍵的「子樹」。 此類查詢稱為遞歸查詢。 此外,Consul 只能選擇前綴後不包含指定字元的鍵,這對應於取得直接「子級」。 但值得記住的是,這正是層次結構的表現:如果父項不存在,則很可能會建立一個金鑰,或刪除具有子項的金鑰,而子項將繼續儲存在系統中。
Consul 沒有監視,而是阻止 HTTP 請求。 本質上,這些是對資料讀取方法的普通調用,其中與其他參數一起指示了資料的最後一個已知版本。 如果伺服器上對應資料的目前版本大於指定的版本,則立即回傳回應,否則 - 當值變更時。 還有可以隨時附加到金鑰的會話。 值得注意的是,與 etcd 和 ZooKeeper 不同,刪除會話會導致關聯鍵的刪除,而有一種模式可以簡單地取消會話與它們的連結。 可用的
把它們放在一起
ZooKeeper擁有最嚴格的資料模型。 etcd 中可用的表達範圍查詢無法在 ZooKeeper 或 Consul 中有效模擬。 嘗試整合所有服務的優點,我們最終得到了一個幾乎與 ZooKeeper 介面相同的介面,但有以下重大例外:
序列、容器和 TTL 節點 不支持- 不支援 ACL
- 如果 key 不存在,set 方法會建立它(在 ZK 中 setData 在這種情況下傳回錯誤)
- set 和 cas 方法是分開的(在 ZK 中它們本質上是同一件事)
- 擦除方法刪除節點及其子樹(在 ZK 中,如果該節點有子節點,則刪除會傳回錯誤)
- 對於每個金鑰只有一個版本 - 值版本(在 ZK
一共有三個 )
拒絕順序節點是因為 etcd 和 Consul 沒有對它們的內建支持,並且用戶可以在生成的庫介面之上輕鬆實現它們。
在刪除頂點時實作類似 ZooKeeper 的行為需要為 etcd 和 Consul 中的每個鍵維護一個單獨的子計數器。 由於我們試圖避免儲存元訊息,因此決定刪除整個子樹。
實施的微妙之處
讓我們仔細看看在不同系統中實作庫介面的一些方面。
etcd 中的層次結構
事實證明,在 etcd 中維護分層視圖是最有趣的任務之一。 範圍查詢可以輕鬆檢索具有指定前綴的鍵列表。 例如,如果您需要以 "/foo"
,你要求一個範圍 ["/foo", "/fop")
。 但這將返回鍵的整個子樹,如果子樹很大,這可能是不可接受的。 一開始我們計劃使用一個按鍵翻譯機制,
"/foo" -> "u01/foo"
"/foo/bar" -> "u02/foo/bar"
然後取得該金鑰的所有直接子級 "/foo"
可以透過請求範圍來實現 ["u02/foo/", "u02/foo0")
。 是的,以 ASCII 表示 "0"
緊隨其後 "/"
.
但這種情況下如何實現頂點的移除呢? 原來需要刪除該類型的所有範圍 ["uXX/foo/", "uXX/foo0")
對於 XX,從 01 到 FF。 然後我們遇到了
於是,發明了一個簡單的金鑰轉換系統,使得可以有效地實作刪除金鑰和取得子清單。 在最後一個標記之前添加一個特殊字元就足夠了。 例如:
"/very" -> "/u00very"
"/very/long" -> "/very/u00long"
"/very/long/path" -> "/very/long/u00path"
然後刪除該鍵 "/very"
變成刪除 "/u00very"
和範圍 ["/very/", "/very0")
,並讓所有孩子 - 請求範圍內的鑰匙 ["/very/u00", "/very/u01")
.
刪除 ZooKeeper 中的金鑰
正如我已經提到的,在 ZooKeeper 中,如果節點有子節點,則無法刪除該節點。 我們想要刪除鍵和子樹。 我該怎麼辦? 我們懷著樂觀的態度來做這件事。 首先,我們遞歸地遍歷子樹,透過單獨的查詢來取得每個頂點的子節點。 然後我們建立一個事務,嘗試以正確的順序刪除子樹的所有節點。 當然,讀取子樹和刪除子樹之間可能會發生變化。 在這種情況下,交易將會失敗。 而且,子樹在讀取過程中可能會改變。 例如,如果節點已被刪除,則對下一個節點的子節點的請求可能會傳回錯誤。 在這兩種情況下,我們都會再次重複整個過程。
這種方法使得刪除具有子項的鍵變得非常無效,如果應用程式繼續使用子樹、刪除和建立鍵則更是如此。 然而,這使我們能夠避免使 etcd 和 Consul 中其他方法的實作變得複雜。
在 ZooKeeper 中設置
在ZooKeeper中,有單獨的方法用於處理樹結構(create、delete、getChildren)和處理節點中的資料(setData、getData)。此外,所有方法都有嚴格的前提條件:如果節點已經創建,則create將返回錯誤已建立、刪除或設定資料 - 如果它尚不存在。 我們需要一個可以在不考慮鍵是否存在的情況下呼叫的 set 方法。
一種選擇是採取樂觀的方法,例如刪除。 檢查節點是否存在。 如果存在則呼叫setData,否則建立。 如果最後一個方法回傳錯誤,請重複一遍。 首先要注意的是,存在性測試是沒有意義的。 您可以立即呼叫create。 成功完成將意味著該節點不存在並且已建立。 否則,create 將傳回對應的錯誤,之後您需要呼叫 setData。 當然,在呼叫之間,頂點可能會被競爭呼叫刪除,並且 setData 也會傳回錯誤。 在這種情況下,你可以重新來過,但這值得嗎?
如果兩種方法都回傳錯誤,那麼我們就可以確定發生了競爭刪除。 讓我們想像一下這個刪除是在呼叫 set 之後發生的。 那麼我們試圖建立的任何意義都已經被抹去了。 這意味著我們可以假設 set 已成功執行,即使實際上沒有寫入任何內容。
更多技術細節
在本節中,我們將暫時脫離分散式系統並討論編碼。
客戶的主要要求之一是跨平台:Linux、MacOS 和 Windows 上必須支援至少一項服務。 最初我們只針對Linux進行開發,後來開始在其他系統上進行測試。 這引起了很多問題,有一段時間完全不清楚如何解決。 因此,Linux 和 MacOS 現在支援所有三種協調服務,而 Windows 上僅支援 Consul KV。
從一開始,我們就嘗試使用現成的庫來存取服務。 就 ZooKeeper 而言,選擇在於
受到 ZooKeeper C++ 函式庫的非同步介面的啟發,我們決定也實作一個非同步介面。 ZooKeeper C++ 為此使用 future/promise 原語。 不幸的是,在 STL 中,它們的實作非常有限。 例如,沒有
我們當時的實現是這樣的。 呼叫時,會建立一個額外的 Promise/Future 對。 傳回新的 future,並將傳遞的 future 與對應的函數和一個附加的 Promise 一起放入佇列中。 池中的執行緒從佇列中選擇多個 future,並使用 wait_for 輪詢它們。 當結果可用時,將呼叫對應的函數並將其傳回值傳遞給 Promise。
我們使用相同的線程池來執行對 etcd 和 Consul 的查詢。 這意味著底層庫可以被多個不同的執行緒存取。 ppconsul 不是線程安全的,因此對它的呼叫受鎖保護。
您可以從多個線程使用 grpc,但有一些微妙之處。 在 etcd 中,手錶是透過 grpc 流實現的。 這些是某種類型訊息的雙向通道。 該庫為所有監視建立一個線程,並為處理傳入訊息建立一個線程。 所以 grpc 禁止並行寫入流。 這意味著在初始化或刪除手錶時,必須等到上一個請求發送完成後才能發送下一個請求。 我們用於同步
總
見自己:
我們的隊伍:
來源: www.habr.com