重談分佈式一致性:zk和kafka中的副本

這篇文章翻譯自:https://www.confluent.fr/blog/distributed-consensus-reloaded-apache-zookeeper-and-replication-in-kafka/,有興趣的可以看原文,由於水平有限翻譯過程中難免有些錯誤,請大家指正,一定要多拍拍。

這篇文章是由Apache Kafka的共同創建者Neha Narkhede和Apache ZooKeeper的共同創建者Flavio Junqueira共同撰寫的。

目前,我們構建和使用的許多分佈式系統都依賴於Apache ZooKeeper,Consul以及etcd,甚至依賴於Raft的自制版本。儘管這些系統在其公開的功能上有所不同,但是核心是相同的並且所有分佈式系統必須解決的:共識(agreement)。分佈式系統中的進程需要和master達成共識,和group中的其他成員達成共識,和鎖的擁有者達成共識,和配置達成共識,等等。這些都是分佈式系統設計中常見的問題,使用Apache ZooKeeper,Consul以及etcd等中的任何一個都是成功的,這些從根本上解決了分佈式共識問題。使用其中的任何一致性解決方案,都可以使分佈式系統可以以更有效的方式協調分佈式系統中的各個進程。例如:管理kakfa中的副本。在這篇文章中,我們將重點集中在此類系統通常如何公開共識以及在Apache Kafka的複製方案中的典型應用。

以解決共識爲核心的服務,被稱爲“共識服務”。但是,“共識服務”這個名字可能是一個糟糕的選擇,因爲這些服務實際上都沒有公開顯式解決共識的方法。如果爲我們提供了鎖服務,那麼我們希望API提供獲取和釋放鎖的功能。但是,我們所討論的服務並未公開的共識的API,因此將其稱爲“共識服務”會產生誤導。

如果是這樣,那麼爲什麼人們將其稱爲共識服務?主要是因爲服務通常用於達成某些協議:和鎖,和主服務器,和配置等。在ZooKeeper的上下文中,我們選擇將其稱爲協調內核[3] [4]。名稱的依據如下,該服務本身公開了類似於文件系統的API,以便客戶端可以操縱簡單的數據文件(znode)。這些文件可以具有一些特殊的屬性,例如臨時的或順序的,但它們都是小型數據文件。我們想到了諸如文件系統,數據庫,鍵值存儲之類的術語,但由於以下原因,它們並不完全適合:

  • 它並不是真正的文件系統,因爲它沒有提供典型文件系統所具有的所有屬性(例如,部分讀取和寫入文件)。
  • 它並不是真正的數據庫,因爲它實際上並不處理批量數據,也沒有複雜的運算符來操縱數據。
  • 它實際上不是鍵值存儲,因爲它比典型的鍵值存儲提供的功能(例如,訂購,層次結構)還多。
  • 它並不是真正的鎖服務,因爲API不會直接公開鎖。

因此,我們決定根據它用來幹什麼而不是能幹什麼來命名它,所以“協調”這個稱呼很合適。我們還決定將其稱爲內核,因爲API像分佈式鎖一樣對原語進行了實現,
但是像Chubby系統一樣沒有直接暴露原語[5]。內核公開了少量功能,足以實現主選舉,鎖定,成員管理等。

然而,共識問題對於理解類似ZooKeeper這樣的系統如何工作以及可以提供什麼確實是至關重要的。這個名字的麻煩部分在於,ZooKeeper不是一個盒子,您可以簡單地問“我們達成了什麼?”。比這更復雜的是,這篇文章的目的是闡明共識問題在分佈式系統中的表現方式和一些注意事項。作爲本練習的一部分,我們將討論共識是如何在Apache Kafka複製方案中體現的以及如何利用ZooKeeper簡化其操作。但是首先,需要有一些共識的背景知識。

共識簡短背景

在分佈式系統設計的上下文中,共識通常被寬鬆地用來表示某種形式的協議。但是,分佈式共識問題是一個定義明確的問題。如Fischer,Lynch和Paterson [2,6]在著名論文中所描述的,共識協議是具有n個進程的系統,因此每個進程都有一個初始值和一個輸出值,一旦設置,該值將不再更改。一旦進程確定了其輸出值。進程確定了其輸出值,我們就說進程已經決定,而進程一旦決定,就無法改變。

這個定義難道不是很狹義嗎?爲什麼進程無法更改其決策值?這種一致性的要求有點像事物提交[2]。如果某個進程提交

