五名學生和三個分散式鍵值存儲

或者我們如何為 ZooKeeper、etcd 和 Consul KV 編寫客戶端 C++ 函式庫

在分散式系統的世界中,有許多典型的任務:儲存有關叢集組成的資訊、管理節點的配置、偵測故障節點、選擇領導者 其他。 為了解決這些問題,創建了特殊的分散式系統—協調服務。 現在我們對其中三個感興趣:ZooKeeper、etcd 和 Consul。 在 Consul 的所有豐富功能中,我們將專注於 Consul KV。

五名學生和三個分散式鍵值存儲

本質上,所有這些系統都是容錯的、可線性化的鍵值儲存。 儘管它們的數據模型存在顯著差異(我們稍後將討論),但它們解決的是相同的實際問題。 顯然,每個使用協調服務的應用程式都與其中一個應用程式綁定,這可能導致需要在一個資料中心支援多個系統,為不同的應用程式解決相同的問題。

解決這個問題的想法起源於澳洲的諮詢機構,然後由我們這個學生小團隊來實施,這就是我要講的。

我們設法創建了一個庫,它提供了與 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")。 但這將返回鍵的整個子樹,如果子樹很大,這可能是不可接受的。 一開始我們計劃使用一個按鍵翻譯機制, 在 zetcd 中實現。 它涉及在鍵的開頭添加一個字節,等於樹中節點的深度。 讓我舉一個例子。

"/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 而言,選擇在於 動物園管理員 C++,最終未能在 Windows 上編譯。 然而,這並不奇怪:該庫被定位為僅限 Linux。 對領事來說唯一的選擇是 PP領事。 必須添加支持 會議 и 交易。 對於etcd,沒有找到支援最新版本協定的成熟庫,所以我們簡單地 產生的grpc客戶端.

受到 ZooKeeper C++ 函式庫的非同步介面的啟發,我們決定也實作一個非同步介面。 ZooKeeper C++ 為此使用 future/promise 原語。 不幸的是,在 STL 中,它們的實作非常有限。 例如,沒有 然後方法,當未來的結果可用時,它將傳遞的函數應用於未來的結果。 在我們的例子中,需要這樣的方法將結果轉換為我們庫的格式。 為了解決這個問題,我們必須實作自己的簡單線程池,因為根據客戶的要求,我們不能使用 Boost 等重型第三方函式庫。

我們當時的實現是這樣的。 呼叫時,會建立一個額外的 Promise/Future 對。 傳回新的 future,並將傳遞的 future 與對應的函數和一個附加的 Promise 一起放入佇列中。 池中的執行緒從佇列中選擇多個 future,並使用 wait_for 輪詢它們。 當結果可用時,將呼叫對應的函數並將其傳回值傳遞給 Promise。

我們使用相同的線程池來執行對 etcd 和 Consul 的查詢。 這意味著底層庫可以被多個不同的執行緒存取。 ppconsul 不是線程安全的,因此對它的呼叫受鎖保護。
您可以從多個線程使用 grpc,但有一些微妙之處。 在 etcd 中,手錶是透過 grpc 流實現的。 這些是某種類型訊息的雙向通道。 該庫為所有監視建立一個線程,並為處理傳入訊息建立一個線程。 所以 grpc 禁止並行寫入流。 這意味著在初始化或刪除手錶時,必須等到上一個請求發送完成後才能發送下一個請求。 我們用於同步 條件變數.

見自己: 利奧夫克夫.

我們的隊伍: 拉德羅曼諾夫, 伊凡‧格魯申科夫, 德米特里·卡馬爾迪諾夫, 維克托·克拉皮文斯基, 維塔利·伊凡寧.

來源: www.habr.com

添加評論