如何擊敗CAP定理?

CAP定理指出數據庫不能同時保證一致性,可用性和分區容錯。但是你不能犧牲分區容忍度(見這裏這裏),所以你必須在可用性和一致性之間進行權衡。管理這種權衡是NoSQL運動的核心焦點。

一致性意味着在您成功寫入之後,將來的讀取將始終考慮該寫入。可用性意味着您始終可以讀取和寫入系統。在分區期間,您只能擁有其中一個屬性。

選擇一致性而非可用性的系統必須處理一些棘手的問題。數據庫不可用時你會怎麼做?您可以嘗試稍後緩衝寫入,但如果丟失了使用緩衝區的計算機,則可能會丟失這些寫入。此外,緩衝寫入可能是一種不一致的形式,因爲客戶端認爲寫入成功但寫入不在數據庫中。或者,您可以在數據庫不可用時將錯誤返回給客戶端。但是如果你曾經使用過一種告訴你“稍後再試”的產品,那麼你知道這會讓你感到噁心。

另一種選擇是選擇可用性而非一致性。這些系統可以提供的最佳一致性保證是“最終一致性”。如果您使用最終一致的數據庫,那麼有時您將閱讀與您剛剛編寫的結果不同的結果。有時多個讀者同時閱讀相同的密鑰會得到不同的結果。更新可能不會傳播到值的所有副本,因此您最終會獲得一些副本以獲得一些更新,而其他副本會獲得不同的更新。一旦檢測到值已經發散,就由您來修復該值。這需要使用矢量時鐘追溯歷史並將更新合併在一起(稱爲“讀取修復”)。

我認爲,在應用程序層中保持最終的一致性對於開發人員來說是一個沉重的負擔。讀取修復代碼極易受開發人員錯誤的影響; 如果您犯了錯誤,錯誤的讀取修復將在數據庫中引入不可逆轉的損壞。

因此犧牲可用性是有問題的,並且最終的一致性太複雜而不能合理地構建應用程序。然而這些是唯一的兩個選擇,所以我覺得如果你這樣做你就該死,如果你不這樣做就該死。CAP定理是自然界的事實,那麼可能有什麼替代方案呢?

還有另一種方式。您無法避免CAP定理,但您可以隔離其複雜性並防止它破壞您推理系統的能力。CAP定理引起的複雜性是我們如何處理建築數據系統的基本問題的症狀。特別突出的兩個問題是:在數據庫中使用可變狀態以及使用增量算法來更新該狀態。正是這些問題與CAP定理之間的相互作用導致了複雜性。

在這篇文章中,我將展示一個系統的設計,該系統通過防止它通常導致的複雜性來擊敗CAP定理。但我不會就此止步。CAP定理是關於數據系統對機器故障具有容錯能力的結果。然而,有一種容錯形式比機器容錯更重要:人類的容錯能力。如果在軟件開發方面有任何確定性,那就是開發人員並不完美,而且bug將不可避免地達到生產。我們的數據系統必須對編寫錯誤數據的錯誤程序具有彈性,而我將要展示的系統就像你可以獲得的人類容錯一樣。

本文將挑戰您對如何構建數據系統的基本假設。但是,通過打破我們當前的思維方式並重新構想如何構建數據系統,出現的結構是一種比您想象的更優雅,可擴展且更健壯的架構。

什麼是數據系統?

在我們談論系統設計之前,讓我們首先定義我們試圖解決的問題。數據系統的目的是什麼?什麼是數據?我們甚至無法開始接近CAP定理,除非我們能夠用明確封裝每個數據應用的定義來回答這些問題。

數據應用程序包括存儲和檢索對象,連接,聚合,流處理,連續計算,機器學習等等。目前尚不清楚數據系統有這麼簡單的定義 - 我們用數據做的事情似乎太多了,無法用一個定義來捕獲。

但是,有這麼簡單的定義。就是這個:

Query = Function(All Data)