在分佈式系統設計的上下文中,共識通常被寬鬆地用來表示某種形式的協議。但是,分佈式共識問題是一個定義明確的問題。如Fischer,Lynch和Paterson [2,6]在著名論文中所描述的,共識協議具有n個進程的系統,每個進程都有一個初始值和一個輸出值,一旦設置,該值將不再更改。一旦了自己的一部分並隨後決定中止,則可能會造成一些問題,因爲該提交可能會產生外部影響(例如,客戶已經提取了1,000,000美元)。因此,我們假設決策值一旦設置就無法更改。

從解決方案到達成共識,我們希望有一些特性:

  • 協議:我們希望所有不會崩潰的進程都同意相同的值
  • 有效性:該值必須是進程之一建議的值(否則,我們可以簡單地決定中止並完成)
  • 終止:我們希望任何共識的執行都會終止。如果協議永不終止,則進程會毫無意義的商定同一件還未決定事。正如FLP不可能結果所示,這個終止屬性實際上是破壞異步系統中共識的原因。

這樣的共識協議與ZooKeeper這樣的系統並不完全相同。事實證明,還有一個更酷的問題被證明等同於共識,而這正是ZooKeeper所實現的(實際上是該問題的變體,請參見[7]),這個問題就是原子廣播[8]。

原子廣播包括確保進程以相同的順序(總順序)傳遞相同的消息(協議)。這個屬性對於複製類系統而言確實是很重要,因爲如果我的消息是命令並且利用原子廣播實現,那麼我可以使用它向所有的副本廣播命令,所有副本將以相同的順序接收所有命令,並以收到的順序執行命令。如果命令是確定性的,則可以確保所有副本之間的狀態始終保持一致。這種觀察是複製狀態機的本質[9]。

ZooKeeper實際上並不廣播命令,而是廣播狀態更新。使用狀態更新是一種將客戶端提交的命令轉換爲冪等事務的方法。例如,可以有條件地更新znode,並且在使用setData請求更新znode時,版本會自動增加。要將調用轉換爲冪等事務,我們需要計算新版本並傳播znode的新狀態。更具體一點,這是請求和相應狀態更新的簡化版本:

        <setData, path, data, expected version>  // setData request
        <path, new data, new version>            // corresponding txn

這在本文章之外進行討論很有用,這在Zab的工作[6]中討論了更多細節。

關於共識和原子廣播,讓我們通過一個簡單的論點來了解爲什麼它們是等效的。使用共識協議實現,進程可以運行一系列共識實例以實現原子廣播。每個共識實例的輸入值是一組要廣播的值。由於進程運行一致,因此它們在每個實例中傳遞相同的消息集。另一個方向也很簡單。如果給我們一個原子廣播實現,那麼爲了獲得共識實現,每個進程都只是通過廣播該值來提出一個值。進程傳遞的第一條消息包含決策值,該決策值對於所有進程都是相同的。

ZooKeeper和共識

在推理ZooKeeper時,考慮原子廣播比共識更有意義。但是等等,它們不是等效的嗎?是的,從簡化的角度來看,它們是,但是它們仍然呈現不同的語義。下邊一個錯誤的推理如何導致問題的簡單示例。

假設我們有一個包含三個客戶端的系統,一個配置器(C)和兩個工作器(W1和W2)。配置器C告訴工作器應該消費的信息,並期望所有工作器都消費相同的信息。協調員選擇的信息會隨時間變化而變化,並且協調員通過ZooKeeper以容錯的方式將其選擇傳達給工作人員。現在考慮以下步驟序列:

  1. C將香草寫入ZooKeeper
  2. W1讀香草
  3. C之後立即將巧克力寫入ZooKeeper
  4. W2讀巧克力,W1已經在食用香草

顯然,由於他們讀取了不同的值,工作器會消費不同的信息,但是到底出了什麼問題呢?服務不應該讓我們達成共識嗎?事實證明,ZooKeeper提供的共識是對ZooKeeper狀態更新的統一視圖。如果兩個或更多客戶端能夠觀察到ZooKeeper服務的所有更新,則它們觀察到以相同順序應用的相同更新,但不一定同時。因此,它提供的那種協議不能與始終遵守相同狀態混淆。保持一致並不意味着讀取的值必須相同。

