systemDesign

Designing Data-Intensive Application - The Future of Data Systems

這是Designing Data-Intensive Application的第三部分第三章節: 數據系統的未來

本文所有圖片或代碼來自於原書內容

數據系統的未來

到目前為止 本書主要描述的是現狀 在這最後一章中 我們將討論未來 關於未來的觀點 就帶有作者比較多的主觀想法

Data integration 數據集成

這本書讀到這邊 我們對於每個任意的問題 都會有好幾種不同的解決方案 各有優缺點和利弊

比如我們在第三章討論存儲引擎時 我們討論日誌結構存儲 B樹 和列存儲 第五章討論複製時 我們討論了單領導者 多領導者以及無領導者

所以對於一個問題 哪一個解法才是正確的 沒有標準答案 一切取決於你應用的使用情況 你必須去了解每一個工具 畢竟工具的供應商不會告訴你他們的工具在什麼情況下不適用

了解了每個工具之後 你還必須知道你需要使用所有工具之中的哪些 來拼湊在一起 達到你應用希望能達到的功能

組合使用衍生數據的工具

我們常常需要組合使用我們手上的工具 比如我們可以把OLTP數據庫和全文搜索索引組合在一起來達到 arbitrary keyword的關鍵字搜索

我們在保持系統同步提到 隨著數據的不同表達形式的增加 集成問題變得更難了 畢竟你可能需要同時在數據庫, 搜尋索引, 分析系統中保存衍生數據的副本 這會讓你的維護成本提高

理解數據流

當需要在多個存儲系統中維護相同數據的副本以滿足不同的訪問模式時 你要對輸入和輸出非常了解: 哪些數據先被寫入(original data) 哪些數據衍生(derived)自哪些來源 如何以正確的格式 將數據導入正確的地方

如果你可以只透過單個系統來提供所有用戶輸入 而且寫入都以相同的順序處理 那所有衍生的數據都很容易被計算出來 如果你的應用是基於事件日誌來更新衍生數據的話 你還可以達到確定性(deterministic)和冪等性(idempotent) 這讓故障修復非常的輕鬆

衍生數據和分布式事務

為了保持不同的數據系統之間的一致性 最經典的方法是透過分布式事務(我們討論過不少關於原子提交和兩階段提交的問題) 那衍生數據系統的方法跟這個比起來如何呢?

抽象點來說 這兩種方法用了不同的方式達到類似的目標

分布式事務:透過鎖機制進行互斥來決定寫操作的順序 透過原子提交來確保更改只發生一次

CDC/事件溯源: 使用日誌來決定寫操作順序 基於確定性和冪等性來確保更改只發生一次

兩者最大的差別就是事務系統通常提供Linearizability 這提供了比如讀己之寫的保證 而衍生數據系統通常是異步的更新 所以沒有類似級別的保證

批處理與流處理

數據集成的目標是確保數據最終能在所有正確的地方表現出正確的形式 而批處理和流處理是實現這一目標的工具

因為批處理和流處理的輸出就是衍生數據集 主要差別是流處理器在無限數據集上運行 批處理的輸入則是已知的有限大小 但這兩者的區別也逐漸模糊 Spark在批處理引擎上執行流處理 將流分解為微批次(microbatches) Apache Flink則在流處理引擎上執行批處理

維護衍生狀態

批處理有著很強的函數式風格 因為它的如下特性:

1.鼓勵確定性的純函數

2.輸出僅依賴於輸入

3.除了顯式輸出外沒有副作用

4.輸入視作不可變的

5.輸出是僅追加的

流處理則是多添加了可以在失敗後重建狀態的好處

這些特性都讓我們很容易維持維護derived data

Lambda架構

我們可以說 批處理用於重新處理歷史數據 流處理用於處理最近的更新 至於要如何將兩者結合起來 Lambda是個不錯的建議

Lambda架構的核心思想是通過將不可變事件附加到不斷增長的數據集來記錄傳入數據(參考事件溯源) 為了從這些事件中衍生出讀取優化的視圖 Lambda架構建議並行運行這兩個不同的系統 主要做法也很簡單:

1.流處理器消耗事件並快速生成對視圖的近似更新

2.批處理器稍後將使用同一組事件並生成衍生視圖的更正版本

這樣的原則簡單清楚 在不可變事件流上建立衍生視圖 並在需要時重新處理事件

但也有些問題存在

1.在批處理和流處理框架中維護相同的邏輯是很顯著的額外工作

2.如果視圖中包含了類似join或是sessionization的操作 那要合併批處理與流處理的結果是很困難的

3.雖然有能力重新處理整個歷史數據集 但因為開銷具大 所以批處理流水線通常需要設置為增量(incremental)批處理 這也帶來了關於時間推理的問題

統一批處理和流處理

最近的發展使得lambda結構可以充分發揮優點而規避缺點 達到同時允許批處理(重新整理歷史數據)以及流計算(事件到達實時處理)在同一個系統中實現

在同一個系統中統一批處理和流處理需要以下功能

1.支持以相同的引擎來處理最新事件和歷史回放事件: 比如message broker可以重播舊信息 或是某些流處理器可以從HDFS中讀資料等等

2.讓流處理支持只處理一次的語義: 即使事實上發生了錯誤 流處理依然要確保最終輸出和未發生錯誤時相同 和批處理一樣 這需要丟棄任何失敗任務的部分輸出

3.支持依據事件發生時間而不是事件處理時間進行窗口化: 因為在支持重新處理歷史事件的前提中 事件的初次處理時間毫無意義

Unbunding Databases 分拆數據庫

分拆數據庫是最近提出的有趣概念 我們會在這節深入討論 我們先聊聊哲學上的問題

從最抽象的層面上理解 數據庫, Hadoop 和操作系統 都提供了相同的功能 他們保存某些數據 並支持處理以及查詢

數據庫將數據存儲在某些數據模型的紀錄中(row, document等等) 而操作系統則是將數據存在於文件 而Hadoop生態系就像是Unix操作系統的分布式版本

雖然概念類似 但也是有很大區別 基本上任何一個文件系統都很難處理10M個檔案的資料夾 但10M條紀錄在隨便一個資料庫中都是很稀鬆平常

再來深入探討一點 UNIX認為它的目的是為程序員提供一個邏輯的但是相當低層次的硬件抽象 而關係型數據庫則希望為應用提供一個高層次的抽象(來隱藏硬盤結構的複雜性 並發性 以及崩潰回覆)

我們在這一節來研究這兩種哲學 並各取所長

編排多種數據存儲技術

我們已經討論了數據庫提供的各種功能和工作原理 比如說

1.二級索引:根據字串高效的搜尋所有紀錄

2.物化視圖: 預先計算查詢結果並緩存

3.複製日誌: 使多節點的數據副本保持最新

4.全文搜索索引: 在文本中進行關鍵字搜索 並且將索引放在某些關聯性資料庫

我們發現 數據庫中提供的這些功能 以及採用批處理+流處理建構的衍生資料系統 有著許多對應關係 比如說創建索引

創建一個索引

想一下 當我們想要在資料庫中建一個索引CREATE INDEX發生了什麼事?

1.數據庫需要掃描所有table的一致性快照

2.挑選出所有被index的字段值

3.進行排序

4.得到索引表

5.再去拿我們取得快照之後的所有更新操作 apply到索引表中

有沒有覺得上面的步驟跟設置新從庫非常類似? 也非常類似於初始變更數據捕獲

無論什麼時候執行CREATE INDEX 數據庫都會重新處理現有的數據集 然後將索引作為一個新的視圖導出到現有的數據上

Meta data of everything

從這個角度想 你有沒有覺得數據的流動 本身就是一個數據庫

Alt text

每當我們做批處理 流處理 或是ETL把數據從A地傳輸到B地時並組裝時 這就像是一個數據庫的子系統 使索引或物化視圖保持最新

這樣想的話 批處理流處理 就像是我們討論過的trigger/stored procedures/materialzed view的精細實現 衍生數據系統就只不過是另一種索引類型罷了

