談談從CAP定理到Lambda架構的演化
CAP 定理指出數據庫不能同時保證一致性、可用性和分區容錯性。但是我們不能犧牲分區容錯性,因此必須在可用性和一致性之間做出權衡。管理這種權衡是 NoSQL 運行的核心焦點。
一致性意味著在成功寫入之后,以后的讀取將始終考慮該寫入。可用性意味著可以隨時讀取和寫入系統。在分區期間,只能擁有這些屬性之一。
選擇一致性而不是可用性的系統必須處理一些棘手的問題。當數據庫不可用時怎么做?可以嘗試緩沖寫入以備后用,但是如果丟失了帶有緩沖區的機器,就有丟失這些寫入的風險。此外,緩沖寫入可能是一種不一致的形式,因為客戶端認為寫入已成功但寫入尚未在數據庫中。或者,可以在數據庫不可用時將錯誤返回給客戶端。但是,如果曾經使用過一種告訴“稍后再試”的產品,就會知道這會是多么令人惱火。
另一種選擇是選擇可用性而不是一致性。這些系統所能提供的最好的一致性保證就是“最終一致性”。如果使用最終一致的數據庫,那么有時會讀取到與剛剛寫入的結果不同的結果。有時多個訪問者同時讀取同一個密鑰會得到不同的結果。更新可能不會傳遞到一個值的所有副本,因此最終會得到一些副本獲得一些更新而其他副本獲得不同的更新。一旦檢測到值出現差異,就可以修復該值。這需要使用矢量時鐘追溯歷史并將更新合并在一起,稱為“讀取修復”。
在應用層維護最終一致性對開發人員來說負擔太重。讀取修復代碼極易受到開發人員錯誤的影響;如果犯了錯誤,錯誤的讀取修復將給數據庫帶來不可逆轉的損壞。因此犧牲可用性是有問題的,最終一致性太復雜以至于無法合理地構建應用程序。然而,這是唯一的兩個選擇, CAP 定理是自然界的事實,那么還有什么替代方案呢?還有另一種方法。你無法避免 CAP 定理,但你可以隔離它的復雜性并防止它破壞你對系統進行推理的能力。CAP 定理引起的復雜性是我們如何構建數據系統的基本問題。有兩個問題特別突出:在數據庫中使用可變狀態以及使用增量算法來更新該狀態。正是這些問題與 CAP 定理之間的相互作用導致了復雜性。
在這篇文章中將展示一個系統的設計,該系統通過防止 CAP 定理通常引起的復雜性來突破它。CAP 定理是關于數據系統對機器故障的容錯程度的結果。然而,有一種比機器容錯更重要的容錯形式:人為容錯。如果軟件開發有任何確定性,那就是開發人員并不完美,錯誤將不可避免地影響生產。我們的數據系統必須對寫入錯誤數據的錯誤程序具有彈性,而下面將要展示的系統具有盡可能多的人為容錯能力。
這篇文章將挑戰對如何構建數據系統的基本假設。但是,通過打破我們當前的思維方式并重新想象應該如何構建數據系統,出現的是一種比想象的更好的、可擴展和健壯的架構。
什么是數據系統
在我們談論系統設計之前,讓我們首先定義我們試圖解決的問題。數據系統的目的是什么?什么是數據?除非我們可以用清楚地封裝每個數據應用程序的定義來回答這些問題,否則我們甚至不需要接近 CAP 定理。
數據應用范圍從存儲和檢索對象、連接、聚合、流處理、連續計算、機器學習等等。目前尚不清楚是否存在如此簡單的數據系統定義——似乎我們對數據所做的事情范圍太廣,無法用單一定義來概括。
但是,有這么一個簡單的定義。就是這個:
Query = Function(All Data)
而已。這個等式總結了數據庫和數據系統的整個領域。該領域的一切——過去 50 年的 RDBMS、索引、OLAP、OLTP、MapReduce、ETL、分布式文件系統、流處理器、NoSQL 等——都以這種或那種方式總結為該等式。
數據系統回答有關數據集的問題。這些問題稱為“查詢”。這個等式表明查詢只是擁有的所有數據的函數。
這個等式可能看起來過于籠統而無用。它似乎沒有捕捉到數據系統設計的任何復雜性。但重要的是每個數據系統都屬于這個等式。該等式是我們探索數據系統的起點,該等式最終將導致一種突破 CAP 定理的方法。
這個等式中有兩個概念:“數據”和“查詢”。這些是在數據庫領域中經常混淆的不同概念,因此讓我們嚴格了解這些概念的含義。
數據
讓我們從“數據”開始。一條數據是一個不可分割的單元,你認為它是真實的,除了它存在之外沒有其他原因。它就像數學中的公理。
關于數據,有兩個重要的屬性需要注意。首先,數據本質上是基于時間的。一條數據是知道在某個時刻是真實的事實。例如,假設張梓涵在她的社交網絡資料中輸入她住在北京。從該輸入中獲取的數據是,截至她將該信息輸入她的個人資料的特定時刻,她住在北京。假設張梓涵稍后將她的個人資料位置更新為上海。然后你知道她在那段時間住在上海。她現在住在上海的事實并沒有改變她曾經住在北京的事實。這兩個數據都是真實的。
數據的第二個屬性緊隨第一個屬性:數據本質上是不可變的。由于它與時間點的聯系,一條數據的真實性永遠不會改變。人們無法回到過去來改變數據的真實性。這意味著只能對數據執行兩個主要操作:讀取現有數據和添加更多數據。CRUD變成了 CR。我省略了“更新”操作。這是因為更新對不可變數據沒有意義。例如,“更新”張梓涵的位置實際上意味著正在添加一條新數據,表明她最近住在一個新位置。我也省略了“刪除”操作。同樣,大多數刪除情況更好地表示為創建新數據。例如,如果張三停止在微博上關注李四,這不會改變他曾經關注過她的事實。因此,與其刪除表示他關注她的數據,不如添加一條新的數據記錄,說明他在某個時刻取消了對她的關注。
在某些情況下,確實希望永久刪除數據,例如要求在一定時間后清除數據的法規。將要展示的數據系統設計很容易支持這些情況,因此為了簡單起見,我們可以忽略這些情況。
這種數據定義幾乎肯定與習慣的不同,特別是如果來自以更新為常態的關系數據庫世界。有兩個原因。首先,這個數據定義非常通用:很難想出一種數據不符合這個定義。其次,數據的不變性是我們在設計一個戰勝 CAP 定理的人類容錯數據系統時要利用的關鍵屬性。
查詢
等式中的第二個概念是“查詢”。查詢是一組數據的推導。從這個意義上說,查詢就像數學中的定理。例如,“張梓涵目前的位置是什么?” 是一個查詢。可以通過返回有關張梓涵 位置的最新數據記錄來計算此查詢。查詢是完整數據集的函數,因此它們可以做任何事情:聚合、將不同類型的數據連接在一起等等。因此,可能會查詢服務的女性用戶數量,或者可能會查詢推文數據集,了解過去幾個小時的熱門話題。
我們已將查詢定義為完整數據集上的函數。當然,許多查詢不需要運行完整的數據集——它們只需要數據集的一個子集。但重要的是我們的定義封裝了所有可能的查詢,如果我們要突破 CAP 定理,我們必須能夠對任何查詢做到這一點。
突破 CAP 定理
計算查詢的最簡單方法是在完整數據集上逐字運行函數。如果可以在延遲限制內執行此操作,那么就完成了。沒有別的東西可以建造了。
當然,期望一個函數在一個完整的數據集上快速完成是不可行的。許多查詢,例如為網站提供服務的查詢,需要毫秒級的響應時間。但是,讓我們假設可以快速計算這些函數,讓我們看看這樣的系統如何與 CAP 定理交互。正如即將看到的,像這樣的系統不僅突破了 CAP 定理,而且還消滅了它。
CAP 定理仍然適用,因此需要在一致性和可用性之間做出選擇。關鍵之處在于,一旦決定了要做出的權衡,就完成了。通過使用不可變數據和從頭開始計算查詢,避免了 CAP 定理通常導致的復雜性。
如果選擇一致性而不是可用性,那么與以前相比不會有太大變化。有時將無法讀取或寫入數據,因為犧牲了可用性。但對于需要嚴格一致性的情況,這是一種選擇。
當選擇可用性而不是一致性時,事情會變得更加有趣。在這種情況下,系統是最終一致的,沒有任何最終一致性的復雜性。由于系統具有高可用性,可以隨時編寫新數據和計算查詢。在失敗的情況下,查詢將返回不包含以前寫入的數據的結果。最終,這些數據將是一致的,并且查詢會將這些數據合并到它們的計算中。
關鍵是數據是不可變的。不可變數據意味著沒有更新這樣的東西,因此一條數據的不同副本不可能變得不一致。這意味著沒有不同的值、矢量時鐘或讀取修復。從查詢的角度來看,一條數據要么存在,要么不存在。該數據上只有數據和功能。無需執行任何操作來強制執行最終一致性,并且最終一致性不會妨礙對系統進行推理。
之前導致復雜的是增量更新和CAP定理的交互。增量更新和 CAP 定理真的不能很好地結合在一起;可變值需要在最終一致的系統中進行讀取修復。通過拒絕增量更新、接受不可變數據以及每次都從頭開始計算查詢,可以避免這種復雜性。CAP 定理已被突破。
當然,我們剛剛經歷的是一個思想實驗。雖然我們希望每次都能從頭開始計算查詢,但這是不可行的。然而,我們已經了解了真實解決方案的一些關鍵屬性:
1.該系統使存儲和擴展不可變、不斷增長的數據集變得容易
2.主要的寫操作是添加新的不可變數據事實
3.系統通過從原始數據重新計算查詢來避免 CAP 定理的復雜性
4.系統使用增量算法將查詢延遲降低到可接受的水平
讓我們開始探索這樣一個系統是什么樣的。請注意,從這里開始的一切都是優化。數據庫、索引、ETL、批處理計算、流處理——這些都是優化查詢功能并將延遲降低到可接受水平的技術。這是一個簡單而深刻的認識。數據庫通常被認為是數據管理的核心,但實際上它們是更大范圍的一部分。
批量計算
弄清楚如何在任意數據集上快速運行任意函數是一個令人生畏的問題。所以讓我們稍微放松一下這個問題。讓我們假設查詢過時幾個小時是可以的。以這種方式放松問題會導致構建數據系統的簡單、優雅和通用的解決方案。之后,我們將擴展解決方案,使問題不再寬松。
由于查詢是所有數據的函數,因此使查詢快速運行的最簡單方法是預先計算它們。每當有新數據時,只需重新計算所有內容。這是可行的,因為我們放寬了問題,允許查詢過時幾個小時。這是此工作流程的示例:
要構建它,需要一個系統:
1.可以輕松存儲龐大且不斷增長的數據集
2.可以以可擴展的方式計算該數據集上的函數
這樣的系統是存在的。它成熟,經過數百個組織的實戰測試,并且擁有龐大的工具生態系統。它叫做Hadoop。Hadoop并不完美,但它是進行批處理的最佳工具。
很多人會說 Hadoop 只適用于“非結構化”數據。這是完全錯誤的。Hadoop 非常適合結構化數據。使用Thrift或Protocol Buffers等工具,可以使用豐富的、可演化的模式來存儲數據。
Hadoop 由兩部分組成:分布式文件系統 (HDFS) 和批處理框架 (MapReduce)。HDFS 擅長以可擴展的方式跨文件存儲大量數據。MapReduce 擅長以可擴展的方式對該數據運行計算。這些系統完全符合我們的需求。
我們會將數據存儲在 HDFS 上的平面文件中。文件將包含一系列數據記錄。要添加新數據,只需將包含新數據記錄的新文件附加到包含所有數據的文件夾即可。在 HDFS 上存儲這樣的數據解決了“存儲一個龐大且不斷增長的數據集”的需求。
對該數據進行預計算查詢同樣簡單明了。MapReduce 是一種具有足夠表現力的范例,幾乎任何功能都可以作為一系列 MapReduce 作業來實現。Cascalog、Cascading和Pig等工具使實現這些功能變得更加容易。
最后,需要為預計算的結果編制索引,以便應用程序可以快速訪問結果。有一類數據庫非常擅長于此。ElephantDB和Voldemort read-only專注于從 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 中,每次頁面瀏覽都會導致一個密鑰計數器遞增。這就是它的全部——Storm 使這些事情變得非常簡單。
批處理層 + 實時層、CAP 定理和人類容錯
在某些方面,我們似乎又回到了起點。實現實時查詢需要我們使用 NoSQL 數據庫和增量算法。這意味著我們回到了不同值、矢量時鐘和讀取修復的復雜世界。
但是有一個關鍵的區別。由于實時層僅補償最后幾個小時的數據,實時層計算的所有內容最終都會被批處理層覆蓋。因此,如果在實時層中犯了錯誤或出了什么問題,批處理層會糾正它。所有這些復雜性都是短暫的。
這并不意味著不應該關心實時層中的讀取修復或最終一致性。仍然希望實時層盡可能保持一致。但是,當犯錯時,不會永久損壞數據。這減輕了巨大的復雜性負擔。
在批處理層,你只需要考慮數據和數據上的函數。批處理層的推理非常簡單。另一方面,在實時層,必須使用增量算法和極其復雜的 NoSQL 數據庫。將所有這些復雜性隔離到實時層中,對于構建健壯、可靠的系統有很大的不同。
此外,實時層不會影響系統的人為容錯能力。批處理層中的 append-only 不可變數據集仍然是系統的核心,因此任何錯誤都可以像以前一樣從中恢復。
讓我們看一個關于在實時層中隔離復雜性的案例。有一個與這里描述的系統非常相似的系統:用于批處理層的 Hadoop 和 ElephantDB,以及用于實時層的 Storm 和 Cassandra。由于監控不力,有一天發現 Cassandra 空間不足并且每次請求都超時。這導致 Storm 拓撲失敗,數據流在隊列中備份。相同的消息不斷被重播并不斷失敗。
如果沒有批處理層,將不得不擴展和恢復 Cassandra。這很重要。更糟糕的是,由于多次重播相同的消息,許多數據庫可能不準確。
幸運的是,所有這些復雜性都隔離在實時層中。將備份的隊列刷新到批處理層并創建了一個新的 Cassandra 集群。批處理層像發條一樣運行,幾個小時內一切恢復正常。沒有數據丟失,查詢也沒有不準確之處。
垃圾收集
我們描述的一切都建立在一個不變的、不斷增長的數據集的基礎上。那么,如果的數據集太大以至于無法一直存儲所有數據,即使使用水平可擴展存儲,會怎么做?這個用例是否破壞了所描述的一切?你應該回去使用可變數據庫嗎?
不。很容易用“垃圾收集”擴展基本模型來處理這個用例。垃圾收集只是一個函數,它接受主數據集并返回主數據集的過濾版本。垃圾收集擺脫了低價值的數據。可以使用任何想要的策略來進行垃圾回收。可以通過僅保留實體的最后一個值來模擬可變性,或者可以保留每個實體的歷史記錄。例如,如果要處理位置數據,可能希望每年為每個人保留一個位置以及當前位置。可變性實際上只是一種不靈活的垃圾收集形式,它與 CAP 定理的交互也很差。
垃圾收集是作為批處理任務實現的。這是偶爾運行的東西,也許每月一次。由于垃圾收集作為離線批處理任務運行,因此它不會影響系統與 CAP 定理的交互方式。
小結
使可伸縮數據系統變得困難的不是 CAP 定理。正是對增量算法和可變狀態的依賴導致了我們系統的復雜性。隨著分布式數據庫的興起,這種復雜性才開始減弱。但這種復雜性一直存在。
批處理/實時架構還有許多其他功能。現在值得總結其中的一些:
- 算法靈活性:一些算法難以增量計算。例如,如果唯一值集變大,計算唯一值可能會很困難。批處理/實時拆分使可以靈活地在批處理層上使用精確算法,在實時層上使用近似算法。批處理層不斷覆蓋實時層,因此近似值得到糾正,系統表現出“最終準確性”的特性。
- 模式遷移很容易:由于批處理計算是系統的核心,因此很容易在完整的數據集上運行函數。這使得更改數據或視圖的模式變得容易。
- 輕松的臨時分析:批處理層的任意性意味著可以對數據運行任何喜歡的查詢。由于所有數據都可以在一個位置訪問,因此簡單方便。
- 自審計:通過將數據視為不可變的,可以獲得自審計數據集。數據集記錄了它自己的歷史。這對于人類容錯非常重要,它對于進行分析也非常有用。
批處理/實時架構具有很高的通用性,可以應用于任何數據系統。要提高我們解決大數據問題的集體能力,還有很多工作要做。以下是一些關鍵的改進領域:
- 批量可寫、隨機讀取數據庫的擴展數據模型:并非每個應用程序都受鍵/值數據模型支持。這就是為什么我的團隊正在投資擴展 ElephantDB 以支持搜索、文檔數據庫、范圍查詢等。
- 更好的批處理原語:Hadoop 不是批處理計算的終極目標。對于某些類型的計算,它可能效率低下。Spark是一個重要的項目,在擴展 MapReduce 范例方面做了有趣的工作。
- 改進的讀/寫 NoSQL 數據庫:有更多具有不同數據模型的數據庫的空間,這些項目通常會從更成熟的過程中受益。
- 高級抽象:未來工作中最有趣的領域之一是映射到批處理組件和實時處理組件的高級抽象。沒有理由不讓聲明性語言的簡潔性和批處理/實時架構的健壯性結合起來。
很多人都想要一個可擴展的關系數據庫。大數據和 NoSQL 運動似乎使數據管理比 RDBMS 更復雜,但這只是因為我們試圖像對待 RDBMS 數據一樣對待“大數據”:通過合并數據和視圖并依賴關于增量算法。大數據的規模讓能夠以完全不同的方式構建系統。通過將數據存儲為一組不斷擴展的不可變事實并將重新計算構建到核心中,大數據系統實際上比關系系統更容易推理。
以上便是Lambda 架構的想法,架構如下圖所示:
它的工作方式是捕獲不可變的記錄序列并將其并行輸入到批處理系統和流處理系統中。實現轉換邏輯兩次,一次在批處理系統中,一次在流處理系統中。在查詢時將兩個系統的結果拼接在一起以產生完整的結果。