解決此問題的一種方法是使用序列化更新,以使工作器就該值達成共識,本質上是像我們之前討論的那樣,使用原子廣播實現共識。 W1讀取一個值後,就可以建議它們消耗香草,因爲配置程序已建議這樣做。 W2的功能相同,但是由於他們提出了不同的值,因此需要打破平局。他們可以通過選擇使用ZooKeeper順序節點寫入的第一個值來做到這一點。

與原始提案相比,該提案爲何有效?因爲每個工作器都“提出”了一個單一的價值,而且這些價值不會發生變化。隨着時間的推移,配置器更改值,他們可以通過運行此簡單協議的獨立實例來商定不同的值。

請注意,這種情況只是爲了說明圍繞協議的推理和提供該協議的服務不正確時的問題。解決問題的真正方法將取決於應用程序的精確語義,並且會有多種實現方法。

通過服務達成共識

共識在分佈式系統中起着重要作用,使用Apache ZooKeeper之類的服務可以使複製的某些方面更加簡單。爲了使論點非常具體,我們在這裏集中討論Apache Kafka的複製方案。 Kafka使用Apache ZooKeeper來存儲元數據。此元數據有多種用途—持久保留組成topic的brokers,從爲其數據提供寫服務的副本中選擇一個leader,並保留了一些子節點的信息。

在這裏我們僅介紹了足以使該參數易於理解的內容。 Kafka暴露了topic的抽象:客戶通過topic進行消息生成和消費。爲了提高併發的性能,主題被進一步劃分爲partitions。partitions是Kafka中並行性和複製的基本單位。在分區的一組副本中,有一個會被選爲領導者(使用ZooKeeper),其餘爲跟隨者。領導負責分區的所有讀取和寫入。 Kafka還具有同步副本(ISR)的概念:副本的子集當前處於活動狀態,並且已被領導者追上,也就是說leader和副本沒有相差太多。

ISR動態更改,並且每次更改時,該集的新成員資格都將保留到ZooKeeper。 ISR有兩個重要目的。首先,在領導者宣佈它們已提交之前,此集合需要確認所有寫入分區的記錄。因此,ISR集必須至少包含f + 1個副本才能承受f次崩潰,並且f + 1所需的值由配置設置。其次,由於ISR擁有分區中以前提交的所有消息,爲了保持一致性,新領導者必須來自最新的ISR。從最新的ISR中選出一位領導者對於確保在領導者過渡期間不會丟失任何已落實的消息來說很重要。當副本發生故障時,會將其從ISR中刪除。當副本在崩潰後重新啓動時,它會被告知當前的領導者和ISR(通過從ZooKeeper中讀取),然後通過從當前領導者中拉出來同步其數據,直到其趕上並足以成爲ISR的一部分爲止。在這種複製方案中,ZooKeeper只處理複製的元數據(分區信息和ISR成員資格),實際數據的複製留給應用程序(Kafka)來考慮。

就持久性而言,Kafka的複製協議和ZooKeeper的複製協議之間還有一個重要區別。由於Kafka依賴ZooKeeper成爲元數據的“真相之源”,因此ZooKeeper必須提供強大的持久性保證。因此,它只有在將數據同步到ZooKeeper仲裁服務器的磁盤後纔會確認寫請求。由於Kafka的brokers需要處理的數據量很大,因此無法承受做到這樣,也無法將分區數據同步到磁盤。分區的消息將寫入相應的文件,但是不會調用fsync/fdatasync,這意味着數據在寫入後仍保留在操作系統頁面高速緩存中,並不一定要刷新到磁盤介質中。這種設計選擇會對性能產生巨大的積極影響,但是具有一個副作用,即正在恢復的副本可能沒有先前確認的某些消息。

爲什麼選擇ISR而不是多數仲裁的方式

在複製系統中使用仲裁的一個很大的優勢是能夠掩蓋崩潰。來自足夠大的副本子集(例如多數)的投票就足以提交。在以2f + 1方式複製的基於仲裁的系統中,如果有任何f個副本崩潰,則該系統仍可以透明地進行進度(可能會有一些小的問題)。每個寫入操作都將到達所有副本,但是隻有多數仲裁的響應才需要提交寫入。查看下圖:
在這裏插入圖片描述
多數仲裁的一個重要缺點是要求至少(n +1)/ 2個確認,所以節點的數量會隨着n的增加而增加。這種仲裁系統方案可與ZooKeeper之類的系統很好地協作,因爲它處理元數據:體積小且寫入很少,通常常見的操作不在關鍵路徑中。