而已。該等式總結了數據庫和數據系統的整個領域。該領域的所有內容 - 過去50年的RDBMS,索引,OLAP,OLTP,MapReduce,ETL,分佈式文件系統,流處理器,NoSQL等 - 都以這種方式以某種方式進行了總結。

數據系統回答有關數據集的問題。這些問題稱爲“查詢”。此等式表明查詢只是您擁有的所有數據的函數。

這個等式似乎太籠統而無用。它似乎沒有捕獲任何數據系統設計的複雜性。但重要的是每個數據系統都屬於這個等式。該等式是我們可以探索數據系統的起點,該等式最終將導致一種擊敗CAP定理的方法。

這個等式中有兩個概念:“數據”和“查詢”。這些是在數據庫領域經常混淆的不同概念,所以讓我們嚴格理解這些概念的含義。

數據

讓我們從“數據”開始。一段數據是一個不可分割的單位,你認爲它是真實的,除了存在之外別無他法。這就像數學中的公理。

關於數據有兩個關鍵屬性需要注意。首先,數據本質上是基於時間的。一段數據是你知道在某個時刻是真實的事實。例如,假設莎莉進入她居住在芝加哥的社交網絡檔案。您從該輸入中獲取的數據是她在芝加哥生活的特定時刻,她將該信息輸入到她的個人資料中。假設Sally稍後將她的個人資料位置更新到亞特蘭大。然後你知道她在特定時間住在亞特蘭大。她現在住在亞特蘭大的事實並沒有改變她過去居住在芝加哥的事實。兩條數據都是真的。

數據的第二個屬性緊跟在第一個屬性之後:數據本質上是不可變的。由於它與某個時間點的連接,一段數據的真實性永遠不會改變。人們無法及時回過頭來改變一段數據的真實性。這意味着您只能對數據執行兩項主要操作:讀取現有數據並添加更多數據。CRUD已成爲CR。

我省略了“更新”操作。這是因爲更新對於不可變數據沒有意義。例如,“更新”Sally的位置確實意味着您正在添加一條新數據,表示她最近一次住在新位置。

我也省略了“刪除”操作。同樣,大多數刪除情況更好地表示爲創建新數據。例如,如果Bob在Twitter上停止關注Mary,那麼這並不會改變他過去跟隨她的事實。因此,不是刪除他跟隨她的數據,而是添加一條新的數據記錄,表明他在某個時刻沒有跟蹤她。

在某些情況下,您確實希望永久刪除數據,例如要求您在一定時間後清除數據的法規。我將要展示的數據系統設計很容易支持這些情況,因此爲了簡單起見,我們可以忽略這些情況。

這種數據定義幾乎肯定與您習慣的不同,特別是如果您來自關係數據庫世界,其中更新是常態。有兩個原因。首先,這種數據定義非常通用:很難想到一種不符合這個定義的數據。其次,數據的不變性是我們在設計勝過CAP定理的人類容錯數據系統時將要利用的關鍵屬性。

詢問

等式中的第二個概念是“查詢”。查詢是一組數據的派生。從這個意義上講,查詢就像是數學中的一個定理。例如,“Sally目前的位置是什麼?” 是一個查詢。您可以通過返回有關Sally位置的最新數據記錄來計算此查詢。查詢是完整數據集的功能,因此它們可以執行任何操作:聚合,將不同類型的數據連接在一起,等等。因此,您可能會詢問服務的女性用戶數量,或者您可能會查詢推文數據集,瞭解過去幾個小時內哪些主題趨勢。

我已經將查詢定義爲完整數據集上的函數。當然,許多查詢不需要運行完整的數據集 - 它們只需要數據集的子集。但重要的是我的定義包含了所有可能的查詢,如果我們要打敗CAP定理,我們必須能夠爲任何查詢做到這一點。

擊敗CAP定理

計算查詢的最簡單方法是在整個數據集上運行一個函數。如果您可以在延遲限制內完成此操作,那麼您就完成了。沒有別的東西要建造。

