Skip to content

第十二章:資料系統的未來

如果船長的終極目標是保護船隻,他應該永遠待在港口。

—— 聖托馬斯・阿奎那《神學大全》(1265-1274)


[TOC]

到目前為止,本書主要描述的是 現狀。在這最後一章中,我們將放眼 未來,討論應該是怎麼樣的:我將提出一些想法與方法,我相信它們能從根本上改進我們設計與構建應用的方式。

對未來的看法與推測當然具有很大的主觀性。所以在撰寫本章時,當提及我個人的觀點時會使用第一人稱。你完全可以不同意這些觀點並提出自己的看法,但我希望本章中的概念,至少能成為富有成效的討論出發點,並澄清一些經常被混淆的概念。

第一章 概述了本書的目標:探索如何建立 可靠可伸縮可維護 的應用與系統。這一主題貫穿了所有的章節:例如,我們討論了許多有助於提高可靠性的容錯演算法,有助於提高可伸縮性的分割槽,以及有助於提高可維護性的演化與抽象機制。在本章中,我們將把所有這些想法結合在一起,並在它們的基礎上展望未來。我們的目標是,發現如何設計出比現有應用更好的應用 —— 健壯、正確、可演化、且最終對人類有益。

資料整合

本書中反覆出現的主題是,對於任何給定的問題都會有好幾種解決方案,所有這些解決方案都有不同的優缺點與利弊權衡。例如在 第三章 討論儲存引擎時,我們看到了日誌結構儲存、B 樹以及列式儲存。在 第五章 討論複製時,我們看到了單領導者、多領導者和無領導者的方法。

如果你有一個類似於 “我想儲存一些資料並稍後再查詢” 的問題,那麼並沒有一種正確的解決方案。但對於不同的具體環境,總會有不同的合適方法。軟體實現通常必須選擇一種特定的方法。使單條程式碼路徑能做到穩定健壯且表現良好已經是一件非常困難的事情了 —— 嘗試在單個軟體中完成所有事情,幾乎可以保證,實現效果會很差。

因此軟體工具的最佳選擇也取決於情況。每一種軟體,甚至所謂的 “通用” 資料庫,都是針對特定的使用模式設計的。

面對讓人眼花繚亂的諸多替代品,第一個挑戰就是弄清軟體與其適用環境的對映關係。供應商不願告訴你他們軟體不適用的工作負載,這是可以理解的。但是希望先前的章節能給你提供一些問題,讓你讀出字裡行間的言外之意,並更好地理解這些權衡。

但是,即使你已經完全理解各種工具與其適用環境間的關係,還有一個挑戰:在複雜的應用中,資料的用法通常花樣百出。不太可能存在適用於 所有 不同資料應用場景的軟體,因此你不可避免地需要拼湊幾個不同的軟體來以提供應用所需的功能。

組合使用衍生資料的工具

例如,為了處理任意關鍵詞的搜尋查詢,將 OLTP 資料庫與全文搜尋索引整合在一起是很常見的的需求。儘管一些資料庫(例如 PostgreSQL)包含了全文索引功能,對於簡單的應用完全夠了【1】,但更複雜的搜尋能力就需要專業的資訊檢索工具了。相反的是,搜尋索引通常不適合作為持久的記錄系統,因此許多應用需要組合這兩種不同的工具以滿足所有需求。

我們在 “保持系統同步” 中接觸過整合資料系統的問題。隨著資料不同表示形式的增加,整合問題變得越來越困難。除了資料庫和搜尋索引之外,也許你需要在分析系統(資料倉庫,或批處理和流處理系統)中維護資料副本;維護從原始資料中衍生的快取,或反規範化的資料版本;將資料灌入機器學習、分類、排名或推薦系統中;或者基於資料變更傳送通知。

令人驚訝的是,我經常看到軟體工程師做出這樣的陳述:“根據我的經驗,99% 的人只需要 X” 或者 “...... 不需要 X”(對於各種各樣的 X)。我認為這種陳述更像是發言人自己的經驗,而不是技術實際上的實用性。可能對資料執行的操作,其範圍極其寬廣。某人認為雞肋而毫無意義的功能可能是別人的核心需求。當你拉高視角,並考慮跨越整個組織範圍的資料流時,資料整合的需求往往就會變得明顯起來。

理解資料流

當需要在多個儲存系統中維護相同資料的副本以滿足不同的訪問模式時,你要對輸入和輸出瞭如指掌:哪些資料先寫入,哪些資料表示衍生自哪些來源?如何以正確的格式,將所有資料匯入正確的地方?

例如,你可能會首先將資料寫入 記錄系統 資料庫,捕獲對該資料庫所做的變更(請參閱 “變更資料捕獲”),然後將變更以相同的順序應用於搜尋索引。如果變更資料捕獲(CDC)是更新索引的唯一方式,則可以確定該索引完全派生自記錄系統,因此與其保持一致(除軟體錯誤外)。寫入資料庫是向該系統提供新輸入的唯一方式。

允許應用程式直接寫入搜尋索引和資料庫引入瞭如 圖 11-4 所示的問題,其中兩個客戶端同時傳送衝突的寫入,且兩個儲存系統按不同順序處理它們。在這種情況下,既不是資料庫說了算,也不是搜尋索引說了算,所以它們做出了相反的決定,進入彼此間永續性的不一致狀態。

如果你可以透過單個系統來提供所有使用者輸入,從而決定所有寫入的排序,則透過按相同順序處理寫入,可以更容易地衍生出其他資料表示。 這是狀態機複製方法的一個應用,我們在 “全序廣播” 中看到。無論你使用變更資料捕獲還是事件溯源日誌,都不如簡單的基於全序的決策原則更重要。

基於事件日誌來更新衍生資料的系統,通常可以做到 確定性冪等性(請參閱 “冪等性”),使得從故障中恢復相當容易。

衍生資料與分散式事務

保持不同資料系統彼此一致的經典方法涉及分散式事務,如 “原子提交與兩階段提交” 中所述。與分散式事務相比,使用衍生資料系統的方法如何?

在抽象層面,它們透過不同的方式達到類似的目標。分散式事務透過 進行互斥來決定寫入的順序(請參閱 “兩階段鎖定”),而 CDC 和事件溯源使用日誌進行排序。分散式事務使用原子提交來確保變更只生效一次,而基於日誌的系統通常基於 確定性重試冪等性

最大的不同之處在於事務系統通常提供 線性一致性,這包含著有用的保證,例如 讀己之寫。另一方面,衍生資料系統通常是非同步更新的,因此它們預設不會提供相同的時序保證。

在願意為分散式事務付出代價的有限場景中,它們已被成功應用。但是,我認為 XA 的容錯能力和效能很差勁(請參閱 “實踐中的分散式事務”),這嚴重限制了它的實用性。我相信為分散式事務設計一種更好的協議是可行的。但使這樣一種協議被現有工具廣泛接受是很有挑戰的,且不是立竿見影的事。

在沒有廣泛支援的良好分散式事務協議的情況下,我認為基於日誌的衍生資料是整合不同資料系統的最有前途的方法。然而,諸如讀己之寫的保證是有用的,我認為告訴所有人 “最終一致性是不可避免的 —— 忍一忍並學會和它打交道” 是沒有什麼建設性的(至少在缺乏 如何 應對的良好指導時)。

在 “將事情做正確” 中,我們將討論一些在非同步衍生系統之上實現更強保障的方法,並邁向分散式事務和基於日誌的非同步系統之間的中間地帶。

全序的限制

對於足夠小的系統,構建一個完全有序的事件日誌是完全可行的(正如單主複製資料庫的流行所證明的那樣,它正好建立了這樣一種日誌)。但是,隨著系統向更大更複雜的工作負載伸縮,限制開始出現:

  • 在大多數情況下,構建完全有序的日誌,需要所有事件彙集於決定順序的 單個領導者節點。如果事件吞吐量大於單臺計算機的處理能力,則需要將其分割槽到多臺計算機上(請參閱 “分割槽日誌”)。然後兩個不同分割槽中的事件順序關係就不明確了。
  • 如果伺服器分佈在多個 地理位置分散 的資料中心上,例如為了容忍整個資料中心掉線,你通常在每個資料中心都有單獨的主庫,因為網路延遲會導致同步的跨資料中心協調效率低下(請參閱 “多主複製“)。這意味著源自兩個不同資料中心的事件順序未定義。
  • 將應用程式部署為微服務時(請參閱 “服務中的資料流:REST 與 RPC”),常見的設計選擇是將每個服務及其持久狀態作為獨立單元進行部署,服務之間不共享持久狀態。當兩個事件來自不同的服務時,這些事件間的順序未定義。
  • 某些應用程式在客戶端儲存狀態,該狀態在使用者輸入時立即更新(無需等待伺服器確認),甚至可以繼續離線工作(請參閱 “需要離線操作的客戶端”)。對於這樣的應用程式,客戶端和伺服器很可能以不同的順序看到事件。

在形式上,決定事件的全域性順序稱為 全序廣播,相當於 共識(請參閱 “共識演算法和全序廣播”)。大多數共識演算法都是針對單個節點的吞吐量足以處理整個事件流的情況而設計的,並且這些演算法不提供多個節點共享事件排序工作的機制。設計可以伸縮至單個節點的吞吐量之上,且在地理位置分散的環境中仍然工作良好的的共識演算法仍然是一個開放的研究問題。

排序事件以捕獲因果關係

在事件之間不存在因果關係的情況下,全序的缺乏並不是一個大問題,因為併發事件可以任意排序。其他一些情況很容易處理:例如,當同一物件有多個更新時,它們可以透過將特定物件 ID 的所有更新路由到相同的日誌分割槽來完全排序。然而,因果關係有時會以更微妙的方式出現(請參閱 “順序與因果關係”)。

例如,考慮一個社交網路服務,以及一對曾處於戀愛關係但剛分手的使用者。其中一個使用者將另一個使用者從好友中移除,然後向剩餘的好友傳送訊息,抱怨他們的前任。使用者的心思是他們的前任不應該看到這些粗魯的訊息,因為訊息是在好友狀態解除後傳送的。

但是如果好友關係狀態與訊息儲存在不同的地方,在這樣一個系統中,可能會出現 解除好友 事件與 傳送訊息 事件之間的因果依賴丟失的情況。如果因果依賴關係沒有被捕捉到,則傳送有關新訊息的通知的服務可能會在 解除好友 事件之前處理 傳送訊息 事件,從而錯誤地向前任傳送通知。

在本例中,通知實際上是訊息和好友列表之間的連線,使得它與我們先前討論的連線的時序問題有關(請參閱 “連線的時間依賴性”)。不幸的是,這個問題似乎並沒有一個簡單的答案【2,3】。起點包括:

  • 邏輯時間戳可以提供無需協調的全域性順序(請參閱 “序列號順序”),因此它們可能有助於全序廣播不可行的情況。但是,他們仍然要求收件人處理不按順序傳送的事件,並且需要傳遞其他元資料。
  • 如果你可以記錄一個事件來記錄使用者在做出決定之前所看到的系統狀態,並給該事件一個唯一的識別符號,那麼後面的任何事件都可以引用該事件識別符號來記錄因果關係【4】。我們將在 “讀也是事件” 中回到這個想法。
  • 衝突解決演算法(請參閱 “自動衝突解決”)有助於處理以意外順序傳遞的事件。它們對於維護狀態很有用,但如果行為有外部副作用(例如,給使用者傳送通知),就沒什麼幫助了。

也許,隨著時間的推移,應用開發模式將出現,使得能夠有效地捕獲因果依賴關係,並且保持正確的衍生狀態,而不會迫使所有事件經歷全序廣播的瓶頸)。

批處理與流處理

我會說資料整合的目標是,確保資料最終能在所有正確的地方表現出正確的形式。這樣做需要消費輸入、轉換、連線、過濾、聚合、訓練模型、評估、以及最終寫出適當的輸出。批處理和流處理是實現這一目標的工具。

批處理和流處理的輸出是衍生資料集,例如搜尋索引、物化檢視、向用戶顯示的建議、聚合指標等(請參閱 “批處理工作流的輸出” 和 “流處理的應用”)。

正如我們在 第十章第十一章 中看到的,批處理和流處理有許多共同的原則,主要的根本區別在於流處理器在無限資料集上執行,而批處理輸入是已知的有限大小。處理引擎的實現方式也有很多細節上的差異,但是這些區別已經開始模糊。

Spark 在批處理引擎上執行流處理,將流分解為 微批次(microbatches),而 Apache Flink 則在流處理引擎上執行批處理【5】。原則上,一種型別的處理可以用另一種型別來模擬,但是效能特徵會有所不同:例如,在跳躍或滑動視窗上,微批次可能表現不佳【6】。

維護衍生狀態

批處理有著很強的函式式風格(即使其程式碼不是用函式式語言編寫的):它鼓勵確定性的純函式,其輸出僅依賴於輸入,除了顯式輸出外沒有副作用,將輸入視作不可變的,且輸出是僅追加的。流處理與之類似,但它擴充套件了運算元以允許受管理的、容錯的狀態(請參閱 “失敗後重建狀態”)。

具有良好定義的輸入和輸出的確定性函式的原理不僅有利於容錯(請參閱 “冪等性”),也簡化了有關組織中資料流的推理【7】。無論衍生資料是搜尋索引、統計模型還是快取,採用這種觀點思考都是很有幫助的:將其視為從一個東西衍生出另一個的資料管道,透過函式式應用程式碼推送一個系統的狀態變更,並將其效果應用至衍生系統中。

原則上,衍生資料系統可以同步地維護,就像關係資料庫在與索引表寫入操作相同的事務中同步更新次級索引一樣。然而,非同步是使基於事件日誌的系統穩健的原因:它允許系統的一部分故障被抑制在本地。而如果任何一個參與者失敗,分散式事務將中止,因此它們傾向於透過將故障傳播到系統的其餘部分來放大故障(請參閱 “分散式事務的限制”)。

我們在 “分割槽與次級索引” 中看到,次級索引經常跨越分割槽邊界。具有次級索引的分割槽系統需要將寫入傳送到多個分割槽(如果索引按關鍵詞分割槽的話)或將讀取傳送到所有分割槽(如果索引是按文件分割槽的話)。如果索引是非同步維護的,這種跨分割槽通訊也是最可靠和最可伸縮的【8】(另請參閱 “多分割槽資料處理”)。

應用演化後重新處理資料

在維護衍生資料時,批處理和流處理都是有用的。流處理允許將輸入中的變化以低延遲反映在衍生檢視中,而批處理允許重新處理大量累積的歷史資料以便將新檢視匯出到現有資料集上。

特別是,重新處理現有資料為維護系統、演化並支援新功能和需求變更提供了一個良好的機制(請參閱 第四章)。沒有重新進行處理,模式演化將僅限於簡單的變化,例如向記錄中新增新的可選欄位或新增新型別的記錄。無論是在寫時模式還是在讀時模式中都是如此(請參閱 “文件模型中的模式靈活性”)。另一方面,透過重新處理,可以將資料集重組為一個完全不同的模型,以便更好地滿足新的要求。

鐵路上的模式遷移

大規模的 “模式遷移” 也發生在非計算機系統中。例如,在 19 世紀英國鐵路建設初期,軌距(兩軌之間的距離)就有了各種各樣的競爭標準。為一種軌距而建的列車不能在另一種軌距的軌道上執行,這限制了火車網路中可能的相互連線【9】。

在 1846 年最終確定了一個標準軌距之後,其他軌距的軌道必須轉換 —— 但是如何在不停運火車線路的情況下進行數月甚至數年的遷移?解決的辦法是首先透過新增第三條軌道將軌道轉換為 雙軌距(dual guage)混合軌距。這種轉換可以逐漸完成,當完成時,兩種軌距的列車都可以線上路上跑,使用三條軌道中的兩條。事實上,一旦所有的列車都轉換成標準軌距,那麼可以移除提供非標準軌距的軌道。

以這種方式 “再加工” 現有的軌道,讓新舊版本並存,可以在幾年的時間內逐漸改變軌距。然而,這是一項昂貴的事業,這就是今天非標準軌距仍然存在的原因。例如,舊金山灣區的 BART 系統使用了與美國大部分地區不同的軌距。

衍生檢視允許 漸進演化(gradual evolution)。如果你想重新構建資料集,不需要執行突然切換式的遷移。取而代之的是,你可以將舊架構和新架構並排維護為相同基礎資料上的兩個獨立衍生檢視。然後可以開始將少量使用者轉移到新檢視,以測試其效能並發現任何錯誤,而大多數使用者仍然會被路由到舊檢視。你可以逐漸地增加訪問新檢視的使用者比例,最終可以刪除舊檢視【10】。

這種逐漸遷移的美妙之處在於,如果出現問題,每個階段的過程都很容易逆轉:你始終有一個可以回滾的可用系統。透過降低不可逆損害的風險,你能對繼續前進更有信心,從而更快地改善系統【11】。

Lambda架構

如果批處理用於重新處理歷史資料,而流處理用於處理最近的更新,那麼如何將這兩者結合起來?Lambda 架構【12】是這方面的一個建議,引起了很多關注。

Lambda 架構的核心思想是透過將不可變事件附加到不斷增長的資料集來記錄傳入資料,這類似於事件溯源(請參閱 “事件溯源”)。為了從這些事件中衍生出讀取最佳化的檢視,Lambda 架構建議並行執行兩個不同的系統:批處理系統(如 Hadoop MapReduce)和獨立的流處理系統(如 Storm)。

在 Lambda 方法中,流處理器消耗事件並快速生成對檢視的近似更新;批處理器稍後將使用同一組事件並生成衍生檢視的更正版本。這個設計背後的原因是批處理更簡單,因此不易出錯,而流處理器被認為是不太可靠和難以容錯的(請參閱 “容錯”)。而且,流處理可以使用快速近似演算法,而批處理使用較慢的精確演算法。