Kafka的ISR方案不使用上邊的多數仲裁的方式,而是要求當前ISR的所有成員做出響應:
在這裏插入圖片描述
爲了提交寫操作,ISR中的所有副本都必須以確認響應,而不僅僅是大多數響應。與經典仲裁方式不同,ISR的大小與副本集的大小脫鉤,這爲副本集的配置提供了更大的靈活性。例如,我們可以有11個副本,其最小ISR大小爲3(f = 2)。對於大多數仲裁,具有11個副本將必然意味着大小爲6的副本。

請記住,最小ISR的大小與系統提供的持久性保證直接相關。由於可以通過配置來調整對ISR最小大小的限制,ISR持久性保證類似於ZooKeeper提供的一種保證,即如果失敗的副本數低於預期的仲裁大小,則不會進行寫操作。如果系統不能保證可以執行新寫入,則它在可用性上權衡了可用性,在持久性方面不接受新寫入。在這方面,Kafka的複製方案非常靈活。通過讓ISR的最小大小可配置,它使主題可以在可用性和耐用性之間進行權衡,反之亦然,而仲裁不需要包含大多數副本。

儘管ISR方案具有更高的可伸縮性並可以容忍更多的故障,但它對副本的某些子集(ISR)的性能也更加敏感。如果基於多數仲裁的方案僅忽略了最慢的副本,則該方案將暫停對分區的所有寫入操作,直到最慢的副本(如果它是ISR的一部分)被刪除。在大多數故障模式下,副本會被快速刪除。對於軟故障,在一定的超時後將刪除無響應的副本。同樣,如果慢速副本落後於配置所定義的值以後,它們也會被刪除。

對於像Apache Kafka這樣的數據系統,其複製方案的這種靈活性具有對持久性保證提供更細粒度控制的優點,事實證明,這在生產中存儲大量數據時非常有用。作爲另一個數據點,像Apache BookKeeper這樣的系統也使用了類似的方案,其中副本和確認的數量是獨立的。但是,BookKeeper不能像Kafka一樣提供ISR。

有趣的是,該方案與PacificA的工作[10]中描述的方案很接近。在PacificA的工作中,該框架將副本組的配置管理與實際數據複製分開,就像Kafka一樣。它還與Cheap Paxos共享一些屬性[11]。使用Cheap Paxos,數據副本僅發送到f + 1個副本,而在Kafka中,數據副本發送到所有副本(以保持副本有效,而不是爲了確保正確性)。但是,這兩種協議都建議保持f +1副本的固定子集爲最新,並在懷疑崩潰時重新配置該集合。 Cheap Paxos從協議內部執行重新配置,而Kafka依賴於外部複製狀態機(ZooKeeper)。

所有這一切的共識在哪裏?

在Kafka複製協議中,ISR更新協議掩蓋了共識。由於ZooKeeper最終通過將ISR信息存儲在ZooKeeper中而最終公開了一個原子廣播原語,因此從本質上來說,可以保證對ISR更改的繼承達成一致。當副本在崩潰後恢復時,它可以轉到ZooKeeper,查找最新的分區元數據(領導者,ISR),並進行相應的同步以獲取最新的提交消息。由於所有副本(通過ZooKeeper)都同意最新的ISR是什麼,因此不可能出現裂腦情況。

讓我們來看一些場景,以更好地瞭解其工作原理。假設我們有一個分區{A,B,C,D,E}的5個副本,並且ISR集最初包含所有副本。下圖說明了這種情況
在這裏插入圖片描述
在某個時候,副本E崩潰。
在這裏插入圖片描述
一旦開始恢復過程,它將從ZooKeeper中讀取分區的狀態,並得知A是當前ISR的領導者。
在這裏插入圖片描述
完成同步後,將其添加回ISR。
在這裏插入圖片描述
如果不存在ZooKeeper,則副本E將需要與其他副本進行對話,以找出哪個副本進行同步。無法查詢像ZooKeeper這樣的“真相之源”,查詢剩餘那些節點會帶來各種各樣的麻煩,因爲副本可能會崩潰或被分割掉,這是在沒有ZooKeeper這樣的依賴的情況下進行仲裁(多數選擇)的關鍵原因。即使使用ZooKeeper,也可能需要注意一些競爭條件。

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