那我們可不可以這麼說呢 比如說一個關聯性數據庫可能本身就支持了B-Tree索引 Hash索引 空間索引等等的各種索引 與前我們全部靠單一數據產品來實現 不如我們各種不同的索引都由不同的軟件提供 每種不同的索引由不同的團隊提供 運行在不同的機器上

之後你想要用B-Tree索引找你要的資料 你就去找團隊A 用工具A 你想用Hash索引找你要的資料 你就去找團隊B 用工具B

要怎麼從沒有適合所有訪問模式的單一數據模型發展到將不同的存儲和處理工具組合成一個有凝聚力的系統呢 有兩種途徑

1.聯合數據庫(Federated database) 又稱多態存儲(polystore): 統一讀端

統一讀端的意思 就是為各式各樣的底層存儲提供一個統一的查詢接口 當然如果你需要query某個特定的底層存儲還是可以直接問 但如果你要的是各種底層存儲的數據整合 還是可以使用那個統一接口來問

聯合查詢接口遵循著單一集成系統與關係型模型的傳統 帶有高級查詢語言和優雅的語義 但實現起來非常複雜

2.分拆式數據庫(Unbundled database, 又稱polystore): 統一寫端

雖然聯合能解決跨多個不同系統的只讀查詢問題 但它並沒有很好的解決跨系統同步寫入的問題

如果只是一個資料庫 要創建一個consistent的索引是很簡單的(基本上是內建的功能) 但如果我們要建構的是由多個存儲系統組成的資料庫 那我們就要確保所有的數據更改 都從一個主要的位置寫入 並且最後都會體現在所有正確的位置上 這樣才能放心的把各個存儲系統可靠地連接在一起

我們可以使用CDC或是事件日誌來把多個存儲系統連結在一起 就像是實現分拆式數據庫的各個索引維護功能

相比於聯合查詢接口遵循關係型模型的傳統 分拆式數據庫則是遵循著Unix的傳統: 用high-level的語言(shell)進行下令 透過統一的標準low-level API(pipes)溝通 每個單獨的小工具都做好自己的工作

細談分拆式如何工作

聯合數據庫的只讀查詢 需要在讀的時候 將一個數據模型映射到另一個數據模型 雖然不容易但終究是個好解決的問題 但分拆式模型的同步寫是個更加困難的問題 所以這節重點討論分拆式

我們知道要在單獨的系統中處理事務是很簡單的 但是當數據跨越不同技術的邊界時 比較好的方法是使用asynchronous的事件日誌來達到idempotent的目的

基於日誌的集成的優勢就是各個組件之間的loose-coupling 這體現在兩個方面

1.系統方面: 異步事件流使整個系統在應對各個組件中斷或性能下降表現更加穩健 畢竟日誌可以有緩衝(先暫存在buffer內) 等到有問題的組件修好之後再繼續處理 不會丟失任何數據 相比之下 分布式系統的同步交互往往使得同步故障升級成大規模故障

2.人員方面: 分拆式數據庫讓各個不同的團隊可以獨立地開發 改進 維護各個不同的軟件服務 專業化使得每個團隊可以專注的做好一件事情 而且被強迫要定義一個清晰的交互接口

觀察衍生數據狀態

上一節討論的數據流系統提供了創建衍生數據集(例如搜索索引, 物化視圖和預測模型)並使其保持更新的過程。我們將這個過程稱為寫路徑(write path): 只要某些信息被寫入系統 它可能會經歷批處理與流處理的多個階段 而最終每個衍生數據集都會被更新,以寫入正確的數據

下圖就是個更新搜尋索引的例子

Alt text

那為什麼我們要大費周章地創建衍生數據集呢 因為我們可能會想在之後查詢它 這就是讀路徑(read path): 當服務用戶請求時 你需要從衍生數據集中讀取 也許還要對結果進行一些額外處理 然後回傳給用戶

總而言之 寫路徑和讀路徑涵蓋了數據的整個旅程 寫路徑是無論如何都會走完 讀路徑則是有人查詢才會走

物化視圖和緩存