當然,期望完整數據集上的函數快速完成是不可行的。許多查詢(例如服務於網站的查詢)需要毫秒的響應時間。但是,讓我們假裝您可以快速計算這些函數,讓我們看看這樣的系統如何與CAP定理相互作用。正如您將要看到的,這樣的系統不僅擊敗了CAP定理,而且還消滅了它。

CAP定理仍然適用,因此您需要在一致性和可用性之間做出選擇。美麗的是,一旦你決定了你想做的權衡,你就完成了。通過使用不可變數據和從頭開始計算查詢,可以避免CAP定理通常導致的複雜性。

如果您選擇一致性而非可用性,那麼之前沒有太大的變化。有時您將無法讀取或寫入數據,因爲您可以獲得可用性。但對於必須採用剛性一致性的情況,這是一種選擇。

當您選擇可用性超過一致性時,事情變得更有趣。在這種情況下,系統最終是一致的,沒有任何最終一致性的複雜性。由於系統具有高可用性,因此您始終可以編寫新數據和計算查詢。在故障情形中,查詢將返回不包含先前寫入的數據的結果。最終,數據將保持一致,查詢將把這些數據合併到他們的計算中。

關鍵是數據是不可變的。不可變數據意味着沒有更新這樣的東西,因此一條數據的不同副本不可能變得不一致。這意味着沒有發散值,矢量時鐘或讀取修復。從查詢的角度來看,一段數據存在或不存在。該數據只有數據和功能。您無需採取任何措施來強制執行最終的一致性,最終的一致性不會妨礙對系統的推理。

造成複雜性的原因是增量更新和CAP定理之間的相互作用。增量更新和CAP定理真的不能很好地結合在一起; 可變值需要在最終一致的系統中進行讀取修復。通過拒絕增量更新,包含不可變數據以及每次從頭開始計算查詢,您可以避免這種複雜性。CAP定理被打敗了。

當然,我們剛剛經歷的是思想實驗。雖然我們希望每次都能從頭開始計算查詢,但這是不可行的。但是,我們已經瞭解了真實解決方案的一些關鍵屬性:

  1. 該系統可以輕鬆存儲和擴展不可變的,不斷增長的數據集
  2. 主要的寫操作是添加新的不可變數據事實
  3. 該系統通過從原始數據重新計算查詢來避免CAP定理的複雜性
  4. 系統使用增量算法將查詢延遲降低到可接受的水平

讓我們開始探索這樣一個系統的樣子。請注意,從現在開始的所有內容都是優化。數據庫,索引,ETL,批量計算,流處理 - 這些都是優化查詢功能並將延遲降低到可接受水平的技術。這是一個簡單而深刻的實現。數據庫通常被認爲是數據管理的核心,但實際上它們只是大局的一部分。

批量計算

弄清楚如何在任意數據集上快速運行任意函數是一個令人生畏的問題。讓我們放鬆一下這個問題。讓我們假裝查詢過時幾個小時就可以了。通過這種方式解決問題,可以爲構建數據系統提供簡單,優雅且通用的解決方案。之後,我們將擴展解決方案,以便不再放鬆問題。

由於查詢是所有數據的函數,因此使查詢快速運行的最簡單方法是預先計算它們。每當有新數據時,您只需重新計算所有內容。這是可行的,因爲我們放寬了問題,允許查詢過時幾個小時。以下是此工作流程的說明:

 

要構建它,您需要一個系統:

  1. 可以輕鬆存儲大量且不斷增長的數據集
  2. 可以以可伸縮的方式計算該數據集上的函數

存在這樣的系統。它成熟,經過數百個組織的戰鬥測試,擁有龐大的工具生態系統。它被稱爲Hadoop。Hadoop 並不完美,但它是進行批處理的最佳工具。

很多人會告訴你,Hadoop只適用於“非結構化”數據。這完全是假的。Hadoop非常適合結構化數據。使用ThriftProtocol Buffers等工具,您可以使用豐富,可演化的模式存儲數據。

Hadoop由兩部分組成:分佈式文件系統(HDFS)和批處理框架(MapReduce)。HDFS擅長以可擴展的方式跨文件存儲大量數據。MapReduce擅長以可擴展的方式對該數據運行計算。這些系統完全符合我們的需求。