Lambda 架構是一種有影響力的想法,它將資料系統的設計變得更好,尤其是透過推廣這樣的原則:在不可變事件流上建立衍生檢視,並在需要時重新處理事件。但是我也認為它有一些實際問題:

  • 在批處理和流處理框架中維護相同的邏輯是很顯著的額外工作。雖然像 Summingbird【13】這樣的庫提供了一種可以在批處理和流處理的上下文中執行的計算抽象。除錯、調整和維護兩個不同系統的操作複雜性依然存在【14】。
  • 由於流管道和批處理管道產生獨立的輸出,因此需要合併它們以響應使用者請求。如果計算是基於滾動視窗的簡單聚合,則合併相當容易,但如果檢視基於更複雜的操作(例如連線和會話化)而匯出,或者輸出不是時間序列,則會變得非常困難。
  • 儘管有能力重新處理整個歷史資料集是很好的,但在大型資料集上這樣做經常會開銷巨大。因此,批處理流水線通常需要設定為處理增量批處理(例如,在每小時結束時處理一小時的資料),而不是重新處理所有內容。這引發了 “時間推理” 中討論的問題,例如處理滯留事件和處理跨批次邊界的視窗。增量化批處理計算會增加複雜性,使其更類似於流式傳輸層,這與保持批處理層儘可能簡單的目標背道而馳。

統一批處理和流處理

最近的工作使得 Lambda 架構的優點在沒有其缺點的情況下得以實現,允許批處理計算(重新處理歷史資料)和流計算(在事件到達時即處理)在同一個系統中實現【15】。

在一個系統中統一批處理和流處理需要以下功能,這些功能也正在越來越廣泛地被提供:

  • 透過處理最近事件流的相同處理引擎來重播歷史事件的能力。例如,基於日誌的訊息代理可以重播訊息(請參閱 “重播舊訊息”),某些流處理器可以從 HDFS 等分散式檔案系統讀取輸入。
  • 對於流處理器來說,恰好一次語義 —— 即確保輸出與未發生故障的輸出相同,即使事實上發生故障(請參閱 “容錯”)。與批處理一樣,這需要丟棄任何失敗任務的部分輸出。
  • 按事件時間進行視窗化的工具,而不是按處理時間進行視窗化,因為處理歷史事件時,處理時間毫無意義(請參閱 “時間推理”)。例如,Apache Beam 提供了用於表達這種計算的 API,可以在 Apache Flink 或 Google Cloud Dataflow 使用。

分拆資料庫

在最抽象的層面上,資料庫,Hadoop 和作業系統都發揮相同的功能:它們儲存一些資料,並允許你處理和查詢這些資料【16】。資料庫將資料儲存為特定資料模型的記錄(表中的行、文件、圖中的頂點等),而作業系統的檔案系統則將資料儲存在檔案中 —— 但其核心都是 “資訊管理” 系統【17】。正如我們在 第十章 中看到的,Hadoop 生態系統有點像 Unix 的分散式版本。

當然,有很多實際的差異。例如,許多檔案系統都不能很好地處理包含 1000 萬個小檔案的目錄,而包含 1000 萬個小記錄的資料庫完全是尋常而不起眼的。無論如何,作業系統和資料庫之間的相似之處和差異值得探討。

Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Unix 認為它的目的是為程式設計師提供一種相當低層次的硬體的邏輯抽象,而關係資料庫則希望為應用程式設計師提供一種高層次的抽象,以隱藏磁碟上資料結構的複雜性,併發性,崩潰恢復等等。Unix 發展出的管道和檔案只是位元組序列,而資料庫則發展出了 SQL 和事務。

哪種方法更好?當然這取決於你想要的是什麼。 Unix 是 “簡單的”,因為它是對硬體資源相當薄的包裝;關係資料庫是 “更簡單” 的,因為一個簡短的宣告性查詢可以利用很多強大的基礎設施(查詢最佳化、索引、連線方法、併發控制、複製等),而不需要查詢的作者理解其實現細節。

這些哲學之間的矛盾已經持續了幾十年(Unix 和關係模型都出現在 70 年代初),仍然沒有解決。例如,我將 NoSQL 運動解釋為,希望將類 Unix 的低級別抽象方法應用於分散式 OLTP 資料儲存的領域。

在這一部分我將試圖調和這兩個哲學,希望我們能各取其美。

組合使用資料儲存技術

在本書的過程中,我們討論了資料庫提供的各種功能及其工作原理,其中包括:

第十章第十一章 中,出現了類似的主題。我們討論了如何構建全文搜尋索引(請參閱 “批處理工作流的輸出”),瞭解瞭如何維護物化檢視(請參閱 “維護物化檢視”)以及如何將變更從資料庫複製到衍生資料系統(請參閱 “變更資料捕獲”)。

資料庫中內建的功能與人們用批處理和流處理器構建的衍生資料系統似乎有相似之處。

建立索引

想想當你執行 CREATE INDEX 在關係資料庫中建立一個新的索引時會發生什麼。資料庫必須掃描表的一致性快照,挑選出所有被索引的欄位值,對它們進行排序,然後寫出索引。然後它必須處理自一致快照以來所做的寫入操作(假設表在建立索引時未被鎖定,所以寫操作可能會繼續)。一旦完成,只要事務寫入表中,資料庫就必須繼續保持索引最新。

此過程非常類似於設定新的從庫副本(請參閱 “設定新從庫”),也非常類似於流處理系統中的 引導(bootstrap) 變更資料捕獲(請參閱 “初始快照”)。

無論何時執行 CREATE INDEX,資料庫都會重新處理現有資料集(如 “應用演化後重新處理資料” 中所述),並將該索引作為新檢視匯出到現有資料上。現有資料可能是狀態的快照,而不是所有發生變化的日誌,但兩者密切相關(請參閱 “狀態、流和不變性”)。

一切的元資料庫

有鑑於此,我認為整個組織的資料流開始像一個巨大的資料庫【7】。每當批處理、流或 ETL 過程將資料從一個地方傳輸到另一個地方並組裝時,它表現地就像資料庫子系統一樣,使索引或物化檢視保持最新。

從這種角度來看,批處理和流處理器就像精心實現的觸發器、儲存過程和物化檢視維護例程。它們維護的衍生資料系統就像不同的索引型別。例如,關係資料庫可能支援 B 樹索引、雜湊索引、空間索引(請參閱 “多列索引”)以及其他型別的索引。在新興的衍生資料系統架構中,不是將這些設施作為單個整合資料庫產品的功能實現,而是由各種不同的軟體提供,執行在不同的機器上,由不同的團隊管理。

這些發展在未來將會把我們帶到哪裡?如果我們從沒有適合所有訪問模式的單一資料模型或儲存格式的前提出發,我推測有兩種途徑可以將不同的儲存和處理工具組合成一個有凝聚力的系統:

聯合資料庫:統一讀取

可以為各種各樣的底層儲存引擎和處理方法提供一個統一的查詢介面 —— 一種稱為 聯合資料庫(federated database)多型儲存(polystore) 的方法【18,19】。例如,PostgreSQL 的 外部資料包裝器(foreign data wrapper) 功能符合這種模式【20】。需要專用資料模型或查詢介面的應用程式仍然可以直接訪問底層儲存引擎,而想要組合來自不同位置的資料的使用者可以透過聯合介面輕鬆完成操作。

聯合查詢介面遵循著單一整合系統的關係型傳統,帶有高階查詢語言和優雅的語義,但實現起來非常複雜。

分拆資料庫:統一寫入

雖然聯合能解決跨多個不同系統的只讀查詢問題,但它並沒有很好的解決跨系統 同步 寫入的問題。我們說過,在單個數據庫中,建立一致的索引是一項內建功能。當我們構建多個儲存系統時,我們同樣需要確保所有資料變更都會在所有正確的位置結束,即使在出現故障時也是如此。想要更容易地將儲存系統可靠地插接在一起(例如,透過變更資料捕獲和事件日誌),就像將資料庫的索引維護功能以可以跨不同技術同步寫入的方式分開【7,21】。

分拆方法遵循 Unix 傳統的小型工具,它可以很好地完成一件事【22】,透過統一的低層級 API(管道)進行通訊,並且可以使用更高層級的語言進行組合(shell)【16】 。

開展分拆工作

聯合和分拆是一個硬幣的兩面:用不同的元件構成可靠、 可伸縮和可維護的系統。聯合只讀查詢需要將一個數據模型對映到另一個數據模型,這需要一些思考,但最終還是一個可解決的問題。而我認為同步寫入到幾個儲存系統是更困難的工程問題,所以我將重點關注它。

傳統的同步寫入方法需要跨異構儲存系統的分散式事務【18】,我認為這是錯誤的解決方案(請參閱 “衍生資料與分散式事務”)。單個儲存或流處理系統內的事務是可行的,但是當資料跨越不同技術之間的邊界時,我認為具有冪等寫入的非同步事件日誌是一種更加健壯和實用的方法。

例如,分散式事務在某些流處理元件內部使用,以匹配 恰好一次(exactly-once) 語義(請參閱 “原子提交再現”),這可以很好地工作。然而,當事務需要涉及由不同人群編寫的系統時(例如,當資料從流處理元件寫入分散式鍵值儲存或搜尋索引時),缺乏標準化的事務協議會使整合更難。有冪等消費者的有序事件日誌(請參閱 “冪等性”)是一種更簡單的抽象,因此在異構系統中實現更加可行【7】。

基於日誌的整合的一大優勢是各個元件之間的 鬆散耦合(loose coupling),這體現在兩個方面:

  1. 在系統級別,非同步事件流使整個系統在個別元件的中斷或效能下降時更加穩健。如果消費者執行緩慢或失敗,那麼事件日誌可以緩衝訊息(請參閱 “磁碟空間使用”),以便生產者和任何其他消費者可以繼續不受影響地執行。有問題的消費者可以在問題修復後趕上,因此不會錯過任何資料,並且包含故障。相比之下,分散式事務的同步互動往往會將本地故障升級為大規模故障(請參閱 “分散式事務的限制”)。
  2. 在人力方面,分拆資料系統允許不同的團隊獨立開發,改進和維護不同的軟體元件和服務。專業化使得每個團隊都可以專注於做好一件事,並與其他團隊的系統以明確的介面互動。事件日誌提供了一個足夠強大的介面,以捕獲相當強的一致性屬性(由於永續性和事件的順序),但也足夠普適於幾乎任何型別的資料。

分拆系統vs整合系統