上圖的例子中 我們讓寫路徑做一點事 讀路徑做一點事 讓兩者在中點交會 但如果我們更佳熟悉讀路徑很常常被查詢的query 我們還可以先預先把這些結果先算好 這種常見的預先處理好的也稱為物化視圖 這讓我們寫路徑走得多一點 讀路徑走的短一點快一點 當然他們最終還是會在邊界相遇

記不記得我們在討論系統設計 - 設計Instagram服務的時候 我們討論到push/pull model 都是在移動讀寫交會的邊界

Doing the Right Thing 做正確的事情

本書中 我們考察了各種不同的數據系統架構 評價了它們的優點與缺點 並探討了構建可靠 可擴展 可維護應用的技術 但是 我們忽略了討論中一個重要而基礎的部分 現在我們來補充一下

每個系統都服務於一個目的 這個目的可能只是簡單地賺錢 但其對世界的影響很可能會遠遠超出最初的目的 我們身為建構這些系統的工程師 有責任去仔細考慮這些後果 並且認真思考我們想存活在什麼樣的世界之中

本書到目前爲止 都把數據當作一個抽象的東西來看待 但事實上 許多數據集都是關於人的 他們的行為 興趣 身份等等 對待這些數據 我們必須懷著人性與尊重

預測性分析

預測性分析是”大數據”炒作的主要內容之一 我們可以使用數據來預測明天天氣 這是一碼子事 但我們使用數據來分析一個罪犯是否會再犯 或是一個貸款申請人會不會違約 則是另一碼子的事 後者會直接影響到一個人的生活

偏見與歧視

算法做出的決定不一定比人類更好或更差 人們希望根據數據做出決定 而不是通過人的主觀評價與直覺 希望這樣能更加公平 並給予傳統體制中經常被忽視的人更好的機會

但預測性分析系統只是基於過去進行推斷 如果過去是歧視性的 它們就會將這種歧視歸納為規律 如果我們希望未來比過去更好 那麼就需要道德想像力 而這是只有人類才能提供的東西

數據與模型應該是我們的工具 而不是我們的主人

責任和問責

自動決策引發了關於責任與問責的問題 如果一個人犯了錯 他會需要被問責 但如果一個自動駕駛的車犯錯了 誰來負責呢 如果機器學期的系統做出的決定要受到司法審查 你能向法官解釋是如何做出決定的嗎

總結

我們討論了如何使用批處理與事件流來解決這一數據集成(data integration)問題 以便讓數據變更在不同系統之間流動

基於這種方法 某些系統被指定為記錄系統 而其他數據則是透過轉換而來 通過這種方式 我們可以維護索引 實體化視圖等等 通過使這些推導和變換異步和鬆耦合 我們防止了一個局部問題擴散到系統中不相關部分 增加整個系統的穩健性與容錯性

將數據流表示為從一個數據集到另一個數據集的轉換也有助於演化應用程序: 如果你想變更其中一個處理步驟(例如變更索引或緩存的結構) 則可以在整個輸入數據集上重新運行新的轉換代碼 就可以重新衍生輸出 出現問題時 你也可以修復代碼並重新處理數據來修復

這些過程與數據庫內建已經完成的過程非常類似 因此我們將數據流應用的概念重新理解為 分拆(unbundling) 數據庫組件 並通過組合這些loose-coupled的組件來構建應用程序

接下來 本書討論了如何確保正確性 作者建議可擴展的強完整性保證可以通過異步事件處理來實現 通過使用端到端操作identifier使達到idempotent 以及通過異步檢查約束 這種方法比使用分佈式事務的傳統方法更具可擴展性與可靠性 並且在實踐中適用於很多業務流程

最後 我們退後一步 審視了構建數據密集型應用的一些道德問題 我們看到 雖然數據可以用來做好事 但它也可能造成很大傷害 比如做出影響人們的決定卻難以申訴 導致歧視與剝削 即使是善意地使用數據也可能會導致意想不到的後果

由於軟件和數據對世界產生了如此巨大的影響 我們工程師們必須牢記 我們有責任為我們想要的那種世界而努力: 一個尊重人們 尊重人性的世界

我希望我們能夠一起為實現這一目標而努力。