我們將數據存儲在HDFS上的平面文件中。文件將包含一系列數據記錄。要添加新數據,只需將包含新數據記錄的新文件附加到包含所有數據的文件夾即可。在HDFS上存儲這樣的數據解決了“存儲大量且不斷增長的數據集”的要求。

從該數據預先計算查詢同樣簡單明瞭。MapReduce是一種富有表現力的範例,幾乎任何函數都可以作爲一系列MapReduce作業來實現。像工具Cascalog層疊使實現這些功能變得更加容易。

最後,您需要索引預計算的結果,以便應用程序可以快速訪問結果。有一類非常擅長的數據庫。ElephantDBVoldemort只讀專用於從Hadoop導出鍵/值數據以進行快速查詢。這些數據庫支持批量寫入和隨機讀取,並且它們支持隨機寫入。隨機寫入會導致數據庫中的大多數複雜性,因此通過不支持隨機寫入,這些數據庫非常簡單。例如,ElephantDB只有幾千行代碼。這種簡單性使得這些數據庫非常強大。

讓我們看一下批處理系統如何組合在一起的例子。假設您正在構建一個跟蹤頁面視圖的Web分析應用程序,並且您希望能夠在任何時間段內查詢頁面視圖的數量,精確到一小時。

 

實現這一點很容易。每個數據記錄包含單個頁面視圖。這些數據記錄存儲在HDFS上的文件中。按小時爲每個URL捲起頁面視圖的函數實現爲一系列MapReduce作業。該函數發出鍵/值對,其中每個鍵是一[URL, hour]對,每個值是頁面查看次數的計數。這些鍵/值對將導出到ElephantDB數據庫中,以便應用程序可以快速獲取任何[URL, hour]對的值。當應用程序想知道某個時間範圍的頁面查看次數時,它會向ElephantDB查詢該時間範圍內每小時的頁面查看次數,並將它們相加以獲得最終結果。

批處理可以計算任意數據的任意函數,但缺點是查詢已經過時了幾個小時。這種系統的“任意性”意味着它可以應用於任何問題。更重要的是,它簡單易懂,完全可擴展。您只需要考慮數據和功能,Hadoop負責並行化。

批處理系統,CAP和人爲容錯

到現在爲止還挺好。那麼我所描述的批處理系統如何與CAP排成一行,它是否符合我們人類容錯的目標?

讓我們從CAP開始吧。批處理系統最終以最極端的方式保持一致:寫入總是需要幾個小時才能合併到查詢中。但它是一種易於推理的最終一致性形式,因爲您只需考慮該數據的數據和功能。沒有讀取修復,併發或其他複雜問題需要考慮。

接下來,讓我們來看看批處理系統的人爲容錯。批處理系統的人體容錯能力就是您所能獲得的。在這樣的系統中,人類只能犯兩個錯誤:部署錯誤的查詢實現或編寫錯誤數據。

如果您部署了一個錯誤的查詢實現,那麼您需要做的就是修復錯誤,部署固定版本,並重新計算主數據集中的所有內容。這是有效的,因爲查詢是純函數。

同樣,編寫錯誤數據有明確的恢復途徑:刪除錯誤數據並再次預先計算查詢。由於數據是不可變的並且主數據集是僅附加的,因此寫入錯誤數據不會覆蓋或以其他方式破壞良好數據。這與幾乎所有傳統數據庫形成鮮明對比,如果您更新密鑰,則會丟失舊值。

請注意,MVCC和類似HBase的行版本控制並未接近此級別的人爲容錯。MVCC和HBase行版本控制不會永久保存數據:一旦數據庫壓縮了行,舊值就會消失。只有不可變數據集才能保證在寫入錯誤數據時有一條恢復路徑。

實時圖層

信不信由你,批處理解決方案几乎解決了實時計算任意數據上任意函數的完整問題。任何早於幾個小時的數據都已合併到批處理視圖中,因此剩下要做的就是補償最後幾個小時的數據。弄清楚如何針對幾小時的數據實時查詢,這比完整數據集更容易。這是一個重要的見解。