如果分拆確實成為未來的方式,它也不會取代目前形式的資料庫 —— 它們仍然會像以往一樣被需要。為了維護流處理元件中的狀態,資料庫仍然是需要的,並且為批處理和流處理器的輸出提供查詢服務(請參閱 “批處理工作流的輸出” 與 “流處理”)。專用查詢引擎對於特定的工作負載仍然非常重要:例如,MPP 資料倉庫中的查詢引擎針對探索性分析查詢進行了最佳化,並且能夠很好地處理這種型別的工作負載(請參閱 “Hadoop 與分散式資料庫的對比” 。

執行幾種不同基礎設施的複雜性可能是一個問題:每種軟體都有一個學習曲線,配置問題和操作怪癖,因此部署儘可能少的移動部件是很有必要的。比起使用應用程式碼拼接多個工具而成的系統,單一整合軟體產品也可以在其設計應對的工作負載型別上實現更好、更可預測的效能【23】。正如在前言中所說的那樣,為了不需要的規模而構建系統是白費精力,而且可能會將你鎖死在一個不靈活的設計中。實際上,這是一種過早最佳化的形式。

分拆的目標不是要針對個別資料庫與特定工作負載的效能進行競爭;我們的目標是允許你結合多個不同的資料庫,以便在比單個軟體可能實現的更廣泛的工作負載範圍內實現更好的效能。這是關於廣度,而不是深度 —— 與我們在 “Hadoop 與分散式資料庫的對比” 中討論的儲存和處理模型的多樣性一樣。

因此,如果有一項技術可以滿足你的所有需求,那麼最好使用該產品,而不是試圖用更低層級的元件重新實現它。只有當沒有單一軟體滿足你的所有需求時,才會出現拆分和聯合的優勢。

少了什麼?

用於組成資料系統的工具正在變得越來越好,但我認為還缺少一個主要的東西:我們還沒有與 Unix shell 類似的分拆資料庫等價物(即,一種宣告式的、簡單的、用於組裝儲存和處理系統的高階語言)。

例如,如果我們可以簡單地宣告 mysql | elasticsearch,類似於 Unix 管道【22】,成為 CREATE INDEX 的分拆等價物:它將讀取 MySQL 資料庫中的所有文件並將其索引到 Elasticsearch 叢集中。然後它會不斷捕獲對資料庫所做的所有變更,並自動將它們應用於搜尋索引,而無需編寫自定義應用程式碼。這種整合應當支援幾乎任何型別的儲存或索引系統。

同樣,能夠更容易地預先計算和更新快取將是一件好事。回想一下,物化檢視本質上是一個預先計算的快取,所以你可以透過為複雜查詢宣告指定物化檢視來建立快取,包括圖上的遞迴查詢(請參閱 “圖資料模型”)和應用邏輯。在這方面有一些有趣的早期研究,如 差分資料流(differential dataflow)【24,25】,我希望這些想法能夠在生產系統中找到自己的方法。

圍繞資料流設計應用

使用應用程式碼組合專用儲存與處理系統來分拆資料庫的方法,也被稱為 “資料庫由內而外(database inside-out)” 方法【26】,該名稱來源於我在 2014 年的一次會議演講標題【27】。然而稱它為 “新架構” 過於誇大,我僅將其看作是一種設計模式,一個討論的起點,我們只是簡單地給它起一個名字,以便我們能更好地討論它。

這些想法不是我的;它們是很多人的思想的融合,這些思想非常值得我們學習。尤其是,以 Oz【28】和 Juttle【29】為代表的資料流語言,以 Elm【30,31】為代表的 函式式響應式程式設計(functional reactive programming, FRP),以 Bloom【32】為代表的邏輯程式語言。在這一語境中的術語 分拆(unbundling) 是由 Jay Kreps 提出的【7】。

即使是 電子表格 也在資料流程式設計能力上甩開大多數主流程式語言幾條街【33】。在電子表格中,可以將公式放入一個單元格中(例如,對另一列中的單元格求和),並且只要公式的任何輸入發生變更,公式的結果都會自動重新計算。這正是我們在資料系統層次所需要的:當資料庫中的記錄發生變更時,我們希望自動更新該記錄的任何索引,並且自動重新整理依賴於記錄的任何快取檢視或聚合。你不必擔心這種重新整理如何發生的技術細節,但能夠簡單地相信它可以正常工作。

因此,我認為絕大多數資料系統仍然可以從 VisiCalc 在 1979 年已經具備的功能中學習【34】。與電子表格的不同之處在於,今天的資料系統需要具有容錯性,可伸縮性以及持久儲存資料。它們還需要能夠整合不同人群編寫的不同技術,並重用現有的庫和服務:期望使用某一種特定的語言、框架或工具來開發所有軟體是不切實際的。

在本節中,我將詳細介紹這些想法,並探討一些圍繞分拆資料庫和資料流的想法構建應用的方法。

應用程式碼作為衍生函式

當一個數據集衍生自另一個數據集時,它會經歷某種轉換函式。例如:

  • 次級索引是由一種直白的轉換函式生成的衍生資料集:對於基礎表中的每行或每個文件,它挑選被索引的列或欄位中的值,並按這些值排序(假設使用 B 樹或 SSTable 索引,按鍵排序,如 第三章 所述)。
  • 全文搜尋索引是透過應用各種自然語言處理函式而建立的,諸如語言檢測、分詞、詞幹或詞彙化、拼寫糾正和同義詞識別,然後構建用於高效查詢的資料結構(例如倒排索引)。
  • 在機器學習系統中,我們可以將模型視作從訓練資料透過應用各種特徵提取、統計分析函式衍生的資料,當模型應用於新的輸入資料時,模型的輸出是從輸入和模型(因此間接地從訓練資料)中衍生的。
  • 快取通常包含將以使用者介面(UI)顯示的形式的資料聚合。因此填充快取需要知道 UI 中引用的欄位;UI 中的變更可能需要更新快取填充方式的定義,並重建快取。

用於次級索引的衍生函式是如此常用的需求,以致於它作為核心功能被內建至許多資料庫中,你可以簡單地透過 CREATE INDEX 來呼叫它。對於全文索引,常見語言的基本語言特徵可能內建到資料庫中,但更複雜的特徵通常需要領域特定的調整。在機器學習中,特徵工程是眾所周知的特定於應用的特徵,通常需要包含很多關於使用者互動與應用部署的詳細知識【35】。

當建立衍生資料集的函式不是像建立次級索引那樣的標準搬磚函式時,需要自定義程式碼來處理特定於應用的東西。而這個自定義程式碼是讓許多資料庫掙扎的地方,雖然關係資料庫通常支援觸發器、儲存過程和使用者定義的函式,可以用它們來在資料庫中執行應用程式碼,但它們有點像資料庫設計裡的事後反思。(請參閱 “傳遞事件流”)。

應用程式碼和狀態的分離

理論上,資料庫可以是任意應用程式碼的部署環境,就如同作業系統一樣。然而實踐中它們對這一目標適配的很差。它們不滿足現代應用開發的要求,例如依賴和軟體包管理、版本控制、滾動升級、可演化性、監控、指標、對網路服務的呼叫以及與外部系統的整合。

另一方面,Mesos、YARN、Docker、Kubernetes 等部署和叢集管理工具專為執行應用程式碼而設計。透過專注於做好一件事情,他們能夠做得比將資料庫作為其眾多功能之一執行使用者定義的功能要好得多。

我認為讓系統的某些部分專門用於持久資料儲存並讓其他部分專門執行應用程式程式碼是有意義的。這兩者可以在保持獨立的同時互動。

現在大多數 Web 應用程式都是作為無狀態服務部署的,其中任何使用者請求都可以路由到任何應用程式伺服器,並且伺服器在傳送響應後會忘記所有請求。這種部署方式很方便,因為可以隨意新增或刪除伺服器,但狀態必須到某個地方:通常是資料庫。趨勢是將無狀態應用程式邏輯與狀態管理(資料庫)分開:不將應用程式邏輯放入資料庫中,也不將持久狀態置於應用程式中【36】。正如函數語言程式設計社群喜歡開玩笑說的那樣,“我們相信 教會(Church)國家(state) 的分離”【37】 [^i]

[^i]: 解釋笑話很少會讓人感覺更好,但我不想讓任何人感到被遺漏。 在這裡,Church 指代的是數學家的阿隆佐・邱奇,他創立了 lambda 演算,這是計算的早期形式,是大多數函數語言程式設計語言的基礎。 lambda 演算不具有可變狀態(即沒有變數可以被覆蓋),所以可以說可變狀態與 Church 的工作是分離的。

在這個典型的 Web 應用模型中,資料庫充當一種可以透過網路同步訪問的可變共享變數。應用程式可以讀取和更新變數,而資料庫負責維持它的永續性,提供一些諸如併發控制和容錯的功能。

但是,在大多數程式語言中,你無法訂閱可變變數中的變更 —— 你只能定期讀取它。與電子表格不同,如果變數的值發生變化,變數的讀者不會收到通知(你可以在自己的程式碼中實現這樣的通知 —— 這被稱為 觀察者模式 —— 但大多數語言沒有將這種模式作為內建功能)。

資料庫繼承了這種可變資料的被動方法:如果你想知道資料庫的內容是否發生了變化,通常你唯一的選擇就是輪詢(即定期重複你的查詢)。 訂閱變更只是剛剛開始出現的功能(請參閱 “變更流的 API 支援”)。

資料流:應用程式碼與狀態變化的互動

從資料流的角度思考應用程式,意味著重新協調應用程式碼和狀態管理之間的關係。我們不再將資料庫視作被應用操縱的被動變數,取而代之的是更多地考慮狀態,狀態變更和處理它們的程式碼之間的相互作用與協同關係。應用程式碼透過在另一個地方觸發狀態變更來響應狀態變更。

我們在 “資料庫與流” 中看到了這一思路,我們討論了將資料庫的變更日誌視為一種我們可以訂閱的事件流。諸如 Actor 的訊息傳遞系統(請參閱 “訊息傳遞中的資料流”)也具有響應事件的概念。早在 20 世紀 80 年代,元組空間(tuple space) 模型就已經探索了表達分散式計算的方式:觀察狀態變更並作出反應的過程【38,39】。

如前所述,當觸發器由於資料變更而被觸發時,或次級索引更新以反映索引表中的變更時,資料庫內部也發生著類似的情況。分拆資料庫意味著將這個想法應用於在主資料庫之外,用於建立衍生資料集:快取、全文搜尋索引、機器學習或分析系統。我們可以為此使用流處理和訊息傳遞系統。

需要記住的重要一點是,維護衍生資料不同於執行非同步任務。傳統的訊息傳遞系統通常是為執行非同步任務設計的(請參閱 “日誌與傳統的訊息傳遞相比”):

  • 在維護衍生資料時,狀態變更的順序通常很重要(如果多個檢視是從事件日誌衍生的,則需要按照相同的順序處理事件,以便它們之間保持一致)。如 “確認與重新傳遞” 中所述,許多訊息代理在重傳未確認訊息時沒有此屬性,雙寫也被排除在外(請參閱 “保持系統同步”)。
  • 容錯是衍生資料的關鍵:僅僅丟失單個訊息就會導致衍生資料集永遠與其資料來源失去同步。訊息傳遞和衍生狀態更新都必須可靠。例如,許多 Actor 系統預設在記憶體中維護 Actor 的狀態和訊息,所以如果執行 Actor 的機器崩潰,狀態和訊息就會丟失。

穩定的訊息排序和容錯訊息處理是相當嚴格的要求,但與分散式事務相比,它們開銷更小,執行更穩定。現代流處理元件可以提供這些排序和可靠性保證,並允許應用程式碼以流運算元的形式執行。

這些應用程式碼可以執行任意處理,包括資料庫內建衍生函式通常不提供的功能。就像透過管道連結的 Unix 工具一樣,流運算元可以圍繞著資料流構建大型系統。每個運算元接受狀態變更的流作為輸入,併產生其他狀態變化的流作為輸出。

流處理器和服務

當今流行的應用開發風格涉及將功能分解為一組透過同步網路請求(如 REST API)進行通訊的 服務(service,請參閱 “服務中的資料流:REST 與 RPC”)。這種面向服務的架構優於單一龐大應用的優勢主要在於:通過鬆散耦合來提供組織上的可伸縮性:不同的團隊可以專職於不同的服務上,從而減少團隊之間的協調工作(因為服務可以獨立部署和更新)。

在資料流中組裝流運算元與微服務方法有很多相似之處【40】。但底層通訊機制是有很大區別:資料流採用單向非同步訊息流,而不是同步的請求 / 響應式互動。

除了在 “訊息傳遞中的資料流” 中列出的優點(如更好的容錯性),資料流系統還能實現更好的效能。例如,假設客戶正在購買以一種貨幣定價,但以另一種貨幣支付的商品。為了執行貨幣換算,你需要知道當前的匯率。這個操作可以透過兩種方式實現【40,41】:

  1. 在微服務方法中,處理購買的程式碼可能會查詢匯率服務或資料庫,以獲取特定貨幣的當前匯率。
  2. 在資料流方法中,處理訂單的程式碼會提前訂閱匯率變更流,並在匯率發生變動時將當前匯率儲存在本地資料庫中。處理訂單時只需查詢本地資料庫即可。

第二種方法能將對另一服務的同步網路請求替換為對本地資料庫的查詢(可能在同一臺機器甚至同一個程序中)[^ii]。資料流方法不僅更快,而且當其他服務失效時也更穩健。最快且最可靠的網路請求就是壓根沒有網路請求!我們現在不再使用 RPC,而是在購買事件和匯率更新事件之間建立流聯接(請參閱 “流表連線(流擴充)”)。

[^ii]: 在微服務方法中,你也可以透過在處理購買的服務中本地快取匯率來避免同步網路請求。 但是為了保證快取的新鮮度,你需要定期輪詢匯率以獲取其更新,或訂閱變更流 —— 這恰好是資料流方法中發生的事情。

連線是時間相關的:如果購買事件在稍後的時間點被重新處理,匯率可能已經改變。如果要重建原始輸出,則需要獲取原始購買時的歷史匯率。無論是查詢服務還是訂閱匯率更新流,你都需要處理這種時間相關性(請參閱 “連線的時間依賴性”)。

訂閱變更流,而不是在需要時查詢當前狀態,使我們更接近類似電子表格的計算模型:當某些資料發生變更時,依賴於此的所有衍生資料都可以快速更新。還有很多未解決的問題,例如關於時間相關連線等問題,但我認為圍繞資料流構建應用的想法是一個非常有希望的方向。

觀察衍生資料狀態

在抽象層面,上一節討論的資料流系統提供了建立衍生資料集(例如搜尋索引、物化檢視和預測模型)並使其保持更新的過程。我們將這個過程稱為 寫路徑(write path):只要某些資訊被寫入系統,它可能會經歷批處理與流處理的多個階段,而最終每個衍生資料集都會被更新,以適配寫入的資料。圖 12-1 顯示了一個更新搜尋索引的例子。

圖 12-1 在搜尋索引中,寫(文件更新)遇上讀(查詢)

但你為什麼一開始就要建立衍生資料集?很可能是因為你想在以後再次查詢它。這就是 讀路徑(read path):當服務使用者請求時,你需要從衍生資料集中讀取,也許還要對結果進行一些額外處理,然後構建給使用者的響應。

總而言之,寫路徑和讀路徑涵蓋了資料的整個旅程,從收集資料開始,到使用資料結束(可能是由另一個人)。寫路徑是預計算過程的一部分 —— 即,一旦資料進入,即刻完成,無論是否有人需要看它。讀路徑是這個過程中只有當有人請求時才會發生的部分。如果你熟悉函數語言程式設計語言,則可能會注意到寫路徑類似於立即求值,讀路徑類似於惰性求值。

圖 12-1 所示,衍生資料集是寫路徑和讀路徑相遇的地方。它代表了在寫入時需要完成的工作量與在讀取時需要完成的工作量之間的權衡。

物化檢視和快取

全文搜尋索引就是一個很好的例子:寫路徑更新索引,讀路徑在索引中搜索關鍵字。讀寫都需要做一些工作。寫入需要更新文件中出現的所有關鍵詞的索引條目。讀取需要搜尋查詢中的每個單詞,並應用布林邏輯來查詢包含查詢中所有單詞(AND 運算子)的文件,或者每個單詞(OR 運算子)的任何同義詞。

如果沒有索引,搜尋查詢將不得不掃描所有文件(如 grep),如果有著大量文件,這樣做的開銷巨大。沒有索引意味著寫入路徑上的工作量較少(沒有要更新的索引),但是在讀取路徑上需要更多工作。

另一方面,可以想象為所有可能的查詢預先計算搜尋結果。在這種情況下,讀路徑上的工作量會減少:不需要布林邏輯,只需查詢查詢結果並返回即可。但寫路徑會更加昂貴:可能的搜尋查詢集合是無限大的,因此預先計算所有可能的搜尋結果將需要無限的時間和儲存空間。那肯定沒戲 ^iii

另一種選擇是預先計算一組固定的最常見查詢的搜尋結果,以便可以快速提供它們而無需轉到索引。不常見的查詢仍然可以透過索引來提供服務。這通常被稱為常見查詢的 快取(cache),儘管我們也可以稱之為 物化檢視(materialized view),因為當新文件出現,且需要被包含在這些常見查詢的搜尋結果之中時,這些索引就需要更新。

從這個例子中我們可以看到,索引不是寫路徑和讀路徑之間唯一可能的邊界;快取常見搜尋結果也是可行的;而在少量文件上使用沒有索引的類 grep 掃描也是可行的。由此來看,快取,索引和物化檢視的作用很簡單:它們改變了讀路徑與寫路徑之間的邊界。透過預先計算結果,從而允許我們在寫路徑上做更多的工作,以節省讀路徑上的工作量。

在寫路徑上完成的工作和讀路徑之間的界限,實際上是本書開始處在 “描述負載” 中推特例子裡談到的主題。在該例中,我們還看到了與普通使用者相比,名人的寫路徑和讀路徑可能有所不同。在 500 頁之後,我們已經繞回了起點!

有狀態、可離線的客戶端

我發現寫路徑和讀路徑之間的邊界很有趣,因為我們可以試著改變這個邊界,並探討這種改變的實際意義。我們來看看不同上下文中的這一想法。

過去二十年來,Web 應用的火熱讓我們對應用開發作出了一些很容易視作理所當然的假設。具體來說就是,客戶端 / 伺服器模型 —— 客戶端大多是無狀態的,而伺服器擁有資料的權威 —— 已經普遍到我們幾乎忘掉了還有其他任何模型的存在。但是技術在不斷地發展,我認為不時地質疑現狀非常重要。

傳統上,網路瀏覽器是無狀態的客戶端,只有當連線到網際網路時才能做一些有用的事情(能離線執行的唯一事情基本上就是上下滾動之前線上時載入好的頁面)。然而,最近的 “單頁面” JavaScript Web 應用已經獲得了很多有狀態的功能,包括客戶端使用者介面互動,以及 Web 瀏覽器中的持久化本地儲存。移動應用可以類似地在裝置上儲存大量狀態,而且大多數使用者互動都不需要與伺服器往返互動。

這些不斷變化的功能重新引發了對 離線優先(offline-first) 應用的興趣,這些應用盡可能地在同一裝置上使用本地資料庫,無需連線網際網路,並在後臺網路連線可用時與遠端伺服器同步【42】。由於移動裝置通常具有緩慢且不可靠的蜂窩網路連線,因此,如果使用者的使用者介面不必等待同步網路請求,且應用主要是離線工作的,則這是一個巨大優勢(請參閱 “需要離線操作的客戶端”)。

當我們擺脫無狀態客戶端與中央資料庫互動的假設,並轉向在終端使用者裝置上維護狀態時,這就開啟了新世界的大門。特別是,我們可以將裝置上的狀態視為 伺服器狀態的快取。螢幕上的畫素是客戶端應用中模型物件的物化檢視;模型物件是遠端資料中心的本地狀態副本【27】。

將狀態變更推送給客戶端

在典型的網頁中,如果你在 Web 瀏覽器中載入頁面,並且隨後伺服器上的資料發生變更,則瀏覽器在重新載入頁面之前對此一無所知。瀏覽器只能在一個時間點讀取資料,假設它是靜態的 —— 它不會訂閱來自伺服器的更新。因此裝置上的狀態是陳舊的快取,除非你顯式輪詢變更否則不會更新。(像 RSS 這樣基於 HTTP 的 Feed 訂閱協議實際上只是一種基本的輪詢形式)

最近的協議已經超越了 HTTP 的基本請求 / 響應模式:服務端傳送的事件(EventSource API)和 WebSockets 提供了通訊通道,透過這些通道,Web 瀏覽器可以與伺服器保持開啟的 TCP 連線,只要瀏覽器仍然連線著,伺服器就能主動向瀏覽器推送資訊。這為伺服器提供了主動通知終端使用者客戶端的機會,伺服器能告知客戶端其本地儲存狀態的任何變化,從而減少客戶端狀態的陳舊程度。

用我們的寫路徑與讀路徑模型來講,主動將狀態變更推至到客戶端裝置,意味著將寫路徑一直延伸到終端使用者。當客戶端首次初始化時,它仍然需要使用讀路徑來獲取其初始狀態,但此後它就能夠依賴伺服器傳送的狀態變更流了。我們在流處理和訊息傳遞部分討論的想法並不侷限於資料中心中:我們可以進一步採納這些想法,並將它們一直延伸到終端使用者裝置【43】。

這些裝置有時會離線,並在此期間無法收到伺服器狀態變更的任何通知。但是我們已經解決了這個問題:在 “消費者偏移量” 中,我們討論了基於日誌的訊息代理的消費者能在失敗或斷開連線後重連,並確保它不會錯過掉線期間任何到達的訊息。同樣的技術適用於單個使用者,每個裝置都是一個小事件流的小小訂閱者。

端到端的事件流

最近用於開發有狀態的客戶端與使用者介面的工具,例如如 Elm 語言【30】和 Facebook 的 React、Flux 和 Redux 工具鏈,已經透過訂閱表示使用者輸入或伺服器響應的事件流來管理客戶端的內部狀態,其結構與事件溯源相似(請參閱 “事件溯源”)。

將這種程式設計模型擴充套件為:允許伺服器將狀態變更事件推送到客戶端的事件管道中,是非常自然的。因此,狀態變化可以透過 端到端(end-to-end) 的寫路徑流動:從一個裝置上的互動觸發狀態變更開始,經由事件日誌,並穿過幾個衍生資料系統與流處理器,一直到另一臺裝置上的使用者介面,而有人正在觀察使用者介面上的狀態變化。這些狀態變化能以相當低的延遲傳播 —— 比如說,在一秒內從一端到另一端。

一些應用(如即時訊息傳遞與線上遊戲)已經具有這種 “實時” 架構(在低延遲互動的意義上,不是在 “響應時間保證” 中的意義上)。但我們為什麼不用這種方式構建所有的應用?

挑戰在於,關於無狀態客戶端和請求 / 響應互動的假設已經根深蒂固地植入在在我們的資料庫、庫、框架以及協議之中。許多資料儲存支援讀取與寫入操作,為請求返回一個響應,但只有極少數提供訂閱變更的能力 —— 請求返回一個隨時間推移的響應流(請參閱 “變更流的 API 支援” )。

為了將寫路徑延伸至終端使用者,我們需要從根本上重新思考我們構建這些系統的方式:從請求 / 響應互動轉向釋出 / 訂閱資料流【27】。更具響應性的使用者介面與更好的離線支援,我認為這些優勢值得我們付出努力。如果你正在設計資料系統,我希望你對訂閱變更的選項留有印象,而不只是查詢當前狀態。

讀也是事件

我們討論過,當流處理器將衍生資料寫入儲存(資料庫,快取或索引)時,以及當用戶請求查詢該儲存時,儲存將充當寫路徑和讀路徑之間的邊界。該儲存應當允許對資料進行隨機訪問的讀取查詢,否則這些查詢將需要掃描整個事件日誌。

在很多情況下,資料儲存與流處理系統是分開的。但回想一下,流處理器還是需要維護狀態以執行聚合和連線的(請參閱 “流連線”)。這種狀態通常隱藏在流處理器內部,但一些框架也允許這些狀態被外部客戶端查詢【45】,將流處理器本身變成一種簡單的資料庫。

我願意進一步思考這個想法。正如到目前為止所討論的那樣,對儲存的寫入是透過事件日誌進行的,而讀取是臨時的網路請求,直接流向儲存著待查資料的節點。這是一個合理的設計,但不是唯一可行的設計。也可以將讀取請求表示為事件流,並同時將讀事件與寫事件送往流處理器;流處理器透過將讀取結果傳送到輸出流來響應讀取事件【46】。

當寫入和讀取都被表示為事件,並且被路由到同一個流運算元以便處理時,我們實際上是在讀取查詢流和資料庫之間執行流表連線。讀取事件需要被送往儲存資料的資料庫分割槽(請參閱 “請求路由”),就像批處理和流處理器在連線時需要在同一個鍵上對輸入分割槽一樣(請參閱 “Reduce 側連線與分組“)。

服務請求與執行連線之間的這種相似之處是非常關鍵的【47】。一次性讀取請求只是將請求傳過連線運算元,然後請求馬上就被忘掉了;而一個訂閱請求,則是與連線另一側過去與未來事件的持久化連線。

記錄讀取事件的日誌可能對於追蹤整個系統中的因果關係與資料來源也有好處:它可以讓你重現出當用戶做出特定決策之前看見了什麼。例如在網商中,向客戶顯示的預測送達日期與庫存狀態,可能會影響他們是否選擇購買一件商品【4】。要分析這種聯絡,則需要記錄使用者查詢運輸與庫存狀態的結果。

將讀取事件寫入持久儲存可以更好地跟蹤因果關係(請參閱 “排序事件以捕獲因果關係”),但會產生額外的儲存與 I/O 成本。最佳化這些系統以減少開銷仍然是一個開放的研究問題【2】。但如果你已經出於運維目的留下了讀取請求日誌,將其作為請求處理的副作用,那麼將這份日誌作為請求事件源並不是什麼特別大的變更。

多分割槽資料處理

對於只涉及單個分割槽的查詢,透過流來發送查詢與收集響應可能是殺雞用牛刀了。然而,這個想法開啟了分散式執行複雜查詢的可能性,這需要合併來自多個分割槽的資料,利用了流處理器已經提供的訊息路由、分割槽和連線的基礎設施。

Storm 的分散式 RPC 功能支援這種使用模式(請參閱 “訊息傳遞和 RPC”)。例如,它已經被用來計算瀏覽過某個推特 URL 的人數 —— 即,發推包含該 URL 的所有人的粉絲集合的並集【48】。由於推特的使用者是分割槽的,因此這種計算需要合併來自多個分割槽的結果。

這種模式的另一個例子是欺詐預防:為了評估特定購買事件是否具有欺詐風險,你可以檢查該使用者 IP 地址,電子郵件地址,帳單地址,送貨地址的信用分。這些信用資料庫中的每一個都是有分割槽的,因此為特定購買事件採集分數需要連線一系列不同的分割槽資料集【49】。

MPP 資料庫的內部查詢執行圖有著類似的特徵(請參閱 “Hadoop 與分散式資料庫的對比”)。如果需要執行這種多分割槽連線,則直接使用提供此功能的資料庫,可能要比使用流處理器實現它要更簡單。然而將查詢視為流提供了一種選項,可以用於實現超出傳統現成解決方案的大規模應用。

將事情做正確

對於只讀取資料的無狀態服務,出問題也沒什麼大不了的:你可以修復該錯誤並重啟服務,而一切都恢復正常。像資料庫這樣的有狀態系統就沒那麼簡單了:它們被設計為永遠記住事物(或多或少),所以如果出現問題,這種(錯誤的)效果也將潛在地永遠持續下去,這意味著它們需要更仔細的思考【50】。

我們希望構建可靠且 正確 的應用(即使面對各種故障,程式的語義也能被很好地定義與理解)。約四十年來,原子性、隔離性和永續性(第七章)等事務特性一直是構建正確應用的首選工具。然而這些地基沒有看上去那麼牢固:例如弱隔離級別帶來的困惑可以佐證(請參閱 “弱隔離級別”)。

事務在某些領域被完全拋棄,並被提供更好效能與可伸縮性的模型取代,但後者有更複雜的語義(例如,請參閱 “無主複製”)。一致性(Consistency) 經常被談起,但其定義並不明確(請參閱 “一致性” 和 第九章)。有些人斷言我們應當為了高可用而 “擁抱弱一致性”,但卻對這些概念實際上意味著什麼缺乏清晰的認識。

對於如此重要的話題,我們的理解,以及我們的工程方法卻是驚人地薄弱。例如,確定在特定事務隔離等級或複製配置下執行特定應用是否安全是非常困難的【51,52】。通常簡單的解決方案似乎在低併發性的情況下工作正常,並且沒有錯誤,但在要求更高的情況下卻會出現許多微妙的錯誤。

例如,Kyle Kingsbury 的 Jepsen 實驗【53】標出了一些產品聲稱的安全保證與其在網路問題與崩潰時的實際行為之間的明顯差異。即使像資料庫這樣的基礎設施產品沒有問題,應用程式碼仍然需要正確使用它們提供的功能才行,如果配置很難理解,這是很容易出錯的(在這種情況下指的是弱隔離級別,法定人數配置等)。

如果你的應用可以容忍偶爾的崩潰,以及以不可預料的方式損壞或丟失資料,那生活就要簡單得多,而你可能只要雙手合十念阿彌陀佛,期望佛祖能保佑最好的結果。另一方面,如果你需要更強的正確性保證,那麼可序列化與原子提交就是久經考驗的方法,但它們是有代價的:它們通常只在單個數據中心中工作(這就排除了地理位置分散的架構),並限制了系統能夠實現的規模與容錯特性。

雖然傳統的事務方法並沒有走遠,但我也相信在使應用正確而靈活地處理錯誤方面上,事務也不是最後一個可以談的。在本節中,我將提出一些在資料流架構中考量正確性的方式。

資料庫的端到端原則

僅僅因為一個應用程式使用了具有相對較強安全屬性的資料系統(例如可序列化的事務),並不意味著就可以保證沒有資料丟失或損壞。例如,如果某個應用有個 Bug,導致它寫入不正確的資料,或者從資料庫中刪除資料,那麼可序列化的事務也救不了你。

這個例子可能看起來很無聊,但值得認真對待:應用會出 Bug,而人也會犯錯誤。我在 “狀態、流和不變性” 中使用了這個例子來支援不可變和僅追加的資料,閹割掉錯誤程式碼摧毀良好資料的能力,能讓從錯誤中恢復更為容易。

雖然不變性很有用,但它本身並非萬靈藥。讓我們來看一個可能發生的、非常微妙的資料損壞案例。

正好執行一次操作

在 “容錯” 中,我們見到了 恰好一次(或 等效一次)語義的概念。如果在處理訊息時出現問題,你可以選擇放棄(丟棄訊息 —— 導致資料丟失)或重試。如果重試,就會有這種風險:第一次實際上成功了,只不過你沒有發現。結果這個訊息就被處理了兩次。

處理兩次是資料損壞的一種形式:為同樣的服務向客戶收費兩次(收費太多)或增長計數器兩次(誇大指標)都不是我們想要的。在這種情況下,恰好一次意味著安排計算,使得最終效果與沒有發生錯誤的情況一樣,即使操作實際上因為某種錯誤而重試。我們先前討論過實現這一目標的幾種方法。

最有效的方法之一是使操作 冪等(idempotent,請參閱 “冪等性”);即確保它無論是執行一次還是執行多次都具有相同的效果。但是,將不是天生冪等的操作變為冪等的操作需要一些額外的努力與關注:你可能需要維護一些額外的元資料(例如更新了值的操作 ID 集合),並在從一個節點故障切換至另一個節點時做好防護(請參閱 “領導者和鎖”)。

抑制重複

除了流處理之外,其他許多地方也需要抑制重複的模式。例如,TCP 使用了資料包上的序列號,以便接收方可以將它們正確排序,並確定網路上是否有資料包丟失或重複。在將資料交付應用前,TCP 協議棧會重新傳輸任何丟失的資料包,也會移除任何重複的資料包。

但是,這種重複抑制僅適用於單條 TCP 連線的場景中。假設 TCP 連線是一個客戶端與資料庫的連線,並且它正在執行 例 12-1 中的事務。在許多資料庫中,事務是繫結在客戶端連線上的(如果客戶端傳送了多個查詢,資料庫就知道它們屬於同一個事務,因為它們是在同一個 TCP 連線上傳送的)。如果客戶端在傳送 COMMIT 之後並在從資料庫伺服器收到響應之前遇到網路中斷與連線超時,客戶端是不知道事務是否已經被提交的(圖 8-1)。

例 12-1 資金從一個賬戶到另一個賬戶的非冪等轉移

sql
BEGIN TRANSACTION;
    UPDATE accounts SET balance = balance + 11.00 WHERE account_id = 1234;
    UPDATE accounts SET balance = balance - 11.00 WHERE account_id = 4321;
COMMIT;

客戶端可以重連到資料庫並重試事務,但現在已經處於 TCP 重複抑制的範圍之外了。因為 例 12-1 中的事務不是冪等的,可能會發生轉了 $22 而不是期望的 $11。因此,儘管 例 12-1 是一個事務原子性的標準樣例,但它實際上並不正確,而真正的銀行並不會這樣辦事【3】。

兩階段提交(請參閱 “原子提交與兩階段提交”)協議會破壞 TCP 連線與事務之間的 1:1 對映,因為它們必須在故障後允許事務協調器重連到資料庫,告訴資料庫將存疑事務提交還是中止。這足以確保事務只被恰好執行一次嗎?不幸的是,並不能。

即使我們可以抑制資料庫客戶端與伺服器之間的重複事務,我們仍然需要擔心終端使用者裝置與應用伺服器之間的網路。例如,如果終端使用者的客戶端是 Web 瀏覽器,則它可能會使用 HTTP POST 請求向伺服器提交指令。也許使用者正處於一個訊號微弱的蜂窩資料網路連線中,它們成功地傳送了 POST,但卻在能夠從伺服器接收響應之前沒了訊號。

在這種情況下,可能會向用戶顯示錯誤訊息,而他們可能會手動重試。 Web 瀏覽器警告說,“你確定要再次提交這個表單嗎?” —— 使用者選 “是”,因為他們希望操作發生(Post/Redirect/Get 模式【54】可以避免在正常操作中出現此警告訊息,但 POST 請求超時就沒辦法了)。從 Web 伺服器的角度來看,重試是一個獨立的請求;從資料庫的角度來看,這是一個獨立的事務。通常的除重機制無濟於事。

操作識別符號

要在通過幾跳的網路通訊上使操作具有冪等性,僅僅依賴資料庫提供的事務機制是不夠的 —— 你需要考慮 端到端(end-to-end) 的請求流。 例如,你可以為操作生成一個唯一的識別符號(例如 UUID),並將其作為隱藏表單欄位包含在客戶端應用中,或透過計算所有表單相關欄位的雜湊來生成操作 ID 【3】。如果 Web 瀏覽器提交了兩次 POST 請求,這兩個請求將具有相同的操作 ID。然後,你可以將該操作 ID 一路傳遞到資料庫,並檢查你是否曾經使用給定的 ID 執行過一個操作,如 例 12-2 中所示。

例 12-2 使用唯一 ID 來抑制重複請求

sql
ALTER TABLE requests ADD UNIQUE (request_id);

BEGIN TRANSACTION;
    INSERT INTO requests
        (request_id, from_account, to_account, amount)
        VALUES('0286FDB8-D7E1-423F-B40B-792B3608036C', 4321, 1234, 11.00);
    UPDATE accounts SET balance = balance + 11.00 WHERE account_id = 1234;
    UPDATE accounts SET balance = balance - 11.00 WHERE account_id = 4321;
COMMIT;

例 12-2 依賴於 request_id 列上的唯一約束。如果一個事務嘗試插入一個已經存在的 ID,那麼 INSERT 失敗,事務被中止,使其無法生效兩次。即使在較弱的隔離級別下,關係資料庫也能正確地維護唯一性約束(而在 “寫入偏差與幻讀” 中討論過,應用級別的 檢查 - 然後 - 插入 可能會在不可序列化的隔離下失敗)。

除了抑制重複的請求之外,例 12-2 中的請求表表現得就像一種事件日誌,暗示著事件溯源的想法(請參閱 “事件溯源”)。更新賬戶餘額事實上不必與插入事件發生在同一個事務中,因為它們是冗餘的,而能由下游消費者從請求事件中衍生出來 —— 只要該事件被恰好處理一次,這又一次可以使用請求 ID 來強制執行。

端到端原則

抑制重複事務的這種情況只是一個更普遍的原則的一個例子,這個原則被稱為 端到端原則(end-to-end argument),它在 1984 年由 Saltzer、Reed 和 Clark 闡述【55】:

只有在通訊系統兩端應用的知識與幫助下,所討論的功能才能完全地正確地實現。因而將這種被質疑的功能作為通訊系統本身的功能是不可能的(有時,通訊系統可以提供這種功能的不完備版本,可能有助於提高效能)。

在我們的例子中 所討論的功能 是重複抑制。我們看到 TCP 在 TCP 連線層次抑制了重複的資料包,一些流處理器在訊息處理層次提供了所謂的恰好一次語義,但這些都無法阻止當一個請求超時時,使用者親自提交重複的請求。TCP,資料庫事務,以及流處理器本身並不能完全排除這些重複。解決這個問題需要一個端到端的解決方案:從終端使用者的客戶端一路傳遞到資料庫的事務識別符號。

端到端原則也適用於檢查資料的完整性:乙太網,TCP 和 TLS 中內建的校驗和可以檢測網路中資料包的損壞情況,但是它們無法檢測到由連線兩端傳送 / 接收軟體中 Bug 導致的損壞。或資料儲存所在磁碟上的損壞。如果你想捕獲資料所有可能的損壞來源,你也需要端到端的校驗和。

類似的原則也適用於加密【55】:家庭 WiFi 網路上的密碼可以防止人們竊聽你的 WiFi 流量,但無法阻止網際網路上其他地方攻擊者的窺探;客戶端與伺服器之間的 TLS/SSL 可以阻擋網路攻擊者,但無法阻止惡意伺服器。只有端到端的加密和認證可以防止所有這些事情。

儘管低層級的功能(TCP 重複抑制、乙太網校驗和、WiFi 加密)無法單獨提供所需的端到端功能,但它們仍然很有用,因為它們能降低較高層級出現問題的可能性。例如,如果我們沒有 TCP 來將資料包排成正確的順序,那麼 HTTP 請求通常就會被攪爛。我們只需要記住,低級別的可靠性功能本身並不足以確保端到端的正確性。

在資料系統中應用端到端思考

這將我帶回最初的論點:僅僅因為應用使用了提供相對較強安全屬性的資料系統,例如可序列化的事務,並不意味著應用的資料就不會丟失或損壞了。應用本身也需要採取端到端的措施,例如除重。

這實在是一個遺憾,因為容錯機制很難弄好。低層級的可靠機制(比如 TCP 中的那些)執行的相當好,因而剩下的高層級錯誤基本很少出現。如果能將這些剩下的高層級容錯機制打包成抽象,而應用不需要再去操心,那該多好呀 —— 但恐怕我們還沒有找到這一正確的抽象。

長期以來,事務被認為是一個很好的抽象,我相信它們確實是很有用的。正如 第七章 導言中所討論的,它們將各種可能的問題(併發寫入、違背約束、崩潰、網路中斷、磁碟故障)合併為兩種可能結果:提交或中止。這是對程式設計模型而言是一種巨大的簡化,但恐怕這還不夠。

事務是代價高昂的,當涉及異構儲存技術時尤為甚(請參閱 “實踐中的分散式事務”)。我們拒絕使用分散式事務是因為它開銷太大,結果我們最後不得不在應用程式碼中重新實現容錯機制。正如本書中大量的例子所示,對併發性與部分失敗的推理是困難且違反直覺的,所以我懷疑大多數應用級別的機制都不能正確工作,最終結果是資料丟失或損壞。

出於這些原因,我認為探索對容錯的抽象是很有價值的。它使提供應用特定的端到端的正確性屬性變得更簡單,而且還能在大規模分散式環境中提供良好的效能與運維特性。

強制約束

讓我們思考一下在 分拆資料庫 上下文中的 正確性(correctness)。我們看到端到端的除重可以透過從客戶端一路透傳到資料庫的請求 ID 實現。那麼其他型別的約束呢?

我們先來特別關注一下 唯一性約束 —— 例如我們在 例 12-2 中所依賴的約束。在 “約束和唯一性保證” 中,我們看到了幾個其他需要強制實施唯一性的應用功能例子:使用者名稱或電子郵件地址必須唯一標識使用者,檔案儲存服務不能包含多個重名檔案,兩個人不能在航班或劇院預訂同一個座位。

其他型別的約束也非常類似:例如,確保帳戶餘額永遠不會變為負數,確保不會超賣庫存,或者會議室沒有重複的預訂。執行唯一性約束的技術通常也可以用於這些約束。

唯一性約束需要達成共識

第九章 中我們看到,在分散式環境中,強制執行唯一性約束需要共識:如果存在多個具有相同值的併發請求,則系統需要決定衝突操作中的哪一個被接受,並拒絕其他違背約束的操作。

達成這一共識的最常見方式是使單個節點作為領導,並使其負責所有決策。只要你不介意所有請求都擠過單個節點(即使客戶端位於世界的另一端),只要該節點沒有失效,系統就能正常工作。如果你需要容忍領導者失效,那麼就又回到了共識問題(請參閱 “單主複製與共識”)。

唯一性檢查可以透過對唯一性欄位分割槽做橫向伸縮。例如,如果需要透過請求 ID 確保唯一性(如 例 12-2 所示),你可以確保所有具有相同請求 ID 的請求都被路由到同一分割槽(請參閱 第六章)。如果你需要讓使用者名稱是唯一的,則可以按使用者名稱的雜湊值做分割槽。

但非同步多主複製排除在外,因為可能會發生不同主庫同時接受衝突寫操作的情況,因而這些值不再是唯一的(請參閱 “實現線性一致的系統”)。如果你想立刻拒絕任何違背約束的寫入,同步協調是無法避免的【56】。

基於日誌訊息傳遞中的唯一性

日誌確保所有消費者以相同的順序看見訊息 —— 這種保證在形式上被稱為 全序廣播(total order boardcast) 並且等價於共識(請參閱 “全序廣播”)。在使用基於日誌的訊息傳遞的分拆資料庫方法中,我們可以使用非常類似的方法來執行唯一性約束。

流處理器在單個執行緒上依次消費單個日誌分割槽中的所有訊息(請參閱 “日誌與傳統的訊息傳遞相比”)。因此,如果日誌是按需要確保唯一的值做的分割槽,則流處理器可以無歧義地、確定性地決定幾個衝突操作中的哪一個先到達。例如,在多個使用者嘗試宣告相同使用者名稱的情況下【57】:

  1. 每個對使用者名稱的請求都被編碼為一條訊息,並追加到按使用者名稱雜湊值確定的分割槽。
  2. 流處理器依序讀取日誌中的請求,並使用本地資料庫來追蹤哪些使用者名稱已經被佔用了。對於所有申請可用使用者名稱的請求,它都會記錄該使用者名稱,並向輸出流傳送一條成功訊息。對於所有申請已佔用使用者名稱的請求,它都會向輸出流傳送一條拒絕訊息。
  3. 請求使用者名稱的客戶端監視輸出流,等待與其請求相對應的成功或拒絕訊息。

該演算法基本上與 “使用全序廣播實現線性一致的儲存” 中的演算法相同。它可以簡單地透過增加分割槽數伸縮至較大的請求吞吐量,因為每個分割槽都可以被獨立處理。

該方法不僅適用於唯一性約束,而且適用於許多其他型別的約束。其基本原理是,任何可能衝突的寫入都會路由到相同的分割槽並按順序處理。正如 “什麼是衝突?” 與 “寫入偏差與幻讀” 中所述,衝突的定義可能取決於應用,但流處理器可以使用任意邏輯來驗證請求。這個想法與 Bayou 在 90 年代開創的方法類似【58】。

多分割槽請求處理

當涉及多個分割槽時,確保操作以原子方式執行且同時滿足約束就變得很有趣了。在 例 12-2 中,可能有三個分割槽:一個包含請求 ID,一個包含收款人賬戶,另一個包含付款人賬戶。沒有理由把這三種東西放入同一個分割槽,因為它們都是相互獨立的。

在資料庫的傳統方法中,執行此事務需要跨全部三個分割槽進行原子提交,就這些分割槽上的所有其他事務而言,這實質上是將該事務嵌入一個全序。而這樣就要求跨分割槽協調,不同的分割槽無法再獨立地進行處理,因此吞吐量很可能會受到影響。

但事實證明,使用分割槽日誌可以達到等價的正確性而無需原子提交:

  1. 從賬戶 A 向賬戶 B 轉賬的請求由客戶端提供一個唯一的請求 ID,並按請求 ID 追加寫入相應日誌分割槽。
  2. 流處理器讀取請求日誌。對於每個請求訊息,它向輸出流發出兩條訊息:付款人賬戶 A 的借記指令(按 A 分割槽),收款人 B 的貸記指令(按 B 分割槽)。被發出的訊息中會帶有原始的請求 ID。
  3. 後續處理器消費借記 / 貸記指令流,按照請求 ID 除重,並將變更應用至賬戶餘額。

步驟 1 和步驟 2 是必要的,因為如果客戶直接傳送貸記與借記指令,則需要在這兩個分割槽之間進行原子提交,以確保兩者要麼都發生或都不發生。為了避免對分散式事務的需要,我們首先將請求持久化記錄為單條訊息,然後從這第一條訊息中衍生出貸記指令與借記指令。幾乎在所有資料系統中,單物件寫入都是原子性的(請參閱 “單物件寫入),因此請求要麼出現在日誌中,要麼就不出現,無需多分割槽原子提交。

如果流處理器在步驟 2 中崩潰,則它會從上一個存檔點恢復處理。這樣做時,它不會跳過任何請求訊息,但可能會多次處理請求併產生重複的貸記與借記指令。但由於它是確定性的,因此它只是再次生成相同的指令,而步驟 3 中的處理器可以使用端到端請求 ID 輕鬆地對其除重。

如果你想確保付款人的帳戶不會因此次轉賬而透支,則可以使用一個額外的流處理器來維護賬戶餘額並校驗事務(按付款人賬戶分割槽),只有有效的事務會被記錄在步驟 1 中的請求日誌中。

透過將多分割槽事務分解為兩個不同分割槽方式的階段,並使用端到端的請求 ID,我們實現了同樣的正確性屬性(每個請求對付款人與收款人都恰好生效一次),即使在出現故障,且沒有使用原子提交協議的情況下依然如此。使用多個不同分割槽階段的想法與我們在 “多分割槽資料處理” 中討論的想法類似(也請參閱 “併發控制”)。

及時性與完整性

事務的一個便利屬性是,它們通常是線性一致的(請參閱 “線性一致性”),也就是說,寫入者會等到事務提交,而之後其寫入立刻對所有讀取者可見。

當我們把一個操作拆分為跨越多個階段的流處理器時,卻並非如此:日誌的消費者在設計上就是非同步的,因此傳送者不會等其訊息被消費者處理完。但是,客戶端等待輸出流中的特定訊息是可能的。這正是我們在 “基於日誌訊息傳遞中的唯一性” 一節中檢查唯一性約束時所做的事情。

在這個例子中,唯一性檢查的正確性不取決於訊息傳送者是否等待結果。等待的目的僅僅是同步通知傳送者唯一性檢查是否成功。但該通知可以與訊息處理的結果相解耦。

更一般地來講,我認為術語 一致性(consistency) 這個術語混淆了兩個值得分別考慮的需求:

  • 及時性(Timeliness)

    及時性意味著確保使用者觀察到系統的最新狀態。我們之前看到,如果使用者從陳舊的資料副本中讀取資料,它們可能會觀察到系統處於不一致的狀態(請參閱 “複製延遲問題”)。但這種不一致是暫時的,而最終會透過等待與重試簡單地得到解決。

    CAP 定理(請參閱 “線性一致性的代價”)使用 線性一致性(linearizability) 意義上的一致性,這是實現及時性的強有力方法。像 寫後讀 這樣及時性更弱的一致性也很有用(請參閱 “讀己之寫”)。

  • 完整性(Integrity)

    完整性意味著沒有損壞;即沒有資料丟失,並且沒有矛盾或錯誤的資料。尤其是如果某些衍生資料集是作為底層資料之上的檢視而維護的(請參閱 “從事件日誌中派生出當前狀態”),這種衍生必須是正確的。例如,資料庫索引必須正確地反映資料庫的內容 —— 缺失某些記錄的索引並不是很有用。

    如果完整性被違背,這種不一致是永久的:在大多數情況下,等待與重試並不能修復資料庫損壞。相反的是,需要顯式地檢查與修復。在 ACID 事務的上下文中(請參閱 “ACID 的含義”),一致性通常被理解為某種特定於應用的完整性概念。原子性和永續性是保持完整性的重要工具。

口號形式:違反及時性,“最終一致性”;違反完整性,“永無一致性”。

我斷言在大多數應用中,完整性比及時性重要得多。違反及時性可能令人困惑與討厭,但違反完整性的結果可能是災難性的。

例如在你的信用卡對賬單上,如果某一筆過去 24 小時內完成的交易尚未出現並不令人奇怪 —— 這些系統有一定的滯後是正常的。我們知道銀行是非同步核算與敲定交易的,這裡的及時性並不是非常重要【3】。但果當期對賬單餘額與上期對賬單餘額加交易總額對不上(求和錯誤),或者出現一筆向你收費但未向商家付款的交易(消失的錢),那實在是太糟糕了。這樣的問題就違背了系統的完整性。

資料流系統的正確性

ACID 事務通常既提供及時性(例如線性一致性)也提供完整性保證(例如原子提交)。因此如果你從 ACID 事務的角度來看待應用的正確性,那麼及時性與完整性的區別是無關緊要的。

另一方面,對於在本章中討論的基於事件的資料流系統而言,它們的一個有趣特性就是將及時性與完整性分開。在非同步處理事件流時不能保證及時性,除非你顯式構建一個在返回之前明確等待特定訊息到達的消費者。但完整性實際上才是流處理系統的核心。

恰好一次等效一次 語義(請參閱 “容錯”)是一種保持完整性的機制。如果事件丟失或者生效兩次,就有可能違背資料系統的完整性。因此在出現故障時,容錯訊息傳遞與重複抑制(例如,冪等操作)對於維護資料系統的完整性是很重要的。

正如我們在上一節看到的那樣,可靠的流處理系統可以在無需分散式事務與原子提交協議的情況下保持完整性,這意味著它們有潛力達到與後者相當的正確性,同時還具備好得多的效能與運維穩健性。為了達成這種正確性,我們組合使用了多種機制:

  • 將寫入操作的內容表示為單條訊息,從而可以輕鬆地被原子寫入 —— 與事件溯源搭配效果拔群(請參閱 “事件溯源”)。
  • 使用與儲存過程類似的確定性衍生函式,從這一訊息中衍生出所有其他的狀態變更(請參閱 “真的序列執行” 和 “應用程式碼作為衍生函式”)
  • 將客戶端生成的請求 ID 傳遞透過所有的處理層次,從而允許端到端的除重,帶來冪等性。
  • 使訊息不可變,並允許衍生資料能隨時被重新處理,這使從錯誤中恢復更加容易(請參閱 “不可變事件的優點”)

這種機制組合在我看來,是未來構建容錯應用的一個非常有前景的方向。

寬鬆地解釋約束

如前所述,執行唯一性約束需要共識,通常透過在單個節點中彙集特定分割槽中的所有事件來實現。如果我們想要傳統的唯一性約束形式,這種限制是不可避免的,流處理也不例外。

然而另一個需要了解的事實是,許多真實世界的應用實際上可以擺脫這種形式,接受弱得多的唯一性:

  • 如果兩個人同時註冊了相同的使用者名稱或預訂了相同的座位,你可以給其中一個人發訊息道歉,並要求他們換一個不同的使用者名稱或座位。這種糾正錯誤的變化被稱為 補償性事務(compensating transaction)【59,60】。
  • 如果客戶訂購的物品多於倉庫中的物品,你可以下單補倉,併為延誤向客戶道歉,向他們提供折扣。實際上,這麼說吧,如果叉車在倉庫中軋過了你的貨物,剩下的貨物比你想象的要少,那麼你也是得這麼做【61】。因此,既然道歉工作流無論如何已經成為你商業過程中的一部分了,那麼對庫存物品數目新增線性一致的約束可能就沒必要了。
  • 與之類似,許多航空公司都會超賣機票,打著一些旅客可能會錯過航班的算盤;許多旅館也會超賣客房,抱著部分客人可能會取消預訂的期望。在這些情況下,出於商業原因而故意違反了 “一人一座” 的約束;當需求超過供給的情況出現時,就會進入補償流程(退款、升級艙位 / 房型、提供隔壁酒店的免費的房間)。即使沒有超賣,為了應對由惡劣天氣或員工罷工導致的航班取消,你還是需要道歉與補償流程 —— 從這些問題中恢復僅僅是商業活動的正常組成部分。
  • 如果有人從賬戶超額取款,銀行可以向他們收取透支費用,並要求他們償還欠款。透過限制每天的提款總額,銀行的風險是有限的。

在許多商業場景中,臨時違背約束並稍後透過道歉來修復,實際上是可以接受的。道歉的成本各不相同,但通常很低(以金錢或名聲來算):你無法撤回已傳送的電子郵件,但可以傳送一封后續電子郵件進行更正。如果你不小心向信用卡收取了兩次費用,則可以將其中一項收費退款,而代價僅僅是手續費,也許還有客戶的投訴。儘管一旦 ATM 吐了錢,你無法直接取回,但原則上如果賬戶透支而客戶拒不支付,你可以派催收員收回欠款。

道歉的成本是否能接受是一個商業決策。如果可以接受的話,在寫入資料之前檢查所有約束的傳統模型反而會帶來不必要的限制,而線性一致性的約束也不是必須的。樂觀寫入,事後檢查可能是一種合理的選擇。你仍然可以在做一些挽回成本高昂的事情前確保有相關的驗證,但這並不意味著寫入資料之前必須先進行驗證。

這些應用 確實 需要完整性:你不會希望丟失預訂資訊,或者由於借方貸方不匹配導致資金消失。但是它們在執行約束時 並不需要 及時性:如果你銷售的貨物多於倉庫中的庫存,可以在事後道歉後並彌補問題。這種做法與我們在 “處理寫入衝突” 中討論的衝突解決方法類似。

無協調資料系統

我們現在已經做了兩個有趣的觀察:

  1. 資料流系統可以維持衍生資料的完整性保證,而無需原子提交、線性一致性或者同步的跨分割槽協調。
  2. 雖然嚴格的唯一性約束要求及時性和協調,但許多應用實際上可以接受寬鬆的約束:只要整個過程保持完整性,這些約束可能會被臨時違反並在稍後被修復。

總之這些觀察意味著,資料流系統可以為許多應用提供無需協調的資料管理服務,且仍能給出很強的完整性保證。這種 無協調(coordination-avoiding) 的資料系統有著很大的吸引力:比起需要執行同步協調的系統,它們能達到更好的效能與更強的容錯能力【56】。

例如,這種系統可以使用多領導者配置運維,跨越多個數據中心,在區域間非同步複製。任何一個數據中心都可以持續獨立執行,因為不需要同步的跨區域協調。這樣的系統的及時性保證會很弱 —— 如果不引入協調它是不可能是線性一致的 —— 但它仍然可以提供有力的完整性保證。

在這種情況下,可序列化事務作為維護衍生狀態的一部分仍然是有用的,但它們只能在小範圍內執行,在那裡它們工作得很好【8】。異構分散式事務(如 XA 事務,請參閱 “實踐中的分散式事務”)不是必需的。同步協調仍然可以在需要的地方引入(例如在無法恢復的操作之前強制執行嚴格的約束),但是如果只是應用的一小部分地方需要它,沒必要讓所有操作都付出協調的代價。【43】。

另一種審視協調與約束的角度是:它們減少了由於不一致而必須做出的道歉數量,但也可能會降低系統的效能和可用性,從而可能增加由於宕機中斷而需要做出的道歉數量。你不可能將道歉數量減少到零,但可以根據自己的需求尋找最佳平衡點 —— 既不存在太多不一致性,又不存在太多可用性問題。

信任但驗證

我們所有關於正確性,完整性和容錯的討論都基於一些假設,假設某些事情可能會出錯,但其他事情不會。我們將這些假設稱為我們的 系統模型(system model,請參閱 “將系統模型對映到現實世界”):例如,我們應該假設程序可能會崩潰,機器可能突然斷電,網路可能會任意延遲或丟棄訊息。但是我們也可能假設寫入磁碟的資料在執行 fsync 後不會丟失,記憶體中的資料沒有損壞,而 CPU 的乘法指令總是能返回正確的結果。

這些假設是相當合理的,因為大多數時候它們都是成立的,如果我們不得不經常擔心計算機出錯,那麼基本上寸步難行。在傳統上,系統模型採用二元方法處理故障:我們假設有些事情可能會發生,而其他事情 永遠 不會發生。實際上,這更像是一個概率問題:有些事情更有可能,其他事情不太可能。問題在於違反我們假設的情況是否經常發生,以至於我們可能在實踐中遇到它們。

我們已經看到,資料可能會在尚未落盤時損壞(請參閱 “複製與永續性”),而網路上的資料損壞有時可能規避了 TCP 校驗和(請參閱 “弱謊言形式” )。也許我們應當更關注這些事情?

我過去所從事的一個應用收集了來自客戶端的崩潰報告,我們收到的一些報告,只有在這些裝置記憶體中出現了隨機位翻轉才解釋的通。這看起來不太可能,但是如果有足夠多的裝置執行你的軟體,那麼即使再不可能發生的事也確實會發生。除了由於硬體故障或輻射導致的隨機儲存器損壞之外,一些病態的儲存器訪問模式甚至可以在沒有故障的儲存器中翻轉位【62】 —— 一種可用於破壞作業系統安全機制的效應【63】(這種技術被稱為 Rowhammer)。一旦你仔細觀察,硬體並不是看上去那樣完美的抽象。

要澄清的是,隨機位翻轉在現代硬體上仍是非常罕見的【64】。我只想指出,它們並沒有超越可能性的範疇,所以值得一些關注。

維護完整性,儘管軟體有Bug

除了這些硬體問題之外,總是存在軟體 Bug 的風險,這些錯誤不會被較低層次的網路、記憶體或檔案系統校驗和所捕獲。即使廣泛使用的資料庫軟體也有 Bug:即使像 MySQL 與 PostgreSQL 這樣穩健、口碑良好、多年來被許多人充分測試過的軟體,就我個人所見也有 Bug,比如 MySQL 未能正確維護唯一約束【65】,以及 PostgreSQL 的可序列化隔離等級存在特定的寫入偏差異常【66】。對於不那麼成熟的軟體來說,情況可能要糟糕得多。

儘管在仔細設計,測試,以及審查上做出很多努力,但 Bug 仍然會在不知不覺中產生。儘管它們很少,而且最終會被發現並被修復,但總會有那麼一段時間,這些 Bug 可能會損壞資料。

而對於應用程式碼,我們不得不假設會有更多的錯誤,因為絕大多數應用的程式碼經受的評審與測試遠遠無法與資料庫的程式碼相比。許多應用甚至沒有正確使用資料庫提供的用於維持完整性的功能,例如外來鍵或唯一性約束【36】。

ACID 意義下的一致性(請參閱 “一致性”)基於這樣一種想法:資料庫以一致的狀態啟動,而事務將其從一個一致狀態轉換至另一個一致的狀態。因此,我們期望資料庫始終處於一致狀態。然而,只有當你假設事務沒有 Bug 時,這種想法才有意義。如果應用以某種錯誤的方式使用資料庫,例如,不安全地使用弱隔離等級,資料庫的完整性就無法得到保證。

不要盲目信任承諾

由於硬體和軟體並不總是符合我們的理想,所以資料損壞似乎早晚不可避免。因此,我們至少應該有辦法查明資料是否已經損壞,以便我們能夠修復它,並嘗試追查錯誤的來源。檢查資料完整性稱為 審計(auditing)

如 “不可變事件的優點” 一節中所述,審計不僅僅適用於財務應用程式。不過,可審計性在財務中是非常非常重要的,因為每個人都知道錯誤總會發生,我們也都認為能夠檢測和解決問題是合理的需求。

成熟的系統同樣傾向於考慮不太可能的事情出錯的可能性,並管理這種風險。例如,HDFS 和 Amazon S3 等大規模儲存系統並不完全信任磁碟:它們執行後臺程序持續回讀檔案,並將其與其他副本進行比較,並將檔案從一個磁碟移動到另一個,以便降低靜默損壞的風險【67】。

如果你想確保你的資料仍然存在,你必須真正讀取它並進行檢查。大多數時候它們仍然會在那裡,但如果不是這樣,你一定想盡早知道答案,而不是更晚。按照同樣的原則,不時地嘗試從備份中恢復是非常重要的 —— 否則當你發現備份損壞時,你可能已經遇到了資料丟失,那時候就真的太晚了。不要盲目地相信它們全都管用。

驗證的文化

像 HDFS 和 S3 這樣的系統仍然需要假設磁碟大部分時間都能正常工作 —— 這是一個合理的假設,但與它們 始終 能正常工作的假設並不相同。然而目前還沒有多少系統採用這種 “信任但是驗證” 的方式來持續審計自己。許多人認為正確性保證是絕對的,並且沒有為罕見的資料損壞的可能性做過準備。我希望未來能看到更多的 自我驗證(self-validating)自我審計(self-auditing) 系統,不斷檢查自己的完整性,而不是依賴盲目的信任【68】。

我擔心 ACID 資料庫的文化導致我們在盲目信任技術(如事務機制)的基礎上開發應用,而忽視了這種過程中的任何可審計性。由於我們所信任的技術在大多數情況下工作得很好,通常會認為審計機制並不值得投資。

但隨之而來的是,資料庫的格局發生了變化:在 NoSQL 的旗幟下,更弱的一致性保證成為常態,更不成熟的儲存技術越來越被廣泛使用。但是由於審計機制還沒有被開發出來,儘管這種方式越來越危險,我們仍不斷在盲目信任的基礎上構建應用。讓我們想一想如何針對可審計性而設計吧。

為可審計性而設計

如果一個事務在一個數據庫中改變了多個物件,在這一事實發生後,很難說清這個事務到底意味著什麼。即使你捕獲了事務日誌(請參閱 “變更資料捕獲”),各種表中的插入、更新和刪除操作並不一定能清楚地表明 為什麼 要執行這些變更。決定這些變更的是應用邏輯中的呼叫,而這一應用邏輯稍縱即逝,無法重現。

相比之下,基於事件的系統可以提供更好的可審計性。在事件溯源方法中,系統的使用者輸入被表示為一個單一不可變事件,而任何其導致的狀態變更都衍生自該事件。衍生可以實現為具有確定性與可重複性,因而相同的事件日誌透過相同版本的衍生程式碼時,會導致相同的狀態變更。

顯式處理資料流(請參閱 “批處理輸出的哲學”)可以使資料的 來龍去脈(provenance) 更加清晰,從而使完整性檢查更具可行性。對於事件日誌,我們可以使用雜湊來檢查事件儲存沒有被破壞。對於任何衍生狀態,我們可以重新執行從事件日誌中衍生它的批處理器與流處理器,以檢查是否獲得相同的結果,或者,甚至並行執行冗餘的衍生流程。

具有確定性且定義良好的資料流,也使除錯與跟蹤系統的執行變得容易,以便確定它 為什麼 做了某些事情【4,69】。如果出現意想之外的事情,那麼重現導致意外事件的確切事故現場的診斷能力 —— 一種時間旅行除錯功能是非常有價值的。

端到端原則重現

如果我們不能完全相信系統的每個元件都不會損壞 —— 每一個硬體都沒缺陷,每一個軟體都沒有 Bug —— 那我們至少必須定期檢查資料的完整性。如果我們不檢查,我們就不能發現損壞,直到無可挽回地導致對下游的破壞時,那時候再去追蹤問題就要難得多,且代價也要高的多。

檢查資料系統的完整性,最好是以端到端的方式進行(請參閱 “資料庫的端到端原則”):我們能在完整性檢查中涵蓋的系統越多,某些處理階中出現不被察覺損壞的機率就越小。如果我們能檢查整個衍生資料管道端到端的正確性,那麼沿著這一路徑的任何磁碟、網路、服務以及演算法的正確性檢查都隱含在其中了。

持續的端到端完整性檢查可以不斷提高你對系統正確性的信心,從而使你能更快地進步【70】。與自動化測試一樣,審計提高了快速發現錯誤的可能性,從而降低了系統變更或新儲存技術可能導致損失的風險。如果你不害怕進行變更,就可以更好地充分演化一個應用,使其滿足不斷變化的需求。

用於可審計資料系統的工具

目前,將可審計性作為頂層關注點的資料系統並不多。一些應用實現了自己的審計機制,例如將所有變更記錄到單獨的審計表中,但是確保審計日誌與資料庫狀態的完整性仍然是很困難的。可以定期使用硬體安全模組對事務日誌進行簽名來防止篡改,但這無法保證正確的事務一開始就能進入到日誌中。

使用密碼學工具來證明系統的完整性是十分有趣的,這種方式對於寬泛的硬體與軟體問題,甚至是潛在的惡意行為都很穩健有效。加密貨幣、區塊鏈、以及諸如比特幣、以太坊、Ripple、Stellar 的分散式賬本技術已經迅速出現在這一領域【71,72,73】。

我沒有資格評論這些技術用於貨幣,或者合同商定機制的價值。但從資料系統的角度來看,它們包含了一些有趣的想法。實質上,它們是分散式資料庫,具有資料模型與事務機制,而不同副本可以由互不信任的組織託管。副本不斷檢查其他副本的完整性,並使用共識協議對應當執行的事務達成一致。

我對這些技術的拜占庭容錯方面有些懷疑(請參閱 “拜占庭故障”),而且我發現 工作證明(proof of work) 技術非常浪費(比如,比特幣挖礦)。比特幣的交易吞吐量相當低,儘管更多是出於政治與經濟原因而非技術上的原因。不過,完整性檢查的方面是很有趣的。

密碼學審計與完整性檢查通常依賴 默克爾樹(Merkle tree)【74】,這是一顆雜湊值的樹,能夠用於高效地證明一條記錄出現在一個數據集中(以及其他一些特性)。除了炒作的沸沸揚揚的加密貨幣之外,證書透明性(certificate transparency) 也是一種依賴 Merkle 樹的安全技術,用來檢查 TLS/SSL 證書的有效性【75,76】。

我可以想象,那些在證書透明度與分散式賬本中使用的完整性檢查和審計演算法,將會在通用資料系統中得到越來越廣泛的應用。要使得這些演算法對於沒有密碼學審計的系統同樣可伸縮,並儘可能降低效能損失還需要一些工作。 但我認為這是一個值得關注的有趣領域。

做正確的事情

在本書的最後部分,我想退後一步。在本書中,我們考察了各種不同的資料系統架構,評價了它們的優點與缺點,並探討了構建可靠,可伸縮,可維護應用的技術。但是,我們忽略了討論中一個重要而基礎的部分,現在我想補充一下。

每個系統都服務於一個目的;我們採取的每個舉措都會同時產生期望的後果與意外的後果。這個目的可能只是簡單地賺錢,但其對世界的影響,可能會遠遠超出最初的目的。我們,建立這些系統的工程師,有責任去仔細考慮這些後果,並有意識地決定,我們希望生活在怎樣的世界中。

我們將資料當成一種抽象的東西來討論,但請記住,許多資料集都是關於人的:他們的行為,他們的興趣,他們的身份。對待這些資料,我們必須懷著人性與尊重。使用者也是人類,人類的尊嚴是至關重要的。

軟體開發越來越多地涉及重要的道德抉擇。有一些指導原則可以幫助軟體工程師解決這些問題,例如 ACM 的軟體工程道德規範與專業實踐【77】,但實踐中很少會討論這些,更不用說應用與強制執行了。因此,工程師和產品經理有時會對隱私與產品潛在的負面後果抱有非常傲慢的態度【78,79,80】。

技術本身並無好壞之分 —— 關鍵在於它被如何使用,以及它如何影響人們。這對槍械這樣的武器是成立的,而搜尋引擎這樣的軟體系統與之類似。我認為,軟體工程師僅僅專注於技術而忽視其後果是不夠的:道德責任也是我們的責任。對道德推理很困難,但它太重要了,我們無法忽視。

預測性分析

舉個例子,預測性分析是 “大資料” 炒作的主要內容之一。使用資料分析預測天氣或疾病傳播是一碼事【81】;而預測一個罪犯是否可能再犯,一個貸款申請人是否有可能違約,或者一個保險客戶是否可能進行昂貴的索賠,則是另外一碼事。後者會直接影響到個人的生活。

當然,支付網路希望防止欺詐交易,銀行希望避免不良貸款,航空公司希望避免劫機,公司希望避免僱傭效率低下或不值得信任的人。從它們的角度來看,失去商機的成本很低,而不良貸款或問題員工的成本則要高得多,因而組織希望保持謹慎也是自然而然的事情。所以如果存疑,它們通常會 Say No。

然而,隨著演算法決策變得越來越普遍,被某種演算法(準確地或錯誤地)標記為有風險的某人可能會遭受大量這種 “No” 的決定。系統性地被排除在工作,航旅,保險,租賃,金融服務,以及其他社會關鍵領域之外。這是一種對個體自由的極大約束,因此被稱為 “演算法監獄”【82】。在尊重人權的國家,刑事司法系統會做無罪推定(預設清白,直到被證明有罪)。另一方面,自動化系統可以系統地,任意地將一個人排除在社會參與之外,不需要任何有罪的證明,而且幾乎沒有申訴的機會。

偏見與歧視

演算法做出的決定不一定比人類更好或更差。每個人都可能有偏見,即使他們主動抗拒這一點;而歧視性做法也可能已經在文化上被制度化了。人們希望根據資料做出決定,而不是透過人的主觀評價與直覺,希望這樣能更加公平,並給予傳統體制中經常被忽視的人更好的機會【83】。

當我們開發預測性分析系統時,不是僅僅用軟體透過一系列 IF ELSE 規則將人類的決策過程自動化,那些規則本身甚至都是從資料中推斷出來的。但這些系統學到的模式是個黑盒:即使資料中存在一些相關性,我們可能也壓根不知道為什麼。如果演算法的輸入中存在系統性的偏見,則系統很有可能會在輸出中學習並放大這種偏見【84】。

在許多國家,反歧視法律禁止按種族、年齡、性別、性取向、殘疾或信仰等受保護的特徵區分對待不同的人。其他的個人特徵可能是允許用於分析的,但是如果這些特徵與受保護的特徵存在關聯,又會發生什麼?例如在種族隔離地區中,一個人的郵政編碼,甚至是他們的 IP 地址,都是很強的種族指示物。這樣的話,相信一種演算法可以以某種方式將有偏見的資料作為輸入,併產生公平和公正的輸出【85】似乎是很荒謬的。然而這種觀點似乎常常潛伏在資料驅動型決策的支持者中,這種態度被諷刺為 “在處理偏差上,機器學習與洗錢類似”(machine learning is like money laundering for bias)【86】。

預測性分析系統只是基於過去進行推斷;如果過去是歧視性的,它們就會將這種歧視歸納為規律。如果我們希望未來比過去更好,那麼就需要道德想象力,而這是隻有人類才能提供的東西【87】。資料與模型應該是我們的工具,而不是我們的主人。

責任與問責

自動決策引發了關於責任與問責的問題【87】。如果一個人犯了錯誤,他可以被追責,受決定影響的人可以申訴。演算法也會犯錯誤,但是如果它們出錯,誰來負責【88】?當一輛自動駕駛汽車引發事故時,誰來負責?如果自動信用評分算法系統性地歧視特定種族或宗教的人,這些人是否有任何追索權?如果機器學習系統的決定要受到司法審查,你能向法官解釋演算法是如何做出決定的嗎?

收集關於人的資料並進行決策,信用評級機構是一個很經典的例子。不良的信用評分會使生活變得更艱難,但至少信用分通常是基於個人 實際的 借款歷史記錄,而記錄中的任何錯誤都能被糾正(儘管機構通常會設定門檻)。然而,基於機器學習的評分演算法通常會使用更寬泛的輸入,並且更不透明;因而很難理解特定決策是怎樣作出的,以及是否有人被不公正地,歧視性地對待【89】。

信用分總結了 “你過去的表現如何?”,而預測性分析通常是基於 “誰與你類似,以及與你類似的人過去表現的如何?”。與他人的行為畫上等號意味著刻板印象,例如,根據他們居住的地方(與種族和階級關係密切的特徵)。那麼那些放錯位置的人怎麼辦?而且,如果是因為錯誤資料導致的錯誤決定,追索幾乎是不可能的【87】。

很多資料本質上是統計性的,這意味著即使概率分佈在總體上是正確的,對於個例也可能是錯誤的。例如,如果貴國的平均壽命是 80 歲,這並不意味著你在 80 歲生日時就會死掉。很難從平均值與概率分佈中對某個特定個體的壽命作出什麼判斷,同樣,預測系統的輸出是概率性的,對於個例可能是錯誤的。

盲目相信資料決策至高無上,這不僅僅是一種妄想,而是有切實危險的。隨著資料驅動的決策變得越來越普遍,我們需要弄清楚,如何使演算法更負責任且更加透明,如何避免加強現有的偏見,以及如何在它們不可避免地出錯時加以修復。

我們還需要想清楚,如何避免資料被用於害人,如何認識資料的積極潛力。例如,分析可以揭示人們生活的財務特點與社會特點。一方面,這種權力可以用來將援助與支援集中在幫助那些最需要援助的人身上。另一方面,它有時會被掠奪性企業用於識別弱勢群體,並向其兜售高風險產品,比如高利貸和沒有價值的大學文憑【87,90】。

反饋迴圈

即使是那些對人直接影響比較小的預測性應用,比如推薦系統,也有一些必須正視的難題。當服務變得善於預測使用者想要看到什麼內容時,它最終可能只會向人們展示他們已經同意的觀點,將人們帶入滋生刻板印象,誤導資訊,與極端思想的 迴音室。我們已經看到過社交媒體迴音室對競選的影響了【91】。

當預測性分析影響人們的生活時,自我強化的反饋迴圈會導致非常有害的問題。例如,考慮僱主使用信用分來評估候選人的例子。你可能是一個信用分不錯的好員工,但因不可抗力的意外而陷入財務困境。由於不能按期付賬單,你的信用分會受到影響,進而導致找到工作更為困難。失業使你陷入貧困,這進一步惡化了你的分數,使你更難找到工作【87】。在資料與數學嚴謹性的偽裝背後,隱藏的是由惡毒假設導致的惡性迴圈。

我們無法預測這種反饋迴圈何時發生。然而透過對整個系統(不僅僅是計算機化的部分,而且還有與之互動的人)進行整體思考,許多後果是可以夠預測的 —— 一種稱為 系統思維(systems thinking) 的方法【92】。我們可以嘗試理解資料分析系統如何響應不同的行為,結構或特性。該系統是否加強和增大了人們之間現有的差異(例如,損不足以奉有餘,富者愈富,貧者愈貧),還是試圖與不公作鬥爭?而且即使有著最好的動機,我們也必須當心意想不到的後果。

隱私和追蹤

除了預測性分析 —— 使用資料來做出關於人的自動決策 —— 資料收集本身也存在道德問題。收集資料的組織,與被收集資料的人之間,到底屬於什麼關係?

當系統只儲存使用者明確輸入的資料時,是因為使用者希望系統以特定方式儲存和處理這些資料,系統是在為使用者提供服務:使用者就是客戶。但是,當用戶的活動被跟蹤並記錄,作為他們正在做的其他事情的副作用時,這種關係就沒有那麼清晰了。該服務不再僅僅完成使用者想要它要做的事情,而是服務於它自己的利益,而這可能與使用者的利益相沖突。

追蹤使用者行為資料對於許多面向用戶的線上服務而言,變得越來越重要:追蹤使用者點選了哪些搜尋結果有助於改善搜尋結果的排名;推薦 “喜歡 X 的人也喜歡 Y”,可以幫助使用者發現實用有趣的東西; A/B 測試和使用者流量分析有助於改善使用者介面。這些功能需要一定量的使用者行為跟蹤,而使用者也可以從中受益。

但不同公司有著不同的商業模式,追蹤並未止步於此。如果服務是透過廣告盈利的,那麼廣告主才是真正的客戶,而使用者的利益則屈居其次。跟蹤的資料會變得更詳細,分析變得更深入,資料會保留很長時間,以便為每個人建立詳細畫像,用於營銷。

現在,公司與被收集資料的使用者之間的關係,看上去就不太一樣了。公司會免費服務使用者,並引誘使用者儘可能多地使用服務。對使用者的追蹤,主要不是服務於該使用者個體,而是服務於掏錢資助該服務的廣告商。我認為這種關係可以用一個更具罪犯內涵的詞來恰當地描述:監視(surveilance)

監視

讓我們做一個思想實驗,嘗試用 監視(surveillance) 一詞替換 資料(data),再看看常見的短語是不是聽起來還那麼漂亮【93】。比如:“在我們的監視驅動的組織中,我們收集實時監視流並將它們儲存在我們的監視倉庫中。我們的監視科學家使用高階分析和監視處理來獲得新的見解。”

對於本書《設計監控密集型應用》而言,這個思想實驗是罕見的爭議性內容,但我認為需要激烈的言辭來強調這一點。在我們嘗試製造軟體 “吞噬世界” 的過程中【94】,我們已經建立了世界上迄今為止所見過的最偉大的大規模監視基礎設施。我們正朝著萬物互聯邁進,我們正在迅速走近這樣一個世界:每個有人居住的空間至少包含一個帶網際網路連線的麥克風,以智慧手機、智慧電視、語音控制助理裝置、嬰兒監視器甚至兒童玩具的形式存在,並使用基於雲的語音識別。這些裝置中的很多都有著可怕的安全記錄【95】。

即使是最為極權與專制的政權,可能也只會想著在每個房間裝一個麥克風,並強迫每個人始終攜帶能夠追蹤其位置與動向的裝置。然而,我們顯然是自願地,甚至熱情地投身於這個全域監視的世界。不同之處在於,資料是由公司,而不是由政府機構收集的【96】。

並不是所有的資料收集都稱得上監視,但檢視這一點有助於理解我們與資料收集者之間的關係。為什麼我們似乎很樂意接受企業的監視呢?也許你覺得自己沒有什麼好隱瞞的 —— 換句話說,你與當權階級穿一條褲子,你不是被邊緣化的少數派,也不必害怕受到迫害【97】。不是每個人都如此幸運。或者,也許這是因為目的似乎是溫和的 —— 這不是公然脅迫,也不是強制性的,而只是更好的推薦與更個性化的營銷。但是,結合上一節中對預測性分析的討論,這種區別似乎並不是很清晰。

我們已經看到與汽車追蹤裝置掛鉤的汽車保險費,以及取決於需要人佩戴健身追蹤裝置來確定的健康保險範圍。當監視被用於決定生活的重要方面時,例如保險或就業,它就開始變得不那麼溫和了。此外,資料分析可以揭示出令人驚訝的私密事物:例如,智慧手錶或健身追蹤器中的運動感測器能以相當好的精度計算出你正在輸入的內容(比如密碼)【98】。而分析演算法只會變得越來越精確。

同意與選擇的自由

我們可能會斷言使用者是自願選擇使用了會跟蹤其活動的服務,而且他們已經同意了服務條款與隱私政策,因此他們同意資料收集。我們甚至可以聲稱,使用者在用所提供的資料來 換取 有價值的服務,並且為了提供服務,追蹤是必要的。毫無疑問,社交網路、搜尋引擎以及各種其他免費的線上服務對於使用者來說都是有價值的,但是這個說法卻存在問題。

使用者幾乎不知道他們提供給我們的是什麼資料,哪些資料被放進了資料庫,資料又是怎樣被保留與處理的 —— 大多數隱私政策都是模稜兩可的,忽悠使用者而不敢開啟天窗說亮話。如果使用者不瞭解他們的資料會發生什麼,就無法給出任何有意義的同意。有時來自一個使用者的資料還會提到一些關於其他人的事,而其他那些人既不是該服務的使用者,也沒有同意任何條款。我們在本書這一部分中討論的衍生資料集 —— 來自整個使用者群的資料,加上行為追蹤與外部資料來源 —— 就恰好是使用者無法(在真正意義上)理解的資料型別。

而且從使用者身上挖掘資料是一個單向過程,而不是真正的互惠關係,也不是公平的價值交換。使用者對能用多少資料換來什麼樣的服務,既沒有沒有發言權也沒有選擇權:服務與使用者之間的關係是非常不對稱與單邊的。這些條款是由服務提出的,而不是由使用者提出的【99】。

對於不同意監視的使用者,唯一真正管用的備選項,就是簡單地不使用服務。但這個選擇也不是真正自由的:如果一項服務如此受歡迎,以至於 “被大多數人認為是基本社會參與的必要條件”【99】,那麼指望人們選擇退出這項服務是不合理的 —— 使用它 事實上(de facto) 是強制性的。例如,在大多數西方社會群體中,攜帶智慧手機,使用 Facebook 進行社交,以及使用 Google 查詢資訊已成為常態。特別是當一項服務具有網路效應時,人們選擇 使用會產生社會成本。

因為一個服務會跟蹤使用者而拒絕使用它,這只是少數人才擁有的權力,他們有足夠的時間與知識來了解隱私政策,並承受得起代價:錯過社會參與,以及使用服務可能帶來的專業機會。對於那些處境不太好的人而言,並沒有真正意義上的選擇:監控是不可避免的。

隱私與資料使用

有時候,人們聲稱 “隱私已死”,理由是有些使用者願意把各種關於他們生活的事情釋出到社交媒體上,有時是平凡俗套,但有時是高度私密的。但這種說法是錯誤的,而且是對 隱私(privacy) 一詞的誤解。

擁有隱私並不意味著保密一切東西;它意味著擁有選擇向誰展示哪些東西的自由,要公開什麼,以及要保密什麼。隱私權是一項決定權:在從保密到透明的光譜上,隱私使得每個人都能決定自己想要在什麼地方位於光譜上的哪個位置【99】。這是一個人自由與自主的重要方面。

當透過監控基礎設施從人身上提取資料時,隱私權不一定受到損害,而是轉移到了資料收集者手中。獲取資料的公司實際上是說 “相信我們會用你的資料做正確的事情”,這意味著,決定要透露什麼和保密什麼的權利從個體手中轉移到了公司手中。

這些公司反過來選擇保密這些監視結果,因為揭露這些會令人毛骨悚然,並損害它們的商業模式(比其他公司更瞭解人)。使用者的私密資訊只會間接地披露,例如針對特定人群定向投放廣告的工具(比如那些患有特定疾病的人群)。

即使特定使用者無法從特定廣告定向的人群中以個體的形式區分出來,但他們已經失去了披露一些私密資訊的能動性,例如他們是否患有某種疾病。決定向誰透露什麼並不是由個體按照自己的喜好決定的,而是由 公司,以利潤最大化為目標來行使隱私權的。

許多公司都有一個目標,不要讓人 感覺到 毛骨悚然 —— 先不說它們收集資料實際上是多麼具有侵犯性,讓我們先關注對使用者感受的管理。這些使用者感受經常被管理得很糟糕:例如,在事實上可能正確的一些東西,如果會觸發痛苦的回憶,使用者可能並不希望被提醒【100】。對於任何型別的資料,我們都應當考慮它出錯、不可取、不合時宜的可能性,並且需要建立處理這些失效的機制。無論是 “不可取” 還是 “不合時宜”,當然都是由人的判斷決定的;除非我們明確地將演算法編碼設計為尊重人類的需求,否則演算法會無視這些概念。作為這些系統的工程師,我們必須保持謙卑,充分規劃,接受這些失效。

允許線上服務的使用者控制其隱私設定,例如控制其他使用者可以看到哪些東西,是將一些控制交還給使用者的第一步。但無論怎麼設定,服務本身仍然可以不受限制地訪問資料,並能以隱私策略允許的任何方式自由使用它。即使服務承諾不會將資料出售給第三方,它通常會授予自己不受限制的權利,以便在內部處理與分析資料,而且往往比使用者公開可見的部分要深入的多。

這種從個體到公司的大規模隱私權轉移在歷史上是史無前例的【99】。監控一直存在,但它過去是昂貴的、手動的,不是可伸縮的、自動化的。信任關係一直存在,例如患者與其醫生之間,或被告與其律師之間 —— 但在這些情況下,資料的使用嚴格受到道德,法律和監管限制的約束。網際網路服務使得在未經有意義的同意下收集大量敏感資訊變得容易得多,而且無需使用者理解他們的私人資料到底發生了什麼。

資料資產與權力

由於行為資料是使用者與服務互動的副產品,因此有時被稱為 “資料廢氣” —— 暗示資料是毫無價值的廢料。從這個角度來看,行為和預測性分析可以被看作是一種從資料中提取價值的回收形式,否則這些資料就會被浪費。

更準確的看法恰恰相反:從經濟的角度來看,如果定向廣告是服務的金主,那麼關於人的行為資料就是服務的核心資產。在這種情況下,使用者與之互動的應用僅僅是一種誘騙使用者將更多的個人資訊提供給監控基礎設施的手段【99】。線上服務中經常表現出的令人愉悅的人類創造力與社會關係,十分諷刺地被資料提取機器所濫用。

個人資料是珍貴資產的說法因為資料中介的存在得到支援,這是陰影中的祕密行業,購買、聚合、分析、推斷以及轉售私密個人資料,主要用於市場營銷【90】。初創公司按照它們的使用者數量,“眼球數”,—— 即它們的監視能力來估值。

因為資料很有價值,所以很多人都想要它。當然,公司也想要它 —— 這就是為什麼它們一開始就收集資料的原因。但政府也想獲得它:透過祕密交易、脅迫、法律強制或者只是竊取【101】。當公司破產時,收集到的個人資料就是被出售的資產之一。而且資料安全很難保護,因此經常發生令人難堪的洩漏事件【102】。

這些觀察已經導致批評者聲稱,資料不僅僅是一種資產,而且是一種 “有毒資產”【101】,或者至少是 “有害物質”【103】。即使我們認為自己有能力阻止資料濫用,但每當我們收集資料時,我們都需要平衡收益以及這些資料落入惡人手中的風險:計算機系統可能會被犯罪分子或敵國特務滲透,資料可能會被內鬼洩露,公司可能會落入不擇手段的管理層手中,而這些管理者有著迥然不同的價值觀,或者國家可能被能毫無愧色迫使我們交出資料的政權所接管。

俗話說,“知識就是力量”。更進一步,“在避免自己被審視的同時審視他人,是權力最重要的形式之一”【105】。這就是極權政府想要監控的原因:這讓它們有能力控制全體居民。儘管今天的科技公司並沒有公開地尋求政治權力,但是它們積累的資料與知識卻給它們帶來了很多權力,其中大部分是在公共監督之外偷偷進行的【106】。

回顧工業革命

資料是資訊時代的決定性特徵。網際網路,資料儲存,處理和軟體驅動的自動化正在對全球經濟和人類社會產生重大影響。我們的日常生活與社會組織在過去十年中發生了變化,而且在未來的十年中可能會繼續發生根本性的變化,所以我們會想到與工業革命對比【87,96】。

工業革命是透過重大的技術與農業進步實現的,它帶來了持續的經濟增長,長期的生活水平顯著提高。然而它也帶來了一些嚴重的問題:空氣汙染(由於煙霧和化學過程)和水汙染(工業垃圾和人類垃圾)是可怖的。工廠老闆生活在紛奢之中,而城市工人經常居住在非常糟糕的住房中,並且在惡劣的條件下長時間工作。童工很常見,甚至包括礦井中危險而低薪的工作。

制定保護措施花費了很長的時間,例如環境保護條例、工作場所安全條例、宣佈使用童工非法以及食品衛生檢查。毫無疑問,生產成本增加了,因為工廠再也不能把廢物倒入河流、銷售汙染的食物或者剝削工人。但是整個社會都從中受益良多,我們中很少會有人想回到這些管制條例之前的日子【87】。

就像工業革命有著黑暗面需要應對一樣,我們轉向資訊時代的過程中,也有需要應對與解決的重大問題。我相信資料的收集與使用就是其中一個問題。用 Bruce Schneier 的話來說【96】:

資料是資訊時代的汙染問題,保護隱私是環境挑戰。幾乎所有的電腦都能生產資訊。它堆積在周圍,開始潰爛。我們如何處理它 —— 我們如何控制它,以及如何擺脫它 —— 是資訊經濟健康發展的核心議題。正如我們今天回顧工業時代的早期年代,並想知道我們的祖先在忙於建設工業世界的過程時怎麼能忽略汙染問題;我們的孫輩在回望資訊時代的早期年代時,將會就我們如何應對資料收集和濫用的挑戰來評斷我們。

我們應該設法讓他們感到驕傲。

立法與自律

資料保護法可能有助於維護個人的權利。例如,1995 年的 “歐洲資料保護指示” 規定,個人資料必須 “為特定的、明確的和合法的目的收集,而不是以與這些目的不相符的方式進一步處理”,並且資料必須 “就收集的目的而言適當、相關、不過分。“【107】。

但是,這個立法在今天的網際網路環境下是否有效還是有疑問的【108】。這些規則直接否定了大資料的哲學,即最大限度地收集資料,將其與其他資料集結合起來進行試驗和探索,以便產生新的洞察。探索意味著將資料用於未曾預期的目的,這與使用者同意的 “特定和明確” 目的相反(如果我們可以有意義地表示同意的話)【109】。更新的規章正在制定中【89】。

那些收集了大量有關人的資料的公司反對監管,認為這是創新的負擔與阻礙。在某種程度上,這種反對是有道理的。例如,分享醫療資料時,存在明顯的隱私風險,但也有潛在的機遇:如果資料分析能夠幫助我們實現更好的診斷或找到更好的治療方法,能夠阻止多少人的死亡【110】?過度監管可能會阻止這種突破。在這種潛在機會與風險之間找出平衡是很困難的【105】。

從根本上說,我認為我們需要科技行業在個人資料方面的文化轉變。我們應該停止將使用者視作待最佳化的指標資料,並記住他們是值得尊重、有尊嚴和能動性的人。我們應當在資料收集和實際處理中自我約束,以建立和維持依賴我們軟體的人們的信任【111】。我們應當將教育終端使用者視為己任,告訴他們我們是如何使用他們的資料的,而不是將他們矇在鼓裡。

我們應該允許每個人保留自己的隱私 —— 即,對自己資料的控制 —— 而不是透過監視來竊取這種控制權。我們控制自己資料的個體權利就像是國家公園的自然環境:如果我們不去明確地保護它、關心它,它就會被破壞。這將是公地的悲劇,我們都會因此而變得更糟。無所不在的監視並非不可避免的 —— 我們現在仍然能阻止它。

我們究竟能做到哪一步,是一個開放的問題。首先,我們不應該永久保留資料,而是一旦不再需要就立即清除資料【111,112】。清除資料與不變性的想法背道而馳(請參閱 “不變性的侷限性”),但這是可以解決的問題。我所看到的一種很有前景的方法是透過加密協議來實施訪問控制,而不僅僅是透過策略【113,114】。總的來說,文化與態度的改變是必要的。

本章小結

在本章中,我們討論了設計資料系統的新方式,而且也包括了我的個人觀點,以及對未來的猜測。我們從這樣一種觀察開始:沒有單種工具能高效服務所有可能的用例,因此應用必須組合使用幾種不同的軟體才能實現其目標。我們討論了如何使用批處理與事件流來解決這一 資料整合(data integration) 問題,以便讓資料變更在不同系統之間流動。

在這種方法中,某些系統被指定為記錄系統,而其他資料則透過轉換衍生自記錄系統。透過這種方式,我們可以維護索引,物化檢視,機器學習模型,統計摘要等等。透過使這些衍生和轉換操作非同步且鬆散耦合,能夠防止一個區域中的問題擴散到系統中不相關部分,從而增加整個系統的穩健性與容錯性。

將資料流表示為從一個數據集到另一個數據集的轉換也有助於演化應用程式:如果你想變更其中一個處理步驟,例如變更索引或快取的結構,則可以在整個輸入資料集上重新執行新的轉換程式碼,以便重新衍生輸出。同樣,出現問題時,你也可以修復程式碼並重新處理資料以便恢復。

這些過程與資料庫內部已經完成的過程非常類似,因此我們將資料流應用的概念重新改寫為,分拆(unbundling) 資料庫元件,並透過組合這些鬆散耦合的元件來構建應用程式。

衍生狀態可以透過觀察底層資料的變更來更新。此外,衍生狀態本身可以進一步被下游消費者觀察。我們甚至可以將這種資料流一路傳送至顯示資料的終端使用者裝置,從而構建可動態更新以反映資料變更,並在離線時能繼續工作的使用者介面。

接下來,我們討論了如何確保所有這些處理在出現故障時保持正確。我們看到可伸縮的強完整性保證可以透過非同步事件處理來實現,透過使用端到端操作識別符號使操作冪等,以及透過非同步檢查約束。客戶端可以等到檢查透過,或者不等待繼續前進,但是可能會冒有違反約束需要道歉的風險。這種方法比使用分散式事務的傳統方法更具可伸縮性與可靠性,並且在實踐中適用於很多業務流程。

透過圍繞資料流構建應用,並非同步檢查約束,我們可以避免絕大多數的協調工作,建立保證完整性且效能仍然表現良好的系統,即使在地理散佈的情況下與出現故障時亦然。然後,我們對使用審計來驗證資料完整性,以及損壞檢測進行了一些討論。

最後,我們退後一步,審視了構建資料密集型應用的一些道德問題。我們看到,雖然資料可以用來做好事,但它也可能造成很大傷害:作出嚴重影響人們生活的決定卻難以申訴,導致歧視與剝削、監視常態化、曝光私密資訊。我們也冒著資料被洩露的風險,並且可能會發現,即使是善意地使用資料也可能會導致意想不到的後果。

由於軟體和資料對世界產生了如此巨大的影響,我們工程師們必須牢記,我們有責任為我們想要的那種世界而努力:一個尊重人們,尊重人性的世界。我希望我們能夠一起為實現這一目標而努力。

參考文獻

  1. Rachid Belaid: “Postgres Full-Text Search is Good Enough!,” rachbelaid.com, July 13, 2015.
  2. Philippe Ajoux, Nathan Bronson, Sanjeev Kumar, et al.: “Challenges to Adopting Stronger Consistency at Scale,” at 15th USENIX Workshop on Hot Topics in Operating Systems (HotOS), May 2015.
  3. Pat Helland and Dave Campbell: “Building on Quicksand,” at 4th Biennial Conference on Innovative Data Systems Research (CIDR), January 2009.
  4. Jessica Kerr: “Provenance and Causality in Distributed Systems,” blog.jessitron.com, September 25, 2016.
  5. Kostas Tzoumas: “Batch Is a Special Case of Streaming,” data-artisans.com, September 15, 2015.
  6. Shinji Kim and Robert Blafford: “Stream Windowing Performance Analysis: Concord and Spark Streaming,” concord.io, July 6, 2016.
  7. Jay Kreps: “The Log: What Every Software Engineer Should Know About Real-Time Data's Unifying Abstraction,” engineering.linkedin.com, December 16, 2013.
  8. Pat Helland: “Life Beyond Distributed Transactions: An Apostate’s Opinion,” at 3rd Biennial Conference on Innovative Data Systems Research (CIDR), January 2007.
  9. Great Western Railway (1835–1948),” Network Rail Virtual Archive, networkrail.co.uk.
  10. Jacqueline Xu: “Online Migrations at Scale,” stripe.com, February 2, 2017.
  11. Molly Bartlett Dishman and Martin Fowler: “Agile Architecture,” at O'Reilly Software Architecture Conference, March 2015.
  12. Nathan Marz and James Warren: Big Data: Principles and Best Practices of Scalable Real-Time Data Systems. Manning, 2015. ISBN: 978-1-617-29034-3
  13. Oscar Boykin, Sam Ritchie, Ian O'Connell, and Jimmy Lin: “Summingbird: A Framework for Integrating Batch and Online MapReduce Computations,” at 40th International Conference on Very Large Data Bases (VLDB), September 2014.
  14. Jay Kreps: “Questioning the Lambda Architecture,” oreilly.com, July 2, 2014.
  15. Raul Castro Fernandez, Peter Pietzuch, Jay Kreps, et al.: “Liquid: Unifying Nearline and Offline Big Data Integration,” at 7th Biennial Conference on Innovative Data Systems Research (CIDR), January 2015.
  16. Dennis M. Ritchie and Ken Thompson: “The UNIX Time-Sharing System,” Communications of the ACM, volume 17, number 7, pages 365–375, July 1974. doi:10.1145/361011.361061
  17. Eric A. Brewer and Joseph M. Hellerstein: “CS262a: Advanced Topics in Computer Systems,” lecture notes, University of California, Berkeley, cs.berkeley.edu, August 2011.
  18. Michael Stonebraker: “The Case for Polystores,” wp.sigmod.org, July 13, 2015.
  19. Jennie Duggan, Aaron J. Elmore, Michael Stonebraker, et al.: “The BigDAWG Polystore System,” ACM SIGMOD Record, volume 44, number 2, pages 11–16, June 2015. doi:10.1145/2814710.2814713
  20. Patrycja Dybka: “Foreign Data Wrappers for PostgreSQL,” vertabelo.com, March 24, 2015.
  21. David B. Lomet, Alan Fekete, Gerhard Weikum, and Mike Zwilling: “Unbundling Transaction Services in the Cloud,” at 4th Biennial Conference on Innovative Data Systems Research (CIDR), January 2009.
  22. Martin Kleppmann and Jay Kreps: “Kafka, Samza and the Unix Philosophy of Distributed Data,” IEEE Data Engineering Bulletin, volume 38, number 4, pages 4–14, December 2015.
  23. John Hugg: “Winning Now and in the Future: Where VoltDB Shines,” voltdb.com, March 23, 2016.
  24. Frank McSherry, Derek G. Murray, Rebecca Isaacs, and Michael Isard: “Differential Dataflow,” at 6th Biennial Conference on Innovative Data Systems Research (CIDR), January 2013.
  25. Derek G Murray, Frank McSherry, Rebecca Isaacs, et al.: “Naiad: A Timely Dataflow System,” at 24th ACM Symposium on Operating Systems Principles (SOSP), pages 439–455, November 2013. doi:10.1145/2517349.2522738
  26. Gwen Shapira: “We have a bunch of customers who are implementing ‘database inside-out’ concept and they all ask ‘is anyone else doing it? are we crazy?’twitter.com, July 28, 2016.
  27. Martin Kleppmann: “Turning the Database Inside-out with Apache Samza,” at Strange Loop, September 2014.
  28. Peter Van Roy and Seif Haridi: Concepts, Techniques, and Models of Computer Programming. MIT Press, 2004. ISBN: 978-0-262-22069-9
  29. Juttle Documentation,” juttle.github.io, 2016.
  30. Evan Czaplicki and Stephen Chong: “Asynchronous Functional Reactive Programming for GUIs,” at 34th ACM SIGPLAN Conference on Programming Language Design and Implementation (PLDI), June 2013. doi:10.1145/2491956.2462161
  31. Engineer Bainomugisha, Andoni Lombide Carreton, Tom van Cutsem, Stijn Mostinckx, and Wolfgang de Meuter: “A Survey on Reactive Programming,” ACM Computing Surveys, volume 45, number 4, pages 1–34, August 2013. doi:10.1145/2501654.2501666
  32. Peter Alvaro, Neil Conway, Joseph M. Hellerstein, and William R. Marczak: “Consistency Analysis in Bloom: A CALM and Collected Approach,” at 5th Biennial Conference on Innovative Data Systems Research (CIDR), January 2011.
  33. Felienne Hermans: “Spreadsheets Are Code,” at Code Mesh, November 2015.
  34. Dan Bricklin and Bob Frankston: “VisiCalc: Information from Its Creators,” danbricklin.com.
  35. D. Sculley, Gary Holt, Daniel Golovin, et al.: “Machine Learning: The High-Interest Credit Card of Technical Debt,” at NIPS Workshop on Software Engineering for Machine Learning (SE4ML), December 2014.
  36. Peter Bailis, Alan Fekete, Michael J Franklin, et al.: “Feral Concurrency Control: An Empirical Investigation of Modern Application Integrity,” at ACM International Conference on Management of Data (SIGMOD), June 2015. doi:10.1145/2723372.2737784
  37. Guy Steele: “Re: Need for Macros (Was Re: Icon),” email to ll1-discuss mailing list, people.csail.mit.edu, December 24, 2001.
  38. David Gelernter: “Generative Communication in Linda,” ACM Transactions on Programming Languages and Systems (TOPLAS), volume 7, number 1, pages 80–112, January 1985. doi:10.1145/2363.2433
  39. Patrick Th. Eugster, Pascal A. Felber, Rachid Guerraoui, and Anne-Marie Kermarrec: “The Many Faces of Publish/Subscribe,” ACM Computing Surveys, volume 35, number 2, pages 114–131, June 2003. doi:10.1145/857076.857078
  40. Ben Stopford: “Microservices in a Streaming World,” at QCon London, March 2016.
  41. Christian Posta: “Why Microservices Should Be Event Driven: Autonomy vs Authority,” blog.christianposta.com, May 27, 2016.
  42. Alex Feyerke: “Say Hello to Offline First,” hood.ie, November 5, 2013.
  43. Sebastian Burckhardt, Daan Leijen, Jonathan Protzenko, and Manuel Fähndrich: “Global Sequence Protocol: A Robust Abstraction for Replicated Shared State,” at 29th European Conference on Object-Oriented Programming (ECOOP), July 2015. doi:10.4230/LIPIcs.ECOOP.2015.568
  44. Mark Soper: “Clearing Up React Data Management Confusion with Flux, Redux, and Relay,” medium.com, December 3, 2015.
  45. Eno Thereska, Damian Guy, Michael Noll, and Neha Narkhede: “Unifying Stream Processing and Interactive Queries in Apache Kafka,” confluent.io, October 26, 2016.
  46. Frank McSherry: “Dataflow as Database,” github.com, July 17, 2016.
  47. Peter Alvaro: “I See What You Mean,” at Strange Loop, September 2015.
  48. Nathan Marz: “Trident: A High-Level Abstraction for Realtime Computation,” blog.twitter.com, August 2, 2012.
  49. Edi Bice: “Low Latency Web Scale Fraud Prevention with Apache Samza, Kafka and Friends,” at Merchant Risk Council MRC Vegas Conference, March 2016.
  50. Charity Majors: “The Accidental DBA,” charity.wtf, October 2, 2016.
  51. Arthur J. Bernstein, Philip M. Lewis, and Shiyong Lu: “Semantic Conditions for Correctness at Different Isolation Levels,” at 16th International Conference on Data Engineering (ICDE), February 2000. doi:10.1109/ICDE.2000.839387
  52. Sudhir Jorwekar, Alan Fekete, Krithi Ramamritham, and S. Sudarshan: “Automating the Detection of Snapshot Isolation Anomalies,” at 33rd International Conference on Very Large Data Bases (VLDB), September 2007.
  53. Kyle Kingsbury: Jepsen blog post series, aphyr.com, 2013–2016.
  54. Michael Jouravlev: “Redirect After Post,” theserverside.com, August 1, 2004.
  55. Jerome H. Saltzer, David P. Reed, and David D. Clark: “End-to-End Arguments in System Design,” ACM Transactions on Computer Systems, volume 2, number 4, pages 277–288, November 1984. doi:10.1145/357401.357402
  56. Peter Bailis, Alan Fekete, Michael J. Franklin, et al.: “Coordination-Avoiding Database Systems,” Proceedings of the VLDB Endowment, volume 8, number 3, pages 185–196, November 2014.
  57. Alex Yarmula: “Strong Consistency in Manhattan,” blog.twitter.com, March 17, 2016.
  58. Douglas B Terry, Marvin M Theimer, Karin Petersen, et al.: “Managing Update Conflicts in Bayou, a Weakly Connected Replicated Storage System,” at 15th ACM Symposium on Operating Systems Principles (SOSP), pages 172–182, December 1995. doi:10.1145/224056.224070
  59. Jim Gray: “The Transaction Concept: Virtues and Limitations,” at 7th International Conference on Very Large Data Bases (VLDB), September 1981.
  60. Hector Garcia-Molina and Kenneth Salem: “Sagas,” at ACM International Conference on Management of Data (SIGMOD), May 1987. doi:10.1145/38713.38742
  61. Pat Helland: “Memories, Guesses, and Apologies,” blogs.msdn.com, May 15, 2007.
  62. Yoongu Kim, Ross Daly, Jeremie Kim, et al.: “Flipping Bits in Memory Without Accessing Them: An Experimental Study of DRAM Disturbance Errors,” at 41st Annual International Symposium on Computer Architecture (ISCA), June 2014. doi:10.1145/2678373.2665726
  63. Mark Seaborn and Thomas Dullien: “Exploiting the DRAM Rowhammer Bug to Gain Kernel Privileges,” googleprojectzero.blogspot.co.uk, March 9, 2015.
  64. Jim N. Gray and Catharine van Ingen: “Empirical Measurements of Disk Failure Rates and Error Rates,” Microsoft Research, MSR-TR-2005-166, December 2005.
  65. Annamalai Gurusami and Daniel Price: “Bug #73170: Duplicates in Unique Secondary Index Because of Fix of Bug#68021,” bugs.mysql.com, July 2014.
  66. Gary Fredericks: “Postgres Serializability Bug,” github.com, September 2015.
  67. Xiao Chen: “HDFS DataNode Scanners and Disk Checker Explained,” blog.cloudera.com, December 20, 2016.
  68. Jay Kreps: “Getting Real About Distributed System Reliability,” blog.empathybox.com, March 19, 2012.
  69. Martin Fowler: “The LMAX Architecture,” martinfowler.com, July 12, 2011.
  70. Sam Stokes: “Move Fast with Confidence,” blog.samstokes.co.uk, July 11, 2016.
  71. Sawtooth Lake Documentation,” Intel Corporation, intelledger.github.io, 2016.
  72. Richard Gendal Brown: “Introducing R3 Corda™: A Distributed Ledger Designed for Financial Services,” gendal.me, April 5, 2016.
  73. Trent McConaghy, Rodolphe Marques, Andreas Müller, et al.: “BigchainDB: A Scalable Blockchain Database,” bigchaindb.com, June 8, 2016.
  74. Ralph C. Merkle: “A Digital Signature Based on a Conventional Encryption Function,” at CRYPTO '87, August 1987. doi:10.1007/3-540-48184-2_32
  75. Ben Laurie: “Certificate Transparency,” ACM Queue, volume 12, number 8, pages 10-19, August 2014. doi:10.1145/2668152.2668154
  76. Mark D. Ryan: “Enhanced Certificate Transparency and End-to-End Encrypted Mail,” at Network and Distributed System Security Symposium (NDSS), February 2014. doi:10.14722/ndss.2014.23379
  77. Software Engineering Code of Ethics and Professional Practice,” Association for Computing Machinery, acm.org, 1999.
  78. François Chollet: “Software development is starting to involve important ethical choices,” twitter.com, October 30, 2016.
  79. Igor Perisic: “Making Hard Choices: The Quest for Ethics in Machine Learning,” engineering.linkedin.com, November 2016.
  80. John Naughton: “Algorithm Writers Need a Code of Conduct,” theguardian.com, December 6, 2015.
  81. Logan Kugler: “What Happens When Big Data Blunders?,” Communications of the ACM, volume 59, number 6, pages 15–16, June 2016. doi:10.1145/2911975
  82. Bill Davidow: “Welcome to Algorithmic Prison,” theatlantic.com, February 20, 2014.
  83. Don Peck: “They're Watching You at Work,” theatlantic.com, December 2013.
  84. Leigh Alexander: “Is an Algorithm Any Less Racist Than a Human?theguardian.com, August 3, 2016.
  85. Jesse Emspak: “How a Machine Learns Prejudice,” scientificamerican.com, December 29, 2016.
  86. Maciej Cegłowski: “The Moral Economy of Tech,” idlewords.com, June 2016.
  87. Cathy O'Neil: Weapons of Math Destruction: How Big Data Increases Inequality and Threatens Democracy. Crown Publishing, 2016. ISBN: 978-0-553-41881-1
  88. Julia Angwin: “Make Algorithms Accountable,” nytimes.com, August 1, 2016.
  89. Bryce Goodman and Seth Flaxman: “European Union Regulations on Algorithmic Decision-Making and a ‘Right to Explanation’,” arXiv:1606.08813, August 31, 2016.
  90. A Review of the Data Broker Industry: Collection, Use, and Sale of Consumer Data for Marketing Purposes,” Staff Report, United States Senate Committee on Commerce, Science, and Transportation, commerce.senate.gov, December 2013.
  91. Olivia Solon: “Facebook’s Failure: Did Fake News and Polarized Politics Get Trump Elected?theguardian.com, November 10, 2016.
  92. Donella H. Meadows and Diana Wright: Thinking in Systems: A Primer. Chelsea Green Publishing, 2008. ISBN: 978-1-603-58055-7
  93. Daniel J. Bernstein: “Listening to a ‘big data’/‘data science’ talk,” twitter.com, May 12, 2015.
  94. Marc Andreessen: “Why Software Is Eating the World,” The Wall Street Journal, 20 August 2011.
  95. J. M. Porup: “‘Internet of Things’ Security Is Hilariously Broken and Getting Worse,” arstechnica.com, January 23, 2016.
  96. Bruce Schneier: Data and Goliath: The Hidden Battles to Collect Your Data and Control Your World. W. W. Norton, 2015. ISBN: 978-0-393-35217-7
  97. The Grugq: “Nothing to Hide,” grugq.tumblr.com, April 15, 2016.
  98. Tony Beltramelli: “Deep-Spying: Spying Using Smartwatch and Deep Learning,” Masters Thesis, IT University of Copenhagen, December 2015. Available at arxiv.org/abs/1512.05616
  99. Shoshana Zuboff: “Big Other: Surveillance Capitalism and the Prospects of an Information Civilization,” Journal of Information Technology, volume 30, number 1, pages 75–89, April 2015.doi:10.1057/jit.2015.5
  100. Carina C. Zona: “Consequences of an Insightful Algorithm,” at GOTO Berlin, November 2016.
  101. Bruce Schneier: “Data Is a Toxic Asset, So Why Not Throw It Out?,” schneier.com, March 1, 2016.
  102. John E. Dunn: “The UK’s 15 Most Infamous Data Breaches,” techworld.com, November 18, 2016.
  103. Cory Scott: “Data is not toxic - which implies no benefit - but rather hazardous material, where we must balance need vs. want,” twitter.com, March 6, 2016.
  104. Bruce Schneier: “Mission Creep: When Everything Is Terrorism,” schneier.com, July 16, 2013.
  105. Lena Ulbricht and Maximilian von Grafenstein: “Big Data: Big Power Shifts?,” Internet Policy Review, volume 5, number 1, March 2016. doi:10.14763/2016.1.406
  106. Ellen P. Goodman and Julia Powles: “Facebook and Google: Most Powerful and Secretive Empires We've Ever Known,” theguardian.com, September 28, 2016.
  107. Directive 95/46/EC on the protection of individuals with regard to the processing of personal data and on the free movement of such data, Official Journal of the European Communities No. L 281/31, eur-lex.europa.eu, November 1995.
  108. Brendan Van Alsenoy: “Regulating Data Protection: The Allocation of Responsibility and Risk Among Actors Involved in Personal Data Processing,” Thesis, KU Leuven Centre for IT and IP Law, August 2016.
  109. Michiel Rhoen: “Beyond Consent: Improving Data Protection Through Consumer Protection Law,” Internet Policy Review, volume 5, number 1, March 2016. doi:10.14763/2016.1.404
  110. Jessica Leber: “Your Data Footprint Is Affecting Your Life in Ways You Can’t Even Imagine,” fastcoexist.com, March 15, 2016.
  111. Maciej Cegłowski: “Haunted by Data,” idlewords.com, October 2015.
  112. Sam Thielman: “You Are Not What You Read: Librarians Purge User Data to Protect Privacy,” theguardian.com, January 13, 2016.
  113. Conor Friedersdorf: “Edward Snowden’s Other Motive for Leaking,” theatlantic.com, May 13, 2014.
  114. Phillip Rogaway: “The Moral Character of Cryptographic Work,” Cryptology ePrint 2015/1162, December 2015.

上一章目錄下一章
第十一章:流處理設計資料密集型應用後記