要補償這幾小時的數據,您需要一個與批處理系統並行運行的實時系統。實時系統預先計算最近幾小時數據的每個查詢函數。要解析查詢功能,請查詢批處理視圖和實時視圖,並將結果合併在一起以獲得最終答案。

 

實時層是您使用Riak或Cassandra等讀/寫數據庫的地方,實時層依賴於增量算法來更新這些數據庫中的狀態。

用於實時計算的Hadoop模擬是Storm。我編寫了Storm,以便以可擴展和健壯的方式輕鬆地進行大量實時數據處理。Storm在數據流上運行無限計算,併爲數據處理提供強有力的保證。

讓我們回到查詢一段時間內URL的頁面查看次數的運行示例,看一下實時層的示例。

 

 

批處理系統與以前相同:基於Hadoop和ElephantDB的批處理工作流預先計算除最後幾小時數據之外的所有內容的查詢。剩下的就是構建實時系統來補償最後幾小時的數據。

我們將最近幾個小時的統計信息彙總到Cassandra中,我們將使用Storm處理頁面瀏覽量流並將更新並行化到數據庫中。每個網頁瀏覽都會導致一個計數器,[URL, hour]以便在Cassandra中增加一個密鑰。這就是它的全部 - 風暴讓這些事情變得非常簡單。

批處理層+實時層,CAP定理和人體容錯

在某些方面,似乎我們回到了我們開始的地方。實現實時查詢需要我們使用NoSQL數據庫和增量算法。這意味着我們又回到了不同價值觀,矢量時鐘和讀取修復的複雜世界。

但是有一個關鍵的區別。由於實時層僅補償最後幾小時的數據,因此實時層計算的所有內容最終都會被批處理層覆蓋。因此,如果您在實時圖層中出錯或出現問題,批處理層將對其進行更正。所有複雜性都是短暫的。

這並不意味着您不應該關心實時層中的讀取修復或最終一致性。您仍然希望實時圖層儘可能一致。但是當你犯錯誤時,你不會永久性地破壞你的數據。這消除了肩膀上巨大的複雜負擔。

在批處理層中,您只需要考慮該數據的數據和功能。批處理層很容易理解。另一方面,在實時層中,您必須使用增量算法和極其複雜的NoSQL數據庫。將所有複雜性分離到實時層中,可以在製作強大,可靠的系統方面產生巨大的差異。

此外,實時層不會影響系統的人體容錯能力。批處理層中僅附加的不可變數據集仍然是系統的核心,因此可以像以前一樣恢復任何錯誤。

讓我分享一個關於隔離實時層複雜性的巨大好處的個人故事。我的系統非常類似於我在這裏描述的系統:批處理層的Hadoop和ElephantDB,以及實時層的Storm和Cassandra。由於我的監控不力,有一天我醒來發現Cassandra已經用完空間並且每次請求都超時。這導致我的Storm拓撲失敗,並且數據流備份在隊列中。相同的消息一遍又一遍地重放(並保持失敗)。

如果我沒有批處理層,我將被迫擴展並恢復Cassandra。這不重要。更糟糕的是,由於多次重播相同的消息,大部分數據庫可能都不準確。

幸運的是,所有這些複雜性都在我的實時層中被隔離。我將備份的隊列刷新到批處理層,並創建了一個新的Cassandra集羣。批處理層像發條一樣運行,幾個小時內一切都恢復正常。沒有數據丟失,我們的查詢沒有任何不準確之處。

垃圾收集

我在這篇文章中描述的所有內容都是建立在不斷變化的,不斷增長的數據集的基礎之上的。那麼,如果您的數據集太大而無法一直存儲所有數據,即使使用水平可擴展存儲,您又該怎麼辦?這個用例是否打破了我所描述的一切?你應該回到使用可變數據庫嗎?

不。使用“垃圾收集”擴展基本模型很容易處理這個用例。垃圾收集只是一個接收主數據集並返回主數據集的過濾版本的函數。垃圾收集擺脫了低價值的數據。您可以使用任何策略進行垃圾回收。您可以通過僅保留實體的最後一個值來模擬可變性,也可以保留每個實體的歷史記錄。例如,如果您要處理位置數據,您可能希望每個人每年保留一個位置以及當前位置。可變性實際上只是一種不靈活的垃圾收集形式(它與CAP定理的交互也很差)。

垃圾收集作爲批處理任務實現。這是你偶爾跑步的東西,也許是每月一次。由於垃圾收集作爲離線批處理任務運行,因此不會影響系統與CAP定理的交互方式。

結論

使可擴展數據系統困難的原因不是CAP定理。它依賴於增量算法和可變狀態,導致我們系統的複雜性。直到最近,隨着分佈式數據庫的興起,這種複雜性已經失去控制。但這種複雜性一直存在。

我在本文開頭說過,我將挑戰你對如何構建數據系統的基本假設。我把CRUD變成了CR,將持久性分成了單獨的批處理和實時系統,並且癡迷於人類容錯的重要性。多年來,經歷了許多來之不易的經歷,打破了我的舊假設並得出了這些結論。

批處理/實時架構有很多有趣的功能,我還沒有介紹。現在值得總結其中一些:

  1. 算法靈活性:某些算法難以逐步計算。例如,如果uniques集合變大,則計算唯一計數可能具有挑戰性。批處理/實時拆分使您可以靈活地在批處理層上使用精確算法,並在實時層上使用近似算法。批處理層不斷覆蓋實時層,因此近似值得到糾正,系統顯示出“最終準確性”的屬性。
  2. 架構遷移很簡單:架構遷移困難的日子已經一去不復返了。由於批處理計算是系統的核心,因此可以輕鬆地在完整數據集上運行函數。這樣可以輕鬆更改數據或視圖的架構。
  3. 輕鬆的臨時分析:批處理層的任意性意味着您可以在數據上運行任何您喜歡的查詢。由於可以在一個位置訪問所有數據,因此這很簡單方便。
  4. 自我審覈:通過將數據視爲不可變數據,您可以獲得自我審覈數據集。數據集記錄自己的歷史記錄。我已經討論過這對於人類容錯有多重要,但它對於進行分析也非常有用。

我並沒有聲稱已經“解決”了大數據領域,但我已經制定了思考大數據的框架。批處理/實時體系結構非常通用,可以應用於任何數據系統。我告訴你如何爲任何種類的魚和任何水製作釣竿,而不是給你一條魚或釣魚竿。

還有很多工作要做,以提高我們攻擊大數據問題的集體能力。以下是一些重要的改進領域:

  1. 批量可寫,隨機讀取數據庫的擴展數據模型:並非每個應用程序都受鍵/值數據模型的支持。這就是爲什麼我的團隊正在投資擴展ElephantDB以支持搜索,文檔數據庫,範圍查詢等。
  2. 更好的批處理原語:Hadoop並不是批處理計算的最終目的。對於某些類型的計算,它可能是低效的。Spark是一個重要的項目,在擴展MapReduce範例方面做了有趣的工作。
  3. 改進的讀/寫NoSQL數據庫:有更多數據庫具有不同數據模型的空間,這些項目通常將受益於更成熟。
  4. 高級抽象:未來工作中最有趣的領域之一是高級抽象,映射到批處理組件和實時處理組件。沒有理由不應該將聲明性語言的簡潔性與批處理/實時體系結構的健壯性結合起來。

很多人都想要一個可擴展的關係數據庫。我希望你在這篇文章中意識到的是你根本不需要它!大數據和NoSQL運動似乎使數據管理比RDBMS更復雜,但這僅僅是因爲我們試圖像處理RDBMS數據一樣處理“大數據”:通過混合數據和視圖以及依賴關於增量算法。大數據的規模使您可以以完全不同的方式構建系統。通過將數據存儲爲不斷擴展的不可變事實集並將重新計算構建到核心中,大數據系統實際上比關係系統更容易推理。它會擴展。

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