diff --git a/content/en/ch7.md b/content/en/ch7.md
index 61afc9f..26633e6 100644
--- a/content/en/ch7.md
+++ b/content/en/ch7.md
@@ -611,7 +611,7 @@ automatically adds its ID to the list of IDs for the index entry `color:red`. As
{{< figure src="/fig/ddia_0709.png" id="fig_sharding_local_secondary" caption="Figure 7-9. Local secondary indexes: each shard indexes only the records within its own shard." class="w-full my-4" >}}
-> [!WARN] WARNING
+> [!WARNING] WARNING
If your database only supports a key-value model, you might be tempted to implement a secondary
index yourself by creating a mapping from values to IDs in application code. If you go down this
diff --git a/content/tw/_index.md b/content/tw/_index.md
index f9f679a..27405cc 100644
--- a/content/tw/_index.md
+++ b/content/tw/_index.md
@@ -13,7 +13,7 @@ breadcrumbs: false
PostgreSQL 專家,資料庫老司機,雲計算泥石流。
[**Pigsty**](https://pgsty.com) 作者與創始人。
架構師,DBA,全棧工程師 @ TanTan,Alibaba,Apple。
-獨立開源貢獻者,[GitStar Ranking 585](https://gitstar-ranking.com/Vonng),[國區活躍 Top20](https://committers.top/china)。
+獨立開源貢獻者,[GitStar Ranking 600](https://gitstar-ranking.com/Vonng),[國區活躍 Top20](https://committers.top/china)。
[DDIA](https://ddia.pigsty.io) / [PG Internal](https://pgint.vonng.com) 中文版譯者,公眾號:《老馮雲數》,資料庫 KOL。
**校訂**: [@yingang](https://github.com/yingang) | [繁體中文](/tw) **版本維護** by [@afunTW](https://github.com/afunTW) | [完整貢獻者列表](/contrib)
@@ -76,10 +76,12 @@ PostgreSQL 專家,資料庫老司機,雲計算泥石流。
### [第三部分:派生資料](/tw/part-iii)
-- [11. 批處理](/tw/ch11) (尚未釋出)
-- [12. 流處理](/tw/ch12) (尚未釋出)
-- [13. 做正確的事](/tw/ch13) (尚未釋出)
+- [11. 批處理](/tw/ch11)
+- [12. 流處理](/tw/ch12)
+- [13. 流式系統的哲學](/tw/ch13)
+- [14. 將事情做正確](/ch14)
- [術語表](/tw/glossary)
+- [索引](/index)
- [後記](/tw/colophon)
@@ -112,6 +114,22 @@ PostgreSQL 專家,資料庫老司機,雲計算泥石流。
| ISSUE & Pull Requests | USER | Title |
|-------------------------------------------------|------------------------------------------------------------|----------------------------------------------------------------|
+| [386](https://github.com/Vonng/ddia/pull/386) | [@uncle-lv](https://github.com/uncle-lv) | ch2: 最佳化一處翻譯 |
+| [384](https://github.com/Vonng/ddia/pull/384) | [@PanggNOTlovebean](https://github.com/PanggNOTlovebean) | docs: 最佳化中文文件的措辭和表達 |
+| [383](https://github.com/Vonng/ddia/pull/383) | [@PanggNOTlovebean](https://github.com/PanggNOTlovebean) | docs: 修正 ch4 中的術語和表達錯誤 |
+| [382](https://github.com/Vonng/ddia/pull/382) | [@uncle-lv](https://github.com/uncle-lv) | ch1: 最佳化一處翻譯 |
+| [381](https://github.com/Vonng/ddia/pull/381) | [@Max-Tortoise](https://github.com/Max-Tortoise) | ch4: 修正一處術語不完整問題 |
+| [377](https://github.com/Vonng/ddia/pull/377) | [@huang06](https://github.com/huang06) | 最佳化翻譯術語 |
+| [375](https://github.com/Vonng/ddia/issues/375) | [@z-soulx](https://github.com/z-soulx) | 對於是否100%全中文翻譯的必要性討論?個人-沒必要100%,特別是“名詞”,有原單詞更加適合it人員 |
+| [371](https://github.com/Vonng/ddia/pull/371) | [@lewiszlw](https://github.com/lewiszlw) | CPU core -> CPU 核心 |
+| [369](https://github.com/Vonng/ddia/pull/369) | [@bbwang-gl](https://github.com/bbwang-gl) | ch7: 可序列化快照隔離檢測一個事務何時修改另一個事務的讀取 |
+| [368](https://github.com/Vonng/ddia/pull/368) | [@yhao3](https://github.com/yhao3) | 更新 zh-tw.py 與 zh-tw 內容 |
+| [367](https://github.com/Vonng/ddia/pull/367) | [@yhao3](https://github.com/yhao3) | 修正拼寫、格式和標點問題 |
+| [366](https://github.com/Vonng/ddia/pull/366) | [@yangshangde](https://github.com/yangshangde) | ch8: 將“電源失敗”改為“電源失效” |
+| [365](https://github.com/Vonng/ddia/pull/365) | [@xyohn](https://github.com/xyohn) | ch1: 最佳化“儲存與計算分離”相關翻譯 |
+| [364](https://github.com/Vonng/ddia/issues/364) | [@xyohn](https://github.com/xyohn) | ch1: 最佳化“儲存與計算分離”相關翻譯 |
+| [363](https://github.com/Vonng/ddia/pull/363) | [@xyohn](https://github.com/xyohn) | #362: 最佳化一處翻譯 |
+| [362](https://github.com/Vonng/ddia/issues/362) | [@xyohn](https://github.com/xyohn) | ch1: 最佳化一處翻譯 |
| [359](https://github.com/Vonng/ddia/pull/359) | [@c25423](https://github.com/c25423) | ch10: 修正一處拼寫錯誤 |
| [358](https://github.com/Vonng/ddia/pull/358) | [@lewiszlw](https://github.com/lewiszlw) | ch4: 修正一處拼寫錯誤 |
| [356](https://github.com/Vonng/ddia/pull/356) | [@lewiszlw](https://github.com/lewiszlw) | ch2: 修正一處標點錯誤 |
diff --git a/content/tw/ch1.md b/content/tw/ch1.md
index 421049b..f9af39e 100644
--- a/content/tw/ch1.md
+++ b/content/tw/ch1.md
@@ -4,23 +4,25 @@ weight: 101
breadcrumbs: false
---
+
+
> *沒有完美的解決方案,只有權衡取捨。[…] 你能做的就是努力獲得最佳的權衡,這就是你所能期望的一切。*
>
> [Thomas Sowell](https://www.youtube.com/watch?v=2YUtKr8-_Fg),接受 Fred Barnes 採訪(2005)
> [!TIP] 早期讀者注意事項
-> 透過早期釋出電子書,您可以在書籍最早期的形式中獲得內容——作者在撰寫時的原始和未經編輯的內容——以便您可以在這些技術正式釋出之前就充分利用它們。
+> 透過 Early Release 電子書,你可以在最早階段讀到作者寫作中的原始、未編輯內容,從而在正式版釋出前儘早使用這些技術。
>
> 這將是最終書籍的第 1 章。本書的 GitHub 倉庫是 https://github.com/ept/ddia2-feedback。
-> 如果您想積極參與審閱和評論本草稿,請在 GitHub 上聯絡我們。
+> 如果你希望積極參與本草稿的審閱與評論,請在 GitHub 上聯絡。
-資料是當今許多應用程式開發的核心。隨著 Web 和移動應用、軟體即服務(SaaS)以及雲服務的興起,將來自不同使用者的資料儲存在共享的基於伺服器的資料基礎設施中已成為常態。來自使用者活動、業務交易、裝置和感測器的資料需要被儲存並可供分析使用。當用戶與應用程式互動時,他們既讀取已儲存的資料,也生成更多的資料。
+資料是當今應用開發的核心。隨著 Web 與移動應用、軟體即服務(SaaS)和雲服務普及,把許多不同使用者的資料存放在共享的伺服器端資料基礎設施中,已經成為常態。來自使用者行為、業務交易、裝置與感測器的資料,需要被儲存並可用於分析。使用者每次與應用互動,既會讀取已有資料,也會產生新資料。
-少量的資料可以儲存和處理在單臺機器上,通常相當容易處理。然而,隨著資料量或查詢速率的增長,資料需要分佈在多臺機器上,這帶來了許多挑戰。隨著應用程式需求變得更加複雜,將所有內容儲存在一個系統中已經不夠,可能需要組合多個提供不同能力的儲存或處理系統。
+當資料量較小、可在單機儲存和處理時,問題往往並不複雜。但隨著資料規模或查詢速率增長,資料必須分佈到多臺機器上,挑戰隨之而來。隨著需求變得更複雜,僅靠單一系統通常已不足夠,你可能需要組合多個具備不同能力的儲存與處理系統。
-如果資料管理是開發應用程式的主要挑戰之一,我們就稱應用程式為 **資料密集型(data-intensive)** 的 [^1]。雖然在 **計算密集型(compute-intensive)** 系統中,挑戰是並行化某些非常大規模的計算,但在資料密集型應用中,我們通常更關心諸如儲存和處理大量資料、管理資料變更、在面對故障和併發時確保一致性,以及確保服務高可用等問題。
+如果“管理資料”是開發過程中的主要挑戰之一,我們稱這樣的應用為 **資料密集型(data-intensive)** 應用 [^1]。與之對照,在 **計算密集型(compute-intensive)** 系統中,難點是並行化超大規模計算;而在資料密集型應用中,我們更常關心的是:如何儲存與處理海量資料、如何管理資料變化、如何在故障與併發下保持一致性,以及如何讓服務保持高可用。
-這些應用程式通常由提供常用功能的標準構建塊構建而成。例如,許多應用程式需要:
+這類應用通常由若干標準構件搭建而成,每個構件負責一種常見能力。例如,很多應用都需要:
* 儲存資料,以便它們或其他應用程式以後能再次找到(**資料庫**)
* 記住昂貴操作的結果,以加快讀取速度(**快取**)
@@ -28,15 +30,15 @@ breadcrumbs: false
* 一旦事件和資料變更發生就立即處理(**流處理**)
* 定期處理累積的大量資料(**批處理**)
-在構建應用程式時,我們通常會採用幾個軟體系統或服務,例如資料庫或 API,並用一些應用程式程式碼將它們粘合在一起。如果你正在做資料系統設計的工作,那麼這個過程可能會相當容易。
+在構建應用時,我們通常會選擇若干軟體系統或服務(例如資料庫或 API),再用應用程式碼把它們拼接起來。如果你的需求恰好落在這些系統的設計邊界內,這並不困難。
-然而,隨著你的應用程式變得更加雄心勃勃,挑戰就會出現。有許多具有不同特性的資料庫系統,適合不同的目的——你如何選擇使用哪一個?有各種快取方法、構建搜尋索引的幾種方式等等——你如何在它們之間進行權衡?你需要找出哪些工具和哪些方法最適合手頭的任務,當你需要做單個工具無法單獨完成的事情時,組合工具可能會很困難。
+但當應用目標更有野心時,問題就會出現。資料庫有很多種,各自特性不同、適用場景也不同,如何選型?快取有多種做法,搜尋索引也有多種構建方式,如何權衡?當單個工具無法獨立完成目標時,如何把多個工具可靠地組合起來?這些都並不簡單。
-本書是一個指南,幫助你決定使用哪些技術以及如何組合它們。正如你將看到的,沒有一種方法從根本上優於其他方法;一切都有利弊。透過本書,你將學會提出正確的問題來評估和比較資料系統,以便你能找出哪種方法最能滿足你特定應用程式的需求。
+本書正是用來幫助你做這類決策:該用什麼技術、怎樣組合技術。你會看到,沒有哪種方案在根本上永遠優於另一種;每種方案都有得失。透過本書,你將學會提出正確問題來評估和比較資料系統,從而為你的具體應用找到更合適的方案。
-我們將透過觀察當今組織中資料的一些典型使用方式來開始我們的旅程。這裡的許多想法起源於 **企業軟體**(即大型組織的軟體需求和工程實踐,大型組織包括大公司和政府等),因為歷史上只有大型組織擁有需要複雜技術解決方案的大資料量。如果你的資料量足夠小,你可以簡單地將其儲存在電子表格中!然而,最近小公司和初創公司管理大資料量並構建資料密集型系統也變得很常見。
+我們將從今天組織內資料的典型使用方式開始。這些思想很多源自 **企業軟體**(即大型組織的軟體需求與工程實踐,例如大公司和政府機構),因為在歷史上,只有這類組織才有足夠大的資料規模,值得投入複雜技術方案。如果你的資料足夠小,電子表格都可能夠用;但近些年,小公司和初創團隊構建資料密集型系統也越來越常見。
-資料系統的關鍵挑戰之一是不同的人需要用資料做非常不同的事情。如果你在一家公司工作,你和你的團隊將有一套優先事項,而另一個團隊可能有完全不同的目標,即使你們可能在處理相同的資料集!此外,這些目標可能沒有被明確闡述,這可能導致對正確方法的誤解和分歧。
+資料系統的核心難點之一在於:不同的人需要用同一份資料做完全不同的事。在公司裡,你和你的團隊有自己的優先順序,另一個團隊即使使用同一資料集,目標也可能完全不同。更麻煩的是,這些目標往往並未被明確表達,容易引發誤解和分歧。
為了幫助你瞭解可以做出哪些選擇,本章比較了幾個對比概念,並探討了它們的權衡:
@@ -45,18 +47,18 @@ breadcrumbs: false
* 何時從單節點系統轉向分散式系統(["分散式與單節點系統"](/tw/ch1#sec_introduction_distributed));以及
* 平衡業務需求和使用者權利(["資料系統、法律與社會"](/tw/ch1#sec_introduction_compliance))。
-此外,本章將為你提供本書其餘部分所需的術語。
+此外,本章還會引入貫穿全書的關鍵術語。
> [!TIP] 術語:前端和後端
-本書中我們將討論的大部分內容都與 **後端開發** 有關。為了解釋這個術語:對於 Web 應用程式,在 Web 瀏覽器中執行的客戶端程式碼稱為 **前端**,處理使用者請求的伺服器端程式碼稱為 **後端**。移動應用類似於前端,它們提供使用者介面,通常透過網際網路與伺服器端後端通訊。前端有時在使用者裝置上本地管理資料 [^2],但最大的資料基礎設施挑戰通常在於後端:前端只需要處理一個使用者的資料,而後端為 **所有** 使用者管理資料。
+本書討論的大部分內容都與 **後端開發** 相關。對 Web 應用而言,執行在瀏覽器中的客戶端程式碼稱為 **前端**,處理使用者請求的伺服器端程式碼稱為 **後端**。移動應用也類似前端:它們提供使用者介面,通常經由網際網路與伺服器端後端通訊。前端有時會在裝置本地管理資料 [^2],但更棘手的資料基礎設施問題通常發生在後端:前端只處理單個使用者的資料,而後端需要代表 **所有** 使用者管理資料。
-後端服務通常可透過 HTTP(有時是 WebSocket)訪問;它通常由一些應用程式程式碼組成,這些程式碼在一個或多個數據庫中讀取和寫入資料,有時還與其他資料系統(如快取或訊息佇列)互動(我們可能將其統稱為 **資料基礎設施**)。應用程式程式碼通常是 **無狀態的**(即,當它完成處理一個 HTTP 請求時,它會忘記關於該請求的所有內容),任何需要從一個請求持續到另一個請求的資訊都需要儲存在客戶端或伺服器端的資料基礎設施中。
+後端服務通常透過 HTTP(有時是 WebSocket)提供訪問。其核心是應用程式碼:在一個或多個數據庫中讀寫資料,並按需接入快取、訊息佇列等其他系統(可統稱為 **資料基礎設施**)。應用程式碼往往是 **無狀態** 的:處理完一個 HTTP 請求後,不保留該請求上下文。因此,凡是需要跨請求持久化的資訊,都必須寫在客戶端,或寫入伺服器端資料基礎設施。
## 分析型與事務型系統 {#sec_introduction_analytics}
-如果你在企業中從事資料系統工作,你可能會遇到幾種不同型別的資料工作者。第一類是 **後端工程師**,他們構建服務來處理讀取和更新資料的請求;這些服務通常直接或間接地透過其他服務為外部使用者提供服務(參見["微服務與 Serverless"](/tw/ch1#sec_introduction_microservices))。有時服務是供組織其他部門內部使用的。
+如果你在企業中從事資料系統工作,往往會遇到幾類不同的資料使用者。第一類是 **後端工程師**,他們構建服務來處理讀取與更新資料的請求;這些服務通常直接面向外部使用者,或透過其他服務間接提供能力(參見["微服務與無伺服器"](/tw/ch1#sec_introduction_microservices))。有時服務也只供組織內部使用。
除了管理後端服務的團隊外,通常還有兩類人需要訪問組織的資料:**業務分析師**,他們生成關於組織活動的報告,以幫助管理層做出更好的決策(**商業智慧** 或 **BI**);以及 **資料科學家**,他們在資料中尋找新的見解,或建立由資料分析和機器學習(AI)支援的面向使用者的產品功能(例如,電子商務網站上的“購買了 X 的人也購買了 Y”推薦、風險評分或垃圾郵件過濾等預測分析,以及搜尋結果排名)。
@@ -67,7 +69,7 @@ breadcrumbs: false
正如我們將在下一節中看到的,事務型系統和分析型系統通常出於充分的理由而保持分離。隨著這些系統的成熟,出現了兩個新的專業角色:**資料工程師** 和 **分析工程師**。資料工程師是知道如何整合事務型系統和分析型系統的人,並更廣泛地負責組織的資料基礎設施 [^3]。分析工程師對資料進行建模和轉換,使其對組織中的業務分析師和資料科學家更有用 [^4]。
-許多工程師專注於事務型系統和分析型系統中的一個。然而,本書涵蓋了事務型和分析型資料系統,因為兩者在組織內資料的生命週期中都扮演著重要角色。我們將深入探討用於向內部和外部使用者提供服務的資料基礎設施,以便你能更好地與分界線另一邊的同事合作。
+許多工程師只專注於事務型或分析型其中一側。然而,本書會同時覆蓋這兩類資料系統,因為它們都在組織內的資料生命週期中扮演關鍵角色。我們將深入討論向內外部使用者提供服務所需的資料基礎設施,幫助你更好地與“另一側”的同事協作。
### 事務處理與分析的特徵 {#sec_introduction_oltp}
@@ -128,7 +130,7 @@ breadcrumbs: false
一些資料庫系統提供 **混合事務/分析處理**(HTAP),目標是在單個系統中同時支援 OLTP 和分析,而無需從一個系統 ETL 到另一個系統 [^8] [^9]。然而,許多 HTAP 系統內部由一個 OLTP 系統與一個單獨的分析系統耦合組成,隱藏在公共介面後面——因此兩者之間的區別對於理解這些系統如何工作仍然很重要。
-此外,儘管 HTAP 存在,但由於目標和要求不同,事務型系統和分析型系統之間的分離是常見的。特別是,讓每個事務型系統擁有自己的資料庫被認為是良好的做法(參見["微服務與 Serverless"](/tw/ch1#sec_introduction_microservices)),這將導致數百個單獨的事務型資料庫;另一方面,企業通常有一個單一的資料倉庫,以便業務分析師可以在單個查詢中組合來自多個事務型系統的資料。
+此外,儘管 HTAP 已出現,但由於目標和約束不同,事務型系統與分析型系統分離仍很常見。尤其是,讓每個事務型系統擁有自己的資料庫通常被視為良好實踐(參見["微服務與無伺服器"](/tw/ch1#sec_introduction_microservices)),這會形成數百個相互獨立的事務型資料庫;與之對應,企業往往只有一個統一的資料倉庫,以便分析師能在單個查詢裡組合多個事務型系統的資料。
因此,HTAP 不會取代資料倉庫。相反,它在同一應用程式既需要執行掃描大量行的分析查詢,又需要以低延遲讀取和更新單個記錄的場景中很有用。例如,欺詐檢測可能涉及此類工作負載 [^10]。
@@ -153,31 +155,31 @@ Apache Hive、Spark SQL、Presto 和 Trino 是這種方法的例子。
#### 超越資料湖 {#beyond-the-data-lake}
-隨著分析實踐的成熟,組織越來越關注分析系統和資料管道的管理和運維,如在 DataOps 宣言中所描述的那樣 [^18]。其中一部分是治理、隱私和遵守 GDPR 和 CCPA 等法規的問題,我們將在["資料系統、法律與社會"](/tw/ch1#sec_introduction_compliance)和[待補充連結]中討論。
+隨著分析實踐的成熟,組織越來越重視分析系統與資料管道的管理和運維,這一點在 DataOps 宣言中已有體現 [^18]。其中一部分是治理、隱私以及對 GDPR、CCPA 等法規的遵從;我們會在["資料系統、法律與社會"](/tw/ch1#sec_introduction_compliance)和["立法與行業自律"](/ch14#sec_future_legislation)中討論。
-此外,分析資料越來越多地不僅作為檔案和關係表提供,還作為事件流(參見[待補充連結])。使用基於檔案的資料分析,你可以定期(例如,每天)重新執行分析以響應資料的變化,但流處理允許分析系統以秒級的速度響應事件。根據應用程式及其時間敏感性,流處理方法可能很有價值,例如識別和阻止潛在的欺詐或濫用活動。
+此外,分析資料的提供形式也越來越多樣:不僅有檔案和關係表,也有事件流(見[第 12 章](/tw/ch12#ch_stream))。基於檔案的分析通常透過週期性重跑(例如每天一次)來響應資料變化,而流處理能夠讓分析系統在秒級響應事件。對於時效性要求高的場景,這種方式很有價值,例如識別並阻斷潛在的欺詐或濫用行為。
-在某些情況下,分析系統的輸出被提供給事務型系統(這個過程有時被稱為 **反向 ETL** [^19])。例如,在分析系統中訓練的機器學習模型可能會部署到生產環境中,以便為終端使用者生成推薦,例如“購買了 X 的人也購買了 Y”。這種分析系統的部署輸出也被稱為 **資料產品** [^20]。機器學習模型可以使用 TFX、Kubeflow 或 MLflow 等專門工具部署到事務型系統。
+在某些場景中,分析系統的輸出還會迴流到事務型系統(這一過程有時稱為 **反向 ETL** [^19])。例如,在分析系統裡訓練出的機器學習模型會部署到生產環境,為終端使用者生成“買了 X 的人也買了 Y”這類推薦。此類分析系統的投產結果也稱為 **資料產品** [^20]。機器學習模型可藉助 TFX、Kubeflow、MLflow 等專用工具部署到事務型系統。
-### 權威資料來源與派生資料 {#sec_introduction_derived}
+### 記錄系統與派生資料 {#sec_introduction_derived}
-與事務型系統和分析型系統之間的區別相關,本書還區分了 **權威記錄系統** 和 **派生資料系統**。這些術語很有用,因為它們可以幫助你澄清資料在系統中的流動:
+與事務型系統和分析型系統的區分相關,本書還區分 **記錄系統** 與 **派生資料系統**。這組術語有助於你理清資料在系統中的流向:
權威記錄系統
-: 權威記錄系統,也稱為 **權威資料來源**,儲存某些資料的權威或 **規範** 版本。當新資料進入時,例如作為使用者輸入,它首先寫入這裡。每個事實只表示一次(表示通常是 **正規化** 的;參見["正規化、反正規化和連線"](/tw/ch3#sec_datamodels_normalization))。如果另一個系統與權威記錄系統之間存在任何差異,那麼權威記錄系統中的值(根據定義)是正確的。
+: 記錄系統,也稱 **真相來源(權威資料來源)**,儲存某類資料的權威(canonical)版本。新資料進入系統時(例如使用者輸入)首先寫入這裡。每個事實只表示一次(這種表示通常是 **正規化** 的;見["正規化、反正規化與連線"](/tw/ch3#sec_datamodels_normalization))。如果其他系統與記錄系統不一致,則按定義以記錄系統為準。
派生資料系統
-: 派生系統中的資料是從另一個系統獲取一些現有資料並以某種方式轉換或處理它的結果。如果你丟失了派生資料,你可以從原始源重新建立它。一個經典的例子是快取:如果存在,可以從快取提供資料,但如果快取不包含你需要的內容,你可以回退到底層資料庫。反正規化值、索引、物化檢視、轉換的資料表示和在資料集上訓練的模型也屬於這一類別。
+: 派生系統中的資料,是對其他系統中已有資料進行轉換或處理後的結果。如果派生資料丟失,可以從原始資料來源重新構建。經典例子是快取:命中時由快取返回,未命中時回退到底層資料庫。反正規化值、索引、物化檢視、變換後的資料表示,以及在資料集上訓練出的模型,都屬於這一類。
-從技術上講,派生資料是 **冗餘** 的,因為它複製了現有資訊。然而,它通常對於在讀取查詢上獲得良好效能至關重要。你可以從單個源派生幾個不同的資料集,使你能夠從不同的"視角"檢視資料。
+從技術上說,派生資料是 **冗餘** 的,因為它複製了已有資訊。但它往往是讀查詢高效能的關鍵。你可以從同一個源資料派生出多個數據集,以不同“視角”觀察同一份事實。
-分析系統通常是派生資料系統,因為它們是在其他地方建立的資料的消費者。事務型服務可能包含權威記錄系統和派生資料系統的混合。權威記錄系統是資料首先被寫入的主資料庫,而派生資料系統是加速常見讀取操作的索引和快取,特別是對於權威記錄系統無法有效回答的查詢。
+分析系統通常屬於派生資料系統,因為它消費的是別處產生的資料。事務型服務往往同時包含記錄系統和派生資料系統:前者是資料首先寫入的主資料庫,後者則是用於加速常見讀取操作的索引與快取,尤其針對記錄系統難以高效回答的查詢。
-大多數資料庫、儲存引擎和查詢語言並非從本質上就是權威記錄系統或派生資料系統。資料庫只是一個工具:如何使用它取決於你。權威記錄系統和派生資料系統之間的區別不取決於工具,而取決於你如何在應用程式中使用它。透過明確哪些資料是從哪些其他資料派生的,你可以為讓原本混亂的系統架構變得清晰。
+大多數資料庫、儲存引擎和查詢語言本身並不天然屬於“記錄系統”或“派生系統”。資料庫只是工具,關鍵在於你如何使用它。兩者的區別不在工具本身,而在應用中的職責劃分。只要明確“哪些資料由哪些資料派生而來”,原本混亂的系統架構就會清晰很多。
-當一個系統中的資料來源自另一個系統中的資料時,你需要一個過程來在權威記錄系統中的原始資料發生變化時更新派生資料。不幸的是,許多資料庫的設計基於這樣的假設:你的應用程式只需要使用那一個數據庫,它們不易於整合多個系統以傳播此類更新。在[待補充連結]中,我們將討論 **資料整合** 的方法,這允許我們組合多個數據系統來實現單個系統無法做到的事情。
+當一個系統的資料由另一個系統的資料派生而來時,你需要在記錄系統原始資料變化時同步更新派生資料。不幸的是,很多資料庫預設假設應用只依賴單一資料庫,並不擅長在多系統之間傳播這類更新。在["資料整合"](/tw/ch13#sec_future_integration)中,我們會討論如何組合多個數據系統,實現單一系統難以獨立完成的能力。
-這就結束了我們對分析和事務處理的比較。在下一節中,我們將研究另一個你可能已經看到多次爭論的權衡。
+至此,我們結束了對分析與事務處理的比較。下一節將討論另一組常被反覆爭論的權衡。
## 雲服務與自託管 {#sec_introduction_cloud}
@@ -238,11 +240,10 @@ Apache Hive、Spark SQL、Presto 和 Trino 是這種方法的例子。
除了具有不同的經濟模型(訂閱服務而不是購買硬體和許可軟體在其上執行)之外,雲的興起也對資料系統在技術層面的實現產生了深遠的影響。
術語 **雲原生** 用於描述旨在利用雲服務的架構。
-原則上,幾乎任何你可以自託管的軟體也可以作為雲服務提供,實際上,許多流行的資料系統現在都有託管服務。
-然而,從頭開始設計為雲原生的系統已被證明具有幾個優勢:在相同硬體上具有更好的效能、從故障中更快恢復、
-能夠快速擴充套件計算資源以匹配負載,以及支援更大的資料集 [^25] [^26] [^27]。[表 1-2](/tw/ch1#tab_cloud_native_dbs) 列出了兩種型別系統的一些示例。
+原則上,幾乎任何可自託管的軟體都可以做成雲服務;事實上,許多主流資料系統都已有託管版本。
+不過,從零設計為雲原生的系統已經展示出若干優勢:同等硬體下效能更好、故障恢復更快、能更快按負載擴縮計算資源,並支援更大資料集 [^25] [^26] [^27]。[表 1-2](/tw/ch1#tab_cloud_native_dbs) 給出兩類系統的一些示例。
-{{< figure id="#tab_cloud_native_dbs" title="表 1-2. 自託管和雲原生資料庫系統示例" class="w-full my-4" >}}
+{{< figure id="tab_cloud_native_dbs" title="表 1-2. 自託管與雲原生資料庫系統示例" class="w-full my-4" >}}
| 類別 | 自託管系統 | 雲原生系統 |
|------------------|----------------------------|----------------------------------------------------------------------|
@@ -304,7 +305,7 @@ Apache Hive、Spark SQL、Presto 和 Trino 是這種方法的例子。
採用雲服務可能比執行自己的基礎設施更容易、更快,儘管學習如何使用它也有成本,也許還要解決其限制。隨著越來越多的供應商提供針對不同用例的更廣泛的雲服務,不同服務之間的整合成為一個特別的挑戰 [^39] [^40]。
-ETL(參見["資料倉庫"](/tw/ch1#sec_introduction_dwh))只是故事的一部分;事務型雲服務也需要相互整合。目前,缺乏促進這種整合的標準,因此它通常涉及大量的手動工作。
+ETL(參見["資料倉庫"](/tw/ch1#sec_introduction_dwh))只是故事的一部分;面向事務處理的雲服務之間也需要相互整合。目前,缺乏能促進這類整合的標準,因此往往仍要投入大量手工工作。
無法完全外包給雲服務的其他運維方面包括維護應用程式及其使用的庫的安全性、管理你自己的服務之間的互動、監控服務的負載,以及追蹤問題的原因,例如效能下降或中斷。雖然雲正在改變運維的角色,但對運維的需求比以往任何時候都大。
@@ -358,11 +359,11 @@ ETL(參見["資料倉庫"](/tw/ch1#sec_introduction_dwh))只是故事的一
出於所有這些原因,如果你可以在單臺機器上做某件事情,與搭建分散式系統相比通常要簡單得多,成本也更低 [^23] [^46] [^51]。CPU、記憶體和磁碟已經變得更大、更快、更可靠。當與 DuckDB、SQLite 和 KùzuDB 等單節點資料庫結合使用時,許多工作負載現在可以在單個節點上執行。我們將在[第 4 章](/tw/ch4#ch_storage)中進一步探討這個主題。
-### 微服務與 Serverless {#sec_introduction_microservices}
+### 微服務與無伺服器 {#sec_introduction_microservices}
在多臺機器上分佈系統的最常見方式是將它們分為客戶端和伺服器,並讓客戶端向伺服器發出請求。最常見的是使用 HTTP 進行此通訊,正如我們將在["流經服務的資料流:REST 和 RPC"](/tw/ch5#sec_encoding_dataflow_rpc)中討論的。同一程序可能既是伺服器(處理傳入請求)又是客戶端(向其他服務發出出站請求)。
-這種構建應用程式的方式傳統上被稱為 **面向服務架構**(SOA);最近,這個想法已經被細化為 **微服務** 架構 [^52] [^53]。在這種架構中,服務有一個明確定義的目的(例如,對於 S3 來說,這個目的是檔案儲存);每個服務公開一個可以由客戶端透過網路呼叫的 API,每個服務有一個負責其維護的團隊。因此,複雜的應用程式可以分解為多個互動服務,每個服務由單獨的團隊管理。
+這種構建應用程式的方式傳統上被稱為 **面向服務的體系結構**(SOA);最近,這個想法已經被細化為 **微服務** 架構 [^52] [^53]。在這種架構中,服務有一個明確定義的目的(例如,對於 S3 來說,這個目的是檔案儲存);每個服務公開一個可以由客戶端透過網路呼叫的 API,每個服務有一個負責其維護的團隊。因此,複雜的應用程式可以分解為多個互動服務,每個服務由單獨的團隊管理。
將複雜的軟體分解為多個服務有幾個優點:每個服務可以獨立更新,減少團隊之間的協調工作;每個服務可以分配它需要的硬體資源;透過將實現細節隱藏在 API 後面,服務所有者可以自由地更改實現而不影響客戶端。在資料儲存方面,每個服務通常有自己的資料庫,而不在服務之間共享資料庫:共享資料庫實際上會使整個資料庫結構成為服務 API 的一部分,然後該結構將很難更改。共享資料庫還可能導致一個服務的查詢對其他服務的效能產生負面影響。
@@ -372,9 +373,9 @@ ETL(參見["資料倉庫"](/tw/ch1#sec_introduction_dwh))只是故事的一
微服務主要是人員問題的技術解決方案:允許不同的團隊獨立取得進展,而無需相互協調。這在大公司中很有價值,但在沒有很多團隊的小公司中,使用微服務可能是不必要的開銷,最好以最簡單的方式實現應用程式 [^52]。
-**Serverless** 或 **函式即服務**(FaaS)是部署服務的另一種方法,其中基礎設施的管理外包給雲供應商 [^33]。使用虛擬機器時,你必須明確選擇何時啟動或關閉例項;相比之下,使用 serverless 模型,雲提供商根據對你服務的傳入請求自動分配和釋放硬體資源 [^54]。Serverless 部署將更多的運營負擔轉移到雲提供商,並透過使用量而不是機器例項實現靈活的計費。為了提供這些好處,許多 serverless 基礎設施提供商對函式執行施加時間限制,限制執行時環境,並且在首次呼叫函式時可能會遭受緩慢的啟動時間。術語“serverless”也可能具有誤導性:每個 serverless 函式執行仍然在伺服器上執行,但後續執行可能在不同的伺服器上執行。此外,BigQuery 和各種 Kafka 產品等基礎設施已經採用“serverless”術語來表示他們的服務自動擴充套件,並且他們按使用量而不是機器例項計費。
+**無伺服器(Serverless)**,或 **函式即服務**(FaaS),是另一種部署方式:基礎設施管理進一步外包給雲廠商 [^33]。使用虛擬機器時,你需要顯式決定何時啟動、何時關閉例項;而在無伺服器模型中,雲廠商會根據進入服務的請求自動分配和回收計算資源 [^54]。這種部署方式把更多運維負擔轉移給雲廠商,並支援按使用量計費,而不是按例項計費。為實現這些優勢,許多無伺服器平臺會限制函式執行時長、限制執行時環境,並在函式首次呼叫時出現較慢冷啟動。術語“無伺服器”本身也容易誤導:每次函式執行依然執行在某臺伺服器上,只是後續執行未必在同一臺機器上。此外,BigQuery 及多種 Kafka 產品也採用“Serverless”術語,強調其服務可自動擴縮容且按使用量計費。
-就像雲端儲存用計量計費模型取代了容量規劃(提前決定購買多少磁碟)一樣,serverless 方法正在為程式碼執行帶來計量計費:你只為應用程式程式碼實際執行的時間付費,而不必提前配置資源。
+就像雲端儲存以計量計費取代了傳統容量規劃(預先決定買多少磁碟)一樣,無伺服器模式把同樣的計費邏輯帶到了程式碼執行層:你只為程式碼實際執行的時間付費,而不必預先準備固定資源。
### 雲計算與超級計算 {#id17}
@@ -398,7 +399,7 @@ ETL(參見["資料倉庫"](/tw/ch1#sec_introduction_dwh))只是故事的一
每個從事此類系統工作的人都有責任考慮道德影響並確保他們遵守相關法律。沒有必要讓每個人都成為法律和道德專家,但對法律和道德原則的基本認識與分散式系統中的一些基礎知識同樣重要。
-法律考慮正在影響資料系統設計的基礎 [^61]。例如,GDPR 授予個人在請求時刪除其資料的權利(有時稱為 **被遺忘權**)。然而,正如我們將在本書中看到的,許多資料系統依賴不可變構造(如僅追加日誌)作為其設計的一部分;我們如何確保刪除應該不可變的檔案中間的某些資料?我們如何處理已被納入派生資料集(參見["權威資料來源與派生資料"](/tw/ch1#sec_introduction_derived))的資料刪除,例如機器學習模型的訓練資料?回答這些問題會帶來新的工程挑戰。
+法律考慮正在影響資料系統設計的基礎 [^61]。例如,GDPR 授予個人在請求時刪除其資料的權利(有時稱為 **被遺忘權**)。然而,正如我們將在本書中看到的,許多資料系統依賴不可變構造(如僅追加日誌)作為其設計的一部分;我們如何確保刪除應該不可變的檔案中間的某些資料?我們如何處理已被納入派生資料集(參見["記錄系統與派生資料"](/tw/ch1#sec_introduction_derived))的資料刪除,例如機器學習模型的訓練資料?回答這些問題會帶來新的工程挑戰。
目前,我們對於哪些特定技術或系統架構應被視為“符合 GDPR”沒有明確的指導方針。法規故意不強制要求特定技術,因為隨著技術的進步,這些技術可能會迅速變化。相反,法律文字規定了需要解釋的高層級原則。這意味著如何遵守隱私法規的問題沒有簡單的答案,但我們將透過這個視角來看待本書中的一些技術。
@@ -410,22 +411,22 @@ ETL(參見["資料倉庫"](/tw/ch1#sec_introduction_dwh))只是故事的一
企業也注意到了隱私和安全問題。信用卡公司要求處理支付的企業遵守嚴格的支付卡行業(PCI)標準。處理商需要經常接受獨立審計師的評估,以驗證持續的合規性。軟體供應商也受到了更多的審查。現在許多買家要求他們的供應商遵守服務組織控制(SOC)型別 2 標準。與 PCI 合規性一樣,供應商需要接受第三方審計以驗證遵守情況。
-一般來說,重要的是平衡你的業務需求與你收集和處理其資料的人的需求。這個話題還有很多內容;在[待補充連結]中,我們將更深入地探討道德和法律合規性的主題,包括偏見和歧視的問題。
+總的來說,關鍵在於平衡業務目標與被收集、被處理資料的人們的權益。這個主題還有很多內容;在[第 14 章](/ch14#ch_right_thing)中,我們會進一步討論倫理與法律合規,以及偏見與歧視等問題。
## 總結 {#summary}
-本章的主題是理解權衡:也就是說要認識到對於許多問題並沒有一個正確的答案,而是有幾種不同的方法,每種方法都有各種利弊。我們探討了影響資料系統架構的一些最重要的選擇,並介紹了本書其餘部分所需的術語。
+本章的主線是理解“權衡”。對許多問題而言,並不存在唯一正確答案,而是有多種路徑,各有利弊。我們討論了影響資料系統架構的幾個關鍵選擇,並引入了後續章節會反覆使用的術語。
-我們首先區分了事務型(事務處理,OLTP)和分析型(OLAP)系統,並看到了它們的不同特徵:不僅透過不同的訪問模式管理不同型別的資料,而且服務於不同的受眾。我們遇到了資料倉庫和資料湖的概念,它們透過 ETL 從事務型系統接收資料。在[第 4 章](/tw/ch4#ch_storage)中,我們將看到事務型和分析型系統通常使用非常不同的內部資料佈局,因為它們需要服務的查詢型別不同。
+我們首先區分了事務型(事務處理,OLTP)和分析型(OLAP)系統。它們不僅面對不同訪問模式與資料型別,也服務於不同人群。我們還看到資料倉庫與資料湖這兩類體系,它們透過 ETL 接收來自事務型系統的資料。在[第 4 章](/tw/ch4#ch_storage)中,我們會看到由於查詢型別不同,事務型與分析型系統常常採用截然不同的內部資料佈局。
-然後,我們將雲服務(一個相對較新的發展)與傳統的自託管軟體正規化進行了比較,後者以前主導了資料系統架構。這些方法中哪一種更具成本效益在很大程度上取決於你的特定情況,但不可否認的是,雲原生方法正在為資料系統的架構帶來重大變化,例如它們分離儲存和計算的方式。
+隨後,我們把相對較新的雲服務模式與長期主導資料系統架構的自託管正規化做了比較。哪種方式更具成本效益高度依賴具體情境,但不可否認,雲原生架構正在深刻改變資料系統的構建方式,例如儲存與計算的分離。
-雲系統本質上是分散式的,我們簡要地研究了分散式系統與使用單臺機器相比的一些權衡。有些情況下你無法避免分散式,但如果可能在單臺機器上執行系統,建議不要急於使系統分散式化。在[第 9 章](/tw/ch9#ch_distributed)中,我們將更詳細地介紹分散式系統的挑戰。
+雲系統天然是分散式系統,我們也簡要討論了它與單機方案之間的權衡。有些場景無法避免分散式,但如果單機可行,不必急於把系統分散式化。在[第 9 章](/tw/ch9#ch_distributed)中,我們會更深入地討論分散式系統的挑戰。
-最後,我們看到資料系統架構不僅由部署系統的企業的需求決定,還由保護其資料被處理的人的權利的隱私法規決定——這是許多工程師容易忽視的一個方面。我們如何將法律要求轉化為技術實現還沒有非常清晰的答案,但在我們閱讀本書的其餘部分時,記住這個問題很重要。
+最後,資料系統架構不僅由企業自身需求決定,也受保護資料主體權利的隱私法規所塑造,而這一點常被工程實踐忽略。如何把法律要求轉化為技術實現,目前仍無標準答案;但在閱讀本書後續內容時,始終帶著這個問題會很重要。
-### 參考
+### 參考文獻
[^1]: Richard T. Kouzes, Gordon A. Anderson, Stephen T. Elbert, Ian Gorton, and Deborah K. Gracio. [The Changing Paradigm of Data-Intensive Computing](http://www2.ic.uff.br/~boeres/slides_AP/papers/TheChanginParadigmDataIntensiveComputing_2009.pdf). *IEEE Computer*, volume 42, issue 1, January 2009. [doi:10.1109/MC.2009.26](https://doi.org/10.1109/MC.2009.26)
[^2]: Martin Kleppmann, Adam Wiggins, Peter van Hardenberg, and Mark McGranaghan. [Local-first software: you own your data, in spite of the cloud](https://www.inkandswitch.com/local-first/). At *2019 ACM SIGPLAN International Symposium on New Ideas, New Paradigms, and Reflections on Programming and Software* (Onward!), October 2019. [doi:10.1145/3359591.3359737](https://doi.org/10.1145/3359591.3359737)
@@ -489,4 +490,4 @@ ETL(參見["資料倉庫"](/tw/ch1#sec_introduction_dwh))只是故事的一
[^60]: Cathy O’Neil: *Weapons of Math Destruction: How Big Data Increases Inequality and Threatens Democracy*. Crown Publishing, 2016. ISBN: 9780553418811
[^61]: Supreeth Shastri, Vinay Banakar, Melissa Wasserman, Arun Kumar, and Vijay Chidambaram. [Understanding and Benchmarking the Impact of GDPR on Database Systems](https://www.vldb.org/pvldb/vol13/p1064-shastri.pdf). *Proceedings of the VLDB Endowment*, volume 13, issue 7, pages 1064–1077, March 2020. [doi:10.14778/3384345.3384354](https://doi.org/10.14778/3384345.3384354)
[^62]: Martin Fowler. [Datensparsamkeit](https://www.martinfowler.com/bliki/Datensparsamkeit.html). *martinfowler.com*, December 2013. Archived at [perma.cc/R9QX-CME6](https://perma.cc/R9QX-CME6)
-[^63]: [Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016 (General Data Protection Regulation)](https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32016R0679&from=EN). *Official Journal of the European Union* L 119/1, May 2016.
+[^63]: [Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016 (General Data Protection Regulation)](https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32016R0679&from=EN). *Official Journal of the European Union* L 119/1, May 2016.
\ No newline at end of file
diff --git a/content/tw/ch10.md b/content/tw/ch10.md
index 8396036..599df42 100644
--- a/content/tw/ch10.md
+++ b/content/tw/ch10.md
@@ -4,6 +4,8 @@ weight: 210
breadcrumbs: false
---
+
+

> *一句古老的格言告誡說:"千萬不要帶著兩塊計時器出海;要麼帶一塊,要麼帶三塊。"*
@@ -42,7 +44,7 @@ breadcrumbs: false
這就是 *線性一致性* [^1] 背後的想法(也稱為 *原子一致性* [^2]、*強一致性*、*即時一致性* 或 *外部一致性* [^3])。線性一致性的確切定義相當微妙,我們將在本節的其餘部分探討它。但基本思想是讓系統看起來好像只有一份資料副本,並且對它的所有操作都是原子的。有了這個保證,即使實際上可能有多個副本,應用程式也不需要擔心它們。
-在線性一致系統中,一旦一個客戶端成功完成寫入,所有從資料庫讀取的客戶端都必須能夠看到剛剛寫入的值。維護單一資料副本的假象意味著保證讀取的值是最新的、最新的值,而不是來自過時的快取或副本。換句話說,線性一致性是一個 *新鮮度保證*。為了闡明這個想法,讓我們看一個非線性一致系統的例子。
+在線性一致系統中,一旦一個客戶端成功完成寫入,所有從資料庫讀取的客戶端都必須能夠看到剛剛寫入的值。維護單一資料副本的假象,意味著要保證讀取到的是最新值,而不是來自過時的快取或副本。換句話說,線性一致性是一種 *新鮮度保證*。為了闡明這個想法,讓我們看一個非線性一致系統的例子。
{{< figure src="/fig/ddia_1001.png" id="fig_consistency_linearizability_0" caption="圖 10-1. 如果這個資料庫是線性一致的,那麼 Alice 的讀取要麼返回 1 而不是 0,要麼 Bob 的讀取返回 0 而不是 1。" class="w-full my-4" >}}
@@ -152,7 +154,7 @@ breadcrumbs: false
如果你想確保銀行賬戶餘額永遠不會變為負數,或者你不會銷售超過倉庫庫存的物品,或者兩個人不會同時預訂同一航班或劇院的同一座位,也會出現類似的問題。這些約束都要求有一個所有節點都同意的單一最新值(賬戶餘額、庫存水平、座位佔用情況)。
-在實際應用中,有時可以接受寬鬆地對待這些約束(例如,如果航班超售,你可以將客戶轉移到其他航班,併為不便提供補償)。在這種情況下,可能不需要線性一致性,我們將在 [Link to Come] 中討論這種寬鬆解釋的約束。
+在實際應用中,有時可以接受寬鬆地對待這些約束(例如,如果航班超售,你可以將客戶轉移到其他航班,併為不便提供補償)。在這種情況下,可能不需要線性一致性,我們將在 ["時效性與完整性"](/tw/ch13#sec_future_integrity) 中討論這種寬鬆解釋的約束。
然而,硬唯一性約束,例如你通常在關係資料庫中找到的約束,需要線性一致性。其他型別的約束,例如外部索引鍵或屬性約束,可以在沒有線性一致性的情況下實現 [^20]。
@@ -162,7 +164,7 @@ breadcrumbs: false
類似的情況可能出現在計算機系統中。例如,假設你有一個網站,使用者可以上傳影片,後臺程序將影片轉碼為較低質量,以便在慢速網際網路連線上流式傳輸。該系統的架構和資料流如 [圖 10-5](/tw/ch10#fig_consistency_transcoder) 所示。
-影片轉碼器需要明確指示執行轉碼作業,此指令透過訊息佇列從 Web 伺服器傳送到轉碼器(見 [Link to Come])。Web 伺服器不會將整個影片放在佇列中,因為大多數訊息代理都是為小訊息設計的,而影片可能有許多兆位元組大小。相反,影片首先寫入檔案儲存服務,寫入完成後,轉碼指令被放入佇列。
+影片轉碼器需要明確指示執行轉碼作業,此指令透過訊息佇列從 Web 伺服器傳送到轉碼器(見 ["訊息傳遞系統"](/tw/ch12#sec_stream_messaging))。Web 伺服器不會將整個影片放在佇列中,因為大多數訊息代理都是為小訊息設計的,而影片可能有許多兆位元組大小。相反,影片首先寫入檔案儲存服務,寫入完成後,轉碼指令被放入佇列。
{{< figure src="/fig/ddia_1005.png" id="fig_consistency_transcoder" caption="圖 10-5. 一個非線性一致的系統:Alice 和 Bob 在不同時間看到上傳的影像,因此 Bob 的請求基於過時的資料。" class="w-full my-4" >}}
@@ -185,7 +187,7 @@ breadcrumbs: false
讓我們重新審視 [第六章](/tw/ch6) 中的複製方法,並比較它們是否可以實現線性一致:
單主複製(可能線性一致)
-: 在單主複製系統中,主節點擁有用於寫入的資料主副本,從節點在其他節點上維護資料的備份副本。只要你在主節點上執行所有讀寫操作,它們很可能是線性一致的。然而,這假設你確定知道誰是主節點。如 ["分散式鎖和租約"](/tw/ch9#sec_distributed_lock_fencing) 中所討論的,一個節點很可能認為自己是主節點,而實際上並不是——如果這個妄想的主節點繼續服務請求,很可能會違反線性一致性 [^21]。使用非同步複製,故障切換甚至可能丟失已提交的寫入,這違反了永續性和線性一致性。
+: 在單主複製系統中,主節點擁有用於寫入的資料主副本,備庫在其他節點上維護資料副本。只要你在主節點上執行所有讀寫操作,它們很可能是線性一致的。然而,這假設你確定知道誰是主節點。如 ["分散式鎖和租約"](/tw/ch9#sec_distributed_lock_fencing) 中所討論的,一個節點很可能認為自己是主節點,而實際上並不是。如果這個“妄想中的主節點”繼續處理請求,很可能會違反線性一致性 [^21]。使用非同步複製時,故障切換甚至可能丟失已提交的寫入,這違反了永續性和線性一致性。
對單主資料庫進行分片,每個分片有一個單獨的主節點,不會影響線性一致性,因為它只是單物件保證。跨分片事務是另一回事(見 ["分散式事務"](/tw/ch8#sec_transactions_distributed))。
@@ -230,11 +232,11 @@ breadcrumbs: false
使用多主資料庫,每個區域可以繼續正常執行:由於來自一個區域的寫入被非同步複製到另一個區域,寫入只是排隊並在網路連線恢復時交換。
-另一方面,如果使用單主複製,那麼主節點必須在其中一個區域。任何寫入和任何線性一致的讀取都必須傳送到主節點——因此,對於連線到從節點區域的任何客戶端,這些讀寫請求必須透過網路同步傳送到主節點區域。
+另一方面,如果使用單主複製,那麼主節點必須在其中一個區域。任何寫入和任何線性一致的讀取都必須傳送到主節點。因此,對於連線到備庫所在區域的任何客戶端,這些讀寫請求都必須透過網路同步傳送到主節點區域。
-如果在單主設定中區域之間的網路中斷,連線到從節點區域的客戶端無法聯絡主節點,因此它們既不能對資料庫進行任何寫入,也不能進行任何線性一致的讀取。它們仍然可以從從節點讀取,但它們可能是過時的(非線性一致)。如果應用程式需要線性一致的讀寫,網路中斷會導致應用程式在無法聯絡主節點的區域中變得不可用。
+如果在單主設定中區域之間的網路中斷,連線到備庫區域的客戶端無法聯絡主節點,因此它們既不能對資料庫進行任何寫入,也不能進行任何線性一致的讀取。它們仍然可以從備庫讀取,但這些讀取可能是過時的(非線性一致)。如果應用程式需要線性一致的讀寫,網路中斷會導致應用程式在無法聯絡主節點的區域中變得不可用。
-如果客戶端可以直接連線到主節點區域,這不是問題,因為應用程式在那裡繼續正常工作。但只能訪問從節點區域的客戶端將在網路連結修復之前遇到中斷。
+如果客戶端可以直接連線到主節點區域,這不是問題,因為應用程式在那裡繼續正常工作。但只能訪問備庫區域的客戶端將在網路鏈路修復之前遇到中斷。
#### CAP 定理 {#the-cap-theorem}
@@ -273,7 +275,7 @@ CP/AP 分類方案還有幾個進一步的缺陷 [^4]。*一致性* 被形式化
許多選擇不提供線性一致保證的分散式資料庫也是如此:它們這樣做主要是為了提高效能,而不是為了容錯 [^42]。線性一致性很慢——這在任何時候都是真的,不僅在網路故障期間。
-我們能否找到更高效的線性一致儲存實現?答案似乎是否定的:Attiya 和 Welch [^49] 證明,如果你想要線性一致性,讀寫請求的響應時間至少與網路中延遲的不確定性成正比。在具有高度可變延遲的網路中,例如大多數計算機網路(見 ["超時和無界延遲"](/tw/ch9#sec_distributed_queueing)),線性一致讀寫的響應時間不可避免地會很高。更快的線性一致性演算法不存在,但較弱的一致性模型可能會快得多,因此這種權衡對於延遲敏感的系統很重要。在 [Link to Come] 中,我們將討論一些在不犧牲正確性的情況下避免線性一致性的方法。
+我們能否找到更高效的線性一致儲存實現?答案似乎是否定的:Attiya 和 Welch [^49] 證明,如果你想要線性一致性,讀寫請求的響應時間至少與網路中延遲的不確定性成正比。在具有高度可變延遲的網路中,例如大多數計算機網路(見 ["超時和無界延遲"](/tw/ch9#sec_distributed_queueing)),線性一致讀寫的響應時間不可避免地會很高。更快的線性一致性演算法不存在,但較弱的一致性模型可能會快得多,因此這種權衡對於延遲敏感的系統很重要。在 ["時效性與完整性"](/tw/ch13#sec_future_integrity) 中,我們將討論一些在不犧牲正確性的情況下避免線性一致性的方法。
## ID 生成器和邏輯時鐘 {#sec_consistency_logical}
@@ -323,7 +325,7 @@ CP/AP 分類方案還有幾個進一步的缺陷 [^4]。*一致性* 被形式化
* 其時間戳緊湊(大小為幾個位元組)且唯一;
* 你可以比較任意兩個時間戳(即它們是 *全序* 的);並且
-* 時間戳的順序與因果關係 *一致*:如果操作 A 發生在 B 之前,那麼 A 的時間戳小於 B 的時間戳。(我們之前在 [""先發生"關係和併發"](/tw/ch6#sec_replication_happens_before) 中討論了因果關係。)
+* 時間戳的順序與因果關係 *一致*:如果操作 A 發生在 B 之前,那麼 A 的時間戳小於 B 的時間戳。(我們之前在 ["“先發生”關係與併發"](/tw/ch6#sec_replication_happens_before) 中討論了因果關係。)
單節點 ID 生成器滿足這些要求,但我們剛剛討論的分散式 ID 生成器不滿足因果排序要求。
@@ -384,7 +386,7 @@ Lamport 時間戳擅長捕獲事物發生的順序,但它們有一些限制:
確保 ID 分配線性一致的最簡單方法實際上是為此目的使用單個節點。該節點只需要原子地遞增計數器並在請求時返回其值,持久化計數器值(以便在節點崩潰並重新啟動時不會生成重複的 ID),並使用單主複製進行容錯複製。這種方法在實踐中使用:例如,TiDB/TiKV 稱之為 *時間戳預言機*,受 Google 的 Percolator [^57] 啟發。
-作為最佳化,你可以避免在每個請求上執行磁碟寫入和複製。相反,ID 生成器可以寫入描述一批 ID 的記錄;一旦該記錄被持久化和複製,節點就可以開始按順序向客戶端分發這些 ID。在它用完該批次中的 ID 之前,它可以為下一批持久化和複製記錄。這樣,如果節點崩潰並重新啟動或你故障轉移到從節點,某些 ID 將被跳過,但你不會發出任何重複或亂序的 ID。
+作為最佳化,你可以避免在每個請求上執行磁碟寫入和複製。相反,ID 生成器可以寫入描述一批 ID 的記錄;一旦該記錄被持久化並完成複製,節點就可以開始按順序向客戶端分發這些 ID。在它用完該批次中的 ID 之前,它可以為下一批持久化並複製記錄。這樣,如果節點崩潰並重啟,或故障切換到備庫,某些 ID 會被跳過,但不會發出任何重複或亂序的 ID。
你不能輕易地對 ID 生成器進行分片,因為如果你有多個分片獨立分發 ID,你就無法再保證它們的順序是線性一致的。你也不能輕易地將 ID 生成器分佈在多個區域;因此,在地理分散式資料庫中,所有 ID 請求都必須轉到單個區域的節點。從好的方面來說,ID 生成器的工作非常簡單,因此單個節點可以處理大量請求吞吐量。
@@ -489,7 +491,7 @@ Lamport 時間戳擅長捕獲事物發生的順序,但它們有一些限制:
#### 共享日誌作為共識 {#sec_consistency_shared_logs}
-我們已經看到了幾個日誌的例子,例如複製日誌、事務日誌和預寫日誌。日誌儲存一系列 *日誌條目*,任何讀取它的人都會看到相同順序的相同條目。有時日誌有一個允許追加新條目的單個寫入者,但 *共享日誌* 是多個節點可以請求追加條目的日誌。單主複製的一個例子:任何客戶端都可以要求主節點進行寫入,主節點將其追加到複製日誌,然後所有從節點按照與主節點相同的順序應用寫入。
+我們已經看到了幾個日誌的例子,例如複製日誌、事務日誌和預寫日誌。日誌儲存一系列 *日誌條目*,任何讀取它的人都會看到相同順序的相同條目。有時日誌有一個允許追加新條目的單個寫入者,但 *共享日誌* 是多個節點可以請求追加條目的日誌。單主複製就是一個例子:任何客戶端都可以要求主節點進行寫入,主節點將其追加到複製日誌,然後所有備庫按照與主節點相同的順序應用寫入。
更正式地說,共享日誌支援兩種操作:你可以請求將值新增到日誌中,並且可以讀取日誌中的條目。它必須滿足以下屬性:
@@ -577,7 +579,7 @@ Lamport 時間戳擅長捕獲事物發生的順序,但它們有一些限制:
#### 使用共享日誌 {#sec_consistency_smr}
-共享日誌非常適合資料庫複製:如果每個日誌條目代表對資料庫的寫入,並且每個副本使用確定性邏輯以相同的順序處理相同的寫入,那麼副本將全部處於一致狀態。這個想法被稱為 *狀態機複製* [^80],它是事件溯源背後的原則,我們在 ["事件溯源和 CQRS"](/tw/ch3#sec_datamodels_events) 中看到了。共享日誌對於流處理也很有用,我們將在 [Link to Come] 中看到。
+共享日誌非常適合資料庫複製:如果每個日誌條目代表對資料庫的寫入,並且每個副本使用確定性邏輯以相同的順序處理相同的寫入,那麼副本將全部處於一致狀態。這個想法被稱為 *狀態機複製* [^80],它是事件溯源背後的原則,我們在 ["事件溯源和 CQRS"](/tw/ch3#sec_datamodels_events) 中看到了。共享日誌對於流處理也很有用,我們將在 [第十二章](/tw/ch12#ch_stream) 中看到。
同樣,共享日誌可用於實現可序列化事務:如 ["實際序列執行"](/tw/ch8#sec_transactions_serial) 中所討論的,如果每個日誌條目代表要作為儲存過程執行的確定性事務,並且如果每個節點以相同的順序執行這些事務,那麼事務將是可序列化的 [^81] [^82]。
@@ -606,7 +608,7 @@ Lamport 時間戳擅長捕獲事物發生的順序,但它們有一些限制:
當節點因為在某個超時時間內沒有收到主節點的訊息而認為當前主節點已死時,它可能會開始投票選舉新的主節點。這次選舉被賦予一個大於任何先前紀元的新紀元編號。如果兩個不同紀元中的兩個不同主節點之間存在衝突(也許是因為先前的主節點實際上並沒有死),那麼具有更高紀元編號的主節點獲勝。
-在主節點被允許將下一個條目追加到共享日誌之前,它必須首先檢查是否有其他具有更高紀元編號的主節點可能追加不同的條目。它可以透過從節點仲裁收集投票來做到這一點——通常但不總是大多數節點 [^85]。只有在節點不知道任何其他具有更高紀元的主節點時,節點才會投贊成票。
+在主節點被允許將下一個條目追加到共享日誌之前,它必須首先檢查是否有其他具有更高紀元編號的主節點可能追加不同的條目。它可以透過從一個節點仲裁收集投票來做到這一點,通常(但並非總是)是多數節點 [^85]。只有在節點不知道任何其他具有更高紀元的主節點時,節點才會投贊成票。
因此,我們有兩輪投票:一次選擇主節點,第二次對主節點提議的下一個要追加到日誌的條目進行投票。這兩次投票的仲裁必須重疊:如果對提議的投票成功,投票支援它的節點中至少有一個也必須參與了最近成功的主節點選舉 [^85]。因此,如果對提議的投票透過而沒有透露任何更高編號的紀元,當前主節點可以得出結論,沒有選出具有更高紀元編號的主節點,因此它可以安全地將提議的條目追加到日誌中 [^26] [^86]。
@@ -625,7 +627,7 @@ Lamport 時間戳擅長捕獲事物發生的順序,但它們有一些限制:
如果你希望共識演算法嚴格保證 ["共享日誌作為共識"](/tw/ch10#sec_consistency_shared_logs) 中列出的屬性,那麼新主節點在處理任何寫入或線性一致讀取之前必須瞭解任何已確認的日誌條目,這一點至關重要。如果具有過時資料的節點成為新主節點,它可能會將新值寫入已經由舊主節點寫入的日誌條目,從而違反共享日誌的僅追加屬性。
-在某些情況下,你可能選擇削弱共識屬性,以便更快地從主節點故障中恢復。例如,Kafka 提供了啟用 *不乾淨的主節點選舉* 的選項,它允許任何副本成為主節點,即使它不是最新的。此外,在具有非同步複製的資料庫中,當主節點失敗時,你無法保證任何從節點是最新的。
+在某些情況下,你可能選擇削弱共識屬性,以便更快地從主節點故障中恢復。例如,Kafka 提供了啟用 *不乾淨的主節點選舉* 的選項,它允許任何副本成為主節點,即使它不是最新的。此外,在採用非同步複製的資料庫中,當主節點失敗時,你無法保證任何備庫是最新的。
如果你放棄新主節點必須是最新的要求,你可能會提高效能和可用性,但你是在薄冰上,因為共識理論不再適用。雖然只要沒有故障,事情就會正常工作,但 [第九章](/tw/ch9) 中討論的問題很容易導致大量資料丟失或損壞。
@@ -649,7 +651,62 @@ Lamport 時間戳擅長捕獲事物發生的順序,但它們有一些限制:
有時,共識演算法對網路問題特別敏感。例如,Raft 已被證明具有不愉快的邊緣情況 [^88] [^89]:如果除了一個始終不可靠的特定網路連結之外,整個網路都正常工作,Raft 可能會進入主節點身份在兩個節點之間不斷跳躍的情況,或者當前主節點不斷被迫辭職,因此係統實際上從未取得進展。設計對不可靠網路更穩健的演算法仍然是一個開放的研究問題。
-對於想要高可用但不想接受共識成本的系統,唯一真正的選擇是使用較弱的一致性模型,例如 [第六章](/tw/ch6) 中討論的無主或多主複製提供的模型。這些方法通常不提供線性一致性,但對於不需要它的應用程式來說這很好。
+對於想要高可用但不想接受共識成本的系統,唯一真正的選擇是使用較弱的一致性模型,例如 [第六章](/tw/ch6) 中討論的無主或多主複製提供的模型。這些方法通常不提供線性一致性,但對於不需要它的應用程式來說已經足夠。
+
+
+### 協調服務 {#sec_consistency_coordination}
+
+共識演算法對於任何希望提供線性一致操作的分散式資料庫都很有價值,許多現代分散式資料庫也都用共識來做複製。但有一類系統是共識演算法的重度使用者:*協調服務*,例如 ZooKeeper、etcd 和 Consul。雖然它們表面上看起來像普通鍵值儲存,但它們並不是為通用資料儲存而設計的。
+
+相反,它們的目標是協調另一個分散式系統中的多個節點。例如,Kubernetes 依賴 etcd;Spark 和 Flink 在高可用模式下會在後臺依賴 ZooKeeper。協調服務通常只儲存小規模資料,這些資料可以完全放入記憶體(同時仍會寫盤以保證永續性),並透過容錯共識演算法在多個節點間複製。
+
+協調服務的設計思路來自 Google 的 Chubby 鎖服務 [^17] [^58]。它把共識演算法與一些在分散式系統裡尤其有用的能力結合在一起:
+
+鎖與租約
+: 我們前面看到,共識系統可以實現具備容錯能力的原子比較並設定(CAS)操作。協調服務正是基於這一點來實現鎖和租約:若多個節點併發嘗試獲取同一個租約,最終只會有一個成功。
+
+支援柵欄
+: 如 ["分散式鎖和租約"](/tw/ch9#sec_distributed_lock_fencing) 所述,當某個資源受租約保護時,需要 *柵欄* 機制來防止程序暫停或網路大延遲時的相互干擾。共識系統可透過為每個日誌條目分配單調遞增 ID 來生成柵欄令牌(ZooKeeper 中的 `zxid` 和 `cversion`,etcd 中的 revision)。
+
+故障檢測
+: 客戶端會在協調服務上維持長連線會話,並透過週期性心跳檢查對端是否存活。即使連線臨時中斷或某臺服務端故障,客戶端持有的租約仍可保持有效;但如果超過租約超時時間仍未收到心跳,協調服務就會認為客戶端已失效並釋放租約(ZooKeeper 將其稱為 *臨時節點*)。
+
+變更通知
+: 客戶端可以請求:當某些鍵發生變化時由協調服務主動通知。這樣客戶端就能知道另一個節點何時加入叢集(基於其寫入的值),或者何時失效(會話超時、臨時節點消失)。這類通知避免了客戶端頻繁輪詢。
+
+故障檢測和變更通知本身不需要共識,但與需要共識的原子操作、柵欄機制結合後,它們對分散式協調非常有用。
+
+--------
+
+> [!TIP] 用協調服務管理配置
+
+應用與基礎設施通常都有配置引數,例如超時時間、執行緒池大小等。有時會把這類配置資料以鍵值對形式存放在協調服務中。程序啟動時載入最新配置,並訂閱後續變更通知。配置更新後,程序可以立即應用新值,或重啟後生效。
+
+配置管理本身不需要協調服務裡的共識能力;但如果系統本來就已經運行了協調服務,那麼直接複用它的通知機制會很方便。另一種做法是程序週期性地從檔案或 URL 拉取配置更新,以避免依賴專門的協調服務。
+
+--------
+
+#### 將工作分配給節點 {#allocating-work-to-nodes}
+
+當你有某個程序或服務的多個例項,且其中一個需要被選為主節點時,協調服務很有用。如果主節點失效,其他節點之一應當接管。這不僅適用於單主資料庫,也適用於作業排程器等有狀態系統。
+
+另一個場景是:你有某種分片資源(資料庫、訊息流、檔案儲存、分散式 Actor 系統等),需要決定每個分片由哪個節點負責。隨著新節點加入叢集,需要把部分分片從舊節點遷移到新節點以實現再平衡;當節點被移除或失效時,其他節點需要接手其工作。
+
+這類任務可以透過協調服務中的原子操作、臨時節點和通知機制配合完成。若實現得當,應用可以在無人值守的情況下自動從故障中恢復。即使有 Apache Curator 這類在 ZooKeeper 客戶端 API 上封裝的高階庫,這件事仍不容易;但它仍遠好於從零實現共識演算法,後者極易引入缺陷。
+
+專用協調服務還有一個優勢:無論被協調系統有多少節點,協調服務本身通常都只需執行在一組固定節點上(常見是 3 個或 5 個)。例如,一個擁有數千分片的儲存系統若在數千節點上直接跑共識會非常低效;把共識“外包”給少量協調服務節點通常更合理。
+
+通常,協調服務管理的資料變化頻率不高:例如“IP 為 10.1.1.23 的節點當前是分片 7 的主節點”這類資訊,更新週期往往是分鐘級或小時級。協調服務不適合儲存每秒變化數千次的資料。對於高頻變化資料,應該使用常規資料庫;或者使用 Apache BookKeeper [^90] [^91] 這類工具複製服務內部的快速變化狀態。
+
+#### 服務發現 {#service-discovery}
+
+ZooKeeper、etcd 和 Consul 也常用於 *服務發現*:即確定連線某個服務所需的 IP 地址(見 ["負載均衡、服務發現和服務網格"](/tw/ch5#sec_encoding_service_discovery))。在雲環境下,虛擬機器常常頻繁上下線,因此你通常無法預先知道服務地址。常見做法是讓服務啟動時把自身網路端點註冊到服務登錄檔,再供其他服務查詢。
+
+用協調服務做服務發現很方便,因為它的故障檢測和變更通知能讓客戶端及時跟蹤服務例項的增減。而且如果你本來就用協調服務做租約、鎖或主節點選舉,那麼繼續複用它做服務發現通常也很自然,因為它已經知道哪個節點應該接收請求。
+
+不過,對服務發現使用共識往往有些“殺雞用牛刀”:這個場景通常不要求線性一致性,更重要的是高可用和低延遲,因為沒有服務發現,整個系統都會停滯。因此通常更傾向於快取服務發現結果,並接受其可能略有陳舊。比如基於 DNS 的服務發現,就是透過多層快取來獲得良好的效能與可用性。
+
+為支援這類需求,ZooKeeper 提供了 *observer*(觀察者)節點:它接收日誌並維護一份 ZooKeeper 資料副本,但不參與共識投票。來自 observer 的讀取不具備線性一致性(可能陳舊),但即使網路中斷仍然可用,並且能透過快取提高系統可支援的讀吞吐量。
## 總結 {#summary}
@@ -676,24 +733,23 @@ Lamport 時間戳擅長捕獲事物發生的順序,但它們有一些限制:
原子事務提交
: 參與分散式事務的資料庫節點必須都以相同的方式 **決定** 是提交還是中止事務。
-線性一致的 fetch-and-add 操作
-: 這個操作可以用來實現 ID 生成器。多個節點可以併發地呼叫該操作,它 **決定** 它們遞增計數器的順序。這種情況實際上只解決了兩個節點之間的共識,而其他的適用於任意數量的節點。
+線性一致的獲取並增加操作
+: 這個操作可以用來實現 ID 生成器。多個節點可以併發呼叫該操作,它 **決定** 它們遞增計數器的順序。這種情況實際上只解決了兩個節點之間的共識,而其他情況適用於任意數量的節點。
-如果你只有一個節點,或者如果你願意將決策能力分配給單個節點,所有這些都是簡單的。這就是單領導者資料庫中發生的事情:所有的決策權都授予了領導者,這就是為什麼這樣的資料庫能夠提供線性一致的操作、唯一性約束、複製日誌等等。
+如果你只有一個節點,或者願意把決策能力交給單個節點,所有這些都很簡單。這就是單主資料庫中發生的事情:所有決策權都授予主節點,這也是這類資料庫能夠提供線性一致操作、唯一性約束和複製日誌等能力的原因。
-然而,如果那個單一的領導者失敗,或者如果網路中斷使領導者無法訪問,這樣的系統就無法取得任何進展,直到人工執行手動故障轉移。廣泛使用的共識演算法如 Raft 和 Paxos 本質上是帶有內建自動領導者選舉和故障轉移的單領導者複製(如果當前領導者失敗)。
+然而,如果這個單一主節點失效,或者網路中斷使其不可達,這樣的系統就無法繼續推進,直到人工完成手動故障切換。Raft 和 Paxos 等廣泛使用的共識演算法,本質上就是內建自動主節點選舉與故障切換的“單主複製”。
共識演算法經過精心設計,以確保在故障轉移期間不會丟失任何已提交的寫入,並且系統不會進入腦裂狀態(多個節點接受寫入)。這要求每個寫入和每個線性一致的讀取都由節點的仲裁(通常是多數)確認。這可能是昂貴的,特別是跨地理區域,但如果你想要共識提供的強一致性和容錯性,這是不可避免的。
像 ZooKeeper 和 etcd 這樣的協調服務也是建立在共識演算法之上的。它們提供鎖、租約、故障檢測和變更通知功能,這些功能對於管理分散式應用程式的狀態很有用。如果你發現自己想要做那些可以歸約為共識的事情之一,並且你希望它是容錯的,建議使用協調服務。它不會保證你做對,但它可能會有所幫助。
-共識演算法是複雜而微妙的,但它們得到了自 1980 年代以來發展起來的豐富理論體系的支援。這個理論使得構建能夠容忍我們在[第 9 章](/tw/ch9#ch_distributed)中討論的所有故障的系統成為可能,同時仍然確保你的資料不會損壞。這是一個了不起的成就,本章末尾的參考文獻展示了這項工作的一些亮點。
+共識演算法複雜而微妙,但其背後有自 1980 年代以來形成的豐富理論體系支援。正是這些理論,使我們能夠構建出能夠容忍 [第九章](/tw/ch9#ch_distributed) 所述故障、同時仍保證資料不被破壞的系統。這是分散式系統工程中的重要成就,本章末尾參考文獻展示了其中一些關鍵工作。
-然而,共識並不總是正確的工具:在某些系統中,不需要它提供的強一致性屬性,使用較弱的一致性以獲得更高的可用性和更好的效能會更好。在這些情況下,通常使用無領導者或多領導者複製,這是我們之前在[第 6 章](/tw/ch6#ch_replication)中討論過的。我們在本章中討論的邏輯時鐘在那種情況下是有幫助的。
+然而,共識並不總是正確的工具:在某些系統中,不需要它提供的強一致性屬性,使用較弱一致性來換取更高可用性和更好效能反而更合適。在這些場景下,通常會使用無主或多主複製,這也是我們之前在 [第六章](/tw/ch6#ch_replication) 討論過的內容。我們在本章討論的邏輯時鐘在那類場景中也很有幫助。
### 參考文獻
-
[^1]: Maurice P. Herlihy and Jeannette M. Wing. [Linearizability: A Correctness Condition for Concurrent Objects](https://cs.brown.edu/~mph/HerlihyW90/p463-herlihy.pdf). *ACM Transactions on Programming Languages and Systems* (TOPLAS), volume 12, issue 3, pages 463–492, July 1990. [doi:10.1145/78969.78972](https://doi.org/10.1145/78969.78972)
[^2]: Leslie Lamport. [On interprocess communication](https://www.microsoft.com/en-us/research/publication/interprocess-communication-part-basic-formalism-part-ii-algorithms/). *Distributed Computing*, volume 1, issue 2, pages 77–101, June 1986. [doi:10.1007/BF01786228](https://doi.org/10.1007/BF01786228)
[^3]: David K. Gifford. [Information Storage in a Decentralized Computer System](https://bitsavers.org/pdf/xerox/parc/techReports/CSL-81-8_Information_Storage_in_a_Decentralized_Computer_System.pdf). Xerox Palo Alto Research Centers, CSL-81-8, June 1981. Archived at [perma.cc/2XXP-3JPB](https://perma.cc/2XXP-3JPB)
diff --git a/content/tw/ch11.md b/content/tw/ch11.md
index 8678a3d..71ddbb1 100644
--- a/content/tw/ch11.md
+++ b/content/tw/ch11.md
@@ -2,69 +2,70 @@
title: "第十一章:批處理"
linkTitle: "11. 批處理"
weight: 311
-math: true
breadcrumbs: false
---
-{{< callout type="warning" >}}
-當前頁面來自本書第一版,第二版尚不可用
-{{< /callout >}}
+

-> 帶有太強個人色彩的系統無法成功。當最初的設計完成並且相對穩定時,不同的人們以自己的方式進行測試,真正的考驗才開始。
+> *帶有太強個人色彩的系統無法成功。當最初的設計完成並且相對穩健時,真正的考驗才剛開始:此後會有許多持不同觀點的人做出各自的實驗。*
>
-> —— 高德納
+> 高德納
-在本書的前兩部分中,我們討論了很多關於 **請求** 和 **查詢** 以及相應的 **響應** 或 **結果**。許多現有資料系統中都採用這種資料處理方式:你傳送請求指令,一段時間後(我們期望)系統會給出一個結果。資料庫、快取、搜尋索引、Web 伺服器以及其他一些系統都以這種方式工作。
+到目前為止,本書大部分內容都圍繞著 *請求(request)* 與 *查詢(query)* 以及對應的 *響應(response)* 或 *結果(result)* 展開。現代很多資料系統都預設採用這種處理方式:你發出請求或指令,系統儘快給出答案。
-像這樣的 **線上(online)** 系統,無論是瀏覽器請求頁面還是呼叫遠端 API 的服務,我們通常認為請求是由人類使用者觸發的,並且正在等待響應。他們不應該等太久,所以我們非常關注系統的響應時間(請參閱 “[描述效能](/tw/ch1#描述效能)”)。
+網頁瀏覽器請求頁面、服務呼叫遠端 API、資料庫、快取、搜尋索引,以及很多其他系統都如此運作。我們稱這類系統為 *線上系統(online systems)*。它們通常以響應時間作為主要效能指標,並且往往需要良好的容錯能力來保證高可用。
-Web 和越來越多的基於 HTTP/REST 的 API 使互動的請求 / 響應風格變得如此普遍,以至於很容易將其視為理所當然。但我們應該記住,這不是構建系統的唯一方式,其他方法也有其優點。我們來看看三種不同型別的系統:
+但有時候,你需要執行的計算比一次互動式請求大得多,或者要處理的資料量遠超單次請求能承載的範圍。例如訓練 AI 模型、把海量資料從一種形式轉換成另一種形式、或者在超大資料集上做分析計算。我們把這類任務稱為 *批處理(batch processing)* 作業,有時也稱為 *離線系統(offline systems)*。
-服務(線上系統)
-: 服務等待客戶的請求或指令到達。每收到一個,服務會試圖儘快處理它,併發回一個響應。響應時間通常是服務效能的主要衡量指標,可用性通常非常重要(如果客戶端無法訪問服務,使用者可能會收到錯誤訊息)。
+批處理作業讀取一批輸入資料(只讀),並生成一批輸出資料(每次執行都從頭生成)。它通常不會像讀寫事務那樣原地修改資料。因此,輸出是由輸入推匯出的 *派生資料(derived data)*(見[“記錄系統與派生資料”](/tw/ch1#sec_introduction_derived)):如果不滿意輸出,你可以直接刪除它,修改作業邏輯,再跑一遍即可。把輸入視為不可變並儘量避免副作用(例如直接寫外部資料庫),不僅有助於效能,也帶來其他好處:
-批處理系統(離線系統)
-: 一個批處理系統有大量的輸入資料,跑一個 **作業(job)** 來處理它,並生成一些輸出資料,這往往需要一段時間(從幾分鐘到幾天),所以通常不會有使用者等待作業完成。相反,批次作業通常會定期執行(例如,每天一次)。批處理作業的主要效能衡量標準通常是吞吐量(處理特定大小的輸入所需的時間)。本章中討論的就是批處理。
+- 如果你在程式碼中引入了 bug 導致輸出錯誤或損壞,可以直接回滾程式碼並重跑作業,輸出就會恢復正確。更簡單的做法是把舊輸出保留在另一個目錄,直接切回舊版本。多數物件儲存與開放表格式(見[“雲資料倉庫”](/tw/ch4#sec_cloud_data_warehouses))都支援這種能力,通常稱為 *時間旅行(time travel)*。大多數支援讀寫事務的資料庫不具備這種特性:如果錯誤程式碼把壞資料寫進資料庫,僅回滾程式碼並不能修復已寫入的資料。能夠從錯誤程式碼中恢復的能力被稱為 *容忍人為失誤* [^1]。
-流處理系統(準即時系統)
-: 流處理介於線上和離線(批處理)之間,所以有時候被稱為 **準即時(near-real-time)** 或 **準線上(nearline)** 處理。像批處理系統一樣,流處理消費輸入併產生輸出(並不需要響應請求)。但是,流式作業在事件發生後不久就會對事件進行操作,而批處理作業則需等待固定的一組輸入資料。這種差異使流處理系統比起批處理系統具有更低的延遲。由於流處理基於批處理,我們將在 [第十二章](/tw/ch12) 討論它。
+- 因為回滾容易,功能開發能比“犯錯會造成不可逆損害”的環境更快推進。這個 *最小化不可逆性* 的原則對敏捷開發非常有益 [^2]。
-正如我們將在本章中看到的那樣,批處理是構建可靠、可伸縮和可維護應用程式的重要組成部分。例如,2004 年釋出的批處理演算法 Map-Reduce(可能被過分熱情地)被稱為 “造就 Google 大規模可伸縮性的演算法”【2】。隨後在各種開源資料系統中得到應用,包括 Hadoop、CouchDB 和 MongoDB。
+- 同一組檔案可以作為多種作業的輸入,包括監控類作業:例如計算指標、驗證輸出是否符合預期(如與上一次結果比較並度量偏差)。
-與多年前為資料倉庫開發的並行處理系統【3,4】相比,MapReduce 是一個相當低級別的程式設計模型,但它使得在商用硬體上能進行的處理規模邁上一個新的臺階。雖然 MapReduce 的重要性正在下降【5】,但它仍然值得去理解,因為它描繪了一幅關於批處理為什麼有用,以及如何做到有用的清晰圖景。
+- 批處理框架能更高效地利用計算資源。雖然也可以用 OLTP 資料庫和應用伺服器等線上系統做批處理,但資源成本通常顯著更高。
-實際上,批處理是一種非常古老的計算方式。早在可程式設計數字計算機誕生之前,打孔卡製表機(例如 1890 年美國人口普查【6】中使用的霍爾里斯機)實現了半機械化的批處理形式,從大量輸入中彙總計算。Map-Reduce 與 1940 年代和 1950 年代廣泛用於商業資料處理的機電 IBM 卡片分類機器有著驚人的相似之處【7】。正如我們所說,歷史總是在不斷重複自己。
+批處理也有挑戰。多數框架中,作業只有在整體完成後,其輸出才能被下游進一步處理。批處理也可能低效:輸入哪怕只變動一個位元組,也可能需要重算整個輸入資料集。儘管如此,批處理在大量場景中依然非常有用,我們會在[“批處理用例”](/tw/ch11#sec_batch_output)中回到這個話題。
-在本章中,我們將瞭解 MapReduce 和其他一些批處理演算法和框架,並探索它們在現代資料系統中的作用。但首先我們將看看使用標準 Unix 工具的資料處理。即使你已經熟悉了它們,Unix 的哲學也值得一讀,Unix 的思想和經驗教訓可以遷移到大規模、異構的分散式資料系統中。
+批處理作業可能執行很久:幾分鐘、幾小時甚至幾天。很多作業是週期排程的(例如每天一次)。它的核心效能指標通常是吞吐量:單位時間能處理多少資料。有些批處理系統透過“中止並整體重啟”應對故障,也有些具備更細粒度容錯能力,可以在部分節點崩潰時仍讓作業完成。
+> [!NOTE]
+> 批處理的另一種替代形態是 *流處理(stream processing)*:作業不會在“處理完輸入後結束”,而是持續監聽輸入,並在變化發生後很快處理。我們將在[第十二章](/tw/ch12#ch_stream)討論流處理。
-## 使用Unix工具的批處理
+線上處理與批處理的邊界並不總是清晰:一個執行很久的資料庫查詢,看起來也很像批處理過程。但批處理有一些獨特特性,使其成為構建可靠、可伸縮、可維護應用的重要積木。例如,它常在 *資料整合(data integration)* 中發揮作用,即把多個數據系統組合起來完成單一系統做不到的事。ETL(見[“資料倉庫”](/tw/ch1#sec_introduction_dwh))就是典型例子。
-我們從一個簡單的例子開始。假設你有一臺 Web 伺服器,每次處理請求時都會在日誌檔案中附加一行。例如,使用 nginx 預設的訪問日誌格式,日誌的一行可能如下所示:
+現代批處理深受 MapReduce 影響。Google 在 2004 年發表了這一批處理演算法 [^3],隨後 Hadoop、CouchDB、MongoDB 等開源系統都實現了它。MapReduce 是相對底層的程式設計模型,其能力不如資料倉庫中的並行查詢執行引擎成熟 [^4] [^5]。它在誕生時確實讓商用硬體上的處理規模躍升一大步,但今天已大體過時,Google 內部也不再使用 [^6] [^7]。
-```bash
-216.58.210.78 - - [27/Feb/2015:17:55:11 +0000] "GET /css/typography.css HTTP/1.1"
-200 3377 "http://martin.kleppmann.com/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5)
-AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36"
-```
+如今批處理更常透過 Spark、Flink 或資料倉庫查詢引擎完成。它們與 MapReduce 一樣高度依賴分片(見[第七章](/tw/ch7#ch_sharding))和並行執行,但快取與執行策略更成熟。隨著這些系統走向成熟,運維問題已大幅緩解,重點轉向可用性:資料流 API、查詢語言、DataFrame API 得到廣泛支援;任務與工作流編排也顯著進化。以 Hadoop 為中心的 Oozie、Azkaban 等排程器,正被 Airflow、Dagster、Prefect 這類更通用方案替代,它們可協調多種批處理框架與雲資料倉庫。
-(實際上這只是一行,分成多行只是為了便於閱讀。)這一行中有很多資訊。為了解釋它,你需要了解日誌格式的定義,如下所示:
+雲計算已無處不在。批處理儲存層也正在從 HDFS、GlusterFS、CephFS 這類分散式檔案系統(DFS)向 S3 等物件儲存遷移。BigQuery、Snowflake 這類可伸縮雲資料倉庫,正在模糊“資料倉庫”和“批處理系統”之間的邊界。
-```bash
- $remote_addr - $remote_user [$time_local] "$request"
- $status $body_bytes_sent "$http_referer" "$http_user_agent"
-```
+為了建立直覺,本章先從單機 Unix 工具示例出發,再擴充套件到分散式多機處理。你會看到,分散式批處理框架在很多方面很像作業系統:它也有排程器和檔案系統。隨後我們會討論編寫批處理作業的幾種處理模型,最後給出常見應用場景。
-日誌的這一行表明在 UTC 時間的 2015 年 2 月 27 日 17 點 55 分 11 秒,伺服器從客戶端 IP 地址 `216.58.210.78` 接收到對檔案 `/css/typography.css` 的請求。使用者沒有認證,所以 `$remote_user` 被設定為連字元(`-`)。響應狀態是 200(即請求成功),響應的大小是 3377 位元組。網頁瀏覽器是 Chrome 40,它載入了這個檔案是因為該檔案在網址為 `http://martin.kleppmann.com/` 的頁面中被引用到了。
+## 使用 Unix 工具的批處理 {#sec_batch_unix}
+假設你有一臺 Web 伺服器,每處理一個請求就在日誌檔案末尾追加一行。例如,使用 nginx 預設訪問日誌格式,一行可能像這樣:
-### 簡單日誌分析
+ 216.58.210.78 - - [27/Jun/2025:17:55:11 +0000] "GET /css/typography.css HTTP/1.1"
+ 200 3377 "https://martin.kleppmann.com/" "Mozilla/5.0 (Macintosh; Intel Mac OS X
+ 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
-很多工具可以從這些日誌檔案生成關於網站流量的漂亮的報告,但為了練手,讓我們使用基本的 Unix 功能建立自己的工具。例如,假設你想在你的網站上找到五個最受歡迎的網頁。則可以在 Unix shell 中這樣做:[^i]
+(實際上這是一行,這裡為了閱讀方便換了行。)這一行包含了很多資訊。要正確解釋它,你需要日誌格式定義:
-[^i]: 有些人認為 `cat` 這裡並沒有必要,因為輸入檔案可以直接作為 awk 的引數。但這種寫法讓線性管道更為顯眼。
+ $remote_addr - $remote_user [$time_local] "$request"
+ $status $body_bytes_sent "$http_referer" "$http_user_agent"
+
+這表示:UTC 時間 2025 年 6 月 27 日 17:55:11,伺服器收到來自客戶端 IP `216.58.210.78` 對 `/css/typography.css` 的請求。使用者未認證,因此 `$remote_user` 是連字元(`-`)。響應狀態碼是 200(成功),響應體大小 3,377 位元組。瀏覽器是 Chrome 137,該檔案是從頁面 *[*https://martin.kleppmann.com/*](https://martin.kleppmann.com/)* 引用而來。
+
+看起來“解析日誌”有點樸素,但它在現代科技公司裡是核心能力之一,從廣告流水線到支付處理都大量依賴。事實上,這也是 MapReduce 與“大資料”浪潮快速興起的重要推動力。
+
+### 簡單日誌分析 {#sec_batch_log_analysis}
+
+很多工具都能從日誌生成漂亮的網站流量報告。這裡為了練手,我們只用基礎 Unix 工具自己做一個。比如你想找出網站最受歡迎的五個頁面,可以在 shell 中這樣做:
```bash
cat /var/log/nginx/access.log | #1
@@ -75,727 +76,494 @@ cat /var/log/nginx/access.log | #1
head -n 5 #6
```
-1. 讀取日誌檔案
-2. 將每一行按空格分割成不同的欄位,每行只輸出第七個欄位,恰好是請求的 URL。在我們的例子中是 `/css/typography.css`。
-3. 按字母順序排列請求的 URL 列表。如果某個 URL 被請求過 n 次,那麼排序後,檔案將包含連續重複出現 n 次的該 URL。
-4. `uniq` 命令透過檢查兩個相鄰的行是否相同來過濾掉輸入中的重複行。`-c` 則表示還要輸出一個計數器:對於每個不同的 URL,它會報告輸入中出現該 URL 的次數。
-5. 第二種排序按每行起始處的數字(`-n`)排序,這是 URL 的請求次數。然後逆序(`-r`)返回結果,大的數字在前。
-6. 最後,只輸出前五行(`-n 5`),並丟棄其餘的。該系列命令的輸出如下所示:
+1. 讀取日誌檔案。(嚴格說這裡不需要 `cat`,可直接把檔案作為 `awk` 引數;但這樣寫更直觀看出線性管道。)
+2. 以空白字元切分每行,只輸出第 7 個欄位,也就是請求 URL。上面的樣例中是 `/css/typography.css`。
+3. 按字典序對 URL 排序。某個 URL 若出現 *n* 次,排序後會連續出現 *n* 行。
+4. `uniq` 透過比較相鄰兩行是否相同來去重。`-c` 讓它輸出計數:每個不同 URL 出現了多少次。
+5. 第二次 `sort` 按每行開頭的數字(`-n`)排序,並用 `-r` 逆序,出現次數最多的排在最前。
+6. `head` 只保留前 5 行(`-n 5`),丟棄其餘。
-```bash
+輸出大致如下:
+
+```
4189 /favicon.ico
- 3631 /2013/05/24/improving-security-of-ssh-private-keys.html
- 2124 /2012/12/05/schema-evolution-in-avro-protocol-buffers-thrift.html
+ 3631 /2016/02/08/how-to-do-distributed-locking.html
+ 2124 /2020/11/18/distributed-systems-and-elliptic-curves.html
1369 /
915 /css/typography.css
```
-如果你不熟悉 Unix 工具,上面的命令列可能看起來有點吃力,但是它非常強大。它能在幾秒鐘內處理幾 GB 的日誌檔案,並且你可以根據需要輕鬆修改命令。例如,如果要從報告中省略 CSS 檔案,可以將 awk 引數更改為 `'$7 !~ /\.css$/ {print $7}'`, 如果想統計最多的客戶端 IP 地址,可以把 awk 引數改為 `'{print $1}'`,等等。
+如果你不熟悉 Unix 工具,這條命令看起來可能有點晦澀,但它威力很強。它能在幾秒內處理 GB 級日誌,而且修改分析邏輯也非常方便:例如要排除 CSS 檔案,可把 `awk` 引數改成 `'$7 !~ /\.css$/ {print $7}'`;若要統計訪問最多的客戶端 IP,把 `awk` 引數改成 `'{print $1}'` 即可。
-我們不會在這裡詳細探索 Unix 工具,但是它非常值得學習。令人驚訝的是,使用 awk、sed、grep、sort、uniq 和 xargs 的組合,可以在幾分鐘內完成許多資料分析,並且它們的效能相當的好【8】。
+本書篇幅有限,無法展開講 Unix 工具,但它們非常值得學。令人驚訝的是,僅靠 `awk`、`sed`、`grep`、`sort`、`uniq`、`xargs` 的組合,就能在幾分鐘內做出很多資料分析,並且效能相當好 [^8]。
-#### 命令鏈與自定義程式
+### 命令鏈與自定義程式 {#sec_batch_custom_program}
-除了 Unix 命令鏈,你還可以寫一個簡單的程式來做同樣的事情。例如在 Ruby 中,它可能看起來像這樣:
+你也可以不用 Unix 管道,而寫個小程式完成同樣的事。比如用 Python:
-```ruby
-counts = Hash.new(0) # 1
-File.open('/var/log/nginx/access.log') do |file|
- file.each do |line|
- url = line.split[6] # 2
- counts[url] += 1 # 3
- end
-end
+```python
+from collections import defaultdict
-top5 = counts.map{|url, count| [count, url] }.sort.reverse[0...5] # 4
-top5.each{|count, url| puts "#{count} #{url}" } # 5
+counts = defaultdict(int) #1
+
+with open('/var/log/nginx/access.log', 'r') as file:
+ for line in file:
+ url = line.split()[6] #2
+ counts[url] += 1 #3
+
+top5 = sorted(((count, url) for url, count in counts.items()), reverse=True)[:5] #4
+
+for count, url in top5: #5
+ print(f"{count} {url}")
```
-1. `counts` 是一個儲存計數器的雜湊表,儲存了每個 URL 被瀏覽的次數,預設為 0。
-2. 逐行讀取日誌,抽取每行第七個被空格分隔的欄位為 URL(這裡的陣列索引是 6,因為 Ruby 的陣列索引從 0 開始計數)
-3. 將日誌當前行中 URL 對應的計數器值加一。
-4. 按計數器值(降序)對雜湊表內容進行排序,並取前五位。
-5. 打印出前五個條目。
+1. `counts` 是散列表,記錄每個 URL 出現次數,預設值為 0。
+2. 每行按空白字元切分,取第 7 個欄位作為 URL(Python 陣列從 0 開始,所以索引是 6)。
+3. 當前行對應 URL 的計數器加一。
+4. 按計數降序排序,取前五項。
+5. 列印前五項。
-這個程式並不像 Unix 管道那樣簡潔,但是它的可讀性很強,喜歡哪一種屬於口味的問題。但兩者除了表面上的差異之外,執行流程也有很大差異,如果你在大檔案上執行此分析,則會變得明顯。
+這個程式不如 Unix 管道簡潔,但可讀性也不錯,偏好取決於習慣。不過兩者除了語法差異,執行流程也很不一樣;在大檔案上執行時,這種差異會很明顯。
-#### 排序 VS 記憶體中的聚合
+### 排序與記憶體聚合 {#id275}
-Ruby 指令碼在記憶體中儲存了一個 URL 的雜湊表,將每個 URL 對映到它出現的次數。Unix 管道沒有這樣的雜湊表,而是依賴於對 URL 列表的排序,在這個 URL 列表中,同一個 URL 的只是簡單地重複出現。
+Python 指令碼在記憶體裡維護了一個“URL -> 出現次數”的散列表。Unix 管道示例沒有這種散列表,而是透過排序把同一 URL 的多次出現排到一起。
-哪種方法更好?這取決於你有多少個不同的 URL。對於大多數中小型網站,你可能可以為所有不同網址提供一個計數器(假設我們使用 1GB 記憶體)。在此例中,作業的 **工作集**(working set,即作業需要隨機訪問的記憶體大小)僅取決於不同 URL 的數量:如果日誌中只有單個 URL,重複出現一百萬次,則散列表所需的空間表就只有一個 URL 加上一個計數器的大小。當工作集足夠小時,記憶體散列表表現良好,甚至在效能較差的筆記型電腦上也可以正常工作。
+哪種方法更好?取決於不同 URL 的數量。對多數中小網站而言,通常可以把所有不同 URL 及其計數器放進(比如)1GB 記憶體。這個作業的 *工作集(working set)*(需要隨機訪問的記憶體規模)只取決於不同 URL 的個數:即便一百萬條日誌都指向同一 URL,散列表也只存一個 URL 和一個計數器。工作集足夠小時,記憶體散列表很好用,筆記本都能跑。
-另一方面,如果作業的工作集大於可用記憶體,則排序方法的優點是可以高效地使用磁碟。這與我們在 “[SSTables 和 LSM 樹](/tw/ch3#SSTables和LSM樹)” 中討論過的原理是一樣的:資料塊可以在記憶體中排序並作為段檔案寫入磁碟,然後多個排序好的段可以合併為一個更大的排序檔案。歸併排序具有在磁碟上執行良好的順序訪問模式。(請記住,針對順序 I/O 進行最佳化是 [第三章](/tw/ch3) 中反覆出現的主題,相同的模式在此重現)
+但如果工作集大於可用記憶體,排序法就有優勢:它能高效使用磁碟。這與[“日誌結構儲存”](/tw/ch4#sec_storage_log_structured)中的原理一樣:先在記憶體對資料塊排序並寫成段檔案,再把多個有序段合併成更大的有序檔案。歸併排序的順序訪問模式對磁碟很友好(見[“SSD 上的順序寫與隨機寫”](/tw/ch4#sidebar_sequential))。
-GNU Coreutils(Linux)中的 `sort` 程式透過溢位至磁碟的方式來自動應對大於記憶體的資料集,並能同時使用多個 CPU 核進行並行排序【9】。這意味著我們之前看到的簡單的 Unix 命令鏈很容易伸縮至大資料集,且不會耗盡記憶體。瓶頸可能是從磁碟讀取輸入檔案的速度。
+GNU Coreutils(Linux)中的 `sort` 能自動把超記憶體資料溢寫到磁碟,並自動利用多核並行排序 [^9]。這意味著前面的 Unix 命令鏈可以自然擴充套件到大資料集而不耗盡記憶體,瓶頸通常變成磁碟讀取輸入檔案的速率。
+Unix 工具的一個侷限是它們只在單機執行。當資料大到單機記憶體或本地磁碟都放不下時,就需要分散式批處理框架。
-### Unix哲學
+## 分散式系統中的批處理 {#sec_batch_distributed}
-我們可以非常容易地使用前一個例子中的一系列命令來分析日誌檔案,這並非巧合:事實上,這實際上是 Unix 的關鍵設計思想之一,而且它直至今天也仍然令人訝異地重要。讓我們更深入地研究一下,以便從 Unix 中借鑑一些想法【10】。
+在前面的 Unix 示例中,單機有幾個協同元件在處理日誌:
-Unix 管道的發明者道格・麥克羅伊(Doug McIlroy)在 1964 年首先描述了這種情況【11】:“我們需要一種類似園藝膠管的方式來拼接程式 —— 當我們需要將訊息從一個程式傳遞另一個程式時,直接接上去就行。I/O 應該也按照這種方式進行 ”。水管的類比仍然在生效,透過管道連線程式的想法成為了現在被稱為 **Unix 哲學** 的一部分 —— 這一組設計原則在 Unix 使用者與開發者之間流行起來,該哲學在 1978 年表述如下【12,13】:
+- 透過作業系統檔案系統介面訪問的儲存裝置。
+- 決定程序何時執行、如何分配 CPU 資源的排程器。
+- 一串透過管道把 `stdin`/`stdout` 連線起來的 Unix 程式。
-1. 讓每個程式都做好一件事。要做一件新的工作,寫一個新程式,而不是透過新增 “功能” 讓老程式複雜化。
-2. 期待每個程式的輸出成為另一個程式的輸入。不要將無關資訊混入輸出。避免使用嚴格的列資料或二進位制輸入格式。不要堅持互動式輸入。
-3. 設計和構建軟體時,即使是作業系統,也讓它們能夠儘早地被試用,最好在幾周內完成。不要猶豫,扔掉笨拙的部分,重建它們。
-4. 優先使用工具來減輕程式設計任務,即使必須繞道去編寫工具,且在用完後很可能要扔掉大部分。
+分散式批處理框架也有對應元件。某種意義上,你可以把分散式處理框架看成“分散式作業系統”:它有檔案系統、有任務排程器,還有透過檔案系統或其他通道互相傳遞資料的程式。
-這種方法 —— 自動化,快速原型設計,增量式迭代,對實驗友好,將大型專案分解成可管理的塊 —— 聽起來非常像今天的敏捷開發和 DevOps 運動。奇怪的是,四十年來變化不大。
+### 分散式檔案系統 {#sec_batch_dfs}
-`sort` 工具是一個很好的例子。可以說它比大多數程式語言標準庫中的實現(它們不會利用磁碟或使用多執行緒,即使這樣做有很大好處)要更好。然而,單獨使用 `sort` 幾乎沒什麼用。它只能與其他 Unix 工具(如 `uniq`)結合使用。
+作業系統提供的檔案系統由多層組成:
-像 `bash` 這樣的 Unix shell 可以讓我們輕鬆地將這些小程式組合成令人訝異的強大資料處理任務。儘管這些程式中有很多是由不同人群編寫的,但它們可以靈活地結合在一起。Unix 如何實現這種可組合性?
+- 最底層是塊裝置驅動,直接與磁碟互動,向上層提供原始塊讀寫。
+- 塊層之上是頁快取,快取最近訪問塊以提升讀取速度。
+- 塊 API 之上是檔案系統層,負責把大檔案切塊,並維護 inode、目錄、檔案等元資料。Linux 常見實現如 ext4、XFS。
+- 最上層,作業系統透過統一 API(虛擬檔案系統,VFS)嚮應用暴露不同檔案系統,讓應用以統一方式讀寫底層不同實現。
-#### 統一的介面
+分散式檔案系統(DFS)工作方式很類似:檔案被切成塊並分散到多臺機器。DFS 的塊通常比本地檔案系統大得多:HDFS 預設 128MB,JuiceFS 和許多物件儲存常用 4MB,而 ext4 預設塊通常是 4096 位元組。塊越大,需要維護的元資料越少,這對 PB 級資料非常關鍵;同時尋道開銷佔比也更低。
-如果你希望一個程式的輸出成為另一個程式的輸入,那意味著這些程式必須使用相同的資料格式 —— 換句話說,一個相容的介面。如果你希望能夠將任何程式的輸出連線到任何程式的輸入,那意味著所有程式必須使用相同的 I/O 介面。
+大多數物理儲存裝置不能做“部分塊寫入”,即使資料不足一個塊也得寫滿塊。DFS 的塊更大且通常構建在作業系統檔案系統之上,因此一般沒有這個約束。比如一個 900MB 檔案在 128MB 分塊下,會有 7 個 128MB 塊和 1 個 4MB 塊。
-在 Unix 中,這種介面是一個 **檔案**(file,更準確地說,是一個檔案描述符)。一個檔案只是一串有序的位元組序列。因為這是一個非常簡單的介面,所以可以使用相同的介面來表示許多不同的東西:檔案系統上的真實檔案,到另一個程序(Unix 套接字,stdin,stdout)的通訊通道,裝置驅動程式(比如 `/dev/audio` 或 `/dev/lp0`),表示 TCP 連線的套接字,等等。很容易將這些設計視為理所當然的,但實際上能讓這些差異巨大的東西共享一個統一的介面是非常厲害的,這使得它們可以很容易地連線在一起 [^ii]。
+讀取 DFS 塊需要透過網路請求到持有該塊的叢集節點。每臺機器都執行守護程序,對外提供 API,使遠端程序能把本地檔案系統中的塊當作檔案讀寫。HDFS 把這些守護程序叫 DataNode,GlusterFS 叫 glusterfsd。後文統稱 *資料節點(data node)*。
-[^ii]: 統一介面的另一個例子是 URL 和 HTTP,這是 Web 的基石。一個 URL 標識一個網站上的一個特定的東西(資源),你可以連結到任何其他網站的任何網址。具有網路瀏覽器的使用者因此可以透過跟隨連結在網站之間無縫跳轉,即使伺服器可能由完全不相關的組織維護。這個原則現在似乎非常明顯,但它卻是網路取能取得今天成就的關鍵。之前的系統並不是那麼統一:例如,在公告板系統(BBS)時代,每個系統都有自己的電話號碼和波特率配置。從一個 BBS 到另一個 BBS 的引用必須以電話號碼和調變解調器設定的形式;使用者將不得不掛斷,撥打其他 BBS,然後手動找到他們正在尋找的資訊。直接連結到另一個 BBS 內的一些內容當時是不可能的。
+DFS 也實現了“分散式版本”的頁快取。因為 DFS 塊作為檔案存放在資料節點本地,讀寫會經過資料節點作業系統,自帶記憶體頁快取,熱門塊會被快取在記憶體中。某些 DFS 還提供更多快取層,例如 JuiceFS 的客戶端快取和本地磁碟快取。
-按照慣例,許多(但不是全部)Unix 程式將這個位元組序列視為 ASCII 文字。我們的日誌分析示例使用了這個事實:`awk`、`sort`、`uniq` 和 `head` 都將它們的輸入檔案視為由 `\n`(換行符,ASCII `0x0A`)字元分隔的記錄列表。`\n` 的選擇是任意的 —— 可以說,ASCII 記錄分隔符 `0x1E` 本來就是一個更好的選擇,因為它是為了這個目的而設計的【14】,但是無論如何,所有這些程式都使用相同的記錄分隔符允許它們互操作。
+像 ext4/XFS 這樣的檔案系統會維護空閒空間、塊位置、目錄結構、許可權等元資料。DFS 同樣需要記錄“檔案塊分佈在哪些機器”“許可權如何”等資訊。Hadoop 使用 NameNode 維護叢集元資料;DeepSeek 的 3FS 使用元資料服務並把元資料持久化到 FoundationDB 之類鍵值儲存。
-每條記錄(即一行輸入)的解析則更加模糊。Unix 工具通常透過空白或製表符將行分割成欄位,但也使用 CSV(逗號分隔),管道分隔和其他編碼。即使像 `xargs` 這樣一個相當簡單的工具也有六個命令列選項,用於指定如何解析輸入。
+在檔案系統之上是 VFS。批處理系統裡最接近它的是 DFS 協議:批處理框架需要透過協議/介面來讀寫儲存。只要實現協議,就能作為可插拔儲存接入。例如 S3 API 已被 MinIO、Cloudflare R2、Tigris、Backblaze B2 等大量系統相容支援。具備 S3 支援的批處理系統通常可直接使用這些儲存。
-ASCII 文字的統一介面大多數時候都能工作,但它不是很優雅:我們的日誌分析示例使用 `{print $7}` 來提取網址,這樣可讀性不是很好。在理想的世界中可能是 `{print $request_url}` 或類似的東西。我們稍後會回顧這個想法。
+有些 DFS 還提供 POSIX 相容檔案系統,讓作業系統 VFS 把它當普通檔案系統。常見整合方式是 FUSE 或 NFS 協議。NFS 可能是最知名分散式檔案系統協議,最初用於讓多個客戶端讀寫單個伺服器上的資料。後來 AWS EFS、Archil 等提供了更可伸縮的 NFS 相容實現。NFS 客戶端雖仍連到一個端點,但底層會與分散式元資料服務和資料節點互動完成讀寫。
-儘管幾十年後還不夠完美,但統一的 Unix 介面仍然是非常出色的設計。沒有多少軟體能像 Unix 工具一樣互動組合的這麼好:你不能透過自定義分析工具輕鬆地將電子郵件帳戶的內容和線上購物歷史記錄以管道傳送至電子表格中,並將結果釋出到社交網路或維基。今天,像 Unix 工具一樣流暢地執行程式是一種例外,而不是規範。
+> [!TIP] 分散式檔案系統與網路儲存
+> 分散式檔案系統基於 *無共享(shared-nothing)* 原則(見[“共享記憶體、共享磁碟與無共享架構”](/tw/ch2#sec_introduction_shared_nothing)),與 NAS(網路附加儲存)和 SAN(儲存區域網路)等 *共享磁碟* 方案形成對照。共享磁碟通常依賴集中式儲存裝置、定製硬體和專用網路(如光纖通道);無共享方案不要求專用硬體,只需普通資料中心網路互聯的機器。
-即使是具有 **相同資料模型** 的資料庫,將資料從一種資料庫匯出再匯入到另一種資料庫也並不容易。缺乏整合導致了資料的 **巴爾幹化**[^譯註i]。
+很多 DFS 構建在商用硬體上,成本更低但故障率高於企業級專用硬體。為容忍機器和磁碟故障,檔案塊通常複製到多臺機器。這也讓排程器更容易均衡負載:任務可在任一持有輸入副本的節點執行。複製可以是多副本(見[第六章](/tw/ch6#ch_replication)),也可以是 Reed-Solomon 等 *糾刪碼* 方案,以更低儲存開銷恢復丟失資料 [^10] [^11] [^12]。這與 RAID 思想類似,只是 RAID 面向同一機器上的多塊磁碟,而 DFS 是透過普通資料中心網路跨機器做訪問和複製。
-[^譯註i]: **巴爾幹化(Balkanization)** 是一個常帶有貶義的地緣政治學術語,其定義為:一個國家或政區分裂成多個互相敵對的國家或政區的過程。
+### 物件儲存 {#id277}
+Amazon S3、Google Cloud Storage、Azure Blob Storage、OpenStack Swift 等物件儲存,已成為批處理場景中對 DFS 的主流替代。實際上兩者邊界越來越模糊:正如前一節和[“由物件儲存支撐的資料庫”](/tw/ch6#sec_replication_object_storage)所述,FUSE 可以把 S3 這類物件儲存“掛載成檔案系統”;JuiceFS、Ceph 等系統也同時提供物件 API 與檔案系統 API。但這些介面、效能、以及一致性保證差異很大,即便 API 看似相容,也需要仔細驗證行為是否符合預期。
-#### 邏輯與佈線相分離
+物件儲存中的每個物件有一個 URL,例如 `s3://my-photo-bucket/2025/04/01/birthday.png`。其中主機部分(`my-photo-bucket`)是 bucket 名,後半部分是物件 *鍵(key)*(示例裡是 `/2025/04/01/birthday.png`)。bucket 名全域性唯一;物件鍵在 bucket 內必須唯一。
-Unix 工具的另一個特點是使用標準輸入(`stdin`)和標準輸出(`stdout`)。如果你執行一個程式,而不指定任何其他的東西,標準輸入來自鍵盤,標準輸出指向螢幕。但是,你也可以從檔案輸入和 / 或將輸出重定向到檔案。管道允許你將一個程序的標準輸出附加到另一個程序的標準輸入(有個小記憶體緩衝區,而不需要將整個中間資料流寫入磁碟)。
+物件讀取用 `get`,寫入用 `put`。與檔案系統檔案不同,物件寫入後通常不可變;更新物件需要透過 `put` 全量重寫,類似鍵值儲存。Azure Blob Storage 和 S3 Express One Zone 支援追加,但多數物件儲存不支援。它也沒有 `fopen`、`fseek` 這類檔案控制代碼 API。
-如果需要,程式仍然可以直接讀取和寫入檔案,但 Unix 方法在程式不關心特定的檔案路徑、只使用標準輸入和標準輸出時效果最好。這允許 shell 使用者以任何他們想要的方式連線輸入和輸出;該程式不知道或不關心輸入來自哪裡以及輸出到哪裡。(人們可以說這是一種 **松耦合(loose coupling)**,**晚期繫結(late binding)**【15】或 **控制反轉(inversion of control)**【16】)。將輸入 / 輸出佈線與程式邏輯分開,可以將小工具組合成更大的系統。
+物件看起來像按目錄組織,這很容易讓人誤解:物件儲存並沒有真正目錄概念。所謂路徑只是約定,斜槓也是 key 的一部分。這個約定允許你按字首列出物件,類似“目錄列表”,但與檔案系統目錄列舉有兩點不同:
-你甚至可以編寫自己的程式,並將它們與作業系統提供的工具組合在一起。你的程式只需要從標準輸入讀取輸入,並將輸出寫入標準輸出,它就可以加入資料處理的管道中。在日誌分析示例中,你可以編寫一個將 User-Agent 字串轉換為更靈敏的瀏覽器識別符號,或者將 IP 地址轉換為國家程式碼的工具,並將其插入管道。`sort` 程式並不關心它是否與作業系統的另一部分或者你寫的程式通訊。
+- 字首 `list` 行為更像 Unix 的遞迴 `ls -R`:會返回所有以該字首開頭的物件,包括“子路徑”下的物件。
+- 不存在“空目錄”。如果你刪除了 `s3://my-photo-bucket/2025/04/01` 下所有物件,再列 `s3://my-photo-bucket/2025/04` 時就看不到 `01`。常見做法是建立 0 位元組物件表示空目錄(如建立空物件 `s3://my-photo-bucket/2025/04/01` 以保留目錄佔位)。
-但是,使用 `stdin` 和 `stdout` 能做的事情是有限的。需要多個輸入或輸出的程式雖然可能,卻非常棘手。你沒法將程式的輸出管道連線至網路連線中【17,18】[^iii] 。如果程式直接開啟檔案進行讀取和寫入,或者將另一個程式作為子程序啟動,或者開啟網路連線,那麼 I/O 的佈線就取決於程式本身了。它仍然可以被配置(例如透過命令列選項),但在 Shell 中對輸入和輸出進行佈線的靈活性就少了。
+DFS 常支援硬連結、符號連結、檔案鎖、原子重新命名等檔案系統操作,而物件儲存通常缺失這些能力:連結和鎖大多不支援;重新命名也非原子,通常是“複製到新 key,再刪除舊 key”。若要“重新命名目錄”,因為目錄名是 key 的一部分,實際上要逐個物件重新命名。
-[^iii]: 除了使用一個單獨的工具,如 `netcat` 或 `curl`。Unix 起初試圖將所有東西都表示為檔案,但是 BSD 套接字 API 偏離了這個慣例【17】。研究用作業系統 Plan 9 和 Inferno 在使用檔案方面更加一致:它們將 TCP 連線表示為 `/net/tcp` 中的檔案【18】。
+[第四章](/tw/ch4#ch_storage)討論的鍵值儲存通常面向小值(通常 KB 級)和高頻低延遲讀寫。相比之下,DFS 和物件儲存通常最佳化的是大物件(MB 到 GB)和低頻大塊讀寫。不過近年物件儲存也在增強小物件高頻訪問能力,例如 S3 Express One Zone 已提供單毫秒級延遲,計費模型也更接近鍵值儲存。
+DFS 與物件儲存另一個區別是:HDFS 等 DFS 可把計算任務排程到持有檔案副本的機器上,讓任務本地讀檔案,減少網路傳輸(當任務程式碼遠小於待讀檔案時尤其划算)。物件儲存通常把儲存和計算解耦,雖然可能用更多頻寬,但現代資料中心網路很快,通常可接受。同時這種解耦讓 CPU/記憶體與儲存容量可以獨立擴充套件。
-#### 透明度和實驗
+### 分散式作業編排 {#id278}
-使 Unix 工具如此成功的部分原因是,它們使檢視正在發生的事情變得非常容易:
+前面的“作業系統類比”同樣適用於作業編排。在單機上跑 Unix 批處理任務時,總得有東西真正去執行 `awk`、`sort`、`uniq`、`head` 程序;需要把一個程序輸出送到另一個程序輸入;要給每個程序分配記憶體;公平排程 CPU 指令;隔離記憶體與 I/O 邊界,等等。單機裡這由作業系統核心負責;分散式環境裡,這就是作業編排器(orchestrator)的職責。
-- Unix 命令的輸入檔案通常被視為不可變的。這意味著你可以隨意執行命令,嘗試各種命令列選項,而不會損壞輸入檔案。
-- 你可以在任何時候結束管道,將管道輸出到 `less`,然後檢視它是否具有預期的形式。這種檢查能力對除錯非常有用。
-- 你可以將一個流水線階段的輸出寫入檔案,並將該檔案用作下一階段的輸入。這使你可以重新啟動後面的階段,而無需重新執行整個管道。
+批處理框架會向編排器的排程器發起“執行作業”請求。請求通常包含如下元資料:
-因此,與關係資料庫的查詢最佳化器相比,即使 Unix 工具非常簡單,但仍然非常有用,特別是對於實驗而言。
+- 需要執行的任務數量;
+- 每個任務所需記憶體、CPU、磁碟;
+- 作業識別符號;
+- 訪問憑據;
+- 輸入輸出等作業引數;
+- 所需硬體資訊(如 GPU、磁碟型別);
+- 作業可執行程式碼的位置。
-然而,Unix 工具的最大侷限在於它們只能在一臺機器上執行 —— 而 Hadoop 這樣的工具即應運而生。
+Kubernetes、Hadoop YARN(Yet Another Resource Negotiator)[^13] 等編排器會結合這些請求與叢集狀態,依靠以下元件執行任務:
+任務執行器(Task executors)
-## MapReduce和分散式檔案系統
+: 每個節點上執行執行器守護程序,例如 YARN 的 *NodeManager* 或 Kubernetes 的 *kubelet*。執行器負責拉起任務、透過心跳上報存活狀態、跟蹤節點上的任務狀態與資源佔用。收到“啟動任務”請求後,執行器會獲取作業程式碼並執行啟動命令;隨後持續監控程序直至結束或失敗,並更新對應狀態元資料。
-MapReduce 有點像 Unix 工具,但分佈在數千臺機器上。像 Unix 工具一樣,它相當簡單粗暴,但令人驚異地管用。一個 MapReduce 作業可以和一個 Unix 程序相類比:它接受一個或多個輸入,併產生一個或多個輸出。
+ 很多執行器還配合作業系統實現安全與效能隔離,例如 YARN 和 Kubernetes 都會使用 Linux *cgroups*。這樣可防止任務越權訪問資料,或因資源濫用影響同機其他任務。
-和大多數 Unix 工具一樣,執行 MapReduce 作業通常不會修改輸入,除了生成輸出外沒有任何副作用。輸出檔案以連續的方式一次性寫入(一旦寫入檔案,不會修改任何現有的檔案部分)。
+資源管理器(Resource Manager)
-雖然 Unix 工具使用 `stdin` 和 `stdout` 作為輸入和輸出,但 MapReduce 作業在分散式檔案系統上讀寫檔案。在 Hadoop 的 MapReduce 實現中,該檔案系統被稱為 **HDFS(Hadoop 分散式檔案系統)**,一個 Google 檔案系統(GFS)的開源實現【19】。
+: 資源管理器維護各節點元資料:可用硬體(CPU、GPU、記憶體、磁碟等)、任務狀態、網路位置、節點健康狀態等,從而形成全域性檢視。其中心化特性可能成為可用性和可伸縮性瓶頸。YARN 藉助 ZooKeeper,Kubernetes 藉助 etcd 儲存叢集狀態(見[“協調服務”](/tw/ch10#sec_consistency_coordination))。
-除 HDFS 外,還有各種其他分散式檔案系統,如 GlusterFS 和 Quantcast File System(QFS)【20】。諸如 Amazon S3、Azure Blob 儲存和 OpenStack Swift【21】等物件儲存服務在很多方面都是相似的 [^iv]。在本章中,我們將主要使用 HDFS 作為示例,但是這些原則適用於任何分散式檔案系統。
+排程器(Scheduler)
-[^iv]: 一個不同之處在於,對於 HDFS,可以將計算任務安排在儲存特定檔案副本的計算機上執行,而物件儲存通常將儲存和計算分開。如果網路頻寬是一個瓶頸,從本地磁碟讀取有效能優勢。但是請注意,如果使用糾刪碼(Erasure Coding),則會丟失區域性,因為來自多臺機器的資料必須進行合併以重建原始檔案【20】。
+: 編排器通常包含中心化排程子系統,接收啟動/停止作業與狀態查詢請求。例如收到“啟動 10 個任務,使用指定 Docker 映象,且必須執行在某類 GPU 節點上”的請求後,排程器會基於請求和資源管理器狀態決定“哪些任務跑在哪些節點”,再通知執行器執行。
-與網路連線儲存(NAS)和儲存區域網路(SAN)架構的共享磁碟方法相比,HDFS 基於 **無共享** 原則(請參閱 [第二部分](/tw/part-ii) 的介紹)。共享磁碟儲存由集中式儲存裝置實現,通常使用定製硬體和專用網路基礎設施(如光纖通道)。而另一方面,無共享方法不需要特殊的硬體,只需要透過傳統資料中心網路連線的計算機。
+不同編排器命名各異,但幾乎都具備這些核心元件。
-HDFS 在每臺機器上運行了一個守護程序,它對外暴露網路服務,允許其他節點訪問儲存在該機器上的檔案(假設資料中心中的每臺通用計算機都掛載著一些磁碟)。名為 **NameNode** 的中央伺服器會跟蹤哪個檔案塊儲存在哪臺機器上。因此,HDFS 在概念上建立了一個大型檔案系統,可以使用所有執行有守護程序的機器的磁碟。
+> [!NOTE]
+> 有些排程決策需要“應用特定排程器”參與,才能考慮更具體的業務約束,例如當查詢量達到閾值時自動擴容只讀副本。中心排程器與應用排程器協同決定如何執行任務。YARN 把這類子排程器稱為 *ApplicationMaster*,Kubernetes 通常稱為 *operator*。
-為了容忍機器和磁碟故障,檔案塊被複制到多臺機器上。複製可能意味著多個機器上的相同資料的多個副本,如 [第五章](/tw/ch5) 中所述,或者諸如 Reed-Solomon 碼這樣的糾刪碼方案,它能以比完全複製更低的儲存開銷來支援恢復丟失的資料【20,22】。這些技術與 RAID 相似,後者可以在連線到同一臺機器的多個磁碟上提供冗餘;區別在於在分散式檔案系統中,檔案訪問和複製是在傳統的資料中心網路上完成的,沒有特殊的硬體。
+#### 資源分配 {#id279}
-HDFS 的可伸縮性已經很不錯了:在撰寫本書時,最大的 HDFS 部署執行在上萬臺機器上,總儲存容量達數百 PB【23】。如此大的規模已經變得可行,因為使用商品硬體和開源軟體的 HDFS 上的資料儲存和訪問成本遠低於在專用儲存裝置上支援同等容量的成本【24】。
+排程器在編排系統中最具挑戰的職責之一,就是在資源有限且作業需求衝突時,做出合理分配。它本質上是在公平與效率之間做平衡。
-### MapReduce作業執行
+假設一個小叢集有 5 個節點,共 160 個 CPU 核。排程器收到兩個作業請求,每個都想要 100 核。怎麼排最好?
-MapReduce 是一個程式設計框架,你可以使用它編寫程式碼來處理 HDFS 等分散式檔案系統中的大型資料集。理解它的最簡單方法是參考 “[簡單日誌分析](#簡單日誌分析)” 中的 Web 伺服器日誌分析示例。MapReduce 中的資料處理模式與此示例非常相似:
+- 可以給每個作業先分 80 個任務,剩餘 20 個等前面的任務結束後再啟動。
+- 也可以先跑完其中一個作業,再等 100 核都空出來後跑另一個。這叫 *gang scheduling*(成組排程)。
+- 如果一個請求先到,排程器還要決定是立即把 100 核都給它,還是為未來請求預留一部分資源。
-1. 讀取一組輸入檔案,並將其分解成 **記錄(records)**。在 Web 伺服器日誌示例中,每條記錄都是日誌中的一行(即 `\n` 是記錄分隔符)。
-2. 呼叫 Mapper 函式,從每條輸入記錄中提取一對鍵值。在前面的例子中,Mapper 函式是 `awk '{print $7}'`:它提取 URL(`$7`)作為鍵,並將值留空。
-3. 按鍵排序所有的鍵值對。在日誌的例子中,這由第一個 `sort` 命令完成。
-4. 呼叫 Reducer 函式遍歷排序後的鍵值對。如果同一個鍵出現多次,排序使它們在列表中相鄰,所以很容易組合這些值而不必在記憶體中保留很多狀態。在前面的例子中,Reducer 是由 `uniq -c` 命令實現的,該命令使用相同的鍵來統計相鄰記錄的數量。
+這是很簡化的例子,但已經能看到艱難權衡。以成組排程為例,如果排程器為了湊齊 100 核而長期預留資源,節點會閒置,資源利用率下降,若其他作業也在搶佔式預留,還可能死鎖。
-這四個步驟可以作為一個 MapReduce 作業執行。步驟 2(Map)和 4(Reduce)是你編寫自定義資料處理程式碼的地方。步驟 1(將檔案分解成記錄)由輸入格式解析器處理。步驟 3 中的排序步驟隱含在 MapReduce 中 —— 你不必編寫它,因為 Mapper 的輸出始終在送往 Reducer 之前進行排序。
+反過來,如果只是被動等 100 核“自然可用”,中間可能被別的作業拿走,導致長時間湊不齊,從而產生 *飢餓(starvation)*。排程器也可以 *搶佔(preempt)* 一部分先到作業任務,把它們殺掉給後到作業騰資源;但被殺任務之後還要重跑,整體效率同樣下降。
-要建立 MapReduce 作業,你需要實現兩個回撥函式,Mapper 和 Reducer,其行為如下(請參閱 “[MapReduce 查詢](/tw/ch2#MapReduce查詢)”):
+把這個問題放大到數百甚至數百萬個請求,想求全域性最優幾乎不可行。事實上這是 *NP-hard* 問題:除了很小規模,很難在可接受時間內算出最優解 [^14] [^15]。
+
+因此工程上排程器通常採用啟發式方法,在非最優前提下做“足夠好”的決策。常見演算法包括 FIFO、主導資源公平(DRF)、優先順序佇列、容量/配額排程、各種裝箱演算法等。細節超出本書範圍,但這是非常有趣的研究領域。
+
+#### 工作流排程 {#sec_batch_workflows}
+
+本章開頭的 Unix 示例是多個命令串聯。分散式批處理中同樣常見:一個作業輸出要成為一個或多個後續作業輸入,而每個作業又可能依賴多個上游輸入。這個依賴結構稱為 *工作流(workflow)* 或 *有向無環圖(DAG)*。
+
+> [!NOTE]
+> 我們在[“持久化執行與工作流”](/tw/ch5#sec_encoding_dataflow_workflows)中討論過“按步驟執行 RPC”的工作流引擎;在批處理語境裡,“工作流”指的是一串批處理過程:每一步讀輸入、產輸出,通常不直接對外做 RPC。持久化執行引擎通常單次請求處理的資料量小於批處理系統,但兩者邊界並非絕對。
+
+需要多作業工作流常見有以下原因:
+
+- 一個作業輸出可能被多個團隊維護的下游作業消費。此時先把輸出寫到公共位置更合理,下游可按“資料更新觸發”或定時方式執行。
+- 你可能要在多個處理工具間傳遞資料。比如 Spark 作業寫 HDFS,再由 Python 觸發 Trino SQL 查詢(見[“雲資料倉庫”](/tw/ch4#sec_cloud_data_warehouses))繼續處理並寫入 S3。
+- 有些流水線內部天然需要多階段。例如第一階段按某鍵分片,下一階段按另一鍵分片,那麼第一階段需要先產出符合第二階段要求的資料佈局。
+
+在 Unix 裡,管道用很小的記憶體緩衝連線前後命令,不落盤。若緩衝區滿,上游必須等待下游消費,這是一種 *背壓(backpressure)*。Spark、Flink 等批處理執行引擎也支援類似模式:一個任務輸出直接傳給下一任務(跨機時經網路傳輸)。
+
+但在工作流中,更常見仍是“上游作業寫 DFS/物件儲存,下游再讀”,這樣可讓作業在時間上解耦。若一個作業有多個輸入,工作流排程器通常會等待所有上游輸入生產成功後再啟動它。
+
+YARN ResourceManager 或 Spark 內建排程器主要做“作業內排程”,不負責整條工作流。為管理跨作業依賴,出現了 Airflow、Dagster、Prefect 等工作流排程器。它們在維護大量批作業時非常關鍵:包含 50~100 個作業的工作流並不罕見;大型組織內很多團隊會跨系統互相消費輸出。沒有工具支撐,很難管理這種複雜資料流。
+
+#### 故障處理 {#id281}
+
+批處理作業往往執行時間長。長時間執行且並行任務多的作業,在執行過程中遇到至少一次任務失敗幾乎是常態。正如[“硬體與軟體故障”](/tw/ch2#sec_introduction_hardware_faults)和[“不可靠網路”](/tw/ch9#sec_distributed_networks)所述,原因可能是硬體故障(商用硬體尤甚)、網路中斷等。
+
+任務無法完成的另一原因是被排程器主動搶佔(kill)。當系統有多優先順序佇列時,這很常見:低優先順序任務便宜、高優先順序任務昂貴。低優先順序任務可用空閒算力跑,但高優先順序任務一到就可能把它們搶佔掉。雲廠商的對應產品名分別是:AWS 的 *spot instances*、Azure 的 *spot virtual machines*、GCP 的 *preemptible instances* [^16]。
+
+批處理很多時候對即時性要求不高,因此很適合利用低優先順序資源/搶佔式例項降成本:本質上它在“吃”否則會閒置的算力,提高叢集利用率。但代價是更高的被殺機率:實際裡搶佔往往比硬體故障更常見 [^17]。
+
+由於批處理每次都從頭生成輸出,任務失敗比線上系統更容易處理:刪掉失敗任務的部分輸出,把任務重新排程到別的機器重跑即可。若只因一個任務失敗就重跑整個作業會非常浪費,因此 MapReduce 及其後繼系統都儘量讓並行任務彼此獨立,從而把重試粒度降到單個任務 [^3]。
+
+當一個任務輸出成為另一任務輸入(即在工作流內傳遞)時,容錯更複雜。MapReduce 的做法是:中間資料總是寫回 DFS,且只有寫入任務成功後才允許下游讀取。這個方案在頻繁搶佔環境中也能工作,但會帶來大量 DFS 寫入,效率不高。
+
+Spark 更傾向把中間資料放記憶體或溢寫本地磁碟,只把最終結果寫 DFS;它還記錄中間資料的計算血緣,丟失時可重算 [^18]。Flink 則採用定期檢查點快照機制 [^19]。我們會在[“資料流引擎”](/tw/ch11#sec_batch_dataflow)繼續討論。
+
+## 批處理模型 {#id431}
+
+前面我們討論了分散式環境中批作業如何排程。現在轉向“批處理框架如何處理資料”。最常見的兩類模型是 MapReduce 與資料流引擎。儘管實踐中資料流引擎已大面積替代 MapReduce,但理解 MapReduce 仍然重要,因為它深刻影響了現代批處理框架。
+
+MapReduce 與資料流引擎都發展出多種程式設計介面:低層 API、關係查詢語言、DataFrame API。它們讓應用工程師、資料分析工程師、業務分析師乃至非技術人員都能參與資料處理。我們將在[“批處理用例”](/tw/ch11#sec_batch_output)中討論這些用途。
+
+### MapReduce {#sec_batch_mapreduce}
+
+MapReduce 的處理模式與[“簡單日誌分析”](/tw/ch11#sec_batch_log_analysis)幾乎同構:
+
+1. 讀取輸入檔案並切分為 *記錄(records)*。在日誌例子裡,每條記錄就是一行(`\n` 為記錄分隔符)。在 Hadoop MapReduce 中,輸入通常存放在 HDFS 或 S3 等物件儲存,檔案格式可能是 Parquet(列式,見[“面向列儲存”](/tw/ch4#sec_storage_column))或 Avro(行式,見[“Avro”](/tw/ch5#sec_encoding_avro))。
+2. 呼叫 mapper,從每條輸入記錄中提取鍵和值。Unix 示例中 mapper 相當於 `awk '{print $7}'`:URL(`$7`)是鍵,值可留空。
+3. 按鍵排序所有鍵值對。日誌示例中這一步對應第一次 `sort`。
+4. 呼叫 reducer 遍歷排序後的鍵值對。同鍵記錄會相鄰,因此可以在很小記憶體狀態下合併。Unix 示例中 reducer 等價於 `uniq -c`,統計相鄰同鍵記錄數。
+
+這四步就是一個 MapReduce 作業。第 2 步(map)與第 4 步(reduce)是你寫業務邏輯的地方;第 1 步(檔案切記錄)由輸入格式解析器完成;第 3 步排序在 MapReduce 中是隱式內建的,你無需手寫。這一步是批處理的基礎演算法,我們會在[“混洗資料”](/tw/ch11#sec_shuffle)再討論。
+
+要建立 MapReduce 作業,你需實現兩個回撥:mapper 與 reducer,其行為如下。
Mapper
-: Mapper 會在每條輸入記錄上呼叫一次,其工作是從輸入記錄中提取鍵值。對於每個輸入,它可以生成任意數量的鍵值對(包括 None)。它不會保留從一個輸入記錄到下一個記錄的任何狀態,因此每個記錄都是獨立處理的。
+
+: 對每條輸入記錄呼叫一次。它從輸入記錄中提取鍵和值,並可為每條輸入產生任意數量鍵值對(包括 0 條)。它不保留跨記錄狀態,每條記錄獨立處理。
Reducer
-: MapReduce 框架拉取由 Mapper 生成的鍵值對,收集屬於同一個鍵的所有值,並在這組值上迭代呼叫 Reducer。Reducer 可以產生輸出記錄(例如相同 URL 的出現次數)。
-在 Web 伺服器日誌的例子中,我們在第 5 步中有第二個 `sort` 命令,它按請求數對 URL 進行排序。在 MapReduce 中,如果你需要第二個排序階段,則可以透過編寫第二個 MapReduce 作業並將第一個作業的輸出用作第二個作業的輸入來實現它。這樣看來,Mapper 的作用是將資料放入一個適合排序的表單中,並且 Reducer 的作用是處理已排序的資料。
+: 框架收集 mapper 產生的鍵值對,把同鍵值集合交給 reducer(以迭代器形式)。reducer 可輸出結果記錄(如同一 URL 的出現次數)。
-#### 分散式執行MapReduce
+在日誌示例裡,第 5 步還有一次 `sort` 用於按請求次數排名 URL。MapReduce 若要第二輪排序,通常要再寫一個作業:前一個輸出作為後一個輸入。換個角度看,mapper 的作用是把資料整理成適合排序的形態;reducer 的作用是處理已排序資料。
-MapReduce 與 Unix 命令管道的主要區別在於,MapReduce 可以在多臺機器上並行執行計算,而無需編寫程式碼來顯式處理並行問題。Mapper 和 Reducer 一次只能處理一條記錄;它們不需要知道它們的輸入來自哪裡,或者輸出去往什麼地方,所以框架可以處理在機器之間移動資料的複雜性。
+> [!TIP] MapReduce 與函數語言程式設計
+> MapReduce 雖用於批處理,但其程式設計模型來自函數語言程式設計。Lisp 把 *map* 與 *reduce/fold* 作為列表上的高階函式引入,後來進入 Python、Rust、Java 等主流語言。包括 SQL 在內的大量資料處理操作都可在 MapReduce 之上表達。Map 和 reduce 以及函數語言程式設計的一些特性恰好契合 MapReduce:可組合、天然適合資料處理鏈;map 還是典型“令人尷尬地並行”(每條輸入獨立處理);reduce 則可按不同鍵並行。
-在分散式計算中可以使用標準的 Unix 工具作為 Mapper 和 Reducer【25】,但更常見的是,它們被實現為傳統程式語言的函式。在 Hadoop MapReduce 中,Mapper 和 Reducer 都是實現特定介面的 Java 類。在 MongoDB 和 CouchDB 中,Mapper 和 Reducer 都是 JavaScript 函式(請參閱 “[MapReduce 查詢](/tw/ch2#MapReduce查詢)”)。
+但用原始 MapReduce API 寫複雜處理其實很費力,例如各種連線演算法都要自己實現 [^20]。MapReduce 相比現代批處理引擎也偏慢,一個重要原因是其“以檔案為中心”的 I/O 讓作業流水化困難:上游不結束,下游很難提前處理輸出。
-[圖 10-1](/v1/ddia_1001.png) 顯示了 Hadoop MapReduce 作業中的資料流。其並行化基於分割槽(請參閱 [第六章](/v1/ch6)):作業的輸入通常是 HDFS 中的一個目錄,輸入目錄中的每個檔案或檔案塊都被認為是一個單獨的分割槽,可以單獨處理 map 任務([圖 10-1](/v1/ddia_1001.png) 中的 m1,m2 和 m3 標記)。
+### 資料流引擎 {#sec_batch_dataflow}
-每個輸入檔案的大小通常是數百兆位元組。MapReduce 排程器(圖中未顯示)試圖在其中一臺儲存輸入檔案副本的機器上執行每個 Mapper,只要該機器有足夠的備用 RAM 和 CPU 資源來執行 Mapper 任務【26】。這個原則被稱為 **將計算放在資料附近**【27】:它節省了透過網路複製輸入檔案的開銷,減少網路負載並增加區域性。
+為解決 MapReduce 的侷限,出現了多種分散式批處理執行引擎,最著名的是 Spark [^18] [^21] 和 Flink [^19]。它們設計細節各異,但有一個共同點:把整條工作流當成一個作業處理,而不是拆成互相獨立的小作業。
-
+因為它們顯式建模了跨多個處理階段的資料流動,所以稱為 *資料流引擎(dataflow engines)*。與 MapReduce 一樣,它們提供低層 API(反覆呼叫使用者函式逐條處理記錄),也提供更高層運算元(如 *join*、*group by*)。它們透過分片並行輸入,並透過網路把一個任務輸出傳給另一個任務輸入。與 MapReduce 不同,運算元不必嚴格在 map/reduce 兩類角色間交替,而可以更靈活組合。
-**圖 10-1 具有三個 Mapper 和三個 Reducer 的 MapReduce 任務**
+這些 API 通常以關係風格構件表達計算:按欄位值連線資料集、按鍵分組、按條件過濾、按計數或求和等函式聚合。內部實現依賴的正是下一節要講的混洗演算法。
-在大多數情況下,應該在 Mapper 任務中執行的應用程式碼在將要執行它的機器上還不存在,所以 MapReduce 框架首先將程式碼(例如 Java 程式中的 JAR 檔案)複製到適當的機器。然後啟動 Map 任務並開始讀取輸入檔案,一次將一條記錄傳入 Mapper 回撥函式。Mapper 的輸出由鍵值對組成。
+這種處理引擎風格可追溯到 Dryad [^22]、Nephele [^23] 等研究系統。相比 MapReduce,它有幾個優勢:
-計算的 Reduce 端也被分割槽。雖然 Map 任務的數量由輸入檔案塊的數量決定,但 Reducer 的任務的數量是由作業作者配置的(它可以不同於 Map 任務的數量)。為了確保具有相同鍵的所有鍵值對最終落在相同的 Reducer 處,框架使用鍵的雜湊值來確定哪個 Reduce 任務應該接收到特定的鍵值對(請參閱 “[根據鍵的雜湊分割槽](/tw/ch6#根據鍵的雜湊分割槽)”)。
+- 像排序這類昂貴操作只在“確實需要”的地方執行,而不是每個 map 與 reduce 階段之間都預設做。
+- 連續多個不改變分片方式的運算元(如 map/filter)可融合成一個任務,減少資料複製開銷。
+- 由於工作流裡的連線與資料依賴都顯式宣告,排程器能全域性最佳化資料區域性。比如把“消費某資料”的任務放到“生產該資料”的同機上,用共享記憶體緩衝交換,而非走網路複製。
+- 運算元間中間狀態通常放記憶體或本地磁碟即可,比寫 DFS/物件儲存 I/O 更低(後者要多副本並落到多機磁碟)。MapReduce 僅對 mapper 輸出做了這類最佳化,資料流引擎把它推廣到所有中間狀態。
+- 輸入一就緒就能啟動下游運算元,無需等待整個上游階段全部完成。
+- 可複用已有程序執行新運算元,減少啟動開銷;MapReduce 往往為每個任務起一個新 JVM。
-鍵值對必須進行排序,但資料集可能太大,無法在單臺機器上使用常規排序演算法進行排序。相反,分類是分階段進行的。首先每個 Map 任務都按照 Reducer 對輸出進行分割槽。每個分割槽都被寫入 Mapper 程式的本地磁碟,使用的技術與我們在 “[SSTables 與 LSM 樹](/tw/ch3#SSTables和LSM樹)” 中討論的類似。
+因此,資料流引擎能實現與 MapReduce 工作流同樣的計算,但通常速度明顯更快。
-只要當 Mapper 讀取完輸入檔案,並寫完排序後的輸出檔案,MapReduce 排程器就會通知 Reducer 可以從該 Mapper 開始獲取輸出檔案。Reducer 連線到每個 Mapper,並下載自己相應分割槽的有序鍵值對檔案。按 Reducer 分割槽,排序,從 Mapper 向 Reducer 複製分割槽資料,這一整個過程被稱為 **混洗(shuffle)**【26】(一個容易混淆的術語 —— 不像洗牌,在 MapReduce 中的混洗沒有隨機性)。
+### 混洗資料 {#sec_shuffle}
-Reduce 任務從 Mapper 獲取檔案,並將它們合併在一起,並保留有序特性。因此,如果不同的 Mapper 生成了鍵相同的記錄,則在 Reducer 的輸入中,這些記錄將會相鄰。
+本章開頭的 Unix 工具示例和 MapReduce 都建立在排序之上。批處理系統要能排序 PB 級資料,單機放不下,因此必須使用“輸入與輸出都分片”的分散式排序演算法,這就是 *混洗(shuffle)*。
-Reducer 呼叫時會收到一個鍵,和一個迭代器作為引數,迭代器會順序地掃過所有具有該鍵的記錄(因為在某些情況可能無法完全放入記憶體中)。Reducer 可以使用任意邏輯來處理這些記錄,並且可以生成任意數量的輸出記錄。這些輸出記錄會寫入分散式檔案系統上的檔案中(通常是在跑 Reducer 的機器本地磁碟上留一份,並在其他機器上留幾份副本)。
+> [!NOTE] 混洗不是隨機
+> “shuffle” 容易引發誤解。洗牌會得到隨機順序;而這裡的 shuffle 產出的是排序後的確定順序,不含隨機性。
-#### MapReduce工作流
+混洗是批處理系統的基礎演算法,連線與聚合都依賴它。MapReduce、Spark、Flink、Daft、Dataflow、BigQuery [^24] 都實現了高可伸縮且高效能的混洗機制以處理大資料集。這裡用 Hadoop MapReduce 的混洗實現做說明 [^25],但核心思想在其他系統同樣適用。
-單個 MapReduce 作業可以解決的問題範圍很有限。以日誌分析為例,單個 MapReduce 作業可以確定每個 URL 的頁面瀏覽次數,但無法確定最常見的 URL,因為這需要第二輪排序。
+[圖 11-1](/tw/ch11#fig_batch_mapreduce) 展示了一個 MapReduce 作業的資料流。假設輸入已分片,標記為 *m1*、*m2*、*m3*。例如每個分片可以是 HDFS 中一個檔案,或物件儲存中的一個物件;同一資料集的所有分片可以放在同一 HDFS 目錄,或使用同一物件字首。
-因此將 MapReduce 作業連結成為 **工作流(workflow)** 中是極為常見的,例如,一個作業的輸出成為下一個作業的輸入。Hadoop MapReduce 框架對工作流沒有特殊支援,所以這個鏈是透過目錄名隱式實現的:第一個作業必須將其輸出配置為 HDFS 中的指定目錄,第二個作業必須將其輸入配置為從同一個目錄。從 MapReduce 框架的角度來看,這是兩個獨立的作業。
+{{< figure src="/fig/ddia_1101.png" id="fig_batch_mapreduce" caption="圖 11-1. 一個包含三個 mapper 和三個 reducer 的 MapReduce 作業。" class="w-full my-4" >}}
-因此,被連結的 MapReduce 作業並沒有那麼像 Unix 命令管道(它直接將一個程序的輸出作為另一個程序的輸入,僅用一個很小的記憶體緩衝區)。它更像是一系列命令,其中每個命令的輸出寫入臨時檔案,下一個命令從臨時檔案中讀取。這種設計有利也有弊,我們將在 “[物化中間狀態](#物化中間狀態)” 中討論。
+框架會為每個輸入分片啟動一個 map 任務。任務讀取分配到的檔案,並逐條記錄呼叫 mapper 回撥。reduce 側也會分片。map 任務數由輸入分片數決定;reduce 任務數由作業作者配置(可與 map 數不同)。
-只有當作業成功完成後,批處理作業的輸出才會被視為有效的(MapReduce 會丟棄失敗作業的部分輸出)。因此,工作流中的一項作業只有在先前的作業 —— 即生產其輸入的作業 —— 成功完成後才能開始。為了處理這些作業之間的依賴,有很多針對 Hadoop 的工作流排程器被開發出來,包括 Oozie、Azkaban、Luigi、Airflow 和 Pinball 【28】。
+mapper 輸出是鍵值對。框架需要保證:若不同 mapper 輸出了同一個鍵,這些鍵值對最終必須由同一個 reducer 處理。為此,每個 mapper 會在本地磁碟為每個 reducer 維護一個輸出檔案(例如[圖 11-1](/tw/ch11#fig_batch_mapreduce)中的 *m1,r2*:由 mapper1 生成,目標是 reducer2)。mapper 每輸出一條鍵值對,通常會按鍵的雜湊決定寫入哪個 reducer 檔案(類似[“按鍵雜湊分片”](/tw/ch7#sec_sharding_hash))。
-這些排程程式還具有管理功能,在維護大量批處理作業時非常有用。在構建推薦系統時,由 50 到 100 個 MapReduce 作業組成的工作流是常見的【29】。而在大型組織中,許多不同的團隊可能執行不同的作業來讀取彼此的輸出。工具支援對於管理這樣複雜的資料流而言非常重要。
+mapper 寫這些檔案的同時,也會在每個檔案內部按鍵排序。可用的正是[“日誌結構儲存”](/tw/ch4#sec_storage_log_structured)中的技術:先在記憶體有序結構裡積累一批鍵值對,寫成有序段檔案,再把小段逐步合併成大段。
-Hadoop 的各種高階工具(如 Pig 【30】、Hive 【31】、Cascading 【32】、Crunch 【33】和 FlumeJava 【34】)也能自動佈線組裝多個 MapReduce 階段,生成合適的工作流。
+每個 mapper 完成後,reducer 會連線到 mapper,把屬於自己的有序檔案複製到本地磁碟。reducer 拿到所有 mapper 的對應分片後,再用歸併排序方式合併它們並保持有序。同鍵記錄即便來自不同 mapper,也會在合併後相鄰。隨後 reducer 以“每個鍵一次呼叫”的方式執行,每次拿到一個可迭代器,遍歷該鍵所有值。
-### Reduce側連線與分組
+reducer 輸出記錄會順序寫入檔案,每個 reduce 任務一個檔案。[圖 11-1](/tw/ch11#fig_batch_mapreduce)中的 *r1*、*r2*、*r3* 就是輸出資料集的分片,最終寫回 DFS 或物件儲存。
-我們在 [第二章](/tw/ch2) 中討論了資料模型和查詢語言的連線,但是我們還沒有深入探討連線是如何實現的。現在是我們再次撿起這條線索的時候了。
+MapReduce 在 map 與 reduce 之間執行混洗;現代資料流引擎和雲資料倉庫則更複雜。BigQuery 等系統已最佳化混洗,使資料儘量留在記憶體,並寫入外部排序服務 [^24],以提升速度並透過複製增強韌性。
-在許多資料集中,一條記錄與另一條記錄存在關聯是很常見的:關係模型中的 **外部索引鍵**,文件模型中的 **文件引用** 或圖模型中的 **邊**。當你需要同時訪問這一關聯的兩側(持有引用的記錄與被引用的記錄)時,連線就是必須的。正如 [第二章](/tw/ch2) 所討論的,反正規化可以減少對連線的需求,但通常無法將其完全移除 [^v]。
+#### JOIN 與 GROUP BY {#sec_batch_join}
-[^v]: 我們在本書中討論的連線通常是等值連線,即最常見的連線型別,其中記錄透過與其他記錄在特定欄位(例如 ID)中具有 **相同值** 相關聯。有些資料庫支援更通用的連線型別,例如使用小於運算子而不是等號運算子,但是我們沒有地方來講這些東西。
+下面看“有序資料”如何簡化分散式連線與聚合。為便於說明仍以 MapReduce 為例,但概念適用於大多數批處理系統。
-在資料庫中,如果執行只涉及少量記錄的查詢,資料庫通常會使用 **索引** 來快速定位感興趣的記錄(請參閱 [第三章](/tw/ch3))。如果查詢涉及到連線,則可能涉及到查詢多個索引。然而 MapReduce 沒有索引的概念 —— 至少在通常意義上沒有。
+批處理裡常見連線場景見[圖 11-2](/tw/ch11#fig_batch_join_example)。左邊是使用者活動日誌(*activity events* 或 *clickstream data*),右邊是使用者資料庫。它可以看作星型模型的一部分(見[“星型與雪花型:分析模式”](/tw/ch3#sec_datamodels_analytics)):活動日誌是事實表,使用者庫是維度表之一。
-當 MapReduce 作業被賦予一組檔案作為輸入時,它讀取所有這些檔案的全部內容;資料庫會將這種操作稱為 **全表掃描**。如果你只想讀取少量的記錄,則全表掃描與索引查詢相比,代價非常高昂。但是在分析查詢中(請參閱 “[事務處理還是分析?](/tw/ch3#事務處理還是分析?)”),通常需要計算大量記錄的聚合。在這種情況下,特別是如果能在多臺機器上並行處理時,掃描整個輸入可能是相當合理的事情。
+{{< figure src="/fig/ddia_1102.png" id="fig_batch_join_example" caption="圖 11-2. 使用者活動日誌與使用者畫像資料庫的連線。" class="w-full my-4" >}}
-當我們在批處理的語境中討論連線時,我們指的是在資料集中解析某種關聯的全量存在。例如我們假設一個作業是同時處理所有使用者的資料,而非僅僅是為某個特定使用者查詢資料(而這能透過索引更高效地完成)。
+如果你要做“結合使用者庫資訊的活動分析”(例如利用使用者出生日期欄位,判斷哪些頁面更受年輕或年長使用者歡迎),就需要連線這兩張表。若兩邊都大到必須分片,怎麼做?
-#### 示例:使用者活動事件分析
+可利用 MapReduce 的關鍵特性:混洗會把同鍵鍵值對匯聚到同一個 reducer,無論它們最初在哪個分片。這裡使用者 ID 就可以作為鍵。因此可寫一個 mapper 掃活動日誌,輸出“按使用者 ID 鍵控的頁面訪問 URL”(見[圖 11-3](/tw/ch11#fig_batch_join_reduce));再寫一個 mapper 按行掃描使用者表,提取“使用者 ID 作為鍵、出生日期作為值”。
-[圖 10-2](/fig/ddia_1102.png) 給出了一個批處理作業中連線的典型例子。左側是事件日誌,描述登入使用者在網站上做的事情(稱為 **活動事件**,即 activity events,或 **點選流資料**,即 clickstream data),右側是使用者資料庫。你可以將此示例看作是星型模式的一部分(請參閱 “[星型和雪花型:分析的模式](/tw/ch3#星型和雪花型:分析的模式)”):事件日誌是事實表,使用者資料庫是其中的一個維度。
+{{< figure src="/fig/ddia_1103.png" id="fig_batch_join_reduce" caption="圖 11-3. 基於使用者 ID 的排序合併連線。若輸入資料集由多個檔案分片組成,可並行啟動多個 mapper 處理。" class="w-full my-4" >}}
-
+混洗保證 reducer 能同時拿到某使用者的出生日期和該使用者全部頁面訪問事件。MapReduce 甚至可以把記錄進一步排成 reducer 先看到使用者記錄、再按時間戳看到活動事件,這稱為 *二次排序(secondary sort)* [^25]。
-**圖 10-2 使用者行為日誌與使用者檔案的連線**
+於是 reducer 很容易實現連線邏輯:先拿到出生日期並存入區域性變數,再遍歷同一使用者 ID 的活動事件,輸出“被訪問 URL + 訪問者出生日期”。因為 reducer 一次處理一個使用者的全部記錄,所以記憶體裡只要保留一條使用者記錄,也無需發任何網路請求。這個演算法稱為 *排序合併連線(sort-merge join)*:mapper 輸出先按鍵排序,reducer 再把連線兩側有序記錄合併。
-分析任務可能需要將使用者活動與使用者檔案資訊相關聯:例如,如果檔案包含使用者的年齡或出生日期,系統就可以確定哪些頁面更受哪些年齡段的使用者歡迎。然而活動事件僅包含使用者 ID,而沒有包含完整的使用者檔案資訊。在每個活動事件中嵌入這些檔案資訊很可能會非常浪費。因此,活動事件需要與使用者檔案資料庫相連線。
+工作流中的下一個 MapReduce 作業就可以繼續計算“每個 URL 的訪問者年齡分佈”:先按 URL 做一次混洗,再在 reducer 中遍歷同 URL 的所有訪問記錄(含出生日期),按年齡段維護計數並逐條累加,從而實現 *group by* 與聚合。
-實現這一連線的最簡單方法是,逐個遍歷活動事件,併為每個遇到的使用者 ID 查詢使用者資料庫(在遠端伺服器上)。這是可能的,但是它的效能可能會非常差:處理吞吐量將受限於受資料庫伺服器的往返時間,本地快取的有效性很大程度上取決於資料的分佈,並行執行大量查詢可能會輕易壓垮資料庫【35】。
+### 查詢語言 {#sec_batch_query_lanauges}
-為了在批處理過程中實現良好的吞吐量,計算必須(儘可能)限於單臺機器上進行。為待處理的每條記錄發起隨機訪問的網路請求實在是太慢了。而且,查詢遠端資料庫意味著批處理作業變為 **非確定的(nondeterministic)**,因為遠端資料庫中的資料可能會改變。
+這些年分散式批處理執行引擎不斷成熟。如今在上萬臺機器的叢集上儲存並處理數 PB 資料,基礎設施已足夠穩健。隨著“如何在這規模下把系統跑起來”基本被解決,重點開始轉向程式設計模型的可用性。
-因此,更好的方法是獲取使用者資料庫的副本(例如,使用 ETL 程序從資料庫備份中提取資料,請參閱 “[資料倉庫](/tw/ch3#資料倉庫)”),並將它和使用者行為日誌放入同一個分散式檔案系統中。然後你可以將使用者資料庫儲存在 HDFS 中的一組檔案中,而使用者活動記錄儲存在另一組檔案中,並能用 MapReduce 將所有相關記錄集中到同一個地方進行高效處理。
+MapReduce、資料流引擎、雲資料倉庫都把 SQL 作為批處理“通用語”。這很自然:傳統資料倉庫本就用 SQL,資料分析/ETL 工具都支援 SQL,幾乎所有開發者和分析師也都熟悉 SQL。
-#### 排序合併連線
+相比手寫 MapReduce,查詢語言介面不僅程式碼更少,還支援互動式使用:可在終端或 GUI 裡寫分析 SQL 並直接執行。這種互動式查詢對於業務分析、產品、銷售、財務等角色探索資料非常高效。雖然它不完全是“經典批處理”形態,但 SQL 讓探索式查詢也能在分散式批處理系統中高效完成。
-回想一下,Mapper 的目的是從每個輸入記錄中提取一對鍵值。在 [圖 10-2](/v1/ddia_1002.png) 的情況下,這個鍵就是使用者 ID:一組 Mapper 會掃過活動事件(提取使用者 ID 作為鍵,活動事件作為值),而另一組 Mapper 將會掃過使用者資料庫(提取使用者 ID 作為鍵,使用者的出生日期作為值)。這個過程如 [圖 10-3](/v1/ddia_1003.png) 所示。
+高階查詢語言不只提升人的生產力,也提高機器執行效率。正如[“雲資料倉庫”](/tw/ch4#sec_cloud_data_warehouses)所述,查詢引擎要把 SQL 轉成在集群裡執行的批處理作業。這個從查詢到語法樹再到物理運算元的轉換過程,讓引擎有機會做最佳化。Hive、Trino、Spark、Flink 等查詢引擎都具備代價最佳化器:它們可分析連線輸入特徵,自動選擇更合適的連線演算法,甚至重排連線順序以減少中間狀態 [^19] [^26] [^27] [^28]。
-
+SQL 是最流行的通用批處理語言,但在一些細分場景中仍有其他語言。Apache Pig 提供了基於關係運算元的逐步式資料流水線描述方式,而非“一個超大 SQL 查詢”。DataFrame(下一節)有相似特徵,Morel 則是受 Pig 影響的更現代語言。還有使用者採用 jq、JMESPath、JsonPath 等 JSON 查詢語言。
-**圖 10-3 在使用者 ID 上進行的 Reduce 端連線。如果輸入資料集分割槽為多個檔案,則每個分割槽都會被多個 Mapper 並行處理**
+在[“圖狀資料模型”](/tw/ch3#sec_datamodels_graph)中,我們討論了圖建模與圖查詢語言如何遍歷邊和頂點。許多圖處理框架也支援透過查詢語言做批計算,例如 Apache TinkerPop 的 Gremlin。我們會在[“批處理用例”](/tw/ch11#sec_batch_output)繼續看圖處理場景。
-當 MapReduce 框架透過鍵對 Mapper 輸出進行分割槽,然後對鍵值對進行排序時,效果是具有相同 ID 的所有活動事件和使用者記錄在 Reducer 輸入中彼此相鄰。Map-Reduce 作業甚至可以也讓這些記錄排序,使 Reducer 總能先看到來自使用者資料庫的記錄,緊接著是按時間戳順序排序的活動事件 —— 這種技術被稱為 **二次排序(secondary sort)**【26】。
+> [!TIP] 批處理與雲資料倉庫正在收斂
+> 歷史上,資料倉庫執行在專用硬體裝置上,主要提供關係資料的 SQL 分析查詢;而 MapReduce 等批處理框架強調更高可伸縮性與更高靈活性,允許使用通用程式語言寫處理邏輯,並讀寫任意資料格式。
+>
+> 隨著發展,兩者越來越像。現代批處理框架已經支援 SQL,並藉助 Parquet 等列式格式和最佳化執行引擎(見[“查詢執行:編譯與向量化”](/tw/ch4#sec_storage_vectorized))在關係查詢上獲得良好效能。與此同時,資料倉庫透過雲化(見[“雲資料倉庫”](/tw/ch4#sec_cloud_data_warehouses))獲得更強可伸縮能力,並實現了許多與分散式批處理框架相同的排程、容錯和混洗技術,很多也使用分散式檔案系統。
+>
+> 正如批處理系統採納 SQL,雲倉庫也在採納 DataFrame 等替代處理模型(下一節)。例如 BigQuery 提供 BigQuery DataFrames,Snowflake 的 Snowpark 能與 Pandas 整合。Airflow、Prefect、Dagster 等批處理工作流編排器也已廣泛整合雲倉庫。
+>
+> 當然,並非所有批任務都容易用 SQL 表達。PageRank 等迭代圖演算法、複雜機器學習任務都很難用 SQL 寫。涉及影像、影片、音訊等非關係多模態資料的 AI 處理同樣如此。
+>
+> 此外,雲資料倉庫在某些負載上並不理想。行級逐條計算與列式儲存不匹配,效率較低,此時更適合使用倉庫的其他 API 或批處理系統。雲倉庫通常也比其他批處理系統更貴,某些大作業放到 Spark/Flink 等系統可能更具成本優勢。
+>
+> 因此,“用批處理系統還是資料倉庫”最終要看成本、便利性、實現複雜度、可用性等綜合因素。大型企業往往並存多套系統以保留選擇空間;小公司通常一套系統也能跑起來。
-然後 Reducer 可以容易地執行實際的連線邏輯:每個使用者 ID 都會被呼叫一次 Reducer 函式,且因為二次排序,第一個值應該是來自使用者資料庫的出生日期記錄。Reducer 將出生日期儲存在區域性變數中,然後使用相同的使用者 ID 遍歷活動事件,輸出 **已觀看網址** 和 **觀看者年齡** 的結果對。隨後的 Map-Reduce 作業可以計算每個 URL 的檢視者年齡分佈,並按年齡段進行聚集。
+### DataFrames {#id287}
-由於 Reducer 一次處理一個特定使用者 ID 的所有記錄,因此一次只需要將一條使用者記錄儲存在記憶體中,而不需要透過網路發出任何請求。這個演算法被稱為 **排序合併連線(sort-merge join)**,因為 Mapper 的輸出是按鍵排序的,然後 Reducer 將來自連線兩側的有序記錄列表合併在一起。
+隨著資料科學家和統計學家開始用分散式批處理框架做機器學習,他們發現原有處理模型不夠順手,因為他們更習慣 R 與 Pandas 裡的 DataFrame 資料模型(見[“DataFrame、矩陣與陣列”](/tw/ch3#sec_datamodels_dataframes))。DataFrame 與關係庫裡的表很像:由多行組成,同一列值型別一致。它不是寫一個超大 SQL,而是透過呼叫對應關係運算元的函式來做過濾、連線、排序、分組等操作。
-#### 把相關資料放在一起
+早期 DataFrame 操作大多在本地記憶體執行,因此只能處理單機裝得下的資料集。資料科學家希望在批處理環境中,仍用熟悉的 DataFrame API 處理大資料。Spark、Flink、Daft 等分散式框架都因此提供了 DataFrame API。需要注意的是,本地 DataFrame 通常帶索引且有順序,而分散式 DataFrame 往往沒有 [^29],遷移時可能出現效能“意外”。
-在排序合併連線中,Mapper 和排序過程確保了所有對特定使用者 ID 執行連線操作的必須資料都被放在同一個地方:單次呼叫 Reducer 的地方。預先排好了所有需要的資料,Reducer 可以是相當簡單的單執行緒程式碼,能夠以高吞吐量和與低記憶體開銷掃過這些記錄。
+DataFrame API 看起來和資料流 API 相似,但實現方式差別不小。Pandas 呼叫方法後通常立刻執行;Spark 則會先把 DataFrame API 呼叫翻譯為查詢計劃,做查詢最佳化後,再在分散式資料流引擎上執行,從而獲得更好效能。
-這種架構可以看做,Mapper 將 “訊息” 傳送給 Reducer。當一個 Mapper 發出一個鍵值對時,這個鍵的作用就像值應該傳遞到的目標地址。即使鍵只是一個任意的字串(不是像 IP 地址和埠號那樣的實際的網路地址),它表現的就像一個地址:所有具有相同鍵的鍵值對將被傳遞到相同的目標(一次 Reducer 的呼叫)。
+Daft 等框架甚至同時支援客戶端與服務端計算:小規模記憶體操作在客戶端執行,大資料與重計算在服務端執行。Apache Arrow 等列式格式提供統一資料模型,可被兩側執行引擎共享。
-使用 MapReduce 程式設計模型,能將計算的物理網路通訊層面(從正確的機器獲取資料)從應用邏輯中剝離出來(獲取資料後執行處理)。這種分離與資料庫的典型用法形成了鮮明對比,從資料庫中獲取資料的請求經常出現在應用程式碼內部【36】。由於 MapReduce 處理了所有的網路通訊,因此它也避免了讓應用程式碼去擔心部分故障,例如另一個節點的崩潰:MapReduce 在不影響應用邏輯的情況下能透明地重試失敗的任務。
+## 批處理用例 {#sec_batch_output}
-#### 分組
+瞭解了批處理如何工作後,我們來看它在不同應用中的落地。批處理非常適合“海量資料的批次計算”,但不適合低延遲場景。因此,只要資料多且新鮮度要求不高,幾乎都能看到批處理的身影。這聽起來像限制,但現實裡大量工作都符合這個模型:
-除了連線之外,“把相關資料放在一起” 的另一種常見模式是,按某個鍵對記錄分組(如 SQL 中的 GROUP BY 子句)。所有帶有相同鍵的記錄構成一個組,而下一步往往是在每個組內進行某種聚合操作,例如:
+- 會計對賬與庫存核對:企業定期驗證交易、銀行賬戶與庫存是否一致,常由批處理完成 [^30]。
+- 製造業需求預測:通常以週期性批任務計算 [^31]。
+- 電商、媒體、社交平臺推薦模型訓練:大量依賴批處理 [^32] [^33]。
+- 許多金融系統也是批處理驅動。例如美國銀行網路幾乎完全基於批任務執行 [^34]。
-- 統計每個組中記錄的數量(例如在統計 PV 的例子中,在 SQL 中表示為 `COUNT(*)` 聚合)
-- 對某個特定欄位求和(SQL 中的 `SUM(fieldname)`)
-- 按某種分級函式取出排名前 k 條記錄。
+下面分別討論幾個幾乎所有行業都常見的批處理用例。
-使用 MapReduce 實現這種分組操作的最簡單方法是設定 Mapper,以便它們生成的鍵值對使用所需的分組鍵。然後分割槽和排序過程將所有具有相同分割槽鍵的記錄導向同一個 Reducer。因此在 MapReduce 之上實現分組和連線看上去非常相似。
+### 提取-轉換-載入(ETL) {#sec_batch_etl_usage}
-分組的另一個常見用途是整理特定使用者會話的所有活動事件,以找出使用者進行的一系列操作(稱為 **會話化(sessionization)**【37】)。例如,可以使用這種分析來確定顯示新版網站的使用者是否比那些顯示舊版本的使用者更有購買慾(A/B 測試),或者計算某個營銷活動是否值得。
+[“資料倉庫”](/tw/ch1#sec_introduction_dwh)介紹了 ETL/ELT:從生產資料庫抽取資料、進行轉換,再載入到下游系統。本節用“ETL”統稱這兩類負載。尤其當下遊是資料倉庫時,ETL 常由批處理作業承載。
-如果你有多個 Web 伺服器處理使用者請求,則特定使用者的活動事件很可能分散在各個不同的伺服器的日誌檔案中。你可以透過使用會話 cookie,使用者 ID 或類似的識別符號作為分組鍵,以將特定使用者的所有活動事件放在一起來實現會話化,與此同時,不同使用者的事件仍然散佈在不同的分割槽中。
+批處理天然並行,非常適合資料轉換。很多轉換任務都是“令人尷尬地並行”:過濾、欄位投影及大量常見倉庫轉換都可並行完成。
-#### 處理偏斜
+批處理環境通常自帶成熟工作流排程器,便於安排、編排和除錯 ETL 流水線。發生故障時,排程器常會自動重試以覆蓋瞬時問題;若持續失敗,則明確標記失敗,便於工程師快速定位流水線中斷點。像 Airflow 還內建大量 source/sink/query 運算元,可直接對接 MySQL、PostgreSQL、Snowflake、Spark、Flink 等數十種系統。排程器與資料處理系統的緊密整合顯著簡化了資料整合。
-如果存在與單個鍵關聯的大量資料,則 “將具有相同鍵的所有記錄放到相同的位置” 這種模式就被破壞了。例如在社交網路中,大多數使用者可能會與幾百人有連線,但少數名人可能有數百萬的追隨者。這種不成比例的活動資料庫記錄被稱為 **關鍵物件(linchpin object)**【38】或 **熱鍵(hot key)**。
+我們也看到,批處理在“出錯後排障與修復”方面很友好,這對除錯資料流水線極其關鍵。失敗檔案可直接檢查,ETL 作業可修復後重跑。比如輸入檔案不再包含某個轉換邏輯依賴欄位,資料工程師就能據此更新轉換邏輯或修復上游生產作業。
-在單個 Reducer 中收集與某個名人相關的所有活動(例如他們釋出內容的回覆)可能導致嚴重的 **偏斜**(也稱為 **熱點**,即 hot spot)—— 也就是說,一個 Reducer 必須比其他 Reducer 處理更多的記錄(請參閱 “[負載偏斜與熱點消除](/tw/ch6#負載偏斜與熱點消除)”)。由於 MapReduce 作業只有在所有 Mapper 和 Reducer 都完成時才完成,所有後續作業必須等待最慢的 Reducer 才能啟動。
+過去資料流水線往往由單一資料工程團隊集中維護,因為讓產品團隊自行編寫和維護複雜批流水線不太現實。近年隨著處理模型和元資料管理改進,組織內更多團隊都能參與並維護自己的流水線。*data mesh* [^35] [^36]、*data contract* [^37]、*data fabric* [^38] 等實踐,正透過規範和工具幫助團隊安全釋出可被全組織消費的資料。
-如果連線的輸入存在熱鍵,可以使用一些演算法進行補償。例如,Pig 中的 **偏斜連線(skewed join)** 方法首先執行一個抽樣作業(Sampling Job)來確定哪些鍵是熱鍵【39】。連線實際執行時,Mapper 會將熱鍵的關聯記錄 **隨機**(相對於傳統 MapReduce 基於鍵雜湊的確定性方法)傳送到幾個 Reducer 之一。對於另外一側的連線輸入,與熱鍵相關的記錄需要被複制到 **所有** 處理該鍵的 Reducer 上【40】。
+如今資料流水線與分析查詢不僅共享處理模型,也常共享執行引擎。很多 ETL 作業與消費其輸出的分析查詢都執行在同一系統裡:例如同樣以 SparkSQL、Trino 或 DuckDB 查詢執行。這樣的架構進一步模糊了應用工程、資料工程、分析工程與業務分析之間的界限。
-這種技術將處理熱鍵的工作分散到多個 Reducer 上,這樣可以使其更好地並行化,代價是需要將連線另一側的輸入記錄複製到多個 Reducer 上。Crunch 中的 **分片連線(sharded join)** 方法與之類似,但需要顯式指定熱鍵而不是使用抽樣作業。這種技術也非常類似於我們在 “[負載偏斜與熱點消除](/tw/ch6#負載偏斜與熱點消除)” 中討論的技術,使用隨機化來緩解分割槽資料庫中的熱點。
+### 分析(Analytics) {#sec_batch_olap}
-Hive 的偏斜連線最佳化採取了另一種方法。它需要在表格元資料中顯式指定熱鍵,並將與這些鍵相關的記錄單獨存放,與其它檔案分開。當在該表上執行連線時,對於熱鍵,它會使用 Map 端連線(請參閱下一節)。
+在[“操作型系統與分析型系統”](/tw/ch1#sec_introduction_analytics)中我們看到,分析查詢(OLAP)通常要掃描大量記錄並做分組聚合。這類負載可以與其他批任務一起執行在批處理系統中。分析人員寫 SQL,經查詢引擎執行,讀寫底層 DFS 或物件儲存。表到檔案對映、名稱、型別等表元資料通常由 Apache Iceberg 等表格式與 Unity 等 catalog 管理(見[“雲資料倉庫”](/tw/ch4#sec_cloud_data_warehouses))。這種架構稱為 *資料湖倉(data lakehouse)* [^39]。
-當按照熱鍵進行分組並聚合時,可以將分組分兩個階段進行。第一個 MapReduce 階段將記錄傳送到隨機 Reducer,以便每個 Reducer 只對熱鍵的子集執行分組,為每個鍵輸出一個更緊湊的中間聚合結果。然後第二個 MapReduce 作業將所有來自第一階段 Reducer 的中間聚合結果合併為每個鍵一個值。
+與 ETL 類似,SQL 介面改進讓很多組織用 Spark 等批框架直接承載分析。常見模式有兩類:
+- 預聚合查詢:先把資料滾動聚合為 OLAP 立方體或資料集市,以提升查詢速度(見[“物化檢視與資料立方”](/tw/ch4#sec_storage_materialized_views))。預聚合結果可在倉庫查詢,或推送到 Apache Druid、Apache Pinot 這類即時 OLAP 系統。預聚合通常按固定週期執行,通常由[“工作流排程”](/tw/ch11#sec_batch_workflows)中提到的排程器管理。
+- 臨時查詢(ad hoc):使用者為回答具體業務問題、分析使用者行為、排查執行問題等隨時發起。該場景非常看重響應時間,分析師通常會根據每次結果繼續迭代提問。執行快的批處理查詢引擎可顯著縮短等待。
-### Map側連線
+SQL 支援還讓批處理系統更易接入電子表格與視覺化工具,如 Tableau、Power BI、Looker、Apache Superset。比如 Tableau 有 SparkSQL、Presto 聯結器;Superset 支援 Trino、Hive、Spark SQL、Presto 等大量最終會觸發批任務的資料系統。
-上一節描述的連線演算法在 Reducer 中執行實際的連線邏輯,因此被稱為 Reduce 側連線。Mapper 扮演著預處理輸入資料的角色:從每個輸入記錄中提取鍵值,將鍵值對分配給 Reducer 分割槽,並按鍵排序。
+### 機器學習 {#id290}
-Reduce 側方法的優點是不需要對輸入資料做任何假設:無論其屬性和結構如何,Mapper 都可以對其預處理以備連線。然而不利的一面是,排序,複製至 Reducer,以及合併 Reducer 輸入,所有這些操作可能開銷巨大。當資料透過 MapReduce 階段時,資料可能需要落盤好幾次,取決於可用的記憶體緩衝區【37】。
+機器學習(ML)高度依賴批處理。資料科學家、ML 工程師、AI 工程師會用批處理框架探索資料模式、做資料轉換、訓練模型。常見用途包括:
-另一方面,如果你 **能** 對輸入資料作出某些假設,則透過使用所謂的 Map 側連線來加快連線速度是可行的。這種方法使用了一個裁減掉 Reducer 與排序的 MapReduce 作業,每個 Mapper 只是簡單地從分散式檔案系統中讀取一個輸入檔案塊,然後將輸出檔案寫入檔案系統,僅此而已。
+- 特徵工程:把原始資料過濾並轉換為可訓練資料。預測模型往往要求數值特徵,因此文字或離散值等資料需要先轉成目標格式。
+- 模型訓練:訓練資料是批過程輸入,訓練後模型權重是輸出。
+- 批次推理:當資料集很大且不要求即時結果時,可對整批資料做預測,也包括在測試集上評估模型預測效果。
-#### 廣播雜湊連線
+很多框架為這些場景提供了專用工具。例如 Spark 的 MLlib、Flink 的 FlinkML 都內建豐富的特徵工程工具、統計函式與分類器。
-適用於執行 Map 端連線的最簡單場景是大資料集與小資料集連線的情況。要點在於小資料集需要足夠小,以便可以將其全部載入到每個 Mapper 的記憶體中。
+推薦系統和排序系統等 ML 應用也大量使用圖處理(見[“圖狀資料模型”](/tw/ch3#sec_datamodels_graph))。許多圖演算法表達為“沿邊逐步傳播資訊並反覆迭代”:把一個頂點與相鄰頂點連線,傳遞某些資訊,重複直到滿足停止條件,例如無邊可繼續,或某個指標收斂。
-例如,假設在 [圖 10-2](/fig/ddia_1102.png) 的情況下,使用者資料庫小到足以放進記憶體中。在這種情況下,當 Mapper 啟動時,它可以首先將使用者資料庫從分散式檔案系統讀取到記憶體中的散列表中。完成此操作後,Mapper 可以掃描使用者活動事件,並簡單地在散列表中查詢每個事件的使用者 ID [^vi]。
+*批同步並行(bulk synchronous parallel, BSP)* 計算模型 [^40] 已成為批圖計算常用模型。Apache Giraph [^20]、Spark GraphX、Flink Gelly [^41] 等都實現了它。它也常被稱為 *Pregel* 模型,因為 Google 的 Pregel 論文讓這一方法廣為人知 [^42]。
-[^vi]: 這個例子假定散列表中的每個鍵只有一個條目,這對使用者資料庫(使用者 ID 唯一標識一個使用者)可能是正確的。通常,雜湊表可能需要包含具有相同鍵的多個條目,而連線運算子將對每個鍵輸出所有的匹配。
+批處理同樣是大語言模型(LLM)資料準備與訓練的重要組成部分。網頁等原始文字通常存放在 DFS 或物件儲存中,必須先預處理才能用於訓練。適合批處理框架的預處理步驟包括:
-參與連線的較大輸入的每個檔案塊各有一個 Mapper(在 [圖 10-2](/fig/ddia_1102.png) 的例子中活動事件是較大的輸入)。每個 Mapper 都會將較小輸入整個載入到記憶體中。
+- 從 HTML 中提取純文字,並修復損壞文字;
+- 檢測並清理低質量、無關或重複文件;
+- 對文字做分詞並轉換為嵌入向量(詞或片段的數值表示)。
-這種簡單有效的演算法被稱為 **廣播雜湊連線(broadcast hash join)**:**廣播** 一詞反映了這樣一個事實,每個連線較大輸入端分割槽的 Mapper 都會將較小輸入端資料集整個讀入記憶體中(所以較小輸入實際上 “廣播” 到較大資料的所有分割槽上),**雜湊** 一詞反映了它使用一個散列表。Pig(名為 “**複製連結(replicated join)**”),Hive(“**MapJoin**”),Cascading 和 Crunch 支援這種連線。它也被諸如 Impala 的資料倉庫查詢引擎使用【41】。
+Kubeflow、Flyte、Ray 等框架就專為這類負載構建。以 OpenAI 為例,ChatGPT 訓練流程中就使用了 Ray [^43]。這些框架通常內建與 PyTorch、TensorFlow、XGBoost 等 LLM/AI 庫的整合,並支援特徵工程、模型訓練、批次推理、微調等能力。
-除了將較小的連線輸入載入到記憶體散列表中,另一種方法是將較小輸入儲存在本地磁碟上的只讀索引中【42】。索引中經常使用的部分將保留在作業系統的頁面快取中,因而這種方法可以提供與記憶體散列表幾乎一樣快的隨機查詢效能,但實際上並不需要資料集能放入記憶體中。
+最後,資料科學家常在 Jupyter、Hex 等互動式 Notebook 中實驗資料。Notebook 由多個 *cell* 組成,每個 cell 是一小段 Markdown、Python 或 SQL;按順序執行可得到表格、圖表或資料結果。很多 Notebook 背後透過 DataFrame API 或 SQL 呼叫批處理系統。
-#### 分割槽雜湊連線
+### 對外提供派生資料 {#sec_batch_serving_derived}
-如果 Map 側連線的輸入以相同的方式進行分割槽,則雜湊連線方法可以獨立應用於每個分割槽。在 [圖 10-2](/fig/ddia_1102.png) 的情況中,你可以根據使用者 ID 的最後一位十進位制數字來對活動事件和使用者資料庫進行分割槽(因此連線兩側各有 10 個分割槽)。例如,Mapper3 首先將所有具有以 3 結尾的 ID 的使用者載入到散列表中,然後掃描 ID 為 3 的每個使用者的所有活動事件。
+批處理常用於構建預計算/派生資料集,如商品推薦、面向使用者的報表、機器學習特徵等。這些資料通常由生產資料庫、鍵值儲存或搜尋引擎對外服務。不論目標系統是什麼,都需要把批處理環境中的 DFS/物件儲存輸出,回灌到線上服務資料庫。
-如果分割槽正確無誤,可以確定的是,所有你可能需要連線的記錄都落在同一個編號的分割槽中。因此每個 Mapper 只需要從輸入兩端各讀取一個分割槽就足夠了。好處是每個 Mapper 都可以在記憶體散列表中少放點資料。
+最直觀的做法是:在批作業裡直接使用資料庫客戶端庫,一條條寫生產資料庫(假設防火牆允許)。這雖然能工作,但通常不是好主意,原因有三:
-這種方法只有當連線兩端輸入有相同的分割槽數,且兩側的記錄都是使用相同的鍵與相同的雜湊函式做分割槽時才適用。如果輸入是由之前執行過這種分組的 MapReduce 作業生成的,那麼這可能是一個合理的假設。
+- 每條記錄一次網路請求,比批任務正常吞吐低幾個數量級。即便客戶端支援批寫,效能通常也不理想。
+- 批處理框架常並行跑很多工。若所有任務同時以批處理速率寫同一資料庫,很容易把資料庫壓垮,進而影響其線上查詢效能,引發系統其他部分故障 [^44]。
+- 批作業通常提供清晰的“全有或全無”輸出語義:作業成功時,結果等價於每個任務恰好執行一次;作業失敗時,無有效輸出。但如果在作業內直接寫外部系統,就產生了外部可見副作用,難以隱藏:部分完成結果可能被其他系統看到,任務失敗重啟還可能造成重複寫。
-分割槽雜湊連線在 Hive 中稱為 **Map 側桶連線(bucketed map joins)【37】**。
+更好的方案是把預計算結果先推送到 Kafka 這類流系統(我們會在[第十二章](/tw/ch12#ch_stream)深入討論)。Elasticsearch、Apache Pinot、Apache Druid、Venice 這類派生資料儲存 [^45],以及 ClickHouse 等雲數倉,都支援從 Kafka 攝入資料。透過流系統過渡可以改善前述問題:
-#### Map側合併連線
+- 流系統針對順序寫最佳化,更適合批作業的大吞吐寫入模式;
+- 流系統可在批作業與生產庫間充當緩衝層,下游可按自身能力限速讀取,避免影響線上流量;
+- 一個批作業輸出可被多個下游系統同時消費;
+- 流系統還可作為批處理網路與生產網路之間的安全邊界(可部署在 DMZ)。
-如果輸入資料集不僅以相同的方式進行分割槽,而且還基於相同的鍵進行 **排序**,則可適用另一種 Map 側連線的變體。在這種情況下,輸入是否小到能放入記憶體並不重要,因為這時候 Mapper 同樣可以執行歸併操作(通常由 Reducer 執行)的歸併操作:按鍵遞增的順序依次讀取兩個輸入檔案,將具有相同鍵的記錄配對。
+但“經由流”並不會自動解決“全有或全無”語義。要實現這一點,批作業需要在完成後向下遊發出“作業完成,可對外可見”的通知。流消費者需要像 *讀已提交(read committed)* 事務那樣,在收到完成通知前讓新資料對查詢不可見(見[“讀已提交”](/tw/ch8#sec_transactions_read_committed))。
-如果能進行 Map 側合併連線,這通常意味著前一個 MapReduce 作業可能一開始就已經把輸入資料做了分割槽並進行了排序。原則上這個連線就可以在前一個作業的 Reduce 階段進行。但使用獨立的僅 Map 作業有時也是合適的,例如,分好區且排好序的中間資料集可能還會用於其他目的。
+另一種在資料庫冷啟動(bootstrap)時更常見的模式,是在批作業內直接構建一個全新資料庫,再把檔案從 DFS、物件儲存或本地檔案系統批次匯入目標資料庫。很多系統都提供這類批次匯入工具,如 TiDB Lightning、Apache Pinot/Apache Druid 的 Hadoop 匯入作業,RocksDB 也提供從批作業批次匯入 SST 的 API。
-#### MapReduce工作流與Map側連線
+“批構建 + 批匯入”速度非常快,也更容易在不同資料版本間做原子切換。但對於需要持續增量更新的場景,這種“每次構建全新庫”的方式會更難。很多系統採用混合策略,同時支援冷啟動與增量載入。比如 Venice 就支援混合儲存,可同時做基於行的批更新和全量資料集切換。
-當下遊作業使用 MapReduce 連線的輸出時,選擇 Map 側連線或 Reduce 側連線會影響輸出的結構。Reduce 側連線的輸出是按照 **連線鍵** 進行分割槽和排序的,而 Map 端連線的輸出則按照與較大輸入相同的方式進行分割槽和排序(因為無論是使用分割槽連線還是廣播連線,連線較大輸入端的每個檔案塊都會啟動一個 Map 任務)。
+## 本章小結 {#id292}
-如前所述,Map 側連線也對輸入資料集的大小,有序性和分割槽方式做出了更多假設。在最佳化連線策略時,瞭解分散式檔案系統中資料集的物理佈局變得非常重要:僅僅知道編碼格式和資料儲存目錄的名稱是不夠的;你還必須知道資料是按哪些鍵做的分割槽和排序,以及分割槽的數量。
+本章討論了批處理系統的設計與實現。我們先從經典 Unix 工具鏈(awk、sort、uniq 等)出發,說明了批處理的基礎原語,例如排序和計數。
-在 Hadoop 生態系統中,這種關於資料集分割槽的元資料通常在 HCatalog 和 Hive Metastore 中維護【37】。
+然後我們把視角擴充套件到分散式批處理系統。批處理以“不可變、有限(bounded)的輸入資料集”為物件,生成輸出資料,這使得重跑和除錯可以不引入副作用。圍繞這一模式,批處理框架通常包含三層核心能力:決定作業何時何地執行的編排層,負責持久化資料的儲存層,以及執行實際計算的計算層。
+我們看了分散式檔案系統和物件儲存如何透過分塊複製、快取和元資料服務管理大檔案,也討論了現代批處理框架如何透過可插拔 API 與這些儲存互動。我們還討論了編排器在大叢集中如何排程任務、分配資源和處理故障,以及“按作業排程”的編排器與“按依賴圖管理整組作業生命週期”的工作流編排器之間的區別。
-### 批處理工作流的輸出
+在處理模型方面,我們回顧了 MapReduce 及其經典 map/reduce 函式,又介紹了 Spark、Flink 等更易用且效能更好的資料流引擎。為了理解批作業如何擴充套件到大規模,我們重點講了混洗(shuffle)演算法,它是實現分組、連線、聚合的基礎操作。
-我們已經說了很多用於實現 MapReduce 工作流的演算法,但卻忽略了一個重要的問題:這些處理完成之後的最終結果是什麼?我們最開始為什麼要跑這些作業?
+隨著批處理系統成熟,焦點轉向可用性。高階查詢語言(尤其 SQL)和 DataFrame API 讓批處理作業更易編寫,也更容易被最佳化器最佳化。查詢最佳化器把宣告式查詢轉換為高效執行計劃。
-在資料庫查詢的場景中,我們將事務處理(OLTP)與分析兩種目的區分開來(請參閱 “[事務處理還是分析?](/tw/ch3#事務處理還是分析?)”)。我們看到,OLTP 查詢通常根據鍵查詢少量記錄,使用索引,並將其呈現給使用者(比如在網頁上)。另一方面,分析查詢通常會掃描大量記錄,執行分組與聚合,輸出通常有著報告的形式:顯示某個指標隨時間變化的圖表,或按照某種排位取前 10 項,或將一些數字細化為子類。這種報告的消費者通常是需要做出商業決策的分析師或經理。
+最後我們回顧了批處理常見用例:
-批處理放哪裡合適?它不屬於事務處理,也不是分析。它和分析比較接近,因為批處理通常會掃過輸入資料集的絕大部分。然而 MapReduce 作業工作流與用於分析目的的 SQL 查詢是不同的(請參閱 “[Hadoop 與分散式資料庫的對比](#Hadoop與分散式資料庫的對比)”)。批處理過程的輸出通常不是報表,而是一些其他型別的結構。
+- ETL 流水線:透過定時工作流在不同系統間提取、轉換、載入資料;
+- 分析:既支援預聚合報表,也支援臨時探索查詢;
+- 機器學習:用於準備與處理大規模訓練資料;
+- 把批處理輸出灌入面向生產流量的系統:常透過流系統或批次匯入工具,把派生資料提供給使用者。
-#### 建立搜尋索引
+下一章我們將轉向流處理。與批處理不同,流處理輸入是 *無界(unbounded)* 的:作業仍在,但輸入是持續不斷的資料流,因此作業不會“完成”。我們會看到,流處理與批處理在一些方面很相似,但“輸入無界”這一前提也會顯著改變系統設計。
-Google 最初使用 MapReduce 是為其搜尋引擎建立索引,其實現為由 5 到 10 個 MapReduce 作業組成的工作流【1】。雖然 Google 後來也不僅僅是為這個目的而使用 MapReduce 【43】,但如果從構建搜尋索引的角度來看,更能幫助理解 MapReduce。(直至今日,Hadoop MapReduce 仍然是為 Lucene/Solr 構建索引的好方法【44】)
-我們在 “[全文檢索與模糊索引](/tw/ch3#全文檢索與模糊索引)” 中簡要地瞭解了 Lucene 這樣的全文檢索索引是如何工作的:它是一個檔案(關鍵詞字典),你可以在其中高效地查詢特定關鍵字,並找到包含該關鍵字的所有文件 ID 列表(文章列表)。這是一種非常簡化的看法 —— 實際上,搜尋索引需要各種額外資料,以便根據相關性對搜尋結果進行排名、糾正拼寫錯誤、解析同義詞等等 —— 但這個原則是成立的。
+### 參考文獻 {#references}
-如果需要對一組固定文件執行全文檢索,則批處理是一種構建索引的高效方法:Mapper 根據需要對文件集合進行分割槽,每個 Reducer 構建該分割槽的索引,並將索引檔案寫入分散式檔案系統。構建這樣的文件分割槽索引(請參閱 “[分割槽與次級索引](/tw/ch6#分割槽與次級索引)”)並行處理效果拔群。
-
-由於按關鍵字查詢搜尋索引是隻讀操作,因而這些索引檔案一旦建立就是不可變的。
-
-如果索引的文件集合發生更改,一種選擇是定期重跑整個索引工作流,並在完成後用新的索引檔案批次替換以前的索引檔案。如果只有少量的文件發生了變化,這種方法的計算成本可能會很高。但它的優點是索引過程很容易理解:文件進,索引出。
-
-另一個選擇是,可以增量建立索引。如 [第三章](/tw/ch3) 中討論的,如果要在索引中新增,刪除或更新文件,Lucene 會寫新的段檔案,並在後臺非同步合併壓縮段檔案。我們將在 [第十二章](/tw/ch12) 中看到更多這種增量處理。
-
-#### 鍵值儲存作為批處理輸出
-
-搜尋索引只是批處理工作流可能輸出的一個例子。批處理的另一個常見用途是構建機器學習系統,例如分類器(比如垃圾郵件過濾器,異常檢測,影像識別)與推薦系統(例如,你可能認識的人,你可能感興趣的產品或相關的搜尋【29】)。
-
-這些批處理作業的輸出通常是某種資料庫:例如,可以透過給定使用者 ID 查詢該使用者推薦好友的資料庫,或者可以透過產品 ID 查詢相關產品的資料庫【45】。
-
-這些資料庫需要被處理使用者請求的 Web 應用所查詢,而它們通常是獨立於 Hadoop 基礎設施的。那麼批處理過程的輸出如何回到 Web 應用可以查詢的資料庫中呢?
-
-最直接的選擇可能是,直接在 Mapper 或 Reducer 中使用你最愛的資料庫的客戶端庫,並從批處理作業直接寫入資料庫伺服器,一次寫入一條記錄。它能工作(假設你的防火牆規則允許從你的 Hadoop 環境直接訪問你的生產資料庫),但這並不是一個好主意,出於以下幾個原因:
-
-- 正如前面在連線的上下文中討論的那樣,為每條記錄發起一個網路請求,要比批處理任務的正常吞吐量慢幾個數量級。即使客戶端庫支援批處理,效能也可能很差。
-- MapReduce 作業經常並行執行許多工。如果所有 Mapper 或 Reducer 都同時寫入相同的輸出資料庫,並以批處理的預期速率工作,那麼該資料庫很可能被輕易壓垮,其查詢效能可能變差。這可能會導致系統其他部分的執行問題【35】。
-- 通常情況下,MapReduce 為作業輸出提供了一個乾淨利落的 “全有或全無” 保證:如果作業成功,則結果就是每個任務恰好執行一次所產生的輸出,即使某些任務失敗且必須一路重試。如果整個作業失敗,則不會生成輸出。然而從作業內部寫入外部系統,會產生外部可見的副作用,這種副作用是不能以這種方式被隱藏的。因此,你不得不去操心對其他系統可見的部分完成的作業結果,並需要理解 Hadoop 任務嘗試與預測執行的複雜性。
-
-更好的解決方案是在批處理作業 **內** 建立一個全新的資料庫,並將其作為檔案寫入分散式檔案系統中作業的輸出目錄,就像上節中的搜尋索引一樣。這些資料檔案一旦寫入就是不可變的,可以批次載入到處理只讀查詢的伺服器中。不少鍵值儲存都支援在 MapReduce 作業中構建資料庫檔案,包括 Voldemort 【46】、Terrapin 【47】、ElephantDB 【48】和 HBase 批次載入【49】。
-
-構建這些資料庫檔案是 MapReduce 的一種好用法:使用 Mapper 提取出鍵並按該鍵排序,已經完成了構建索引所必需的大量工作。由於這些鍵值儲存大多都是隻讀的(檔案只能由批處理作業一次性寫入,然後就不可變),所以資料結構非常簡單。比如它們就不需要預寫式日誌(WAL,請參閱 “[讓 B 樹更可靠](/tw/ch3#讓B樹更可靠)”)。
-
-將資料載入到 Voldemort 時,伺服器將繼續用舊資料檔案服務請求,同時將新資料檔案從分散式檔案系統複製到伺服器的本地磁碟。一旦複製完成,伺服器會自動將查詢切換到新檔案。如果在這個過程中出現任何問題,它可以輕易回滾至舊檔案,因為它們仍然存在而且不可變【46】。
-
-#### 批處理輸出的哲學
-
-本章前面討論過的 Unix 哲學(“[Unix 哲學](#Unix哲學)”)鼓勵以顯式指明資料流的方式進行實驗:程式讀取輸入並寫入輸出。在這一過程中,輸入保持不變,任何先前的輸出都被新輸出完全替換,且沒有其他副作用。這意味著你可以隨心所欲地重新執行一個命令,略做改動或進行除錯,而不會攪亂系統的狀態。
-
-MapReduce 作業的輸出處理遵循同樣的原理。透過將輸入視為不可變且避免副作用(如寫入外部資料庫),批處理作業不僅實現了良好的效能,而且更容易維護:
-
-- 如果在程式碼中引入了一個錯誤,而輸出錯誤或損壞了,則可以簡單地回滾到程式碼的先前版本,然後重新執行該作業,輸出將重新被糾正。或者,甚至更簡單,你可以將舊的輸出儲存在不同的目錄中,然後切換回原來的目錄。具有讀寫事務的資料庫沒有這個屬性:如果你部署了錯誤的程式碼,將錯誤的資料寫入資料庫,那麼回滾程式碼將無法修復資料庫中的資料。(能夠從錯誤程式碼中恢復的概念被稱為 **人類容錯(human fault tolerance)**【50】)
-- 由於回滾很容易,比起在錯誤意味著不可挽回的傷害的環境,功能開發進展能快很多。這種 **最小化不可逆性(minimizing irreversibility)** 的原則有利於敏捷軟體開發【51】。
-- 如果 Map 或 Reduce 任務失敗,MapReduce 框架將自動重新排程,並在同樣的輸入上再次執行它。如果失敗是由程式碼中的錯誤造成的,那麼它會不斷崩潰,並最終導致作業在幾次嘗試之後失敗。但是如果故障是由於臨時問題導致的,那麼故障就會被容忍。因為輸入不可變,這種自動重試是安全的,而失敗任務的輸出會被 MapReduce 框架丟棄。
-- 同一組檔案可用作各種不同作業的輸入,包括計算指標的監控作業並且評估作業的輸出是否具有預期的性質(例如,將其與前一次執行的輸出進行比較並測量差異) 。
-- 與 Unix 工具類似,MapReduce 作業將邏輯與佈線(配置輸入和輸出目錄)分離,這使得關注點分離,可以重用程式碼:一個團隊可以專注實現一個做好一件事的作業;而其他團隊可以決定何時何地執行這項作業。
-
-在這些領域,在 Unix 上表現良好的設計原則似乎也適用於 Hadoop,但 Unix 和 Hadoop 在某些方面也有所不同。例如,因為大多數 Unix 工具都假設輸入輸出是無型別文字檔案,所以它們必須做大量的輸入解析工作(本章開頭的日誌分析示例使用 `{print $7}` 來提取 URL)。在 Hadoop 上可以透過使用更結構化的檔案格式消除一些低價值的語法轉換:比如 Avro(請參閱 “[Avro](/tw/ch4#Avro)”)和 Parquet(請參閱 “[列式儲存](/tw/ch3#列式儲存)”)經常使用,因為它們提供了基於模式的高效編碼,並允許模式隨時間推移而演進(見 [第四章](/tw/ch4))。
-
-### Hadoop與分散式資料庫的對比
-
-正如我們所看到的,Hadoop 有點像 Unix 的分散式版本,其中 HDFS 是檔案系統,而 MapReduce 是 Unix 程序的怪異實現(總是在 Map 階段和 Reduce 階段執行 `sort` 工具)。我們瞭解了如何在這些原語的基礎上實現各種連線和分組操作。
-
-當 MapReduce 論文發表時【1】,它從某種意義上來說 —— 並不新鮮。我們在前幾節中討論的所有處理和並行連線演算法已經在十多年前所謂的 **大規模並行處理(MPP,massively parallel processing)** 資料庫中實現了【3,40】。比如 Gamma database machine、Teradata 和 Tandem NonStop SQL 就是這方面的先驅【52】。
-
-最大的區別是,MPP 資料庫專注於在一組機器上並行執行分析 SQL 查詢,而 MapReduce 和分散式檔案系統【19】的組合則更像是一個可以執行任意程式的通用作業系統。
-
-#### 儲存多樣性
-
-資料庫要求你根據特定的模型(例如關係或文件)來構造資料,而分散式檔案系統中的檔案只是位元組序列,可以使用任何資料模型和編碼來編寫。它們可能是資料庫記錄的集合,但同樣可以是文字、影像、影片、感測器讀數、稀疏矩陣、特徵向量、基因組序列或任何其他型別的資料。
-
-說白了,Hadoop 開放了將資料不加區分地轉儲到 HDFS 的可能性,允許後續再研究如何進一步處理【53】。相比之下,在將資料匯入資料庫專有儲存格式之前,MPP 資料庫通常需要對資料和查詢模式進行仔細的前期建模。
-
-在純粹主義者看來,這種仔細的建模和匯入似乎是可取的,因為這意味著資料庫的使用者有更高質量的資料來處理。然而實踐經驗表明,簡單地使資料快速可用 —— 即使它很古怪,難以使用,使用原始格式 —— 也通常要比事先決定理想資料模型要更有價值【54】。
-
-這個想法與資料倉庫類似(請參閱 “[資料倉庫](/tw/ch3#資料倉庫)”):將大型組織的各個部分的資料集中在一起是很有價值的,因為它可以跨越以前相互分離的資料集進行連線。MPP 資料庫所要求的謹慎模式設計拖慢了集中式資料收集速度;以原始形式收集資料,稍後再操心模式的設計,能使資料收集速度加快(有時被稱為 “**資料湖(data lake)**” 或 “**企業資料中心(enterprise data hub)**”【55】)。
-
-不加區分的資料轉儲轉移了解釋資料的負擔:資料集的生產者不再需要強制將其轉化為標準格式,資料的解釋成為消費者的問題(**讀時模式** 方法【56】;請參閱 “[文件模型中的模式靈活性](/tw/ch2#文件模型中的模式靈活性)”)。如果生產者和消費者是不同優先順序的不同團隊,這可能是一種優勢。甚至可能不存在一個理想的資料模型,對於不同目的有不同的合適視角。以原始形式簡單地轉儲資料,可以允許多種這樣的轉換。這種方法被稱為 **壽司原則(sushi principle)**:“原始資料更好”【57】。
-
-因此,Hadoop 經常被用於實現 ETL 過程(請參閱 “[資料倉庫](/tw/ch3#資料倉庫)”):事務處理系統中的資料以某種原始形式轉儲到分散式檔案系統中,然後編寫 MapReduce 作業來清理資料,將其轉換為關係形式,並將其匯入 MPP 資料倉庫以進行分析。資料建模仍然在進行,但它在一個單獨的步驟中進行,與資料收集相解耦。這種解耦是可行的,因為分散式檔案系統支援以任何格式編碼的資料。
-
-#### 處理模型的多樣性
-
-MPP 資料庫是單體的,緊密整合的軟體,負責磁碟上的儲存佈局,查詢計劃,排程和執行。由於這些元件都可以針對資料庫的特定需求進行調整和最佳化,因此整個系統可以在其設計針對的查詢型別上取得非常好的效能。而且,SQL 查詢語言允許以優雅的語法表達查詢,而無需編寫程式碼,可以在業務分析師使用的視覺化工具(例如 Tableau)中訪問到。
-
-另一方面,並非所有型別的處理都可以合理地表達為 SQL 查詢。例如,如果要構建機器學習和推薦系統,或者使用相關性排名模型的全文檢索索引,或者執行影像分析,則很可能需要更一般的資料處理模型。這些型別的處理通常是特別針對特定應用的(例如機器學習的特徵工程,機器翻譯的自然語言模型,欺詐預測的風險評估函式),因此它們不可避免地需要編寫程式碼,而不僅僅是查詢。
-
-MapReduce 使工程師能夠輕鬆地在大型資料集上執行自己的程式碼。如果你有 HDFS 和 MapReduce,那麼你 **可以** 在它之上建立一個 SQL 查詢執行引擎,事實上這正是 Hive 專案所做的【31】。但是,你也可以編寫許多其他形式的批處理,這些批處理不必非要用 SQL 查詢表示。
-
-隨後,人們發現 MapReduce 對於某些型別的處理而言侷限性很大,表現很差,因此在 Hadoop 之上其他各種處理模型也被開發出來(我們將在 “[MapReduce 之後](#MapReduce之後)” 中看到其中一些)。只有兩種處理模型,SQL 和 MapReduce,還不夠,需要更多不同的模型!而且由於 Hadoop 平臺的開放性,實施一整套方法是可行的,而這在單體 MPP 資料庫的範疇內是不可能的【58】。
-
-至關重要的是,這些不同的處理模型都可以在共享的單個機器叢集上執行,所有這些機器都可以訪問分散式檔案系統上的相同檔案。在 Hadoop 方式中,不需要將資料匯入到幾個不同的專用系統中進行不同型別的處理:系統足夠靈活,可以支援同一個叢集內不同的工作負載。不需要移動資料,使得從資料中挖掘價值變得容易得多,也使採用新的處理模型容易的多。
-
-Hadoop 生態系統包括隨機訪問的 OLTP 資料庫,如 HBase(請參閱 “[SSTables 和 LSM 樹](/tw/ch3#SSTables和LSM樹)”)和 MPP 風格的分析型資料庫,如 Impala 【41】。HBase 與 Impala 都不使用 MapReduce,但都使用 HDFS 進行儲存。它們是迥異的資料訪問與處理方法,但是它們可以共存,並被整合到同一個系統中。
-
-#### 針對頻繁故障設計
-
-當比較 MapReduce 和 MPP 資料庫時,兩種不同的設計思路出現了:處理故障和使用記憶體與磁碟的方式。與線上系統相比,批處理對故障不太敏感,因為就算失敗也不會立即影響到使用者,而且它們總是能再次執行。
-
-如果一個節點在執行查詢時崩潰,大多數 MPP 資料庫會中止整個查詢,並讓使用者重新提交查詢或自動重新執行它【3】。由於查詢通常最多執行幾秒鐘或幾分鐘,所以這種錯誤處理的方法是可以接受的,因為重試的代價不是太大。MPP 資料庫還傾向於在記憶體中保留儘可能多的資料(例如,使用雜湊連線)以避免從磁碟讀取的開銷。
-
-另一方面,MapReduce 可以容忍單個 Map 或 Reduce 任務的失敗,而不會影響作業的整體,透過以單個任務的粒度重試工作。它也會非常急切地將資料寫入磁碟,一方面是為了容錯,另一部分是因為假設資料集太大而不能適應記憶體。
-
-MapReduce 方式更適用於較大的作業:要處理如此之多的資料並執行很長時間的作業,以至於在此過程中很可能至少遇到一個任務故障。在這種情況下,由於單個任務失敗而重新執行整個作業將是非常浪費的。即使以單個任務的粒度進行恢復引入了使得無故障處理更慢的開銷,但如果任務失敗率足夠高,這仍然是一種合理的權衡。
-
-但是這些假設有多麼現實呢?在大多數叢集中,機器故障確實會發生,但是它們不是很頻繁 —— 可能少到絕大多數作業都不會經歷機器故障。為了容錯,真的值得帶來這麼大的額外開銷嗎?
-
-要了解 MapReduce 節約使用記憶體和在任務的層次進行恢復的原因,瞭解最初設計 MapReduce 的環境是很有幫助的。Google 有著混用的資料中心,線上生產服務和離線批處理作業在同樣機器上執行。每個任務都有一個透過容器強制執行的資源配給(CPU 核心、RAM、磁碟空間等)。每個任務也具有優先順序,如果優先順序較高的任務需要更多的資源,則可以終止(搶佔)同一臺機器上較低優先順序的任務以釋放資源。優先順序還決定了計算資源的定價:團隊必須為他們使用的資源付費,而優先順序更高的程序花費更多【59】。
-
-這種架構允許非生產(低優先順序)計算資源被 **過量使用(overcommitted)**,因為系統知道必要時它可以回收資源。與分離生產和非生產任務的系統相比,過量使用資源可以更好地利用機器並提高效率。但由於 MapReduce 作業以低優先順序執行,它們隨時都有被搶佔的風險,因為優先順序較高的程序可能需要其資源。在高優先順序程序拿走所需資源後,批次作業能有效地 “撿麵包屑”,利用剩下的任何計算資源。
-
-在谷歌,執行一個小時的 MapReduce 任務有大約有 5% 的風險被終止,為了給更高優先順序的程序挪地方。這一機率比硬體問題、機器重啟或其他原因的機率高了一個數量級【59】。按照這種搶佔率,如果一個作業有 100 個任務,每個任務執行 10 分鐘,那麼至少有一個任務在完成之前被終止的風險大於 50%。
-
-這就是 MapReduce 被設計為容忍頻繁意外任務終止的原因:不是因為硬體很不可靠,而是因為任意終止程序的自由有利於提高計算叢集中的資源利用率。
-
-在開源的叢集排程器中,搶佔的使用較少。YARN 的 CapacityScheduler 支援搶佔,以平衡不同佇列的資源分配【58】,但在編寫本文時,YARN,Mesos 或 Kubernetes 不支援通用的優先順序搶佔【60】。在任務不經常被終止的環境中,MapReduce 的這一設計決策就沒有多少意義了。在下一節中,我們將研究一些與 MapReduce 設計決策相異的替代方案。
-
-
-## MapReduce之後
-
-雖然 MapReduce 在 2000 年代後期變得非常流行,並受到大量的炒作,但它只是分散式系統的許多可能的程式設計模型之一。對於不同的資料量,資料結構和處理型別,其他工具可能更適合表示計算。
-
-
-不管如何,我們在這一章花了大把時間來討論 MapReduce,因為它是一種有用的學習工具,它是分散式檔案系統的一種相當簡單明晰的抽象。在這裡,**簡單** 意味著我們能理解它在做什麼,而不是意味著使用它很簡單。恰恰相反:使用原始的 MapReduce API 來實現複雜的處理工作實際上是非常困難和費力的 —— 例如,任意一種連線演算法都需要你從頭開始實現【37】。
-
-針對直接使用 MapReduce 的困難,在 MapReduce 上有很多高階程式設計模型(Pig、Hive、Cascading、Crunch)被創造出來,作為建立在 MapReduce 之上的抽象。如果你瞭解 MapReduce 的原理,那麼它們學起來相當簡單。而且它們的高階結構能顯著簡化許多常見批處理任務的實現。
-
-但是,MapReduce 執行模型本身也存在一些問題,這些問題並沒有透過增加另一個抽象層次而解決,而對於某些型別的處理,它表現得非常差勁。一方面,MapReduce 非常穩健:你可以使用它在任務會頻繁終止的多租戶系統上處理幾乎任意大量級的資料,並且仍然可以完成工作(雖然速度很慢)。另一方面,對於某些型別的處理而言,其他工具有時會快上幾個數量級。
-
-在本章的其餘部分中,我們將介紹一些批處理方法。在 [第十二章](/tw/ch12) 我們將轉向流處理,它可以看作是加速批處理的另一種方法。
-
-### 物化中間狀態
-
-如前所述,每個 MapReduce 作業都獨立於其他任何作業。作業與世界其他地方的主要連線點是分散式檔案系統上的輸入和輸出目錄。如果希望一個作業的輸出成為第二個作業的輸入,則需要將第二個作業的輸入目錄配置為第一個作業輸出目錄,且外部工作流排程程式必須在第一個作業完成後再啟動第二個。
-
-如果第一個作業的輸出是要在組織內廣泛釋出的資料集,則這種配置是合理的。在這種情況下,你需要透過名稱引用它,並將其重用為多個不同作業的輸入(包括由其他團隊開發的作業)。將資料釋出到分散式檔案系統中眾所周知的位置能夠帶來 **松耦合**,這樣作業就不需要知道是誰在提供輸入或誰在消費輸出(請參閱 “[邏輯與佈線相分離](#邏輯與佈線相分離)”)。
-
-但在很多情況下,你知道一個作業的輸出只能用作另一個作業的輸入,這些作業由同一個團隊維護。在這種情況下,分散式檔案系統上的檔案只是簡單的 **中間狀態(intermediate state)**:一種將資料從一個作業傳遞到下一個作業的方式。在一個用於構建推薦系統的,由 50 或 100 個 MapReduce 作業組成的複雜工作流中,存在著很多這樣的中間狀態【29】。
-
-將這個中間狀態寫入檔案的過程稱為 **物化(materialization)**。(在 “[聚合:資料立方體和物化檢視](/tw/ch3#聚合:資料立方體和物化檢視)” 中已經在物化檢視的背景中遇到過這個術語。它意味著對某個操作的結果立即求值並寫出來,而不是在請求時按需計算)
-
-作為對照,本章開頭的日誌分析示例使用 Unix 管道將一個命令的輸出與另一個命令的輸入連線起來。管道並沒有完全物化中間狀態,而是隻使用一個小的記憶體緩衝區,將輸出增量地 **流(stream)** 向輸入。
-
-與 Unix 管道相比,MapReduce 完全物化中間狀態的方法存在不足之處:
-
-- MapReduce 作業只有在前驅作業(生成其輸入)中的所有任務都完成時才能啟動,而由 Unix 管道連線的程序會同時啟動,輸出一旦生成就會被消費。不同機器上的資料偏斜或負載不均意味著一個作業往往會有一些掉隊的任務,比其他任務要慢得多才能完成。必須等待至前驅作業的所有任務完成,拖慢了整個工作流程的執行。
-- Mapper 通常是多餘的:它們僅僅是讀取剛剛由 Reducer 寫入的同樣檔案,為下一個階段的分割槽和排序做準備。在許多情況下,Mapper 程式碼可能是前驅 Reducer 的一部分:如果 Reducer 和 Mapper 的輸出有著相同的分割槽與排序方式,那麼 Reducer 就可以直接串在一起,而不用與 Mapper 相互交織。
-- 將中間狀態儲存在分散式檔案系統中意味著這些檔案被複制到多個節點,對這些臨時資料這麼搞就比較過分了。
-
-#### 資料流引擎
-
-為了解決 MapReduce 的這些問題,幾種用於分散式批處理的新執行引擎被開發出來,其中最著名的是 Spark 【61,62】,Tez 【63,64】和 Flink 【65,66】。它們的設計方式有很多區別,但有一個共同點:把整個工作流作為單個作業來處理,而不是把它分解為獨立的子作業。
-
-由於它們將工作流顯式建模為資料從幾個處理階段穿過,所以這些系統被稱為 **資料流引擎(dataflow engines)**。像 MapReduce 一樣,它們在一條線上透過反覆呼叫使用者定義的函式來一次處理一條記錄,它們透過輸入分割槽來並行化載荷,它們透過網路將一個函式的輸出複製到另一個函式的輸入。
-
-與 MapReduce 不同,這些函式不需要嚴格扮演交織的 Map 與 Reduce 的角色,而是可以以更靈活的方式進行組合。我們稱這些函式為 **運算元(operators)**,資料流引擎提供了幾種不同的選項來將一個運算元的輸出連線到另一個運算元的輸入:
-
-- 一種選項是對記錄按鍵重新分割槽並排序,就像在 MapReduce 的混洗階段一樣(請參閱 “[分散式執行 MapReduce](#分散式執行MapReduce)”)。這種功能可以用於實現排序合併連線和分組,就像在 MapReduce 中一樣。
-- 另一種可能是接受多個輸入,並以相同的方式進行分割槽,但跳過排序。當記錄的分割槽重要但順序無關緊要時,這省去了分割槽雜湊連線的工作,因為構建散列表還是會把順序隨機打亂。
-- 對於廣播雜湊連線,可以將一個運算元的輸出,傳送到連線運算元的所有分割槽。
-
-這種型別的處理引擎是基於像 Dryad【67】和 Nephele【68】這樣的研究系統,與 MapReduce 模型相比,它有幾個優點:
-
-- 排序等昂貴的工作只需要在實際需要的地方執行,而不是預設地在每個 Map 和 Reduce 階段之間出現。
-- 沒有不必要的 Map 任務,因為 Mapper 所做的工作通常可以合併到前面的 Reduce 運算元中(因為 Mapper 不會更改資料集的分割槽)。
-- 由於工作流中的所有連線和資料依賴都是顯式宣告的,因此排程程式能夠總覽全域性,知道哪裡需要哪些資料,因而能夠利用區域性進行最佳化。例如,它可以嘗試將消費某些資料的任務放在與生成這些資料的任務相同的機器上,從而資料可以透過共享記憶體緩衝區傳輸,而不必透過網路複製。
-- 通常,運算元間的中間狀態足以儲存在記憶體中或寫入本地磁碟,這比寫入 HDFS 需要更少的 I/O(必須將其複製到多臺機器,並將每個副本寫入磁碟)。MapReduce 已經對 Mapper 的輸出做了這種最佳化,但資料流引擎將這種思想推廣至所有的中間狀態。
-- 運算元可以在輸入就緒後立即開始執行;後續階段無需等待前驅階段整個完成後再開始。
-- 與 MapReduce(為每個任務啟動一個新的 JVM)相比,現有 Java 虛擬機器(JVM)程序可以重用來執行新運算元,從而減少啟動開銷。
-
-你可以使用資料流引擎執行與 MapReduce 工作流同樣的計算,而且由於此處所述的最佳化,通常執行速度要明顯快得多。既然運算元是 Map 和 Reduce 的泛化,那麼相同的處理程式碼就可以在任一執行引擎上執行:Pig,Hive 或 Cascading 中實現的工作流可以無需修改程式碼,可以透過修改配置,簡單地從 MapReduce 切換到 Tez 或 Spark【64】。
-
-Tez 是一個相當薄的庫,它依賴於 YARN shuffle 服務來實現節點間資料的實際複製【58】,而 Spark 和 Flink 則是包含了獨立網路通訊層,排程器,及使用者向 API 的大型框架。我們將簡要討論這些高階 API。
-
-#### 容錯
-
-完全物化中間狀態至分散式檔案系統的一個優點是,它具有永續性,這使得 MapReduce 中的容錯相當容易:如果一個任務失敗,它可以在另一臺機器上重新啟動,並從檔案系統重新讀取相同的輸入。
-
-Spark、Flink 和 Tez 避免將中間狀態寫入 HDFS,因此它們採取了不同的方法來容錯:如果一臺機器發生故障,並且該機器上的中間狀態丟失,則它會從其他仍然可用的資料重新計算(在可行的情況下是先前的中間狀態,要麼就只能是原始輸入資料,通常在 HDFS 上)。
-
-為了實現這種重新計算,框架必須跟蹤一個給定的資料是如何計算的 —— 使用了哪些輸入分割槽?應用了哪些運算元? Spark 使用 **彈性分散式資料集(RDD,Resilient Distributed Dataset)** 的抽象來跟蹤資料的譜系【61】,而 Flink 對運算元狀態存檔,允許恢復執行在執行過程中遇到錯誤的運算元【66】。
-
-在重新計算資料時,重要的是要知道計算是否是 **確定性的**:也就是說,給定相同的輸入資料,運算元是否始終產生相同的輸出?如果一些丟失的資料已經發送給下游運算元,這個問題就很重要。如果運算元重新啟動,重新計算的資料與原有的丟失資料不一致,下游運算元很難解決新舊資料之間的矛盾。對於不確定性運算元來說,解決方案通常是殺死下游運算元,然後再重跑新資料。
-
-為了避免這種級聯故障,最好讓運算元具有確定性。但需要注意的是,非確定性行為很容易悄悄溜進來:例如,許多程式語言在迭代雜湊表的元素時不能對順序作出保證,許多機率和統計算法顯式依賴於使用隨機數,以及用到系統時鐘或外部資料來源,這些都是都不確定性的行為。為了能可靠地從故障中恢復,需要消除這種不確定性因素,例如使用固定的種子生成偽隨機數。
-
-透過重算資料來從故障中恢復並不總是正確的答案:如果中間狀態資料要比源資料小得多,或者如果計算量非常大,那麼將中間資料物化為檔案可能要比重新計算廉價的多。
-
-#### 關於物化的討論
-
-回到 Unix 的類比,我們看到,MapReduce 就像是將每個命令的輸出寫入臨時檔案,而資料流引擎看起來更像是 Unix 管道。尤其是 Flink 是基於管道執行的思想而建立的:也就是說,將運算元的輸出增量地傳遞給其他運算元,不待輸入完成便開始處理。
-
-排序運算元不可避免地需要消費全部的輸入後才能生成任何輸出,因為輸入中最後一條輸入記錄可能具有最小的鍵,因此需要作為第一條記錄輸出。因此,任何需要排序的運算元都需要至少暫時地累積狀態。但是工作流的許多其他部分可以以流水線方式執行。
-
-當作業完成時,它的輸出需要持續到某個地方,以便使用者可以找到並使用它 —— 很可能它會再次寫入分散式檔案系統。因此,在使用資料流引擎時,HDFS 上的物化資料集通常仍是作業的輸入和最終輸出。和 MapReduce 一樣,輸入是不可變的,輸出被完全替換。比起 MapReduce 的改進是,你不用再自己去將中間狀態寫入檔案系統了。
-
-### 圖與迭代處理
-
-在 “[圖資料模型](/tw/ch2#圖資料模型)” 中,我們討論了使用圖來建模資料,並使用圖查詢語言來遍歷圖中的邊與點。[第二章](/tw/ch2) 的討論集中在 OLTP 風格的應用場景:快速執行查詢來查詢少量符合特定條件的頂點。
-
-批處理上下文中的圖也很有趣,其目標是在整個圖上執行某種離線處理或分析。這種需求經常出現在機器學習應用(如推薦引擎)或排序系統中。例如,最著名的圖形分析演算法之一是 PageRank 【69】,它試圖根據連結到某個網頁的其他網頁來估計該網頁的流行度。它作為配方的一部分,用於確定網路搜尋引擎呈現結果的順序。
-
-> 像 Spark、Flink 和 Tez 這樣的資料流引擎(請參閱 “[物化中間狀態](#物化中間狀態)”)通常將運算元作為 **有向無環圖(DAG)** 的一部分安排在作業中。這與圖處理不一樣:在資料流引擎中,**從一個運算元到另一個運算元的資料流** 被構造成一個圖,而資料本身通常由關係型元組構成。在圖處理中,資料本身具有圖的形式。又一個不幸的命名混亂!
-
-許多圖演算法是透過一次遍歷一條邊來表示的,將一個頂點與近鄰的頂點連線起來,以傳播一些資訊,並不斷重複,直到滿足一些條件為止 —— 例如,直到沒有更多的邊要跟進,或直到一些指標收斂。我們在 [圖 2-6](/v1/ddia_0206.png) 中看到一個例子,它透過重複跟進標明地點歸屬關係的邊,生成了資料庫中北美包含的所有地點列表(這種演算法被稱為 **傳遞閉包**,即 transitive closure)。
-
-可以在分散式檔案系統中儲存圖(包含頂點和邊的列表的檔案),但是這種 “重複至完成” 的想法不能用普通的 MapReduce 來表示,因為它只掃過一趟資料。這種演算法因此經常以 **迭代** 的風格實現:
-
-1. 外部排程程式執行批處理來計算演算法的一個步驟。
-2. 當批處理過程完成時,排程器檢查它是否完成(基於完成條件 —— 例如,沒有更多的邊要跟進,或者與上次迭代相比的變化低於某個閾值)。
-3. 如果尚未完成,則排程程式返回到步驟 1 並執行另一輪批處理。
-
-這種方法是有效的,但是用 MapReduce 實現它往往非常低效,因為 MapReduce 沒有考慮演算法的迭代性質:它總是讀取整個輸入資料集併產生一個全新的輸出資料集,即使與上次迭代相比,改變的僅僅是圖中的一小部分。
-
-#### Pregel處理模型
-
-針對圖批處理的最佳化 —— **批次同步並行(BSP,Bulk Synchronous Parallel)** 計算模型【70】已經開始流行起來。其中,Apache Giraph 【37】,Spark 的 GraphX API 和 Flink 的 Gelly API 【71】實現了它。它也被稱為 **Pregel** 模型,因為 Google 的 Pregel 論文推廣了這種處理圖的方法【72】。
-
-回想一下在 MapReduce 中,Mapper 在概念上向 Reducer 的特定呼叫 “傳送訊息”,因為框架將所有具有相同鍵的 Mapper 輸出集中在一起。Pregel 背後有一個類似的想法:一個頂點可以向另一個頂點 “傳送訊息”,通常這些訊息是沿著圖的邊傳送的。
-
-在每次迭代中,為每個頂點呼叫一個函式,將所有傳送給它的訊息傳遞給它 —— 就像呼叫 Reducer 一樣。與 MapReduce 的不同之處在於,在 Pregel 模型中,頂點在一次迭代到下一次迭代的過程中會記住它的狀態,所以這個函式只需要處理新的傳入訊息。如果圖的某個部分沒有被傳送訊息,那裡就不需要做任何工作。
-
-這與 Actor 模型有些相似(請參閱 “[分散式的 Actor 框架](/tw/ch4#分散式的Actor框架)”),除了頂點狀態和頂點之間的訊息具有容錯性和永續性,且通訊以固定的回合進行:在每次迭代中,框架遞送上次迭代中傳送的所有訊息。Actor 通常沒有這樣的時序保證。
-
-#### 容錯
-
-頂點只能透過訊息傳遞進行通訊(而不是直接相互查詢)的事實有助於提高 Pregel 作業的效能,因為訊息可以成批處理,且等待通訊的次數也減少了。唯一的等待是在迭代之間:由於 Pregel 模型保證所有在一輪迭代中傳送的訊息都在下輪迭代中送達,所以在下一輪迭代開始前,先前的迭代必須完全完成,而所有的訊息必須在網路上完成複製。
-
-即使底層網路可能丟失、重複或任意延遲訊息(請參閱 “[不可靠的網路](/tw/ch8#不可靠的網路)”),Pregel 的實現能保證在後續迭代中訊息在其目標頂點恰好處理一次。像 MapReduce 一樣,框架能從故障中透明地恢復,以簡化在 Pregel 上實現演算法的程式設計模型。
-
-這種容錯是透過在迭代結束時,定期存檔所有頂點的狀態來實現的,即將其全部狀態寫入持久化儲存。如果某個節點發生故障並且其記憶體中的狀態丟失,則最簡單的解決方法是將整個圖計算回滾到上一個存檔點,然後重啟計算。如果演算法是確定性的,且訊息記錄在日誌中,那麼也可以選擇性地只恢復丟失的分割槽(就像之前討論過的資料流引擎)【72】。
-
-#### 並行執行
-
-頂點不需要知道它在哪臺物理機器上執行;當它向其他頂點發送訊息時,它只是簡單地將訊息發往某個頂點 ID。圖的分割槽取決於框架 —— 即,確定哪個頂點執行在哪臺機器上,以及如何透過網路路由訊息,以便它們到達正確的地方。
-
-由於程式設計模型一次僅處理一個頂點(有時稱為 “像頂點一樣思考”),所以框架可以以任意方式對圖分割槽。理想情況下如果頂點需要進行大量的通訊,那麼它們最好能被分割槽到同一臺機器上。然而找到這樣一種最佳化的分割槽方法是很困難的 —— 在實踐中,圖經常按照任意分配的頂點 ID 分割槽,而不會嘗試將相關的頂點分組在一起。
-
-因此,圖演算法通常會有很多跨機器通訊的額外開銷,而中間狀態(節點之間傳送的訊息)往往比原始圖大。透過網路傳送訊息的開銷會顯著拖慢分散式圖演算法的速度。
-
-出於這個原因,如果你的圖可以放入一臺計算機的記憶體中,那麼單機(甚至可能是單執行緒)演算法很可能會超越分散式批處理【73,74】。圖比記憶體大也沒關係,只要能放入單臺計算機的磁碟,使用 GraphChi 等框架進行單機處理是就一個可行的選擇【75】。如果圖太大,不適合單機處理,那麼像 Pregel 這樣的分散式方法是不可避免的。高效的並行圖演算法是一個進行中的研究領域【76】。
-
-
-### 高階API和語言
-
-自 MapReduce 開始流行的這幾年以來,分散式批處理的執行引擎已經很成熟了。到目前為止,基礎設施已經足夠強大,能夠儲存和處理超過 10,000 臺機器叢集上的數 PB 的資料。由於在這種規模下物理執行批處理的問題已經被認為或多或少解決了,所以關注點已經轉向其他領域:改進程式設計模型,提高處理效率,擴大這些技術可以解決的問題集。
-
-如前所述,Hive、Pig、Cascading 和 Crunch 等高階語言和 API 變得越來越流行,因為手寫 MapReduce 作業實在是個苦力活。隨著 Tez 的出現,這些高階語言還有一個額外好處,可以遷移到新的資料流執行引擎,而無需重寫作業程式碼。Spark 和 Flink 也有它們自己的高階資料流 API,通常是從 FlumeJava 中獲取的靈感【34】。
-
-這些資料流 API 通常使用關係型構建塊來表達一個計算:按某個欄位連線資料集;按鍵對元組做分組;按某些條件過濾;並透過計數求和或其他函式來聚合元組。在內部,這些操作是使用本章前面討論過的各種連線和分組演算法來實現的。
-
-除了少寫程式碼的明顯優勢之外,這些高階介面還支援互動式用法,在這種互動式使用中,你可以在 Shell 中增量式編寫分析程式碼,頻繁執行來觀察它做了什麼。這種開發風格在探索資料集和試驗處理方法時非常有用。這也讓人聯想到 Unix 哲學,我們在 “[Unix 哲學](#Unix哲學)” 中討論過這個問題。
-
-此外,這些高階介面不僅提高了人類的工作效率,也提高了機器層面的作業執行效率。
-
-#### 向宣告式查詢語言的轉變
-
-與硬寫執行連線的程式碼相比,指定連線關係運算元的優點是,框架可以分析連線輸入的屬性,並自動決定哪種上述連線演算法最適合當前任務。Hive、Spark 和 Flink 都有基於代價的查詢最佳化器可以做到這一點,甚至可以改變連線順序,最小化中間狀態的數量【66,77,78,79】。
-
-連線演算法的選擇可以對批處理作業的效能產生巨大影響,而無需理解和記住本章中討論的各種連線演算法。如果連線是以 **宣告式(declarative)** 的方式指定的,那這就這是可行的:應用只是簡單地說明哪些連線是必需的,查詢最佳化器決定如何最好地執行連線。我們以前在 “[資料查詢語言](/tw/ch2#資料查詢語言)” 中見過這個想法。
-
-但 MapReduce 及其資料流後繼者在其他方面,與 SQL 的完全宣告式查詢模型有很大區別。MapReduce 是圍繞著回撥函式的概念建立的:對於每條記錄或者一組記錄,呼叫一個使用者定義的函式(Mapper 或 Reducer),並且該函式可以自由地呼叫任意程式碼來決定輸出什麼。這種方法的優點是可以基於大量已有庫的生態系統創作:解析、自然語言分析、影像分析以及執行數值或統計算法等。
-
-自由執行任意程式碼,長期以來都是傳統 MapReduce 批處理系統與 MPP 資料庫的區別所在(請參閱 “[Hadoop 與分散式資料庫的對比](#Hadoop與分散式資料庫的對比)” 一節)。雖然資料庫具有編寫使用者定義函式的功能,但是它們通常使用起來很麻煩,而且與大多數程式語言中廣泛使用的程式包管理器和依賴管理系統相容不佳(例如 Java 的 Maven、Javascript 的 npm 以及 Ruby 的 gems)。
-
-然而資料流引擎已經發現,支援除連線之外的更多 **宣告式特性** 還有其他的優勢。例如,如果一個回撥函式只包含一個簡單的過濾條件,或者只是從一條記錄中選擇了一些欄位,那麼在為每條記錄呼叫函式時會有相當大的額外 CPU 開銷。如果以宣告方式表示這些簡單的過濾和對映操作,那麼查詢最佳化器可以利用列式儲存佈局(請參閱 “[列式儲存](/tw/ch3#列式儲存)”),只從磁碟讀取所需的列。Hive、Spark DataFrames 和 Impala 還使用了向量化執行(請參閱 “[記憶體頻寬和向量化處理](/tw/ch3#記憶體頻寬和向量化處理)”):在對 CPU 快取友好的內部迴圈中迭代資料,避免函式呼叫。Spark 生成 JVM 位元組碼【79】,Impala 使用 LLVM 為這些內部迴圈生成本機程式碼【41】。
-
-透過在高階 API 中引入宣告式的部分,並使查詢最佳化器可以在執行期間利用這些來做最佳化,批處理框架看起來越來越像 MPP 資料庫了(並且能實現可與之媲美的效能)。同時,透過擁有執行任意程式碼和以任意格式讀取資料的可擴充套件性,它們保持了靈活性的優勢。
-
-#### 專業化的不同領域
-
-儘管能夠執行任意程式碼的可擴充套件性是很有用的,但是也有很多常見的例子,不斷重複著標準的處理模式。因而這些模式值得擁有自己的可重用通用構建模組實現。傳統上,MPP 資料庫滿足了商業智慧分析和業務報表的需求,但這只是許多使用批處理的領域之一。
-
-另一個越來越重要的領域是統計和數值演算法,它們是機器學習應用所需要的(例如分類器和推薦系統)。可重用的實現正在出現:例如,Mahout 在 MapReduce、Spark 和 Flink 之上實現了用於機器學習的各種演算法,而 MADlib 在關係型 MPP 資料庫(Apache HAWQ)中實現了類似的功能【54】。
-
-空間演算法也是有用的,例如 **k 近鄰搜尋(k-nearest neighbors, kNN)**【80】,它在一些多維空間中搜索與給定項最近的專案 —— 這是一種相似性搜尋。近似搜尋對於基因組分析演算法也很重要,它們需要找到相似但不相同的字串【81】。
-
-批處理引擎正被用於分散式執行日益廣泛的各領域演算法。隨著批處理系統獲得各種內建功能以及高階宣告式運算元,且隨著 MPP 資料庫變得更加靈活和易於程式設計,兩者開始看起來相似了:最終,它們都只是儲存和處理資料的系統。
-
-
-## 本章小結
-
-在本章中,我們探索了批處理的主題。我們首先看到了諸如 awk、grep 和 sort 之類的 Unix 工具,然後我們看到了這些工具的設計理念是如何應用到 MapReduce 和更近的資料流引擎中的。一些設計原則包括:輸入是不可變的,輸出是為了作為另一個(仍未知的)程式的輸入,而複雜的問題是透過編寫 “做好一件事” 的小工具來解決的。
-
-在 Unix 世界中,允許程式與程式組合的統一介面是檔案與管道;在 MapReduce 中,該介面是一個分散式檔案系統。我們看到資料流引擎添加了自己的管道式資料傳輸機制,以避免將中間狀態物化至分散式檔案系統,但作業的初始輸入和最終輸出通常仍是 HDFS。
-
-分散式批處理框架需要解決的兩個主要問題是:
-
-分割槽
-: 在 MapReduce 中,Mapper 根據輸入檔案塊進行分割槽。Mapper 的輸出被重新分割槽、排序併合併到可配置數量的 Reducer 分割槽中。這一過程的目的是把所有的 **相關** 資料(例如帶有相同鍵的所有記錄)都放在同一個地方。
- 後 MapReduce 時代的資料流引擎若非必要會盡量避免排序,但它們也採取了大致類似的分割槽方法。
-
-容錯
-: MapReduce 經常寫入磁碟,這使得從單個失敗的任務恢復很輕鬆,無需重新啟動整個作業,但在無故障的情況下減慢了執行速度。資料流引擎更多地將中間狀態儲存在記憶體中,更少地物化中間狀態,這意味著如果節點發生故障,則需要重算更多的資料。確定性運算元減少了需要重算的資料量。
-
-
-我們討論了幾種 MapReduce 的連線演算法,其中大多數也在 MPP 資料庫和資料流引擎內部使用。它們也很好地演示了分割槽演算法是如何工作的:
-
-排序合併連線
-: 每個參與連線的輸入都透過一個提取連線鍵的 Mapper。透過分割槽、排序和合並,具有相同鍵的所有記錄最終都會進入相同的 Reducer 呼叫。這個函式能輸出連線好的記錄。
-
-廣播雜湊連線
-: 兩個連線輸入之一很小,所以它並沒有分割槽,而且能被完全載入進一個雜湊表中。因此,你可以為連線輸入大端的每個分割槽啟動一個 Mapper,將輸入小端的散列表載入到每個 Mapper 中,然後掃描大端,一次一條記錄,併為每條記錄查詢散列表。
-
-分割槽雜湊連線
-: 如果兩個連線輸入以相同的方式分割槽(使用相同的鍵,相同的雜湊函式和相同數量的分割槽),則可以獨立地對每個分割槽應用散列表方法。
-
-分散式批處理引擎有一個刻意限制的程式設計模型:回撥函式(比如 Mapper 和 Reducer)被假定是無狀態的,而且除了指定的輸出外,必須沒有任何外部可見的副作用。這一限制允許框架在其抽象下隱藏一些困難的分散式系統問題:當遇到崩潰和網路問題時,任務可以安全地重試,任何失敗任務的輸出都被丟棄。如果某個分割槽的多個任務成功,則其中只有一個能使其輸出實際可見。
-
-得益於這個框架,你在批處理作業中的程式碼無需操心實現容錯機制:框架可以保證作業的最終輸出與沒有發生錯誤的情況相同,雖然實際上也許不得不重試各種任務。比起線上服務一邊處理使用者請求一邊將寫入資料庫作為處理請求的副作用,批處理提供的這種可靠性語義要強得多。
-
-批處理作業的顯著特點是,它讀取一些輸入資料併產生一些輸出資料,但不修改輸入 —— 換句話說,輸出是從輸入派生出的。最關鍵的是,輸入資料是 **有界的(bounded)**:它有一個已知的,固定的大小(例如,它包含一些時間點的日誌檔案或資料庫內容的快照)。因為它是有界的,一個作業知道自己什麼時候完成了整個輸入的讀取,所以一個工作在做完後,最終總是會完成的。
-
-在下一章中,我們將轉向流處理,其中的輸入是 **無界的(unbounded)** —— 也就是說,你還有活兒要幹,然而它的輸入是永無止境的資料流。在這種情況下,作業永無完成之日。因為在任何時候都可能有更多的工作湧入。我們將看到,在某些方面上,流處理和批處理是相似的。但是關於無盡資料流的假設也對我們構建系統的方式產生了很多改變。
-
-
-## 參考文獻
-
-1. Jeffrey Dean and Sanjay Ghemawat: “[MapReduce: Simplified Data Processing on Large Clusters](https://research.google/pubs/pub62/),” at *6th USENIX Symposium on Operating System Design and Implementation* (OSDI), December 2004.
-1. Joel Spolsky: “[The Perils of JavaSchools](https://www.joelonsoftware.com/2005/12/29/the-perils-of-javaschools-2/),” *joelonsoftware.com*, December 29, 2005.
-1. Shivnath Babu and Herodotos Herodotou: “[Massively Parallel Databases and MapReduce Systems](https://www.microsoft.com/en-us/research/wp-content/uploads/2013/11/db-mr-survey-final.pdf),” *Foundations and Trends in Databases*, volume 5, number 1, pages 1–104, November 2013. [doi:10.1561/1900000036](http://dx.doi.org/10.1561/1900000036)
-1. David J. DeWitt and Michael Stonebraker: “[MapReduce: A Major Step Backwards](https://homes.cs.washington.edu/~billhowe/mapreduce_a_major_step_backwards.html),” originally published at *databasecolumn.vertica.com*, January 17, 2008.
-1. Henry Robinson: “[The Elephant Was a Trojan Horse: On the Death of Map-Reduce at Google](https://www.the-paper-trail.org/post/2014-06-25-the-elephant-was-a-trojan-horse-on-the-death-of-map-reduce-at-google/),” *the-paper-trail.org*, June 25, 2014.
-1. “[The Hollerith Machine](https://www.census.gov/history/www/innovations/technology/the_hollerith_tabulator.html),” United States Census Bureau, *census.gov*.
-1. “[IBM 82, 83, and 84 Sorters Reference Manual](https://bitsavers.org/pdf/ibm/punchedCard/Sorter/A24-1034-1_82-83-84_sorters.pdf),” Edition A24-1034-1, International Business Machines Corporation, July 1962.
-1. Adam Drake: “[Command-Line Tools Can Be 235x Faster than Your Hadoop Cluster](https://adamdrake.com/command-line-tools-can-be-235x-faster-than-your-hadoop-cluster.html),” *aadrake.com*, January 25, 2014.
-1. “[GNU Coreutils 8.23 Documentation](http://www.gnu.org/software/coreutils/manual/html_node/index.html),” Free Software Foundation, Inc., 2014.
-1. Martin Kleppmann: “[Kafka, Samza, and the Unix Philosophy of Distributed Data](http://martin.kleppmann.com/2015/08/05/kafka-samza-unix-philosophy-distributed-data.html),” *martin.kleppmann.com*, August 5, 2015.
-1. Doug McIlroy: [Internal Bell Labs memo](https://swtch.com/~rsc/thread/mdmpipe.pdf), October 1964. Cited in: Dennis M. Richie: “[Advice from Doug McIlroy](https://www.bell-labs.com/usr/dmr/www/mdmpipe.html),” *bell-labs.com*.
-1. M. D. McIlroy, E. N. Pinson, and B. A. Tague: “[UNIX Time-Sharing System: Foreword](https://archive.org/details/bstj57-6-1899),” *The Bell System Technical Journal*, volume 57, number 6, pages 1899–1904, July 1978.
-1. Eric S. Raymond: [*The Art of UNIX Programming*](http://www.catb.org/~esr/writings/taoup/html/). Addison-Wesley, 2003. ISBN: 978-0-13-142901-7
-1. Ronald Duncan: “[Text File Formats – ASCII Delimited Text – Not CSV or TAB Delimited Text](https://ronaldduncan.wordpress.com/2009/10/31/text-file-formats-ascii-delimited-text-not-csv-or-tab-delimited-text/),” *ronaldduncan.wordpress.com*, October 31, 2009.
-1. Alan Kay: “[Is 'Software Engineering' an Oxymoron?](http://tinlizzie.org/~takashi/IsSoftwareEngineeringAnOxymoron.pdf),” *tinlizzie.org*.
-1. Martin Fowler: “[InversionOfControl](http://martinfowler.com/bliki/InversionOfControl.html),” *martinfowler.com*, June 26, 2005.
-1. Daniel J. Bernstein: “[Two File Descriptors for Sockets](http://cr.yp.to/tcpip/twofd.html),” *cr.yp.to*.
-1. Rob Pike and Dennis M. Ritchie: “[The Styx Architecture for Distributed Systems](http://doc.cat-v.org/inferno/4th_edition/styx),” *Bell Labs Technical Journal*, volume 4, number 2, pages 146–152, April 1999.
-1. Sanjay Ghemawat, Howard Gobioff, and Shun-Tak Leung: “[The Google File System](http://research.google.com/archive/gfs-sosp2003.pdf),” at *19th ACM Symposium on Operating Systems Principles* (SOSP), October 2003. [doi:10.1145/945445.945450](http://dx.doi.org/10.1145/945445.945450)
-1. Michael Ovsiannikov, Silvius Rus, Damian Reeves, et al.: “[The Quantcast File System](http://db.disi.unitn.eu/pages/VLDBProgram/pdf/industry/p808-ovsiannikov.pdf),” *Proceedings of the VLDB Endowment*, volume 6, number 11, pages 1092–1101, August 2013. [doi:10.14778/2536222.2536234](http://dx.doi.org/10.14778/2536222.2536234)
-1. “[OpenStack Swift 2.6.1 Developer Documentation](http://docs.openstack.org/developer/swift/),” OpenStack Foundation, *docs.openstack.org*, March 2016.
-1. Zhe Zhang, Andrew Wang, Kai Zheng, et al.: “[Introduction to HDFS Erasure Coding in Apache Hadoop](https://blog.cloudera.com/introduction-to-hdfs-erasure-coding-in-apache-hadoop/),” *blog.cloudera.com*, September 23, 2015.
-1. Peter Cnudde: “[Hadoop Turns 10](https://web.archive.org/web/20190119112713/https://yahoohadoop.tumblr.com/post/138739227316/hadoop-turns-10),” *yahoohadoop.tumblr.com*, February 5, 2016.
-1. Eric Baldeschwieler: “[Thinking About the HDFS vs. Other Storage Technologies](https://web.archive.org/web/20190529215115/http://hortonworks.com/blog/thinking-about-the-hdfs-vs-other-storage-technologies/),” *hortonworks.com*, July 25, 2012.
-1. Brendan Gregg: “[Manta: Unix Meets Map Reduce](https://web.archive.org/web/20220125052545/http://dtrace.org/blogs/brendan/2013/06/25/manta-unix-meets-map-reduce/),” *dtrace.org*, June 25, 2013.
-1. Tom White: *Hadoop: The Definitive Guide*, 4th edition. O'Reilly Media, 2015. ISBN: 978-1-491-90163-2
-1. Jim N. Gray: “[Distributed Computing Economics](http://arxiv.org/pdf/cs/0403019.pdf),” Microsoft Research Tech Report MSR-TR-2003-24, March 2003.
-1. Márton Trencséni: “[Luigi vs Airflow vs Pinball](http://bytepawn.com/luigi-airflow-pinball.html),” *bytepawn.com*, February 6, 2016.
-1. Roshan Sumbaly, Jay Kreps, and Sam Shah: “[The 'Big Data' Ecosystem at LinkedIn](http://www.slideshare.net/s_shah/the-big-data-ecosystem-at-linkedin-23512853),” at *ACM International Conference on Management of Data* (SIGMOD), July 2013. [doi:10.1145/2463676.2463707](http://dx.doi.org/10.1145/2463676.2463707)
-1. Alan F. Gates, Olga Natkovich, Shubham Chopra, et al.: “[Building a High-Level Dataflow System on Top of Map-Reduce: The Pig Experience](http://www.vldb.org/pvldb/vol2/vldb09-1074.pdf),” at *35th International Conference on Very Large Data Bases* (VLDB), August 2009.
-1. Ashish Thusoo, Joydeep Sen Sarma, Namit Jain, et al.: “[Hive – A Petabyte Scale Data Warehouse Using Hadoop](http://i.stanford.edu/~ragho/hive-icde2010.pdf),” at *26th IEEE International Conference on Data Engineering* (ICDE), March 2010. [doi:10.1109/ICDE.2010.5447738](http://dx.doi.org/10.1109/ICDE.2010.5447738)
-1. “[Cascading 3.0 User Guide](https://web.archive.org/web/20231206195311/http://docs.cascading.org/cascading/3.0/userguide/),” Concurrent, Inc., *docs.cascading.org*, January 2016.
-1. “[Apache Crunch User Guide](https://crunch.apache.org/user-guide.html),” Apache Software Foundation, *crunch.apache.org*.
-1. Craig Chambers, Ashish Raniwala, Frances Perry, et al.: “[FlumeJava: Easy, Efficient Data-Parallel Pipelines](https://research.google.com/pubs/archive/35650.pdf),” at *31st ACM SIGPLAN Conference on Programming Language Design and Implementation* (PLDI), June 2010. [doi:10.1145/1806596.1806638](http://dx.doi.org/10.1145/1806596.1806638)
-1. Jay Kreps: “[Why Local State is a Fundamental Primitive in Stream Processing](https://www.oreilly.com/ideas/why-local-state-is-a-fundamental-primitive-in-stream-processing),” *oreilly.com*, July 31, 2014.
-1. Martin Kleppmann: “[Rethinking Caching in Web Apps](http://martin.kleppmann.com/2012/10/01/rethinking-caching-in-web-apps.html),” *martin.kleppmann.com*, October 1, 2012.
-1. Mark Grover, Ted Malaska, Jonathan Seidman, and Gwen Shapira: *[Hadoop Application Architectures](http://shop.oreilly.com/product/0636920033196.do)*. O'Reilly Media, 2015. ISBN: 978-1-491-90004-8
-1. Philippe Ajoux, Nathan Bronson, Sanjeev Kumar, et al.: “[Challenges to Adopting Stronger Consistency at Scale](https://www.usenix.org/system/files/conference/hotos15/hotos15-paper-ajoux.pdf),” at *15th USENIX Workshop on Hot Topics in Operating Systems* (HotOS), May 2015.
-1. Sriranjan Manjunath: “[Skewed Join](https://web.archive.org/web/20151228114742/https://wiki.apache.org/pig/PigSkewedJoinSpec),” *wiki.apache.org*, 2009.
-1. David J. DeWitt, Jeffrey F. Naughton, Donovan A. Schneider, and S. Seshadri: “[Practical Skew Handling in Parallel Joins](http://www.vldb.org/conf/1992/P027.PDF),” at *18th International Conference on Very Large Data Bases* (VLDB), August 1992.
-1. Marcel Kornacker, Alexander Behm, Victor Bittorf, et al.: “[Impala: A Modern, Open-Source SQL Engine for Hadoop](http://pandis.net/resources/cidr15impala.pdf),” at *7th Biennial Conference on Innovative Data Systems Research* (CIDR), January 2015.
-1. Matthieu Monsch: “[Open-Sourcing PalDB, a Lightweight Companion for Storing Side Data](https://engineering.linkedin.com/blog/2015/10/open-sourcing-paldb--a-lightweight-companion-for-storing-side-da),” *engineering.linkedin.com*, October 26, 2015.
-1. Daniel Peng and Frank Dabek: “[Large-Scale Incremental Processing Using Distributed Transactions and Notifications](https://www.usenix.org/legacy/event/osdi10/tech/full_papers/Peng.pdf),” at *9th USENIX conference on Operating Systems Design and Implementation* (OSDI), October 2010.
-1. “["Cloudera Search User Guide,"](http://www.cloudera.com/documentation/cdh/5-1-x/Search/Cloudera-Search-User-Guide/Cloudera-Search-User-Guide.html) Cloudera, Inc., September 2015.
-1. Lili Wu, Sam Shah, Sean Choi, et al.: “[The Browsemaps: Collaborative Filtering at LinkedIn](http://ceur-ws.org/Vol-1271/Paper3.pdf),” at *6th Workshop on Recommender Systems and the Social Web* (RSWeb), October 2014.
-1. Roshan Sumbaly, Jay Kreps, Lei Gao, et al.: “[Serving Large-Scale Batch Computed Data with Project Voldemort](http://static.usenix.org/events/fast12/tech/full_papers/Sumbaly.pdf),” at *10th USENIX Conference on File and Storage Technologies* (FAST), February 2012.
-1. Varun Sharma: “[Open-Sourcing Terrapin: A Serving System for Batch Generated Data](https://web.archive.org/web/20170215032514/https://engineering.pinterest.com/blog/open-sourcing-terrapin-serving-system-batch-generated-data-0),” *engineering.pinterest.com*, September 14, 2015.
-1. Nathan Marz: “[ElephantDB](http://www.slideshare.net/nathanmarz/elephantdb),” *slideshare.net*, May 30, 2011.
-1. Jean-Daniel (JD) Cryans: “[How-to: Use HBase Bulk Loading, and Why](https://blog.cloudera.com/how-to-use-hbase-bulk-loading-and-why/),” *blog.cloudera.com*, September 27, 2013.
-1. Nathan Marz: “[How to Beat the CAP Theorem](http://nathanmarz.com/blog/how-to-beat-the-cap-theorem.html),” *nathanmarz.com*, October 13, 2011.
-1. Molly Bartlett Dishman and Martin Fowler: “[Agile Architecture](https://web.archive.org/web/20161130034721/http://conferences.oreilly.com/software-architecture/sa2015/public/schedule/detail/40388),” at *O'Reilly Software Architecture Conference*, March 2015.
-1. David J. DeWitt and Jim N. Gray: “[Parallel Database Systems: The Future of High Performance Database Systems](http://www.cs.cmu.edu/~pavlo/courses/fall2013/static/papers/dewittgray92.pdf),” *Communications of the ACM*, volume 35, number 6, pages 85–98, June 1992. [doi:10.1145/129888.129894](http://dx.doi.org/10.1145/129888.129894)
-1. Jay Kreps: “[But the multi-tenancy thing is actually really really hard](https://twitter.com/jaykreps/status/528235702480142336),” tweetstorm, *twitter.com*, October 31, 2014.
-1. Jeffrey Cohen, Brian Dolan, Mark Dunlap, et al.: “[MAD Skills: New Analysis Practices for Big Data](http://www.vldb.org/pvldb/vol2/vldb09-219.pdf),” *Proceedings of the VLDB Endowment*, volume 2, number 2, pages 1481–1492, August 2009. [doi:10.14778/1687553.1687576](http://dx.doi.org/10.14778/1687553.1687576)
-1. Ignacio Terrizzano, Peter Schwarz, Mary Roth, and John E. Colino: “[Data Wrangling: The Challenging Journey from the Wild to the Lake](http://cidrdb.org/cidr2015/Papers/CIDR15_Paper2.pdf),” at *7th Biennial Conference on Innovative Data Systems Research* (CIDR), January 2015.
-1. Paige Roberts: “[To Schema on Read or to Schema on Write, That Is the Hadoop Data Lake Question](https://web.archive.org/web/20171105001306/http://adaptivesystemsinc.com/blog/to-schema-on-read-or-to-schema-on-write-that-is-the-hadoop-data-lake-question/),” *adaptivesystemsinc.com*, July 2, 2015.
-1. Bobby Johnson and Joseph Adler: “[The Sushi Principle: Raw Data Is Better](https://web.archive.org/web/20161126104941/https://conferences.oreilly.com/strata/big-data-conference-ca-2015/public/schedule/detail/38737),” at *Strata+Hadoop World*, February 2015.
-1. Vinod Kumar Vavilapalli, Arun C. Murthy, Chris Douglas, et al.: “[Apache Hadoop YARN: Yet Another Resource Negotiator](https://www.cs.cmu.edu/~garth/15719/papers/yarn.pdf),” at *4th ACM Symposium on Cloud Computing* (SoCC), October 2013. [doi:10.1145/2523616.2523633](http://dx.doi.org/10.1145/2523616.2523633)
-1. Abhishek Verma, Luis Pedrosa, Madhukar Korupolu, et al.: “[Large-Scale Cluster Management at Google with Borg](http://research.google.com/pubs/pub43438.html),” at *10th European Conference on Computer Systems* (EuroSys), April 2015. [doi:10.1145/2741948.2741964](http://dx.doi.org/10.1145/2741948.2741964)
-1. Malte Schwarzkopf: “[The Evolution of Cluster Scheduler Architectures](https://web.archive.org/web/20201109052657/http://www.firmament.io/blog/scheduler-architectures.html),” *firmament.io*, March 9, 2016.
-1. Matei Zaharia, Mosharaf Chowdhury, Tathagata Das, et al.: “[Resilient Distributed Datasets: A Fault-Tolerant Abstraction for In-Memory Cluster Computing](https://www.usenix.org/system/files/conference/nsdi12/nsdi12-final138.pdf),” at *9th USENIX Symposium on Networked Systems Design and Implementation* (NSDI), April 2012.
-1. Holden Karau, Andy Konwinski, Patrick Wendell, and Matei Zaharia: *Learning Spark*. O'Reilly Media, 2015. ISBN: 978-1-449-35904-1
-1. Bikas Saha and Hitesh Shah: “[Apache Tez: Accelerating Hadoop Query Processing](http://www.slideshare.net/Hadoop_Summit/w-1205phall1saha),” at *Hadoop Summit*, June 2014.
-1. Bikas Saha, Hitesh Shah, Siddharth Seth, et al.: “[Apache Tez: A Unifying Framework for Modeling and Building Data Processing Applications](http://home.cse.ust.hk/~weiwa/teaching/Fall15-COMP6611B/reading_list/Tez.pdf),” at *ACM International Conference on Management of Data* (SIGMOD), June 2015. [doi:10.1145/2723372.2742790](http://dx.doi.org/10.1145/2723372.2742790)
-1. Kostas Tzoumas: “[Apache Flink: API, Runtime, and Project Roadmap](http://www.slideshare.net/KostasTzoumas/apache-flink-api-runtime-and-project-roadmap),” *slideshare.net*, January 14, 2015.
-1. Alexander Alexandrov, Rico Bergmann, Stephan Ewen, et al.: “[The Stratosphere Platform for Big Data Analytics](https://ssc.io/pdf/2014-VLDBJ_Stratosphere_Overview.pdf),” *The VLDB Journal*, volume 23, number 6, pages 939–964, May 2014. [doi:10.1007/s00778-014-0357-y](http://dx.doi.org/10.1007/s00778-014-0357-y)
-1. Michael Isard, Mihai Budiu, Yuan Yu, et al.: “[Dryad: Distributed Data-Parallel Programs from Sequential Building Blocks](https://www.microsoft.com/en-us/research/publication/dryad-distributed-data-parallel-programs-from-sequential-building-blocks/),” at *European Conference on Computer Systems* (EuroSys), March 2007. [doi:10.1145/1272996.1273005](http://dx.doi.org/10.1145/1272996.1273005)
-1. Daniel Warneke and Odej Kao: “[Nephele: Efficient Parallel Data Processing in the Cloud](https://stratosphere2.dima.tu-berlin.de/assets/papers/Nephele_09.pdf),” at *2nd Workshop on Many-Task Computing on Grids and Supercomputers* (MTAGS), November 2009. [doi:10.1145/1646468.1646476](http://dx.doi.org/10.1145/1646468.1646476)
-1. Lawrence Page, Sergey Brin, Rajeev Motwani, and Terry Winograd: “[The PageRank Citation Ranking: Bringing Order to the Web](https://web.archive.org/web/20230219170930/http://ilpubs.stanford.edu:8090/422/),” Stanford InfoLab Technical Report 422, 1999.
-1. Leslie G. Valiant: “[A Bridging Model for Parallel Computation](http://dl.acm.org/citation.cfm?id=79181),” *Communications of the ACM*, volume 33, number 8, pages 103–111, August 1990. [doi:10.1145/79173.79181](http://dx.doi.org/10.1145/79173.79181)
-1. Stephan Ewen, Kostas Tzoumas, Moritz Kaufmann, and Volker Markl: “[Spinning Fast Iterative Data Flows](http://vldb.org/pvldb/vol5/p1268_stephanewen_vldb2012.pdf),” *Proceedings of the VLDB Endowment*, volume 5, number 11, pages 1268-1279, July 2012. [doi:10.14778/2350229.2350245](http://dx.doi.org/10.14778/2350229.2350245)
-1. Grzegorz Malewicz, Matthew H. Austern, Aart J. C. Bik, et al.: “[Pregel: A System for Large-Scale Graph Processing](https://kowshik.github.io/JPregel/pregel_paper.pdf),” at *ACM International Conference on Management of Data* (SIGMOD), June 2010. [doi:10.1145/1807167.1807184](http://dx.doi.org/10.1145/1807167.1807184)
-1. Frank McSherry, Michael Isard, and Derek G. Murray: “[Scalability! But at What COST?](http://www.frankmcsherry.org/assets/COST.pdf),” at *15th USENIX Workshop on Hot Topics in Operating Systems* (HotOS), May 2015.
-1. Ionel Gog, Malte Schwarzkopf, Natacha Crooks, et al.: “[Musketeer: All for One, One for All in Data Processing Systems](http://www.cl.cam.ac.uk/research/srg/netos/camsas/pubs/eurosys15-musketeer.pdf),” at *10th European Conference on Computer Systems* (EuroSys), April 2015. [doi:10.1145/2741948.2741968](http://dx.doi.org/10.1145/2741948.2741968)
-1. Aapo Kyrola, Guy Blelloch, and Carlos Guestrin: “[GraphChi: Large-Scale Graph Computation on Just a PC](https://www.usenix.org/system/files/conference/osdi12/osdi12-final-126.pdf),” at *10th USENIX Symposium on Operating Systems Design and Implementation* (OSDI), October 2012.
-1. Andrew Lenharth, Donald Nguyen, and Keshav Pingali: “[Parallel Graph Analytics](http://cacm.acm.org/magazines/2016/5/201591-parallel-graph-analytics/fulltext),” *Communications of the ACM*, volume 59, number 5, pages 78–87, May 2016. [doi:10.1145/2901919](http://dx.doi.org/10.1145/2901919)
-1. Fabian Hüske: “[Peeking into Apache Flink's Engine Room](http://flink.apache.org/news/2015/03/13/peeking-into-Apache-Flinks-Engine-Room.html),” *flink.apache.org*, March 13, 2015.
-1. Mostafa Mokhtar: “[Hive 0.14 Cost Based Optimizer (CBO) Technical Overview](https://web.archive.org/web/20170607112708/http://hortonworks.com/blog/hive-0-14-cost-based-optimizer-cbo-technical-overview/),” *hortonworks.com*, March 2, 2015.
-1. Michael Armbrust, Reynold S Xin, Cheng Lian, et al.: “[Spark SQL: Relational Data Processing in Spark](http://people.csail.mit.edu/matei/papers/2015/sigmod_spark_sql.pdf),” at *ACM International Conference on Management of Data* (SIGMOD), June 2015. [doi:10.1145/2723372.2742797](http://dx.doi.org/10.1145/2723372.2742797)
-1. Daniel Blazevski: “[Planting Quadtrees for Apache Flink](https://blog.insightdatascience.com/planting-quadtrees-for-apache-flink-b396ebc80d35),” *insightdataengineering.com*, March 25, 2016.
-1. Tom White: “[Genome Analysis Toolkit: Now Using Apache Spark for Data Processing](https://web.archive.org/web/20190215132904/http://blog.cloudera.com/blog/2016/04/genome-analysis-toolkit-now-using-apache-spark-for-data-processing/),” *blog.cloudera.com*, April 6, 2016.
\ No newline at end of file
+[^1]: Nathan Marz. [How to Beat the CAP Theorem](http://nathanmarz.com/blog/how-to-beat-the-cap-theorem.html). *nathanmarz.com*, October 2011. Archived at [perma.cc/4BS9-R9A4](https://perma.cc/4BS9-R9A4)
+[^2]: Molly Bartlett Dishman and Martin Fowler. [Agile Architecture](https://www.youtube.com/watch?v=VjKYO6DP3fo&list=PL055Epbe6d5aFJdvWNtTeg_UEHZEHdInE). At *O'Reilly Software Architecture Conference*, March 2015.
+[^3]: Jeffrey Dean and Sanjay Ghemawat. [MapReduce: Simplified Data Processing on Large Clusters](https://www.usenix.org/legacy/publications/library/proceedings/osdi04/tech/full_papers/dean/dean.pdf). At *6th USENIX Symposium on Operating System Design and Implementation* (OSDI), December 2004.
+[^4]: Shivnath Babu and Herodotos Herodotou. [Massively Parallel Databases and MapReduce Systems](https://www.microsoft.com/en-us/research/wp-content/uploads/2013/11/db-mr-survey-final.pdf). *Foundations and Trends in Databases*, volume 5, issue 1, pages 1--104, November 2013. [doi:10.1561/1900000036](https://doi.org/10.1561/1900000036)
+[^5]: David J. DeWitt and Michael Stonebraker. [MapReduce: A Major Step Backwards](https://homes.cs.washington.edu/~billhowe/mapreduce_a_major_step_backwards.html). Originally published at *databasecolumn.vertica.com*, January 2008. Archived at [perma.cc/U8PA-K48V](https://perma.cc/U8PA-K48V)
+[^6]: Henry Robinson. [The Elephant Was a Trojan Horse: On the Death of Map-Reduce at Google](https://www.the-paper-trail.org/post/2014-06-25-the-elephant-was-a-trojan-horse-on-the-death-of-map-reduce-at-google/). *the-paper-trail.org*, June 2014. Archived at [perma.cc/9FEM-X787](https://perma.cc/9FEM-X787)
+[^7]: Urs Hölzle. [R.I.P. MapReduce. After having served us well since 2003, today we removed the remaining internal codebase for good](https://twitter.com/uhoelzle/status/1177360023976067077). *twitter.com*, September 2019. Archived at [perma.cc/B34T-LLY7](https://perma.cc/B34T-LLY7)
+[^8]: Adam Drake. [Command-Line Tools Can Be 235x Faster than Your Hadoop Cluster](https://adamdrake.com/command-line-tools-can-be-235x-faster-than-your-hadoop-cluster.html). *aadrake.com*, January 2014. Archived at [perma.cc/87SP-ZMCY](https://perma.cc/87SP-ZMCY)
+[^9]: [`sort`: Sort text files](https://www.gnu.org/software/coreutils/manual/html_node/sort-invocation.html). GNU Coreutils 9.7 Documentation, Free Software Foundation, Inc., 2025.
+[^10]: Michael Ovsiannikov, Silvius Rus, Damian Reeves, Paul Sutter, Sriram Rao, and Jim Kelly. [The Quantcast File System](https://db.disi.unitn.eu/pages/VLDBProgram/pdf/industry/p808-ovsiannikov.pdf). *Proceedings of the VLDB Endowment*, volume 6, issue 11, pages 1092--1101, August 2013. [doi:10.14778/2536222.2536234](https://doi.org/10.14778/2536222.2536234)
+[^11]: Andrew Wang, Zhe Zhang, Kai Zheng, Uma Maheswara G., and Vinayakumar B. [Introduction to HDFS Erasure Coding in Apache Hadoop](https://www.cloudera.com/blog/technical/introduction-to-hdfs-erasure-coding-in-apache-hadoop.html). *blog.cloudera.com*, September 2015. Archived at [archive.org](https://web.archive.org/web/20250731115546/https://www.cloudera.com/blog/technical/introduction-to-hdfs-erasure-coding-in-apache-hadoop.html)
+[^12]: Andy Warfield. [Building and operating a pretty big storage system called S3](https://www.allthingsdistributed.com/2023/07/building-and-operating-a-pretty-big-storage-system.html). *allthingsdistributed.com*, July 2023. Archived at [perma.cc/7LPK-TP7V](https://perma.cc/7LPK-TP7V)
+[^13]: Vinod Kumar Vavilapalli, Arun C. Murthy, Chris Douglas, Sharad Agarwal, Mahadev Konar, Robert Evans, Thomas Graves, Jason Lowe, Hitesh Shah, Siddharth Seth, Bikas Saha, Carlo Curino, Owen O'Malley, Sanjay Radia, Benjamin Reed, and Eric Baldeschwieler. [Apache Hadoop YARN: Yet Another Resource Negotiator](https://opencourse.inf.ed.ac.uk/sites/default/files/2023-10/yarn-socc13.pdf). At *4th Annual Symposium on Cloud Computing* (SoCC), October 2013. [doi:10.1145/2523616.2523633](https://doi.org/10.1145/2523616.2523633)
+[^14]: Richard M. Karp. [Reducibility Among Combinatorial Problems](https://www.cs.purdue.edu/homes/hosking/197/canon/karp.pdf). *Complexity of Computer Computations. The IBM Research Symposia Series*. Springer, 1972. [doi:10.1007/978-1-4684-2001-2_9](https://doi.org/10.1007/978-1-4684-2001-2_9)
+[^15]: J. D. Ullman. [NP-Complete Scheduling Problems](https://www.cs.montana.edu/bhz/classes/fall-2018/csci460/paper4.pdf). *Journal of Computer and System Sciences*, volume 10, issue 3, June 1975. [doi:10.1016/S0022-0000(75)80008-0](https://doi.org/10.1016/S0022-0000(75)80008-0)
+[^16]: Gilad David Maayan. [The complete guide to spot instances on AWS, Azure and GCP](https://www.datacenterdynamics.com/en/opinions/complete-guide-spot-instances-aws-azure-and-gcp/). *datacenterdynamics.com*, March 2021. Archived at [archive.org](https://web.archive.org/web/20250722114617/https://www.datacenterdynamics.com/en/opinions/complete-guide-spot-instances-aws-azure-and-gcp/)
+[^17]: Abhishek Verma, Luis Pedrosa, Madhukar Korupolu, David Oppenheimer, Eric Tune, and John Wilkes. [Large-Scale Cluster Management at Google with Borg](https://dl.acm.org/doi/pdf/10.1145/2741948.2741964). At *10th European Conference on Computer Systems* (EuroSys), April 2015. [doi:10.1145/2741948.2741964](https://doi.org/10.1145/2741948.2741964)
+[^18]: Matei Zaharia, Mosharaf Chowdhury, Tathagata Das, Ankur Dave, Justin Ma, Murphy McCauley, Michael J. Franklin, Scott Shenker, and Ion Stoica. [Resilient Distributed Datasets: A Fault-Tolerant Abstraction for In-Memory Cluster Computing](https://www.usenix.org/system/files/conference/nsdi12/nsdi12-final138.pdf). At *9th USENIX Symposium on Networked Systems Design and Implementation* (NSDI), April 2012.
+[^19]: Paris Carbone, Stephan Ewen, Seif Haridi, Asterios Katsifodimos, Volker Markl, and Kostas Tzoumas. [Apache Flink™: Stream and Batch Processing in a Single Engine](http://sites.computer.org/debull/A15dec/p28.pdf). *Bulletin of the IEEE Computer Society Technical Committee on Data Engineering*, volume 38, issue 4, December 2015. Archived at [perma.cc/G3N3-BKX5](https://perma.cc/G3N3-BKX5)
+[^20]: Mark Grover, Ted Malaska, Jonathan Seidman, and Gwen Shapira. *[Hadoop Application Architectures](https://learning.oreilly.com/library/view/hadoop-application-architectures/9781491910313/)*. O'Reilly Media, 2015. ISBN: 978-1-491-90004-8
+[^21]: Jules S. Damji, Brooke Wenig, Tathagata Das, and Denny Lee. *[Learning Spark, 2nd Edition](https://learning.oreilly.com/library/view/learning-spark-2nd/9781492050032/)*. O'Reilly Media, 2020. ISBN: 978-1492050049
+[^22]: Michael Isard, Mihai Budiu, Yuan Yu, Andrew Birrell, and Dennis Fetterly. [Dryad: Distributed Data-Parallel Programs from Sequential Building Blocks](https://www.microsoft.com/en-us/research/publication/dryad-distributed-data-parallel-programs-from-sequential-building-blocks/). At *2nd European Conference on Computer Systems* (EuroSys), March 2007. [doi:10.1145/1272996.1273005](https://doi.org/10.1145/1272996.1273005)
+[^23]: Daniel Warneke and Odej Kao. [Nephele: Efficient Parallel Data Processing in the Cloud](https://stratosphere2.dima.tu-berlin.de/assets/papers/Nephele_09.pdf). At *2nd Workshop on Many-Task Computing on Grids and Supercomputers* (MTAGS), November 2009. [doi:10.1145/1646468.1646476](https://doi.org/10.1145/1646468.1646476)
+[^24]: Hossein Ahmadi. [In-memory query execution in Google BigQuery](https://cloud.google.com/blog/products/bigquery/in-memory-query-execution-in-google-bigquery). *cloud.google.com*, August 2016. Archived at [perma.cc/DGG2-FL9W](https://perma.cc/DGG2-FL9W)
+[^25]: Tom White. *[Hadoop: The Definitive Guide](https://learning.oreilly.com/library/view/hadoop-the-definitive/9781491901687/)*, 4th edition. O'Reilly Media, 2015. ISBN: 978-1-491-90163-2
+[^26]: Fabian Hüske. [Peeking into Apache Flink's Engine Room](https://flink.apache.org/2015/03/13/peeking-into-apache-flinks-engine-room/). *flink.apache.org*, March 2015. Archived at [perma.cc/44BW-ALJX](https://perma.cc/44BW-ALJX)
+[^27]: Mostafa Mokhtar. [Hive 0.14 Cost Based Optimizer (CBO) Technical Overview](https://web.archive.org/web/20170607112708/http://hortonworks.com/blog/hive-0-14-cost-based-optimizer-cbo-technical-overview/). *hortonworks.com*, March 2015. Archived on [archive.org](https://web.archive.org/web/20170607112708/http://hortonworks.com/blog/hive-0-14-cost-based-optimizer-cbo-technical-overview/)
+[^28]: Michael Armbrust, Reynold S. Xin, Cheng Lian, Yin Huai, Davies Liu, Joseph K. Bradley, Xiangrui Meng, Tomer Kaftan, Michael J. Franklin, Ali Ghodsi, and Matei Zaharia. [Spark SQL: Relational Data Processing in Spark](https://people.csail.mit.edu/matei/papers/2015/sigmod_spark_sql.pdf). At *ACM International Conference on Management of Data* (SIGMOD), June 2015. [doi:10.1145/2723372.2742797](https://doi.org/10.1145/2723372.2742797)
+[^29]: Kaya Kupferschmidt. [Spark vs Pandas, part 2 -- Spark](https://towardsdatascience.com/spark-vs-pandas-part-2-spark-c57f8ea3a781/). *towardsdatascience.com*, October 2020. Archived at [perma.cc/5BRK-G4N5](https://perma.cc/5BRK-G4N5)
+[^30]: Ammar Chalifah. [Tracking payments at scale](https://bolt.eu/en/blog/tracking-payments-at-scale). *bolt.eu.com*, June 2025. Archived at [perma.cc/Q4KX-8K3J](https://perma.cc/Q4KX-8K3J)
+[^31]: Nafi Ahmet Turgut, Hamza Akyıldız, Hasan Burak Yel, Mehmet İkbal Özmen, Mutlu Polatcan, Pinar Baki, and Esra Kayabali. [Demand forecasting at Getir built with Amazon Forecast](https://aws.amazon.com/blogs/machine-learning/demand-forecasting-at-getir-built-with-amazon-forecast). *aws.amazon.com.com*, May 2023. Archived at [perma.cc/H3H6-GNL7](https://perma.cc/H3H6-GNL7)
+[^32]: Jason (Siyu) Zhu. [Enhancing homepage feed relevance by harnessing the power of large corpus sparse ID embeddings](https://www.linkedin.com/blog/engineering/feed/enhancing-homepage-feed-relevance-by-harnessing-the-power-of-lar). *linkedin.com*, August 2023. Archived at [archive.org](https://web.archive.org/web/20250225094424/https://www.linkedin.com/blog/engineering/feed/enhancing-homepage-feed-relevance-by-harnessing-the-power-of-lar)
+[^33]: Avery Ching, Sital Kedia, and Shuojie Wang. [Apache Spark \@Scale: A 60 TB+ production use case](https://engineering.fb.com/2016/08/31/core-infra/apache-spark-scale-a-60-tb-production-use-case/). *engineering.fb.com*, August 2016. Archived at [perma.cc/F7R5-YFAV](https://perma.cc/F7R5-YFAV)
+[^34]: Edward Kim. [How ACH works: A developer perspective --- Part 1](https://engineering.gusto.com/how-ach-works-a-developer-perspective-part-1-339d3e7bea1). *engineering.gusto.com*, April 2014. Archived at [perma.cc/F67P-VBLK](https://perma.cc/F67P-VBLK)
+[^35]: Zhamak Dehghani. [How to Move Beyond a Monolithic Data Lake to a Distributed Data Mesh](https://martinfowler.com/articles/data-monolith-to-mesh.html). *martinfowler.com*, May 2019. Archived at [perma.cc/LN2L-L4VC](https://perma.cc/LN2L-L4VC)
+[^36]: Chris Riccomini. [What the Heck is a Data Mesh?!](https://cnr.sh/essays/what-the-heck-data-mesh) *cnr.sh*, June 2021. Archived at [perma.cc/NEJ2-BAX3](https://perma.cc/NEJ2-BAX3)
+[^37]: Chad Sanderson, Mark Freeman, B. E. Schmidt. [*Data Contracts*](https://www.oreilly.com/library/view/data-contracts/9781098157623/). O'Reilly Media, 2025. ISBN: 9781098157623
+[^38]: Daniel Abadi. [Data Fabric vs. Data Mesh: What's the Difference?](https://www.starburst.io/blog/data-fabric-vs-data-mesh-whats-the-difference/) *starburst.io*, November 2021. Archived at [perma.cc/RSK3-HXDK](https://perma.cc/RSK3-HXDK)
+[^39]: Michael Armbrust, Ali Ghodsi, Reynold Xin, and Matei Zaharia. [Lakehouse: A New Generation of Open Platforms that Unify Data Warehousing and Advanced Analytics](https://www.cidrdb.org/cidr2021/papers/cidr2021_paper17.pdf). At *11th Annual Conference on Innovative Data Systems Research* (CIDR), January 2021.
+[^40]: Leslie G. Valiant. [A Bridging Model for Parallel Computation](https://dl.acm.org/doi/pdf/10.1145/79173.79181). *Communications of the ACM*, volume 33, issue 8, pages 103--111, August 1990. [doi:10.1145/79173.79181](https://doi.org/10.1145/79173.79181)
+[^41]: Stephan Ewen, Kostas Tzoumas, Moritz Kaufmann, and Volker Markl. [Spinning Fast Iterative Data Flows](https://vldb.org/pvldb/vol5/p1268_stephanewen_vldb2012.pdf). *Proceedings of the VLDB Endowment*, volume 5, issue 11, pages 1268-1279, July 2012. [doi:10.14778/2350229.2350245](https://doi.org/10.14778/2350229.2350245)
+[^42]: Grzegorz Malewicz, Matthew H. Austern, Aart J. C. Bik, James C. Dehnert, Ilan Horn, Naty Leiser, and Grzegorz Czajkowski. [Pregel: A System for Large-Scale Graph Processing](https://kowshik.github.io/JPregel/pregel_paper.pdf). At *ACM International Conference on Management of Data* (SIGMOD), June 2010. [doi:10.1145/1807167.1807184](https://doi.org/10.1145/1807167.1807184)
+[^43]: Richard MacManus. [OpenAI Chats about Scaling LLMs at Anyscale's Ray Summit](https://thenewstack.io/openai-chats-about-scaling-llms-at-anyscales-ray-summit/). *thenewstack.io*, September 2023. Archived at [perma.cc/YJD6-KUXU](https://perma.cc/YJD6-KUXU)
+[^44]: Jay Kreps. [Why Local State is a Fundamental Primitive in Stream Processing](https://www.oreilly.com/ideas/why-local-state-is-a-fundamental-primitive-in-stream-processing). *oreilly.com*, July 2014. Archived at [perma.cc/P8HU-R5LA](https://perma.cc/P8HU-R5LA)
+[^45]: Félix GV. [Open Sourcing Venice -- LinkedIn's Derived Data Platform](https://www.linkedin.com/blog/engineering/open-source/open-sourcing-venice-linkedin-s-derived-data-platform). *linkedin.com*, September 2022. Archived at [archive.org](https://web.archive.org/web/20250226160927/https://www.linkedin.com/blog/engineering/open-source/open-sourcing-venice-linkedin-s-derived-data-platform)
\ No newline at end of file
diff --git a/content/tw/ch12.md b/content/tw/ch12.md
index 12af233..4153226 100644
--- a/content/tw/ch12.md
+++ b/content/tw/ch12.md
@@ -2,12 +2,11 @@
title: "第十二章:流處理"
linkTitle: "12. 流處理"
weight: 312
+math: true
breadcrumbs: false
---
-{{< callout type="warning" >}}
-當前頁面來自本書第一版,第二版尚不可用
-{{< /callout >}}
+

@@ -15,120 +14,123 @@ breadcrumbs: false
>
> —— 約翰・加爾,Systemantics(1975)
-
在 [第十一章](/tw/ch11) 中,我們討論了批處理技術,它讀取一組檔案作為輸入,並生成一組新的檔案作為輸出。輸出是 **派生資料(derived data)** 的一種形式;也就是說,如果需要,可以透過再次執行批處理過程來重新建立資料集。我們看到了如何使用這個簡單而強大的想法來建立搜尋索引、推薦系統、做分析等等。
然而,在 [第十一章](/tw/ch11) 中仍然有一個很大的假設:即輸入是有界的,即已知和有限的大小,所以批處理知道它何時完成輸入的讀取。例如,MapReduce 核心的排序操作必須讀取其全部輸入,然後才能開始生成輸出:可能發生這種情況:最後一條輸入記錄具有最小的鍵,因此需要第一個被輸出,所以提早開始輸出是不可行的。
-實際上,很多資料是 **無界限** 的,因為它隨著時間的推移而逐漸到達:你的使用者在昨天和今天產生了資料,明天他們將繼續產生更多的資料。除非你停業,否則這個過程永遠都不會結束,所以資料集從來就不會以任何有意義的方式 “完成”【1】。因此,批處理程式必須將資料人為地分成固定時間段的資料塊,例如,在每天結束時處理一天的資料,或者在每小時結束時處理一小時的資料。
+實際上,很多資料是 **無界限** 的,因為它隨著時間的推移而逐漸到達:你的使用者在昨天和今天產生了資料,明天他們將繼續產生更多的資料。除非你停業,否則這個過程永遠都不會結束,所以資料集從來就不會以任何有意義的方式 “完成”[^1]。因此,批處理程式必須將資料人為地分成固定時間段的資料塊,例如,在每天結束時處理一天的資料,或者在每小時結束時處理一小時的資料。
-日常批處理中的問題是,輸入的變更只會在一天之後的輸出中反映出來,這對於許多急躁的使用者來說太慢了。為了減少延遲,我們可以更頻繁地執行處理 —— 比如說,在每秒鐘的末尾 —— 或者甚至更連續一些,完全拋開固定的時間切片,當事件發生時就立即進行處理,這就是 **流處理(stream processing)** 背後的想法。
+日常批處理中的問題是,輸入的變更只會在一天之後的輸出中反映出來,這對於許多急躁的使用者來說太慢了。為了減少延遲,我們可以更頻繁地執行處理 —— 比如說,在每秒鐘的末尾 —— 或者甚至更連續一些,完全拋開固定的時間切片,當事件發生時就立即進行處理,這就是 **流處理(stream processing)** 背後的想法。
-一般來說,“流” 是指隨著時間的推移逐漸可用的資料。這個概念出現在很多地方:Unix 的 stdin 和 stdout、程式語言(惰性列表)【2】、檔案系統 API(如 Java 的 `FileInputStream`)、TCP 連線、透過網際網路傳送音訊和影片等等。
+一般來說,“流” 是指隨著時間的推移逐漸可用的資料。這個概念出現在很多地方:Unix 的 stdin 和 stdout、程式語言(惰性列表)[^2]、檔案系統 API(如 Java 的 `FileInputStream`)、TCP 連線、透過網際網路傳送音訊和影片等等。
-在本章中,我們將把 **事件流(event stream)** 視為一種資料管理機制:無界限,增量處理,與上一章中的批次資料相對應。我們將首先討論怎樣表示、儲存、透過網路傳輸流。在 “[資料庫與流](#資料庫與流)” 中,我們將研究流和資料庫之間的關係。最後在 “[流處理](#流處理)” 中,我們將研究連續處理這些流的方法和工具,以及它們用於應用構建的方式。
+在本章中,我們將把 **事件流(event stream)** 視為一種資料管理機制:無界限,增量處理,與上一章中的批次資料相對應。我們將首先討論怎樣表示、儲存、透過網路傳輸流。在 “[資料庫與流](/tw/ch12#sec_stream_databases)” 中,我們將研究流和資料庫之間的關係。最後在 “[流處理](/tw/ch12#sec_stream_processing)” 中,我們將研究連續處理這些流的方法和工具,以及它們用於應用構建的方式。
-## 傳遞事件流
+## 傳遞事件流 {#sec_stream_transmit}
在批處理領域,作業的輸入和輸出是檔案(也許在分散式檔案系統上)。流處理領域中的等價物看上去是什麼樣子的?
-當輸入是一個檔案(一個位元組序列),第一個處理步驟通常是將其解析為一系列記錄。在流處理的上下文中,記錄通常被叫做 **事件(event)** ,但它本質上是一樣的:一個小的、自包含的、不可變的物件,包含某個時間點發生的某件事情的細節。一個事件通常包含一個來自日曆時鐘的時間戳,以指明事件發生的時間(請參閱 “[單調鍾與日曆時鐘](/tw/ch8#單調鍾與日曆時鐘)”)。
+當輸入是一個檔案(一個位元組序列),第一個處理步驟通常是將其解析為一系列記錄。在流處理的上下文中,記錄通常被叫做 **事件(event)** ,但它本質上是一樣的:一個小的、自包含的、不可變的物件,包含某個時間點發生的某件事情的細節。一個事件通常包含一個來自日曆時鐘的時間戳,以指明事件發生的時間(請參閱 “[單調鍾與日曆時鐘](/tw/ch9#sec_distributed_monotonic_timeofday)”)。
-例如,發生的事件可能是使用者採取的行動,例如檢視頁面或進行購買。它也可能來源於機器,例如對溫度感測器或 CPU 利用率的週期性測量。在 “[使用 Unix 工具的批處理](/tw/ch11#使用Unix工具的批處理)” 的示例中,Web 伺服器日誌的每一行都是一個事件。
+例如,發生的事件可能是使用者採取的行動,例如檢視頁面或進行購買。它也可能來源於機器,例如對溫度感測器或 CPU 利用率的週期性測量。在 “[使用 Unix 工具的批處理](/tw/ch11#sec_batch_unix)” 的示例中,Web 伺服器日誌的每一行都是一個事件。
-事件可能被編碼為文字字串或 JSON,或者某種二進位制編碼,如 [第四章](/tw/ch4) 所述。這種編碼允許你儲存一個事件,例如將其追加到一個檔案,將其插入關係表,或將其寫入文件資料庫。它還允許你透過網路將事件傳送到另一個節點以進行處理。
+事件可能被編碼為文字字串或 JSON,或者某種二進位制編碼,如 [第五章](/tw/ch5) 所述。這種編碼允許你儲存一個事件,例如將其追加到一個檔案,將其插入關係表,或將其寫入文件資料庫。它還允許你透過網路將事件傳送到另一個節點以進行處理。
-在批處理中,檔案被寫入一次,然後可能被多個作業讀取。類似地,在流處理術語中,一個事件由 **生產者(producer)** (也稱為 **釋出者(publisher)** 或 **傳送者(sender)** )生成一次,然後可能由多個 **消費者(consumer)** ( **訂閱者(subscribers)** 或 **接收者(recipients)** )進行處理【3】。在檔案系統中,檔名標識一組相關記錄;在流式系統中,相關的事件通常被聚合為一個 **主題(topic)** 或 **流(stream)** 。
+在批處理中,檔案被寫入一次,然後可能被多個作業讀取。類似地,在流處理術語中,一個事件由 **生產者(producer)** (也稱為 **釋出者(publisher)** 或 **傳送者(sender)** )生成一次,然後可能由多個 **消費者(consumer)** ( **訂閱者(subscribers)** 或 **接收者(recipients)** )進行處理[^3]。在檔案系統中,檔名標識一組相關記錄;在流式系統中,相關的事件通常被聚合為一個 **主題(topic)** 或 **流(stream)** 。
原則上講,檔案或資料庫就足以連線生產者和消費者:生產者將其生成的每個事件寫入資料儲存,且每個消費者定期輪詢資料儲存,檢查自上次執行以來新出現的事件。這實際上正是批處理在每天結束時處理當天資料時所做的事情。
但當我們想要進行低延遲的連續處理時,如果資料儲存不是為這種用途專門設計的,那麼輪詢開銷就會很大。輪詢的越頻繁,能返回新事件的請求比例就越低,而額外開銷也就越高。相比之下,最好能在新事件出現時直接通知消費者。
-資料庫在傳統上對這種通知機制支援的並不好,關係型資料庫通常有 **觸發器(trigger)** ,它們可以對變化(如,插入表中的一行)作出反應,但是它們的功能非常有限,並且在資料庫設計中有些後顧之憂【4,5】。相應的是,已經開發了專門的工具來提供事件通知。
+資料庫在傳統上對這種通知機制支援的並不好,關係型資料庫通常有 **觸發器(trigger)** ,它們可以對變化(如,插入表中的一行)作出反應,但是它們的功能非常有限,並且在資料庫設計中有些後顧之憂[^4]。相應的是,已經開發了專門的工具來提供事件通知。
-### 訊息傳遞系統
+### 訊息傳遞系統 {#sec_stream_messaging}
-向消費者通知新事件的常用方式是使用 **訊息傳遞系統(messaging system)**:生產者傳送包含事件的訊息,然後將訊息推送給消費者。我們之前在 “[訊息傳遞中的資料流](/tw/ch4#訊息傳遞中的資料流)” 中談到了這些系統,但現在我們將詳細介紹這些系統。
+向消費者通知新事件的常用方式是使用 **訊息傳遞系統(messaging system)**:生產者傳送包含事件的訊息,然後將訊息推送給消費者。我們之前在 “[訊息傳遞中的資料流](/tw/ch5#sec_encoding_dataflow_msg)” 中談到了這些系統,但現在我們將詳細介紹這些系統。
像生產者和消費者之間的 Unix 管道或 TCP 連線這樣的直接通道,是實現訊息傳遞系統的簡單方法。但是,大多數訊息傳遞系統都在這一基本模型上進行了擴充套件。特別的是,Unix 管道和 TCP 將恰好一個傳送者與恰好一個接收者連線,而一個訊息傳遞系統允許多個生產者節點將訊息傳送到同一個主題,並允許多個消費者節點接收主題中的訊息。
在這個 **釋出 / 訂閱** 模式中,不同的系統採取各種各樣的方法,並沒有針對所有目的的通用答案。為了區分這些系統,問一下這兩個問題會特別有幫助:
-1. **如果生產者傳送訊息的速度比消費者能夠處理的速度快會發生什麼?** 一般來說,有三種選擇:系統可以丟掉訊息,將訊息放入緩衝佇列,或使用 **背壓**(backpressure,也稱為 **流量控制**,即 flow control:阻塞生產者,以免其傳送更多的訊息)。例如 Unix 管道和 TCP 就使用了背壓:它們有一個固定大小的小緩衝區,如果填滿,傳送者會被阻塞,直到接收者從緩衝區中取出資料(請參閱 “[網路擁塞和排隊](/tw/ch8#網路擁塞和排隊)”)。
+1. **如果生產者傳送訊息的速度比消費者能夠處理的速度快會發生什麼?** 一般來說,有三種選擇:系統可以丟掉訊息,將訊息放入緩衝佇列,或使用 **背壓**(backpressure,也稱為 **流量控制**,即 flow control:阻塞生產者,以免其傳送更多的訊息)。例如 Unix 管道和 TCP 就使用了背壓:它們有一個固定大小的小緩衝區,如果填滿,傳送者會被阻塞,直到接收者從緩衝區中取出資料(請參閱 “[網路擁塞和排隊](/tw/ch9#sec_distributed_congestion)”)。
- 如果訊息被快取在佇列中,那麼理解佇列增長會發生什麼是很重要的。當佇列裝不進記憶體時系統會崩潰嗎?還是將訊息寫入磁碟?如果是這樣,磁碟訪問又會如何影響訊息傳遞系統的效能【6】?
+ 如果訊息被快取在佇列中,那麼理解佇列增長會發生什麼是很重要的。當佇列裝不進記憶體時系統會崩潰嗎?還是將訊息寫入磁碟?如果是這樣,磁碟訪問又會如何影響訊息傳遞系統的效能[^5],磁碟寫滿又會發生什麼[^6]?
-2. **如果節點崩潰或暫時離線,會發生什麼情況? —— 是否會有訊息丟失?** 與資料庫一樣,永續性可能需要寫入磁碟和 / 或複製的某種組合(請參閱 “[複製與永續性](/tw/ch7#複製與永續性)”),這是有代價的。如果你能接受有時訊息會丟失,則可能在同一硬體上獲得更高的吞吐量和更低的延遲。
+2. **如果節點崩潰或暫時離線,會發生什麼情況? —— 是否會有訊息丟失?** 與資料庫一樣,永續性可能需要寫入磁碟和 / 或複製的某種組合(請參閱 “[複製與永續性](/tw/ch8#sidebar_transactions_durability)”),這是有代價的。如果你能接受有時訊息會丟失,則可能在同一硬體上獲得更高的吞吐量和更低的延遲。
-是否可以接受訊息丟失取決於應用。例如,對於週期傳輸的感測器讀數和指標,偶爾丟失的資料點可能並不重要,因為更新的值會在短時間內發出。但要注意,如果大量的訊息被丟棄,可能無法立刻意識到指標已經不正確了【7】。如果你正在對事件計數,那麼它們能夠可靠送達是更重要的,因為每個丟失的訊息都意味著使計數器的錯誤擴大。
+是否可以接受訊息丟失取決於應用。例如,對於週期傳輸的感測器讀數和指標,偶爾丟失的資料點可能並不重要,因為更新的值會在短時間內發出。但要注意,如果大量的訊息被丟棄,可能無法立刻意識到指標已經不正確了[^7]。如果你正在對事件計數,那麼它們能夠可靠送達是更重要的,因為每個丟失的訊息都意味著使計數器的錯誤擴大。
我們在 [第十一章](/tw/ch11) 中探討的批處理系統的一個很好的特性是,它們提供了強大的可靠性保證:失敗的任務會自動重試,失敗任務的部分輸出會自動丟棄。這意味著輸出與沒有發生故障一樣,這有助於簡化程式設計模型。在本章的後面,我們將研究如何在流處理的上下文中提供類似的保證。
-#### 直接從生產者傳遞給消費者
+#### 直接從生產者傳遞給消費者 {#id296}
許多訊息傳遞系統使用生產者和消費者之間的直接網路通訊,而不透過中間節點:
-* UDP 組播廣泛應用於金融行業,例如股票市場,其中低時延非常重要【8】。雖然 UDP 本身是不可靠的,但應用層的協議可以恢復丟失的資料包(生產者必須記住它傳送的資料包,以便能按需重新發送資料包)。
-* 無代理的訊息庫,如 ZeroMQ 【9】和 nanomsg 採取類似的方法,透過 TCP 或 IP 多播實現釋出 / 訂閱訊息傳遞。
-* StatsD 【10】和 Brubeck 【7】使用不可靠的 UDP 訊息傳遞來收集網路中所有機器的指標並對其進行監控。(在 StatsD 協議中,只有接收到所有訊息,才認為計數器指標是正確的;使用 UDP 將使得指標處在一種最佳近似狀態【11】。另請參閱 “[TCP 與 UDP](/tw/ch8#TCP與UDP)”
-* 如果消費者在網路上公開了服務,生產者可以直接傳送 HTTP 或 RPC 請求(請參閱 “[服務中的資料流:REST 與 RPC](/tw/ch4#服務中的資料流:REST與RPC)”)將訊息推送給使用者。這就是 webhooks 背後的想法【12】,一種服務的回撥 URL 被註冊到另一個服務中,並且每當事件發生時都會向該 URL 發出請求。
+* UDP 組播廣泛應用於金融行業,例如股票市場,其中低時延非常重要[^8]。雖然 UDP 本身是不可靠的,但應用層的協議可以恢復丟失的資料包(生產者必須記住它傳送的資料包,以便能按需重新發送資料包)。
+* 無代理的訊息庫,如 ZeroMQ 和 nanomsg 採取類似的方法,透過 TCP 或 IP 多播實現釋出 / 訂閱訊息傳遞。
+* 一些指標採集代理(例如 StatsD [^9])使用不可靠的 UDP 訊息傳遞來收集網路中所有機器的指標並進行監控。(在 StatsD 協議中,計數器指標只有在所有訊息都被接收時才是準確的;使用 UDP 使得這些指標至多是近似值[^10]。另請參閱 “[TCP 與 UDP](/tw/ch9#sidebar_distributed_tcp_udp)”)。
+* 如果消費者在網路上公開了服務,生產者可以直接傳送 HTTP 或 RPC 請求(請參閱 “[服務中的資料流:REST 與 RPC](/tw/ch5#sec_encoding_dataflow_rpc)”)將訊息推送給使用者。這就是 webhooks 背後的想法[^11]:把一個服務的回撥 URL 註冊到另一個服務中,當事件發生時向該 URL 發起請求。
儘管這些直接訊息傳遞系統在設計它們的環境中執行良好,但是它們通常要求應用程式碼意識到訊息丟失的可能性。它們的容錯程度極為有限:即使協議檢測到並重傳在網路中丟失的資料包,它們通常也只是假設生產者和消費者始終線上。
如果消費者處於離線狀態,則可能會丟失其不可達時傳送的訊息。一些協議允許生產者重試失敗的訊息傳遞,但當生產者崩潰時,它可能會丟失訊息緩衝區及其本應傳送的訊息,這種方法可能就沒用了。
-#### 訊息代理
+#### 訊息代理 {#id433}
-一種廣泛使用的替代方法是透過 **訊息代理**(message broker,也稱為 **訊息佇列**,即 message queue)傳送訊息,訊息代理實質上是一種針對處理訊息流而最佳化的資料庫。它作為伺服器執行,生產者和消費者作為客戶端連線到伺服器。生產者將訊息寫入代理,消費者透過從代理那裡讀取來接收訊息。
+一種廣泛使用的替代方法是透過 **訊息代理**(message broker,也稱為 **訊息佇列**,即 message queue)傳送訊息,訊息代理實質上是一種針對處理訊息流而最佳化的資料庫[^12]。它作為伺服器執行,生產者和消費者作為客戶端連線到伺服器。生產者將訊息寫入代理,消費者透過從代理那裡讀取來接收訊息。
透過將資料集中在代理上,這些系統可以更容易地容忍來來去去的客戶端(連線,斷開連線和崩潰),而永續性問題則轉移到代理的身上。一些訊息代理只將訊息儲存在記憶體中,而另一些訊息代理(取決於配置)將其寫入磁碟,以便在代理崩潰的情況下不會丟失。針對緩慢的消費者,它們通常會允許無上限的排隊(而不是丟棄訊息或背壓),儘管這種選擇也可能取決於配置。
排隊的結果是,消費者通常是 **非同步(asynchronous)** 的:當生產者傳送訊息時,通常只會等待代理確認訊息已經被快取,而不等待訊息被消費者處理。向消費者遞送訊息將發生在未來某個未定的時間點 —— 通常在幾分之一秒之內,但有時當訊息堆積時會顯著延遲。
-#### 訊息代理與資料庫的對比
+#### 訊息代理與資料庫的對比 {#id297}
-有些訊息代理甚至可以使用 XA 或 JTA 參與兩階段提交協議(請參閱 “[實踐中的分散式事務](/tw/ch9#實踐中的分散式事務)”)。這個功能與資料庫在本質上非常相似,儘管訊息代理和資料庫之間仍存在實踐上很重要的差異:
+有些訊息代理甚至可以使用 XA 或 JTA 參與兩階段提交協議(請參閱 “[實踐中的分散式事務](/tw/ch8#sec_transactions_xa)”)。這個功能與資料庫在本質上非常相似,儘管訊息代理和資料庫之間仍存在實踐上很重要的差異:
* 資料庫通常保留資料直至顯式刪除,而大多數訊息代理在訊息成功遞送給消費者時會自動刪除訊息。這樣的訊息代理不適合長期的資料儲存。
-* 由於它們很快就能刪除訊息,大多數訊息代理都認為它們的工作集相當小 —— 即佇列很短。如果代理需要緩衝很多訊息,比如因為消費者速度較慢(如果記憶體裝不下訊息,可能會溢位到磁碟),每個訊息需要更長的處理時間,整體吞吐量可能會惡化【6】。
+* 由於它們很快就能刪除訊息,大多數訊息代理都認為它們的工作集相當小 —— 即佇列很短。如果代理需要緩衝很多訊息,比如因為消費者速度較慢(如果記憶體裝不下訊息,可能會溢位到磁碟),每個訊息需要更長的處理時間,整體吞吐量可能會惡化[^5]。
* 資料庫通常支援次級索引和各種搜尋資料的方式,而訊息代理通常支援按照某種模式匹配主題,訂閱其子集。雖然機制並不一樣,但對於客戶端選擇想要了解的資料的一部分,都是基本的方式。
* 查詢資料庫時,結果通常基於某個時間點的資料快照;如果另一個客戶端隨後向資料庫寫入一些改變了查詢結果的內容,則第一個客戶端不會發現其先前結果現已過期(除非它重複查詢或輪詢變更)。相比之下,訊息代理不支援任意查詢,但是當資料發生變化時(即新訊息可用時),它們會通知客戶端。
-這是關於訊息代理的傳統觀點,它被封裝在諸如 JMS 【14】和 AMQP 【15】的標準中,並且被諸如 RabbitMQ、ActiveMQ、HornetQ、Qpid、TIBCO 企業訊息服務、IBM MQ、Azure Service Bus 和 Google Cloud Pub/Sub 所實現 【16】。
+這是關於訊息代理的傳統觀點,它被封裝在諸如 JMS [^13] 和 AMQP [^14] 的標準中,並且被諸如 RabbitMQ、ActiveMQ、HornetQ、Qpid、TIBCO 企業訊息服務、IBM MQ、Azure Service Bus 和 Google Cloud Pub/Sub 所實現[^15]。儘管可以把資料庫當作佇列來用,但要調優到理想效能並不容易[^16]。
-#### 多個消費者
+#### 多個消費者 {#id298}
-當多個消費者從同一主題中讀取訊息時,有兩種主要的訊息傳遞模式,如 [圖 11-1](/v1/ddia_1101.png) 所示:
+當多個消費者從同一主題中讀取訊息時,有兩種主要的訊息傳遞模式,如 [圖 12-1](/fig/ddia_1201.png) 所示:
負載均衡(load balancing)
: 每條訊息都被傳遞給消費者 **之一**,所以處理該主題下訊息的工作能被多個消費者共享。代理可以為消費者任意分配訊息。當處理訊息的代價高昂,希望能並行處理訊息時,此模式非常有用(在 AMQP 中,可以透過讓多個客戶端從同一個佇列中消費來實現負載均衡,而在 JMS 中則稱之為 **共享訂閱**,即 shared subscription)。
扇出(fan-out)
-: 每條訊息都被傳遞給 **所有** 消費者。扇出允許幾個獨立的消費者各自 “收聽” 相同的訊息廣播,而不會相互影響 —— 這個流處理中的概念對應批處理中多個不同批處理作業讀取同一份輸入檔案 (JMS 中的主題訂閱與 AMQP 中的交叉繫結提供了這一功能)。
+: 每條訊息都被傳遞給 **所有** 消費者。扇出允許幾個獨立的消費者各自 “收聽” 相同的訊息廣播,而不會相互影響 —— 這個流處理中的概念對應批處理中多個不同批處理作業讀取同一份輸入檔案 (JMS 中的主題訂閱與 AMQP 中的交叉繫結提供了這一功能)。
-
+
-**圖 11-1 (a)負載平衡:在消費者間共享消費主題;(b)扇出:將每條訊息傳遞給多個消費者。**
+**圖 12-1 (a)負載平衡:在消費者間共享消費主題;(b)扇出:將每條訊息傳遞給多個消費者。**
兩種模式可以組合使用:例如,兩個獨立的消費者組可以每組各訂閱同一個主題,每一組都共同收到所有訊息,但在每一組內部,每條訊息僅由單個節點處理。
-#### 確認與重新傳遞
+#### 確認與重新傳遞 {#sec_stream_reordering}
消費者隨時可能會崩潰,所以有一種可能的情況是:代理向消費者遞送訊息,但消費者沒有處理,或者在消費者崩潰之前只進行了部分處理。為了確保訊息不會丟失,訊息代理使用 **確認(acknowledgments)**:客戶端必須顯式告知代理訊息處理完畢的時間,以便代理能將訊息從佇列中移除。
-如果與客戶端的連線關閉,或者代理超出一段時間未收到確認,代理則認為訊息沒有被處理,因此它將訊息再遞送給另一個消費者。(請注意可能發生這樣的情況,訊息 **實際上是** 處理完畢的,但 **確認** 在網路中丟失了。需要一種原子提交協議才能處理這種情況,正如在 “[實踐中的分散式事務](/tw/ch9#實踐中的分散式事務)” 中所討論的那樣)
+如果與客戶端的連線關閉,或者代理超出一段時間未收到確認,代理則認為訊息沒有被處理,因此它將訊息再遞送給另一個消費者。(請注意可能發生這樣的情況,訊息 **實際上是** 處理完畢的,但 **確認** 在網路中丟失了。需要一種原子提交協議才能處理這種情況,正如在 “[實踐中的分散式事務](/tw/ch8#sec_transactions_xa)” 中所討論的那樣)
-當與負載均衡相結合時,這種重傳行為對訊息的順序有種有趣的影響。在[圖 11-2](/v1/ddia_1102.png) 中,消費者通常按照生產者傳送的順序處理訊息。然而消費者 2 在處理訊息 m3 時崩潰,與此同時消費者 1 正在處理訊息 m4。未確認的訊息 m3 隨後被重新發送給消費者 1,結果消費者 1 按照 m4,m3,m5 的順序處理訊息。因此 m3 和 m4 的交付順序與生產者 1 的傳送順序不同。
+當與負載均衡相結合時,這種重傳行為對訊息的順序有種有趣的影響。在[圖 12-2](/fig/ddia_1202.png) 中,消費者通常按照生產者傳送的順序處理訊息。然而消費者 2 在處理訊息 m3 時崩潰,與此同時消費者 1 正在處理訊息 m4。未確認的訊息 m3 隨後被重新發送給消費者 1,結果消費者 1 按照 m4,m3,m5 的順序處理訊息。因此 m3 和 m4 的交付順序與生產者 1 的傳送順序不同。
-
+
-**圖 11-2 在處理 m3 時消費者 2 崩潰,因此稍後重傳至消費者 1**
+**圖 12-2 在處理 m3 時消費者 2 崩潰,因此稍後重傳至消費者 1**
即使訊息代理試圖保留訊息的順序(如 JMS 和 AMQP 標準所要求的),負載均衡與重傳的組合也不可避免地導致訊息被重新排序。為避免此問題,你可以讓每個消費者使用單獨的佇列(即不使用負載均衡功能)。如果訊息是完全獨立的,則訊息順序重排並不是一個問題。但正如我們將在本章後續部分所述,如果訊息之間存在因果依賴關係,這就是一個很重要的問題。
-### 分割槽日誌
+重傳還可能導致資源浪費、資源飢餓,甚至使流永久阻塞。一個常見場景是生產者錯誤地序列化訊息,例如 JSON 物件缺少必填鍵。任何讀取到該訊息的消費者都會因為缺鍵而失敗,無法傳送確認,於是代理會不斷重傳,導致其他消費者也不斷失敗。如果代理強順序保證,後續訊息可能被徹底卡住;即便允許重排,也會持續浪費資源在永遠無法確認的壞訊息上。
+
+這類問題通常透過 **死信佇列(dead letter queue, DLQ)** 處理:不再無限重試,而是把問題訊息移到另一條佇列中,從而解堵主消費鏈路[^17][^18]。運維通常會對死信佇列設定告警 —— 只要有訊息進入,就代表出現了錯誤。收到告警後,操作員可以決定永久丟棄該訊息、人工修復後重新投遞,或修復消費者程式碼以正確處理該訊息。除了傳統佇列系統,基於日誌的訊息系統和流處理系統也開始支援 DLQ[^19]。
+
+### 基於日誌的訊息代理 {#sec_stream_log}
透過網路傳送資料包或向網路服務傳送請求通常是短暫的操作,不會留下永久的痕跡。儘管可以永久記錄(透過抓包與日誌),但我們通常不這麼做。即使是將訊息持久地寫入磁碟的訊息代理,在送達給消費者之後也會很快刪除訊息,因為它們建立在短暫訊息傳遞的思維方式上。
@@ -140,56 +142,54 @@ breadcrumbs: false
為什麼我們不能把它倆雜交一下,既有資料庫的持久儲存方式,又有訊息傳遞的低延遲通知?這就是 **基於日誌的訊息代理(log-based message brokers)** 背後的想法。
-#### 使用日誌進行訊息儲存
+#### 使用日誌進行訊息儲存 {#id300}
-日誌只是磁碟上簡單的僅追加記錄序列。我們先前在 [第三章](/tw/ch3) 中日誌結構儲存引擎和預寫式日誌的上下文中討論了日誌,在 [第五章](/tw/ch5) 複製的上下文裡也討論了它。
+日誌只是磁碟上簡單的僅追加記錄序列。我們先前在 [第四章](/tw/ch4) 中日誌結構儲存引擎和預寫式日誌的上下文中討論了日誌,在 [第六章](/tw/ch6) 複製的上下文裡也討論了它。
同樣的結構可以用於實現訊息代理:生產者透過將訊息追加到日誌末尾來發送訊息,而消費者透過依次讀取日誌來接收訊息。如果消費者讀到日誌末尾,則會等待新訊息追加的通知。Unix 工具 `tail -f` 能監視檔案被追加寫入的資料,基本上就是這樣工作的。
-為了伸縮超出單個磁碟所能提供的更高吞吐量,可以對日誌進行 **分割槽**(按 [第六章](/tw/ch6) 的定義)。不同的分割槽可以託管在不同的機器上,使得每個分割槽都有一份能獨立於其他分割槽進行讀寫的日誌。一個主題可以定義為一組攜帶相同型別訊息的分割槽。這種方法如 [圖 11-3](/fig/ddia_1203.png) 所示。
+為了伸縮超出單個磁碟所能提供的更高吞吐量,可以對日誌進行 **分割槽**(按 [第七章](/tw/ch7) 的定義)。不同的分割槽可以託管在不同的機器上,使得每個分割槽都有一份能獨立於其他分割槽進行讀寫的日誌。一個主題可以定義為一組攜帶相同型別訊息的分割槽。這種方法如 [圖 12-3](/fig/ddia_1203.png) 所示。
-在每個分割槽內,代理為每個訊息分配一個單調遞增的序列號或 **偏移量**(offset,在 [圖 11-3](/v1/ddia_1103.png) 中,框中的數字是訊息偏移量)。這種序列號是有意義的,因為分割槽是僅追加寫入的,所以分割槽內的訊息是完全有序的。沒有跨不同分割槽的順序保證。
+在每個分割槽內,代理為每個訊息分配一個單調遞增的序列號或 **偏移量**(offset,在 [圖 12-3](/fig/ddia_1203.png) 中,框中的數字是訊息偏移量)。這種序列號是有意義的,因為分割槽是僅追加寫入的,所以分割槽內的訊息是完全有序的。沒有跨不同分割槽的順序保證。
-
+
-**圖 11-3 生產者透過將訊息追加寫入主題分割槽檔案來發送訊息,消費者依次讀取這些檔案**
+**圖 12-3 生產者透過將訊息追加寫入主題分割槽檔案來發送訊息,消費者依次讀取這些檔案**
-Apache Kafka 【17,18】、Amazon Kinesis Streams 【19】和 Twitter 的 DistributedLog 【20,21】都是基於日誌的訊息代理。Google Cloud Pub/Sub 在架構上類似,但對外暴露的是 JMS 風格的 API,而不是日誌抽象【16】。儘管這些訊息代理將所有訊息寫入磁碟,但透過跨多臺機器分割槽,每秒能夠實現數百萬條訊息的吞吐量,並透過複製訊息來實現容錯性【22,23】。
+Apache Kafka [^20] 和 Amazon Kinesis Streams 都是按這種方式工作的基於日誌的訊息代理。Google Cloud Pub/Sub 在架構上類似,但對外暴露的是 JMS 風格的 API,而不是日誌抽象[^15]。儘管這些訊息代理將所有訊息寫入磁碟,但透過跨多臺機器分割槽,依然能夠達到每秒數百萬條訊息的吞吐量,並透過複製訊息實現容錯[^21][^22]。
-#### 日誌與傳統的訊息傳遞相比
+#### 日誌與傳統的訊息傳遞相比 {#sec_stream_logs_vs_messaging}
基於日誌的方法天然支援扇出式訊息傳遞,因為多個消費者可以獨立讀取日誌,而不會相互影響 —— 讀取訊息不會將其從日誌中刪除。為了在一組消費者之間實現負載平衡,代理可以將整個分割槽分配給消費者組中的節點,而不是將單條訊息分配給消費者客戶端。
然後每個客戶端將消費被指派分割槽中的 **所有** 訊息。通常情況下,當一個使用者被指派了一個日誌分割槽時,它會以簡單的單執行緒方式順序地讀取分割槽中的訊息。這種粗粒度的負載均衡方法有一些缺點:
-* 共享消費主題工作的節點數,最多為該主題中的日誌分割槽數,因為同一個分割槽內的所有訊息被遞送到同一個節點 [^i]。
-* 如果某條訊息處理緩慢,則它會阻塞該分割槽中後續訊息的處理(一種行首阻塞的形式;請參閱 “[描述效能](/tw/ch1#描述效能)”)。
+* 共享消費主題工作的節點數,最多為該主題中的日誌分割槽數,因為同一個分割槽內的所有訊息被遞送到同一個節點。
+* 如果某條訊息處理緩慢,則它會阻塞該分割槽中後續訊息的處理(一種頭部阻塞的形式;請參閱 “[描述效能](/tw/ch2#sec_introduction_percentiles)”)。
-因此在訊息處理代價高昂,希望逐條並行處理,以及訊息的順序並沒有那麼重要的情況下,JMS/AMQP 風格的訊息代理是可取的。另一方面,在訊息吞吐量很高,處理迅速,順序很重要的情況下,基於日誌的方法表現得非常好。
+因此在訊息處理代價高昂,希望逐條並行處理,以及訊息的順序並沒有那麼重要的情況下,JMS/AMQP 風格的訊息代理是可取的。另一方面,在訊息吞吐量很高,處理迅速,順序很重要的情況下,基於日誌的方法表現得非常好[^23][^24]。不過,基於日誌與傳統訊息系統的邊界並不絕對:例如,一個主題分割槽通常一次只分配給一個消費者[^25][^26]。
-[^i]: 要設計一種負載均衡方案也是有可能的,在這種方案中,兩個消費者透過讀取全部訊息來共享分割槽處理的工作,但是其中一個只考慮具有偶數偏移量的訊息,而另一個消費者只處理奇數編號的偏移量。或者你可以將訊息攤到一個執行緒池中來處理,但這種方法會使消費者偏移量管理變得複雜。一般來說,單執行緒處理單分割槽是合適的,可以透過增加更多分割槽來提高並行度。
-
-#### 消費者偏移量
+#### 消費者偏移量 {#sec_stream_log_offsets}
順序消費一個分割槽使得判斷訊息是否已經被處理變得相當容易:所有偏移量小於消費者的當前偏移量的訊息已經被處理,而具有更大偏移量的訊息還沒有被看到。因此,代理不需要跟蹤確認每條訊息,只需要定期記錄消費者的偏移即可。這種方法減少了額外簿記開銷,而且在批處理和流處理中採用這種方法有助於提高基於日誌的系統的吞吐量。
-實際上,這種偏移量與單領導者資料庫複製中常見的日誌序列號非常相似,我們在 “[設定新從庫](/tw/ch5#設定新從庫)” 中討論了這種情況。在資料庫複製中,日誌序列號允許跟隨者斷開連線後,重新連線到領導者,並在不跳過任何寫入的情況下恢復複製。這裡原理完全相同:訊息代理表現得像一個主庫,而消費者就像一個從庫。
+實際上,這種偏移量與單領導者資料庫複製中常見的日誌序列號非常相似,我們在 “[設定新從庫](/tw/ch6#sec_replication_new_replica)” 中討論了這種情況。在資料庫複製中,日誌序列號允許跟隨者斷開連線後,重新連線到領導者,並在不跳過任何寫入的情況下恢復複製。這裡原理完全相同:訊息代理表現得像一個主庫,而消費者就像一個從庫。
如果消費者節點失效,則失效消費者的分割槽將指派給其他節點,並從最後記錄的偏移量開始消費訊息。如果消費者已經處理了後續的訊息,但還沒有記錄它們的偏移量,那麼重啟後這些訊息將被處理兩次。我們將在本章後面討論這個問題的處理方法。
-#### 磁碟空間使用
+#### 磁碟空間使用 {#sec_stream_disk_usage}
如果只追加寫入日誌,則磁碟空間終究會耗盡。為了回收磁碟空間,日誌實際上被分割成段,並不時地將舊段刪除或移動到歸檔儲存。(我們將在後面討論一種更為複雜的磁碟空間釋放方式)
這就意味著如果一個慢消費者跟不上訊息產生的速率而落後得太多,它的消費偏移量指向了刪除的段,那麼它就會錯過一些訊息。實際上,日誌實現了一個有限大小的緩衝區,當緩衝區填滿時會丟棄舊訊息,它也被稱為 **迴圈緩衝區(circular buffer)** 或 **環形緩衝區(ring buffer)**。不過由於緩衝區在磁碟上,因此緩衝區可能相當的大。
-讓我們做個簡單計算。在撰寫本文時,典型的大型硬碟容量為 6TB,順序寫入吞吐量為 150MB/s。如果以最快的速度寫訊息,則需要大約 11 個小時才能填滿磁碟。因而磁碟可以緩衝 11 個小時的訊息,之後它將開始覆蓋舊的訊息。即使使用多個磁碟和機器,這個比率也是一樣的。實踐中的部署很少能用滿磁碟的寫入頻寬,所以通常可以儲存一個幾天甚至幾周的日誌緩衝區。
+讓我們做個粗略估算。在撰寫本文時,典型的大容量硬碟約為 20 TB,順序寫入吞吐量約為 250 MB/s。如果持續以最高速率寫入訊息,磁碟大約 22 小時就會寫滿並開始刪除最舊訊息。這意味著,即使在滿速寫入下,磁碟日誌也至少可以緩衝約 22 小時的資料。實踐中部署很少持續打滿磁碟頻寬,因此通常可以保留數天甚至數週的訊息緩衝區。
-不管保留多長時間的訊息,日誌的吞吐量或多或少保持不變,因為無論如何,每個訊息都會被寫入磁碟【18】。這種行為與預設將訊息儲存在記憶體中,僅當佇列太長時才寫入磁碟的訊息傳遞系統形成鮮明對比。當佇列很短時,這些系統非常快;而當這些系統開始寫入磁碟時,就要慢的多,所以吞吐量取決於保留的歷史數量。
+許多基於日誌的訊息代理現在也將訊息分層儲存到物件儲存中,以進一步提升容量,方式與我們在第六章中討論“物件儲存支撐資料庫”時類似。像 Apache Kafka 和 Redpanda 可以把較舊訊息放在物件儲存中按需讀取;還有一些系統直接將全部訊息儲存在物件儲存中。除了成本優勢外,這種架構也有資料整合優勢:如果物件儲存中的訊息以 Iceberg 表形式組織,批處理和資料倉庫作業可以直接在這些資料上執行,而無需再複製一份資料。
-#### 當消費者跟不上生產者時
+#### 當消費者跟不上生產者時 {#id459}
-在 “[訊息傳遞系統](#訊息傳遞系統)” 中,如果消費者無法跟上生產者傳送資訊的速度時,我們討論了三種選擇:丟棄資訊,進行緩衝或施加背壓。在這種分類法裡,基於日誌的方法是緩衝的一種形式,具有很大但大小固定的緩衝區(受可用磁碟空間的限制)。
+在 “[訊息傳遞系統](#sec_stream_messaging)” 中,如果消費者無法跟上生產者傳送資訊的速度時,我們討論了三種選擇:丟棄資訊,進行緩衝或施加背壓。在這種分類法裡,基於日誌的方法是緩衝的一種形式,具有很大但大小固定的緩衝區(受可用磁碟空間的限制)。
如果消費者遠遠落後,而所要求的資訊比保留在磁碟上的資訊還要舊,那麼它將不能讀取這些資訊,所以代理實際上丟棄了比緩衝區容量更大的舊資訊。你可以監控消費者落後日誌頭部的距離,如果落後太多就發出報警。由於緩衝區很大,因而有足夠的時間讓運維人員來修復慢消費者,並在訊息開始丟失之前讓其趕上。
@@ -197,150 +197,133 @@ Apache Kafka 【17,18】、Amazon Kinesis Streams 【19】和 Twitter 的 Distri
這種行為也與傳統的訊息代理形成了鮮明對比,在那種情況下,你需要小心地刪除那些消費者已經關閉的佇列 —— 否則那些佇列就會累積不必要的訊息,從其他仍活躍的消費者那裡佔走記憶體。
-#### 重播舊訊息
+#### 重播舊訊息 {#sec_stream_replay}
我們之前提到,使用 AMQP 和 JMS 風格的訊息代理,處理和確認訊息是一個破壞性的操作,因為它會導致訊息在代理上被刪除。另一方面,在基於日誌的訊息代理中,使用訊息更像是從檔案中讀取資料:這是隻讀操作,不會更改日誌。
除了消費者的任何輸出之外,處理的唯一副作用是消費者偏移量的前進。但偏移量是在消費者的控制之下的,所以如果需要的話可以很容易地操縱:例如你可以用昨天的偏移量跑一個消費者副本,並將輸出寫到不同的位置,以便重新處理最近一天的訊息。你可以使用各種不同的處理程式碼重複任意次。
-這一方面使得基於日誌的訊息傳遞更像上一章的批處理,其中派生資料透過可重複的轉換過程與輸入資料顯式分離。它允許進行更多的實驗,更容易從錯誤和漏洞中恢復,使其成為在組織內整合資料流的良好工具【24】。
+這一方面使得基於日誌的訊息傳遞更像上一章的批處理,其中派生資料透過可重複的轉換過程與輸入資料顯式分離。它允許進行更多的實驗,更容易從錯誤和漏洞中恢復,使其成為在組織內整合資料流的良好工具[^27]。
-## 資料庫與流
+## 資料庫與流 {#sec_stream_databases}
我們已經在訊息代理和資料庫之間進行了一些比較。儘管傳統上它們被視為單獨的工具類別,但是我們看到基於日誌的訊息代理已經成功地從資料庫中獲取靈感並將其應用於訊息傳遞。我們也可以反過來:從訊息傳遞和流中獲取靈感,並將它們應用於資料庫。
我們之前曾經說過,事件是某個時刻發生的事情的記錄。發生的事情可能是使用者操作(例如鍵入搜尋查詢)或讀取感測器,但也可能是 **寫入資料庫**。某些東西被寫入資料庫的事實是可以被捕獲、儲存和處理的事件。這一觀察結果表明,資料庫和資料流之間的聯絡不僅僅是磁碟日誌的物理儲存 —— 而是更深層的聯絡。
-事實上,複製日誌(請參閱 “[複製日誌的實現](/tw/ch5#複製日誌的實現)”)是一個由資料庫寫入事件組成的流,由主庫在處理事務時生成。從庫將寫入流應用到它們自己的資料庫副本,從而最終得到相同資料的精確副本。複製日誌中的事件描述發生的資料更改。
+事實上,複製日誌(請參閱 “[複製日誌的實現](/tw/ch6#sec_replication_implementation)”)是一個由資料庫寫入事件組成的流,由主庫在處理事務時生成。從庫將寫入流應用到它們自己的資料庫副本,從而最終得到相同資料的精確副本。複製日誌中的事件描述發生的資料更改。
-我們還在 “[全序廣播](/tw/ch9#全序廣播)” 中遇到了狀態機複製原理,其中指出:如果每個事件代表對資料庫的寫入,並且每個副本按相同的順序處理相同的事件,則副本將達到相同的最終狀態 (假設事件處理是一個確定性的操作)。這是事件流的又一種場景!
+我們還在 “[使用共享日誌](/tw/ch10#sec_consistency_smr)” 中遇到了狀態機複製原理,其中指出:如果每個事件代表對資料庫的寫入,並且每個副本按相同的順序處理相同的事件,則副本將達到相同的最終狀態(假設事件處理是一個確定性的操作)。這是事件流的又一種場景!
在本節中,我們將首先看看異構資料系統中出現的一個問題,然後探討如何透過將事件流的想法帶入資料庫來解決這個問題。
-### 保持系統同步
+### 保持系統同步 {#sec_stream_sync}
正如我們在本書中所看到的,沒有一個系統能夠滿足所有的資料儲存、查詢和處理需求。在實踐中,大多數重要應用都需要組合使用幾種不同的技術來滿足所有的需求:例如,使用 OLTP 資料庫來為使用者請求提供服務,使用快取來加速常見請求,使用全文索引來處理搜尋查詢,使用資料倉庫用於分析。每一種技術都有自己的資料副本,並根據自己的目的進行儲存方式的最佳化。
-由於相同或相關的資料出現在了不同的地方,因此相互間需要保持同步:如果某個專案在資料庫中被更新,它也應當在快取、搜尋索引和資料倉庫中被更新。對於資料倉庫,這種同步通常由 ETL 程序執行(請參閱 “[資料倉庫](/tw/ch3#資料倉庫)”),通常是先取得資料庫的完整副本,然後執行轉換,並批次載入到資料倉庫中 —— 換句話說,批處理。我們在 “[批處理工作流的輸出](/tw/ch11#批處理工作流的輸出)” 中同樣看到了如何使用批處理建立搜尋索引、推薦系統和其他派生資料系統。
+由於相同或相關的資料出現在了不同的地方,因此相互間需要保持同步:如果某個專案在資料庫中被更新,它也應當在快取、搜尋索引和資料倉庫中被更新。對於資料倉庫,這種同步通常由 ETL 程序執行(請參閱 “[資料倉庫](/tw/ch1#sec_introduction_dwh)”),通常是先取得資料庫的完整副本,然後執行轉換,並批次載入到資料倉庫中 —— 換句話說,批處理。我們在 “[批處理工作流的輸出](/tw/ch11#sec_batch_output)” 中同樣看到了如何使用批處理建立搜尋索引、推薦系統和其他派生資料系統。
如果週期性的完整資料庫轉儲過於緩慢,有時會使用的替代方法是 **雙寫(dual write)**,其中應用程式碼在資料變更時明確寫入每個系統:例如,首先寫入資料庫,然後更新搜尋索引,然後使快取項失效(甚至同時執行這些寫入)。
-但是,雙寫有一些嚴重的問題,其中一個是競爭條件,如 [圖 11-4](/v1/ddia_1104.png) 所示。在這個例子中,兩個客戶端同時想要更新一個專案 X:客戶端 1 想要將值設定為 A,客戶端 2 想要將其設定為 B。兩個客戶端首先將新值寫入資料庫,然後將其寫入到搜尋索引。因為運氣不好,這些請求的時序是交錯的:資料庫首先看到來自客戶端 1 的寫入將值設定為 A,然後來自客戶端 2 的寫入將值設定為 B,因此資料庫中的最終值為 B。搜尋索引首先看到來自客戶端 2 的寫入,然後是客戶端 1 的寫入,所以搜尋索引中的最終值是 A。即使沒發生錯誤,這兩個系統現在也永久地不一致了。
+但是,雙寫有一些嚴重的問題,其中一個是競爭條件,如 [圖 12-4](/fig/ddia_1204.png) 所示。在這個例子中,兩個客戶端同時想要更新一個專案 X:客戶端 1 想要將值設定為 A,客戶端 2 想要將其設定為 B。兩個客戶端首先將新值寫入資料庫,然後將其寫入到搜尋索引。因為運氣不好,這些請求的時序是交錯的:資料庫首先看到來自客戶端 1 的寫入將值設定為 A,然後來自客戶端 2 的寫入將值設定為 B,因此資料庫中的最終值為 B。搜尋索引首先看到來自客戶端 2 的寫入,然後是客戶端 1 的寫入,所以搜尋索引中的最終值是 A。即使沒發生錯誤,這兩個系統現在也永久地不一致了。
-
+
-**圖 11-4 在資料庫中 X 首先被設定為 A,然後被設定為 B,而在搜尋索引處,寫入以相反的順序到達**
+**圖 12-4 在資料庫中 X 首先被設定為 A,然後被設定為 B,而在搜尋索引處,寫入以相反的順序到達**
-除非有一些額外的併發檢測機制,例如我們在 “[檢測併發寫入](/tw/ch5#檢測併發寫入)” 中討論的版本向量,否則你甚至不會意識到發生了併發寫入 —— 一個值將簡單地以無提示方式覆蓋另一個值。
+除非有一些額外的併發檢測機制,例如我們在 “[檢測併發寫入](/tw/ch6#sec_replication_concurrent)” 中討論的版本向量,否則你甚至不會意識到發生了併發寫入 —— 一個值將簡單地以無提示方式覆蓋另一個值。
-雙重寫入的另一個問題是,其中一個寫入可能會失敗,而另一個成功。這是一個容錯問題,而不是一個併發問題,但也會造成兩個系統互相不一致的結果。確保它們要麼都成功要麼都失敗,是原子提交問題的一個例子,解決這個問題的代價是昂貴的(請參閱 “[原子提交與兩階段提交](/tw/ch9#原子提交與兩階段提交)”)。
+雙重寫入的另一個問題是,其中一個寫入可能會失敗,而另一個成功。這是一個容錯問題,而不是一個併發問題,但也會造成兩個系統互相不一致的結果。確保它們要麼都成功要麼都失敗,是原子提交問題的一個例子,解決這個問題的代價是昂貴的(請參閱 “[原子提交與兩階段提交](/tw/ch8#sec_transactions_2pc)”)。
-如果你只有一個單領導者複製的資料庫,那麼這個領導者決定了寫入順序,而狀態機複製方法可以在資料庫副本上工作。然而,在 [圖 11-4](/v1/ddia_1104.png) 中,沒有單個主庫:資料庫可能有一個領導者,搜尋索引也可能有一個領導者,但是兩者都不追隨對方,所以可能會發生衝突(請參閱 “[多主複製](/tw/ch5#多主複製)”)。
+如果你只有一個單領導者複製的資料庫,那麼這個領導者決定了寫入順序,而狀態機複製方法可以在資料庫副本上工作。然而,在 [圖 12-4](/fig/ddia_1204.png) 中,沒有單個主庫:資料庫可能有一個領導者,搜尋索引也可能有一個領導者,但是兩者都不追隨對方,所以可能會發生衝突(請參閱 “[多主複製](/tw/ch6#sec_replication_multi_leader)”)。
如果實際上只有一個領導者 —— 例如,資料庫 —— 而且我們能讓搜尋索引成為資料庫的追隨者,情況要好得多。但這在實踐中可能嗎?
-### 變更資料捕獲
+### 資料變更捕獲 {#sec_stream_cdc}
大多數資料庫的複製日誌的問題在於,它們一直被當做資料庫的內部實現細節,而不是公開的 API。客戶端應該透過其資料模型和查詢語言來查詢資料庫,而不是解析複製日誌並嘗試從中提取資料。
數十年來,許多資料庫根本沒有記錄在檔的獲取變更日誌的方式。由於這個原因,捕獲資料庫中所有的變更,然後將其複製到其他儲存技術(搜尋索引、快取或資料倉庫)中是相當困難的。
-最近,人們對 **變更資料捕獲(change data capture, CDC)** 越來越感興趣,這是一種觀察寫入資料庫的所有資料變更,並將其提取並轉換為可以複製到其他系統中的形式的過程。CDC 是非常有意思的,尤其是當變更能在被寫入後立刻用於流時。
+最近,人們對 **資料變更捕獲(change data capture, CDC)** 越來越感興趣,這是一種觀察寫入資料庫的所有資料變更,並將其提取並轉換為可以複製到其他系統中的形式的過程。CDC 是非常有意思的,尤其是當變更能在被寫入後立刻用於流時[^28]。
-例如,你可以捕獲資料庫中的變更,並不斷將相同的變更應用至搜尋索引。如果變更日誌以相同的順序應用,則可以預期搜尋索引中的資料與資料庫中的資料是匹配的。搜尋索引和任何其他派生資料系統只是變更流的消費者,如 [圖 11-5](/v1/ddia_1105.png) 所示。
+例如,你可以捕獲資料庫中的變更,並不斷將相同的變更應用至搜尋索引。如果變更日誌以相同的順序應用,則可以預期搜尋索引中的資料與資料庫中的資料是匹配的。搜尋索引和任何其他派生資料系統只是變更流的消費者,如 [圖 12-5](/fig/ddia_1205.png) 所示。
-
+
-**圖 11-5 將資料按順序寫入一個數據庫,然後按照相同的順序將這些更改應用到其他系統**
+**圖 12-5 將資料按順序寫入一個數據庫,然後按照相同的順序將這些更改應用到其他系統**
-#### 變更資料捕獲的實現
+#### 資料變更捕獲的實現 {#id307}
-我們可以將日誌消費者叫做 **派生資料系統**,正如在 [第三部分](/tw/part-iii) 的介紹中所討論的:儲存在搜尋索引和資料倉庫中的資料,只是 **記錄系統** 資料的額外檢視。變更資料捕獲是一種機制,可確保對記錄系統所做的所有更改都反映在派生資料系統中,以便派生系統具有資料的準確副本。
+我們可以將日誌消費者叫做 **派生資料系統**,正如在 [第一章](/tw/ch1#sec_introduction_derived) 討論“記錄系統與派生資料”時所述:儲存在搜尋索引和資料倉庫中的資料,只是 **記錄系統** 資料的額外檢視。資料變更捕獲是一種機制,可確保對記錄系統所做的所有更改都反映在派生資料系統中,以便派生系統具有資料的準確副本。
-從本質上說,變更資料捕獲使得一個數據庫成為領導者(被捕獲變化的資料庫),並將其他元件變為追隨者。基於日誌的訊息代理非常適合從源資料庫傳輸變更事件,因為它保留了訊息的順序(避免了 [圖 11-2](/v1/ddia_1102.png) 的重新排序問題)。
+從本質上說,資料變更捕獲使得一個數據庫成為領導者(被捕獲變化的資料庫),並將其他元件變為追隨者。基於日誌的訊息代理非常適合從源資料庫傳輸變更事件,因為它保留了訊息的順序(避免了 [圖 12-2](/fig/ddia_1202.png) 的重新排序問題)。
-資料庫觸發器可用來實現變更資料捕獲(請參閱 “[基於觸發器的複製](/tw/ch5#基於觸發器的複製)”),透過註冊觀察所有變更的觸發器,並將相應的變更項寫入變更日誌表中。但是它們往往是脆弱的,而且有顯著的效能開銷。解析複製日誌可能是一種更穩健的方法,但它也很有挑戰,例如如何應對模式變更。
+資料庫觸發器可用來實現資料變更捕獲(請參閱 “[基於觸發器的複製](/tw/ch6#sec_replication_logical)”),透過註冊觀察所有變更的觸發器,並將相應的變更項寫入變更日誌表中。但是它們往往是脆弱的,而且有顯著的效能開銷。解析複製日誌可能是一種更穩健的方法,但它也很有挑戰,例如如何應對模式變更。
-LinkedIn 的 Databus【25】,Facebook 的 Wormhole【26】和 Yahoo! 的 Sherpa【27】大規模地應用這個思路。Bottled Water 使用解碼 WAL 的 API 實現了 PostgreSQL 的 CDC【28】,Maxwell 和 Debezium 透過解析 binlog 對 MySQL 做了類似的事情【29,30,31】,Mongoriver 讀取 MongoDB oplog【32,33】,而 GoldenGate 為 Oracle 提供類似的功能【34,35】。
+邏輯複製日誌可以用於實現 CDC(請參閱 “[邏輯(基於行)的日誌複製](/tw/ch6#sec_replication_logical)”),但會帶來不少挑戰,例如模式變更和更新建模。Debezium 開源專案專門解決這些問題,提供了面向 MySQL、PostgreSQL、Oracle、SQL Server、Db2、Cassandra 等資料庫的源聯結器。Kafka Connect 也為多種資料庫提供了 CDC 聯結器;Maxwell 透過解析 binlog 為 MySQL 提供類似能力[^29],GoldenGate 為 Oracle 提供類似能力,pgcapture 為 PostgreSQL 提供類似能力。
-類似於訊息代理,變更資料捕獲通常是非同步的:記錄資料庫系統在提交變更之前不會等待消費者應用變更。這種設計具有的運維優勢是,新增緩慢的消費者不會過度影響記錄系統。不過,所有複製延遲可能有的問題在這裡都可能出現(請參閱 “[複製延遲問題](/tw/ch5#複製延遲問題)”)。
+類似於訊息代理,資料變更捕獲通常是非同步的:記錄資料庫系統在提交變更之前不會等待消費者應用變更。這種設計具有的運維優勢是,新增緩慢的消費者不會過度影響記錄系統。不過,所有複製延遲可能有的問題在這裡都可能出現(請參閱 “[複製延遲問題](/tw/ch6#sec_replication_lag)”)。
-#### 初始快照
+#### 初始快照 {#sec_stream_cdc_snapshot}
如果你擁有 **所有** 對資料庫進行變更的日誌,則可以透過重播該日誌,來重建資料庫的完整狀態。但是在許多情況下,永遠保留所有更改會耗費太多磁碟空間,且重播過於費時,因此日誌需要被截斷。
-例如,構建新的全文索引需要整個資料庫的完整副本 —— 僅僅應用最近變更的日誌是不夠的,因為這樣會丟失最近未曾更新的專案。因此,如果你沒有完整的歷史日誌,則需要從一個一致的快照開始,如先前的 “[設定新從庫](/tw/ch5#設定新從庫)” 中所述。
+例如,構建新的全文索引需要整個資料庫的完整副本 —— 僅僅應用最近變更的日誌是不夠的,因為這樣會丟失最近未曾更新的專案。因此,如果你沒有完整的歷史日誌,則需要從一個一致的快照開始,如先前的 “[設定新從庫](/tw/ch6#sec_replication_new_replica)” 中所述。
-資料庫的快照必須與變更日誌中的已知位置或偏移量相對應,以便在處理完快照後知道從哪裡開始應用變更。一些 CDC 工具集成了這種快照功能,而其他工具則把它留給你手動執行。
+資料庫的快照必須與變更日誌中的已知位置或偏移量相對應,以便在處理完快照後知道從哪裡開始應用變更。一些 CDC 工具集成了這種快照功能,而其他工具則把它留給你手動執行。Debezium 使用 Netflix 的 DBLog 水位線演算法提供增量快照能力[^30][^31]。
-#### 日誌壓縮
+#### 日誌壓縮 {#sec_stream_log_compaction}
如果你只能保留有限的歷史日誌,則每次要新增新的派生資料系統時,都需要做一次快照。但 **日誌壓縮(log compaction)** 提供了一個很好的備選方案。
-我們之前在 “[雜湊索引](/tw/ch4#雜湊索引)” 中關於日誌結構儲存引擎的上下文中討論了日誌壓縮(請參閱 [圖 3-2](/v1/ddia_0302.png) 的示例)。原理很簡單:儲存引擎定期在日誌中查詢具有相同鍵的記錄,丟掉所有重複的內容,並只保留每個鍵的最新更新。這個壓縮與合併過程在後臺執行。
+我們之前在 “[日誌結構儲存](/tw/ch4#sec_storage_log_structured)” 的上下文中討論過日誌壓縮(可參閱 [圖 4-3](/fig/ddia_0403.png) 的示例)。原理很簡單:儲存引擎定期在日誌中查詢具有相同鍵的記錄,丟掉所有重複的內容,並只保留每個鍵的最新更新。這個壓縮與合併過程在後臺執行。
在日誌結構儲存引擎中,具有特殊值 NULL(**墓碑**,即 tombstone)的更新表示該鍵被刪除,並會在日誌壓縮過程中被移除。但只要鍵不被覆蓋或刪除,它就會永遠留在日誌中。這種壓縮日誌所需的磁碟空間僅取決於資料庫的當前內容,而不取決於資料庫中曾經發生的寫入次數。如果相同的鍵經常被覆蓋寫入,則先前的值將最終將被垃圾回收,只有最新的值會保留下來。
-在基於日誌的訊息代理與變更資料捕獲的上下文中也適用相同的想法。如果 CDC 系統被配置為,每個變更都包含一個主鍵,且每個鍵的更新都替換了該鍵以前的值,那麼只需要保留對鍵的最新寫入就足夠了。
+在基於日誌的訊息代理與資料變更捕獲的上下文中也適用相同的想法。如果 CDC 系統被配置為,每個變更都包含一個主鍵,且每個鍵的更新都替換了該鍵以前的值,那麼只需要保留對鍵的最新寫入就足夠了。
現在,無論何時需要重建派生資料系統(如搜尋索引),你可以從壓縮日誌主題的零偏移量處啟動新的消費者,然後依次掃描日誌中的所有訊息。日誌能保證包含資料庫中每個鍵的最新值(也可能是一些較舊的值)—— 換句話說,你可以使用它來獲取資料庫內容的完整副本,而無需從 CDC 源資料庫取一個快照。
Apache Kafka 支援這種日誌壓縮功能。正如我們將在本章後面看到的,它允許訊息代理被當成永續性儲存使用,而不僅僅是用於臨時訊息。
-#### 變更流的API支援
+#### 變更流的 API 支援 {#sec_stream_change_api}
-越來越多的資料庫開始將變更流作為第一等的介面,而不像傳統上要去做加裝改造,或者費工夫逆向工程一個 CDC。例如,RethinkDB 允許查詢訂閱通知,當查詢結果變更時獲得通知【36】,Firebase 【37】和 CouchDB 【38】基於變更流進行同步,該變更流同樣可用於應用。而 Meteor 使用 MongoDB oplog 訂閱資料變更,並改變了使用者介面【39】。
+如今許多主流資料庫都把變更流作為一等介面提供,而不再像過去那樣主要依賴“事後補丁式”或逆向工程式的 CDC。MySQL、PostgreSQL 等關係資料庫通常透過與自身複製相同的日誌通道輸出變更;各大雲廠商也提供了對應的 CDC 服務,例如 Google Cloud 的 Datastream 可向關係資料庫與資料倉庫提供流式資料訪問。
-VoltDB 允許事務以流的形式連續地從資料庫中匯出資料【40】。資料庫將關係資料模型中的輸出流表示為一個表,事務可以向其中插入元組,但不能查詢。已提交事務按照提交順序寫入這個特殊表,而流則由該表中的元組日誌構成。外部消費者可以非同步消費該日誌,並使用它來更新派生資料系統。
+即使是 Cassandra 這類最終一致、基於法定票數的資料庫,也開始支援資料變更捕獲。正如我們在第十章關於線性一致與法定票數中看到的,寫入是否“可見”取決於讀寫一致性設定,這使得其 CDC 的統一抽象更困難。Cassandra 的做法通常是公開各節點原始日誌段,而不是提供單一統一的變更流;消費方需要自己讀取併合並各節點日誌,生成業務可用的單一事件流[^32]。
-Kafka Connect【41】致力於將廣泛的資料庫系統的變更資料捕獲工具與 Kafka 整合。一旦變更事件進入 Kafka 中,它就可以用於更新派生資料系統,比如搜尋索引,也可以用於本章稍後討論的流處理系統。
+Kafka Connect[^33]提供了大量資料庫系統與 Kafka 的 CDC 整合能力。變更事件一旦進入 Kafka,就可以用於更新搜尋索引等派生系統,也可以繼續送入後續流處理鏈路。
-### 事件溯源
+#### 資料變更捕獲與事件溯源 {#sec_stream_event_sourcing}
-我們在這裡討論的想法和 **事件溯源(Event Sourcing)** 之間有一些相似之處,這是一個在 **領域驅動設計(domain-driven design, DDD)** 社群中折騰出來的技術。我們將簡要討論事件溯源,因為它包含了一些關於流處理系統的有用想法。
+資料變更捕獲與事件溯源都把狀態變化表示成事件日誌,但二者抽象層級不同:
-與變更資料捕獲類似,事件溯源涉及到 **將所有對應用狀態的變更** 儲存為變更事件日誌。最大的區別是事件溯源將這一想法應用到了一個不同的抽象層次上:
+* 在資料變更捕獲中,應用仍以可變方式使用資料庫,任意更新/刪除記錄;變更日誌從資料庫底層抽取(如複製日誌),因此能保證抽取順序與真實寫入順序一致,避免 [圖 12-4](/fig/ddia_1204.png) 這類競態問題。
+* 在事件溯源中,應用邏輯從一開始就構建在不可變事件之上,事件儲存通常是僅追加寫入,更新和刪除被限制或禁止。事件語義是應用層行為,而非底層狀態差異。
-* 在變更資料捕獲中,應用以 **可變方式(mutable way)** 使用資料庫,可以任意更新和刪除記錄。變更日誌是從資料庫的底層提取的(例如,透過解析複製日誌),從而確保從資料庫中提取的寫入順序與實際寫入的順序相匹配,從而避免 [圖 11-4](/v1/ddia_1104.png) 中的競態條件。寫入資料庫的應用不需要知道 CDC 的存在。
-* 在事件溯源中,應用邏輯顯式構建在寫入事件日誌的不可變事件之上。在這種情況下,事件儲存是僅追加寫入的,更新與刪除是不鼓勵的或禁止的。事件被設計為旨在反映應用層面發生的事情,而不是底層的狀態變更。
+二者孰優取決於場景。對未採用事件溯源的系統而言,引入它通常是一次較大架構變更;而資料變更捕獲通常可在現有資料庫上以較小改動接入,應用層甚至可以感知不到 CDC 的存在。
-事件溯源是一種強大的資料建模技術:從應用的角度來看,將使用者的行為記錄為不可變的事件更有意義,而不是在可變資料庫中記錄這些行為的影響。事件溯源使得應用隨時間演化更為容易,透過更容易理解事情發生的原因來幫助除錯的進行,並有利於防止應用 Bug(請參閱 “[不可變事件的優點](#不可變事件的優點)”)。
+> [!TIP] 資料變更捕獲與資料庫模式
+> 資料變更捕獲看上去比事件溯源更容易落地,但它也有自己的工程挑戰。
+>
+> 在微服務架構中,資料庫通常只由所屬服務直接訪問;其他服務透過該服務 API 互動,因此資料庫模式本應是服務內部實現細節,可隨服務演化。
+>
+> 但 CDC 往往直接複用上游資料庫模式做複製,這會把原本“內部模式”變成“外部契約”。刪除某個列可能會直接破壞下游消費者[^34]。
+>
+> 一種常見解法是 **Outbox 模式**:專門維護對外發布的 outbox 表,讓 CDC 讀取 outbox,而不是直接讀取內部領域模型表。這樣可以在儘量不影響外部消費者的前提下演化內部模式[^35][^36]。它看起來像雙寫,實際上也是雙寫;但它把兩次寫入留在同一個資料庫系統內,因此可放進同一事務,規避跨系統雙寫的一致性問題。
-例如,儲存 “學生取消選課” 事件以中性的方式清楚地表達了單個行為的意圖,而其副作用 “從登記表中刪除了一個條目,而一條取消原因的記錄被新增到學生反饋表” 則嵌入了很多有關稍後對資料的使用方式的假設。如果引入一個新的應用功能,例如 “將位置留給等待列表中的下一個人” —— 事件溯源方法允許將新的副作用輕鬆地從現有事件中脫開。
+和資料變更捕獲一樣,重放事件日誌也能重建當前狀態,但日誌壓縮策略不同:
-事件溯源類似於 **編年史(chronicle)** 資料模型【45】,事件日誌與星型模式中的事實表之間也存在相似之處(請參閱 “[星型和雪花型:分析的模式](/tw/ch3#星型和雪花型:分析的模式)”) 。
+* 對於 CDC,更新事件通常攜帶記錄的完整新版本,因此同一主鍵的最新事件就足以決定當前值,舊事件可被壓縮。
+* 對於事件溯源,事件通常描述使用者意圖而非狀態覆蓋,後續事件一般不會“覆蓋”先前事件,因此重建狀態通常需要完整歷史,不能按 CDC 的方式壓縮。
-諸如 Event Store【46】這樣的專業資料庫已經被開發出來,供使用事件溯源的應用使用,但總的來說,這種方法獨立於任何特定的工具。傳統的資料庫或基於日誌的訊息代理也可以用來構建這種風格的應用。
+採用事件溯源的系統通常會儲存由事件日誌匯出的狀態快照,以降低讀取與恢復成本;但快照本質上是效能最佳化。其核心假設仍是:原始事件可長期儲存,並在需要時可完整重放。我們將在“不變性的侷限性”中討論這一假設的邊界。
-#### 從事件日誌中派生出當前狀態
+### 狀態、流和不變性 {#sec_stream_immutability}
-事件日誌本身並不是很有用,因為使用者通常期望看到的是系統的當前狀態,而不是變更歷史。例如,在購物網站上,使用者期望能看到他們購物車裡的當前內容,而不是他們購物車所有變更的一個僅追加列表。
-
-因此,使用事件溯源的應用需要拉取事件日誌(表示 **寫入** 系統的資料),並將其轉換為適合向用戶顯示的應用狀態(從系統 **讀取** 資料的方式【47】)。這種轉換可以使用任意邏輯,但它應當是確定性的,以便能再次執行,並從事件日誌中派生出相同的應用狀態。
-
-與變更資料捕獲一樣,重播事件日誌允許讓你重新構建系統的當前狀態。不過,日誌壓縮需要採用不同的方式處理:
-
-* 用於記錄更新的 CDC 事件通常包含記錄的 **完整新版本**,因此主鍵的當前值完全由該主鍵的最近事件確定,而日誌壓縮可以丟棄相同主鍵的先前事件。
-* 另一方面,事件溯源在更高層次進行建模:事件通常表示使用者操作的意圖,而不是因為操作而發生的狀態更新機制。在這種情況下,後面的事件通常不會覆蓋先前的事件,所以你需要完整的歷史事件來重新構建最終狀態。這裡進行同樣的日誌壓縮是不可能的。
-
-使用事件溯源的應用通常有一些機制,用於儲存從事件日誌中匯出的當前狀態快照,因此它們不需要重複處理完整的日誌。然而這只是一種效能最佳化,用來加速讀取,提高從崩潰中恢復的速度;真正的目的是系統能夠永久儲存所有原始事件,並在需要時重新處理完整的事件日誌。我們將在 “[不變性的侷限性](#不變性的侷限性)” 中討論這個假設。
-
-#### 命令和事件
-
-事件溯源的哲學是仔細區分 **事件(event)** 和 **命令(command)**【48】。當來自使用者的請求剛到達時,它一開始是一個命令:在這個時間點上它仍然可能失敗,比如,因為違反了一些完整性條件。應用必須首先驗證它是否可以執行該命令。如果驗證成功並且命令被接受,則它變為一個持久化且不可變的事件。
-
-例如,如果使用者試圖註冊特定使用者名稱,或預定飛機或劇院的座位,則應用需要檢查使用者名稱或座位是否已被佔用。(先前在 “[容錯共識](/tw/ch9#容錯共識)” 中討論過這個例子)當檢查成功時,應用可以生成一個事件,指示特定的使用者名稱是由特定的使用者 ID 註冊的,或者座位已經預留給特定的顧客。
-
-在事件生成的時刻,它就成為了 **事實(fact)**。即使客戶稍後決定更改或取消預訂,他們之前曾預定了某個特定座位的事實仍然成立,而更改或取消是之後新增的單獨的事件。
-
-事件流的消費者不允許拒絕事件:當消費者看到事件時,它已經成為日誌中不可變的一部分,並且可能已經被其他消費者看到了。因此任何對命令的驗證,都需要在它成為事件之前同步完成。例如,透過使用一個可以原子性地自動驗證命令併發布事件的可序列事務。
-
-或者,預訂座位的使用者請求可以拆分為兩個事件:第一個是暫時預約,第二個是驗證預約後的獨立的確認事件(如 “[使用全序廣播實現線性一致的儲存](/tw/ch9#使用全序廣播實現線性一致的儲存)” 中所述) 。這種分割方式允許驗證發生在一個非同步的過程中。
-
-### 狀態、流和不變性
-
-我們在 [第十一章](/tw/ch11) 中看到,批處理因其輸入檔案不變性而受益良多,你可以在現有輸入檔案上執行實驗性處理作業,而不用擔心損壞它們。這種不變性原則也是使得事件溯源與變更資料捕獲如此強大的原因。
+我們在 [第十一章](/tw/ch11) 中看到,批處理因其輸入檔案不變性而受益良多,你可以在現有輸入檔案上執行實驗性處理作業,而不用擔心損壞它們。這種不變性原則也是使得事件溯源與資料變更捕獲如此強大的原因。
我們通常將資料庫視為應用程式當前狀態的儲存 —— 這種表示針對讀取進行了最佳化,而且通常對於服務查詢而言是最為方便的表示。狀態的本質是,它會變化,所以資料庫才會支援資料的增刪改。這又該如何匹配不變性呢?
@@ -348,74 +331,83 @@ Kafka Connect【41】致力於將廣泛的資料庫系統的變更資料捕獲
無論狀態如何變化,總是有一系列事件導致了這些變化。即使事情已經執行與回滾,這些事件出現是始終成立的。關鍵的想法是:可變的狀態與不可變事件的僅追加日誌相互之間並不矛盾:它們是一體兩面,互為陰陽的。所有變化的日誌 —— **變化日誌(changelog)**,表示了隨時間演變的狀態。
-如果你傾向於數學表示,那麼你可能會說,應用狀態是事件流對時間求積分得到的結果,而變更流是狀態對時間求微分的結果,如 [圖 11-6](/v1/ddia_1106.png) 所示【49,50,51】。這個比喻有一些侷限性(例如,狀態的二階導似乎沒有意義),但這是考慮資料的一個實用出發點。
+如果你傾向於數學表示,那麼你可能會說,應用狀態是事件流對時間求積分得到的結果,而變更流是狀態對時間求微分的結果,如 [圖 12-7](/fig/ddia_1207.png) 所示[^37][^38]。這個比喻有一些侷限性(例如,狀態的二階導似乎沒有意義),但這是考慮資料的一個實用出發點。
+
$$
-state(now) = \int_{t=0}^{now}{stream(t) \ dt} \\
-stream(t) = \frac{d\ state(t)}{dt}
+\begin{aligned}
+state(now) &= \int_{t=0}^{now} stream(t)\,dt \\
+stream(t) &= \frac{d\,state(t)}{dt}
+\end{aligned}
$$
-
+
-**圖 11-6 應用當前狀態與事件流之間的關係**
+**圖 12-7 應用當前狀態與事件流之間的關係**
-如果你持久儲存了變更日誌,那麼重現狀態就非常簡單。如果你認為事件日誌是你的記錄系統,而所有的派生狀態都從它派生而來,那麼系統中的資料流動就容易理解的多。正如帕特・赫蘭(Pat Helland)所說的【52】:
+如果你持久儲存了變更日誌,那麼重現狀態就非常簡單。如果你將事件日誌視為記錄系統,而把可變狀態視為其派生結果,那麼系統中的資料流就更容易推理。正如 Jim Gray 和 Andreas Reuter 在 1992 年所說[^39]:
-> 事務日誌記錄了資料庫的所有變更。高速追加是更改日誌的唯一方法。從這個角度來看,資料庫的內容其實是日誌中記錄最新值的快取。日誌才是真相,資料庫是日誌子集的快取,這一快取子集恰好來自日誌中每條記錄與索引值的最新值。
+> 從原理上講,資料庫並非必需;日誌已經包含了全部資訊。之所以要保留資料庫(即日誌末端的當前狀態),只是為了提高讀取效能。
-日誌壓縮(如 “[日誌壓縮](#日誌壓縮)” 中所述)是連線日誌與資料庫狀態之間的橋樑:它只保留每條記錄的最新版本,並丟棄被覆蓋的版本。
+日誌壓縮(如 “[日誌壓縮](#sec_stream_log_compaction)” 中所述)是連線日誌與資料庫狀態之間的橋樑:它只保留每條記錄的最新版本,並丟棄被覆蓋的版本。
-#### 不可變事件的優點
+#### 不可變事件的優點 {#sec_stream_immutability_pros}
-資料庫中的不變性是一個古老的概念。例如,會計在幾個世紀以來一直在財務記賬中應用不變性。一筆交易發生時,它被記錄在一個僅追加寫入的分類帳中,實質上是描述貨幣、商品或服務轉手的事件日誌。賬目,比如利潤、虧損、資產負債表,是從分類賬中的交易求和派生而來【53】。
+資料庫中的不變性是一個古老的概念。例如,會計在幾個世紀以來一直在財務記賬中應用不變性。一筆交易發生時,它被記錄在一個僅追加寫入的分類帳中,實質上是描述貨幣、商品或服務轉手的事件日誌。賬目,比如利潤、虧損、資產負債表,是從分類賬中的交易求和派生而來[^40]。
-如果發生錯誤,會計師不會刪除或更改分類帳中的錯誤交易 —— 而是新增另一筆交易以補償錯誤,例如退還一筆不正確的費用。不正確的交易將永遠保留在分類帳中,對於審計而言可能非常重要。如果從不正確的分類賬派生出的錯誤數字已經公佈,那麼下一個會計週期的數字就會包括一個更正。這個過程在會計事務中是很常見的【54】。
+如果發生錯誤,會計師不會刪除或更改分類帳中的錯誤交易 —— 而是新增另一筆交易以補償錯誤,例如退還一筆不正確的費用。不正確的交易將永遠保留在分類帳中,對於審計而言可能非常重要。如果從不正確的分類賬派生出的錯誤數字已經公佈,那麼下一個會計週期的數字就會包括一個更正。這個過程在會計事務中是很常見的[^41]。
-儘管這種可審計性只在金融系統中尤其重要,但對於不受這種嚴格監管的許多其他系統,也是很有幫助的。如 “[批處理輸出的哲學](/tw/ch11#批處理輸出的哲學)” 中所討論的,如果你意外地部署了將錯誤資料寫入資料庫的錯誤程式碼,當代碼會破壞性地覆寫資料時,恢復要困難得多。使用不可變事件的僅追加日誌,診斷問題與故障恢復就要容易的多。
+儘管這種可審計性只在金融系統中尤其重要,但對於不受這種嚴格監管的許多其他系統,也是很有幫助的。如 “[批處理用例](/tw/ch11#sec_batch_output)” 中所討論的,如果你意外地部署了將錯誤資料寫入資料庫的錯誤程式碼,當代碼會破壞性地覆寫資料時,恢復要困難得多。使用不可變事件的僅追加日誌,診斷問題與故障恢復就要容易得多。
-不可變的事件也包含了比當前狀態更多的資訊。例如在購物網站上,顧客可以將物品新增到他們的購物車,然後再將其移除。雖然從履行訂單的角度,第二個事件取消了第一個事件,但對分析目的而言,知道客戶考慮過某個特定項而之後又反悔,可能是很有用的。也許他們會選擇在未來購買,或者他們已經找到了替代品。這個資訊被記錄在事件日誌中,但對於移出購物車就刪除記錄的資料庫而言,這個資訊在移出購物車時可能就丟失了【42】。
+不可變的事件也包含了比當前狀態更多的資訊。例如在購物網站上,顧客可以將物品新增到他們的購物車,然後再將其移除。雖然從履行訂單的角度,第二個事件取消了第一個事件,但對分析目的而言,知道客戶考慮過某個特定項而之後又反悔,可能是很有用的。也許他們會選擇在未來購買,或者他們已經找到了替代品。這個資訊被記錄在事件日誌中,但對於移出購物車就刪除記錄的資料庫而言,這個資訊在移出購物車時可能就丟失了。
-#### 從同一事件日誌中派生多個檢視
+#### 從同一事件日誌中派生多個檢視 {#sec_stream_deriving_views}
-此外,透過從不變的事件日誌中分離出可變的狀態,你可以針對不同的讀取方式,從相同的事件日誌中派生出幾種不同的表現形式。效果就像一個流的多個消費者一樣([圖 11-5](/v1/ddia_1105.png)):例如,分析型資料庫 Druid 使用這種方式直接從 Kafka 攝取資料【55】,Pistachio 是一個分散式的鍵值儲存,使用 Kafka 作為提交日誌【56】,Kafka Connect 能將來自 Kafka 的資料匯出到各種不同的資料庫與索引【41】。這對於許多其他儲存和索引系統(如搜尋伺服器)來說是很有意義的,當系統要從分散式日誌中獲取輸入時亦然(請參閱 “[保持系統同步](#保持系統同步)”)。
+此外,透過從不變的事件日誌中分離出可變的狀態,你可以針對不同的讀取方式,從相同的事件日誌中派生出幾種不同的表現形式。效果就像一個流的多個消費者一樣([圖 12-5](/fig/ddia_1205.png)):例如,Kafka Connect 能將來自 Kafka 的資料匯出到各種不同的資料庫與索引[^33]。這對於許多其他儲存和索引系統(如搜尋伺服器)來說也是有意義的,當系統要從分散式日誌中獲取輸入時尤其如此(請參閱 “[保持系統同步](#sec_stream_sync)”)。
-新增從事件日誌到資料庫的顯式轉換,能夠使應用更容易地隨時間演進:如果你想要引入一個新功能,以新的方式表示現有資料,則可以使用事件日誌來構建一個單獨的、針對新功能的讀取最佳化檢視,無需修改現有系統而與之共存。並行執行新舊系統通常比在現有系統中執行複雜的模式遷移更容易。一旦不再需要舊的系統,你可以簡單地關閉它並回收其資源【47,57】。
+新增從事件日誌到資料庫的顯式轉換,能夠使應用更容易地隨時間演進:如果你想要引入一個新功能,以新的方式表示現有資料,則可以使用事件日誌來構建一個單獨的、針對新功能的讀取最佳化檢視,無需修改現有系統而與之共存。並行執行新舊系統通常比在現有系統中執行複雜的模式遷移更容易。一旦不再需要舊的系統,你可以簡單地關閉它並回收其資源[^42][^43]。
-如果你不需要擔心如何查詢與訪問資料,那麼儲存資料通常是非常簡單的。模式設計、索引和儲存引擎的許多複雜性,都是希望支援某些特定查詢和訪問模式的結果(請參閱 [第三章](/tw/ch3))。出於這個原因,透過將資料寫入的形式與讀取形式相分離,並允許幾個不同的讀取檢視,你能獲得很大的靈活性。這個想法有時被稱為 **命令查詢責任分離(command query responsibility segregation, CQRS)**【42,58,59】。
+如果你不需要擔心如何查詢與訪問資料,那麼儲存資料通常是非常簡單的。模式設計、索引和儲存引擎的許多複雜性,都是希望支援某些特定查詢和訪問模式的結果(請參閱 [第三章](/tw/ch3))。出於這個原因,透過將資料寫入的形式與讀取形式相分離,並允許幾個不同的讀取檢視,你能獲得很大的靈活性。這個想法有時被稱為 **命令查詢責任分離(command query responsibility segregation, CQRS)**[^44]。
-資料庫和模式設計的傳統方法是基於這樣一種謬論,資料必須以與查詢相同的形式寫入。如果可以將資料從針對寫入最佳化的事件日誌轉換為針對讀取最佳化的應用狀態,那麼有關正規化和反正規化的爭論就變得無關緊要了(請參閱 “[多對一和多對多的關係](/tw/ch2#多對一和多對多的關係)”):在針對讀取最佳化的檢視中對資料進行反正規化是完全合理的,因為翻譯過程提供了使其與事件日誌保持一致的機制。
+資料庫和模式設計的傳統方法是基於這樣一種謬論,資料必須以與查詢相同的形式寫入。如果可以將資料從針對寫入最佳化的事件日誌轉換為針對讀取最佳化的應用狀態,那麼有關正規化和反正規化的爭論就變得無關緊要了(請參閱 “[多對一和多對多的關係](/tw/ch3#sec_datamodels_normalization)”):在針對讀取最佳化的檢視中對資料進行反正規化是完全合理的,因為翻譯過程提供了使其與事件日誌保持一致的機制。
-在 “[描述負載](/tw/ch1#描述負載)” 中,我們討論了推特主頁時間線,它是特定使用者關注的人群所發推特的快取(類似郵箱)。這是 **針對讀取最佳化的狀態** 的又一個例子:主頁時間線是高度反正規化的,因為你的推文與你所有粉絲的時間線都構成了重複。然而,扇出服務保持了這種重複狀態與新推特以及新關注關係的同步,從而保證了重複的可管理性。
+在 “[描述負載](/tw/ch2#sec_introduction_twitter)” 中,我們討論了推特主頁時間線,它是特定使用者關注的人群所發推特的快取(類似郵箱)。這是 **針對讀取最佳化的狀態** 的又一個例子:主頁時間線是高度反正規化的,因為你的推文與你所有粉絲的時間線都構成了重複。然而,扇出服務保持了這種重複狀態與新推特以及新關注關係的同步,從而保證了重複的可管理性。
-#### 併發控制
+#### 併發控制 {#sec_stream_concurrency}
-事件溯源和變更資料捕獲的最大缺點是,事件日誌的消費者通常是非同步的,所以可能會出現這樣的情況:使用者會寫入日誌,然後從日誌派生檢視中讀取,結果發現他的寫入還沒有反映在讀取檢視中。我們之前在 “[讀己之寫](/tw/ch5#讀己之寫)” 中討論了這個問題以及可能的解決方案。
+事件溯源和資料變更捕獲的最大缺點是,事件日誌的消費者通常是非同步的,所以可能會出現這樣的情況:使用者會寫入日誌,然後從日誌派生檢視中讀取,結果發現他的寫入還沒有反映在讀取檢視中。我們之前在 “[讀己之寫](/tw/ch6#sec_replication_ryw)” 中討論了這個問題以及可能的解決方案。
-一種解決方案是將事件追加到日誌時同步執行讀取檢視的更新。而將這些寫入操作合併為一個原子單元需要 **事務**,所以要麼將事件日誌和讀取檢視儲存在同一個儲存系統中,要麼就需要跨不同系統進行分散式事務。或者,你也可以使用在 “[使用全序廣播實現線性一致的儲存](/tw/ch9#使用全序廣播實現線性一致的儲存)” 中討論的方法。
+一種解決方案是將事件追加到日誌時同步執行讀取檢視的更新。而將這些寫入操作合併為一個原子單元需要 **事務**,所以要麼將事件日誌和讀取檢視儲存在同一個儲存系統中,要麼就需要跨不同系統進行分散式事務。或者,你也可以使用在 “[使用共享日誌](/tw/ch10#sec_consistency_smr)” 中討論的方法。
-另一方面,從事件日誌匯出當前狀態也簡化了併發控制的某些部分。許多對於多物件事務的需求(請參閱 “[單物件和多物件操作](/tw/ch7#單物件和多物件操作)”)源於單個使用者操作需要在多個不同的位置更改資料。透過事件溯源,你可以設計一個自包含的事件以表示一個使用者操作。然後使用者操作就只需要在一個地方進行單次寫入操作 —— 即將事件附加到日誌中 —— 這個還是很容易使原子化的。
+另一方面,從事件日誌匯出當前狀態也簡化了併發控制的某些部分。許多對於多物件事務的需求(請參閱 “[單物件和多物件操作](/tw/ch8#sec_transactions_multi_object)”)源於單個使用者操作需要在多個不同的位置更改資料。透過事件溯源,你可以設計一個自包含的事件以表示一個使用者操作。然後使用者操作就只需要在一個地方進行單次寫入操作 —— 即將事件附加到日誌中 —— 這個還是很容易使原子化的。
-如果事件日誌與應用狀態以相同的方式分割槽(例如,處理分割槽 3 中的客戶事件只需要更新分割槽 3 中的應用狀態),那麼直接使用單執行緒日誌消費者就不需要寫入併發控制了。它從設計上一次只處理一個事件(請參閱 “[真的序列執行](/tw/ch7#真的序列執行)”)。日誌透過在分割槽中定義事件的序列順序,消除了併發性的不確定性【24】。如果一個事件觸及多個狀態分割槽,那麼需要做更多的工作,我們將在 [第十三章](/tw/ch13) 討論。
+如果事件日誌與應用狀態以相同的方式分割槽(例如,處理分割槽 3 中的客戶事件只需要更新分割槽 3 中的應用狀態),那麼直接使用單執行緒日誌消費者就不需要寫入併發控制了。它從設計上一次只處理一個事件(請參閱 “[真的序列執行](/tw/ch8#sec_transactions_serial)”)。日誌透過在分割槽中定義事件的序列順序,消除了併發性的不確定性[^27]。如果一個事件觸及多個狀態分割槽,那麼需要做更多的工作,我們將在 [第十三章](/tw/ch13) 討論。
-#### 不變性的侷限性
+#### 不變性的侷限性 {#sec_stream_immutability_limitations}
-許多不使用事件溯源模型的系統也還是依賴不可變性:各種資料庫在內部使用不可變的資料結構或多版本資料來支援時間點快照(請參閱 “[索引和快照隔離](/tw/ch7#索引和快照隔離)” )。Git、Mercurial 和 Fossil 等版本控制系統也依靠不可變的資料來儲存檔案的版本歷史記錄。
+許多不使用事件溯源模型的系統也還是依賴不可變性:各種資料庫在內部使用不可變的資料結構或多版本資料來支援時間點快照(請參閱 “[索引和快照隔離](/tw/ch8#sec_transactions_snapshot_indexes)” )。Git、Mercurial 和 Fossil 等版本控制系統也依靠不可變的資料來儲存檔案的版本歷史記錄。
-永遠保持所有變更的不變歷史,在多大程度上是可行的?答案取決於資料集的流失率。一些工作負載主要是新增資料,很少更新或刪除;它們很容易保持不變。其他工作負載在相對較小的資料集上有較高的更新 / 刪除率;在這些情況下,不可變的歷史可能增至難以接受的巨大,碎片化可能成為一個問題,壓縮與垃圾收集的表現對於運維的穩健性變得至關重要【60,61】。
+永遠保持所有變更的不變歷史,在多大程度上是可行的?答案取決於資料集的流失率。一些工作負載主要是新增資料,很少更新或刪除;它們很容易保持不變。其他工作負載在相對較小的資料集上有較高的更新 / 刪除率;在這些情況下,不可變的歷史可能增至難以接受的巨大,碎片化可能成為一個問題,壓縮與垃圾收集的表現對於運維的穩健性變得至關重要[^45][^46]。
除了效能方面的原因外,也可能有出於管理方面的原因需要刪除資料的情況,儘管這些資料都是不可變的。例如,隱私條例可能要求在使用者關閉帳戶後刪除他們的個人資訊,資料保護立法可能要求刪除錯誤的資訊,或者可能需要阻止敏感資訊的意外洩露。
-在這種情況下,僅僅在日誌中新增另一個事件來指明先前的資料應該被視為刪除是不夠的 —— 你實際上是想改寫歷史,並假裝資料從一開始就沒有寫入。例如,Datomic 管這個特性叫 **切除(excision)** 【62】,而 Fossil 版本控制系統有一個類似的概念叫 **避免(shunning)** 【63】。
+在這種情況下,僅僅在日誌中新增另一個事件來指明先前的資料應該被視為刪除是不夠的 —— 你實際上是想改寫歷史,並假裝資料從一開始就沒有寫入。例如,Datomic 管這個特性叫 **切除(excision)**[^47],而 Fossil 版本控制系統有一個類似的概念叫 **避免(shunning)**[^48]。
-真正刪除資料是非常非常困難的【64】,因為副本可能存在於很多地方:例如,儲存引擎,檔案系統和 SSD 通常會向一個新位置寫入,而不是原地覆蓋舊資料【52】,而備份通常是特意做成不可變的,防止意外刪除或損壞。刪除操作更多的是指 “使取回資料更困難”,而不是指 “使取回資料不可能”。無論如何,有時你必須得嘗試,正如我們在 “[立法與自律](/tw/ch13#立法與自律)” 中所看到的。
+真正刪除資料是非常非常困難的[^49],因為副本可能存在於很多地方:例如,儲存引擎、檔案系統和 SSD 通常會向新位置寫入,而不是原地覆蓋舊資料[^41];而備份往往刻意設計為不可變,以防誤刪或損壞。
+
+一種支援刪除不可變資料的方法是 **加密粉碎(crypto-shredding)**[^50]:將未來可能需要刪除的資料以加密形式儲存,刪除時僅銷燬金鑰。這樣,密文仍在,但不可再被使用。從某種意義上說,這只是把可變性從“資料本身”轉移到“金鑰管理”上。
+
+此外,你需要預先決定哪些資料共享同一金鑰、哪些資料使用不同金鑰,因為後續你能“粉碎”的粒度通常是“該金鑰加密的全部資料”或“都不刪”,很難只刪其中一部分。若為每條記錄單獨存金鑰,金鑰儲存規模又會變得不可控。像 puncturable encryption 這樣的高階方案[^51]可以提供更細粒度的撤銷能力,但尚未廣泛落地。
+
+總的來說,刪除更多是在“讓資料更難被取回”,而非“讓資料絕對不可恢復”。儘管如此,在某些場景下仍必須盡力而為,正如我們在 “[立法與自律](/ch14#sec_future_legislation)” 中會看到的。
-## 流處理
+## 流處理 {#sec_stream_processing}
到目前為止,本章中我們已經討論了流的來源(使用者活動事件,感測器和寫入資料庫),我們討論了流如何傳輸(直接透過訊息傳送,透過訊息代理,透過事件日誌)。
剩下的就是討論一下你可以用流做什麼 —— 也就是說,你可以處理它。一般來說,有三種選項:
-1. 你可以將事件中的資料寫入資料庫、快取、搜尋索引或類似的儲存系統,然後能被其他客戶端查詢。如 [圖 11-5](/v1/ddia_1105.png) 所示,這是資料庫與系統其他部分所發生的變更保持同步的好方法 —— 特別是當流消費者是寫入資料庫的唯一客戶端時。如 “[批處理工作流的輸出](/tw/ch11#批處理工作流的輸出)” 中所討論的,它是寫入儲存系統的流等價物。
+1. 你可以將事件中的資料寫入資料庫、快取、搜尋索引或類似的儲存系統,然後能被其他客戶端查詢。如 [圖 12-5](/fig/ddia_1205.png) 所示,這是資料庫與系統其他部分所發生的變更保持同步的好方法 —— 特別是當流消費者是寫入資料庫的唯一客戶端時。如 “[批處理工作流的輸出](/tw/ch11#sec_batch_output)” 中所討論的,它是寫入儲存系統的流等價物。
2. 你能以某種方式將事件推送給使用者,例如傳送報警郵件或推送通知,或將事件流式傳輸到可即時顯示的儀表板上。在這種情況下,人是流的最終消費者。
3. 你可以處理一個或多個輸入流,併產生一個或多個輸出流。流可能會經過由幾個這樣的處理階段組成的流水線,最後再輸出(選項 1 或 2)。
@@ -423,9 +415,9 @@ $$
流處理中的分割槽和並行化模式也非常類似於 [第十一章](/tw/ch11) 中介紹的 MapReduce 和資料流引擎,因此我們不再重複這些主題。基本的 Map 操作(如轉換和過濾記錄)也是一樣的。
-與批次作業相比的一個關鍵區別是,流不會結束。這種差異會帶來很多隱含的結果。正如本章開始部分所討論的,排序對無界資料集沒有意義,因此無法使用 **排序合併連線**(請參閱 “[Reduce 側連線與分組](/tw/ch11#Reduce側連線與分組)”)。容錯機制也必須改變:對於已經運行了幾分鐘的批處理作業,可以簡單地從頭開始重啟失敗任務,但是對於已經執行數年的流作業,重啟後從頭開始跑可能並不是一個可行的選項。
+與批次作業相比的一個關鍵區別是,流不會結束。這種差異會帶來很多隱含的結果。正如本章開始部分所討論的,排序對無界資料集沒有意義,因此無法使用 **排序合併連線**(請參閱 “[Reduce 側連線與分組](/tw/ch11#sec_batch_join)”)。容錯機制也必須改變:對於已經運行了幾分鐘的批處理作業,可以簡單地從頭開始重啟失敗任務,但是對於已經執行數年的流作業,重啟後從頭開始跑可能並不是一個可行的選項。
-### 流處理的應用
+### 流處理的應用 {#sec_stream_uses}
長期以來,流處理一直用於監控目的,如果某個事件發生,組織希望能得到警報。例如:
@@ -436,17 +428,17 @@ $$
這些型別的應用需要非常精密複雜的模式匹配與相關檢測。然而隨著時代的進步,流處理的其他用途也開始出現。在本節中,我們將簡要比較一下這些應用。
-#### 複合事件處理
+#### 複合事件處理 {#id317}
-**複合事件處理(complex event processing, CEP)** 是 20 世紀 90 年代為分析事件流而開發出的一種方法,尤其適用於需要搜尋某些事件模式的應用【65,66】。與正則表示式允許你在字串中搜索特定字元模式的方式類似,CEP 允許你指定規則以在流中搜索某些事件模式。
+**複合事件處理(complex event processing, CEP)** 是 20 世紀 90 年代為分析事件流而開發出的一種方法,尤其適用於需要搜尋某些事件模式的應用[^52]。與正則表示式允許你在字串中搜索特定字元模式的方式類似,CEP 允許你指定規則以在流中搜索某些事件模式。
-CEP 系統通常使用高層次的宣告式查詢語言,比如 SQL,或者圖形使用者介面,來描述應該檢測到的事件模式。這些查詢被提交給處理引擎,該引擎消費輸入流,並在內部維護一個執行所需匹配的狀態機。當發現匹配時,引擎發出一個 **複合事件**(即 complex event,CEP 因此得名),並附有檢測到的事件模式詳情【67】。
+CEP 系統通常使用高層次的宣告式查詢語言,比如 SQL,或者圖形使用者介面,來描述應該檢測到的事件模式。這些查詢被提交給處理引擎,該引擎消費輸入流,並在內部維護一個執行所需匹配的狀態機。當發現匹配時,引擎發出一個 **複合事件**(即 complex event,CEP 因此得名),並附有檢測到的事件模式詳情[^53]。
-在這些系統中,查詢和資料之間的關係與普通資料庫相比是顛倒的。通常情況下,資料庫會持久儲存資料,並將查詢視為臨時的:當查詢進入時,資料庫搜尋與查詢匹配的資料,然後在查詢完成時丟掉查詢。CEP 引擎反轉了角色:查詢是長期儲存的,來自輸入流的事件不斷流過它們,搜尋匹配事件模式的查詢【68】。
+在這些系統中,查詢和資料之間的關係與普通資料庫相比是顛倒的。通常情況下,資料庫會持久儲存資料,並將查詢視為臨時的:當查詢進入時,資料庫搜尋與查詢匹配的資料,然後在查詢完成時丟掉查詢。CEP 引擎反轉了角色:查詢是長期儲存的,來自輸入流的事件不斷流過它們,搜尋匹配事件模式的查詢[^54]。
-CEP 的實現包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIBCO StreamBase 和 SQLstream。像 Samza 這樣的分散式流處理元件,支援使用 SQL 在流上進行宣告式查詢【71】。
+CEP 的實現包括 Esper、Apama 和 TIBCO StreamBase。像 Flink 和 Spark Streaming 這樣的分散式流處理框架,也支援在流上使用 SQL 進行宣告式查詢。
-#### 流分析
+#### 流分析 {#id318}
使用流處理的另一個領域是對流進行分析。CEP 與流分析之間的邊界是模糊的,但一般來說,分析往往對找出特定事件序列並不關心,而更關注大量事件上的聚合與統計指標 —— 例如:
@@ -454,86 +446,94 @@ CEP 的實現包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
* 滾動計算一段時間視窗內某個值的平均值
* 將當前的統計值與先前的時間區間的值對比(例如,檢測趨勢,當指標與上週同比異常偏高或偏低時報警)
-這些統計值通常是在固定時間區間內進行計算的,例如,你可能想知道在過去 5 分鐘內服務每秒查詢次數的均值,以及此時間段內響應時間的第 99 百分位點。在幾分鐘內取平均,能抹平秒和秒之間的無關波動,且仍然能向你展示流量模式的時間圖景。聚合的時間間隔稱為 **視窗(window)**,我們將在 “[時間推理](#時間推理)” 中更詳細地討論視窗。
+這些統計值通常是在固定時間區間內進行計算的,例如,你可能想知道在過去 5 分鐘內服務每秒查詢次數的均值,以及此時間段內響應時間的第 99 百分位點。在幾分鐘內取平均,能抹平秒和秒之間的無關波動,且仍然能向你展示流量模式的時間圖景。聚合的時間間隔稱為 **視窗(window)**,我們將在 “[時間推理](#sec_stream_time)” 中更詳細地討論視窗。
-流分析系統有時會使用機率演算法,例如 Bloom filter(我們在 “[效能最佳化](/tw/ch3#效能最佳化)” 中遇到過)來管理成員資格,HyperLogLog【72】用於基數估計以及各種百分比估計算法(請參閱 “[實踐中的百分位點](/tw/ch1#實踐中的百分位點)”)。機率演算法產出近似的結果,但比起精確演算法的優點是記憶體使用要少得多。使用近似演算法有時讓人們覺得流處理系統總是有損的和不精確的,但這是錯誤看法:流處理並沒有任何內在的近似性,而機率演算法只是一種最佳化【73】。
+流分析系統有時會使用機率演算法,例如 Bloom filter(我們在 “[效能最佳化](/tw/ch4#sec_storage_bloom_filter)” 中遇到過)來管理成員資格,HyperLogLog[^55]用於基數估計以及各種百分比估計算法(請參閱 “[實踐中的百分位點](/tw/ch2#sec_introduction_percentiles)”)。機率演算法產出近似的結果,但比起精確演算法的優點是記憶體使用要少得多。使用近似演算法有時讓人們覺得流處理系統總是有損的和不精確的,但這是錯誤看法:流處理並沒有任何內在的近似性,而機率演算法只是一種最佳化[^56]。
-許多開源分散式流處理框架的設計都是針對分析設計的:例如 Apache Storm、Spark Streaming、Flink、Concord、Samza 和 Kafka Streams 【74】。託管服務包括 Google Cloud Dataflow 和 Azure Stream Analytics。
+許多開源分散式流處理框架的設計都是針對分析設計的:例如 Apache Storm、Spark Streaming、Flink、Samza、Apache Beam 和 Kafka Streams[^57]。託管服務包括 Google Cloud Dataflow 和 Azure Stream Analytics。
-#### 維護物化檢視
+#### 維護物化檢視 {#sec_stream_mat_view}
-我們在 “[資料庫與流](#資料庫與流)” 中看到,資料庫的變更流可以用於維護派生資料系統(如快取、搜尋索引和資料倉庫),並使其與源資料庫保持最新。我們可以將這些示例視作維護 **物化檢視(materialized view)** 的一種具體場景(請參閱 “[聚合:資料立方體和物化檢視](/tw/ch3#聚合:資料立方體和物化檢視)”):在某個資料集上派生出一個替代檢視以便高效查詢,並在底層資料變更時更新檢視【50】。
+我們在 “[資料庫與流](#sec_stream_databases)” 中看到,資料庫的變更流可以用於維護派生資料系統(如快取、搜尋索引和資料倉庫),並使其與源資料庫保持最新。我們可以將這些示例視作維護 **物化檢視(materialized view)** 的一種具體場景:在某個資料集上派生出一個替代檢視以便高效查詢,並在底層資料變更時更新檢視[^37]。
-同樣,在事件溯源中,應用程式的狀態是透過應用事件日誌來維護的;這裡的應用程式狀態也是一種物化檢視。與流分析場景不同的是,僅考慮某個時間視窗內的事件通常是不夠的:構建物化檢視可能需要任意時間段內的 **所有** 事件,除了那些可能由日誌壓縮丟棄的過時事件(請參閱 “[日誌壓縮](#日誌壓縮)”)。實際上,你需要一個可以一直延伸到時間開端的視窗。
+同樣,在事件溯源中,應用程式的狀態是透過應用事件日誌來維護的;這裡的應用程式狀態也是一種物化檢視。與流分析場景不同的是,僅考慮某個時間視窗內的事件通常是不夠的:構建物化檢視可能需要任意時間段內的 **所有** 事件,除了那些可能由日誌壓縮丟棄的過時事件(請參閱 “[日誌壓縮](#sec_stream_log_compaction)”)。實際上,你需要一個可以一直延伸到時間開端的視窗。
-原則上講,任何流處理元件都可以用於維護物化檢視,儘管 “永遠執行” 與一些面向分析的框架假設的 “主要在有限時間段視窗上執行” 背道而馳,Samza 和 Kafka Streams 支援這種用法,建立在 Kafka 對日誌壓縮的支援上【75】。
+原則上講,任何流處理元件都可以用於維護物化檢視,儘管 “永遠執行” 與一些面向分析的框架假設的 “主要在有限時間段視窗上執行” 背道而馳,Kafka Streams 和 Confluent 的 ksqlDB 支援這種用法,建立在 Kafka 對日誌壓縮的支援上[^58]。
-#### 在流上搜索
+> [!TIP] 增量檢視維護
+> 資料庫看起來很適合做物化檢視維護:它們本來就擅長儲存完整資料副本,也常常支援物化檢視。
+>
+> 但很多資料庫重新整理物化檢視仍依賴批處理或按需觸發(例如 PostgreSQL 的 `REFRESH MATERIALIZED VIEW`),而不是在源資料變化時做增量維護。這會帶來兩個問題:
+>
+> 1. 效率低:每次重新整理都重算全量資料,而不是隻處理變化部分[^38][^59][^60]。
+> 2. 不夠即時:重新整理間隔內的變化不會立刻反映在視圖裡。
+>
+> Materialize、RisingWave、ClickHouse、Feldera 等系統都在探索更即時的增量維護路徑[^61]。
+
+#### 在流上搜索 {#id320}
除了允許搜尋由多個事件構成模式的 CEP 外,有時也存在基於複雜標準(例如全文檢索查詢)來搜尋單個事件的需求。
-例如,媒體監測服務可以訂閱新聞文章 Feed 與來自媒體的播客,搜尋任何關於公司、產品或感興趣的話題的新聞。這是透過預先構建一個搜尋查詢來完成的,然後不斷地將新聞項的流與該查詢進行匹配。在一些網站上也有類似的功能:例如,當市場上出現符合其搜尋條件的新房產時,房地產網站的使用者可以要求網站通知他們。Elasticsearch 的這種過濾器功能,是實現這種流搜尋的一種選擇【76】。
+例如,媒體監測服務可以訂閱新聞文章 Feed 與來自媒體的播客,搜尋任何關於公司、產品或感興趣的話題的新聞。這是透過預先構建一個搜尋查詢來完成的,然後不斷地將新聞項的流與該查詢進行匹配。在一些網站上也有類似的功能:例如,當市場上出現符合其搜尋條件的新房產時,房地產網站的使用者可以要求網站通知他們。Elasticsearch 的 percolator 功能,是實現這種流搜尋的一種選擇[^62]。
-傳統的搜尋引擎首先索引檔案,然後在索引上跑查詢。相比之下,搜尋一個數據流則反了過來:查詢被儲存下來,文件從查詢中流過,就像在 CEP 中一樣。最簡單的情況就是,你可以為每個文件測試每個查詢。但是如果你有大量查詢,這可能會變慢。為了最佳化這個過程,可以像對文件一樣,為查詢建立索引。因而收窄可能匹配的查詢集合【77】。
+傳統的搜尋引擎首先索引檔案,然後在索引上跑查詢。相比之下,搜尋一個數據流則反了過來:查詢被儲存下來,文件從查詢中流過,就像在 CEP 中一樣。最簡單的情況就是,你可以為每個文件測試每個查詢。但是如果你有大量查詢,這可能會變慢。為了最佳化這個過程,可以像對文件一樣,為查詢建立索引。因而收窄可能匹配的查詢集合[^63]。
-#### 訊息傳遞和RPC
+#### 事件驅動架構與 RPC {#sec_stream_actors_drpc}
-在 “[訊息傳遞中的資料流](/tw/ch4#訊息傳遞中的資料流)” 中我們討論過,訊息傳遞系統可以作為 RPC 的替代方案,即作為一種服務間通訊的機制,比如在 Actor 模型中所使用的那樣。儘管這些系統也是基於訊息和事件,但我們通常不會將其視作流處理元件:
+在 “[訊息傳遞中的資料流](/tw/ch5#sec_encoding_dataflow_msg)” 中我們討論過,訊息傳遞系統可以作為 RPC 的替代方案,即作為一種服務間通訊的機制,比如在 Actor 模型中所使用的那樣。儘管這些系統也是基於訊息和事件,但我們通常不會將其視作流處理元件:
* Actor 框架主要是管理模組通訊的併發和分散式執行的一種機制,而流處理主要是一種資料管理技術。
* Actor 之間的交流往往是短暫的、一對一的;而事件日誌則是持久的、多訂閱者的。
* Actor 可以以任意方式進行通訊(包括迴圈的請求 / 響應模式),但流處理通常配置在無環流水線中,其中每個流都是一個特定作業的輸出,由良好定義的輸入流中派生而來。
-也就是說,RPC 類系統與流處理之間有一些交叉領域。例如,Apache Storm 有一個稱為 **分散式 RPC** 的功能,它允許將使用者查詢分散到一系列也處理事件流的節點上;然後這些查詢與來自輸入流的事件交織,而結果可以被彙總併發回給使用者【78】(另請參閱 “[多分割槽資料處理](/tw/ch13#多分割槽資料處理)”)。
+也就是說,RPC 類系統與流處理之間有一些交叉領域。例如,Apache Storm 有一個稱為 **分散式 RPC** 的功能,它允許將使用者查詢分散到一系列也處理事件流的節點上;然後這些查詢與來自輸入流的事件交織,而結果可以被彙總併發回給使用者(另請參閱 “[多分割槽資料處理](/tw/ch13#多分割槽資料處理)”)。
也可以使用 Actor 框架來處理流。但是,很多這樣的框架在崩潰時不能保證訊息的傳遞,除非你實現了額外的重試邏輯,否則這種處理不是容錯的。
-### 時間推理
+### 時間推理 {#sec_stream_time}
流處理通常需要與時間打交道,尤其是用於分析目的時候,會頻繁使用時間視窗,例如 “過去五分鐘的平均值”。“過去五分鐘” 的含義看上去似乎是清晰而無歧義的,但不幸的是,這個概念非常棘手。
-在批處理中過程中,大量的歷史事件被快速地處理。如果需要按時間來分析,批處理器需要檢查每個事件中嵌入的時間戳。讀取執行批處理機器的系統時鐘沒有任何意義,因為處理執行的時間與事件實際發生的時間無關。
+在批處理過程中,大量的歷史事件被快速地處理。如果需要按時間來分析,批處理器需要檢查每個事件中嵌入的時間戳。讀取執行批處理機器的系統時鐘沒有任何意義,因為處理執行的時間與事件實際發生的時間無關。
-批處理可以在幾分鐘內讀取一年的歷史事件;在大多數情況下,感興趣的時間線是歷史中的一年,而不是處理中的幾分鐘。而且使用事件中的時間戳,使得處理是 **確定性** 的:在相同的輸入上再次執行相同的處理過程會得到相同的結果(請參閱 “[容錯](/tw/ch11#容錯)”)。
+批處理可以在幾分鐘內讀取一年的歷史事件;在大多數情況下,感興趣的時間線是歷史中的一年,而不是處理中的幾分鐘。而且使用事件中的時間戳,使得處理是 **確定性** 的:在相同的輸入上再次執行相同的處理過程會得到相同的結果。
-另一方面,許多流處理框架使用處理機器上的本地系統時鐘(**處理時間**,即 processing time)來確定 **視窗(windowing)**【79】。這種方法的優點是簡單,如果事件建立與事件處理之間的延遲可以忽略不計,那也是合理的。然而,如果存在任何顯著的處理延遲 —— 即,事件處理顯著地晚於事件實際發生的時間,這種處理方式就失效了。
+另一方面,許多流處理框架使用處理機器上的本地系統時鐘(**處理時間**,即 processing time)來確定 **視窗(windowing)**[^64]。這種方法的優點是簡單,如果事件建立與事件處理之間的延遲可以忽略不計,那也是合理的。然而,如果存在任何顯著的處理延遲 —— 即,事件處理顯著地晚於事件實際發生的時間,這種處理方式就失效了。
-#### 事件時間與處理時間
+#### 事件時間與處理時間 {#id322}
-很多原因都可能導致處理延遲:排隊,網路故障(請參閱 “[不可靠的網路](/tw/ch8#不可靠的網路)”),效能問題導致訊息代理 / 訊息處理器出現爭用,流消費者重啟,從故障中恢復時重新處理過去的事件(請參閱 “[重播舊訊息](#重播舊訊息)”),或者在修復程式碼 BUG 之後。
+很多原因都可能導致處理延遲:排隊,網路故障(請參閱 “[不可靠的網路](/tw/ch9#sec_distributed_networks)”),效能問題導致訊息代理 / 訊息處理器出現爭用,流消費者重啟,從故障中恢復時重新處理過去的事件(請參閱 “[重播舊訊息](#sec_stream_replay)”),或者在修復程式碼 BUG 之後。
而且,訊息延遲還可能導致無法預測訊息順序。例如,假設使用者首先發出一個 Web 請求(由 Web 伺服器 A 處理),然後發出第二個請求(由伺服器 B 處理)。A 和 B 發出描述它們所處理請求的事件,但是 B 的事件在 A 的事件發生之前到達訊息代理。現在,流處理器將首先看到 B 事件,然後看到 A 事件,即使它們實際上是以相反的順序發生的。
-有一個類比也許能幫助理解,“星球大戰” 電影:第四集於 1977 年發行,第五集於 1980 年,第六集於 1983 年,緊隨其後的是 1999 年的第一集,2002 年的第二集,和 2005 年的第三集,以及 2015 年的第七集【80】[^ii]。如果你按照按照它們上映的順序觀看電影,你處理電影的順序與它們敘事的順序就是不一致的。(集數編號就像事件時間戳,而你觀看電影的日期就是處理時間)作為人類,我們能夠應對這種不連續性,但是流處理演算法需要專門編寫,以適應這種時序與順序的問題。
+有一個類比也許能幫助理解,“星球大戰” 電影:第四集於 1977 年發行,第五集於 1980 年,第六集於 1983 年,緊隨其後的是 1999 年的第一集、2002 年的第二集、2005 年的第三集,以及 2015 年、2017 年和 2019 年的第七至第九集[^65]。如果你按照它們上映的順序觀看電影,你處理電影的順序與它們敘事的順序就是不一致的。(集數編號就像事件時間戳,而你觀看電影的日期就是處理時間)作為人類,我們能夠應對這種不連續性,但是流處理演算法需要專門編寫,以適應這種時序與順序的問題。
-[^ii]: 感謝 Flink 社群的 Kostas Kloudas 提出這個比喻。
+將事件時間和處理時間搞混會導致錯誤的資料。例如,假設你有一個流處理器用於測量請求速率(計算每秒請求數)。如果你重新部署流處理器,它可能會停止一分鐘,並在恢復之後處理積壓的事件。如果你按處理時間來衡量速率,那麼在處理積壓日誌時,請求速率看上去就像有一個異常的突發尖峰,而實際上請求速率是穩定的([圖 12-8](/fig/ddia_1208.png))。
-將事件時間和處理時間搞混會導致錯誤的資料。例如,假設你有一個流處理器用於測量請求速率(計算每秒請求數)。如果你重新部署流處理器,它可能會停止一分鐘,並在恢復之後處理積壓的事件。如果你按處理時間來衡量速率,那麼在處理積壓日誌時,請求速率看上去就像有一個異常的突發尖峰,而實際上請求速率是穩定的([圖 11-7](/v1/ddia_1107.png))。
+
-
+**圖 12-8 按處理時間分窗,會因為處理速率的變動引入人為因素**
-**圖 11-7 按處理時間分窗,會因為處理速率的變動引入人為因素**
-
-#### 知道什麼時候準備好了
+#### 處理滯留事件 {#id323}
用事件時間來定義視窗的一個棘手的問題是,你永遠也無法確定是不是已經收到了特定視窗的所有事件,還是說還有一些事件正在來的路上。
例如,假設你將事件分組為一分鐘的視窗,以便統計每分鐘的請求數。你已經計數了一些帶有本小時內第 37 分鐘時間戳的事件,時間流逝,現在進入的主要都是本小時內第 38 和第 39 分鐘的事件。什麼時候才能宣佈你已經完成了第 37 分鐘的視窗計數,並輸出其計數器值?
-在一段時間沒有看到任何新的事件之後,你可以超時並宣佈一個視窗已經就緒,但仍然可能發生這種情況:某些事件被緩衝在另一臺機器上,由於網路中斷而延遲。你需要能夠處理這種在視窗宣告完成之後到達的 **滯留(straggler)** 事件。大體上,你有兩種選擇【1】:
+在一段時間沒有看到任何新的事件之後,你可以超時並宣佈一個視窗已經就緒,但仍然可能發生這種情況:某些事件被緩衝在另一臺機器上,由於網路中斷而延遲。你需要能夠處理這種在視窗宣告完成之後到達的 **滯留(straggler)** 事件。大體上,你有兩種選擇[^1]:
1. 忽略這些滯留事件,因為在正常情況下它們可能只是事件中的一小部分。你可以將丟棄事件的數量作為一個監控指標,並在出現大量丟訊息的情況時報警。
2. 釋出一個 **更正(correction)**,一個包括滯留事件的更新視窗值。你可能還需要收回以前的輸出。
-在某些情況下,可以使用特殊的訊息來指示 “從現在開始,不會有比 t 更早時間戳的訊息了”,消費者可以使用它來觸發視窗【81】。但是,如果不同機器上的多個生產者都在生成事件,每個生產者都有自己的最小時間戳閾值,則消費者需要分別跟蹤每個生產者。在這種情況下,新增和刪除生產者都是比較棘手的。
+在某些情況下,可以使用特殊的訊息來指示 “從現在開始,不會有比 t 更早時間戳的訊息了”,消費者可以使用它來觸發視窗[^66]。但是,如果不同機器上的多個生產者都在生成事件,每個生產者都有自己的最小時間戳閾值,則消費者需要分別跟蹤每個生產者。在這種情況下,新增和刪除生產者都是比較棘手的。
-#### 你用的是誰的時鐘?
+#### 你用的是誰的時鐘? {#id438}
當事件可能在系統內多個地方進行緩衝時,為事件分配時間戳更加困難了。例如,考慮一個移動應用向伺服器上報關於用量的事件。該應用可能會在裝置處於離線狀態時被使用,在這種情況下,它將在裝置本地緩衝事件,並在下一次網際網路連線可用時向伺服器上報這些事件(可能是幾小時甚至幾天)。對於這個流的任意消費者而言,它們就如延遲極大的滯留事件一樣。
-在這種情況下,事件上的事件戳實際上應當是使用者交互發生的時間,取決於移動裝置的本地時鐘。然而使用者控制的裝置上的時鐘通常是不可信的,因為它可能會被無意或故意設定成錯誤的時間(請參閱 “[時鐘同步與準確性](/tw/ch8#時鐘同步與準確性)”)。伺服器收到事件的時間(取決於伺服器的時鐘)可能是更準確的,因為伺服器在你的控制之下,但在描述使用者互動方面意義不大。
+在這種情況下,事件上的時間戳實際上應當是使用者交互發生的時間,取決於移動裝置的本地時鐘。然而使用者控制的裝置上的時鐘通常是不可信的,因為它可能會被無意或故意設定成錯誤的時間(請參閱 “[時鐘同步與準確性](/tw/ch9#sec_distributed_clock_accuracy)”)。伺服器收到事件的時間(取決於伺服器的時鐘)可能是更準確的,因為伺服器在你的控制之下,但在描述使用者互動方面意義不大。
-要校正不正確的裝置時鐘,一種方法是記錄三個時間戳【82】:
+要校正不正確的裝置時鐘,一種方法是記錄三個時間戳[^67]:
* 事件發生的時間,取決於裝置時鐘
* 事件傳送往伺服器的時間,取決於裝置時鐘
@@ -543,31 +543,31 @@ CEP 的實現包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
這並不是流處理獨有的問題,批處理有著完全一樣的時間推理問題。只是在流處理的上下文中,我們更容易意識到時間的流逝。
-#### 視窗的型別
+#### 視窗的型別 {#id324}
-當你知道如何確定一個事件的時間戳後,下一步就是如何定義時間段的視窗。然後視窗就可以用於聚合,例如事件計數,或計算視窗內值的平均值。有幾種視窗很常用【79,83】:
+當你知道如何確定一個事件的時間戳後,下一步就是如何定義時間段的視窗。然後視窗就可以用於聚合,例如事件計數,或計算視窗內值的平均值。有幾種視窗很常用[^64][^68]:
滾動視窗(Tumbling Window)
: 滾動視窗有著固定的長度,每個事件都僅能屬於一個視窗。例如,假設你有一個 1 分鐘的滾動視窗,則所有時間戳在 `10:03:00` 和 `10:03:59` 之間的事件會被分組到一個視窗中,`10:04:00` 和 `10:04:59` 之間的事件被分組到下一個視窗,依此類推。透過將每個事件時間戳四捨五入至最近的分鐘來確定它所屬的視窗,可以實現 1 分鐘的滾動視窗。
跳動視窗(Hopping Window)
-: 跳動視窗也有著固定的長度,但允許視窗重疊以提供一些平滑。例如,一個帶有 1 分鐘跳躍步長的 5 分鐘視窗將包含 `10:03:00` 至 `10:07:59` 之間的事件,而下一個視窗將覆蓋 `10:04:00` 至 `10:08:59` 之間的事件,等等。透過首先計算 1 分鐘的滾動視窗(tunmbling window),然後在幾個相鄰視窗上進行聚合,可以實現這種跳動視窗。
+: 跳動視窗也有著固定的長度,但允許視窗重疊以提供一些平滑。例如,一個帶有 1 分鐘跳躍步長的 5 分鐘視窗將包含 `10:03:00` 至 `10:07:59` 之間的事件,而下一個視窗將覆蓋 `10:04:00` 至 `10:08:59` 之間的事件,等等。透過首先計算 1 分鐘的滾動視窗(tumbling window),然後在幾個相鄰視窗上進行聚合,可以實現這種跳動視窗。
滑動視窗(Sliding Window)
: 滑動視窗包含了彼此間距在特定時長內的所有事件。例如,一個 5 分鐘的滑動視窗應當覆蓋 `10:03:39` 和 `10:08:12` 的事件,因為它們相距不超過 5 分鐘(注意滾動視窗與步長 5 分鐘的跳動視窗可能不會把這兩個事件分組到同一個視窗中,因為它們使用固定的邊界)。透過維護一個按時間排序的事件緩衝區,並不斷從視窗中移除過期的舊事件,可以實現滑動視窗。
會話視窗(Session window)
-: 與其他視窗型別不同,會話視窗沒有固定的持續時間,而定義為:將同一使用者出現時間相近的所有事件分組在一起,而當用戶一段時間沒有活動時(例如,如果 30 分鐘內沒有事件)視窗結束。會話切分是網站分析的常見需求(請參閱 “[分組](/v1/ch10#分組)”)。
+: 與其他視窗型別不同,會話視窗沒有固定的持續時間,而定義為:將同一使用者出現時間相近的所有事件分組在一起,而當用戶一段時間沒有活動時(例如,如果 30 分鐘內沒有事件)視窗結束。會話切分是網站分析的常見需求(請參閱 “[JOIN 與 GROUP BY](/tw/ch11#sec_batch_join)”)。
-### 流連線
+### 流連線 {#sec_stream_joins}
在 [第十一章](/tw/ch11) 中,我們討論了批處理作業如何透過鍵來連線資料集,以及這種連線是如何成為資料管道的重要組成部分的。由於流處理將資料管道泛化為對無限資料集進行增量處理,因此對流進行連線的需求也是完全相同的。
-然而,新事件隨時可能出現在一個流中,這使得流連線要比批處理連線更具挑戰性。為了更好地理解情況,讓我們先來區分三種不同型別的連線:**流 - 流** 連線,**流 - 表** 連線,與 **表 - 表** 連線【84】。我們將在下面的章節中透過例子來說明。
+然而,新事件隨時可能出現在一個流中,這使得流連線要比批處理連線更具挑戰性。為了更好地理解情況,讓我們先來區分三種不同型別的連線:**流 - 流** 連線,**流 - 表** 連線,與 **表 - 表** 連線。我們將在下面的章節中透過例子來說明。
-#### 流流連線(視窗連線)
+#### 流流連線(視窗連線) {#id440}
-假設你的網站上有搜尋功能,而你想要找出搜尋 URL 的近期趨勢。每當有人鍵入搜尋查詢時,都會記錄下一個包含查詢與其返回結果的事件。每當有人點選其中一個搜尋結果時,就會記錄另一個記錄點選事件。為了計算搜尋結果中每個 URL 的點選率,你需要將搜尋動作與點選動作的事件連在一起,這些事件透過相同的會話 ID 進行連線。廣告系統中需要類似的分析【85】。
+假設你的網站上有搜尋功能,而你想要找出搜尋 URL 的近期趨勢。每當有人鍵入搜尋查詢時,都會記錄下一個包含查詢與其返回結果的事件。每當有人點選其中一個搜尋結果時,就會記錄另一個記錄點選事件。為了計算搜尋結果中每個 URL 的點選率,你需要將搜尋動作與點選動作的事件連在一起,這些事件透過相同的會話 ID 進行連線。廣告系統中需要類似的分析[^69]。
如果使用者丟棄了搜尋結果,點選可能永遠不會發生,即使它出現了,搜尋與點選之間的時間可能是高度可變的:在很多情況下,它可能是幾秒鐘,但也可能長達幾天或幾周(如果使用者執行搜尋,忘掉了這個瀏覽器頁面,過了一段時間後重新回到這個瀏覽器頁面上,並點選了一個結果)。由於可變的網路延遲,點選事件甚至可能先於搜尋事件到達。你可以選擇合適的連線視窗 —— 例如,如果點選與搜尋之間的時間間隔在一小時內,你可能會選擇連線兩者。
@@ -575,30 +575,30 @@ CEP 的實現包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
為了實現這種型別的連線,流處理器需要維護 **狀態**:例如,按會話 ID 索引最近一小時內發生的所有事件。無論何時發生搜尋事件或點選事件,都會被新增到合適的索引中,而流處理器也會檢查另一個索引是否有具有相同會話 ID 的事件到達。如果有匹配事件就會發出一個表示搜尋結果被點選的事件;如果搜尋事件直到過期都沒看見有匹配的點選事件,就會發出一個表示搜尋結果未被點選的事件。
-#### 流表連線(流擴充)
+#### 流表連線(流擴充) {#sec_stream_table_joins}
-在 “[示例:使用者活動事件分析](/v1/ch10#示例:使用者活動事件分析)”([圖 10-2](/v1/ddia_1002.png))中,我們看到了連線兩個資料集的批處理作業示例:一組使用者活動事件和一個使用者檔案資料庫。將使用者活動事件視為流,並在流處理器中連續執行相同的連線是很自然的想法:輸入是包含使用者 ID 的活動事件流,而輸出還是活動事件流,但其中使用者 ID 已經被擴充套件為使用者的檔案資訊。這個過程有時被稱為使用資料庫的資訊來 **擴充(enriching)** 活動事件。
+在 “[示例:使用者活動事件分析](/tw/ch11#sec_batch_join)”([圖 11-2](/fig/ddia_1102.png))中,我們看到了連線兩個資料集的批處理作業示例:一組使用者活動事件和一個使用者檔案資料庫。將使用者活動事件視為流,並在流處理器中連續執行相同的連線是很自然的想法:輸入是包含使用者 ID 的活動事件流,而輸出還是活動事件流,但其中使用者 ID 已經被擴充套件為使用者的檔案資訊。這個過程有時被稱為使用資料庫的資訊來 **擴充(enriching)** 活動事件。
-要執行此連線,流處理器需要一次處理一個活動事件,在資料庫中查詢事件的使用者 ID,並將檔案資訊新增到活動事件中。資料庫查詢可以透過查詢遠端資料庫來實現。但正如在 “[示例:使用者活動事件分析](/v1/ch10#示例:使用者活動事件分析)” 一節中討論的,此類遠端查詢可能會很慢,並且有可能導致資料庫過載【75】。
+要執行此連線,流處理器需要一次處理一個活動事件,在資料庫中查詢事件的使用者 ID,並將檔案資訊新增到活動事件中。資料庫查詢可以透過查詢遠端資料庫來實現。但正如在 “[示例:使用者活動事件分析](/tw/ch11#sec_batch_join)” 一節中討論的,此類遠端查詢可能會很慢,並且有可能導致資料庫過載[^58]。
-另一種方法是將資料庫副本載入到流處理器中,以便在本地進行查詢而無需網路往返。這種技術與我們在 “[Map 側連線](/v1/ch10#Map側連線)” 中討論的雜湊連線非常相似:如果資料庫的本地副本足夠小,則可以是記憶體中的散列表,比較大的話也可以是本地磁碟上的索引。
+另一種方法是將資料庫副本載入到流處理器中,以便在本地進行查詢而無需網路往返。這種技術與我們在 “[JOIN 與 GROUP BY](/tw/ch11#sec_batch_join)” 中討論的雜湊連線非常相似:如果資料庫的本地副本足夠小,則可以是記憶體中的散列表,比較大的話也可以是本地磁碟上的索引。
-與批處理作業的區別在於,批處理作業使用資料庫的時間點快照作為輸入,而流處理器是長時間執行的,且資料庫的內容可能隨時間而改變,所以流處理器資料庫的本地副本需要保持更新。這個問題可以透過變更資料捕獲來解決:流處理器可以訂閱使用者檔案資料庫的更新日誌,如同活動事件流一樣。當增添或修改檔案時,流處理器會更新其本地副本。因此,我們有了兩個流之間的連線:活動事件和檔案更新。
+與批處理作業的區別在於,批處理作業使用資料庫的時間點快照作為輸入,而流處理器是長時間執行的,且資料庫的內容可能隨時間而改變,所以流處理器資料庫的本地副本需要保持更新。這個問題可以透過資料變更捕獲來解決:流處理器可以訂閱使用者檔案資料庫的更新日誌,如同活動事件流一樣。當增添或修改檔案時,流處理器會更新其本地副本。因此,我們有了兩個流之間的連線:活動事件和檔案更新。
流表連線實際上非常類似於流流連線;最大的區別在於對於表的變更日誌流,連線使用了一個可以回溯到 “時間起點” 的視窗(概念上是無限的視窗),新版本的記錄會覆蓋更早的版本。對於輸入的流,連線可能壓根兒就沒有維護任何視窗。
-#### 表表連線(維護物化檢視)
+#### 表表連線(維護物化檢視) {#id326}
-我們在 “[描述負載](/v1/ch1#描述負載)” 中討論的推特時間線例子時說過,當用戶想要檢視他們的主頁時間線時,迭代使用者所關注人群的推文併合並它們是一個開銷巨大的操作。
+我們在 “[描述負載](/tw/ch2#sec_introduction_twitter)” 中討論的推特時間線例子時說過,當用戶想要檢視他們的主頁時間線時,迭代使用者所關注人群的推文併合並它們是一個開銷巨大的操作。
相反,我們需要一個時間線快取:一種每個使用者的 “收件箱”,在傳送推文的時候寫入這些資訊,因而讀取時間線時只需要簡單地查詢即可。物化與維護這個快取需要處理以下事件:
* 當用戶 u 傳送新的推文時,它將被新增到每個關注使用者 u 的時間線上。
-* 使用者刪除推文時,推文將從所有使用者的時間表中刪除。
-* 當用戶 $u_1$ 開始關注使用者 $u_2$ 時,$u_2$ 最近的推文將被新增到 $u_1$ 的時間線上。
-* 當用戶 $u_1$ 取消關注使用者 $u_2$ 時,$u_2$ 的推文將從 $u_1$ 的時間線中移除。
+* 使用者刪除推文時,推文將從所有使用者的時間線中刪除。
+* 當用戶 *u*~1~ 開始關注使用者 *u*~2~ 時,*u*~2~ 最近的推文將被新增到 *u*~1~ 的時間線上。
+* 當用戶 *u*~1~ 取消關注使用者 *u*~2~ 時,*u*~2~ 的推文將從 *u*~1~ 的時間線中移除。
-要在流處理器中實現這種快取維護,你需要推文事件流(傳送與刪除)和關注關係事件流(關注與取消關注)。流處理需要維護一個數據庫,包含每個使用者的粉絲集合。以便知道當一條新推文到達時,需要更新哪些時間線【86】。
+要在流處理器中實現這種快取維護,你需要推文事件流(傳送與刪除)和關注關係事件流(關注與取消關注)。流處理需要維護一個數據庫,包含每個使用者的粉絲集合,以便知道當一條新推文到達時,需要更新哪些時間線。
觀察這個流處理過程的另一種視角是:它維護了一個連線了兩個表(推文與關注)的物化檢視,如下所示:
@@ -610,90 +610,91 @@ JOIN follows ON follows.followee_id = tweets.sender_id
GROUP BY follows.follower_id
```
-流連線直接對應於這個查詢中的表連線。時間線實際上是這個查詢結果的快取,每當底層的表發生變化時都會更新 [^iii]。
+流連線直接對應於這個查詢中的表連線。時間線實際上是這個查詢結果的快取,每當底層的表發生變化時都會更新。
-[^iii]: 如果你將流視作表的派生物,如 [圖 11-6](/v1/ddia_1106.png) 所示,而把一個連線看作是兩個表的乘法u·v,那麼會發生一些有趣的事情:物化連線的變化流遵循乘積法則:(u·v)'= u'v + uv'。換句話說,任何推文的變化量都與當前的關注聯絡在一起,任何關注的變化量都與當前的推文相連線【49,50】。
+> [!NOTE]
+> 如果你將流視作表的導數(如 [圖 12-7](/fig/ddia_1207.png) 所示),並把連線看作兩個表 *u·v* 的乘積,那麼會出現一個有趣現象:物化連線的變化流遵循乘積法則 \( (u \cdot v)' = u'v + uv' \)。換句話說,任何推文變化都要和當前關注關係連線,任何關注關係變化都要和當前推文連線[^37]。
-#### 連線的時間依賴性
+#### 連線的時間依賴性 {#sec_stream_join_time}
這裡描述的三種連線(流流,流表,表表)有很多共通之處:它們都需要流處理器維護連線一側的一些狀態(搜尋與點選事件,使用者檔案,關注列表),然後當連線另一側的訊息到達時查詢該狀態。
用於維護狀態的事件順序是很重要的(先關注然後取消關注,或者其他類似操作)。在分割槽日誌中,單個分割槽內的事件順序是保留下來的。但典型情況下是沒有跨流或跨分割槽的順序保證的。
-這就產生了一個問題:如果不同流中的事件發生在近似的時間範圍內,則應該按照什麼樣的順序進行處理?在流表連線的例子中,如果使用者更新了它們的檔案,哪些活動事件與舊檔案連線(在檔案更新前處理),哪些又與新檔案連線(在檔案更新之後處理)?換句話說:你需要對一些狀態做連線,如果狀態會隨著時間推移而變化,那應當使用什麼時間點來連線呢【45】?
+這就產生了一個問題:如果不同流中的事件發生在近似的時間範圍內,則應該按照什麼樣的順序進行處理?在流表連線的例子中,如果使用者更新了它們的檔案,哪些活動事件與舊檔案連線(在檔案更新前處理),哪些又與新檔案連線(在檔案更新之後處理)?換句話說:你需要對一些狀態做連線,如果狀態會隨著時間推移而變化,那應當使用什麼時間點來連線呢?
這種時序依賴可能出現在很多地方。例如銷售東西需要對發票應用適當的稅率,這取決於所處的國家 / 州,產品型別,銷售日期(因為稅率時不時會變化)。當連線銷售額與稅率表時,你可能期望的是使用銷售時的稅率參與連線。如果你正在重新處理歷史資料,銷售時的稅率可能和現在的稅率有所不同。
-如果跨越流的事件順序是未定的,則連線會變為不確定性的【87】,這意味著你在同樣輸入上重跑相同的作業未必會得到相同的結果:當你重跑任務時,輸入流上的事件可能會以不同的方式交織。
+如果跨越流的事件順序是未定的,則連線會變為不確定性的[^70],這意味著你在同樣輸入上重跑相同的作業未必會得到相同的結果:當你重跑任務時,輸入流上的事件可能會以不同的方式交織。
-在資料倉庫中,這個問題被稱為 **緩慢變化的維度(slowly changing dimension, SCD)**,通常透過對特定版本的記錄使用唯一的識別符號來解決:例如,每當稅率改變時都會獲得一個新的識別符號,而發票在銷售時會帶有稅率的識別符號【88,89】。這種變化使連線變為確定性的,但也會導致日誌壓縮無法進行:表中所有的記錄版本都需要保留。
+在資料倉庫中,這個問題被稱為 **緩慢變化的維度(slowly changing dimension, SCD)**,通常透過對特定版本的記錄使用唯一的識別符號來解決:例如,每當稅率改變時都會獲得一個新的識別符號,而發票在銷售時會帶有稅率的識別符號[^71][^72]。這種變化使連線變為確定性的,但也會導致日誌壓縮無法進行:表中所有的記錄版本都需要保留。
-### 容錯
+### 容錯 {#sec_stream_fault_tolerance}
在本章的最後一節中,讓我們看一看流處理是如何容錯的。我們在 [第十一章](/tw/ch11) 中看到,批處理框架可以很容易地容錯:如果 MapReduce 作業中的任務失敗,可以簡單地在另一臺機器上再次啟動,並且丟棄失敗任務的輸出。這種透明的重試是可能的,因為輸入檔案是不可變的,每個任務都將其輸出寫入到 HDFS 上的獨立檔案中,而輸出僅當任務成功完成後可見。
-特別是,批處理容錯方法可確保批處理作業的輸出與沒有出錯的情況相同,即使實際上某些任務失敗了。看起來好像每條輸入記錄都被處理了恰好一次 —— 沒有記錄被跳過,而且沒有記錄被處理兩次。儘管重啟任務意味著實際上可能會多次處理記錄,但輸出中的可見效果看上去就像只處理過一次。這個原則被稱為 **恰好一次語義(exactly-once semantics)**,儘管 **等效一次(effectively-once)** 可能會是一個更寫實的術語【90】。
+特別是,批處理容錯方法可確保批處理作業的輸出與沒有出錯的情況相同,即使實際上某些任務失敗了。看起來好像每條輸入記錄都被處理了恰好一次 —— 沒有記錄被跳過,而且沒有記錄被處理兩次。儘管重啟任務意味著實際上可能會多次處理記錄,但輸出中的可見效果看上去就像只處理過一次。這個原則被稱為 **恰好一次語義(exactly-once semantics)**,儘管 **等效一次(effectively-once)** 可能會是一個更寫實的術語[^73]。
在流處理中也出現了同樣的容錯問題,但是處理起來沒有那麼直觀:等待某個任務完成之後再使其輸出可見並不是一個可行選項,因為你永遠無法處理完一個無限的流。
-#### 微批次與存檔點
+#### 微批次與存檔點 {#id329}
-一個解決方案是將流分解成小塊,並像微型批處理一樣處理每個塊。這種方法被稱為 **微批次(microbatching)**,它被用於 Spark Streaming 【91】。批次的大小通常約為 1 秒,這是對效能妥協的結果:較小的批次會導致更大的排程與協調開銷,而較大的批次意味著流處理器結果可見之前的延遲要更長。
+一個解決方案是將流分解成小塊,並像微型批處理一樣處理每個塊。這種方法被稱為 **微批次(microbatching)**,它被用於 Spark Streaming[^74]。批次的大小通常約為 1 秒,這是對效能妥協的結果:較小的批次會導致更大的排程與協調開銷,而較大的批次意味著流處理器結果可見之前的延遲要更長。
微批次也隱式提供了一個與批次大小相等的滾動視窗(按處理時間而不是事件時間戳分窗)。任何需要更大視窗的作業都需要顯式地將狀態從一個微批次轉移到下一個微批次。
-Apache Flink 則使用不同的方法,它會定期生成狀態的滾動存檔點並將其寫入持久儲存【92,93】。如果流運算元崩潰,它可以從最近的存檔點重啟,並丟棄從最近檢查點到崩潰之間的所有輸出。存檔點會由訊息流中的 **壁障(barrier)** 觸發,類似於微批次之間的邊界,但不會強制一個特定的視窗大小。
+Apache Flink 則使用不同的方法,它會定期生成狀態的滾動存檔點並將其寫入持久儲存[^75][^76]。如果流運算元崩潰,它可以從最近的存檔點重啟,並丟棄從最近檢查點到崩潰之間的所有輸出。存檔點會由訊息流中的 **壁障(barrier)** 觸發,類似於微批次之間的邊界,但不會強制一個特定的視窗大小。
在流處理框架的範圍內,微批次與存檔點方法提供了與批處理一樣的 **恰好一次語義**。但是,只要輸出離開流處理器(例如,寫入資料庫,向外部訊息代理傳送訊息,或傳送電子郵件),框架就無法拋棄失敗批次的輸出了。在這種情況下,重啟失敗任務會導致外部副作用發生兩次,只有微批次或存檔點不足以阻止這一問題。
-#### 原子提交再現
+#### 原子提交再現 {#sec_stream_atomic_commit}
為了在出現故障時表現出恰好處理一次的樣子,我們需要確保事件處理的所有輸出和副作用 **當且僅當** 處理成功時才會生效。這些影響包括傳送給下游運算元或外部訊息傳遞系統(包括電子郵件或推送通知)的任何訊息,任何資料庫寫入,對運算元狀態的任何變更,以及對輸入訊息的任何確認(包括在基於日誌的訊息代理中將消費者偏移量前移)。
-這些事情要麼都原子地發生,要麼都不發生,但是它們不應當失去同步。如果這種方法聽起來很熟悉,那是因為我們在分散式事務和兩階段提交的上下文中討論過它(請參閱 “[恰好一次的訊息處理](/tw/ch9#恰好一次的訊息處理)”)。
+這些事情要麼都原子地發生,要麼都不發生,但是它們不應當失去同步。如果這種方法聽起來很熟悉,那是因為我們在分散式事務和兩階段提交的上下文中討論過它(請參閱 “[恰好一次的訊息處理](/tw/ch8#sec_transactions_exactly_once)”)。
-在 [第九章](/tw/ch9) 中,我們討論了分散式事務傳統實現中的問題(如 XA)。然而在限制更為嚴苛的環境中,也是有可能高效實現這種原子提交機制的。Google Cloud Dataflow【81,92】和 VoltDB 【94】中使用了這種方法,Apache Kafka 有計劃加入類似的功能【95,96】。與 XA 不同,這些實現不會嘗試跨異構技術提供事務,而是透過在流處理框架中同時管理狀態變更與訊息傳遞來內化事務。事務協議的開銷可以透過在單個事務中處理多個輸入訊息來分攤。
+在 [第十章](/tw/ch10) 中,我們討論了分散式事務傳統實現中的問題(如 XA)。然而在限制更為嚴苛的環境中,也是有可能高效實現這種原子提交機制的。Google Cloud Dataflow[^66][^75]、VoltDB[^77] 和 Apache Kafka[^78][^79] 中都使用了這種方法。與 XA 不同,這些實現不會嘗試跨異構技術提供事務,而是透過在流處理框架中同時管理狀態變更與訊息傳遞來內化事務。事務協議的開銷可以透過在單個事務中處理多個輸入訊息來分攤。
-#### 冪等性
+#### 冪等性 {#sec_stream_idempotence}
-我們的目標是丟棄任何失敗任務的部分輸出,以便能安全地重試,而不會生效兩次。分散式事務是實現這個目標的一種方式,而另一種方式是依賴 **冪等性(idempotence)**【97】。
+我們的目標是丟棄任何失敗任務的部分輸出,以便能安全地重試,而不會生效兩次。分散式事務是實現這個目標的一種方式,而另一種方式是依賴 **冪等性(idempotence)**[^80]。
冪等操作是多次重複執行與單次執行效果相同的操作。例如,將鍵值儲存中的某個鍵設定為某個特定值是冪等的(再次寫入該值,只是用同樣的值替代),而遞增一個計數器不是冪等的(再次執行遞增意味著該值遞增兩次)。
即使一個操作不是天生冪等的,往往可以透過一些額外的元資料做成冪等的。例如,在使用來自 Kafka 的訊息時,每條訊息都有一個持久的、單調遞增的偏移量。將值寫入外部資料庫時可以將這個偏移量帶上,這樣你就可以判斷一條更新是不是已經執行過了,因而避免重複執行。
-Storm 的 Trident 基於類似的想法來處理狀態【78】。依賴冪等性意味著隱含了一些假設:重啟一個失敗的任務必須以相同的順序重播相同的訊息(基於日誌的訊息代理能做這些事),處理必須是確定性的,沒有其他節點能同時更新相同的值【98,99】。
+Storm 的 Trident 基於類似的想法來處理狀態。依賴冪等性意味著隱含了一些假設:重啟一個失敗的任務必須以相同的順序重播相同的訊息(基於日誌的訊息代理能做這些事),處理必須是確定性的,沒有其他節點能同時更新相同的值[^81][^82]。
-當從一個處理節點故障切換到另一個節點時,可能需要進行 **防護**(fencing,請參閱 “[領導者和鎖](/tw/ch8#領導者和鎖)”),以防止被假死節點干擾。儘管有這麼多注意事項,冪等操作是一種實現 **恰好一次語義** 的有效方式,僅需很小的額外開銷。
+當從一個處理節點故障切換到另一個節點時,可能需要進行 **防護**(fencing,請參閱 “[領導者和鎖](/tw/ch9#sec_distributed_lock_fencing)”),以防止被假死節點干擾。儘管有這麼多注意事項,冪等操作是一種實現 **恰好一次語義** 的有效方式,僅需很小的額外開銷。
-#### 失敗後重建狀態
+#### 失敗後重建狀態 {#sec_stream_state_fault_tolerance}
任何需要狀態的流處理 —— 例如,任何視窗聚合(例如計數器,平均值和直方圖)以及任何用於連線的表和索引,都必須確保在失敗之後能恢復其狀態。
-一種選擇是將狀態儲存在遠端資料儲存中,並進行復制,然而正如在 “[流表連線(流擴充)](#流表連線(流擴充))” 中所述,每個訊息都要查詢遠端資料庫可能會很慢。另一種方法是在流處理器本地儲存狀態,並定期複製。然後當流處理器從故障中恢復時,新任務可以讀取狀態副本,恢復處理而不丟失資料。
+一種選擇是將狀態儲存在遠端資料儲存中,並進行復制,然而正如在 “[流表連線(流擴充)](#sec_stream_table_joins)” 中所述,每個訊息都要查詢遠端資料庫可能會很慢。另一種方法是在流處理器本地儲存狀態,並定期複製。然後當流處理器從故障中恢復時,新任務可以讀取狀態副本,恢復處理而不丟失資料。
-例如,Flink 定期捕獲運算元狀態的快照,並將它們寫入 HDFS 等持久儲存中【92,93】。Samza 和 Kafka Streams 透過將狀態變更傳送到具有日誌壓縮功能的專用 Kafka 主題來複制狀態變更,這與變更資料捕獲類似【84,100】。VoltDB 透過在多個節點上對每個輸入訊息進行冗餘處理來複制狀態(請參閱 “[真的序列執行](/tw/ch7#真的序列執行)”)。
+例如,Flink 定期捕獲運算元狀態的快照,並將它們寫入 HDFS 等持久儲存中[^75][^76]。Kafka Streams 透過將狀態變更傳送到具有日誌壓縮功能的專用 Kafka 主題來複制狀態變更,這與資料變更捕獲類似[^83]。VoltDB 透過在多個節點上對每個輸入訊息進行冗餘處理來複制狀態(請參閱 “[真的序列執行](/tw/ch8#sec_transactions_serial)”)。
-在某些情況下,甚至可能都不需要複製狀態,因為它可以從輸入流重建。例如,如果狀態是從相當短的視窗中聚合而成,則簡單地重播該視窗中的輸入事件可能是足夠快的。如果狀態是透過變更資料捕獲來維護的資料庫的本地副本,那麼也可以從日誌壓縮的變更流中重建資料庫(請參閱 “[日誌壓縮](#日誌壓縮)”)。
+在某些情況下,甚至可能都不需要複製狀態,因為它可以從輸入流重建。例如,如果狀態是從相當短的視窗中聚合而成,則簡單地重播該視窗中的輸入事件可能是足夠快的。如果狀態是透過資料變更捕獲來維護的資料庫的本地副本,那麼也可以從日誌壓縮的變更流中重建資料庫(請參閱 “[日誌壓縮](#sec_stream_log_compaction)”)。
然而,所有這些權衡取決於底層基礎架構的效能特徵:在某些系統中,網路延遲可能低於磁碟訪問延遲,網路頻寬也可能與磁碟頻寬相當。沒有針對所有情況的普適理想權衡,隨著儲存和網路技術的發展,本地狀態與遠端狀態的優點也可能會互換。
-## 本章小結
+## 本章小結 {#id332}
-在本章中,我們討論了事件流,它們所服務的目的,以及如何處理它們。在某些方面,流處理非常類似於在 [第十一章](/tw/ch11) 中討論的批處理,不過是在無限的(永無止境的)流而不是固定大小的輸入上持續進行。從這個角度來看,訊息代理和事件日誌可以視作檔案系統的流式等價物。
+在本章中,我們討論了事件流,它們所服務的目的,以及如何處理它們。在某些方面,流處理非常類似於在 [第十一章](/tw/ch11) 中討論的批處理,不過是在無限的(永無止境的)流而不是固定大小的輸入上持續進行[^84]。從這個角度來看,訊息代理和事件日誌可以視作檔案系統的流式等價物。
我們花了一些時間比較兩種訊息代理:
AMQP/JMS 風格的訊息代理
-: 代理將單條訊息分配給消費者,消費者在成功處理單條訊息後確認訊息。訊息被確認後從代理中刪除。這種方法適合作為一種非同步形式的 RPC(另請參閱 “[訊息傳遞中的資料流](/v1/ch4#訊息傳遞中的資料流)”),例如在任務佇列中,訊息處理的確切順序並不重要,而且訊息在處理完之後,不需要回頭重新讀取舊訊息。
+: 代理將單條訊息分配給消費者,消費者在成功處理單條訊息後確認訊息。訊息被確認後從代理中刪除。這種方法適合作為一種非同步形式的 RPC(另請參閱 “[事件驅動的架構](/tw/ch5#sec_encoding_dataflow_msg)”),例如在任務佇列中,訊息處理的確切順序並不重要,而且訊息在處理完之後,不需要回頭重新讀取舊訊息。
基於日誌的訊息代理
: 代理將一個分割槽中的所有訊息分配給同一個消費者節點,並始終以相同的順序傳遞訊息。並行是透過分割槽實現的,消費者透過存檔最近處理訊息的偏移量來跟蹤工作進度。訊息代理將訊息保留在磁碟上,因此如有必要的話,可以回跳並重新讀取舊訊息。
-基於日誌的方法與資料庫中的複製日誌(請參閱 [第五章](/v1/ch5))和日誌結構儲存引擎(請參閱 [第三章](/v1/ch3))有相似之處。我們看到,這種方法對於消費輸入流,併產生派生狀態或派生輸出資料流的系統而言特別適用。
+基於日誌的方法與資料庫中的複製日誌(請參閱 [第六章](/tw/ch6))和日誌結構儲存引擎(請參閱 [第四章](/tw/ch4))有相似之處。我們看到,這種方法對於消費輸入流,併產生派生狀態或派生輸出資料流的系統而言特別適用。
-就流的來源而言,我們討論了幾種可能性:使用者活動事件,定期讀數的感測器,和 Feed 資料(例如,金融中的市場資料)能夠自然地表示為流。我們發現將資料庫寫入視作流也是很有用的:我們可以捕獲變更日誌 —— 即對資料庫所做的所有變更的歷史記錄 —— 隱式地透過變更資料捕獲,或顯式地透過事件溯源。日誌壓縮允許流也能保有資料庫內容的完整副本。
+就流的來源而言,我們討論了幾種可能性:使用者活動事件,定期讀數的感測器,和 Feed 資料(例如,金融中的市場資料)能夠自然地表示為流。我們發現將資料庫寫入視作流也是很有用的:我們可以捕獲變更日誌 —— 即對資料庫所做的所有變更的歷史記錄 —— 隱式地透過資料變更捕獲,或顯式地透過事件溯源。日誌壓縮允許流也能保有資料庫內容的完整副本。
將資料庫表示為流為系統整合帶來了很多強大機遇。透過消費變更日誌並將其應用至派生系統,你能使諸如搜尋索引、快取以及分析系統這類派生資料系統不斷保持更新。你甚至能從頭開始,透過讀取從創世至今的所有變更日誌,為現有資料建立全新的檢視。
@@ -714,105 +715,90 @@ AMQP/JMS 風格的訊息代理
最後,我們討論了在流處理中實現容錯和恰好一次語義的技術。與批處理一樣,我們需要放棄任何失敗任務的部分輸出。然而由於流處理長時間執行並持續產生輸出,所以不能簡單地丟棄所有的輸出。相反,可以使用更細粒度的恢復機制,基於微批次、存檔點、事務或冪等寫入。
-## 參考文獻
-1. Tyler Akidau, Robert Bradshaw, Craig Chambers, et al.: “[The Dataflow Model: A Practical Approach to Balancing Correctness, Latency, and Cost in Massive-Scale, Unbounded, Out-of-Order Data Processing](http://www.vldb.org/pvldb/vol8/p1792-Akidau.pdf),” *Proceedings of the VLDB Endowment*, volume 8, number 12, pages 1792–1803, August 2015. [doi:10.14778/2824032.2824076](http://dx.doi.org/10.14778/2824032.2824076)
-1. Harold Abelson, Gerald Jay Sussman, and Julie Sussman: [*Structure and Interpretation of Computer Programs*](https://web.archive.org/web/20220807043536/https://mitpress.mit.edu/sites/default/files/sicp/index.html), 2nd edition. MIT Press, 1996. ISBN: 978-0-262-51087-5, available online at *mitpress.mit.edu*
-1. Patrick Th. Eugster, Pascal A. Felber, Rachid Guerraoui, and Anne-Marie Kermarrec: “[The Many Faces of Publish/Subscribe](http://www.cs.ru.nl/~pieter/oss/manyfaces.pdf),” *ACM Computing Surveys*, volume 35, number 2, pages 114–131, June 2003. [doi:10.1145/857076.857078](http://dx.doi.org/10.1145/857076.857078)
-1. Joseph M. Hellerstein and Michael Stonebraker: [*Readings in Database Systems*](http://redbook.cs.berkeley.edu/), 4th edition. MIT Press, 2005. ISBN: 978-0-262-69314-1, available online at *redbook.cs.berkeley.edu*
-1. Don Carney, Uğur Çetintemel, Mitch Cherniack, et al.: “[Monitoring Streams – A New Class of Data Management Applications](http://www.vldb.org/conf/2002/S07P02.pdf),” at *28th International Conference on Very Large Data Bases* (VLDB), August 2002.
-1. Matthew Sackman: “[Pushing Back](https://wellquite.org/posts/lshift/pushing_back/),” *lshift.net*, May 5, 2016.
-1. Vicent Martí: “[Brubeck, a statsd-Compatible Metrics Aggregator](http://githubengineering.com/brubeck/),” *githubengineering.com*, June 15, 2015.
-1. Seth Lowenberger: “[MoldUDP64 Protocol Specification V 1.00](http://www.nasdaqtrader.com/content/technicalsupport/specifications/dataproducts/moldudp64.pdf),” *nasdaqtrader.com*, July 2009.
-1. Pieter Hintjens: [*ZeroMQ – The Guide*](http://zguide.zeromq.org/page:all). O'Reilly Media, 2013. ISBN: 978-1-449-33404-8
-1. Ian Malpass: “[Measure Anything, Measure Everything](https://codeascraft.com/2011/02/15/measure-anything-measure-everything/),” *codeascraft.com*, February 15, 2011.
-1. Dieter Plaetinck: “[25 Graphite, Grafana and statsd Gotchas](https://grafana.com/blog/2016/03/03/25-graphite-grafana-and-statsd-gotchas/),” *grafana.com*, March 3, 2016.
-1. Jeff Lindsay: “[Web Hooks to Revolutionize the Web](https://web.archive.org/web/20180928201955/http://progrium.com/blog/2007/05/03/web-hooks-to-revolutionize-the-web/),” *progrium.com*, May 3, 2007.
-1. Jim N. Gray: “[Queues Are Databases](https://arxiv.org/pdf/cs/0701158.pdf),” Microsoft Research Technical Report MSR-TR-95-56, December 1995.
-1. Mark Hapner, Rich Burridge, Rahul Sharma, et al.: “[JSR-343 Java Message Service (JMS) 2.0 Specification](https://jcp.org/en/jsr/detail?id=343),” *jms-spec.java.net*, March 2013.
-1. Sanjay Aiyagari, Matthew Arrott, Mark Atwell, et al.: “[AMQP: Advanced Message Queuing Protocol Specification](http://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf),” Version 0-9-1, November 2008.
-1. “[Google Cloud Pub/Sub: A Google-Scale Messaging Service](https://cloud.google.com/pubsub/architecture),” *cloud.google.com*, 2016.
-1. “[Apache Kafka 0.9 Documentation](http://kafka.apache.org/documentation.html),” *kafka.apache.org*, November 2015.
-1. Jay Kreps, Neha Narkhede, and Jun Rao: “[Kafka: A Distributed Messaging System for Log Processing](https://www.microsoft.com/en-us/research/wp-content/uploads/2017/09/Kafka.pdf),” at *6th International Workshop on Networking Meets Databases* (NetDB), June 2011.
-1. “[Amazon Kinesis Streams Developer Guide](http://docs.aws.amazon.com/streams/latest/dev/introduction.html),” *docs.aws.amazon.com*, April 2016.
-1. Leigh Stewart and Sijie Guo: “[Building DistributedLog: Twitter’s High-Performance Replicated Log Service](https://blog.twitter.com/2015/building-distributedlog-twitter-s-high-performance-replicated-log-service),” *blog.twitter.com*, September 16, 2015.
-1. “[DistributedLog Documentation](https://web.archive.org/web/20210517201308/https://bookkeeper.apache.org/distributedlog/docs/latest/),” Apache Software Foundation, *distributedlog.io*.
-1. Jay Kreps: “[Benchmarking Apache Kafka: 2 Million Writes Per Second (On Three Cheap Machines)](https://engineering.linkedin.com/kafka/benchmarking-apache-kafka-2-million-writes-second-three-cheap-machines),” *engineering.linkedin.com*, April 27, 2014.
-1. Kartik Paramasivam: “[How We’re Improving and Advancing Kafka at LinkedIn](https://engineering.linkedin.com/apache-kafka/how-we_re-improving-and-advancing-kafka-linkedin),” *engineering.linkedin.com*, September 2, 2015.
-1. Jay Kreps: “[The Log: What Every Software Engineer Should Know About Real-Time Data's Unifying Abstraction](http://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying),” *engineering.linkedin.com*, December 16, 2013.
-1. Shirshanka Das, Chavdar Botev, Kapil Surlaker, et al.: “[All Aboard the Databus!](http://www.socc2012.org/s18-das.pdf),” at *3rd ACM Symposium on Cloud Computing* (SoCC), October 2012.
-1. Yogeshwer Sharma, Philippe Ajoux, Petchean Ang, et al.: “[Wormhole: Reliable Pub-Sub to Support Geo-Replicated Internet Services](https://www.usenix.org/system/files/conference/nsdi15/nsdi15-paper-sharma.pdf),” at *12th USENIX Symposium on Networked Systems Design and Implementation* (NSDI), May 2015.
-1. P. P. S. Narayan: “[Sherpa Update](http://web.archive.org/web/20160801221400/https://developer.yahoo.com/blogs/ydn/sherpa-7992.html),” *developer.yahoo.com*, June 8, .
-1. Martin Kleppmann: “[Bottled Water: Real-Time Integration of PostgreSQL and Kafka](http://martin.kleppmann.com/2015/04/23/bottled-water-real-time-postgresql-kafka.html),” *martin.kleppmann.com*, April 23, 2015.
-1. Ben Osheroff: “[Introducing Maxwell, a mysql-to-kafka Binlog Processor](https://web.archive.org/web/20170208100334/https://developer.zendesk.com/blog/introducing-maxwell-a-mysql-to-kafka-binlog-processor),” *developer.zendesk.com*, August 20, 2015.
-1. Randall Hauch: “[Debezium 0.2.1 Released](https://debezium.io/blog/2016/06/10/Debezium-0.2.1-Released/),” *debezium.io*, June 10, 2016.
-1. Prem Santosh Udaya Shankar: “[Streaming MySQL Tables in Real-Time to Kafka](https://engineeringblog.yelp.com/2016/08/streaming-mysql-tables-in-real-time-to-kafka.html),” *engineeringblog.yelp.com*, August 1, 2016.
-1. “[Mongoriver](https://github.com/stripe/mongoriver),” Stripe, Inc., *github.com*, September 2014.
-1. Dan Harvey: “[Change Data Capture with Mongo + Kafka](http://www.slideshare.net/danharvey/change-data-capture-with-mongodb-and-kafka),” at *Hadoop Users Group UK*, August 2015.
-1. “[Oracle GoldenGate 12c: Real-Time Access to Real-Time Information](https://web.archive.org/web/20160923105841/http://www.oracle.com/us/products/middleware/data-integration/oracle-goldengate-realtime-access-2031152.pdf),” Oracle White Paper, March 2015.
-1. “[Oracle GoldenGate Fundamentals: How Oracle GoldenGate Works](https://www.youtube.com/watch?v=6H9NibIiPQE),” Oracle Corporation, *youtube.com*, November 2012.
-1. Slava Akhmechet: “[Advancing the Realtime Web](http://rethinkdb.com/blog/realtime-web/),” *rethinkdb.com*, January 27, 2015.
-1. “[Firebase Realtime Database Documentation](https://firebase.google.com/docs/database/),” Google, Inc., *firebase.google.com*, May 2016.
-1. “[Apache CouchDB 1.6 Documentation](http://docs.couchdb.org/en/latest/),” *docs.couchdb.org*, 2014.
-1. Matt DeBergalis: “[Meteor 0.7.0: Scalable Database Queries Using MongoDB Oplog Instead of Poll-and-Diff](https://web.archive.org/web/20160324055429/http://info.meteor.com/blog/meteor-070-scalable-database-queries-using-mongodb-oplog-instead-of-poll-and-diff),” *info.meteor.com*, December 17, 2013.
-1. “[Chapter 15. Importing and Exporting Live Data](https://docs.voltdb.com/UsingVoltDB/ChapExport.php),” VoltDB 6.4 User Manual, *docs.voltdb.com*, June 2016.
-1. Neha Narkhede: “[Announcing Kafka Connect: Building Large-Scale Low-Latency Data Pipelines](http://www.confluent.io/blog/announcing-kafka-connect-building-large-scale-low-latency-data-pipelines),” *confluent.io*, February 18, 2016.
-1. Greg Young: “[CQRS and Event Sourcing](https://www.youtube.com/watch?v=JHGkaShoyNs),” at *Code on the Beach*, August 2014.
-1. Martin Fowler: “[Event Sourcing](http://martinfowler.com/eaaDev/EventSourcing.html),” *martinfowler.com*, December 12, 2005.
-1. Vaughn Vernon: [*Implementing Domain-Driven Design*](https://www.informit.com/store/implementing-domain-driven-design-9780321834577). Addison-Wesley Professional, 2013. ISBN: 978-0-321-83457-7
-1. H. V. Jagadish, Inderpal Singh Mumick, and Abraham Silberschatz: “[View Maintenance Issues for the Chronicle Data Model](https://dl.acm.org/doi/10.1145/212433.220201),” at *14th ACM SIGACT-SIGMOD-SIGART Symposium on Principles of Database Systems* (PODS), May 1995. [doi:10.1145/212433.220201](http://dx.doi.org/10.1145/212433.220201)
-1. “[Event Store 3.5.0 Documentation](http://docs.geteventstore.com/),” Event Store LLP, *docs.geteventstore.com*, February 2016.
-1. Martin Kleppmann: [*Making Sense of Stream Processing*](http://www.oreilly.com/data/free/stream-processing.csp). Report, O'Reilly Media, May 2016.
-1. Sander Mak: “[Event-Sourced Architectures with Akka](http://www.slideshare.net/SanderMak/eventsourced-architectures-with-akka),” at *JavaOne*, September 2014.
-1. Julian Hyde: [personal communication](https://twitter.com/julianhyde/status/743374145006641153), June 2016.
-1. Ashish Gupta and Inderpal Singh Mumick: *Materialized Views: Techniques, Implementations, and Applications*. MIT Press, 1999. ISBN: 978-0-262-57122-7
-1. Timothy Griffin and Leonid Libkin: “[Incremental Maintenance of Views with Duplicates](http://homepages.inf.ed.ac.uk/libkin/papers/sigmod95.pdf),” at *ACM International Conference on Management of Data* (SIGMOD), May 1995. [doi:10.1145/223784.223849](http://dx.doi.org/10.1145/223784.223849)
-1. Pat Helland: “[Immutability Changes Everything](http://cidrdb.org/cidr2015/Papers/CIDR15_Paper16.pdf),” at *7th Biennial Conference on Innovative Data Systems Research* (CIDR), January 2015.
-1. Martin Kleppmann: “[Accounting for Computer Scientists](http://martin.kleppmann.com/2011/03/07/accounting-for-computer-scientists.html),” *martin.kleppmann.com*, March 7, 2011.
-1. Pat Helland: “[Accountants Don't Use Erasers](https://web.archive.org/web/20200220161036/https://blogs.msdn.microsoft.com/pathelland/2007/06/14/accountants-dont-use-erasers/),” *blogs.msdn.com*, June 14, 2007.
-1. Fangjin Yang: “[Dogfooding with Druid, Samza, and Kafka: Metametrics at Metamarkets](https://metamarkets.com/2015/dogfooding-with-druid-samza-and-kafka-metametrics-at-metamarkets/),” *metamarkets.com*, June 3, 2015.
-1. Gavin Li, Jianqiu Lv, and Hang Qi: “[Pistachio: Co-Locate the Data and Compute for Fastest Cloud Compute](https://web.archive.org/web/20181214032620/https://yahoohadoop.tumblr.com/post/116365275781/pistachio-co-locate-the-data-and-compute-for),” *yahoohadoop.tumblr.com*, April 13, 2015.
-1. Kartik Paramasivam: “[Stream Processing Hard Problems – Part 1: Killing Lambda](https://engineering.linkedin.com/blog/2016/06/stream-processing-hard-problems-part-1-killing-lambda),” *engineering.linkedin.com*, June 27, 2016.
-1. Martin Fowler: “[CQRS](http://martinfowler.com/bliki/CQRS.html),” *martinfowler.com*, July 14, 2011.
-1. Greg Young: “[CQRS Documents](https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf),” *cqrs.files.wordpress.com*, November 2010.
-1. Baron Schwartz: “[Immutability, MVCC, and Garbage Collection](https://web.archive.org/web/20161110094746/http://www.xaprb.com/blog/2013/12/28/immutability-mvcc-and-garbage-collection/),” *xaprb.com*, December 28, 2013.
-1. Daniel Eloff, Slava Akhmechet, Jay Kreps, et al.: ["Re: Turning the Database Inside-out with Apache Samza](https://news.ycombinator.com/item?id=9145197)," Hacker News discussion, *news.ycombinator.com*, March 4, 2015.
-1. “[Datomic Development Resources: Excision](http://docs.datomic.com/excision.html),” Cognitect, Inc., *docs.datomic.com*.
-1. “[Fossil Documentation: Deleting Content from Fossil](http://fossil-scm.org/index.html/doc/trunk/www/shunning.wiki),” *fossil-scm.org*, 2016.
-1. Jay Kreps: “[The irony of distributed systems is that data loss is really easy but deleting data is surprisingly hard,](https://twitter.com/jaykreps/status/582580836425330688)” *twitter.com*, March 30, 2015.
-1. David C. Luckham: “[What’s the Difference Between ESP and CEP?](http://www.complexevents.com/2006/08/01/what%E2%80%99s-the-difference-between-esp-and-cep/),” *complexevents.com*, August 1, 2006.
-1. Srinath Perera: “[How Is Stream Processing and Complex Event Processing (CEP) Different?](https://www.quora.com/How-is-stream-processing-and-complex-event-processing-CEP-different),” *quora.com*, December 3, 2015.
-1. Arvind Arasu, Shivnath Babu, and Jennifer Widom: “[The CQL Continuous Query Language: Semantic Foundations and Query Execution](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/cql.pdf),” *The VLDB Journal*, volume 15, number 2, pages 121–142, June 2006. [doi:10.1007/s00778-004-0147-z](http://dx.doi.org/10.1007/s00778-004-0147-z)
-1. Julian Hyde: “[Data in Flight: How Streaming SQL Technology Can Help Solve the Web 2.0 Data Crunch](http://queue.acm.org/detail.cfm?id=1667562),” *ACM Queue*, volume 7, number 11, December 2009. [doi:10.1145/1661785.1667562](http://dx.doi.org/10.1145/1661785.1667562)
-1. “[Esper Reference, Version 5.4.0](http://esper.espertech.com/release-5.4.0/esper-reference/html_single/index.html),” EsperTech, Inc., *espertech.com*, April 2016.
-1. Zubair Nabi, Eric Bouillet, Andrew Bainbridge, and Chris Thomas: “[Of Streams and Storms](https://web.archive.org/web/20170711081434/https://developer.ibm.com/streamsdev/wp-content/uploads/sites/15/2014/04/Streams-and-Storm-April-2014-Final.pdf),” IBM technical report, *developer.ibm.com*, April 2014.
-1. Milinda Pathirage, Julian Hyde, Yi Pan, and Beth Plale: “[SamzaSQL: Scalable Fast Data Management with Streaming SQL](https://github.com/milinda/samzasql-hpbdc2016/blob/master/samzasql-hpbdc2016.pdf),” at *IEEE International Workshop on High-Performance Big Data Computing* (HPBDC), May 2016. [doi:10.1109/IPDPSW.2016.141](http://dx.doi.org/10.1109/IPDPSW.2016.141)
-1. Philippe Flajolet, Éric Fusy, Olivier Gandouet, and Frédéric Meunier: “[HyperLogLog: The Analysis of a Near-Optimal Cardinality Estimation Algorithm](http://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf),” at *Conference on Analysis of Algorithms* (AofA), June 2007.
-1. Jay Kreps: “[Questioning the Lambda Architecture](https://www.oreilly.com/ideas/questioning-the-lambda-architecture),” *oreilly.com*, July 2, 2014.
-1. Ian Hellström: “[An Overview of Apache Streaming Technologies](https://databaseline.bitbucket.io/an-overview-of-apache-streaming-technologies/),” *databaseline.bitbucket.io*, March 12, 2016.
-1. Jay Kreps: “[Why Local State Is a Fundamental Primitive in Stream Processing](https://www.oreilly.com/ideas/why-local-state-is-a-fundamental-primitive-in-stream-processing),” *oreilly.com*, July 31, 2014.
-1. Shay Banon: “[Percolator](https://www.elastic.co/blog/percolator),” *elastic.co*, February 8, 2011.
-1. Alan Woodward and Martin Kleppmann: “[Real-Time Full-Text Search with Luwak and Samza](http://martin.kleppmann.com/2015/04/13/real-time-full-text-search-luwak-samza.html),” *martin.kleppmann.com*, April 13, 2015.
-1. “[Apache Storm 2.1.0 Documentation](https://storm.apache.org/releases/2.1.0/index.html),” *storm.apache.org*, October 2019.
-1. Tyler Akidau: “[The World Beyond Batch: Streaming 102](https://www.oreilly.com/ideas/the-world-beyond-batch-streaming-102),” *oreilly.com*, January 20, 2016.
-1. Stephan Ewen: “[Streaming Analytics with Apache Flink](https://www.confluent.io/resources/kafka-summit-2016/advanced-streaming-analytics-apache-flink-apache-kafka/),” at *Kafka Summit*, April 2016.
-1. Tyler Akidau, Alex Balikov, Kaya Bekiroğlu, et al.: “[MillWheel: Fault-Tolerant Stream Processing at Internet Scale](http://research.google.com/pubs/pub41378.html),” at *39th International Conference on Very Large Data Bases* (VLDB), August 2013.
-1. Alex Dean: “[Improving Snowplow's Understanding of Time](https://snowplow.io/blog/improving-snowplows-understanding-of-time/),” *snowplowanalytics.com*, September 15, 2015.
-1. “[Windowing (Azure Stream Analytics)](https://msdn.microsoft.com/en-us/library/azure/dn835019.aspx),” Microsoft Azure Reference, *msdn.microsoft.com*, April 2016.
-1. “[State Management](http://samza.apache.org/learn/documentation/0.10/container/state-management.html),” Apache Samza 0.10 Documentation, *samza.apache.org*, December 2015.
-1. Rajagopal Ananthanarayanan, Venkatesh Basker, Sumit Das, et al.: “[Photon: Fault-Tolerant and Scalable Joining of Continuous Data Streams](http://research.google.com/pubs/pub41318.html),” at *ACM International Conference on Management of Data* (SIGMOD), June 2013. [doi:10.1145/2463676.2465272](http://dx.doi.org/10.1145/2463676.2465272)
-1. Martin Kleppmann: “[Samza Newsfeed Demo](https://github.com/ept/newsfeed),” *github.com*, September 2014.
-1. Ben Kirwin: “[Doing the Impossible: Exactly-Once Messaging Patterns in Kafka](http://ben.kirw.in/2014/11/28/kafka-patterns/),” *ben.kirw.in*, November 28, 2014.
-1. Pat Helland: “[Data on the Outside Versus Data on the Inside](http://cidrdb.org/cidr2005/papers/P12.pdf),” at *2nd Biennial Conference on Innovative Data Systems Research* (CIDR), January 2005.
-1. Ralph Kimball and Margy Ross: *The Data Warehouse Toolkit: The Definitive Guide to Dimensional Modeling*, 3rd edition. John Wiley & Sons, 2013. ISBN: 978-1-118-53080-1
-1. Viktor Klang: “[I'm coining the phrase 'effectively-once' for message processing with at-least-once + idempotent operations](https://twitter.com/viktorklang/status/789036133434978304),” *twitter.com*, October 20, 2016.
-1. Matei Zaharia, Tathagata Das, Haoyuan Li, et al.: “[Discretized Streams: An Efficient and Fault-Tolerant Model for Stream Processing on Large Clusters](https://www.usenix.org/system/files/conference/hotcloud12/hotcloud12-final28.pdf),” at *4th USENIX Conference in Hot Topics in Cloud Computing* (HotCloud), June 2012.
-1. Kostas Tzoumas, Stephan Ewen, and Robert Metzger: “[High-Throughput, Low-Latency, and Exactly-Once Stream Processing with Apache Flink](https://www.ververica.com/blog/high-throughput-low-latency-and-exactly-once-stream-processing-with-apache-flink),” *ververica.com*, August 5, 2015.
-1. Paris Carbone, Gyula Fóra, Stephan Ewen, et al.: “[Lightweight Asynchronous Snapshots for Distributed Dataflows](http://arxiv.org/abs/1506.08603),” arXiv:1506.08603 [cs.DC], June 29, 2015.
-1. Ryan Betts and John Hugg: [*Fast Data: Smart and at Scale*](http://www.oreilly.com/data/free/fast-data-smart-and-at-scale.csp). Report, O'Reilly Media, October 2015.
-1. Flavio Junqueira: “[Making Sense of Exactly-Once Semantics](https://web.archive.org/web/20160812172900/http://conferences.oreilly.com/strata/hadoop-big-data-eu/public/schedule/detail/49690),” at *Strata+Hadoop World London*, June 2016.
-1. Jason Gustafson, Flavio Junqueira, Apurva Mehta, Sriram Subramanian, and Guozhang Wang: “[KIP-98 – Exactly Once Delivery and Transactional Messaging](https://cwiki.apache.org/confluence/display/KAFKA/KIP-98+-+Exactly+Once+Delivery+and+Transactional+Messaging),” *cwiki.apache.org*, November 2016.
-1. Pat Helland: “[Idempotence Is Not a Medical Condition](https://citeseerx.ist.psu.edu/document?repid=rep1&type=pdf&doi=4b6dda7fe75b51e1c543a87ca7b3b322fbf55614),” *Communications of the ACM*, volume 55, number 5, page 56, May 2012. [doi:10.1145/2160718.2160734](http://dx.doi.org/10.1145/2160718.2160734)
-1. Jay Kreps: “[Re: Trying to Achieve Deterministic Behavior on Recovery/Rewind](http://mail-archives.apache.org/mod_mbox/samza-dev/201409.mbox/%3CCAOeJiJg%2Bc7Ei%3DgzCuOz30DD3G5Hm9yFY%3DUJ6SafdNUFbvRgorg%40mail.gmail.com%3E),” email to *samza-dev* mailing list, September 9, 2014.
-1. E. N. (Mootaz) Elnozahy, Lorenzo Alvisi, Yi-Min Wang, and David B. Johnson: “[A Survey of Rollback-Recovery Protocols in Message-Passing Systems](http://www.cs.utexas.edu/~lorenzo/papers/SurveyFinal.pdf),” *ACM Computing Surveys*, volume 34, number 3, pages 375–408, September 2002. [doi:10.1145/568522.568525](http://dx.doi.org/10.1145/568522.568525)
-1. Adam Warski: “[Kafka Streams – How Does It Fit the Stream Processing Landscape?](https://softwaremill.com/kafka-streams-how-does-it-fit-stream-landscape/),” *softwaremill.com*, June 1, 2016.
+### 參考文獻 {#references}
+
+[^1]: Tyler Akidau, Robert Bradshaw, Craig Chambers, Slava Chernyak, Rafael J. Fernández-Moctezuma, Reuven Lax, Sam McVeety, Daniel Mills, Frances Perry, Eric Schmidt, and Sam Whittle. [The Dataflow Model: A Practical Approach to Balancing Correctness, Latency, and Cost in Massive-Scale, Unbounded, Out-of-Order Data Processing](https://www.vldb.org/pvldb/vol8/p1792-Akidau.pdf). *Proceedings of the VLDB Endowment*, volume 8, issue 12, pages 1792--1803, August 2015. [doi:10.14778/2824032.2824076](https://doi.org/10.14778/2824032.2824076)
+[^2]: Harold Abelson, Gerald Jay Sussman, and Julie Sussman. [*Structure and Interpretation of Computer Programs*](https://web.mit.edu/6.001/6.037/sicp.pdf), 2nd edition. MIT Press, 1996. ISBN: 978-0-262-51087-5, archived at [archive.org/details/sicp_20211010](https://archive.org/details/sicp_20211010)
+[^3]: Patrick Th. Eugster, Pascal A. Felber, Rachid Guerraoui, and Anne-Marie Kermarrec. [The Many Faces of Publish/Subscribe](https://www.cs.ru.nl/~pieter/oss/manyfaces.pdf). *ACM Computing Surveys*, volume 35, issue 2, pages 114--131, June 2003. [doi:10.1145/857076.857078](https://doi.org/10.1145/857076.857078)
+[^4]: Don Carney, Uğur Çetintemel, Mitch Cherniack, Christian Convey, Sangdon Lee, Greg Seidman, Michael Stonebraker, Nesime Tatbul, and Stan Zdonik. [Monitoring Streams -- A New Class of Data Management Applications](https://www.vldb.org/conf/2002/S07P02.pdf). At *28th International Conference on Very Large Data Bases* (VLDB), August 2002. [doi:10.1016/B978-155860869-6/50027-5](https://doi.org/10.1016/B978-155860869-6/50027-5)
+[^5]: Matthew Sackman. [Pushing Back](https://wellquite.org/posts/lshift/pushing_back/). *wellquite.org*, May 2016. Archived at [perma.cc/3KCZ-RUFY](https://perma.cc/3KCZ-RUFY)
+[^6]: Thomas Figg (tef). [how (not) to write a pipeline](https://web.archive.org/web/20250107135013/https://cohost.org/tef/post/1764930-how-not-to-write-a). *cohost.org*, June 2023. Archived at [perma.cc/A3V8-NYCM](https://perma.cc/A3V8-NYCM)
+[^7]: Vicent Martí. [Brubeck, a statsd-Compatible Metrics Aggregator](https://github.blog/news-insights/the-library/brubeck/). *github.blog*, June 2015. Archived at [perma.cc/TP3Q-DJYM](https://perma.cc/TP3Q-DJYM)
+[^8]: Seth Lowenberger. [MoldUDP64 Protocol Specification V 1.00](https://www.nasdaqtrader.com/content/technicalsupport/specifications/dataproducts/moldudp64.pdf). *nasdaqtrader.com*, July 2009. Archived at
+[^9]: Ian Malpass. [Measure Anything, Measure Everything](https://codeascraft.com/2011/02/15/measure-anything-measure-everything/). *codeascraft.com*, February 2011. Archived at [archive.org](https://web.archive.org/web/20250820034209/https://www.etsy.com/codeascraft/measure-anything-measure-everything/)
+[^10]: Dieter Plaetinck. [25 Graphite, Grafana and statsd Gotchas](https://grafana.com/blog/2016/03/03/25-graphite-grafana-and-statsd-gotchas/). *grafana.com*, March 2016. Archived at [perma.cc/3NP3-67U7](https://perma.cc/3NP3-67U7)
+[^11]: Jeff Lindsay. [Web Hooks to Revolutionize the Web](https://progrium.github.io/blog/2007/05/03/web-hooks-to-revolutionize-the-web/). *progrium.com*, May 2007. Archived at [perma.cc/BF9U-XNX4](https://perma.cc/BF9U-XNX4)
+[^12]: Jim N. Gray. [Queues Are Databases](https://arxiv.org/pdf/cs/0701158.pdf). Microsoft Research Technical Report MSR-TR-95-56, December 1995. Archived at [arxiv.org](https://arxiv.org/pdf/cs/0701158)
+[^13]: Mark Hapner, Rich Burridge, Rahul Sharma, Joseph Fialli, Kate Stout, and Nigel Deakin. [JSR-343 Java Message Service (JMS) 2.0 Specification](https://jcp.org/en/jsr/detail?id=343). *jms-spec.java.net*, March 2013. Archived at [perma.cc/E4YG-46TA](https://perma.cc/E4YG-46TA)
+[^14]: Sanjay Aiyagari, Matthew Arrott, Mark Atwell, Jason Brome, Alan Conway, Robert Godfrey, Robert Greig, Pieter Hintjens, John O'Hara, Matthias Radestock, Alexis Richardson, Martin Ritchie, Shahrokh Sadjadi, Rafael Schloming, Steven Shaw, Martin Sustrik, Carl Trieloff, Kim van der Riet, and Steve Vinoski. [AMQP: Advanced Message Queuing Protocol Specification](https://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf). Version 0-9-1, November 2008. Archived at [perma.cc/6YJJ-GM9X](https://perma.cc/6YJJ-GM9X)
+[^15]: [Architectural overview of Pub/Sub](https://cloud.google.com/pubsub/architecture). *cloud.google.com*, 2025. Archived at [perma.cc/VWF5-ABP4](https://perma.cc/VWF5-ABP4)
+[^16]: Aris Tzoumas. [Lessons from scaling PostgreSQL queues to 100k events per second](https://www.rudderstack.com/blog/scaling-postgres-queue/). *rudderstack.com*, July 2025. Archived at [perma.cc/QD8C-VA4Y](https://perma.cc/QD8C-VA4Y)
+[^17]: Robin Moffatt. [Kafka Connect Deep Dive -- Error Handling and Dead Letter Queues](https://www.confluent.io/blog/kafka-connect-deep-dive-error-handling-dead-letter-queues/). *confluent.io*, March 2019. Archived at [perma.cc/KQ5A-AB28](https://perma.cc/KQ5A-AB28)
+[^18]: Dunith Danushka. [Message reprocessing: How to implement the dead letter queue](https://redpanda.com/blog/reliable-message-processing-with-dead-letter-queue). *redpanda.com*. Archived at [perma.cc/R7UB-WEWF](https://perma.cc/R7UB-WEWF)
+[^19]: Damien Gasparina, Loic Greffier, and Sebastien Viale. [KIP-1034: Dead letter queue in Kafka Streams](https://cwiki.apache.org/confluence/display/KAFKA/KIP-1034%3A+Dead+letter+queue+in+Kafka+Streams). *cwiki.apache.org*, April 2024. Archived at [perma.cc/3VXV-QXAN](https://perma.cc/3VXV-QXAN)
+[^20]: Jay Kreps, Neha Narkhede, and Jun Rao. [Kafka: A Distributed Messaging System for Log Processing](https://www.microsoft.com/en-us/research/wp-content/uploads/2017/09/Kafka.pdf). At *6th International Workshop on Networking Meets Databases* (NetDB), June 2011. Archived at [perma.cc/CSW7-TCQ5](https://perma.cc/CSW7-TCQ5)
+[^21]: Jay Kreps. [Benchmarking Apache Kafka: 2 Million Writes Per Second (On Three Cheap Machines)](https://engineering.linkedin.com/kafka/benchmarking-apache-kafka-2-million-writes-second-three-cheap-machines). *engineering.linkedin.com*, April 2014. Archived at [archive.org](https://web.archive.org/web/20140921000742/https://engineering.linkedin.com/kafka/benchmarking-apache-kafka-2-million-writes-second-three-cheap-machines)
+[^22]: Kartik Paramasivam. [How We're Improving and Advancing Kafka at LinkedIn](https://engineering.linkedin.com/apache-kafka/how-we_re-improving-and-advancing-kafka-linkedin). *engineering.linkedin.com*, September 2015. Archived at [perma.cc/3S3V-JCYJ](https://perma.cc/3S3V-JCYJ)
+[^23]: Philippe Dobbelaere and Kyumars Sheykh Esmaili. [Kafka versus RabbitMQ: A comparative study of two industry reference publish/subscribe implementations](https://arxiv.org/abs/1709.00333). At *11th ACM International Conference on Distributed and Event-based Systems* (DEBS), June 2017. [doi:10.1145/3093742.3093908](https://doi.org/10.1145/3093742.3093908)
+[^24]: Kate Holterhoff. [Why Message Queues Endure: A History](https://redmonk.com/kholterhoff/2024/12/12/why-message-queues-endure-a-history/). *redmonk.com*, December 2024. Archived at [perma.cc/6DX8-XK4W](https://perma.cc/6DX8-XK4W)
+[^25]: Andrew Schofield. [KIP-932: Queues for Kafka](https://cwiki.apache.org/confluence/display/KAFKA/KIP-932%3A+Queues+for+Kafka). *cwiki.apache.org*, May 2023. Archived at [perma.cc/LBE4-BEMK](https://perma.cc/LBE4-BEMK)
+[^26]: Jack Vanlightly. [The advantages of queues on logs](https://jack-vanlightly.com/blog/2023/10/2/the-advantages-of-queues-on-logs). *jack-vanlightly.com*, October 2023. Archived at [perma.cc/WJ7V-287K](https://perma.cc/WJ7V-287K)
+[^27]: Jay Kreps. [The Log: What Every Software Engineer Should Know About Real-Time Data's Unifying Abstraction](https://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying). *engineering.linkedin.com*, December 2013. Archived at [perma.cc/2JHR-FR64](https://perma.cc/2JHR-FR64)
+[^28]: Andy Hattemer. [Change Data Capture is having a moment. Why?](https://materialize.com/blog/change-data-capture-is-having-a-moment-why/) *materialize.com*, September 2021. Archived at [perma.cc/AL37-P53C](https://perma.cc/AL37-P53C)
+[^29]: Prem Santosh Udaya Shankar. [Streaming MySQL Tables in Real-Time to Kafka](https://engineeringblog.yelp.com/2016/08/streaming-mysql-tables-in-real-time-to-kafka.html). *engineeringblog.yelp.com*, August 2016. Archived at [perma.cc/5ZR3-2GVV](https://perma.cc/5ZR3-2GVV)
+[^30]: Andreas Andreakis, Ioannis Papapanagiotou. [DBLog: A Watermark Based Change-Data-Capture Framework](https://arxiv.org/pdf/2010.12597). October 2020. Archived at [arxiv.org](https://arxiv.org/pdf/2010.12597)
+[^31]: Jiri Pechanec. [Percolator](https://debezium.io/blog/2021/10/07/incremental-snapshots/). *debezium.io*, October 2021. Archived at [perma.cc/EQ8E-W6KQ](https://perma.cc/EQ8E-W6KQ)
+[^32]: Debezium maintainers. [Debezium Connector for Cassandra](https://debezium.io/documentation/reference/stable/connectors/cassandra.html). *debezium.io*. Archived at [perma.cc/WR6K-EKMD](https://perma.cc/WR6K-EKMD)
+[^33]: Neha Narkhede. [Announcing Kafka Connect: Building Large-Scale Low-Latency Data Pipelines](https://www.confluent.io/blog/announcing-kafka-connect-building-large-scale-low-latency-data-pipelines/). *confluent.io*, February 2016. Archived at [perma.cc/8WXJ-L6GF](https://perma.cc/8WXJ-L6GF)
+[^34]: Chris Riccomini. [Kafka change data capture breaks database encapsulation](https://cnr.sh/posts/2018-11-05-kafka-change-data-capture-breaks-database-encapsulation/). *cnr.sh*, November 2018. Archived at [perma.cc/P572-9MKF](https://perma.cc/P572-9MKF)
+[^35]: Gunnar Morling. ["Change Data Capture Breaks Encapsulation". Does it, though?](https://www.decodable.co/blog/change-data-capture-breaks-encapsulation-does-it-though) *decodable.co*, November 2023. Archived at [perma.cc/YX2P-WNWR](https://perma.cc/YX2P-WNWR)
+[^36]: Gunnar Morling. [Revisiting the Outbox Pattern](https://www.decodable.co/blog/revisiting-the-outbox-pattern). *decodable.co*, October 2024. Archived at [perma.cc/M5ZL-RPS9](https://perma.cc/M5ZL-RPS9)
+[^37]: Ashish Gupta and Inderpal Singh Mumick. [Maintenance of Materialized Views: Problems, Techniques, and Applications](https://web.archive.org/web/20220407025818id_/http://sites.computer.org/debull/95JUN-CD.pdf#page=5). *IEEE Data Engineering Bulletin*, volume 18, issue 2, pages 3--18, June 1995. Archived at [archive.org](https://web.archive.org/web/20220407025818id_/http://sites.computer.org/debull/95JUN-CD.pdf#page=5)
+[^38]: Mihai Budiu, Tej Chajed, Frank McSherry, Leonid Ryzhyk, Val Tannen. [DBSP: Incremental Computation on Streams and Its Applications to Databases](https://sigmodrecord.org/publications/sigmodRecord/2403/pdfs/20_dbsp-budiu.pdf). *SIGMOD Record*, volume 53, issue 1, pages 87--95, March 2024. [doi:10.1145/3665252.3665271](https://doi.org/10.1145/3665252.3665271)
+[^39]: Jim Gray and Andreas Reuter. [*Transaction Processing: Concepts and Techniques*](https://learning.oreilly.com/library/view/transaction-processing/9780080519555/). Morgan Kaufmann, 1992. ISBN: 9781558601901
+[^40]: Martin Kleppmann. [Accounting for Computer Scientists](https://martin.kleppmann.com/2011/03/07/accounting-for-computer-scientists.html). *martin.kleppmann.com*, March 2011. Archived at [perma.cc/9EGX-P38N](https://perma.cc/9EGX-P38N)
+[^41]: Pat Helland. [Immutability Changes Everything](https://www.cidrdb.org/cidr2015/Papers/CIDR15_Paper16.pdf). At *7th Biennial Conference on Innovative Data Systems Research* (CIDR), January 2015.
+[^42]: Martin Kleppmann. [*Making Sense of Stream Processing*](https://martin.kleppmann.com/papers/stream-processing.pdf). Report, O'Reilly Media, May 2016. Archived at [perma.cc/RAY4-JDVX](https://perma.cc/RAY4-JDVX)
+[^43]: Kartik Paramasivam. [Stream Processing Hard Problems -- Part 1: Killing Lambda](https://engineering.linkedin.com/blog/2016/06/stream-processing-hard-problems-part-1-killing-lambda). *engineering.linkedin.com*, June 2016. Archived at [archive.org](https://web.archive.org/web/20240621211312/https://www.linkedin.com/blog/engineering/data-streaming-processing/stream-processing-hard-problems-part-1-killing-lambda)
+[^44]: Stéphane Derosiaux. [CQRS: What? Why? How?](https://sderosiaux.medium.com/cqrs-what-why-how-945543482313) *sderosiaux.medium.com*, September 2019. Archived at [perma.cc/FZ3U-HVJ4](https://perma.cc/FZ3U-HVJ4)
+[^45]: Baron Schwartz. [Immutability, MVCC, and Garbage Collection](https://web.archive.org/web/20220122020806/http://www.xaprb.com/blog/2013/12/28/immutability-mvcc-and-garbage-collection/). *xaprb.com*, December 2013. Archived at [archive.org](https://web.archive.org/web/20220122020806/http://www.xaprb.com/blog/2013/12/28/immutability-mvcc-and-garbage-collection/)
+[^46]: Daniel Eloff, Slava Akhmechet, Jay Kreps, et al. [Re: Turning the Database Inside-out with Apache Samza](https://news.ycombinator.com/item?id=9145197). Hacker News discussion, *news.ycombinator.com*, March 2015. Archived at [perma.cc/ML9E-JC83](https://perma.cc/ML9E-JC83)
+[^47]: [Datomic Documentation: Excision](https://docs.datomic.com/operation/excision.html). Cognitect, Inc., *docs.datomic.com*. Archived at [perma.cc/J5QQ-SH32](https://perma.cc/J5QQ-SH32)
+[^48]: [Fossil Documentation: Deleting Content from Fossil](https://fossil-scm.org/home/doc/trunk/www/shunning.wiki). *fossil-scm.org*, 2025. Archived at [perma.cc/DS23-GTNG](https://perma.cc/DS23-GTNG)
+[^49]: Jay Kreps. [The irony of distributed systems is that data loss is really easy but deleting data is surprisingly hard.](https://x.com/jaykreps/status/582580836425330688) *x.com*, March 2015. Archived at [perma.cc/7RRZ-V7B7](https://perma.cc/7RRZ-V7B7)
+[^50]: Brent Robinson. [Crypto shredding: How it can solve modern data retention challenges](https://medium.com/@brentrobinson5/crypto-shredding-how-it-can-solve-modern-data-retention-challenges-da874b01745b). *medium.com*, January 2019. Archived at
+[^51]: Matthew D. Green and Ian Miers. [Forward Secure Asynchronous Messaging from Puncturable Encryption](https://isi.jhu.edu/~mgreen/forward_sec.pdf). At *IEEE Symposium on Security and Privacy*, May 2015. [doi:10.1109/SP.2015.26](https://doi.org/10.1109/SP.2015.26)
+[^52]: David C. Luckham. [What's the Difference Between ESP and CEP?](https://complexevents.com/2020/06/15/whats-the-difference-between-esp-and-cep-2/) *complexevents.com*, June 2019. Archived at [perma.cc/E7PZ-FDEF](https://perma.cc/E7PZ-FDEF)
+[^53]: Arvind Arasu, Shivnath Babu, and Jennifer Widom. [The CQL Continuous Query Language: Semantic Foundations and Query Execution](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/cql.pdf). *The VLDB Journal*, volume 15, issue 2, pages 121--142, June 2006. [doi:10.1007/s00778-004-0147-z](https://doi.org/10.1007/s00778-004-0147-z)
+[^54]: Julian Hyde. [Data in Flight: How Streaming SQL Technology Can Help Solve the Web 2.0 Data Crunch](https://queue.acm.org/detail.cfm?id=1667562). *ACM Queue*, volume 7, issue 11, December 2009. [doi:10.1145/1661785.1667562](https://doi.org/10.1145/1661785.1667562)
+[^55]: Philippe Flajolet, Éric Fusy, Olivier Gandouet, and Frédéric Meunier. [HyperLogLog: The Analysis of a Near-Optimal Cardinality Estimation Algorithm](https://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf). At *Conference on Analysis of Algorithms* (AofA), June 2007. [doi:10.46298/dmtcs.3545](https://doi.org/10.46298/dmtcs.3545)
+[^56]: Jay Kreps. [Questioning the Lambda Architecture](https://www.oreilly.com/ideas/questioning-the-lambda-architecture). *oreilly.com*, July 2014. Archived at [perma.cc/2WY5-HC8Y](https://perma.cc/2WY5-HC8Y)
+[^57]: Ian Reppel. [An Overview of Apache Streaming Technologies](https://ianreppel.org/an-overview-of-apache-streaming-technologies/). *ianreppel.org*, March 2016. Archived at [perma.cc/BB3E-QJLW](https://perma.cc/BB3E-QJLW)
+[^58]: Jay Kreps. [Why Local State is a Fundamental Primitive in Stream Processing](https://www.oreilly.com/ideas/why-local-state-is-a-fundamental-primitive-in-stream-processing). *oreilly.com*, July 2014. Archived at [perma.cc/P8HU-R5LA](https://perma.cc/P8HU-R5LA)
+[^59]: RisingWave Labs. [Deep Dive Into the RisingWave Stream Processing Engine - Part 2: Computational Model](https://risingwave.com/blog/deep-dive-into-the-risingwave-stream-processing-engine-part-2-computational-model/). *risingwave.com*, November 2023. Archived at [perma.cc/LM74-XDEL](https://perma.cc/LM74-XDEL)
+[^60]: Frank McSherry, Derek G. Murray, Rebecca Isaacs, and Michael Isard. [Differential dataflow](https://www.cidrdb.org/cidr2013/Papers/CIDR13_Paper111.pdf). At *6th Biennial Conference on Innovative Data Systems Research* (CIDR), January 2013.
+[^61]: Andy Hattemer. [Incremental Computation in the Database](https://materialize.com/guides/incremental-computation/). *materialize.com*, March 2020. Archived at [perma.cc/AL94-YVRN](https://perma.cc/AL94-YVRN)
+[^62]: Shay Banon. [Percolator](https://www.elastic.co/blog/percolator). *elastic.co*, February 2011. Archived at [perma.cc/LS5R-4FQX](https://perma.cc/LS5R-4FQX)
+[^63]: Alan Woodward and Martin Kleppmann. [Real-Time Full-Text Search with Luwak and Samza](https://martin.kleppmann.com/2015/04/13/real-time-full-text-search-luwak-samza.html). *martin.kleppmann.com*, April 2015. Archived at [perma.cc/2U92-Q7R4](https://perma.cc/2U92-Q7R4)
+[^64]: Tyler Akidau. [The World Beyond Batch: Streaming 102](https://www.oreilly.com/ideas/the-world-beyond-batch-streaming-102). *oreilly.com*, January 2016. Archived at [perma.cc/4XF9-8M2K](https://perma.cc/4XF9-8M2K)
+[^65]: Stephan Ewen. [Streaming Analytics with Apache Flink](https://www.slideshare.net/slideshow/advanced-streaming-analytics-with-apache-flink-and-apache-kafka-stephan-ewen/61920008). At *Kafka Summit*, April 2016. Archived at [perma.cc/QBQ4-F9MR](https://perma.cc/QBQ4-F9MR)
+[^66]: Tyler Akidau, Alex Balikov, Kaya Bekiroğlu, Slava Chernyak, Josh Haberman, Reuven Lax, Sam McVeety, Daniel Mills, Paul Nordstrom, and Sam Whittle. [MillWheel: Fault-Tolerant Stream Processing at Internet Scale](https://www.vldb.org/pvldb/vol6/p1033-akidau.pdf). *Proceedings of the VLDB Endowment*, volume 6, issue 11, pages 1033--1044, August 2013. [doi:10.14778/2536222.2536229](https://doi.org/10.14778/2536222.2536229)
+[^67]: Alex Dean. [Improving Snowplow's Understanding of Time](https://snowplow.io/blog/improving-snowplows-understanding-of-time). *snowplow.io*, September 2015. Archived at [perma.cc/6CT9-Z3Q2](https://perma.cc/6CT9-Z3Q2)
+[^68]: [Azure Stream Analytics: Windowing functions](https://learn.microsoft.com/en-gb/stream-analytics-query/windowing-azure-stream-analytics). Microsoft Azure Reference, *learn.microsoft.com*, July 2025. Archived at [archive.org](https://web.archive.org/web/20250901140013/https://learn.microsoft.com/en-gb/stream-analytics-query/windowing-azure-stream-analytics)
+[^69]: Rajagopal Ananthanarayanan, Venkatesh Basker, Sumit Das, Ashish Gupta, Haifeng Jiang, Tianhao Qiu, Alexey Reznichenko, Deomid Ryabkov, Manpreet Singh, and Shivakumar Venkataraman. [Photon: Fault-Tolerant and Scalable Joining of Continuous Data Streams](https://research.google.com/pubs/archive/41529.pdf). At *ACM International Conference on Management of Data* (SIGMOD), June 2013. [doi:10.1145/2463676.2465272](https://doi.org/10.1145/2463676.2465272)
+[^70]: Ben Kirwin. [Doing the Impossible: Exactly-Once Messaging Patterns in Kafka](https://ben.kirw.in/2014/11/28/kafka-patterns/). *ben.kirw.in*, November 2014. Archived at [perma.cc/A5QL-QRX7](https://perma.cc/A5QL-QRX7)
+[^71]: Pat Helland. [Data on the Outside Versus Data on the Inside](https://www.cidrdb.org/cidr2005/papers/P12.pdf). At *2nd Biennial Conference on Innovative Data Systems Research* (CIDR), January 2005.
+[^72]: Ralph Kimball and Margy Ross. [*The Data Warehouse Toolkit: The Definitive Guide to Dimensional Modeling*](https://learning.oreilly.com/library/view/the-data-warehouse/9781118530801/), 3rd edition. John Wiley & Sons, 2013. ISBN: 978-1-118-53080-1
+[^73]: Viktor Klang. [I'm coining the phrase 'effectively-once' for message processing with at-least-once + idempotent operations](https://x.com/viktorklang/status/789036133434978304). *x.com*, October 2016. Archived at [perma.cc/7DT9-TDG2](https://perma.cc/7DT9-TDG2)
+[^74]: Matei Zaharia, Tathagata Das, Haoyuan Li, Scott Shenker, and Ion Stoica. [Discretized Streams: An Efficient and Fault-Tolerant Model for Stream Processing on Large Clusters](https://www.usenix.org/system/files/conference/hotcloud12/hotcloud12-final28.pdf). At *4th USENIX Conference in Hot Topics in Cloud Computing* (HotCloud), June 2012.
+[^75]: Kostas Tzoumas, Stephan Ewen, and Robert Metzger. [High-Throughput, Low-Latency, and Exactly-Once Stream Processing with Apache Flink](https://web.archive.org/web/20250429165534/https://www.ververica.com/blog/high-throughput-low-latency-and-exactly-once-stream-processing-with-apache-flink). *ververica.com*, August 2015. Archived at [archive.org](https://web.archive.org/web/20250429165534/https://www.ververica.com/blog/high-throughput-low-latency-and-exactly-once-stream-processing-with-apache-flink)
+[^76]: Paris Carbone, Gyula Fóra, Stephan Ewen, Seif Haridi, and Kostas Tzoumas. [Lightweight Asynchronous Snapshots for Distributed Dataflows](https://arxiv.org/abs/1506.08603). arXiv:1506.08603 [cs.DC], June 2015.
+[^77]: Ryan Betts and John Hugg. [*Fast Data: Smart and at Scale*](https://www.voltactivedata.com/wp-content/uploads/2017/03/hv-ebook-fast-data-smart-and-at-scale.pdf). Report, O'Reilly Media, October 2015. Archived at [perma.cc/VQ6S-XQQY](https://perma.cc/VQ6S-XQQY)
+[^78]: Neha Narkhede and Guozhang Wang. [Exactly-Once Semantics Are Possible: Here's How Kafka Does It](https://www.confluent.io/blog/exactly-once-semantics-are-possible-heres-how-apache-kafka-does-it/). *confluent.io*, June 2019. Archived at [perma.cc/Q2AU-Q2ED](https://perma.cc/Q2AU-Q2ED)
+[^79]: Jason Gustafson, Flavio Junqueira, Apurva Mehta, Sriram Subramanian, and Guozhang Wang. [KIP-98 -- Exactly Once Delivery and Transactional Messaging](https://cwiki.apache.org/confluence/display/KAFKA/KIP-98+-+Exactly+Once+Delivery+and+Transactional+Messaging). *cwiki.apache.org*, November 2016. Archived at [perma.cc/95PT-RCTG](https://perma.cc/95PT-RCTG)
+[^80]: Pat Helland. [Idempotence Is Not a Medical Condition](https://dl.acm.org/doi/pdf/10.1145/2160718.2160734). *Communications of the ACM*, volume 55, issue 5, page 56, May 2012. [doi:10.1145/2160718.2160734](https://doi.org/10.1145/2160718.2160734)
+[^81]: Jay Kreps. [Re: Trying to Achieve Deterministic Behavior on Recovery/Rewind](https://lists.apache.org/thread/n0sz6zld72nvjtnytv09pxc57mdcf9ft). Email to *samza-dev* mailing list, September 2014. Archived at [perma.cc/7DPD-GJNL](https://perma.cc/7DPD-GJNL)
+[^82]: E. N. (Mootaz) Elnozahy, Lorenzo Alvisi, Yi-Min Wang, and David B. Johnson. [A Survey of Rollback-Recovery Protocols in Message-Passing Systems](https://www.cs.utexas.edu/~lorenzo/papers/SurveyFinal.pdf). *ACM Computing Surveys*, volume 34, issue 3, pages 375--408, September 2002. [doi:10.1145/568522.568525](https://doi.org/10.1145/568522.568525)
+[^83]: Adam Warski. [Kafka Streams -- How Does It Fit the Stream Processing Landscape?](https://softwaremill.com/kafka-streams-how-does-it-fit-stream-landscape/) *softwaremill.com*, June 2016. Archived at [perma.cc/WQ5Q-H2J2](https://perma.cc/WQ5Q-H2J2)
+[^84]: Stephan Ewen, Fabian Hueske, and Xiaowei Jiang. [Batch as a Special Case of Streaming and Alibaba's contribution of Blink](https://flink.apache.org/2019/02/13/batch-as-a-special-case-of-streaming-and-alibabas-contribution-of-blink/). *flink.apache.org*, February 2019. Archived at [perma.cc/A529-SKA9](https://perma.cc/A529-SKA9)
\ No newline at end of file
diff --git a/content/tw/ch13.md b/content/tw/ch13.md
index c43a128..08f191d 100644
--- a/content/tw/ch13.md
+++ b/content/tw/ch13.md
@@ -1,14 +1,12 @@
---
-title: "第十三章:資料系統的未來"
-linkTitle: "13. 資料系統的未來"
+title: "第十三章:流式系統的哲學"
+linkTitle: "13. 流式系統的哲學"
weight: 313
breadcrumbs: false
---
-
-{{< callout type="warning" >}}
-當前頁面來自本書第一版,第二版尚不可用
-{{< /callout >}}
+
+

@@ -16,15 +14,13 @@ breadcrumbs: false
>
> —— 聖托馬斯・阿奎那《神學大全》(1265-1274)
-到目前為止,本書主要描述的是 **現狀**。在這最後一章中,我們將放眼 **未來**,討論應該是怎麼樣的:我將提出一些想法與方法,我相信它們能從根本上改進我們設計與構建應用的方式。
+[第二章](/tw/ch2) 討論了構建 **可靠**、**可伸縮**、**可維護** 應用與系統的目標。這些主題貫穿了全書:例如,我們討論了提升可靠性的多種容錯演算法、提升可伸縮性的分割槽方法,以及提升可維護性的演化與抽象機制。
-對未來的看法與推測當然具有很大的主觀性。所以在撰寫本章時,當提及我個人的觀點時會使用第一人稱。你完全可以不同意這些觀點並提出自己的看法,但我希望本章中的概念,至少能成為富有成效的討論出發點,並澄清一些經常被混淆的概念。
+在本章中,我們將把這些想法整合起來,並特別基於 [第十二章](/tw/ch12) 的流式/事件驅動架構思路,提出一套滿足這些目標的應用開發哲學。與前幾章相比,本章立場更鮮明:不是並列比較多種方案,而是深入展開一種特定的設計哲學。
-[第一章](/tw/ch1) 概述了本書的目標:探索如何建立 **可靠**、**可伸縮** 和 **可維護** 的應用與系統。這一主題貫穿了所有的章節:例如,我們討論了許多有助於提高可靠性的容錯演算法,有助於提高可伸縮性的分割槽,以及有助於提高可維護性的演化與抽象機制。在本章中,我們將把所有這些想法結合在一起,並在它們的基礎上展望未來。我們的目標是,發現如何設計出比現有應用更好的應用 —— 健壯、正確、可演化、且最終對人類有益。
+## 資料整合 {#sec_future_integration}
-## 資料整合
-
-本書中反覆出現的主題是,對於任何給定的問題都會有好幾種解決方案,所有這些解決方案都有不同的優缺點與利弊權衡。例如在 [第三章](/tw/ch3) 討論儲存引擎時,我們看到了日誌結構儲存、B 樹以及列式儲存。在 [第五章](/tw/ch5) 討論複製時,我們看到了單領導者、多領導者和無領導者的方法。
+本書中反覆出現的主題是,對於任何給定的問題都會有好幾種解決方案,所有這些解決方案都有不同的優缺點與利弊權衡。例如在 [第四章](/tw/ch4) 討論儲存引擎時,我們看到了日誌結構儲存、B 樹以及列式儲存。在 [第六章](/tw/ch6) 討論複製時,我們看到了單領導者、多領導者和無領導者的方法。
如果你有一個類似於 “我想儲存一些資料並稍後再查詢” 的問題,那麼並沒有一種正確的解決方案。但對於不同的具體環境,總會有不同的合適方法。軟體實現通常必須選擇一種特定的方法。使單條程式碼路徑能做到穩定健壯且表現良好已經是一件非常困難的事情了 —— 嘗試在單個軟體中完成所有事情,幾乎可以保證,實現效果會很差。
@@ -34,133 +30,115 @@ breadcrumbs: false
但是,即使你已經完全理解各種工具與其適用環境間的關係,還有一個挑戰:在複雜的應用中,資料的用法通常花樣百出。不太可能存在適用於 **所有** 不同資料應用場景的軟體,因此你不可避免地需要拼湊幾個不同的軟體來以提供應用所需的功能。
-### 組合使用派生資料的工具
+### 組合使用派生資料的工具 {#id442}
-例如,為了處理任意關鍵詞的搜尋查詢,將 OLTP 資料庫與全文檢索索引整合在一起是很常見的需求。儘管一些資料庫(例如 PostgreSQL)包含了全文索引功能,對於簡單的應用完全夠了【1】,但更複雜的搜尋能力就需要專業的資訊檢索工具了。相反的是,搜尋索引通常不適合作為持久的記錄系統,因此許多應用需要組合這兩種不同的工具以滿足所有需求。
+例如,為了處理任意關鍵詞的搜尋查詢,將 OLTP 資料庫與全文檢索索引整合在一起是很常見的需求。儘管一些資料庫(例如 PostgreSQL)包含了全文索引功能,對於簡單的應用完全夠了[^1],但更複雜的搜尋能力就需要專業的資訊檢索工具了。相反的是,搜尋索引通常不適合作為持久的記錄系統,因此許多應用需要組合這兩種不同的工具以滿足所有需求。
-我們在 “[保持系統同步](/tw/ch12#保持系統同步)” 中接觸過整合資料系統的問題。隨著資料不同表示形式的增加,整合問題變得越來越困難。除了資料庫和搜尋索引之外,也許你需要在分析系統(資料倉庫,或批處理和流處理系統)中維護資料副本;維護從原始資料中派生的快取,或反正規化的資料版本;將資料灌入機器學習、分類、排名或推薦系統中;或者基於資料變更傳送通知。
+我們在 “[保持系統同步](/tw/ch12#sec_stream_sync)” 中接觸過整合資料系統的問題。隨著資料不同表示形式的增加,整合問題變得越來越困難。除了資料庫和搜尋索引之外,也許你需要在分析系統(資料倉庫,或批處理和流處理系統)中維護資料副本;維護從原始資料中派生的快取,或反正規化的資料版本;將資料灌入機器學習、分類、排名或推薦系統中;或者基於資料變更傳送通知。
-令人驚訝的是,我經常看到軟體工程師做出這樣的陳述:“根據我的經驗,99% 的人只需要 X” 或者 “...... 不需要 X”(對於各種各樣的 X)。我認為這種陳述更像是發言人自己的經驗,而不是技術實際上的實用性。可能對資料執行的操作,其範圍極其寬廣。某人認為雞肋而毫無意義的功能可能是別人的核心需求。當你拉高視角,並考慮跨越整個組織範圍的資料流時,資料整合的需求往往就會變得明顯起來。
-
-#### 理解資料流
+#### 理解資料流 {#id443}
當需要在多個儲存系統中維護相同資料的副本以滿足不同的訪問模式時,你要對輸入和輸出瞭如指掌:哪些資料先寫入,哪些資料表示派生自哪些來源?如何以正確的格式,將所有資料匯入正確的地方?
-例如,你可能會首先將資料寫入 **記錄系統** 資料庫,捕獲對該資料庫所做的變更(請參閱 “[變更資料捕獲](/tw/ch12#變更資料捕獲)”),然後將變更以相同的順序應用於搜尋索引。如果變更資料捕獲(CDC)是更新索引的唯一方式,則可以確定該索引完全派生自記錄系統,因此與其保持一致(除軟體錯誤外)。寫入資料庫是向該系統提供新輸入的唯一方式。
+例如,你可能會首先將資料寫入 **記錄系統** 資料庫,捕獲對該資料庫所做的變更(請參閱 “[變更資料捕獲](/tw/ch12#sec_stream_cdc)”),然後將變更以相同的順序應用於搜尋索引。如果變更資料捕獲(CDC)是更新索引的唯一方式,則可以確定該索引完全派生自記錄系統,因此與其保持一致(除軟體錯誤外)。寫入資料庫是向該系統提供新輸入的唯一方式。
-允許應用程式直接寫入搜尋索引和資料庫引入了如 [圖 11-4](/fig/ddia_1204.png) 所示的問題,其中兩個客戶端同時傳送衝突的寫入,且兩個儲存系統按不同順序處理它們。在這種情況下,既不是資料庫說了算,也不是搜尋索引說了算,所以它們做出了相反的決定,進入彼此間永續性的不一致狀態。
+允許應用程式直接寫入搜尋索引和資料庫引入了如 [圖 12-4](/fig/ddia_1204.png) 所示的問題,其中兩個客戶端同時傳送衝突的寫入,且兩個儲存系統按不同順序處理它們。在這種情況下,既不是資料庫說了算,也不是搜尋索引說了算,所以它們做出了相反的決定,進入彼此間永續性的不一致狀態。
-如果你可以透過單個系統來提供所有使用者輸入,從而決定所有寫入的排序,則透過按相同順序處理寫入,可以更容易地派生出其他資料表示。這是狀態機複製方法的一個應用,我們在 “[全序廣播](/tw/ch9#全序廣播)” 中看到。無論你使用變更資料捕獲還是事件溯源日誌,都不如簡單的基於全序的決策原則更重要。
+如果你可以透過單個系統來提供所有使用者輸入,從而決定所有寫入的排序,則透過按相同順序處理寫入,可以更容易地派生出其他資料表示。這是狀態機複製方法的一個應用,我們在 “[全序廣播](/tw/ch10#sec_consistency_total_order)” 中看到。無論你使用變更資料捕獲還是事件溯源日誌,都不如簡單的基於全序的決策原則更重要。
-基於事件日誌來更新派生資料的系統,通常可以做到 **確定性** 與 **冪等性**(請參閱 “[冪等性](/tw/ch12#冪等性)”),使得從故障中恢復相當容易。
+基於事件日誌來更新派生資料的系統,通常可以做到 **確定性** 與 **冪等性**(請參閱 “[冪等性](/tw/ch12#sec_stream_idempotence)”),使得從故障中恢復相當容易。
-#### 派生資料與分散式事務
+#### 派生資料與分散式事務 {#sec_future_derived_vs_transactions}
-保持不同資料系統彼此一致的經典方法涉及分散式事務,如 “[原子提交與兩階段提交](/tw/ch9#原子提交與兩階段提交)” 中所述。與分散式事務相比,使用派生資料系統的方法如何?
+保持不同資料系統彼此一致的經典方法涉及分散式事務,如 “[原子提交與兩階段提交](/tw/ch8#sec_transactions_2pc)” 中所述。與分散式事務相比,使用派生資料系統的方法如何?
-在抽象層面,它們透過不同的方式達到類似的目標。分散式事務透過 **鎖** 進行互斥來決定寫入的順序(請參閱 “[兩階段鎖定](/tw/ch7#兩階段鎖定)”),而 CDC 和事件溯源使用日誌進行排序。分散式事務使用原子提交來確保變更只生效一次,而基於日誌的系統通常基於 **確定性重試** 和 **冪等性**。
+在抽象層面,它們透過不同的方式達到類似的目標。分散式事務透過 **鎖** 進行互斥來決定寫入的順序(請參閱 “[兩階段鎖定](/tw/ch8#sec_transactions_2pl)”),而 CDC 和事件溯源使用日誌進行排序。分散式事務使用原子提交來確保變更只生效一次,而基於日誌的系統通常基於 **確定性重試** 和 **冪等性**。
-最大的不同之處在於事務系統通常提供 [線性一致性](/tw/ch9#線性一致性),這包含著有用的保證,例如 [讀己之寫](/tw/ch5#讀己之寫)。另一方面,派生資料系統通常是非同步更新的,因此它們預設不會提供相同的時序保證。
+最大的不同之處在於事務系統通常提供 [線性一致性](/tw/ch10#sec_consistency_linearizability),這包含著有用的保證,例如 [讀己之寫](/tw/ch6#sec_replication_ryw)。另一方面,派生資料系統通常是非同步更新的,因此它們預設不會提供相同的時序保證。
-在願意為分散式事務付出代價的有限場景中,它們已被成功應用。但是,我認為 XA 的容錯能力和效能很差勁(請參閱 “[實踐中的分散式事務](/tw/ch9#實踐中的分散式事務)”),這嚴重限制了它的實用性。我相信為分散式事務設計一種更好的協議是可行的。但使這樣一種協議被現有工具廣泛接受是很有挑戰的,且不是立竿見影的事。
+在願意為分散式事務付出代價的有限場景中,它們已被成功應用。但是,我認為 XA 的容錯能力和效能很差勁(請參閱 “[實踐中的分散式事務](/tw/ch8#sec_transactions_xa)”),這嚴重限制了它的實用性。我相信為分散式事務設計一種更好的協議是可行的。但使這樣一種協議被現有工具廣泛接受是很有挑戰的,且不是立竿見影的事。
在沒有廣泛支援的良好分散式事務協議的情況下,我認為基於日誌的派生資料是整合不同資料系統的最有前途的方法。然而,諸如讀己之寫的保證是有用的,我認為告訴所有人 “最終一致性是不可避免的 —— 忍一忍並學會和它打交道” 是沒有什麼建設性的(至少在缺乏 **如何** 應對的良好指導時)。
-在 “[將事情做正確](#將事情做正確)” 中,我們將討論一些在非同步派生系統之上實現更強保障的方法,並邁向分散式事務和基於日誌的非同步系統之間的中間地帶。
+在本章後文中,我們將討論一些在非同步派生系統之上實現更強保障的方法,並邁向分散式事務和基於日誌的非同步系統之間的中間地帶。
-#### 全序的限制
+#### 全序的限制 {#id335}
對於足夠小的系統,構建一個完全有序的事件日誌是完全可行的(正如單主複製資料庫的流行所證明的那樣,它正好建立了這樣一種日誌)。但是,隨著系統向更大更複雜的工作負載伸縮,限制開始出現:
-* 在大多數情況下,構建完全有序的日誌,需要所有事件彙集於決定順序的 **單個領導者節點**。如果事件吞吐量大於單臺計算機的處理能力,則需要將其分割槽到多臺計算機上(請參閱 “[分割槽日誌](/tw/ch12#分割槽日誌)”)。然後兩個不同分割槽中的事件順序關係就不明確了。
-* 如果伺服器分佈在多個 **地理位置分散** 的資料中心上,例如為了容忍整個資料中心掉線,你通常在每個資料中心都有單獨的主庫,因為網路延遲會導致同步的跨資料中心協調效率低下(請參閱 “[多主複製](/tw/ch5#多主複製)”)。這意味著源自兩個不同資料中心的事件順序未定義。
-* 將應用程式部署為微服務時(請參閱 “[服務中的資料流:REST 與 RPC](/tw/ch4#服務中的資料流:REST與RPC)”),常見的設計選擇是將每個服務及其持久狀態作為獨立單元進行部署,服務之間不共享持久狀態。當兩個事件來自不同的服務時,這些事件間的順序未定義。
-* 某些應用程式在客戶端儲存狀態,該狀態在使用者輸入時立即更新(無需等待伺服器確認),甚至可以繼續離線工作(請參閱 “[需要離線操作的客戶端](/tw/ch5#需要離線操作的客戶端)”)。對於這樣的應用程式,客戶端和伺服器很可能以不同的順序看到事件。
+* 在大多數情況下,構建完全有序的日誌,需要所有事件彙集於決定順序的 **單個領導者節點**。如果事件吞吐量大於單臺計算機的處理能力,則需要將其分割槽到多臺計算機上(請參閱 “[分割槽日誌](/tw/ch12#sec_stream_log)”)。然後兩個不同分割槽中的事件順序關係就不明確了。
+* 如果伺服器分佈在多個 **地理位置分散** 的資料中心上,例如為了容忍整個資料中心掉線,你通常在每個資料中心都有單獨的主庫,因為網路延遲會導致同步的跨資料中心協調效率低下(請參閱 “[多主複製](/tw/ch6#sec_replication_multi_leader)”)。這意味著源自兩個不同資料中心的事件順序未定義。
+* 將應用程式部署為微服務時(請參閱 “[服務中的資料流:REST 與 RPC](/tw/ch5#sec_encoding_dataflow_rpc)”),常見的設計選擇是將每個服務及其持久狀態作為獨立單元進行部署,服務之間不共享持久狀態。當兩個事件來自不同的服務時,這些事件間的順序未定義。
+* 某些應用程式在客戶端儲存狀態,該狀態在使用者輸入時立即更新(無需等待伺服器確認),甚至可以繼續離線工作(請參閱 “[需要離線操作的客戶端](/tw/ch6#sec_replication_offline_clients)”)。對於這樣的應用程式,客戶端和伺服器很可能以不同的順序看到事件。
-在形式上,決定事件的全域性順序稱為 **全序廣播**,相當於 **共識**(請參閱 “[共識演算法和全序廣播](/tw/ch9#共識演算法和全序廣播)”)。大多數共識演算法都是針對單個節點的吞吐量足以處理整個事件流的情況而設計的,並且這些演算法不提供多個節點共享事件排序工作的機制。設計可以伸縮至單個節點的吞吐量之上,且在地理位置分散的環境中仍然工作良好的的共識演算法仍然是一個開放的研究問題。
+在形式上,決定事件的全域性順序稱為 **全序廣播**,相當於 **共識**(請參閱 “[共識演算法和全序廣播](/tw/ch10#sec_consistency_faces)”)。大多數共識演算法都是針對單個節點的吞吐量足以處理整個事件流的情況而設計的,並且這些演算法不提供多個節點共享事件排序工作的機制。設計可以伸縮至單個節點的吞吐量之上,且在地理位置分散環境中仍能良好工作的共識演算法仍然是一個開放研究問題。
-#### 排序事件以捕獲因果關係
+#### 排序事件以捕獲因果關係 {#sec_future_capture_causality}
-在事件之間不存在因果關係的情況下,全序的缺乏並不是一個大問題,因為併發事件可以任意排序。其他一些情況很容易處理:例如,當同一物件有多個更新時,它們可以透過將特定物件 ID 的所有更新路由到相同的日誌分割槽來完全排序。然而,因果關係有時會以更微妙的方式出現(請參閱 “[順序與因果關係](/tw/ch9#順序與因果關係)”)。
+在事件之間不存在因果關係的情況下,全序的缺乏並不是一個大問題,因為併發事件可以任意排序。其他一些情況很容易處理:例如,當同一物件有多個更新時,它們可以透過將特定物件 ID 的所有更新路由到相同的日誌分割槽來完全排序。然而,因果關係有時會以更微妙的方式出現(請參閱 “[順序與因果關係](/tw/ch10#sec_consistency_logical)”)。
例如,考慮一個社交網路服務,以及一對曾處於戀愛關係但剛分手的使用者。其中一個使用者將另一個使用者從好友中移除,然後向剩餘的好友傳送訊息,抱怨他們的前任。使用者的心思是他們的前任不應該看到這些粗魯的訊息,因為訊息是在好友狀態解除後傳送的。
但是如果好友關係狀態與訊息儲存在不同的地方,在這樣一個系統中,可能會出現 **解除好友** 事件與 **傳送訊息** 事件之間的因果依賴丟失的情況。如果因果依賴關係沒有被捕捉到,則傳送有關新訊息的通知的服務可能會在 **解除好友** 事件之前處理 **傳送訊息** 事件,從而錯誤地向前任傳送通知。
-在本例中,通知實際上是訊息和好友列表之間的連線,使得它與我們先前討論的連線的時序問題有關(請參閱 “[連線的時間依賴性](/tw/ch12#連線的時間依賴性)”)。不幸的是,這個問題似乎並沒有一個簡單的答案【2,3】。起點包括:
+在本例中,通知實際上是訊息和好友列表之間的連線,使得它與我們先前討論的連線的時序問題有關(請參閱 “[連線的時間依賴性](/tw/ch12#sec_stream_join_time)”)。不幸的是,這個問題似乎並沒有一個簡單的答案[^2][^3]。起點包括:
-* 邏輯時間戳可以提供無需協調的全域性順序(請參閱 “[序列號順序](/tw/ch9#序列號順序)”),因此它們可能有助於全序廣播不可行的情況。但是,他們仍然要求收件人處理不按順序傳送的事件,並且需要傳遞其他元資料。
-* 如果你可以記錄一個事件來記錄使用者在做出決定之前所看到的系統狀態,並給該事件一個唯一的識別符號,那麼後面的任何事件都可以引用該事件識別符號來記錄因果關係【4】。我們將在 “[讀也是事件](#讀也是事件)” 中回到這個想法。
-* 衝突解決演算法(請參閱 “[自動衝突解決](/tw/ch5#自動衝突解決)”)有助於處理以意外順序傳遞的事件。它們對於維護狀態很有用,但如果行為有外部副作用(例如,給使用者傳送通知),就沒什麼幫助了。
+* 邏輯時間戳可以提供無需協調的全域性順序(請參閱 “[序列號順序](/tw/ch10#sec_consistency_logical)”),因此它們可能有助於全序廣播不可行的情況。但是,他們仍然要求收件人處理不按順序傳送的事件,並且需要傳遞其他元資料。
+* 如果你可以記錄一個事件來記錄使用者在做出決定之前所看到的系統狀態,並給該事件一個唯一的識別符號,那麼後面的任何事件都可以引用該事件識別符號來記錄因果關係[^4]。我們將在 “[讀也是事件](#讀也是事件)” 中回到這個想法。
+* 衝突解決演算法(請參閱 “[自動衝突解決](/tw/ch6#automatic-conflict-resolution)”)有助於處理以意外順序傳遞的事件。它們對於維護狀態很有用,但如果行為有外部副作用(例如,給使用者傳送通知),就沒什麼幫助了。
也許,隨著時間的推移,應用開發模式將出現,使得能夠有效地捕獲因果依賴關係,並且保持正確的派生狀態,而不會迫使所有事件經歷全序廣播的瓶頸)。
-### 批處理與流處理
+### 批處理與流處理 {#sec_future_batch_streaming}
我會說資料整合的目標是,確保資料最終能在所有正確的地方表現出正確的形式。這樣做需要消費輸入、轉換、連線、過濾、聚合、訓練模型、評估、以及最終寫出適當的輸出。批處理和流處理是實現這一目標的工具。
-批處理和流處理的輸出是派生資料集,例如搜尋索引、物化檢視、向用戶顯示的建議、聚合指標等(請參閱 “[批處理工作流的輸出](/tw/ch11#批處理工作流的輸出)” 和 “[流處理的應用](/tw/ch12#流處理的應用)”)。
+批處理和流處理的輸出是派生資料集,例如搜尋索引、物化檢視、向用戶顯示的建議、聚合指標等(請參閱 “[批處理工作流的輸出](/tw/ch11#sec_batch_output)” 和 “[流處理的應用](/tw/ch12#sec_stream_uses)”)。
-正如我們在 [第十一章](/tw/ch11) 和 [第十二章](/tw/ch12) 中看到的,批處理和流處理有許多共同的原則,主要的根本區別在於流處理器在無限資料集上執行,而批處理輸入是已知的有限大小。處理引擎的實現方式也有很多細節上的差異,但是這些區別已經開始模糊。
+正如我們在 [第十一章](/tw/ch11) 和 [第十二章](/tw/ch12) 中看到的,批處理和流處理有許多共同的原則,主要的根本區別在於流處理器在無限資料集上執行,而批處理輸入是已知的有限大小。
-Spark 在批處理引擎上執行流處理,將流分解為 **微批次(microbatches)**,而 Apache Flink 則在流處理引擎上執行批處理【5】。原則上,一種型別的處理可以用另一種型別來模擬,但是效能特徵會有所不同:例如,在跳躍或滑動視窗上,微批次可能表現不佳【6】。
+#### 維護派生狀態 {#id446}
-#### 維護派生狀態
+批處理有著很強的函式式風格(即使其程式碼不是用函式式語言編寫的):它鼓勵確定性的純函式,其輸出僅依賴於輸入,除了顯式輸出外沒有副作用,將輸入視作不可變的,且輸出是僅追加的。流處理與之類似,但它擴充套件了運算元以允許受管理的、容錯的狀態(請參閱 “[失敗後重建狀態](/tw/ch12#sec_stream_state_fault_tolerance)”)。
-批處理有著很強的函式式風格(即使其程式碼不是用函式式語言編寫的):它鼓勵確定性的純函式,其輸出僅依賴於輸入,除了顯式輸出外沒有副作用,將輸入視作不可變的,且輸出是僅追加的。流處理與之類似,但它擴充套件了運算元以允許受管理的、容錯的狀態(請參閱 “[失敗後重建狀態”](/tw/ch12#失敗後重建狀態))。
+具有良好定義的輸入和輸出的確定性函式的原理不僅有利於容錯(請參閱 “[冪等性](/tw/ch12#sec_stream_idempotence)”),也簡化了有關組織中資料流的推理[^7]。無論派生資料是搜尋索引、統計模型還是快取,採用這種觀點思考都是很有幫助的:將其視為從一個東西派生出另一個的資料管道,透過函式式應用程式碼推送一個系統的狀態變更,並將其效果應用至派生系統中。
-具有良好定義的輸入和輸出的確定性函式的原理不僅有利於容錯(請參閱 “[冪等性](/tw/ch12#冪等性)”),也簡化了有關組織中資料流的推理【7】。無論派生資料是搜尋索引、統計模型還是快取,採用這種觀點思考都是很有幫助的:將其視為從一個東西派生出另一個的資料管道,透過函式式應用程式碼推送一個系統的狀態變更,並將其效果應用至派生系統中。
+原則上,派生資料系統可以同步地維護,就像關係資料庫在與索引表寫入操作相同的事務中同步更新次級索引一樣。然而,非同步是使基於事件日誌的系統穩健的原因:它允許系統的一部分故障被抑制在本地。而如果任何一個參與者失敗,分散式事務將中止,因此它們傾向於透過將故障傳播到系統的其餘部分來放大故障(請參閱 “[分散式事務的限制](/tw/ch8#sec_transactions_xa)”)。
-原則上,派生資料系統可以同步地維護,就像關係資料庫在與索引表寫入操作相同的事務中同步更新次級索引一樣。然而,非同步是使基於事件日誌的系統穩健的原因:它允許系統的一部分故障被抑制在本地。而如果任何一個參與者失敗,分散式事務將中止,因此它們傾向於透過將故障傳播到系統的其餘部分來放大故障(請參閱 “[分散式事務的限制](/tw/ch9#分散式事務的限制)”)。
+我們在 “[分割槽與次級索引](/tw/ch7#sec_sharding_secondary_indexes)” 中看到,次級索引經常跨越分割槽邊界。具有次級索引的分割槽系統需要將寫入傳送到多個分割槽(如果索引按關鍵詞分割槽的話)或將讀取傳送到所有分割槽(如果索引是按文件分割槽的話)。如果索引是非同步維護的,這種跨分割槽通訊也是最可靠和最可伸縮的[^8](另請參閱 “[多分割槽資料處理](#多分割槽資料處理)”)。
-我們在 “[分割槽與次級索引](/tw/ch6#分割槽與次級索引)” 中看到,次級索引經常跨越分割槽邊界。具有次級索引的分割槽系統需要將寫入傳送到多個分割槽(如果索引按關鍵詞分割槽的話)或將讀取傳送到所有分割槽(如果索引是按文件分割槽的話)。如果索引是非同步維護的,這種跨分割槽通訊也是最可靠和最可伸縮的【8】(另請參閱 “[多分割槽資料處理](#多分割槽資料處理)”)。
-
-#### 應用演化後重新處理資料
+#### 應用演化後重新處理資料 {#sec_future_reprocessing}
在維護派生資料時,批處理和流處理都是有用的。流處理允許將輸入中的變化以低延遲反映在派生檢視中,而批處理允許重新處理大量累積的歷史資料以便將新檢視匯出到現有資料集上。
-特別是,重新處理現有資料為維護系統、演化並支援新功能和需求變更提供了一個良好的機制(請參閱 [第四章](/tw/ch4))。沒有重新進行處理,模式演化將僅限於簡單的變化,例如向記錄中新增新的可選欄位或新增新型別的記錄。無論是在寫時模式還是在讀時模式中都是如此(請參閱 “[文件模型中的模式靈活性](/tw/ch2#文件模型中的模式靈活性)”)。另一方面,透過重新處理,可以將資料集重組為一個完全不同的模型,以便更好地滿足新的要求。
+特別是,重新處理現有資料為維護系統、演化並支援新功能和需求變更提供了一個良好的機制(請參閱 [第四章](/tw/ch4))。沒有重新進行處理,模式演化將僅限於簡單的變化,例如向記錄中新增新的可選欄位或新增新型別的記錄。無論是在寫時模式還是在讀時模式中都是如此(請參閱 “[文件模型中的模式靈活性](/tw/ch3#sec_datamodels_schema_flexibility)”)。另一方面,透過重新處理,可以將資料集重組為一個完全不同的模型,以便更好地滿足新的要求。
> ### 鐵路上的模式遷移
>
-> 大規模的 “模式遷移” 也發生在非計算機系統中。例如,在 19 世紀英國鐵路建設初期,軌距(兩軌之間的距離)就有了各種各樣的競爭標準。為一種軌距而建的列車不能在另一種軌距的軌道上執行,這限制了火車網路中可能的相互連線【9】。
+> 大規模的 “模式遷移” 也發生在非計算機系統中。例如,在 19 世紀英國鐵路建設初期,軌距(兩軌之間的距離)就有了各種各樣的競爭標準。為一種軌距而建的列車不能在另一種軌距的軌道上執行,這限制了火車網路中可能的相互連線[^9]。
>
-> 在 1846 年最終確定了一個標準軌距之後,其他軌距的軌道必須轉換 —— 但是如何在不停運火車線路的情況下進行數月甚至數年的遷移?解決的辦法是首先透過新增第三條軌道將軌道轉換為 **雙軌距(dual guage)** 或 **混合軌距**。這種轉換可以逐漸完成,當完成時,兩種軌距的列車都可以線上路上跑,使用三條軌道中的兩條。事實上,一旦所有的列車都轉換成標準軌距,那麼可以移除提供非標準軌距的軌道。
+> 在 1846 年最終確定了一個標準軌距之後,其他軌距的軌道必須轉換 —— 但是如何在不停運火車線路的情況下進行數月甚至數年的遷移?解決的辦法是首先透過新增第三條軌道將軌道轉換為 **雙軌距(dual gauge)** 或 **混合軌距**。這種轉換可以逐漸完成,當完成時,兩種軌距的列車都可以線上路上跑,使用三條軌道中的兩條。事實上,一旦所有的列車都轉換成標準軌距,那麼可以移除提供非標準軌距的軌道。
>
> 以這種方式 “再加工” 現有的軌道,讓新舊版本並存,可以在幾年的時間內逐漸改變軌距。然而,這是一項昂貴的事業,這就是今天非標準軌距仍然存在的原因。例如,舊金山灣區的 BART 系統使用了與美國大部分地區不同的軌距。
-派生檢視允許 **漸進演化(gradual evolution)**。如果你想重新構建資料集,不需要執行突然切換式的遷移。取而代之的是,你可以將舊架構和新架構並排維護為相同基礎資料上的兩個獨立派生檢視。然後可以開始將少量使用者轉移到新檢視,以測試其效能並發現任何錯誤,而大多數使用者仍然會被路由到舊檢視。你可以逐漸地增加訪問新檢視的使用者比例,最終可以刪除舊檢視【10】。
+派生檢視允許 **漸進演化(gradual evolution)**。如果你想重新構建資料集,不需要執行突然切換式的遷移。取而代之的是,你可以將舊架構和新架構並排維護為相同基礎資料上的兩個獨立派生檢視。然後可以開始將少量使用者轉移到新檢視,以測試其效能並發現任何錯誤,而大多數使用者仍然會被路由到舊檢視。你可以逐漸地增加訪問新檢視的使用者比例,最終可以刪除舊檢視[^10]。
-這種逐漸遷移的美妙之處在於,如果出現問題,每個階段的過程都很容易逆轉:你始終有一個可以回滾的可用系統。透過降低不可逆損害的風險,你能對繼續前進更有信心,從而更快地改善系統【11】。
+這種逐漸遷移的美妙之處在於,如果出現問題,每個階段的過程都很容易逆轉:你始終有一個可以回滾的可用系統。透過降低不可逆損害的風險,你能對繼續前進更有信心,從而更快地改善系統[^11]。
-#### Lambda架構
+#### 統一批處理和流處理 {#id338}
-如果批處理用於重新處理歷史資料,而流處理用於處理最近的更新,那麼如何將這兩者結合起來?Lambda 架構【12】是這方面的一個建議,引起了很多關注。
-
-Lambda 架構的核心思想是透過將不可變事件附加到不斷增長的資料集來記錄傳入資料,這類似於事件溯源(請參閱 “[事件溯源](/tw/ch12#事件溯源)”)。為了從這些事件中派生出讀取最佳化的檢視,Lambda 架構建議並行執行兩個不同的系統:批處理系統(如 Hadoop MapReduce)和獨立的流處理系統(如 Storm)。
-
-在 Lambda 方法中,流處理器消耗事件並快速生成對檢視的近似更新;批處理器稍後將使用同一組事件並生成派生檢視的更正版本。這個設計背後的原因是批處理更簡單,因此不易出錯,而流處理器被認為是不太可靠和難以容錯的(請參閱 “[容錯](/tw/ch12#容錯)”)。而且,流處理可以使用快速近似演算法,而批處理使用較慢的精確演算法。
-
-Lambda 架構是一種有影響力的想法,它將資料系統的設計變得更好,尤其是透過推廣這樣的原則:在不可變事件流上建立派生檢視,並在需要時重新處理事件。但是我也認為它有一些實際問題:
-
-* 在批處理和流處理框架中維護相同的邏輯是很顯著的額外工作。雖然像 Summingbird【13】這樣的庫提供了一種可以在批處理和流處理的上下文中執行的計算抽象。除錯、調整和維護兩個不同系統的操作複雜性依然存在【14】。
-* 由於流管道和批處理管道產生獨立的輸出,因此需要合併它們以響應使用者請求。如果計算是基於滾動視窗的簡單聚合,則合併相當容易,但如果檢視基於更複雜的操作(例如連線和會話化)而匯出,或者輸出不是時間序列,則會變得非常困難。
-* 儘管有能力重新處理整個歷史資料集是很好的,但在大型資料集上這樣做經常會開銷巨大。因此,批處理流水線通常需要設定為處理增量批處理(例如,在每小時結束時處理一小時的資料),而不是重新處理所有內容。這引發了 “[時間推理](/tw/ch12#時間推理)” 中討論的問題,例如處理滯留事件和處理跨批次邊界的視窗。增量化批處理計算會增加複雜性,使其更類似於流式傳輸層,這與保持批處理層儘可能簡單的目標背道而馳。
-
-#### 統一批處理和流處理
-
-最近的工作使得 Lambda 架構的優點在沒有其缺點的情況下得以實現,允許批處理計算(重新處理歷史資料)和流計算(在事件到達時即處理)在同一個系統中實現【15】。
+早期統一批處理與流處理的提案是 **Lambda 架構**[^12],但它有不少問題,並且已經逐漸淡出主流。更新的系統允許在同一個系統中同時實現批計算(重處理歷史資料)和流計算(事件到達即處理)[^15]。
在一個系統中統一批處理和流處理需要以下功能,這些功能也正在越來越廣泛地被提供:
-* 透過處理最近事件流的相同處理引擎來重播歷史事件的能力。例如,基於日誌的訊息代理可以重播訊息(請參閱 “[重播舊訊息](/tw/ch12#重播舊訊息)”),某些流處理器可以從 HDFS 等分散式檔案系統讀取輸入。
-* 對於流處理器來說,恰好一次語義 —— 即確保輸出與未發生故障的輸出相同,即使事實上發生故障(請參閱 “[容錯](/tw/ch12#容錯)”)。與批處理一樣,這需要丟棄任何失敗任務的部分輸出。
-* 按事件時間進行視窗化的工具,而不是按處理時間進行視窗化,因為處理歷史事件時,處理時間毫無意義(請參閱 “[時間推理](/tw/ch12#時間推理)”)。例如,Apache Beam 提供了用於表達這種計算的 API,可以在 Apache Flink 或 Google Cloud Dataflow 使用。
+* 透過處理最近事件流的相同處理引擎來重播歷史事件的能力。例如,基於日誌的訊息代理可以重播訊息(請參閱 “[重播舊訊息](/tw/ch12#sec_stream_replay)”),某些流處理器可以從 HDFS 等分散式檔案系統讀取輸入。
+* 對於流處理器來說,恰好一次語義 —— 即確保輸出與未發生故障的輸出相同,即使事實上發生故障(請參閱 “[容錯](/tw/ch12#sec_stream_fault_tolerance)”)。與批處理一樣,這需要丟棄任何失敗任務的部分輸出。
+* 按事件時間進行視窗化的工具,而不是按處理時間進行視窗化,因為處理歷史事件時,處理時間毫無意義(請參閱 “[時間推理](/tw/ch12#sec_stream_time)”)。例如,Apache Beam 提供了用於表達這種計算的 API,可以在 Apache Flink 或 Google Cloud Dataflow 使用。
-## 分拆資料庫
+## 分拆資料庫 {#sec_future_unbundling}
-在最抽象的層面上,資料庫,Hadoop 和作業系統都發揮相同的功能:它們儲存一些資料,並允許你處理和查詢這些資料【16】。資料庫將資料儲存為特定資料模型的記錄(表中的行、文件、圖中的頂點等),而作業系統的檔案系統則將資料儲存在檔案中 —— 但其核心都是 “資訊管理” 系統【17】。正如我們在 [第十一章](/tw/ch11) 中看到的,Hadoop 生態系統有點像 Unix 的分散式版本。
+在最抽象的層面上,資料庫、批/流處理器和作業系統都在做相似的事情:儲存資料,並允許你處理和查詢這些資料[^16]。資料庫將資料儲存為某種資料模型下的記錄(例如錶行、文件、圖頂點等),而作業系統檔案系統將資料存為檔案;但它們本質上都可視作 “資訊管理” 系統[^17]。正如我們在 [第十一章](/tw/ch11) 中看到的,批處理系統在很多方面像是 Unix 的分散式版本。
當然,有很多實際的差異。例如,許多檔案系統都不能很好地處理包含 1000 萬個小檔案的目錄,而包含 1000 萬個小記錄的資料庫完全是尋常而不起眼的。無論如何,作業系統和資料庫之間的相似之處和差異值得探討。
@@ -172,104 +150,90 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni
在這一部分我將試圖調和這兩個哲學,希望我們能各取其美。
-### 組合使用資料儲存技術
+### 組合使用資料儲存技術 {#id447}
在本書的過程中,我們討論了資料庫提供的各種功能及其工作原理,其中包括:
-* 次級索引,使你可以根據欄位的值有效地搜尋記錄(請參閱 “[其他索引結構](/tw/ch3#其他索引結構)”)
-* 物化檢視,這是一種預計算的查詢結果快取(請參閱 “[聚合:資料立方體和物化檢視](/tw/ch3#聚合:資料立方體和物化檢視)”)
-* 複製日誌,保持其他節點上資料的副本最新(請參閱 “[複製日誌的實現](/tw/ch5#複製日誌的實現)”)
-* 全文檢索索引,允許在文字中進行關鍵字搜尋(請參閱 “[全文檢索與模糊索引](/tw/ch3#全文檢索與模糊索引)”),也內置於某些關係資料庫【1】
+* 次級索引,使你可以根據欄位的值有效地搜尋記錄(請參閱 “[其他索引結構](/tw/ch4#sec_storage_index_multicolumn)”)
+* 物化檢視,這是一種預計算的查詢結果快取(請參閱 “[聚合:資料立方體和物化檢視](/tw/ch4#sec_storage_materialized_views)”)
+* 複製日誌,保持其他節點上資料的副本最新(請參閱 “[複製日誌的實現](/tw/ch6#sec_replication_implementation)”)
+* 全文檢索索引,允許在文字中進行關鍵字搜尋(請參閱 “[全文檢索與模糊索引](/tw/ch4#sec_storage_full_text)”),也內置於某些關係資料庫[^1]
-在 [第十一章](/tw/ch11) 和 [第十二章](/tw/ch12) 中,出現了類似的主題。我們討論了如何構建全文檢索索引(請參閱 “[批處理工作流的輸出](/tw/ch11#批處理工作流的輸出)”),瞭解了如何維護物化檢視(請參閱 “[維護物化檢視](/tw/ch12#維護物化檢視)”)以及如何將變更從資料庫複製到派生資料系統(請參閱 “[變更資料捕獲](/tw/ch12#變更資料捕獲)”)。
+在 [第十一章](/tw/ch11) 和 [第十二章](/tw/ch12) 中,出現了類似的主題。我們討論了如何構建全文檢索索引(請參閱 “[批處理工作流的輸出](/tw/ch11#sec_batch_output)”),瞭解了如何維護物化檢視(請參閱 “[維護物化檢視](/tw/ch12#sec_stream_mat_view)”)以及如何將變更從資料庫複製到派生資料系統(請參閱 “[變更資料捕獲](/tw/ch12#sec_stream_cdc)”)。
資料庫中內建的功能與人們用批處理和流處理器構建的派生資料系統似乎有相似之處。
-#### 建立索引
+#### 建立索引 {#id340}
想想當你執行 `CREATE INDEX` 在關係資料庫中建立一個新的索引時會發生什麼。資料庫必須掃描表的一致性快照,挑選出所有被索引的欄位值,對它們進行排序,然後寫出索引。然後它必須處理自一致快照以來所做的寫入操作(假設表在建立索引時未被鎖定,所以寫操作可能會繼續)。一旦完成,只要事務寫入表中,資料庫就必須繼續保持索引最新。
-此過程非常類似於設定新的從庫副本(請參閱 “[設定新從庫](/tw/ch5#設定新從庫)”),也非常類似於流處理系統中的 **引導(bootstrap)** 變更資料捕獲(請參閱 “[初始快照](/tw/ch12#初始快照)”)。
+此過程非常類似於設定新的從庫副本(請參閱 “[設定新從庫](/tw/ch6#sec_replication_new_replica)”),也非常類似於流處理系統中的 **引導(bootstrap)** 變更資料捕獲(請參閱 “[初始快照](/tw/ch12#sec_stream_cdc_snapshot)”)。
-無論何時執行 `CREATE INDEX`,資料庫都會重新處理現有資料集(如 “[應用演化後重新處理資料](#應用演化後重新處理資料)” 中所述),並將該索引作為新檢視匯出到現有資料上。現有資料可能是狀態的快照,而不是所有發生變化的日誌,但兩者密切相關(請參閱 “[狀態、流和不變性](/tw/ch12#狀態、流和不變性)”)。
+無論何時執行 `CREATE INDEX`,資料庫都會重新處理現有資料集(如 “[應用演化後重新處理資料](#應用演化後重新處理資料)” 中所述),並將該索引作為新檢視匯出到現有資料上。現有資料可能是狀態的快照,而不是所有發生變化的日誌,但兩者密切相關(請參閱 “[狀態、流和不變性](/tw/ch12#sec_stream_immutability)”)。
-#### 一切的元資料庫
+#### 一切的元資料庫 {#id341}
-有鑑於此,我認為整個組織的資料流開始像一個巨大的資料庫【7】。每當批處理、流或 ETL 過程將資料從一個地方傳輸到另一個地方並組裝時,它表現地就像資料庫子系統一樣,使索引或物化檢視保持最新。
+有鑑於此,我認為整個組織的資料流開始像一個巨大的資料庫[^7]。每當批處理、流處理或 ETL 過程將資料從一個地方傳輸並轉換到另一個地方時,它都像資料庫子系統在維護索引或物化檢視。
-從這種角度來看,批處理和流處理器就像精心實現的觸發器、儲存過程和物化檢視維護例程。它們維護的派生資料系統就像不同的索引型別。例如,關係資料庫可能支援 B 樹索引、雜湊索引、空間索引(請參閱 “[多列索引](/tw/ch3#多列索引)”)以及其他型別的索引。在新興的派生資料系統架構中,不是將這些設施作為單個整合資料庫產品的功能實現,而是由各種不同的軟體提供,執行在不同的機器上,由不同的團隊管理。
+從這種角度來看,批處理和流處理器就像精心實現的觸發器、儲存過程和物化檢視維護例程。它們維護的派生資料系統就像不同的索引型別。例如,關係資料庫可能支援 B 樹索引、雜湊索引、空間索引(請參閱 “[多列索引](/tw/ch4#sec_storage_index_multicolumn)”)以及其他型別的索引。在新興的派生資料系統架構中,不是將這些設施作為單個整合資料庫產品的功能實現,而是由各種不同的軟體提供,執行在不同的機器上,由不同的團隊管理。
這些發展在未來將會把我們帶到哪裡?如果我們從沒有適合所有訪問模式的單一資料模型或儲存格式的前提出發,我推測有兩種途徑可以將不同的儲存和處理工具組合成一個有凝聚力的系統:
**聯合資料庫:統一讀取**
-可以為各種各樣的底層儲存引擎和處理方法提供一個統一的查詢介面 —— 一種稱為 **聯合資料庫(federated database)** 或 **多型儲存(polystore)** 的方法【18,19】。例如,PostgreSQL 的 **外部資料包裝器(foreign data wrapper)** 功能符合這種模式【20】。需要專用資料模型或查詢介面的應用程式仍然可以直接訪問底層儲存引擎,而想要組合來自不同位置的資料的使用者可以透過聯合介面輕鬆完成操作。
+可以為各種各樣的底層儲存引擎和處理方法提供一個統一的查詢介面 —— 一種稱為 **聯合資料庫(federated database)** 或 **多型儲存(polystore)** 的方法[^18][^19]。例如,PostgreSQL 的 **外部資料包裝器(foreign data wrapper)** 功能符合這種模式[^20]。需要專用資料模型或查詢介面的應用程式仍然可以直接訪問底層儲存引擎,而想要組合來自不同位置的資料的使用者可以透過聯合介面輕鬆完成操作。
聯合查詢介面遵循著單一整合系統的關係型傳統,帶有高階查詢語言和優雅的語義,但實現起來非常複雜。
**分拆資料庫:統一寫入**
-雖然聯合能解決跨多個不同系統的只讀查詢問題,但它並沒有很好的解決跨系統 **同步** 寫入的問題。我們說過,在單個數據庫中,建立一致的索引是一項內建功能。當我們構建多個儲存系統時,我們同樣需要確保所有資料變更都會在所有正確的位置結束,即使在出現故障時也是如此。想要更容易地將儲存系統可靠地插接在一起(例如,透過變更資料捕獲和事件日誌),就像將資料庫的索引維護功能以可以跨不同技術同步寫入的方式分開【7,21】。
+雖然聯合能解決跨多個不同系統的只讀查詢問題,但它並沒有很好的解決跨系統 **同步** 寫入的問題。我們說過,在單個數據庫中,建立一致的索引是一項內建功能。當我們構建多個儲存系統時,我們同樣需要確保所有資料變更都會在所有正確的位置結束,即使在出現故障時也是如此。想要更容易地將儲存系統可靠地插接在一起(例如,透過變更資料捕獲和事件日誌),就像將資料庫的索引維護功能以可以跨不同技術同步寫入的方式分開[^7][^21]。
-分拆方法遵循 Unix 傳統的小型工具,它可以很好地完成一件事【22】,透過統一的低層級 API(管道)進行通訊,並且可以使用更高層級的語言進行組合(shell)【16】 。
+分拆方法遵循 Unix 傳統的小型工具,它可以很好地完成一件事[^22],透過統一的低層級 API(管道)進行通訊,並且可以使用更高層級的語言進行組合(shell)[^16] 。
-#### 開展分拆工作
+#### 開展分拆工作 {#sec_future_unbundling_favor}
聯合和分拆是一個硬幣的兩面:用不同的元件構成可靠、 可伸縮和可維護的系統。聯合只讀查詢需要將一個數據模型對映到另一個數據模型,這需要一些思考,但最終還是一個可解決的問題。而我認為同步寫入到幾個儲存系統是更困難的工程問題,所以我將重點關注它。
-傳統的同步寫入方法需要跨異構儲存系統的分散式事務【18】,我認為這是錯誤的解決方案(請參閱 “[派生資料與分散式事務](#派生資料與分散式事務)”)。單個儲存或流處理系統內的事務是可行的,但是當資料跨越不同技術之間的邊界時,我認為具有冪等寫入的非同步事件日誌是一種更加健壯和實用的方法。
+傳統的同步寫入方法需要跨異構儲存系統的分散式事務[^18],我認為這是錯誤的解決方案(請參閱 “[派生資料與分散式事務](#派生資料與分散式事務)”)。單個儲存或流處理系統內的事務是可行的,但是當資料跨越不同技術之間的邊界時,我認為具有冪等寫入的非同步事件日誌是一種更加健壯和實用的方法。
-例如,分散式事務在某些流處理元件內部使用,以匹配 **恰好一次(exactly-once)** 語義(請參閱 “[原子提交再現](/tw/ch12#原子提交再現)”),這可以很好地工作。然而,當事務需要涉及由不同人群編寫的系統時(例如,當資料從流處理元件寫入分散式鍵值儲存或搜尋索引時),缺乏標準化的事務協議會使整合更難。有冪等消費者的有序事件日誌(請參閱 “[冪等性](/tw/ch12#冪等性)”)是一種更簡單的抽象,因此在異構系統中實現更加可行【7】。
+例如,分散式事務在某些流處理元件內部使用,以匹配 **恰好一次(exactly-once)** 語義(請參閱 “[原子提交再現](/tw/ch12#sec_stream_atomic_commit)”),這可以很好地工作。然而,當事務需要涉及由不同人群編寫的系統時(例如,當資料從流處理元件寫入分散式鍵值儲存或搜尋索引時),缺乏標準化的事務協議會使整合更難。有冪等消費者的有序事件日誌(請參閱 “[冪等性](/tw/ch12#sec_stream_idempotence)”)是一種更簡單的抽象,因此在異構系統中實現更加可行[^7]。
基於日誌的整合的一大優勢是各個元件之間的 **鬆散耦合(loose coupling)**,這體現在兩個方面:
-1. 在系統級別,非同步事件流使整個系統在個別元件的中斷或效能下降時更加穩健。如果消費者執行緩慢或失敗,那麼事件日誌可以緩衝訊息(請參閱 “[磁碟空間使用](/tw/ch12#磁碟空間使用)”),以便生產者和任何其他消費者可以繼續不受影響地執行。有問題的消費者可以在問題修復後趕上,因此不會錯過任何資料,並且包含故障。相比之下,分散式事務的同步互動往往會將本地故障升級為大規模故障(請參閱 “[分散式事務的限制](/tw/ch9#分散式事務的限制)”)。
+1. 在系統級別,非同步事件流使整個系統在個別元件的中斷或效能下降時更加穩健。如果消費者執行緩慢或失敗,那麼事件日誌可以緩衝訊息(請參閱 “[磁碟空間使用](/tw/ch12#sec_stream_disk_usage)”),以便生產者和任何其他消費者可以繼續不受影響地執行。有問題的消費者可以在問題修復後趕上,因此不會錯過任何資料,並且包含故障。相比之下,分散式事務的同步互動往往會將本地故障升級為大規模故障(請參閱 “[分散式事務的限制](/tw/ch8#sec_transactions_xa)”)。
2. 在人力方面,分拆資料系統允許不同的團隊獨立開發,改進和維護不同的軟體元件和服務。專業化使得每個團隊都可以專注於做好一件事,並與其他團隊的系統以明確的介面互動。事件日誌提供了一個足夠強大的介面,以捕獲相當強的一致性屬性(由於永續性和事件的順序),但也足夠普適於幾乎任何型別的資料。
-#### 分拆系統vs整合系統
+#### 分拆系統與整合系統 {#id448}
-如果分拆確實成為未來的方式,它也不會取代目前形式的資料庫 —— 它們仍然會像以往一樣被需要。為了維護流處理元件中的狀態,資料庫仍然是需要的,並且為批處理和流處理器的輸出提供查詢服務(請參閱 “[批處理工作流的輸出](/tw/ch11#批處理工作流的輸出)” 與 “[流處理](/tw/ch12#流處理)”)。專用查詢引擎對於特定的工作負載仍然非常重要:例如,MPP 資料倉庫中的查詢引擎針對探索性分析查詢進行了最佳化,並且能夠很好地處理這種型別的工作負載(請參閱 “[Hadoop 與分散式資料庫的對比](/tw/ch11#Hadoop與分散式資料庫的對比)”)。
+如果分拆確實成為未來的方式,它也不會取代目前形式的資料庫 —— 它們仍然會像以往一樣被需要。為了維護流處理元件中的狀態,資料庫仍然是需要的,並且為批處理和流處理器的輸出提供查詢服務(請參閱 “[批處理工作流的輸出](/tw/ch11#sec_batch_output)” 與 “[流處理](/tw/ch12#sec_stream_processing)”)。專用查詢引擎對於特定的工作負載仍然非常重要:例如,MPP 資料倉庫中的查詢引擎針對探索性分析查詢進行了最佳化,並且能夠很好地處理這種型別的工作負載(請參閱 “[Hadoop 與分散式資料庫的對比](/tw/ch11#sec_batch_distributed)”)。
-執行幾種不同基礎設施的複雜性可能是一個問題:每種軟體都有一個學習曲線,配置問題和操作怪癖,因此部署儘可能少的移動部件是很有必要的。比起使用應用程式碼拼接多個工具而成的系統,單一整合軟體產品也可以在其設計應對的工作負載型別上實現更好、更可預測的效能【23】。正如在前言中所說的那樣,為了不需要的規模而構建系統是白費精力,而且可能會將你鎖死在一個不靈活的設計中。實際上,這是一種過早最佳化的形式。
+執行幾種不同基礎設施的複雜性可能是一個問題:每種軟體都有一個學習曲線,配置問題和操作怪癖,因此部署儘可能少的移動部件是很有必要的。比起使用應用程式碼拼接多個工具而成的系統,單一整合軟體產品也可以在其設計應對的工作負載型別上實現更好、更可預測的效能[^23]。正如在前言中所說的那樣,為了不需要的規模而構建系統是白費精力,而且可能會將你鎖死在一個不靈活的設計中。實際上,這是一種過早最佳化的形式。
-分拆的目標不是要針對個別資料庫與特定工作負載的效能進行競爭;我們的目標是允許你結合多個不同的資料庫,以便在比單個軟體可能實現的更廣泛的工作負載範圍內實現更好的效能。這是關於廣度,而不是深度 —— 與我們在 “[Hadoop 與分散式資料庫的對比](/tw/ch11#Hadoop與分散式資料庫的對比)” 中討論的儲存和處理模型的多樣性一樣。
+分拆的目標不是要針對個別資料庫與特定工作負載的效能進行競爭;我們的目標是允許你結合多個不同的資料庫,以便在比單個軟體可能實現的更廣泛的工作負載範圍內實現更好的效能。這是關於廣度,而不是深度 —— 與我們在 “[Hadoop 與分散式資料庫的對比](/tw/ch11#sec_batch_distributed)” 中討論的儲存和處理模型的多樣性一樣。
因此,如果有一項技術可以滿足你的所有需求,那麼最好使用該產品,而不是試圖用更低層級的元件重新實現它。只有當沒有單一軟體滿足你的所有需求時,才會出現拆分和聯合的優勢。
-#### 少了什麼?
+### 圍繞資料流設計應用 {#sec_future_dataflow}
-用於組成資料系統的工具正在變得越來越好,但我認為還缺少一個主要的東西:我們還沒有與 Unix shell 類似的分拆資料庫等價物(即,一種宣告式的、簡單的、用於組裝儲存和處理系統的高階語言)。
+當底層資料發生變化時去更新派生資料,這個思路並不新鮮。比如電子表格就有很強的資料流程式設計能力[^33]:你可以在一個單元格寫公式(例如對另一列求和),只要輸入變化,結果就會自動重算。這正是我們希望資料系統具備的能力:資料庫記錄一旦變化,相關索引、快取檢視和聚合結果都應自動重新整理,而不需要應用開發者關心重新整理細節。
-例如,如果我們可以簡單地宣告 `mysql | elasticsearch`,類似於 Unix 管道【22】,成為 `CREATE INDEX` 的分拆等價物:它將讀取 MySQL 資料庫中的所有文件並將其索引到 Elasticsearch 叢集中。然後它會不斷捕獲對資料庫所做的所有變更,並自動將它們應用於搜尋索引,而無需編寫自定義應用程式碼。這種整合應當支援幾乎任何型別的儲存或索引系統。
+從這個意義上說,今天很多資料系統仍可以向 VisiCalc 在 1979 年就具備的特性學習[^34]。與電子表格不同的是,現代資料系統還必須同時滿足容錯、可伸縮、持久化儲存、跨團隊異構技術整合等要求,也必須能夠複用已有庫與服務。指望所有軟體都在一種語言、框架或工具上統一實現並不現實。
-同樣,能夠更容易地預先計算和更新快取將是一件好事。回想一下,物化檢視本質上是一個預先計算的快取,所以你可以透過為複雜查詢宣告指定物化檢視來建立快取,包括圖上的遞迴查詢(請參閱 “[圖資料模型](/tw/ch2#圖資料模型)”)和應用邏輯。在這方面有一些有趣的早期研究,如 **差分資料流(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】。與電子表格的不同之處在於,今天的資料系統需要具有容錯性,可伸縮性以及持久儲存資料。它們還需要能夠整合不同人群編寫的不同技術,並重用現有的庫和服務:期望使用某一種特定的語言、框架或工具來開發所有軟體是不切實際的。
-
-在本節中,我將詳細介紹這些想法,並探討一些圍繞分拆資料庫和資料流的想法構建應用的方法。
-
-#### 應用程式碼作為派生函式
+#### 應用程式碼作為派生函式 {#sec_future_dataflow_derivation}
當一個數據集派生自另一個數據集時,它會經歷某種轉換函式。例如:
-* 次級索引是由一種直白的轉換函式生成的派生資料集:對於基礎表中的每行或每個文件,它挑選被索引的列或欄位中的值,並按這些值排序(假設使用 B 樹或 SSTable 索引,按鍵排序,如 [第三章](/tw/ch3) 所述)。
+* 次級索引是由一種直白的轉換函式生成的派生資料集:對於基礎表中的每行或每個文件,它挑選被索引的列或欄位中的值,並按這些值排序(假設使用 B 樹或 SSTable 索引,按鍵排序,如 [第四章](/tw/ch4) 所述)。
* 全文檢索索引是透過應用各種自然語言處理函式而建立的,諸如語言檢測、分詞、詞幹或詞彙化、拼寫糾正和同義詞識別,然後構建用於高效查詢的資料結構(例如倒排索引)。
* 在機器學習系統中,我們可以將模型視作從訓練資料透過應用各種特徵提取、統計分析函式派生的資料,當模型應用於新的輸入資料時,模型的輸出是從輸入和模型(因此間接地從訓練資料)中派生的。
* 快取通常包含將以使用者介面(UI)顯示的形式的資料聚合。因此填充快取需要知道 UI 中引用的欄位;UI 中的變更可能需要更新快取填充方式的定義,並重建快取。
-用於次級索引的派生函式是如此常用的需求,以致於它作為核心功能被內建至許多資料庫中,你可以簡單地透過 `CREATE INDEX` 來呼叫它。對於全文索引,常見語言的基本語言特徵可能內建到資料庫中,但更複雜的特徵通常需要領域特定的調整。在機器學習中,特徵工程是眾所周知的特定於應用的特徵,通常需要包含很多關於使用者互動與應用部署的詳細知識【35】。
+用於次級索引的派生函式是如此常用的需求,以致於它作為核心功能被內建至許多資料庫中,你可以簡單地透過 `CREATE INDEX` 來呼叫它。對於全文索引,常見語言的基本語言特徵可能內建到資料庫中,但更複雜的特徵通常需要領域特定的調整。在機器學習中,特徵工程是眾所周知的特定於應用的特徵,通常需要包含很多關於使用者互動與應用部署的詳細知識[^35]。
-當建立派生資料集的函式不是像建立次級索引那樣的標準搬磚函式時,需要自定義程式碼來處理特定於應用的東西。而這個自定義程式碼是讓許多資料庫掙扎的地方,雖然關係資料庫通常支援觸發器、儲存過程和使用者定義的函式,可以用它們來在資料庫中執行應用程式碼,但它們有點像資料庫設計裡的事後反思。(請參閱 “[傳遞事件流](/tw/ch12#傳遞事件流)”)。
+當建立派生資料集的函式不是像建立次級索引那樣的標準搬磚函式時,需要自定義程式碼來處理特定於應用的東西。而這個自定義程式碼是讓許多資料庫掙扎的地方,雖然關係資料庫通常支援觸發器、儲存過程和使用者定義的函式,可以用它們來在資料庫中執行應用程式碼,但它們有點像資料庫設計裡的事後反思。(請參閱 “[傳遞事件流](/tw/ch12#sec_stream_transmit)”)。
-#### 應用程式碼和狀態的分離
+#### 應用程式碼和狀態的分離 {#id344}
理論上,資料庫可以是任意應用程式碼的部署環境,就如同作業系統一樣。然而實踐中它們對這一目標適配的很差。它們不滿足現代應用開發的要求,例如依賴和軟體包管理、版本控制、滾動升級、可演化性、監控、指標、對網路服務的呼叫以及與外部系統的整合。
@@ -277,83 +241,75 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni
我認為讓系統的某些部分專門用於持久資料儲存並讓其他部分專門執行應用程式程式碼是有意義的。這兩者可以在保持獨立的同時互動。
-現在大多數 Web 應用程式都是作為無狀態服務部署的,其中任何使用者請求都可以路由到任何應用程式伺服器,並且伺服器在傳送響應後會忘記所有請求。這種部署方式很方便,因為可以隨意新增或刪除伺服器,但狀態必須到某個地方:通常是資料庫。趨勢是將無狀態應用程式邏輯與狀態管理(資料庫)分開:不將應用程式邏輯放入資料庫中,也不將持久狀態置於應用程式中【36】。正如函數語言程式設計社群喜歡開玩笑說的那樣,“我們相信 **教會(Church)** 與 **國家(state)** 的分離”【37】 [^i]
-
-[^i]: 解釋笑話很少會讓人感覺更好,但我不想讓任何人感到被遺漏。在這裡,Church 指代的是數學家的阿隆佐・邱奇,他創立了 lambda 演算,這是計算的早期形式,是大多數函數語言程式設計語言的基礎。lambda 演算不具有可變狀態(即沒有變數可以被覆蓋),所以可以說可變狀態與 Church 的工作是分離的。
+現在大多數 Web 應用程式都是作為無狀態服務部署的,其中任何使用者請求都可以路由到任何應用程式伺服器,並且伺服器在傳送響應後會忘記所有請求。這種部署方式很方便,因為可以隨意新增或刪除伺服器,但狀態必須到某個地方:通常是資料庫。趨勢是將無狀態應用程式邏輯與狀態管理(資料庫)分開:不將應用程式邏輯放入資料庫中,也不將持久狀態置於應用程式中[^36]。正如函數語言程式設計社群喜歡開玩笑說的那樣,“我們相信 **教會(Church)** 與 **國家(state)** 的分離”[^37]。
在這個典型的 Web 應用模型中,資料庫充當一種可以透過網路同步訪問的可變共享變數。應用程式可以讀取和更新變數,而資料庫負責維持它的永續性,提供一些諸如併發控制和容錯的功能。
但是,在大多數程式語言中,你無法訂閱可變變數中的變更 —— 你只能定期讀取它。與電子表格不同,如果變數的值發生變化,變數的讀者不會收到通知(你可以在自己的程式碼中實現這樣的通知 —— 這被稱為 **觀察者模式** —— 但大多數語言沒有將這種模式作為內建功能)。
-資料庫繼承了這種可變資料的被動方法:如果你想知道資料庫的內容是否發生了變化,通常你唯一的選擇就是輪詢(即定期重複你的查詢)。訂閱變更只是剛剛開始出現的功能(請參閱 “[變更流的 API 支援](/tw/ch12#變更流的API支援)”)。
+資料庫繼承了這種可變資料的被動方法:如果你想知道資料庫的內容是否發生了變化,通常你唯一的選擇就是輪詢(即定期重複你的查詢)。訂閱變更只是剛剛開始出現的功能(請參閱 “[變更流的 API 支援](/tw/ch12#sec_stream_change_api)”)。
-#### 資料流:應用程式碼與狀態變化的互動
+#### 資料流:應用程式碼與狀態變化的互動 {#id450}
從資料流的角度思考應用程式,意味著重新協調應用程式碼和狀態管理之間的關係。我們不再將資料庫視作被應用操縱的被動變數,取而代之的是更多地考慮狀態,狀態變更和處理它們的程式碼之間的相互作用與協同關係。應用程式碼透過在另一個地方觸發狀態變更來響應狀態變更。
-我們在 “[資料庫與流](/tw/ch12#資料庫與流)” 中看到了這一思路,我們討論了將資料庫的變更日誌視為一種我們可以訂閱的事件流。諸如 Actor 的訊息傳遞系統(請參閱 “[訊息傳遞中的資料流](/tw/ch4#訊息傳遞中的資料流)”)也具有響應事件的概念。早在 20 世紀 80 年代,**元組空間(tuple space)** 模型就已經探索了表達分散式計算的方式:觀察狀態變更並作出反應的過程【38,39】。
+我們在 “[資料庫與流](/tw/ch12#sec_stream_databases)” 中看到了這一思路,我們討論了將資料庫的變更日誌視為一種我們可以訂閱的事件流。諸如 Actor 的訊息傳遞系統(請參閱 “[訊息傳遞中的資料流](/tw/ch5#sec_encoding_dataflow_msg)”)也具有響應事件的概念。早在 20 世紀 80 年代,**元組空間(tuple space)** 模型就已經探索了表達分散式計算的方式:觀察狀態變更並作出反應的過程[^38][^39]。
如前所述,當觸發器由於資料變更而被觸發時,或次級索引更新以反映索引表中的變更時,資料庫內部也發生著類似的情況。分拆資料庫意味著將這個想法應用於在主資料庫之外,用於建立派生資料集:快取、全文檢索索引、機器學習或分析系統。我們可以為此使用流處理和訊息傳遞系統。
-需要記住的重要一點是,維護派生資料不同於執行非同步任務。傳統的訊息傳遞系統通常是為執行非同步任務設計的(請參閱 “[日誌與傳統的訊息傳遞相比](/tw/ch12#日誌與傳統的訊息傳遞相比)”):
+需要記住的重要一點是,維護派生資料不同於執行非同步任務。傳統的訊息傳遞系統通常是為執行非同步任務設計的(請參閱 “[日誌與傳統的訊息傳遞相比](/tw/ch12#sec_stream_logs_vs_messaging)”):
-* 在維護派生資料時,狀態變更的順序通常很重要(如果多個檢視是從事件日誌派生的,則需要按照相同的順序處理事件,以便它們之間保持一致)。如 “[確認與重新傳遞](/tw/ch12#確認與重新傳遞)” 中所述,許多訊息代理在重傳未確認訊息時沒有此屬性,雙寫也被排除在外(請參閱 “[保持系統同步](/tw/ch12#保持系統同步)”)。
+* 在維護派生資料時,狀態變更的順序通常很重要(如果多個檢視是從事件日誌派生的,則需要按照相同的順序處理事件,以便它們之間保持一致)。如 “[確認與重新傳遞](/tw/ch12#sec_stream_reordering)” 中所述,許多訊息代理在重傳未確認訊息時沒有此屬性,雙寫也被排除在外(請參閱 “[保持系統同步](/tw/ch12#sec_stream_sync)”)。
* 容錯是派生資料的關鍵:僅僅丟失單個訊息就會導致派生資料集永遠與其資料來源失去同步。訊息傳遞和派生狀態更新都必須可靠。例如,許多 Actor 系統預設在記憶體中維護 Actor 的狀態和訊息,所以如果執行 Actor 的機器崩潰,狀態和訊息就會丟失。
穩定的訊息排序和容錯訊息處理是相當嚴格的要求,但與分散式事務相比,它們開銷更小,執行更穩定。現代流處理元件可以提供這些排序和可靠性保證,並允許應用程式碼以流運算元的形式執行。
這些應用程式碼可以執行任意處理,包括資料庫內建派生函式通常不提供的功能。就像透過管道連結的 Unix 工具一樣,流運算元可以圍繞著資料流構建大型系統。每個運算元接受狀態變更的流作為輸入,併產生其他狀態變化的流作為輸出。
-#### 流處理器和服務
+#### 流處理器和服務 {#id345}
-當今流行的應用開發風格涉及將功能分解為一組透過同步網路請求(如 REST API)進行通訊的 **服務**(service,請參閱 “[服務中的資料流:REST 與 RPC](/tw/ch4#服務中的資料流:REST與RPC)”)。這種面向服務的架構優於單一龐大應用的優勢主要在於:通過鬆散耦合來提供組織上的可伸縮性:不同的團隊可以專職於不同的服務上,從而減少團隊之間的協調工作(因為服務可以獨立部署和更新)。
+當今流行的應用開發風格涉及將功能分解為一組透過同步網路請求(如 REST API)進行通訊的 **服務**(service,請參閱 “[服務中的資料流:REST 與 RPC](/tw/ch5#sec_encoding_dataflow_rpc)”)。這種面向服務的架構優於單一龐大應用的優勢主要在於:通過鬆散耦合來提供組織上的可伸縮性:不同的團隊可以專職於不同的服務上,從而減少團隊之間的協調工作(因為服務可以獨立部署和更新)。
-在資料流中組裝流運算元與微服務方法有很多相似之處【40】。但底層通訊機制是有很大區別:資料流採用單向非同步訊息流,而不是同步的請求 / 響應式互動。
+在資料流中組裝流運算元與微服務方法有很多相似之處[^40]。但底層通訊機制是有很大區別:資料流採用單向非同步訊息流,而不是同步的請求 / 響應式互動。
-除了在 “[訊息傳遞中的資料流](/tw/ch4#訊息傳遞中的資料流)” 中列出的優點(如更好的容錯性),資料流系統還能實現更好的效能。例如,假設客戶正在購買以一種貨幣定價,但以另一種貨幣支付的商品。為了執行貨幣換算,你需要知道當前的匯率。這個操作可以透過兩種方式實現【40,41】:
+除了在 “[訊息傳遞中的資料流](/tw/ch5#sec_encoding_dataflow_msg)” 中列出的優點(如更好的容錯性),資料流系統還能實現更好的效能。例如,假設客戶正在購買以一種貨幣定價,但以另一種貨幣支付的商品。為了執行貨幣換算,你需要知道當前的匯率。這個操作可以透過兩種方式實現[^40][^41]:
1. 在微服務方法中,處理購買的程式碼可能會查詢匯率服務或資料庫,以獲取特定貨幣的當前匯率。
2. 在資料流方法中,處理訂單的程式碼會提前訂閱匯率變更流,並在匯率發生變動時將當前匯率儲存在本地資料庫中。處理訂單時只需查詢本地資料庫即可。
-第二種方法能將對另一服務的同步網路請求替換為對本地資料庫的查詢(可能在同一臺機器甚至同一個程序中)[^ii]。資料流方法不僅更快,而且當其他服務失效時也更穩健。最快且最可靠的網路請求就是壓根沒有網路請求!我們現在不再使用 RPC,而是在購買事件和匯率更新事件之間建立流聯接(請參閱 “[流表連線(流擴充)](/tw/ch12#流表連線(流擴充))”)。
+第二種方法能將對另一服務的同步網路請求替換為對本地資料庫的查詢(可能在同一臺機器甚至同一個程序中)。資料流方法不僅更快,而且當其他服務失效時也更穩健。最快且最可靠的網路請求就是壓根沒有網路請求!我們現在不再使用 RPC,而是在購買事件和匯率更新事件之間建立流聯接(請參閱 “[流表連線(流擴充)](/tw/ch12#sec_stream_table_joins)”)。
-[^ii]: 在微服務方法中,你也可以透過在處理購買的服務中本地快取匯率來避免同步網路請求。但是為了保證快取的新鮮度,你需要定期輪詢匯率以獲取其更新,或訂閱變更流 —— 這恰好是資料流方法中發生的事情。
-
-連線是時間相關的:如果購買事件在稍後的時間點被重新處理,匯率可能已經改變。如果要重建原始輸出,則需要獲取原始購買時的歷史匯率。無論是查詢服務還是訂閱匯率更新流,你都需要處理這種時間相關性(請參閱 “[連線的時間依賴性](/tw/ch12#連線的時間依賴性)”)。
+連線是時間相關的:如果購買事件在稍後的時間點被重新處理,匯率可能已經改變。如果要重建原始輸出,則需要獲取原始購買時的歷史匯率。無論是查詢服務還是訂閱匯率更新流,你都需要處理這種時間相關性(請參閱 “[連線的時間依賴性](/tw/ch12#sec_stream_join_time)”)。
訂閱變更流,而不是在需要時查詢當前狀態,使我們更接近類似電子表格的計算模型:當某些資料發生變更時,依賴於此的所有派生資料都可以快速更新。還有很多未解決的問題,例如關於時間相關連線等問題,但我認為圍繞資料流構建應用的想法是一個非常有希望的方向。
-### 觀察派生資料狀態
+### 觀察派生資料狀態 {#sec_future_observing}
-在抽象層面,上一節討論的資料流系統提供了建立派生資料集(例如搜尋索引、物化檢視和預測模型)並使其保持更新的過程。我們將這個過程稱為 **寫路徑(write path)**:只要某些資訊被寫入系統,它可能會經歷批處理與流處理的多個階段,而最終每個派生資料集都會被更新,以適配寫入的資料。[圖 12-1](/v1/ddia_1201.png) 顯示了一個更新搜尋索引的例子。
+在抽象層面,上一節討論的資料流系統給出了建立並維護派生資料集(如搜尋索引、物化檢視、預測模型)的過程。我們把這稱為 **寫路徑(write path)**:當資訊寫入系統後,它可能經過多個批處理與流處理階段,最終所有相關派生資料集都會被更新。[圖 13-1](/tw/ch13#fig_future_write_read_paths) 展示了搜尋索引更新的例子。
-
-
-**圖 12-1 在搜尋索引中,寫(文件更新)遇上讀(查詢)**
+{{< figure src="/fig/ddia_1301.png" id="fig_future_write_read_paths" caption="圖 13-1 在搜尋索引中,寫入(文件更新)與讀取(查詢)相遇。" class="w-full my-4" >}}
但你為什麼一開始就要建立派生資料集?很可能是因為你想在以後再次查詢它。這就是 **讀路徑(read path)**:當服務使用者請求時,你需要從派生資料集中讀取,也許還要對結果進行一些額外處理,然後構建給使用者的響應。
總而言之,寫路徑和讀路徑涵蓋了資料的整個旅程,從收集資料開始,到使用資料結束(可能是由另一個人)。寫路徑是預計算過程的一部分 —— 即,一旦資料進入,即刻完成,無論是否有人需要看它。讀路徑是這個過程中只有當有人請求時才會發生的部分。如果你熟悉函數語言程式設計語言,則可能會注意到寫路徑類似於立即求值,讀路徑類似於惰性求值。
-如 [圖 12-1](/v1/ddia_1201.png) 所示所示,派生資料集是寫路徑和讀路徑相遇的地方。它代表了在寫入時需要完成的工作量與在讀取時需要完成的工作量之間的權衡。
+如 [圖 13-1](/tw/ch13#fig_future_write_read_paths) 所示,派生資料集是寫路徑和讀路徑相遇的地方。它代表了寫入時工作量與讀取時工作量之間的權衡。
-#### 物化檢視和快取
+#### 物化檢視和快取 {#id451}
全文檢索索引就是一個很好的例子:寫路徑更新索引,讀路徑在索引中搜索關鍵字。讀寫都需要做一些工作。寫入需要更新文件中出現的所有關鍵詞的索引條目。讀取需要搜尋查詢中的每個單詞,並應用布林邏輯來查詢包含查詢中所有單詞(AND 運算子)的文件,或者每個單詞(OR 運算子)的任何同義詞。
如果沒有索引,搜尋查詢將不得不掃描所有文件(如 grep),如果有著大量文件,這樣做的開銷巨大。沒有索引意味著寫入路徑上的工作量較少(沒有要更新的索引),但是在讀取路徑上需要更多工作。
-另一方面,可以想象為所有可能的查詢預先計算搜尋結果。在這種情況下,讀路徑上的工作量會減少:不需要布林邏輯,只需查詢查詢結果並返回即可。但寫路徑會更加昂貴:可能的搜尋查詢集合是無限大的,因此預先計算所有可能的搜尋結果將需要無限的時間和儲存空間。那肯定沒戲 [^iii]。
-
-[^iii]: 假設一個有限的語料庫,那麼返回非空搜尋結果的搜尋查詢集合是有限的。然而,它是與語料庫中的術語數量呈指數關係,這仍是一個壞訊息。
+另一方面,可以想象為所有可能的查詢預先計算搜尋結果。在這種情況下,讀路徑上的工作量會減少:不需要布林邏輯,只需查詢查詢結果並返回即可。但寫路徑會更加昂貴:可能的搜尋查詢集合是無限大的,因此預先計算所有可能的搜尋結果將需要無限的時間和儲存空間,這在實踐中不可行。
另一種選擇是預先計算一組固定的最常見查詢的搜尋結果,以便可以快速提供它們而無需轉到索引。不常見的查詢仍然可以透過索引來提供服務。這通常被稱為常見查詢的 **快取(cache)**,儘管我們也可以稱之為 **物化檢視(materialized view)**,因為當新文件出現,且需要被包含在這些常見查詢的搜尋結果之中時,這些索引就需要更新。
從這個例子中我們可以看到,索引不是寫路徑和讀路徑之間唯一可能的邊界;快取常見搜尋結果也是可行的;而在少量文件上使用沒有索引的類 grep 掃描也是可行的。由此來看,快取,索引和物化檢視的作用很簡單:它們改變了讀路徑與寫路徑之間的邊界。透過預先計算結果,從而允許我們在寫路徑上做更多的工作,以節省讀路徑上的工作量。
-在寫路徑上完成的工作和讀路徑之間的界限,實際上是本書開始處在 “[描述負載](/tw/ch1#描述負載)” 中推特例子裡談到的主題。在該例中,我們還看到了與普通使用者相比,名人的寫路徑和讀路徑可能有所不同。在 500 頁之後,我們已經繞回了起點!
+在寫路徑上完成的工作和讀路徑之間的界限,實際上是本書開始處在 “[描述負載](/tw/ch2#sec_introduction_twitter)” 中推特例子裡談到的主題。在該例中,我們還看到了與普通使用者相比,名人的寫路徑和讀路徑可能有所不同。在 500 頁之後,我們已經繞回了起點!
-#### 有狀態、可離線的客戶端
+#### 有狀態、可離線的客戶端 {#id347}
我發現寫路徑和讀路徑之間的邊界很有趣,因為我們可以試著改變這個邊界,並探討這種改變的實際意義。我們來看看不同上下文中的這一想法。
@@ -361,98 +317,100 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni
傳統上,網路瀏覽器是無狀態的客戶端,只有當連線到網際網路時才能做一些有用的事情(能離線執行的唯一事情基本上就是上下滾動之前線上時載入好的頁面)。然而,最近的 “單頁面” JavaScript Web 應用已經獲得了很多有狀態的功能,包括客戶端使用者介面互動,以及 Web 瀏覽器中的持久化本地儲存。移動應用可以類似地在裝置上儲存大量狀態,而且大多數使用者互動都不需要與伺服器往返互動。
-這些不斷變化的功能重新引發了對 **離線優先(offline-first)** 應用的興趣,這些應用盡可能地在同一裝置上使用本地資料庫,無需連線網際網路,並在後臺網路連線可用時與遠端伺服器同步【42】。由於移動裝置通常具有緩慢且不可靠的蜂窩網路連線,因此,如果使用者的使用者介面不必等待同步網路請求,且應用主要是離線工作的,則這是一個巨大優勢(請參閱 “[需要離線操作的客戶端](/tw/ch5#需要離線操作的客戶端)”)。
+這些不斷變化的功能重新引發了對 **離線優先(offline-first)** 應用的興趣,這些應用盡可能地在同一裝置上使用本地資料庫,無需連線網際網路,並在後臺網路連線可用時與遠端伺服器同步[^42]。由於移動裝置通常具有緩慢且不可靠的蜂窩網路連線,因此,如果使用者的使用者介面不必等待同步網路請求,且應用主要是離線工作的,則這是一個巨大優勢(請參閱 “[需要離線操作的客戶端](/tw/ch6#sec_replication_offline_clients)”)。
-當我們擺脫無狀態客戶端與中央資料庫互動的假設,並轉向在終端使用者裝置上維護狀態時,這就開啟了新世界的大門。特別是,我們可以將裝置上的狀態視為 **伺服器狀態的快取**。螢幕上的畫素是客戶端應用中模型物件的物化檢視;模型物件是遠端資料中心的本地狀態副本【27】。
+當我們擺脫無狀態客戶端與中央資料庫互動的假設,並轉向在終端使用者裝置上維護狀態時,這就開啟了新世界的大門。特別是,我們可以將裝置上的狀態視為 **伺服器狀態的快取**。螢幕上的畫素是客戶端應用中模型物件的物化檢視;模型物件是遠端資料中心的本地狀態副本[^27]。
-#### 將狀態變更推送給客戶端
+#### 將狀態變更推送給客戶端 {#id348}
在典型的網頁中,如果你在 Web 瀏覽器中載入頁面,並且隨後伺服器上的資料發生變更,則瀏覽器在重新載入頁面之前對此一無所知。瀏覽器只能在一個時間點讀取資料,假設它是靜態的 —— 它不會訂閱來自伺服器的更新。因此裝置上的狀態是陳舊的快取,除非你顯式輪詢變更否則不會更新。(像 RSS 這樣基於 HTTP 的 Feed 訂閱協議實際上只是一種基本的輪詢形式)
最近的協議已經超越了 HTTP 的基本請求 / 響應模式:服務端傳送的事件(EventSource API)和 WebSockets 提供了通訊通道,透過這些通道,Web 瀏覽器可以與伺服器保持開啟的 TCP 連線,只要瀏覽器仍然連線著,伺服器就能主動向瀏覽器推送資訊。這為伺服器提供了主動通知終端使用者客戶端的機會,伺服器能告知客戶端其本地儲存狀態的任何變化,從而減少客戶端狀態的陳舊程度。
-用我們的寫路徑與讀路徑模型來講,主動將狀態變更推至到客戶端裝置,意味著將寫路徑一直延伸到終端使用者。當客戶端首次初始化時,它仍然需要使用讀路徑來獲取其初始狀態,但此後它就能夠依賴伺服器傳送的狀態變更流了。我們在流處理和訊息傳遞部分討論的想法並不侷限於資料中心中:我們可以進一步採納這些想法,並將它們一直延伸到終端使用者裝置【43】。
+用我們的寫路徑與讀路徑模型來講,主動將狀態變更推至到客戶端裝置,意味著將寫路徑一直延伸到終端使用者。當客戶端首次初始化時,它仍然需要使用讀路徑來獲取其初始狀態,但此後它就能夠依賴伺服器傳送的狀態變更流了。我們在流處理和訊息傳遞部分討論的想法並不侷限於資料中心中:我們可以進一步採納這些想法,並將它們一直延伸到終端使用者裝置[^43]。
-這些裝置有時會離線,並在此期間無法收到伺服器狀態變更的任何通知。但是我們已經解決了這個問題:在 “[消費者偏移量](/tw/ch12#消費者偏移量)” 中,我們討論了基於日誌的訊息代理的消費者能在失敗或斷開連線後重連,並確保它不會錯過掉線期間任何到達的訊息。同樣的技術適用於單個使用者,每個裝置都是一個小事件流的小小訂閱者。
+這些裝置有時會離線,並在此期間無法收到伺服器狀態變更的任何通知。但是我們已經解決了這個問題:在 “[消費者偏移量](/tw/ch12#sec_stream_log_offsets)” 中,我們討論了基於日誌的訊息代理的消費者能在失敗或斷開連線後重連,並確保它不會錯過掉線期間任何到達的訊息。同樣的技術適用於單個使用者,每個裝置都是一個小事件流的小小訂閱者。
-#### 端到端的事件流
+#### 端到端的事件流 {#id349}
-最近用於開發有狀態的客戶端與使用者介面的工具,例如如 Elm 語言【30】和 Facebook 的 React、Flux 和 Redux 工具鏈,已經透過訂閱表示使用者輸入或伺服器響應的事件流來管理客戶端的內部狀態,其結構與事件溯源相似(請參閱 “[事件溯源](/tw/ch12#事件溯源)”)。
+最近用於開發有狀態的客戶端與使用者介面的工具,例如如 Elm 語言[^30]和 Facebook 的 React、Flux 和 Redux 工具鏈,已經透過訂閱表示使用者輸入或伺服器響應的事件流來管理客戶端的內部狀態,其結構與事件溯源相似(請參閱 “[事件溯源](/tw/ch12#sec_stream_event_sourcing)”)。
將這種程式設計模型擴充套件為:允許伺服器將狀態變更事件推送到客戶端的事件管道中,是非常自然的。因此,狀態變化可以透過 **端到端(end-to-end)** 的寫路徑流動:從一個裝置上的互動觸發狀態變更開始,經由事件日誌,並穿過幾個派生資料系統與流處理器,一直到另一臺裝置上的使用者介面,而有人正在觀察使用者介面上的狀態變化。這些狀態變化能以相當低的延遲傳播 —— 比如說,在一秒內從一端到另一端。
-一些應用(如即時訊息傳遞與線上遊戲)已經具有這種 “即時” 架構(在低延遲互動的意義上,不是在 “[響應時間保證](/tw/ch8#響應時間保證)” 中的意義上)。但我們為什麼不用這種方式構建所有的應用?
+一些應用(如即時訊息傳遞與線上遊戲)已經具有這種 “即時” 架構(在低延遲互動的意義上,不是在 “[響應時間保證](/tw/ch9#sec_distributed_clocks_realtime)” 中的意義上)。但我們為什麼不用這種方式構建所有的應用?
-挑戰在於,關於無狀態客戶端和請求 / 響應互動的假設已經根深蒂固地植入在我們的資料庫、庫、框架以及協議之中。許多資料儲存支援讀取與寫入操作,為請求返回一個響應,但只有極少數提供訂閱變更的能力 —— 請求返回一個隨時間推移的響應流(請參閱 “[變更流的 API 支援](/tw/ch12#變更流的API支援)” )。
+挑戰在於,關於無狀態客戶端和請求 / 響應互動的假設已經根深蒂固地植入在我們的資料庫、庫、框架以及協議之中。許多資料儲存支援讀取與寫入操作,為請求返回一個響應,但只有極少數提供訂閱變更的能力 —— 請求返回一個隨時間推移的響應流(請參閱 “[變更流的 API 支援](/tw/ch12#sec_stream_change_api)” )。
-為了將寫路徑延伸至終端使用者,我們需要從根本上重新思考我們構建這些系統的方式:從請求 / 響應互動轉向釋出 / 訂閱資料流【27】。更具響應性的使用者介面與更好的離線支援,我認為這些優勢值得我們付出努力。如果你正在設計資料系統,我希望你對訂閱變更的選項留有印象,而不只是查詢當前狀態。
+為了將寫路徑延伸至終端使用者,我們需要從根本上重新思考我們構建這些系統的方式:從請求 / 響應互動轉向釋出 / 訂閱資料流[^27]。更具響應性的使用者介面與更好的離線支援,我認為這些優勢值得我們付出努力。如果你正在設計資料系統,我希望你對訂閱變更的選項留有印象,而不只是查詢當前狀態。
-#### 讀也是事件
+#### 讀也是事件 {#sec_future_read_events}
我們討論過,當流處理器將派生資料寫入儲存(資料庫,快取或索引)時,以及當用戶請求查詢該儲存時,儲存將充當寫路徑和讀路徑之間的邊界。該儲存應當允許對資料進行隨機訪問的讀取查詢,否則這些查詢將需要掃描整個事件日誌。
-在很多情況下,資料儲存與流處理系統是分開的。但回想一下,流處理器還是需要維護狀態以執行聚合和連線的(請參閱 “[流連線](/tw/ch12#流連線)”)。這種狀態通常隱藏在流處理器內部,但一些框架也允許這些狀態被外部客戶端查詢【45】,將流處理器本身變成一種簡單的資料庫。
+在很多情況下,資料儲存與流處理系統是分開的。但回想一下,流處理器還是需要維護狀態以執行聚合和連線的(請參閱 “[流連線](/tw/ch12#sec_stream_joins)”)。這種狀態通常隱藏在流處理器內部,但一些框架也允許這些狀態被外部客戶端查詢[^45],將流處理器本身變成一種簡單的資料庫。
-我願意進一步思考這個想法。正如到目前為止所討論的那樣,對儲存的寫入是透過事件日誌進行的,而讀取是臨時的網路請求,直接流向儲存著待查資料的節點。這是一個合理的設計,但不是唯一可行的設計。也可以將讀取請求表示為事件流,並同時將讀事件與寫事件送往流處理器;流處理器透過將讀取結果傳送到輸出流來響應讀取事件【46】。
+我願意進一步思考這個想法。正如到目前為止所討論的那樣,對儲存的寫入是透過事件日誌進行的,而讀取是臨時的網路請求,直接流向儲存著待查資料的節點。這是一個合理的設計,但不是唯一可行的設計。也可以將讀取請求表示為事件流,並同時將讀事件與寫事件送往流處理器;流處理器透過將讀取結果傳送到輸出流來響應讀取事件[^46]。
-當寫入和讀取都被表示為事件,並且被路由到同一個流運算元以便處理時,我們實際上是在讀取查詢流和資料庫之間執行流表連線。讀取事件需要被送往儲存資料的資料庫分割槽(請參閱 “[請求路由](/tw/ch6#請求路由)”),就像批處理和流處理器在連線時需要在同一個鍵上對輸入分割槽一樣(請參閱 “[Reduce 側連線與分組](/tw/ch11#Reduce側連線與分組)”)。
+當寫入和讀取都被表示為事件,並且被路由到同一個流運算元以便處理時,我們實際上是在讀取查詢流和資料庫之間執行流表連線。讀取事件需要被送往儲存資料的資料庫分割槽(請參閱 “[請求路由](/tw/ch7#sec_sharding_routing)”),就像批處理和流處理器在連線時需要在同一個鍵上對輸入分割槽一樣(請參閱 “[Reduce 側連線與分組](/tw/ch11#sec_batch_join)”)。
-服務請求與執行連線之間的這種相似之處是非常關鍵的【47】。一次性讀取請求只是將請求傳過連線運算元,然後請求馬上就被忘掉了;而一個訂閱請求,則是與連線另一側過去與未來事件的持久化連線。
+服務請求與執行連線之間的這種相似之處是非常關鍵的[^47]。一次性讀取請求只是將請求傳過連線運算元,然後請求馬上就被忘掉了;而一個訂閱請求,則是與連線另一側過去與未來事件的持久化連線。
-記錄讀取事件的日誌可能對於追蹤整個系統中的因果關係與資料來源也有好處:它可以讓你重現出當用戶做出特定決策之前看見了什麼。例如在網商中,向客戶顯示的預測送達日期與庫存狀態,可能會影響他們是否選擇購買一件商品【4】。要分析這種聯絡,則需要記錄使用者查詢運輸與庫存狀態的結果。
+記錄讀取事件的日誌可能對於追蹤整個系統中的因果關係與資料來源也有好處:它可以讓你重現出當用戶做出特定決策之前看見了什麼。例如在網商中,向客戶顯示的預測送達日期與庫存狀態,可能會影響他們是否選擇購買一件商品[^4]。要分析這種聯絡,則需要記錄使用者查詢運輸與庫存狀態的結果。
-將讀取事件寫入持久儲存可以更好地跟蹤因果關係(請參閱 “[排序事件以捕獲因果關係](#排序事件以捕獲因果關係)”),但會產生額外的儲存與 I/O 成本。最佳化這些系統以減少開銷仍然是一個開放的研究問題【2】。但如果你已經出於運維目的留下了讀取請求日誌,將其作為請求處理的副作用,那麼將這份日誌作為請求事件源並不是什麼特別大的變更。
+將讀取事件寫入持久儲存可以更好地跟蹤因果關係(請參閱 “[排序事件以捕獲因果關係](#排序事件以捕獲因果關係)”),但會產生額外的儲存與 I/O 成本。最佳化這些系統以減少開銷仍然是一個開放的研究問題[^2]。但如果你已經出於運維目的留下了讀取請求日誌,將其作為請求處理的副作用,那麼將這份日誌作為請求事件源並不是什麼特別大的變更。
-#### 多分割槽資料處理
+#### 多分割槽資料處理 {#sec_future_unbundled_multi_shard}
對於只涉及單個分割槽的查詢,透過流來發送查詢與收集響應可能是殺雞用牛刀了。然而,這個想法開啟了分散式執行複雜查詢的可能性,這需要合併來自多個分割槽的資料,利用了流處理器已經提供的訊息路由、分割槽和連線的基礎設施。
-Storm 的分散式 RPC 功能支援這種使用模式(請參閱 “[訊息傳遞和 RPC](/tw/ch12#訊息傳遞和RPC)”)。例如,它已經被用來計算瀏覽過某個推特 URL 的人數 —— 即,發推包含該 URL 的所有人的粉絲集合的並集【48】。由於推特的使用者是分割槽的,因此這種計算需要合併來自多個分割槽的結果。
+Storm 的分散式 RPC 功能支援這種使用模式(請參閱 “[訊息傳遞和 RPC](/tw/ch12#sec_stream_actors_drpc)”)。例如,它已經被用來計算瀏覽過某個推特 URL 的人數 —— 即,發推包含該 URL 的所有人的粉絲集合的並集[^48]。由於推特的使用者是分割槽的,因此這種計算需要合併來自多個分割槽的結果。
-這種模式的另一個例子是欺詐預防:為了評估特定購買事件是否具有欺詐風險,你可以檢查該使用者 IP 地址,電子郵件地址,帳單地址,送貨地址的信用分。這些信用資料庫中的每一個都是有分割槽的,因此為特定購買事件採集分數需要連線一系列不同的分割槽資料集【49】。
+這種模式的另一個例子是欺詐預防:為了評估特定購買事件是否具有欺詐風險,你可以檢查該使用者 IP 地址,電子郵件地址,帳單地址,送貨地址的信用分。這些信用資料庫中的每一個都是有分割槽的,因此為特定購買事件採集分數需要連線一系列不同的分割槽資料集[^49]。
-MPP 資料庫的內部查詢執行圖有著類似的特徵(請參閱 “[Hadoop 與分散式資料庫的對比](/tw/ch11#Hadoop與分散式資料庫的對比)”)。如果需要執行這種多分割槽連線,則直接使用提供此功能的資料庫,可能要比使用流處理器實現它要更簡單。然而將查詢視為流提供了一種選項,可以用於實現超出傳統現成解決方案的大規模應用。
+MPP 資料庫的內部查詢執行圖有著類似的特徵(請參閱 “[Hadoop 與分散式資料庫的對比](/tw/ch11#sec_batch_distributed)”)。如果需要執行這種多分割槽連線,則直接使用提供此功能的資料庫,可能要比使用流處理器實現它要更簡單。然而將查詢視為流提供了一種選項,可以用於實現超出傳統現成解決方案的大規模應用。
-## 將事情做正確
+## 追求正確性 {#sec_future_correctness}
-對於只讀取資料的無狀態服務,出問題也沒什麼大不了的:你可以修復該錯誤並重啟服務,而一切都恢復正常。像資料庫這樣的有狀態系統就沒那麼簡單了:它們被設計為永遠記住事物(或多或少),所以如果出現問題,這種(錯誤的)效果也將潛在地永遠持續下去,這意味著它們需要更仔細的思考【50】。
+對於只讀取資料的無狀態服務,出問題也沒什麼大不了的:你可以修復該錯誤並重啟服務,而一切都恢復正常。像資料庫這樣的有狀態系統就沒那麼簡單了:它們被設計為永遠記住事物(或多或少),所以如果出現問題,這種(錯誤的)效果也將潛在地永遠持續下去,這意味著它們需要更仔細的思考[^50]。
-我們希望構建可靠且 **正確** 的應用(即使面對各種故障,程式的語義也能被很好地定義與理解)。約四十年來,原子性、隔離性和永續性([第七章](/tw/ch7))等事務特性一直是構建正確應用的首選工具。然而這些地基沒有看上去那麼牢固:例如弱隔離級別帶來的困惑可以佐證(請參閱 “[弱隔離級別](/tw/ch7#弱隔離級別)”)。
+我們希望構建可靠且 **正確** 的應用(即使面對各種故障,程式的語義也能被很好地定義與理解)。約四十年來,原子性、隔離性和永續性([第八章](/tw/ch8))等事務特性一直是構建正確應用的首選工具。然而這些地基沒有看上去那麼牢固:例如弱隔離級別帶來的困惑可以佐證(請參閱 “[弱隔離級別](/tw/ch8#sec_transactions_isolation_levels)”)。
-事務在某些領域被完全拋棄,並被提供更好效能與可伸縮性的模型取代,但後者有更複雜的語義(例如,請參閱 “[無主複製](/tw/ch5#無主複製)”)。**一致性(Consistency)** 經常被談起,但其定義並不明確(請參閱 “[一致性](/tw/ch7#一致性)” 和 [第九章](/tw/ch9))。有些人斷言我們應當為了高可用而 “擁抱弱一致性”,但卻對這些概念實際上意味著什麼缺乏清晰的認識。
+事務在某些領域被完全拋棄,並被提供更好效能與可伸縮性的模型取代,但後者有更複雜的語義(例如,請參閱 “[無主複製](/tw/ch6#sec_replication_leaderless)”)。**一致性(Consistency)** 經常被談起,但其定義並不明確(請參閱 “[一致性](/tw/ch8#sec_transactions_acid_consistency)” 和 [第十章](/tw/ch10))。有些人斷言我們應當為了高可用而 “擁抱弱一致性”,但卻對這些概念實際上意味著什麼缺乏清晰的認識。
-對於如此重要的話題,我們的理解,以及我們的工程方法卻是驚人地薄弱。例如,確定在特定事務隔離等級或複製配置下執行特定應用是否安全是非常困難的【51,52】。通常簡單的解決方案似乎在低併發性的情況下工作正常,並且沒有錯誤,但在要求更高的情況下卻會出現許多微妙的錯誤。
+對於如此重要的話題,我們的理解,以及我們的工程方法卻是驚人地薄弱。例如,確定在特定事務隔離等級或複製配置下執行特定應用是否安全是非常困難的[^51][^52]。通常簡單的解決方案似乎在低併發性的情況下工作正常,並且沒有錯誤,但在要求更高的情況下卻會出現許多微妙的錯誤。
-例如,Kyle Kingsbury 的 Jepsen 實驗【53】標出了一些產品聲稱的安全保證與其在網路問題與崩潰時的實際行為之間的明顯差異。即使像資料庫這樣的基礎設施產品沒有問題,應用程式碼仍然需要正確使用它們提供的功能才行,如果配置很難理解,這是很容易出錯的(在這種情況下指的是弱隔離級別,法定人數配置等)。
+例如,Kyle Kingsbury 的 Jepsen 實驗[^53]標出了一些產品聲稱的安全保證與其在網路問題與崩潰時的實際行為之間的明顯差異。即使像資料庫這樣的基礎設施產品沒有問題,應用程式碼仍然需要正確使用它們提供的功能才行,如果配置很難理解,這是很容易出錯的(在這種情況下指的是弱隔離級別,法定人數配置等)。
如果你的應用可以容忍偶爾的崩潰,以及以不可預料的方式損壞或丟失資料,那生活就要簡單得多,而你可能只要雙手合十念阿彌陀佛,期望佛祖能保佑最好的結果。另一方面,如果你需要更強的正確性保證,那麼可序列化與原子提交就是久經考驗的方法,但它們是有代價的:它們通常只在單個數據中心中工作(這就排除了地理位置分散的架構),並限制了系統能夠實現的規模與容錯特性。
雖然傳統的事務方法並沒有走遠,但我也相信在使應用正確而靈活地處理錯誤方面上,事務也不是最後一個可以談的。在本節中,我將提出一些在資料流架構中考量正確性的方式。
-### 資料庫的端到端原則
+### 資料庫的端到端原則 {#sec_future_end_to_end}
僅僅因為一個應用程式使用了具有相對較強安全屬性的資料系統(例如可序列化的事務),並不意味著就可以保證沒有資料丟失或損壞。例如,如果某個應用有個 Bug,導致它寫入不正確的資料,或者從資料庫中刪除資料,那麼可序列化的事務也救不了你。
-這個例子可能看起來很無聊,但值得認真對待:應用會出 Bug,而人也會犯錯誤。我在 “[狀態、流和不變性](/tw/ch12#狀態、流和不變性)” 中使用了這個例子來支援不可變和僅追加的資料,閹割掉錯誤程式碼摧毀良好資料的能力,能讓從錯誤中恢復更為容易。
+這個例子可能看起來很無聊,但值得認真對待:應用會出 Bug,而人也會犯錯誤。我在 “[狀態、流和不變性](/tw/ch12#sec_stream_immutability)” 中使用了這個例子來支援不可變和僅追加的資料,閹割掉錯誤程式碼摧毀良好資料的能力,能讓從錯誤中恢復更為容易。
雖然不變性很有用,但它本身並非萬靈藥。讓我們來看一個可能發生的、非常微妙的資料損壞案例。
-#### 正好執行一次操作
+#### 恰好執行一次操作 {#id353}
-在 “[容錯](/tw/ch12#容錯)” 中,我們見到了 **恰好一次**(或 **等效一次**)語義的概念。如果在處理訊息時出現問題,你可以選擇放棄(丟棄訊息 —— 導致資料丟失)或重試。如果重試,就會有這種風險:第一次實際上成功了,只不過你沒有發現。結果這個訊息就被處理了兩次。
+在 “[容錯](/tw/ch12#sec_stream_fault_tolerance)” 中,我們見到了 **恰好一次**(或 **等效一次**)語義的概念。如果在處理訊息時出現問題,你可以選擇放棄(丟棄訊息 —— 導致資料丟失)或重試。如果重試,就會有這種風險:第一次實際上成功了,只不過你沒有發現。結果這個訊息就被處理了兩次。
處理兩次是資料損壞的一種形式:為同樣的服務向客戶收費兩次(收費太多)或增長計數器兩次(誇大指標)都不是我們想要的。在這種情況下,恰好一次意味著安排計算,使得最終效果與沒有發生錯誤的情況一樣,即使操作實際上因為某種錯誤而重試。我們先前討論過實現這一目標的幾種方法。
-最有效的方法之一是使操作 **冪等**(idempotent,請參閱 “[冪等性](/tw/ch12#冪等性)”):即確保它無論是執行一次還是執行多次都具有相同的效果。但是,將不是天生冪等的操作變為冪等的操作需要一些額外的努力與關注:你可能需要維護一些額外的元資料(例如更新了值的操作 ID 集合),並在從一個節點故障切換至另一個節點時做好防護(請參閱 “[領導者和鎖](/tw/ch8#領導者和鎖)”)。
+最有效的方法之一是使操作 **冪等**(idempotent,請參閱 “[冪等性](/tw/ch12#sec_stream_idempotence)”):即確保它無論是執行一次還是執行多次都具有相同的效果。但是,將不是天生冪等的操作變為冪等的操作需要一些額外的努力與關注:你可能需要維護一些額外的元資料(例如更新了值的操作 ID 集合),並在從一個節點故障切換至另一個節點時做好防護(請參閱 “[領導者和鎖](/tw/ch9#sec_distributed_lock_fencing)”)。
-#### 抑制重複
+#### 抑制重複 {#id354}
除了流處理之外,其他許多地方也需要抑制重複的模式。例如,TCP 使用了資料包上的序列號,以便接收方可以將它們正確排序,並確定網路上是否有資料包丟失或重複。在將資料交付應用前,TCP 協議棧會重新傳輸任何丟失的資料包,也會移除任何重複的資料包。
-但是,這種重複抑制僅適用於單條 TCP 連線的場景中。假設 TCP 連線是一個客戶端與資料庫的連線,並且它正在執行 [例 12-1]() 中的事務。在許多資料庫中,事務是繫結在客戶端連線上的(如果客戶端傳送了多個查詢,資料庫就知道它們屬於同一個事務,因為它們是在同一個 TCP 連線上傳送的)。如果客戶端在傳送 `COMMIT` 之後並在從資料庫伺服器收到響應之前遇到網路中斷與連線超時,客戶端是不知道事務是否已經被提交的([圖 9-1](/tw/ch9#fig_distributed_network))。
+但是,這種重複抑制僅適用於單條 TCP 連線的場景中。假設 TCP 連線是一個客戶端與資料庫的連線,並且它正在執行 [例 13-1](#fig_future_non_idempotent) 中的事務。在許多資料庫中,事務是繫結在客戶端連線上的(如果客戶端傳送了多個查詢,資料庫就知道它們屬於同一個事務,因為它們是在同一個 TCP 連線上傳送的)。如果客戶端在傳送 `COMMIT` 之後並在從資料庫伺服器收到響應之前遇到網路中斷與連線超時,客戶端是不知道事務是否已經被提交的([圖 9-1](/tw/ch9#fig_distributed_network))。
-**例 12-1 資金從一個賬戶到另一個賬戶的非冪等轉移**
+
+
+##### 例 13-1 資金從一個賬戶到另一個賬戶的非冪等轉移
```sql
BEGIN TRANSACTION;
@@ -461,20 +419,22 @@ BEGIN TRANSACTION;
COMMIT;
```
-客戶端可以重連到資料庫並重試事務,但現在已經處於 TCP 重複抑制的範圍之外了。因為 [例 12-1]() 中的事務不是冪等的,可能會發生轉了 \$22 而不是期望的 \$11。因此,儘管 [例 12-1]() 是一個事務原子性的標準樣例,但它實際上並不正確,而真正的銀行並不會這樣辦事【3】。
+客戶端可以重連到資料庫並重試事務,但現在已經處於 TCP 重複抑制的範圍之外了。因為 [例 13-1](#fig_future_non_idempotent) 中的事務不是冪等的,可能會發生轉了 \$22 而不是期望的 \$11。因此,儘管 [例 13-1](#fig_future_non_idempotent) 是一個事務原子性的標準樣例,但它實際上並不正確,而真正的銀行並不會這樣辦事[^3]。
-兩階段提交(請參閱 “[原子提交與兩階段提交](/tw/ch9#原子提交與兩階段提交)”)協議會破壞 TCP 連線與事務之間的 1:1 對映,因為它們必須在故障後允許事務協調器重連到資料庫,告訴資料庫將存疑事務提交還是中止。這足以確保事務只被恰好執行一次嗎?不幸的是,並不能。
+兩階段提交(請參閱 “[原子提交與兩階段提交](/tw/ch8#sec_transactions_2pc)”)協議會破壞 TCP 連線與事務之間的 1:1 對映,因為它們必須在故障後允許事務協調器重連到資料庫,告訴資料庫將存疑事務提交還是中止。這足以確保事務只被恰好執行一次嗎?不幸的是,並不能。
即使我們可以抑制資料庫客戶端與伺服器之間的重複事務,我們仍然需要擔心終端使用者裝置與應用伺服器之間的網路。例如,如果終端使用者的客戶端是 Web 瀏覽器,則它可能會使用 HTTP POST 請求向伺服器提交指令。也許使用者正處於一個訊號微弱的蜂窩資料網路連線中,它們成功地傳送了 POST,但卻在能夠從伺服器接收響應之前沒了訊號。
-在這種情況下,可能會向用戶顯示錯誤訊息,而他們可能會手動重試。Web 瀏覽器警告說,“你確定要再次提交這個表單嗎?” —— 使用者選 “是”,因為他們希望操作發生(Post/Redirect/Get 模式【54】可以避免在正常操作中出現此警告訊息,但 POST 請求超時就沒辦法了)。從 Web 伺服器的角度來看,重試是一個獨立的請求;從資料庫的角度來看,這是一個獨立的事務。通常的除重機制無濟於事。
+在這種情況下,可能會向用戶顯示錯誤訊息,而他們可能會手動重試。Web 瀏覽器警告說,“你確定要再次提交這個表單嗎?” —— 使用者選 “是”,因為他們希望操作發生(Post/Redirect/Get 模式[^54]可以避免在正常操作中出現此警告訊息,但 POST 請求超時就沒辦法了)。從 Web 伺服器的角度來看,重試是一個獨立的請求;從資料庫的角度來看,這是一個獨立的事務。通常的除重機制無濟於事。
-#### 操作識別符號
+#### 操作識別符號 {#id355}
-要在通過幾跳的網路通訊上使操作具有冪等性,僅僅依賴資料庫提供的事務機制是不夠的 —— 你需要考慮 **端到端(end-to-end)** 的請求流。
-例如,你可以為操作生成一個唯一的識別符號(例如 UUID),並將其作為隱藏表單欄位包含在客戶端應用中,或透過計算所有表單相關欄位的雜湊來生成操作 ID 【3】。如果 Web 瀏覽器提交了兩次 POST 請求,這兩個請求將具有相同的操作 ID。然後,你可以將該操作 ID 一路傳遞到資料庫,並檢查你是否曾經使用給定的 ID 執行過一個操作,如 [例 12-2]() 中所示。
+要在通過幾跳的網路通訊上使操作具有冪等性,僅僅依賴資料庫提供的事務機制是不夠的,你需要考慮 **端到端(end-to-end)** 的請求流。
+例如,你可以為操作生成一個唯一識別符號(例如 UUID),並將其作為隱藏表單欄位包含在客戶端應用中,或透過計算所有相關表單欄位的雜湊來生成操作 ID[^3]。如果瀏覽器提交了兩次 POST,請求會攜帶相同操作 ID。你就可以把這個 ID 貫穿傳遞到資料庫,並確保同一個 ID 最多隻執行一次,如 [例 13-2](#fig_future_request_id) 所示。
-**例 12-2 使用唯一 ID 來抑制重複請求**
+
+
+##### 例 13-2 使用唯一 ID 抑制重複請求
```sql
ALTER TABLE requests ADD UNIQUE (request_id);
@@ -488,13 +448,13 @@ BEGIN TRANSACTION;
COMMIT;
```
-[例 12-2]() 依賴於 `request_id` 列上的唯一約束。如果一個事務嘗試插入一個已經存在的 ID,那麼 `INSERT` 失敗,事務被中止,使其無法生效兩次。即使在較弱的隔離級別下,關係資料庫也能正確地維護唯一性約束(而在 “[寫入偏差與幻讀](/tw/ch7#寫入偏差與幻讀)” 中討論過,應用級別的 **檢查 - 然後 - 插入** 可能會在不可序列化的隔離下失敗)。
+[例 13-2](#fig_future_request_id) 依賴於 `request_id` 列上的唯一約束。如果事務嘗試插入已存在的 ID,`INSERT` 會失敗並中止事務,從而避免重複生效。即使在較弱隔離級別下,關係資料庫通常也能正確維護唯一性約束(而應用層的 “先檢查再插入” 在不可序列化隔離下可能失敗,見 “[寫入偏差與幻讀](/tw/ch8#sec_transactions_write_skew)”)。
-除了抑制重複的請求之外,[例 12-2]() 中的請求表表現得就像一種事件日誌,暗示著事件溯源的想法(請參閱 “[事件溯源](/tw/ch12#事件溯源)”)。更新賬戶餘額事實上不必與插入事件發生在同一個事務中,因為它們是冗餘的,而能由下游消費者從請求事件中派生出來 —— 只要該事件被恰好處理一次,這又一次可以使用請求 ID 來強制執行。
+除了抑制重複請求,[例 13-2](#fig_future_request_id) 中的 `requests` 表本身也像一份事件日誌,可用於事件溯源或變更資料捕獲。賬戶餘額更新並不一定要與事件插入放在同一事務中,因為餘額是可由下游消費者從請求事件派生出的冗餘狀態;只要請求事件被恰好處理一次(同樣可透過請求 ID 保證),即可保持正確性。
-#### 端到端原則
+#### 端到端原則 {#sec_future_e2e_argument}
-抑制重複事務的這種情況只是一個更普遍的原則的一個例子,這個原則被稱為 **端到端原則(end-to-end argument)**,它在 1984 年由 Saltzer、Reed 和 Clark 闡述【55】:
+抑制重複事務的這種情況只是一個更普遍的原則的一個例子,這個原則被稱為 **端到端原則(end-to-end argument)**,它在 1984 年由 Saltzer、Reed 和 Clark 闡述[^55]:
> 只有在通訊系統兩端應用的知識與幫助下,所討論的功能才能完全地正確地實現。因而將這種被質疑的功能作為通訊系統本身的功能是不可能的(有時,通訊系統可以提供這種功能的不完備版本,可能有助於提高效能)。
>
@@ -503,77 +463,79 @@ COMMIT;
端到端原則也適用於檢查資料的完整性:乙太網,TCP 和 TLS 中內建的校驗和可以檢測網路中資料包的損壞情況,但是它們無法檢測到由連線兩端傳送 / 接收軟體中 Bug 導致的損壞。或資料儲存所在磁碟上的損壞。如果你想捕獲資料所有可能的損壞來源,你也需要端到端的校驗和。
-類似的原則也適用於加密【55】:家庭 WiFi 網路上的密碼可以防止人們竊聽你的 WiFi 流量,但無法阻止網際網路上其他地方攻擊者的窺探;客戶端與伺服器之間的 TLS/SSL 可以阻擋網路攻擊者,但無法阻止惡意伺服器。只有端到端的加密和認證可以防止所有這些事情。
+類似的原則也適用於加密[^55]:家庭 WiFi 網路上的密碼可以防止人們竊聽你的 WiFi 流量,但無法阻止網際網路上其他地方攻擊者的窺探;客戶端與伺服器之間的 TLS/SSL 可以阻擋網路攻擊者,但無法阻止惡意伺服器。只有端到端的加密和認證可以防止所有這些事情。
儘管低層級的功能(TCP 重複抑制、乙太網校驗和、WiFi 加密)無法單獨提供所需的端到端功能,但它們仍然很有用,因為它們能降低較高層級出現問題的可能性。例如,如果我們沒有 TCP 來將資料包排成正確的順序,那麼 HTTP 請求通常就會被攪爛。我們只需要記住,低級別的可靠性功能本身並不足以確保端到端的正確性。
-#### 在資料系統中應用端到端思考
+#### 在資料系統中應用端到端思考 {#id357}
這將我帶回最初的論點:僅僅因為應用使用了提供相對較強安全屬性的資料系統,例如可序列化的事務,並不意味著應用的資料就不會丟失或損壞了。應用本身也需要採取端到端的措施,例如除重。
這實在是一個遺憾,因為容錯機制很難弄好。低層級的可靠機制(比如 TCP 中的那些)執行的相當好,因而剩下的高層級錯誤基本很少出現。如果能將這些剩下的高層級容錯機制打包成抽象,而應用不需要再去操心,那該多好呀 —— 但恐怕我們還沒有找到這一正確的抽象。
-長期以來,事務被認為是一個很好的抽象,我相信它們確實是很有用的。正如 [第七章](/tw/ch7) 導言中所討論的,它們將各種可能的問題(併發寫入、違背約束、崩潰、網路中斷、磁碟故障)合併為兩種可能結果:提交或中止。這是對程式設計模型而言是一種巨大的簡化,但恐怕這還不夠。
+長期以來,事務被認為是一個很好的抽象,我相信它們確實是很有用的。正如 [第八章](/tw/ch8) 中所討論的,它們將各種可能的問題(併發寫入、違背約束、崩潰、網路中斷、磁碟故障)合併為兩種可能結果:提交或中止。這是對程式設計模型而言的一種巨大簡化,但這還不夠。
-事務是代價高昂的,當涉及異構儲存技術時尤為甚(請參閱 “[實踐中的分散式事務](/tw/ch9#實踐中的分散式事務)”)。我們拒絕使用分散式事務是因為它開銷太大,結果我們最後不得不在應用程式碼中重新實現容錯機制。正如本書中大量的例子所示,對併發性與部分失敗的推理是困難且違反直覺的,所以我懷疑大多數應用級別的機制都不能正確工作,最終結果是資料丟失或損壞。
+事務是代價高昂的,當涉及異構儲存技術時尤為甚(請參閱 “[實踐中的分散式事務](/tw/ch8#sec_transactions_xa)”)。我們拒絕使用分散式事務是因為它開銷太大,結果我們最後不得不在應用程式碼中重新實現容錯機制。正如本書中大量的例子所示,對併發性與部分失敗的推理是困難且違反直覺的,所以我懷疑大多數應用級別的機制都不能正確工作,最終結果是資料丟失或損壞。
出於這些原因,我認為探索對容錯的抽象是很有價值的。它使提供應用特定的端到端的正確性屬性變得更簡單,而且還能在大規模分散式環境中提供良好的效能與運維特性。
-### 強制約束
+### 強制約束 {#sec_future_constraints}
讓我們思考一下在 [分拆資料庫](#分拆資料庫) 上下文中的 **正確性(correctness)**。我們看到端到端的除重可以透過從客戶端一路透傳到資料庫的請求 ID 實現。那麼其他型別的約束呢?
-我們先來特別關注一下 **唯一性約束** —— 例如我們在 [例 12-2]() 中所依賴的約束。在 “[約束和唯一性保證](/tw/ch9#約束和唯一性保證)” 中,我們看到了幾個其他需要強制實施唯一性的應用功能例子:使用者名稱或電子郵件地址必須唯一標識使用者,檔案儲存服務不能包含多個重名檔案,兩個人不能在航班或劇院預訂同一個座位。
+我們先來特別關注一下 **唯一性約束** —— 例如我們在 [例 13-2](#fig_future_request_id) 中所依賴的約束。在 “[約束和唯一性保證](/tw/ch10#sec_consistency_uniqueness)” 中,我們看到了幾個其他需要強制實施唯一性的應用功能例子:使用者名稱或電子郵件地址必須唯一標識使用者,檔案儲存服務不能包含多個重名檔案,兩個人不能在航班或劇院預訂同一個座位。
其他型別的約束也非常類似:例如,確保帳戶餘額永遠不會變為負數,確保不會超賣庫存,或者會議室沒有重複的預訂。執行唯一性約束的技術通常也可以用於這些約束。
-#### 唯一性約束需要達成共識
+#### 唯一性約束需要達成共識 {#id452}
-在 [第九章](/tw/ch9) 中我們看到,在分散式環境中,強制執行唯一性約束需要共識:如果存在多個具有相同值的併發請求,則系統需要決定衝突操作中的哪一個被接受,並拒絕其他違背約束的操作。
+在 [第十章](/tw/ch10) 中我們看到,在分散式環境中,強制執行唯一性約束需要共識:如果存在多個具有相同值的併發請求,則系統需要決定衝突操作中的哪一個被接受,並拒絕其他違背約束的操作。
-達成這一共識的最常見方式是使單個節點作為領導,並使其負責所有決策。只要你不介意所有請求都擠過單個節點(即使客戶端位於世界的另一端),只要該節點沒有失效,系統就能正常工作。如果你需要容忍領導者失效,那麼就又回到了共識問題(請參閱 “[單主複製與共識](/tw/ch9#單主複製與共識)”)。
+達成這一共識的最常見方式是使單個節點作為領導,並使其負責所有決策。只要你不介意所有請求都擠過單個節點(即使客戶端位於世界的另一端),只要該節點沒有失效,系統就能正常工作。如果你需要容忍領導者失效,那麼就又回到了共識問題(請參閱 “[單主複製與共識](/tw/ch10#from-single-leader-replication-to-consensus)”)。
-唯一性檢查可以透過對唯一性欄位分割槽做橫向伸縮。例如,如果需要透過請求 ID 確保唯一性(如 [例 12-2]() 所示),你可以確保所有具有相同請求 ID 的請求都被路由到同一分割槽(請參閱 [第六章](/tw/ch6))。如果你需要讓使用者名稱是唯一的,則可以按使用者名稱的雜湊值做分割槽。
+唯一性檢查可以透過對唯一性欄位分割槽做橫向伸縮。例如,如果需要透過請求 ID 確保唯一性(如 [例 13-2](#fig_future_request_id) 所示),你可以確保所有具有相同請求 ID 的請求都被路由到同一分割槽(請參閱 [第七章](/tw/ch7))。如果你需要讓使用者名稱是唯一的,則可以按使用者名稱的雜湊值做分割槽。
-但非同步多主複製排除在外,因為可能會發生不同主庫同時接受衝突寫操作的情況,因而這些值不再是唯一的(請參閱 “[實現線性一致的系統](/tw/ch9#實現線性一致的系統)”)。如果你想立刻拒絕任何違背約束的寫入,同步協調是無法避免的【56】。
+但非同步多主複製排除在外,因為可能會發生不同主庫同時接受衝突寫操作的情況,因而這些值不再是唯一的(請參閱 “[實現線性一致的系統](/tw/ch10#sec_consistency_implementing_linearizable)”)。如果你想立刻拒絕任何違背約束的寫入,同步協調是無法避免的[^56]。
-#### 基於日誌訊息傳遞中的唯一性
+#### 基於日誌訊息傳遞中的唯一性 {#sec_future_uniqueness_log}
-日誌確保所有消費者以相同的順序看見訊息 —— 這種保證在形式上被稱為 **全序廣播(total order boardcast)** 並且等價於共識(請參閱 “[全序廣播](/tw/ch9#全序廣播)”)。在使用基於日誌的訊息傳遞的分拆資料庫方法中,我們可以使用非常類似的方法來執行唯一性約束。
+日誌確保所有消費者以相同順序看到訊息,這在形式上稱為 **全序廣播(total order broadcast)**,並且等價於共識(請參閱 “[全序廣播](/tw/ch10#sec_consistency_total_order)”)。在基於日誌訊息傳遞的分拆資料庫方案中,我們可以用同樣的思路來實施唯一性約束。
-流處理器在單個執行緒上依次消費單個日誌分割槽中的所有訊息(請參閱 “[日誌與傳統的訊息傳遞相比](/tw/ch12#日誌與傳統的訊息傳遞相比)”)。因此,如果日誌是按需要確保唯一的值做的分割槽,則流處理器可以無歧義地、確定性地決定幾個衝突操作中的哪一個先到達。例如,在多個使用者嘗試宣告相同使用者名稱的情況下【57】:
+流處理器在單個執行緒上依次消費單個日誌分割槽中的所有訊息(請參閱 “[日誌與傳統的訊息傳遞相比](/tw/ch12#sec_stream_logs_vs_messaging)”)。因此,如果日誌是按需要確保唯一的值做的分割槽,則流處理器可以無歧義地、確定性地決定幾個衝突操作中的哪一個先到達。例如,在多個使用者嘗試宣告相同使用者名稱的情況下[^57]:
1. 每個對使用者名稱的請求都被編碼為一條訊息,並追加到按使用者名稱雜湊值確定的分割槽。
2. 流處理器依序讀取日誌中的請求,並使用本地資料庫來追蹤哪些使用者名稱已經被佔用了。對於所有申請可用使用者名稱的請求,它都會記錄該使用者名稱,並向輸出流傳送一條成功訊息。對於所有申請已佔用使用者名稱的請求,它都會向輸出流傳送一條拒絕訊息。
3. 請求使用者名稱的客戶端監視輸出流,等待與其請求相對應的成功或拒絕訊息。
-該演算法基本上與 “[使用全序廣播實現線性一致的儲存](/tw/ch9#使用全序廣播實現線性一致的儲存)” 中的演算法相同。它可以簡單地透過增加分割槽數伸縮至較大的請求吞吐量,因為每個分割槽都可以被獨立處理。
+該演算法基本上與 “[使用全序廣播實現線性一致的儲存](/tw/ch10#sec_consistency_total_order)” 中的演算法相同。它可以簡單地透過增加分割槽數伸縮至較大的請求吞吐量,因為每個分割槽都可以被獨立處理。
-該方法不僅適用於唯一性約束,而且適用於許多其他型別的約束。其基本原理是,任何可能衝突的寫入都會路由到相同的分割槽並按順序處理。正如 “[什麼是衝突?](/tw/ch5#什麼是衝突?)” 與 “[寫入偏差與幻讀](/tw/ch7#寫入偏差與幻讀)” 中所述,衝突的定義可能取決於應用,但流處理器可以使用任意邏輯來驗證請求。這個想法與 Bayou 在 90 年代開創的方法類似【58】。
+該方法不僅適用於唯一性約束,而且適用於許多其他型別的約束。其基本原理是,任何可能衝突的寫入都會路由到相同的分割槽並按順序處理。正如 “[什麼是衝突?](/tw/ch6#what-is-a-conflict)” 與 “[寫入偏差與幻讀](/tw/ch8#sec_transactions_write_skew)” 中所述,衝突的定義可能取決於應用,但流處理器可以使用任意邏輯來驗證請求。這個想法與 Bayou 在 90 年代開創的方法類似[^58]。
-#### 多分割槽請求處理
+#### 多分割槽請求處理 {#id360}
-當涉及多個分割槽時,確保操作以原子方式執行且同時滿足約束就變得很有趣了。在 [例 12-2]() 中,可能有三個分割槽:一個包含請求 ID,一個包含收款人賬戶,另一個包含付款人賬戶。沒有理由把這三種東西放入同一個分割槽,因為它們都是相互獨立的。
+當請求涉及多個分割槽時,如何在滿足約束的同時保證原子效果,會更有挑戰性。在 [例 13-2](#fig_future_request_id) 中,至少可能涉及三個分割槽:請求 ID 所在分割槽、收款賬戶所在分割槽、付款賬戶所在分割槽。它們彼此獨立,並不必然位於同一分割槽。
-在資料庫的傳統方法中,執行此事務需要跨全部三個分割槽進行原子提交,就這些分割槽上的所有其他事務而言,這實質上是將該事務嵌入一個全序。而這樣就要求跨分割槽協調,不同的分割槽無法再獨立地進行處理,因此吞吐量很可能會受到影響。
+在傳統資料庫方案裡,這類事務通常需要跨分割槽原子提交;這會把事務強行納入跨分割槽全序,從而引入同步協調開銷並影響吞吐量。
+但使用分割槽日誌與流處理器,也可以在不使用跨分割槽原子提交的情況下達到等價正確性。
-但事實證明,使用分割槽日誌可以達到等價的正確性而無需原子提交:
+{{< figure src="/fig/ddia_1302.png" id="fig_future_multi_shard" caption="圖 13-2 使用事件日誌與流處理器,檢查源賬戶是否有足夠餘額,並將資金原子地劃轉到目標賬戶與手續費賬戶。" class="w-full my-4" >}}
-1. 從賬戶 A 向賬戶 B 轉賬的請求由客戶端提供一個唯一的請求 ID,並按請求 ID 追加寫入相應日誌分割槽。
-2. 流處理器讀取請求日誌。對於每個請求訊息,它向輸出流發出兩條訊息:付款人賬戶 A 的借記指令(按 A 分割槽),收款人 B 的貸記指令(按 B 分割槽)。被發出的訊息中會帶有原始的請求 ID。
-3. 後續處理器消費借記 / 貸記指令流,按照請求 ID 除重,並將變更應用至賬戶餘額。
+1. 客戶端為轉賬請求生成全域性唯一請求 ID,並將請求按源賬戶 ID 路由到相應日誌分割槽。
+2. 一個流處理器消費該請求日誌,並維護源賬戶本地狀態及已處理請求 ID 集。遇到新請求 ID 時,先檢查餘額是否充足;若充足,則在本地狀態中預留金額,併發出多個後續事件:源賬戶的出賬事件、目標賬戶的入賬事件、手續費賬戶的入賬事件。所有事件都攜帶同一請求 ID。
+3. 源賬戶處理器稍後會再次收到出賬事件。它根據請求 ID 識別出這是先前預留過的支付,執行真正扣款並更新本地狀態;若重複到達則忽略。
+4. 目標賬戶與手續費賬戶各自由獨立處理任務消費。收到入賬事件後更新本地狀態,並基於請求 ID 去重。
-步驟 1 和步驟 2 是必要的,因為如果客戶直接傳送貸記與借記指令,則需要在這兩個分割槽之間進行原子提交,以確保兩者要麼都發生或都不發生。為了避免對分散式事務的需要,我們首先將請求持久化記錄為單條訊息,然後從這第一條訊息中派生出貸記指令與借記指令。幾乎在所有資料系統中,單物件寫入都是原子性的(請參閱 “[單物件寫入](/tw/ch7#單物件寫入)),因此請求要麼出現在日誌中,要麼就不出現,無需多分割槽原子提交。
+圖 13-2 雖然畫成三個賬戶落在三個分割槽中,但即使在同一分割槽也同樣成立。關鍵條件是:同一賬戶的事件必須按日誌順序處理,且訊息投遞具備至少一次語義,處理邏輯保持確定性。
-如果流處理器在步驟 2 中崩潰,則它會從上一個存檔點恢復處理。這樣做時,它不會跳過任何請求訊息,但可能會多次處理請求併產生重複的貸記與借記指令。但由於它是確定性的,因此它只是再次生成相同的指令,而步驟 3 中的處理器可以使用端到端請求 ID 輕鬆地對其除重。
+如果源賬戶處理器在處理中崩潰,恢復後會重放相同請求並做出相同決策,發出相同請求 ID 的後續事件。下游消費者會基於請求 ID 去重,因此不會重複生效。
-如果你想確保付款人的帳戶不會因此次轉賬而透支,則可以使用一個額外的流處理器來維護賬戶餘額並校驗事務(按付款人賬戶分割槽),只有有效的事務會被記錄在步驟 1 中的請求日誌中。
+這個系統的原子性不來自分散式事務,而來自初始請求事件寫入源賬戶日誌這一原子動作。只要這個起點事件寫入成功,後續事件最終都會出現:它們可能因故障恢復而延遲,也可能短暫重複,但最終可達。
-透過將多分割槽事務分解為兩個不同分割槽方式的階段,並使用端到端的請求 ID,我們實現了同樣的正確性屬性(每個請求對付款人與收款人都恰好生效一次),即使在出現故障,且沒有使用原子提交協議的情況下依然如此。使用多個不同分割槽階段的想法與我們在 “[多分割槽資料處理](#多分割槽資料處理)” 中討論的想法類似(也請參閱 “[併發控制](/tw/ch12#併發控制)”)。
+透過把多分割槽事務拆成多個按不同鍵分割槽的階段,並貫穿端到端請求 ID,我們在故障場景下依然能保證“每個請求對付款方與收款方都恰好生效一次”,同時避免使用原子提交協議。
-### 及時性與完整性
+### 及時性與完整性 {#sec_future_integrity}
-事務的一個便利屬性是,它們通常是線性一致的(請參閱 “[線性一致性](/tw/ch9#線性一致性)”),也就是說,寫入者會等到事務提交,而之後其寫入立刻對所有讀取者可見。
+事務的一個便利屬性是,它們通常是線性一致的(請參閱 “[線性一致性](/tw/ch10#sec_consistency_linearizability)”),也就是說,寫入者會等到事務提交,而之後其寫入立刻對所有讀取者可見。
當我們把一個操作拆分為跨越多個階段的流處理器時,卻並非如此:日誌的消費者在設計上就是非同步的,因此傳送者不會等其訊息被消費者處理完。但是,客戶端等待輸出流中的特定訊息是可能的。這正是我們在 “[基於日誌訊息傳遞中的唯一性](#基於日誌訊息傳遞中的唯一性)” 一節中檢查唯一性約束時所做的事情。
@@ -583,48 +545,48 @@ COMMIT;
* 及時性(Timeliness)
- 及時性意味著確保使用者觀察到系統的最新狀態。我們之前看到,如果使用者從陳舊的資料副本中讀取資料,它們可能會觀察到系統處於不一致的狀態(請參閱 “[複製延遲問題](/tw/ch5#複製延遲問題)”)。但這種不一致是暫時的,而最終會透過等待與重試簡單地得到解決。
+ 及時性意味著確保使用者觀察到系統的最新狀態。我們之前看到,如果使用者從陳舊的資料副本中讀取資料,它們可能會觀察到系統處於不一致的狀態(請參閱 “[複製延遲問題](/tw/ch6#sec_replication_lag)”)。但這種不一致是暫時的,而最終會透過等待與重試簡單地得到解決。
- CAP 定理(請參閱 “[線性一致性的代價](/tw/ch9#線性一致性的代價)”)使用 **線性一致性(linearizability)** 意義上的一致性,這是實現及時性的強有力方法。像 **寫後讀** 這樣及時性更弱的一致性也很有用(請參閱 “[讀己之寫](/tw/ch5#讀己之寫)”)。
+ CAP 定理(請參閱 “[線性一致性的代價](/tw/ch10#sec_linearizability_cost)”)使用 **線性一致性(linearizability)** 意義上的一致性,這是實現及時性的強有力方法。像 **寫後讀** 這樣及時性更弱的一致性也很有用(請參閱 “[讀己之寫](/tw/ch6#sec_replication_ryw)”)。
* 完整性(Integrity)
- 完整性意味著沒有損壞;即沒有資料丟失,並且沒有矛盾或錯誤的資料。尤其是如果某些派生資料集是作為底層資料之上的檢視而維護的(請參閱 “[從事件日誌中派生出當前狀態](/tw/ch12#從事件日誌中派生出當前狀態)”),這種派生必須是正確的。例如,資料庫索引必須正確地反映資料庫的內容 —— 缺失某些記錄的索引並不是很有用。
+ 完整性意味著沒有損壞;即沒有資料丟失,並且沒有矛盾或錯誤的資料。尤其是如果某些派生資料集是作為底層資料之上的檢視而維護的(請參閱 “[從事件日誌中派生出當前狀態](/tw/ch12#sec_stream_deriving_views)”),這種派生必須是正確的。例如,資料庫索引必須正確地反映資料庫的內容 —— 缺失某些記錄的索引並不是很有用。
- 如果完整性被違背,這種不一致是永久的:在大多數情況下,等待與重試並不能修復資料庫損壞。相反的是,需要顯式地檢查與修復。在 ACID 事務的上下文中(請參閱 “[ACID 的含義](/tw/ch7#ACID的含義)”),一致性通常被理解為某種特定於應用的完整性概念。原子性和永續性是保持完整性的重要工具。
+ 如果完整性被違背,這種不一致是永久的:在大多數情況下,等待與重試並不能修復資料庫損壞。相反的是,需要顯式地檢查與修復。在 ACID 事務的上下文中(請參閱 “[ACID 的含義](/tw/ch8#sec_transactions_acid)”),一致性通常被理解為某種特定於應用的完整性概念。原子性和永續性是保持完整性的重要工具。
口號形式:違反及時性,“最終一致性”;違反完整性,“永無一致性”。
我斷言在大多數應用中,完整性比及時性重要得多。違反及時性可能令人困惑與討厭,但違反完整性的結果可能是災難性的。
-例如在你的信用卡對賬單上,如果某一筆過去 24 小時內完成的交易尚未出現並不令人奇怪 —— 這些系統有一定的滯後是正常的。我們知道銀行是非同步核算與敲定交易的,這裡的及時性並不是非常重要【3】。但如果當期對賬單餘額與上期對賬單餘額加交易總額對不上(求和錯誤),或者出現一筆向你收費但未向商家付款的交易(消失的錢),那就實在是太糟糕了,這樣的問題就違背了系統的完整性。
+例如在你的信用卡對賬單上,如果某一筆過去 24 小時內完成的交易尚未出現並不令人奇怪 —— 這些系統有一定的滯後是正常的。我們知道銀行是非同步核算與敲定交易的,這裡的及時性並不是非常重要[^3]。但如果當期對賬單餘額與上期對賬單餘額加交易總額對不上(求和錯誤),或者出現一筆向你收費但未向商家付款的交易(消失的錢),那就實在是太糟糕了,這樣的問題就違背了系統的完整性。
-#### 資料流系統的正確性
+#### 資料流系統的正確性 {#id453}
ACID 事務通常既提供及時性(例如線性一致性)也提供完整性保證(例如原子提交)。因此如果你從 ACID 事務的角度來看待應用的正確性,那麼及時性與完整性的區別是無關緊要的。
另一方面,對於在本章中討論的基於事件的資料流系統而言,它們的一個有趣特性就是將及時性與完整性分開。在非同步處理事件流時不能保證及時性,除非你顯式構建一個在返回之前明確等待特定訊息到達的消費者。但完整性實際上才是流處理系統的核心。
-**恰好一次** 或 **等效一次** 語義(請參閱 “[容錯](/tw/ch12#容錯)”)是一種保持完整性的機制。如果事件丟失或者生效兩次,就有可能違背資料系統的完整性。因此在出現故障時,容錯訊息傳遞與重複抑制(例如,冪等操作)對於維護資料系統的完整性是很重要的。
+**恰好一次** 或 **等效一次** 語義(請參閱 “[容錯](/tw/ch12#sec_stream_fault_tolerance)”)是一種保持完整性的機制。如果事件丟失或者生效兩次,就有可能違背資料系統的完整性。因此在出現故障時,容錯訊息傳遞與重複抑制(例如,冪等操作)對於維護資料系統的完整性是很重要的。
正如我們在上一節看到的那樣,可靠的流處理系統可以在無需分散式事務與原子提交協議的情況下保持完整性,這意味著它們有潛力達到與後者相當的正確性,同時還具備好得多的效能與運維穩健性。為了達成這種正確性,我們組合使用了多種機制:
-* 將寫入操作的內容表示為單條訊息,從而可以輕鬆地被原子寫入 —— 與事件溯源搭配效果拔群(請參閱 “[事件溯源](/tw/ch12#事件溯源)”)。
-* 使用與儲存過程類似的確定性派生函式,從這一訊息中派生出所有其他的狀態變更(請參閱 “[真的序列執行](/tw/ch7#真的序列執行)” 和 “[應用程式碼作為派生函式](/tw/ch13#應用程式碼作為派生函式)”)
+* 將寫入操作的內容表示為單條訊息,從而可以輕鬆地被原子寫入 —— 與事件溯源搭配效果拔群(請參閱 “[事件溯源](/tw/ch12#sec_stream_event_sourcing)”)。
+* 使用與儲存過程類似的確定性派生函式,從這一訊息中派生出所有其他的狀態變更(請參閱 “[真的序列執行](/tw/ch8#sec_transactions_serial)” 和 “[應用程式碼作為派生函式](/tw/ch13#sec_future_dataflow_derivation)”)
* 將客戶端生成的請求 ID 傳遞透過所有的處理層次,從而允許端到端的除重,帶來冪等性。
-* 使訊息不可變,並允許派生資料能隨時被重新處理,這使從錯誤中恢復更加容易(請參閱 “[不可變事件的優點](/tw/ch12#不可變事件的優點)”)
+* 使訊息不可變,並允許派生資料能隨時被重新處理,這使從錯誤中恢復更加容易(請參閱 “[不可變事件的優點](/tw/ch12#sec_stream_immutability_pros)”)
這種機制組合在我看來,是未來構建容錯應用的一個非常有前景的方向。
-#### 寬鬆地解釋約束
+#### 寬鬆地解釋約束 {#id362}
如前所述,執行唯一性約束需要共識,通常透過在單個節點中彙集特定分割槽中的所有事件來實現。如果我們想要傳統的唯一性約束形式,這種限制是不可避免的,流處理也不例外。
然而另一個需要了解的事實是,許多真實世界的應用實際上可以擺脫這種形式,接受弱得多的唯一性:
-* 如果兩個人同時註冊了相同的使用者名稱或預訂了相同的座位,你可以給其中一個人發訊息道歉,並要求他們換一個不同的使用者名稱或座位。這種糾正錯誤的變化被稱為 **補償性事務(compensating transaction)**【59,60】。
-* 如果客戶訂購的物品多於倉庫中的物品,你可以下單補倉,併為延誤向客戶道歉,向他們提供折扣。實際上,這麼說吧,如果叉車在倉庫中軋過了你的貨物,剩下的貨物比你想象的要少,那麼你也是得這麼做【61】。因此,既然道歉工作流無論如何已經成為你商業過程中的一部分了,那麼對庫存物品數目新增線性一致的約束可能就沒必要了。
+* 如果兩個人同時註冊了相同的使用者名稱或預訂了相同的座位,你可以給其中一個人發訊息道歉,並要求他們換一個不同的使用者名稱或座位。這種糾正錯誤的變化被稱為 **補償性事務(compensating transaction)**[^59][^60]。
+* 如果客戶訂購的物品多於倉庫中的物品,你可以下單補倉,併為延誤向客戶道歉,向他們提供折扣。實際上,這麼說吧,如果叉車在倉庫中軋過了你的貨物,剩下的貨物比你想象的要少,那麼你也是得這麼做[^61]。因此,既然道歉工作流無論如何已經成為你商業過程中的一部分了,那麼對庫存物品數目新增線性一致的約束可能就沒必要了。
* 與之類似,許多航空公司都會超賣機票,打著一些旅客可能會錯過航班的算盤;許多旅館也會超賣客房,抱著部分客人可能會取消預訂的期望。在這些情況下,出於商業原因而故意違反了 “一人一座” 的約束;當需求超過供給的情況出現時,就會進入補償流程(退款、升級艙位 / 房型、提供隔壁酒店的免費的房間)。即使沒有超賣,為了應對由惡劣天氣或員工罷工導致的航班取消,你還是需要道歉與補償流程 —— 從這些問題中恢復僅僅是商業活動的正常組成部分。
* 如果有人從賬戶超額取款,銀行可以向他們收取透支費用,並要求他們償還欠款。透過限制每天的提款總額,銀行的風險是有限的。
@@ -632,246 +594,81 @@ ACID 事務通常既提供及時性(例如線性一致性)也提供完整性
道歉的成本是否能接受是一個商業決策。如果可以接受的話,在寫入資料之前檢查所有約束的傳統模型反而會帶來不必要的限制,而線性一致性的約束也不是必須的。樂觀寫入,事後檢查可能是一種合理的選擇。你仍然可以在做一些挽回成本高昂的事情前確保有相關的驗證,但這並不意味著寫入資料之前必須先進行驗證。
-這些應用 **確實** 需要完整性:你不會希望丟失預訂資訊,或者由於借方貸方不匹配導致資金消失。但是它們在執行約束時 **並不需要** 及時性:如果你銷售的貨物多於倉庫中的庫存,可以在事後道歉後並彌補問題。這種做法與我們在 “[處理寫入衝突](/tw/ch5#處理寫入衝突)” 中討論的衝突解決方法類似。
+這些應用 **確實** 需要完整性:你不會希望丟失預訂資訊,或者由於借方貸方不匹配導致資金消失。但是它們在執行約束時 **並不需要** 及時性:如果你銷售的貨物多於倉庫中的庫存,可以在事後道歉後並彌補問題。這種做法與我們在 “[處理寫入衝突](/tw/ch6#sec_replication_write_conflicts)” 中討論的衝突解決方法類似。
-#### 無協調資料系統
+#### 無協調資料系統 {#id454}
我們現在已經做了兩個有趣的觀察:
1. 資料流系統可以維持派生資料的完整性保證,而無需原子提交、線性一致性或者同步的跨分割槽協調。
2. 雖然嚴格的唯一性約束要求及時性和協調,但許多應用實際上可以接受寬鬆的約束:只要整個過程保持完整性,這些約束可能會被臨時違反並在稍後被修復。
-總之這些觀察意味著,資料流系統可以為許多應用提供無需協調的資料管理服務,且仍能給出很強的完整性保證。這種 **無協調(coordination-avoiding)** 的資料系統有著很大的吸引力:比起需要執行同步協調的系統,它們能達到更好的效能與更強的容錯能力【56】。
+總之這些觀察意味著,資料流系統可以為許多應用提供無需協調的資料管理服務,且仍能給出很強的完整性保證。這種 **無協調(coordination-avoiding)** 的資料系統有著很大的吸引力:比起需要執行同步協調的系統,它們能達到更好的效能與更強的容錯能力[^56]。
例如,這種系統可以使用多領導者配置運維,跨越多個數據中心,在區域間非同步複製。任何一個數據中心都可以持續獨立執行,因為不需要同步的跨區域協調。這樣的系統的及時性保證會很弱 —— 如果不引入協調它是不可能是線性一致的 —— 但它仍然可以提供有力的完整性保證。
-在這種情況下,可序列化事務作為維護派生狀態的一部分仍然是有用的,但它們只能在小範圍內執行,在那裡它們工作得很好【8】。異構分散式事務(如 XA 事務,請參閱 “[實踐中的分散式事務](/tw/ch9#實踐中的分散式事務)”)不是必需的。同步協調仍然可以在需要的地方引入(例如在無法恢復的操作之前強制執行嚴格的約束),但是如果只是應用的一小部分地方需要它,沒必要讓所有操作都付出協調的代價。【43】。
+在這種情況下,可序列化事務作為維護派生狀態的一部分仍然是有用的,但它們只能在小範圍內執行,在那裡它們工作得很好[^8]。異構分散式事務(如 XA 事務,請參閱 “[實踐中的分散式事務](/tw/ch8#sec_transactions_xa)”)不是必需的。同步協調仍然可以在需要的地方引入(例如在無法恢復的操作之前強制執行嚴格的約束),但是如果只是應用的一小部分地方需要它,沒必要讓所有操作都付出協調的代價。[^43]。
另一種審視協調與約束的角度是:它們減少了由於不一致而必須做出的道歉數量,但也可能會降低系統的效能和可用性,從而可能增加由於宕機中斷而需要做出的道歉數量。你不可能將道歉數量減少到零,但可以根據自己的需求尋找最佳平衡點 —— 既不存在太多不一致性,又不存在太多可用性問題。
-### 信任但驗證
+### 信任但驗證 {#sec_future_verification}
-我們所有關於正確性,完整性和容錯的討論都基於一些假設,假設某些事情可能會出錯,但其他事情不會。我們將這些假設稱為我們的 **系統模型**(system model,請參閱 “[將系統模型對映到現實世界](/tw/ch8#將系統模型對映到現實世界)”):例如,我們應該假設程序可能會崩潰,機器可能突然斷電,網路可能會任意延遲或丟棄訊息。但是我們也可能假設寫入磁碟的資料在執行 `fsync` 後不會丟失,記憶體中的資料沒有損壞,而 CPU 的乘法指令總是能返回正確的結果。
+我們所有關於正確性,完整性和容錯的討論都基於一些假設,假設某些事情可能會出錯,但其他事情不會。我們將這些假設稱為我們的 **系統模型**(system model,請參閱 “[將系統模型對映到現實世界](/tw/ch9#sec_distributed_system_model)”):例如,我們應該假設程序可能會崩潰,機器可能突然斷電,網路可能會任意延遲或丟棄訊息。但是我們也可能假設寫入磁碟的資料在執行 `fsync` 後不會丟失,記憶體中的資料沒有損壞,而 CPU 的乘法指令總是能返回正確的結果。
這些假設是相當合理的,因為大多數時候它們都是成立的,如果我們不得不經常擔心計算機出錯,那麼基本上寸步難行。在傳統上,系統模型採用二元方法處理故障:我們假設有些事情可能會發生,而其他事情 **永遠** 不會發生。實際上,這更像是一個機率問題:有些事情更有可能,其他事情不太可能。問題在於違反我們假設的情況是否經常發生,以至於我們可能在實踐中遇到它們。
-我們已經看到,資料可能會在尚未落盤時損壞(請參閱 “[複製與永續性](/tw/ch7#複製與永續性)”),而網路上的資料損壞有時可能規避了 TCP 校驗和(請參閱 “[弱謊言形式](/tw/ch8#弱謊言形式)” )。也許我們應當更關注這些事情?
+我們已經看到,資料可能會在記憶體中、磁碟上、以及網路傳輸過程中出現損壞。也許這件事值得我們投入更多關注:當系統規模足夠大時,哪怕機率再低的問題也會在現實中發生。
-我過去所從事的一個應用收集了來自客戶端的崩潰報告,我們收到的一些報告,只有在這些裝置記憶體中出現了隨機位翻轉才解釋的通。這看起來不太可能,但是如果有足夠多的裝置執行你的軟體,那麼即使再不可能發生的事也確實會發生。除了由於硬體故障或輻射導致的隨機儲存器損壞之外,一些病態的儲存器訪問模式甚至可以在沒有故障的儲存器中翻轉位【62】 —— 一種可用於破壞作業系統安全機制的效應【63】(這種技術被稱為 **Rowhammer**)。一旦你仔細觀察,硬體並不是看上去那樣完美的抽象。
+#### 維護完整性,儘管軟體有Bug {#id455}
-要澄清的是,隨機位翻轉在現代硬體上仍是非常罕見的【64】。我只想指出,它們並沒有超越可能性的範疇,所以值得一些關注。
-
-#### 維護完整性,儘管軟體有Bug
-
-除了這些硬體問題之外,總是存在軟體 Bug 的風險,這些錯誤不會被較低層次的網路、記憶體或檔案系統校驗和所捕獲。即使廣泛使用的資料庫軟體也有 Bug:即使像 MySQL 與 PostgreSQL 這樣穩健、口碑良好、多年來被許多人充分測試過的軟體,就我個人所見也有 Bug,比如 MySQL 未能正確維護唯一約束【65】,以及 PostgreSQL 的可序列化隔離等級存在特定的寫入偏差異常【66】。對於不那麼成熟的軟體來說,情況可能要糟糕得多。
+除了這些硬體問題之外,總是存在軟體 Bug 的風險,這些錯誤不會被較低層次的網路、記憶體或檔案系統校驗和所捕獲。即使廣泛使用的資料庫軟體也有 Bug:即使像 MySQL 與 PostgreSQL 這樣穩健、口碑良好、多年來被許多人充分測試過的軟體,就我個人所見也有 Bug,比如 MySQL 未能正確維護唯一約束[^65],以及 PostgreSQL 的可序列化隔離等級存在特定的寫入偏差異常[^66]。對於不那麼成熟的軟體來說,情況可能要糟糕得多。
儘管在仔細設計,測試,以及審查上做出很多努力,但 Bug 仍然會在不知不覺中產生。儘管它們很少,而且最終會被發現並被修復,但總會有那麼一段時間,這些 Bug 可能會損壞資料。
-而對於應用程式碼,我們不得不假設會有更多的錯誤,因為絕大多數應用的程式碼經受的評審與測試遠遠無法與資料庫的程式碼相比。許多應用甚至沒有正確使用資料庫提供的用於維持完整性的功能,例如外部索引鍵或唯一性約束【36】。
+而對於應用程式碼,我們不得不假設會有更多的錯誤,因為絕大多數應用的程式碼經受的評審與測試遠遠無法與資料庫的程式碼相比。許多應用甚至沒有正確使用資料庫提供的用於維持完整性的功能,例如外部索引鍵或唯一性約束[^36]。
-ACID 意義下的一致性(請參閱 “[一致性](/tw/ch7#一致性)”)基於這樣一種想法:資料庫以一致的狀態啟動,而事務將其從一個一致狀態轉換至另一個一致的狀態。因此,我們期望資料庫始終處於一致狀態。然而,只有當你假設事務沒有 Bug 時,這種想法才有意義。如果應用以某種錯誤的方式使用資料庫,例如,不安全地使用弱隔離等級,資料庫的完整性就無法得到保證。
+ACID 意義下的一致性(請參閱 “[一致性](/tw/ch8#sec_transactions_acid_consistency)”)基於這樣一種想法:資料庫以一致的狀態啟動,而事務將其從一個一致狀態轉換至另一個一致的狀態。因此,我們期望資料庫始終處於一致狀態。然而,只有當你假設事務沒有 Bug 時,這種想法才有意義。如果應用以某種錯誤的方式使用資料庫,例如,不安全地使用弱隔離等級,資料庫的完整性就無法得到保證。
-#### 不要盲目信任承諾
+#### 不要盲目信任承諾 {#id364}
由於硬體和軟體並不總是符合我們的理想,所以資料損壞似乎早晚不可避免。因此,我們至少應該有辦法查明資料是否已經損壞,以便我們能夠修復它,並嘗試追查錯誤的來源。檢查資料完整性稱為 **審計(auditing)**。
-如 “[不可變事件的優點](/tw/ch12#不可變事件的優點)” 一節中所述,審計不僅僅適用於財務應用程式。不過,可審計性在財務中是非常非常重要的,因為每個人都知道錯誤總會發生,我們也都認為能夠檢測和解決問題是合理的需求。
+如 “[不可變事件的優點](/tw/ch12#sec_stream_immutability_pros)” 一節中所述,審計不僅僅適用於財務應用程式。不過,可審計性在財務中是非常非常重要的,因為每個人都知道錯誤總會發生,我們也都認為能夠檢測和解決問題是合理的需求。
-成熟的系統同樣傾向於考慮不太可能的事情出錯的可能性,並管理這種風險。例如,HDFS 和 Amazon S3 等大規模儲存系統並不完全信任磁碟:它們執行後臺程序持續回讀檔案,並將其與其他副本進行比較,並將檔案從一個磁碟移動到另一個,以便降低靜默損壞的風險【67】。
+成熟的系統同樣傾向於考慮不太可能的事情出錯的可能性,並管理這種風險。例如,HDFS 和 Amazon S3 等大規模儲存系統並不完全信任磁碟:它們執行後臺程序持續回讀檔案,並將其與其他副本進行比較,並將檔案從一個磁碟移動到另一個,以便降低靜默損壞的風險[^67]。
如果你想確保你的資料仍然存在,你必須真正讀取它並進行檢查。大多數時候它們仍然會在那裡,但如果不是這樣,你一定想盡早知道答案,而不是更晚。按照同樣的原則,不時地嘗試從備份中恢復是非常重要的 —— 否則當你發現備份損壞時,你可能已經遇到了資料丟失,那時候就真的太晚了。不要盲目地相信它們全都管用。
-#### 驗證的文化
+#### 為可審計性而設計 {#id365}
-像 HDFS 和 S3 這樣的系統仍然需要假設磁碟大部分時間都能正常工作 —— 這是一個合理的假設,但與它們 **始終** 能正常工作的假設並不相同。然而目前還沒有多少系統採用這種 “信任但是驗證” 的方式來持續審計自己。許多人認為正確性保證是絕對的,並且沒有為罕見的資料損壞的可能性做過準備。我希望未來能看到更多的 **自我驗證(self-validating)** 或 **自我審計(self-auditing)** 系統,不斷檢查自己的完整性,而不是依賴盲目的信任【68】。
-
-我擔心 ACID 資料庫的文化導致我們在盲目信任技術(如事務機制)的基礎上開發應用,而忽視了這種過程中的任何可審計性。由於我們所信任的技術在大多數情況下工作得很好,通常會認為審計機制並不值得投資。
-
-但隨之而來的是,資料庫的格局發生了變化:在 NoSQL 的旗幟下,更弱的一致性保證成為常態,更不成熟的儲存技術越來越被廣泛使用。但是由於審計機制還沒有被開發出來,儘管這種方式越來越危險,我們仍不斷在盲目信任的基礎上構建應用。讓我們想一想如何針對可審計性而設計吧。
-
-#### 為可審計性而設計
-
-如果一個事務在一個數據庫中改變了多個物件,在這一事實發生後,很難說清這個事務到底意味著什麼。即使你捕獲了事務日誌(請參閱 “[變更資料捕獲](/tw/ch12#變更資料捕獲)”),各種表中的插入、更新和刪除操作並不一定能清楚地表明 **為什麼** 要執行這些變更。決定這些變更的是應用邏輯中的呼叫,而這一應用邏輯稍縱即逝,無法重現。
+如果一個事務在一個數據庫中改變了多個物件,在這一事實發生後,很難說清這個事務到底意味著什麼。即使你捕獲了事務日誌(請參閱 “[變更資料捕獲](/tw/ch12#sec_stream_cdc)”),各種表中的插入、更新和刪除操作並不一定能清楚地表明 **為什麼** 要執行這些變更。決定這些變更的是應用邏輯中的呼叫,而這一應用邏輯稍縱即逝,無法重現。
相比之下,基於事件的系統可以提供更好的可審計性。在事件溯源方法中,系統的使用者輸入被表示為一個單一不可變事件,而任何其導致的狀態變更都派生自該事件。派生可以實現為具有確定性與可重複性,因而相同的事件日誌透過相同版本的派生程式碼時,會導致相同的狀態變更。
-顯式處理資料流(請參閱 “[批處理輸出的哲學](/tw/ch11#批處理輸出的哲學)”)可以使資料的 **來龍去脈(provenance)** 更加清晰,從而使完整性檢查更具可行性。對於事件日誌,我們可以使用雜湊來檢查事件儲存沒有被破壞。對於任何派生狀態,我們可以重新執行從事件日誌中派生它的批處理器與流處理器,以檢查是否獲得相同的結果,或者,甚至並行執行冗餘的派生流程。
+顯式處理資料流(請參閱 “[批處理輸出的哲學](/tw/ch11#sec_batch_output)”)可以使資料的 **來龍去脈(provenance)** 更加清晰,從而使完整性檢查更具可行性。對於事件日誌,我們可以使用雜湊來檢查事件儲存沒有被破壞。對於任何派生狀態,我們可以重新執行從事件日誌中派生它的批處理器與流處理器,以檢查是否獲得相同的結果,或者,甚至並行執行冗餘的派生流程。
-具有確定性且定義良好的資料流,也使除錯與跟蹤系統的執行變得容易,以便確定它 **為什麼** 做了某些事情【4,69】。如果出現意想之外的事情,那麼重現導致意外事件的確切事故現場的診斷能力 —— 一種時間旅行除錯功能是非常有價值的。
+具有確定性且定義良好的資料流,也使除錯與跟蹤系統的執行變得容易,以便確定它 **為什麼** 做了某些事情[^4][^69]。如果出現意想之外的事情,那麼重現導致意外事件的確切事故現場的診斷能力 —— 一種時間旅行除錯功能是非常有價值的。
-#### 端到端原則重現
+#### 端到端原則重現 {#id456}
如果我們不能完全相信系統的每個元件都不會損壞 —— 每一個硬體都沒缺陷,每一個軟體都沒有 Bug —— 那我們至少必須定期檢查資料的完整性。如果我們不檢查,我們就不能發現損壞,直到無可挽回地導致對下游的破壞時,那時候再去追蹤問題就要難得多,且代價也要高的多。
檢查資料系統的完整性,最好是以端到端的方式進行(請參閱 “[資料庫的端到端原則](#資料庫的端到端原則)”):我們能在完整性檢查中涵蓋的系統越多,某些處理階中出現不被察覺損壞的機率就越小。如果我們能檢查整個派生資料管道端到端的正確性,那麼沿著這一路徑的任何磁碟、網路、服務以及演算法的正確性檢查都隱含在其中了。
-持續的端到端完整性檢查可以不斷提高你對系統正確性的信心,從而使你能更快地進步【70】。與自動化測試一樣,審計提高了快速發現錯誤的可能性,從而降低了系統變更或新儲存技術可能導致損失的風險。如果你不害怕進行變更,就可以更好地充分演化一個應用,使其滿足不斷變化的需求。
+持續的端到端完整性檢查可以不斷提高你對系統正確性的信心,從而使你能更快地進步[^70]。與自動化測試一樣,審計提高了快速發現錯誤的可能性,從而降低了系統變更或新儲存技術可能導致損失的風險。如果你不害怕進行變更,就可以更好地充分演化一個應用,使其滿足不斷變化的需求。
-#### 用於可審計資料系統的工具
+#### 用於可審計資料系統的工具 {#id366}
-目前,將可審計性作為頂層關注點的資料系統並不多。一些應用實現了自己的審計機制,例如將所有變更記錄到單獨的審計表中,但是確保審計日誌與資料庫狀態的完整性仍然是很困難的。可以定期使用硬體安全模組對事務日誌進行簽名來防止篡改,但這無法保證正確的事務一開始就能進入到日誌中。
+目前,把可審計性作為一級目標的資料系統還不多。一些應用會實現自己的審計機制(例如把變更寫入獨立審計表),但要同時保證審計日誌與主資料庫狀態都不可篡改仍然很難。
-使用密碼學工具來證明系統的完整性是十分有趣的,這種方式對於寬泛的硬體與軟體問題,甚至是潛在的惡意行為都很穩健有效。加密貨幣、區塊鏈、以及諸如比特幣、以太坊、Ripple、Stellar 的分散式賬本技術已經迅速出現在這一領域【71,72,73】。
+像 Bitcoin、Ethereum 這樣的區塊鏈,本質上是帶密碼學一致性校驗的共享僅追加日誌;交易可視作事件,智慧合約可視作流處理器。它們透過共識協議讓所有節點同意同一事件序列。與本書 [第十章](/tw/ch10) 的共識協議相比,區塊鏈的一個差異是強調拜占庭容錯:參與節點會持續相互校驗完整性[^71][^72][^73]。
-我沒有資格評論這些技術用於貨幣,或者合同商定機制的價值。但從資料系統的角度來看,它們包含了一些有趣的想法。實質上,它們是分散式資料庫,具有資料模型與事務機制,而不同副本可以由互不信任的組織託管。副本不斷檢查其他副本的完整性,並使用共識協議對應當執行的事務達成一致。
+對多數應用而言,區塊鏈整體開銷仍偏高;但其中一些密碼學工具可在更輕量的場景複用。比如 **默克爾樹(Merkle tree)**[^74]可高效證明某條記錄屬於某資料集。**證書透明性(certificate transparency)** 使用可驗證的僅追加日誌與默克爾樹來校驗 TLS/SSL 證書有效性[^75][^76]。
-我對這些技術的拜占庭容錯方面有些懷疑(請參閱 “[拜占庭故障](/tw/ch8#拜占庭故障)”),而且我發現 **工作證明(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】。清除資料與不變性的想法背道而馳(請參閱 “[不變性的侷限性](/tw/ch12#不變性的侷限性)”),但這是可以解決的問題。我所看到的一種很有前景的方法是透過加密協議來實施訪問控制,而不僅僅是透過策略【113,114】。總的來說,文化與態度的改變是必要的。
-
-
-## 本章小結
+## 本章小結 {#id367}
在本章中,我們討論了設計資料系統的新方式,而且也包括了我的個人觀點,以及對未來的猜測。我們從這樣一種觀察開始:沒有單種工具能高效服務所有可能的用例,因此應用必須組合使用幾種不同的軟體才能實現其目標。我們討論了如何使用批處理與事件流來解決這一 **資料整合(data integration)** 問題,以便讓資料變更在不同系統之間流動。
@@ -885,126 +682,86 @@ ACID 意義下的一致性(請參閱 “[一致性](/tw/ch7#一致性)”)
接下來,我們討論了如何確保所有這些處理在出現故障時保持正確。我們看到可伸縮的強完整性保證可以透過非同步事件處理來實現,透過使用端到端操作識別符號使操作冪等,以及透過非同步檢查約束。客戶端可以等到檢查透過,或者不等待繼續前進,但是可能會冒有違反約束需要道歉的風險。這種方法比使用分散式事務的傳統方法更具可伸縮性與可靠性,並且在實踐中適用於很多業務流程。
-透過圍繞資料流構建應用,並非同步檢查約束,我們可以避免絕大多數的協調工作,建立保證完整性且效能仍然表現良好的系統,即使在地理散佈的情況下與出現故障時亦然。然後,我們對使用審計來驗證資料完整性,以及損壞檢測進行了一些討論。
-
-最後,我們退後一步,審視了構建資料密集型應用的一些道德問題。我們看到,雖然資料可以用來做好事,但它也可能造成很大傷害:作出嚴重影響人們生活的決定卻難以申訴,導致歧視與剝削、監視常態化、曝光私密資訊。我們也冒著資料被洩露的風險,並且可能會發現,即使是善意地使用資料也可能會導致意想不到的後果。
-
-由於軟體和資料對世界產生了如此巨大的影響,我們工程師們必須牢記,我們有責任為我們想要的那種世界而努力:一個尊重人們,尊重人性的世界。我希望我們能夠一起為實現這一目標而努力。
+透過圍繞資料流構建應用,並非同步檢查約束,我們可以避免絕大多數協調,構建在地理分佈和故障場景下依然保持完整性且效能良好的系統。隨後我們還討論了如何透過審計驗證完整性、發現損壞,並指出區塊鏈/分散式賬本所使用的一些機制與事件驅動系統在思想上也存在共通之處。
-## 參考文獻
+##### Footnotes
-1. Rachid Belaid: “[Postgres Full-Text Search is Good Enough!](http://rachbelaid.com/postgres-full-text-search-is-good-enough/),” *rachbelaid.com*, July 13, 2015.
-1. Philippe Ajoux, Nathan Bronson, Sanjeev Kumar, et al.: “[Challenges to Adopting Stronger Consistency at Scale](https://www.usenix.org/system/files/conference/hotos15/hotos15-paper-ajoux.pdf),” at *15th USENIX Workshop on Hot Topics in Operating Systems* (HotOS), May 2015.
-1. Pat Helland and Dave Campbell: “[Building on Quicksand](https://web.archive.org/web/20220606172817/https://database.cs.wisc.edu/cidr/cidr2009/Paper_133.pdf),” at *4th Biennial Conference on Innovative Data Systems Research* (CIDR), January 2009.
-1. Jessica Kerr: “[Provenance and Causality in Distributed Systems](https://web.archive.org/web/20190425150540/http://blog.jessitron.com/2016/09/provenance-and-causality-in-distributed.html),” *blog.jessitron.com*, September 25, 2016.
-1. Kostas Tzoumas: “[Batch Is a Special Case of Streaming](http://data-artisans.com/blog/batch-is-a-special-case-of-streaming/),” *data-artisans.com*, September 15, 2015.
-1. Shinji Kim and Robert Blafford: “[Stream Windowing Performance Analysis: Concord and Spark Streaming](https://web.archive.org/web/20180125074821/http://concord.io/posts/windowing_performance_analysis_w_spark_streaming),” *concord.io*, July 6, 2016.
-1. Jay Kreps: “[The Log: What Every Software Engineer Should Know About Real-Time Data's Unifying Abstraction](http://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying),” *engineering.linkedin.com*, December 16, 2013.
-1. Pat Helland: “[Life Beyond Distributed Transactions: An Apostate’s Opinion](https://web.archive.org/web/20200730171311/http://www-db.cs.wisc.edu/cidr/cidr2007/papers/cidr07p15.pdf),” at *3rd Biennial Conference on Innovative Data Systems Research* (CIDR), January 2007.
-1. “[Great Western Railway (1835–1948)](https://web.archive.org/web/20160122155425/https://www.networkrail.co.uk/VirtualArchive/great-western/),” Network Rail Virtual Archive, *networkrail.co.uk*.
-1. Jacqueline Xu: “[Online Migrations at Scale](https://stripe.com/blog/online-migrations),” *stripe.com*, February 2, 2017.
-1. Molly Bartlett Dishman and Martin Fowler: “[Agile Architecture](https://web.archive.org/web/20161130034721/http://conferences.oreilly.com/software-architecture/sa2015/public/schedule/detail/40388),” at *O'Reilly Software Architecture Conference*, March 2015.
-1. Nathan Marz and James Warren: [*Big Data: Principles and Best Practices of Scalable Real-Time Data Systems*](https://www.manning.com/books/big-data). Manning, 2015. ISBN: 978-1-617-29034-3
-1. Oscar Boykin, Sam Ritchie, Ian O'Connell, and Jimmy Lin: “[Summingbird: A Framework for Integrating Batch and Online MapReduce Computations](http://www.vldb.org/pvldb/vol7/p1441-boykin.pdf),” at *40th International Conference on Very Large Data Bases* (VLDB), September 2014.
-1. Jay Kreps: “[Questioning the Lambda Architecture](https://www.oreilly.com/ideas/questioning-the-lambda-architecture),” *oreilly.com*, July 2, 2014.
-1. Raul Castro Fernandez, Peter Pietzuch, Jay Kreps, et al.: “[Liquid: Unifying Nearline and Offline Big Data Integration](http://cidrdb.org/cidr2015/Papers/CIDR15_Paper25u.pdf),” at *7th Biennial Conference on Innovative Data Systems Research* (CIDR), January 2015.
-1. Dennis M. Ritchie and Ken Thompson: “[The UNIX Time-Sharing System](http://web.eecs.utk.edu/~qcao1/cs560/papers/paper-unix.pdf),” *Communications of the ACM*, volume 17, number 7, pages 365–375, July 1974. [doi:10.1145/361011.361061](http://dx.doi.org/10.1145/361011.361061)
-1. Eric A. Brewer and Joseph M. Hellerstein: “[CS262a: Advanced Topics in Computer Systems](http://people.eecs.berkeley.edu/~brewer/cs262/systemr.html),” lecture notes, University of California, Berkeley, *cs.berkeley.edu*, August 2011.
-1. Michael Stonebraker: “[The Case for Polystores](http://wp.sigmod.org/?p=1629),” *wp.sigmod.org*, July 13, 2015.
-1. Jennie Duggan, Aaron J. Elmore, Michael Stonebraker, et al.: “[The BigDAWG Polystore System](https://dspace.mit.edu/handle/1721.1/100936),” *ACM SIGMOD Record*, volume 44, number 2, pages 11–16, June 2015. [doi:10.1145/2814710.2814713](http://dx.doi.org/10.1145/2814710.2814713)
-1. Patrycja Dybka: “[Foreign Data Wrappers for PostgreSQL](https://web.archive.org/web/20221003115732/https://www.vertabelo.com/blog/foreign-data-wrappers-for-postgresql/),” *vertabelo.com*, March 24, 2015.
-1. David B. Lomet, Alan Fekete, Gerhard Weikum, and Mike Zwilling: “[Unbundling Transaction Services in the Cloud](https://www.microsoft.com/en-us/research/publication/unbundling-transaction-services-in-the-cloud/),” at *4th Biennial Conference on Innovative Data Systems Research* (CIDR), January 2009.
-1. Martin Kleppmann and Jay Kreps: “[Kafka, Samza and the Unix Philosophy of Distributed Data](http://martin.kleppmann.com/papers/kafka-debull15.pdf),” *IEEE Data Engineering Bulletin*, volume 38, number 4, pages 4–14, December 2015.
-1. John Hugg: “[Winning Now and in the Future: Where VoltDB Shines](https://voltdb.com/blog/winning-now-and-future-where-voltdb-shines),” *voltdb.com*, March 23, 2016.
-1. Frank McSherry, Derek G. Murray, Rebecca Isaacs, and Michael Isard: “[Differential Dataflow](http://cidrdb.org/cidr2013/Papers/CIDR13_Paper111.pdf),” at *6th Biennial Conference on Innovative Data Systems Research* (CIDR), January 2013.
-1. Derek G Murray, Frank McSherry, Rebecca Isaacs, et al.: “[Naiad: A Timely Dataflow System](http://sigops.org/s/conferences/sosp/2013/papers/p439-murray.pdf),” at *24th ACM Symposium on Operating Systems Principles* (SOSP), pages 439–455, November 2013. [doi:10.1145/2517349.2522738](http://dx.doi.org/10.1145/2517349.2522738)
-1. 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?’](https://twitter.com/gwenshap/status/758800071110430720)” *twitter.com*, July 28, 2016.
-1. Martin Kleppmann: “[Turning the Database Inside-out with Apache Samza,](http://martin.kleppmann.com/2015/03/04/turning-the-database-inside-out.html)” at *Strange Loop*, September 2014.
-1. Peter Van Roy and Seif Haridi: [*Concepts, Techniques, and Models of Computer Programming*](https://www.info.ucl.ac.be/~pvr/book.html). MIT Press, 2004. ISBN: 978-0-262-22069-9
-1. “[Juttle Documentation](http://juttle.github.io/juttle/),” *juttle.github.io*, 2016.
-1. Evan Czaplicki and Stephen Chong: “[Asynchronous Functional Reactive Programming for GUIs](http://people.seas.harvard.edu/~chong/pubs/pldi13-elm.pdf),” at *34th ACM SIGPLAN Conference on Programming Language Design and Implementation* (PLDI), June 2013. [doi:10.1145/2491956.2462161](http://dx.doi.org/10.1145/2491956.2462161)
-1. Engineer Bainomugisha, Andoni Lombide Carreton, Tom van Cutsem, Stijn Mostinckx, and Wolfgang de Meuter: “[A Survey on Reactive Programming](http://soft.vub.ac.be/Publications/2012/vub-soft-tr-12-13.pdf),” *ACM Computing Surveys*, volume 45, number 4, pages 1–34, August 2013. [doi:10.1145/2501654.2501666](http://dx.doi.org/10.1145/2501654.2501666)
-1. Peter Alvaro, Neil Conway, Joseph M. Hellerstein, and William R. Marczak: “[Consistency Analysis in Bloom: A CALM and Collected Approach](https://dsf.berkeley.edu/cs286/papers/calm-cidr2011.pdf),” at *5th Biennial Conference on Innovative Data Systems Research* (CIDR), January 2011.
-1. Felienne Hermans: “[Spreadsheets Are Code](https://vimeo.com/145492419),” at *Code Mesh*, November 2015.
-1. Dan Bricklin and Bob Frankston: “[VisiCalc: Information from Its Creators](http://danbricklin.com/visicalc.htm),” *danbricklin.com*.
-1. D. Sculley, Gary Holt, Daniel Golovin, et al.: “[Machine Learning: The High-Interest Credit Card of Technical Debt](http://research.google.com/pubs/pub43146.html),” at *NIPS Workshop on Software Engineering for Machine Learning* (SE4ML), December 2014.
-1. Peter Bailis, Alan Fekete, Michael J Franklin, et al.: “[Feral Concurrency Control: An Empirical Investigation of Modern Application Integrity](http://www.bailis.org/papers/feral-sigmod2015.pdf),” at *ACM International Conference on Management of Data* (SIGMOD), June 2015. [doi:10.1145/2723372.2737784](http://dx.doi.org/10.1145/2723372.2737784)
-1. Guy Steele: “[Re: Need for Macros (Was Re: Icon)](https://people.csail.mit.edu/gregs/ll1-discuss-archive-html/msg01134.html),” email to *ll1-discuss* mailing list, *people.csail.mit.edu*, December 24, 2001.
-1. David Gelernter: “[Generative Communication in Linda](http://cseweb.ucsd.edu/groups/csag/html/teaching/cse291s03/Readings/p80-gelernter.pdf),” *ACM Transactions on Programming Languages and Systems* (TOPLAS), volume 7, number 1, pages 80–112, January 1985. [doi:10.1145/2363.2433](http://dx.doi.org/10.1145/2363.2433)
-1. Patrick Th. Eugster, Pascal A. Felber, Rachid Guerraoui, and Anne-Marie Kermarrec: “[The Many Faces of Publish/Subscribe](http://www.cs.ru.nl/~pieter/oss/manyfaces.pdf),” *ACM Computing Surveys*, volume 35, number 2, pages 114–131, June 2003. [doi:10.1145/857076.857078](http://dx.doi.org/10.1145/857076.857078)
-1. Ben Stopford: “[Microservices in a Streaming World](https://www.infoq.com/presentations/microservices-streaming),” at *QCon London*, March 2016.
-1. Christian Posta: “[Why Microservices Should Be Event Driven: Autonomy vs Authority](http://blog.christianposta.com/microservices/why-microservices-should-be-event-driven-autonomy-vs-authority/),” *blog.christianposta.com*, May 27, 2016.
-1. Alex Feyerke: “[Say Hello to Offline First](https://web.archive.org/web/20210420014747/http://hood.ie/blog/say-hello-to-offline-first.html),” *hood.ie*, November 5, 2013.
-1. Sebastian Burckhardt, Daan Leijen, Jonathan Protzenko, and Manuel Fähndrich: “[Global Sequence Protocol: A Robust Abstraction for Replicated Shared State](http://drops.dagstuhl.de/opus/volltexte/2015/5238/),” at *29th European Conference on Object-Oriented Programming* (ECOOP), July 2015. [doi:10.4230/LIPIcs.ECOOP.2015.568](http://dx.doi.org/10.4230/LIPIcs.ECOOP.2015.568)
-1. Mark Soper: “[Clearing Up React Data Management Confusion with Flux, Redux, and Relay](https://medium.com/@marksoper/clearing-up-react-data-management-confusion-with-flux-redux-and-relay-aad504e63cae),” *medium.com*, December 3, 2015.
-1. Eno Thereska, Damian Guy, Michael Noll, and Neha Narkhede: “[Unifying Stream Processing and Interactive Queries in Apache Kafka](http://www.confluent.io/blog/unifying-stream-processing-and-interactive-queries-in-apache-kafka/),” *confluent.io*, October 26, 2016.
-1. Frank McSherry: “[Dataflow as Database](https://github.com/frankmcsherry/blog/blob/master/posts/2016-07-17.md),” *github.com*, July 17, 2016.
-1. Peter Alvaro: “[I See What You Mean](https://www.youtube.com/watch?v=R2Aa4PivG0g),” at *Strange Loop*, September 2015.
-1. Nathan Marz: “[Trident: A High-Level Abstraction for Realtime Computation](https://blog.twitter.com/2012/trident-a-high-level-abstraction-for-realtime-computation),” *blog.twitter.com*, August 2, 2012.
-1. Edi Bice: “[Low Latency Web Scale Fraud Prevention with Apache Samza, Kafka and Friends](http://www.slideshare.net/edibice/extremely-low-latency-web-scale-fraud-prevention-with-apache-samza-kafka-and-friends),” at *Merchant Risk Council MRC Vegas Conference*, March 2016.
-1. Charity Majors: “[The Accidental DBA](https://charity.wtf/2016/10/02/the-accidental-dba/),” *charity.wtf*, October 2, 2016.
-1. Arthur J. Bernstein, Philip M. Lewis, and Shiyong Lu: “[Semantic Conditions for Correctness at Different Isolation Levels](http://db.cs.berkeley.edu/cs286/papers/isolation-icde2000.pdf),” at *16th International Conference on Data Engineering* (ICDE), February 2000. [doi:10.1109/ICDE.2000.839387](http://dx.doi.org/10.1109/ICDE.2000.839387)
-1. Sudhir Jorwekar, Alan Fekete, Krithi Ramamritham, and S. Sudarshan: “[Automating the Detection of Snapshot Isolation Anomalies](http://www.vldb.org/conf/2007/papers/industrial/p1263-jorwekar.pdf),” at *33rd International Conference on Very Large Data Bases* (VLDB), September 2007.
-1. Kyle Kingsbury: [Jepsen blog post series](https://aphyr.com/tags/jepsen), *aphyr.com*, 2013–2016.
-1. Michael Jouravlev: “[Redirect After Post](http://www.theserverside.com/news/1365146/Redirect-After-Post),” *theserverside.com*, August 1, 2004.
-1. Jerome H. Saltzer, David P. Reed, and David D. Clark: “[End-to-End Arguments in System Design](https://groups.csail.mit.edu/ana/Publications/PubPDFs/End-to-End%20Arguments%20in%20System%20Design.pdf),” *ACM Transactions on Computer Systems*, volume 2, number 4, pages 277–288, November 1984. [doi:10.1145/357401.357402](http://dx.doi.org/10.1145/357401.357402)
-1. Peter Bailis, Alan Fekete, Michael J. Franklin, et al.: “[Coordination-Avoiding Database Systems](http://arxiv.org/pdf/1402.2237.pdf),” *Proceedings of the VLDB Endowment*, volume 8, number 3, pages 185–196, November 2014.
-1. Alex Yarmula: “[Strong Consistency in Manhattan](https://blog.twitter.com/2016/strong-consistency-in-manhattan),” *blog.twitter.com*, March 17, 2016.
-1. Douglas B Terry, Marvin M Theimer, Karin Petersen, et al.: “[Managing Update Conflicts in Bayou, a Weakly Connected Replicated Storage System](http://css.csail.mit.edu/6.824/2014/papers/bayou-conflicts.pdf),” at *15th ACM Symposium on Operating Systems Principles* (SOSP), pages 172–182, December 1995. [doi:10.1145/224056.224070](http://dx.doi.org/10.1145/224056.224070)
-1. Jim Gray: “[The Transaction Concept: Virtues and Limitations](http://jimgray.azurewebsites.net/papers/thetransactionconcept.pdf),” at *7th International Conference on Very Large Data Bases* (VLDB), September 1981.
-1. Hector Garcia-Molina and Kenneth Salem: “[Sagas](http://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf),” at *ACM International Conference on Management of Data* (SIGMOD), May 1987. [doi:10.1145/38713.38742](http://dx.doi.org/10.1145/38713.38742)
-1. Pat Helland: “[Memories, Guesses, and Apologies](https://web.archive.org/web/20160304020907/http://blogs.msdn.com/b/pathelland/archive/2007/05/15/memories-guesses-and-apologies.aspx),” *blogs.msdn.com*, May 15, 2007.
-1. Yoongu Kim, Ross Daly, Jeremie Kim, et al.: “[Flipping Bits in Memory Without Accessing Them: An Experimental Study of DRAM Disturbance Errors](https://users.ece.cmu.edu/~yoonguk/papers/kim-isca14.pdf),” at *41st Annual International Symposium on Computer Architecture* (ISCA), June 2014. [doi:10.1145/2678373.2665726](http://dx.doi.org/10.1145/2678373.2665726)
-1. Mark Seaborn and Thomas Dullien: “[Exploiting the DRAM Rowhammer Bug to Gain Kernel Privileges](https://googleprojectzero.blogspot.co.uk/2015/03/exploiting-dram-rowhammer-bug-to-gain.html),” *googleprojectzero.blogspot.co.uk*, March 9, 2015.
-1. Jim N. Gray and Catharine van Ingen: “[Empirical Measurements of Disk Failure Rates and Error Rates](https://www.microsoft.com/en-us/research/publication/empirical-measurements-of-disk-failure-rates-and-error-rates/),” Microsoft Research, MSR-TR-2005-166, December 2005.
-1. Annamalai Gurusami and Daniel Price: “[Bug #73170: Duplicates in Unique Secondary Index Because of Fix of Bug#68021](http://bugs.mysql.com/bug.php?id=73170),” *bugs.mysql.com*, July 2014.
-1. Gary Fredericks: “[Postgres Serializability Bug](https://github.com/gfredericks/pg-serializability-bug),” *github.com*, September 2015.
-1. Xiao Chen: “[HDFS DataNode Scanners and Disk Checker Explained](http://blog.cloudera.com/blog/2016/12/hdfs-datanode-scanners-and-disk-checker-explained/),” *blog.cloudera.com*, December 20, 2016.
-1. Jay Kreps: “[Getting Real About Distributed System Reliability](http://blog.empathybox.com/post/19574936361/getting-real-about-distributed-system-reliability),” *blog.empathybox.com*, March 19, 2012.
-1. Martin Fowler: “[The LMAX Architecture](http://martinfowler.com/articles/lmax.html),” *martinfowler.com*, July 12, 2011.
-1. Sam Stokes: “[Move Fast with Confidence](http://blog.samstokes.co.uk/blog/2016/07/11/move-fast-with-confidence/),” *blog.samstokes.co.uk*, July 11, 2016.
-1. “[Hyperledger Sawtooth documentation](https://web.archive.org/web/20220120211548/https://sawtooth.hyperledger.org/docs/core/releases/latest/introduction.html),” Intel Corporation, *sawtooth.hyperledger.org*, 2017.
-1. Richard Gendal Brown: “[Introducing R3 Corda™: A Distributed Ledger Designed for Financial Services](https://gendal.me/2016/04/05/introducing-r3-corda-a-distributed-ledger-designed-for-financial-services/),” *gendal.me*, April 5, 2016.
-1. Trent McConaghy, Rodolphe Marques, Andreas Müller, et al.: “[BigchainDB: A Scalable Blockchain Database](https://www.bigchaindb.com/whitepaper/bigchaindb-whitepaper.pdf),” *bigchaindb.com*, June 8, 2016.
-1. Ralph C. Merkle: “[A Digital Signature Based on a Conventional Encryption Function](https://people.eecs.berkeley.edu/~raluca/cs261-f15/readings/merkle.pdf),” at *CRYPTO '87*, August 1987. [doi:10.1007/3-540-48184-2_32](http://dx.doi.org/10.1007/3-540-48184-2_32)
-1. Ben Laurie: “[Certificate Transparency](http://queue.acm.org/detail.cfm?id=2668154),” *ACM Queue*, volume 12, number 8, pages 10-19, August 2014. [doi:10.1145/2668152.2668154](http://dx.doi.org/10.1145/2668152.2668154)
-1. Mark D. Ryan: “[Enhanced Certificate Transparency and End-to-End Encrypted Mail](https://www.ndss-symposium.org/wp-content/uploads/2017/09/12_2_1.pdf),” at *Network and Distributed System Security Symposium* (NDSS), February 2014. [doi:10.14722/ndss.2014.23379](http://dx.doi.org/10.14722/ndss.2014.23379)
-1. “[ACM Code of Ethics and Professional Conduct](https://www.acm.org/code-of-ethics),” Association for Computing Machinery, *acm.org*, 2018.
-1. François Chollet: “[Software development is starting to involve important ethical choices](https://twitter.com/fchollet/status/792958695722201088),” *twitter.com*, October 30, 2016.
-1. Igor Perisic: “[Making Hard Choices: The Quest for Ethics in Machine Learning](https://engineering.linkedin.com/blog/2016/11/making-hard-choices--the-quest-for-ethics-in-machine-learning),” *engineering.linkedin.com*, November 2016.
-1. John Naughton: “[Algorithm Writers Need a Code of Conduct](https://www.theguardian.com/commentisfree/2015/dec/06/algorithm-writers-should-have-code-of-conduct),” *theguardian.com*, December 6, 2015.
-1. Logan Kugler: “[What Happens When Big Data Blunders?](http://cacm.acm.org/magazines/2016/6/202655-what-happens-when-big-data-blunders/fulltext),” *Communications of the ACM*, volume 59, number 6, pages 15–16, June 2016. [doi:10.1145/2911975](http://dx.doi.org/10.1145/2911975)
-1. Bill Davidow: “[Welcome to Algorithmic Prison](http://www.theatlantic.com/technology/archive/2014/02/welcome-to-algorithmic-prison/283985/),” *theatlantic.com*, February 20, 2014.
-1. Don Peck: “[They're Watching You at Work](http://www.theatlantic.com/magazine/archive/2013/12/theyre-watching-you-at-work/354681/),” *theatlantic.com*, December 2013.
-1. Leigh Alexander: “[Is an Algorithm Any Less Racist Than a Human?](https://www.theguardian.com/technology/2016/aug/03/algorithm-racist-human-employers-work)” *theguardian.com*, August 3, 2016.
-1. Jesse Emspak: “[How a Machine Learns Prejudice](https://www.scientificamerican.com/article/how-a-machine-learns-prejudice/),” *scientificamerican.com*, December 29, 2016.
-1. Maciej Cegłowski: “[The Moral Economy of Tech](http://idlewords.com/talks/sase_panel.htm),” *idlewords.com*, June 2016.
-1. Cathy O'Neil: [*Weapons of Math Destruction: How Big Data Increases Inequality and Threatens Democracy*](https://web.archive.org/web/20210621234447/https://weaponsofmathdestructionbook.com/). Crown Publishing, 2016. ISBN: 978-0-553-41881-1
-1. Julia Angwin: “[Make Algorithms Accountable](http://www.nytimes.com/2016/08/01/opinion/make-algorithms-accountable.html),” *nytimes.com*, August 1, 2016.
-1. Bryce Goodman and Seth Flaxman: “[European Union Regulations on Algorithmic Decision-Making and a ‘Right to Explanation’](https://arxiv.org/abs/1606.08813),” *arXiv:1606.08813*, August 31, 2016.
-1. “[A Review of the Data Broker Industry: Collection, Use, and Sale of Consumer Data for Marketing Purposes](https://web.archive.org/web/20240619042302/http://educationnewyork.com/files/rockefeller_databroker.pdf),” Staff Report, *United States Senate Committee on Commerce, Science, and Transportation*, *commerce.senate.gov*, December 2013.
-1. Olivia Solon: “[Facebook’s Failure: Did Fake News and Polarized Politics Get Trump Elected?](https://www.theguardian.com/technology/2016/nov/10/facebook-fake-news-election-conspiracy-theories)” *theguardian.com*, November 10, 2016.
-1. Donella H. Meadows and Diana Wright: *Thinking in Systems: A Primer*. Chelsea Green Publishing, 2008. ISBN: 978-1-603-58055-7
-1. Daniel J. Bernstein: “[Listening to a ‘big data’/‘data science’ talk](https://twitter.com/hashbreaker/status/598076230437568512),” *twitter.com*, May 12, 2015.
-1. Marc Andreessen: “[Why Software Is Eating the World](http://genius.com/Marc-andreessen-why-software-is-eating-the-world-annotated),” *The Wall Street Journal*, 20 August 2011.
-1. J. M. Porup: “[‘Internet of Things’ Security Is Hilariously Broken and Getting Worse](http://arstechnica.com/security/2016/01/how-to-search-the-internet-of-things-for-photos-of-sleeping-babies/),” *arstechnica.com*, January 23, 2016.
-1. Bruce Schneier: [*Data and Goliath: The Hidden Battles to Collect Your Data and Control Your World*](https://www.schneier.com/books/data_and_goliath/). W. W. Norton, 2015. ISBN: 978-0-393-35217-7
-1. The Grugq: “[Nothing to Hide](https://grugq.tumblr.com/post/142799983558/nothing-to-hide),” *grugq.tumblr.com*, April 15, 2016.
-1. Tony Beltramelli: “[Deep-Spying: Spying Using Smartwatch and Deep Learning](https://arxiv.org/abs/1512.05616),” Masters Thesis, IT University of Copenhagen, December 2015. Available at *arxiv.org/abs/1512.05616*
-1. Shoshana Zuboff: “[Big Other: Surveillance Capitalism and the Prospects of an Information Civilization](http://papers.ssrn.com/sol3/papers.cfm?abstract_id=2594754),” *Journal of Information Technology*, volume 30, number 1, pages 75–89, April 2015. [doi:10.1057/jit.2015.5](http://dx.doi.org/10.1057/jit.2015.5)
-1. Carina C. Zona: “[Consequences of an Insightful Algorithm](https://www.youtube.com/watch?v=YRI40A4tyWU),” at *GOTO Berlin*, November 2016.
-1. Bruce Schneier: “[Data Is a Toxic Asset, So Why Not Throw It Out?](https://www.schneier.com/essays/archives/2016/03/data_is_a_toxic_asse.html),” *schneier.com*, March 1, 2016.
-1. John E. Dunn: “[The UK’s 15 Most Infamous Data Breaches](https://web.archive.org/web/20161120070058/http://www.techworld.com/security/uks-most-infamous-data-breaches-2016-3604586/),” *techworld.com*, November 18, 2016.
-1. Cory Scott: “[Data is not toxic - which implies no benefit - but rather hazardous material, where we must balance need vs. want](https://twitter.com/cory_scott/status/706586399483437056),” *twitter.com*, March 6, 2016.
-1. Bruce Schneier: “[Mission Creep: When Everything Is Terrorism](https://www.schneier.com/essays/archives/2013/07/mission_creep_when_e.html),” *schneier.com*, July 16, 2013.
-1. Lena Ulbricht and Maximilian von Grafenstein: “[Big Data: Big Power Shifts?](http://policyreview.info/articles/analysis/big-data-big-power-shifts),” *Internet Policy Review*, volume 5, number 1, March 2016. [doi:10.14763/2016.1.406](http://dx.doi.org/10.14763/2016.1.406)
-1. Ellen P. Goodman and Julia Powles: “[Facebook and Google: Most Powerful and Secretive Empires We've Ever Known](https://www.theguardian.com/technology/2016/sep/28/google-facebook-powerful-secretive-empire-transparency),” *theguardian.com*, September 28, 2016.
-1. [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](http://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:31995L0046), Official Journal of the European Communities No. L 281/31, *eur-lex.europa.eu*, November 1995.
-1. Brendan Van Alsenoy: “[Regulating Data Protection: The Allocation of Responsibility and Risk Among Actors Involved in Personal Data Processing](https://lirias.kuleuven.be/handle/123456789/545027),” Thesis, KU Leuven Centre for IT and IP Law, August 2016.
-1. Michiel Rhoen: “[Beyond Consent: Improving Data Protection Through Consumer Protection Law](http://policyreview.info/articles/analysis/beyond-consent-improving-data-protection-through-consumer-protection-law),” *Internet Policy Review*, volume 5, number 1, March 2016. [doi:10.14763/2016.1.404](http://dx.doi.org/10.14763/2016.1.404)
-1. Jessica Leber: “[Your Data Footprint Is Affecting Your Life in Ways You Can’t Even Imagine](https://www.fastcoexist.com/3057514/your-data-footprint-is-affecting-your-life-in-ways-you-cant-even-imagine),” *fastcoexist.com*, March 15, 2016.
-1. Maciej Cegłowski: “[Haunted by Data](http://idlewords.com/talks/haunted_by_data.htm),” *idlewords.com*, October 2015.
-1. Sam Thielman: “[You Are Not What You Read: Librarians Purge User Data to Protect Privacy](https://www.theguardian.com/us-news/2016/jan/13/us-library-records-purged-data-privacy),” *theguardian.com*, January 13, 2016.
-1. Conor Friedersdorf: “[Edward Snowden’s Other Motive for Leaking](http://www.theatlantic.com/politics/archive/2014/05/edward-snowdens-other-motive-for-leaking/370068/),” *theatlantic.com*, May 13, 2014.
-1. Phillip Rogaway: “[The Moral Character of Cryptographic Work](http://web.cs.ucdavis.edu/~rogaway/papers/moral-fn.pdf),” Cryptology ePrint 2015/1162, December 2015.
\ No newline at end of file
+### References {#references}
+
+[^1]: Rachid Belaid: “[Postgres Full-Text Search is Good Enough!](http://rachbelaid.com/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](https://www.usenix.org/system/files/conference/hotos15/hotos15-paper-ajoux.pdf),” at *15th USENIX Workshop on Hot Topics in Operating Systems* (HotOS), May 2015.
+[^3]: Pat Helland and Dave Campbell: “[Building on Quicksand](https://web.archive.org/web/20220606172817/https://database.cs.wisc.edu/cidr/cidr2009/Paper_133.pdf),” at *4th Biennial Conference on Innovative Data Systems Research* (CIDR), January 2009.
+[^4]: Jessica Kerr: “[Provenance and Causality in Distributed Systems](https://web.archive.org/web/20190425150540/http://blog.jessitron.com/2016/09/provenance-and-causality-in-distributed.html),” *blog.jessitron.com*, September 25, 2016.
+[^5]: Kostas Tzoumas: “[Batch Is a Special Case of Streaming](http://data-artisans.com/blog/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](https://web.archive.org/web/20180125074821/http://concord.io/posts/windowing_performance_analysis_w_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](http://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying),” *engineering.linkedin.com*, December 16, 2013.
+[^8]: Pat Helland: “[Life Beyond Distributed Transactions: An Apostate’s Opinion](https://web.archive.org/web/20200730171311/http://www-db.cs.wisc.edu/cidr/cidr2007/papers/cidr07p15.pdf),” at *3rd Biennial Conference on Innovative Data Systems Research* (CIDR), January 2007.
+[^9]: “[Great Western Railway (1835–1948)](https://web.archive.org/web/20160122155425/https://www.networkrail.co.uk/VirtualArchive/great-western/),” Network Rail Virtual Archive, *networkrail.co.uk*.
+[^10]: Jacqueline Xu: “[Online Migrations at Scale](https://stripe.com/blog/online-migrations),” *stripe.com*, February 2, 2017.
+[^11]: Molly Bartlett Dishman and Martin Fowler: “[Agile Architecture](https://web.archive.org/web/20161130034721/http://conferences.oreilly.com/software-architecture/sa2015/public/schedule/detail/40388),” 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*](https://www.manning.com/books/big-data). 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](http://www.vldb.org/pvldb/vol7/p1441-boykin.pdf),” at *40th International Conference on Very Large Data Bases* (VLDB), September 2014.
+[^14]: Jay Kreps: “[Questioning the Lambda Architecture](https://www.oreilly.com/ideas/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](http://cidrdb.org/cidr2015/Papers/CIDR15_Paper25u.pdf),” at *7th Biennial Conference on Innovative Data Systems Research* (CIDR), January 2015.
+[^16]: Dennis M. Ritchie and Ken Thompson: “[The UNIX Time-Sharing System](http://web.eecs.utk.edu/~qcao1/cs560/papers/paper-unix.pdf),” *Communications of the ACM*, volume 17, number 7, pages 365–375, July 1974. [doi:10.1145/361011.361061](http://dx.doi.org/10.1145/361011.361061)
+[^17]: Eric A. Brewer and Joseph M. Hellerstein: “[CS262a: Advanced Topics in Computer Systems](http://people.eecs.berkeley.edu/~brewer/cs262/systemr.html),” lecture notes, University of California, Berkeley, *cs.berkeley.edu*, August 2011.
+[^18]: Michael Stonebraker: “[The Case for Polystores](http://wp.sigmod.org/?p=1629),” *wp.sigmod.org*, July 13, 2015.
+[^19]: Jennie Duggan, Aaron J. Elmore, Michael Stonebraker, et al.: “[The BigDAWG Polystore System](https://dspace.mit.edu/handle/1721.1/100936),” *ACM SIGMOD Record*, volume 44, number 2, pages 11–16, June 2015. [doi:10.1145/2814710.2814713](http://dx.doi.org/10.1145/2814710.2814713)
+[^20]: Patrycja Dybka: “[Foreign Data Wrappers for PostgreSQL](https://web.archive.org/web/20221003115732/https://www.vertabelo.com/blog/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](https://www.microsoft.com/en-us/research/publication/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](http://martin.kleppmann.com/papers/kafka-debull15.pdf),” *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](https://voltdb.com/blog/winning-now-and-future-where-voltdb-shines),” *voltdb.com*, March 23, 2016.
+[^24]: Frank McSherry, Derek G. Murray, Rebecca Isaacs, and Michael Isard: “[Differential Dataflow](http://cidrdb.org/cidr2013/Papers/CIDR13_Paper111.pdf),” 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](http://sigops.org/s/conferences/sosp/2013/papers/p439-murray.pdf),” at *24th ACM Symposium on Operating Systems Principles* (SOSP), pages 439–455, November 2013. [doi:10.1145/2517349.2522738](http://dx.doi.org/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?’](https://twitter.com/gwenshap/status/758800071110430720)” *twitter.com*, July 28, 2016.
+[^27]: Martin Kleppmann: “[Turning the Database Inside-out with Apache Samza,](http://martin.kleppmann.com/2015/03/04/turning-the-database-inside-out.html)” at *Strange Loop*, September 2014.
+[^28]: Peter Van Roy and Seif Haridi: [*Concepts, Techniques, and Models of Computer Programming*](https://www.info.ucl.ac.be/~pvr/book.html). MIT Press, 2004. ISBN: 978-0-262-22069-9
+[^29]: “[Juttle Documentation](http://juttle.github.io/juttle/),” *juttle.github.io*, 2016.
+[^30]: Evan Czaplicki and Stephen Chong: “[Asynchronous Functional Reactive Programming for GUIs](http://people.seas.harvard.edu/~chong/pubs/pldi13-elm.pdf),” at *34th ACM SIGPLAN Conference on Programming Language Design and Implementation* (PLDI), June 2013. [doi:10.1145/2491956.2462161](http://dx.doi.org/10.1145/2491956.2462161)
+[^31]: Engineer Bainomugisha, Andoni Lombide Carreton, Tom van Cutsem, Stijn Mostinckx, and Wolfgang de Meuter: “[A Survey on Reactive Programming](http://soft.vub.ac.be/Publications/2012/vub-soft-tr-12-13.pdf),” *ACM Computing Surveys*, volume 45, number 4, pages 1–34, August 2013. [doi:10.1145/2501654.2501666](http://dx.doi.org/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](https://dsf.berkeley.edu/cs286/papers/calm-cidr2011.pdf),” at *5th Biennial Conference on Innovative Data Systems Research* (CIDR), January 2011.
+[^33]: Felienne Hermans: “[Spreadsheets Are Code](https://vimeo.com/145492419),” at *Code Mesh*, November 2015.
+[^34]: Dan Bricklin and Bob Frankston: “[VisiCalc: Information from Its Creators](http://danbricklin.com/visicalc.htm),” *danbricklin.com*.
+[^35]: D. Sculley, Gary Holt, Daniel Golovin, et al.: “[Machine Learning: The High-Interest Credit Card of Technical Debt](http://research.google.com/pubs/pub43146.html),” 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](http://www.bailis.org/papers/feral-sigmod2015.pdf),” at *ACM International Conference on Management of Data* (SIGMOD), June 2015. [doi:10.1145/2723372.2737784](http://dx.doi.org/10.1145/2723372.2737784)
+[^37]: Guy Steele: “[Re: Need for Macros (Was Re: Icon)](https://people.csail.mit.edu/gregs/ll1-discuss-archive-html/msg01134.html),” email to *ll1-discuss* mailing list, *people.csail.mit.edu*, December 24, 2001.
+[^38]: David Gelernter: “[Generative Communication in Linda](http://cseweb.ucsd.edu/groups/csag/html/teaching/cse291s03/Readings/p80-gelernter.pdf),” *ACM Transactions on Programming Languages and Systems* (TOPLAS), volume 7, number 1, pages 80–112, January 1985. [doi:10.1145/2363.2433](http://dx.doi.org/10.1145/2363.2433)
+[^39]: Patrick Th. Eugster, Pascal A. Felber, Rachid Guerraoui, and Anne-Marie Kermarrec: “[The Many Faces of Publish/Subscribe](http://www.cs.ru.nl/~pieter/oss/manyfaces.pdf),” *ACM Computing Surveys*, volume 35, number 2, pages 114–131, June 2003. [doi:10.1145/857076.857078](http://dx.doi.org/10.1145/857076.857078)
+[^40]: Ben Stopford: “[Microservices in a Streaming World](https://www.infoq.com/presentations/microservices-streaming),” at *QCon London*, March 2016.
+[^41]: Christian Posta: “[Why Microservices Should Be Event Driven: Autonomy vs Authority](http://blog.christianposta.com/microservices/why-microservices-should-be-event-driven-autonomy-vs-authority/),” *blog.christianposta.com*, May 27, 2016.
+[^42]: Alex Feyerke: “[Say Hello to Offline First](https://web.archive.org/web/20210420014747/http://hood.ie/blog/say-hello-to-offline-first.html),” *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](http://drops.dagstuhl.de/opus/volltexte/2015/5238/),” at *29th European Conference on Object-Oriented Programming* (ECOOP), July 2015. [doi:10.4230/LIPIcs.ECOOP.2015.568](http://dx.doi.org/10.4230/LIPIcs.ECOOP.2015.568)
+[^44]: Mark Soper: “[Clearing Up React Data Management Confusion with Flux, Redux, and Relay](https://medium.com/@marksoper/clearing-up-react-data-management-confusion-with-flux-redux-and-relay-aad504e63cae),” *medium.com*, December 3, 2015.
+[^45]: Eno Thereska, Damian Guy, Michael Noll, and Neha Narkhede: “[Unifying Stream Processing and Interactive Queries in Apache Kafka](http://www.confluent.io/blog/unifying-stream-processing-and-interactive-queries-in-apache-kafka/),” *confluent.io*, October 26, 2016.
+[^46]: Frank McSherry: “[Dataflow as Database](https://github.com/frankmcsherry/blog/blob/master/posts/2016-07-17.md),” *github.com*, July 17, 2016.
+[^47]: Peter Alvaro: “[I See What You Mean](https://www.youtube.com/watch?v=R2Aa4PivG0g),” at *Strange Loop*, September 2015.
+[^48]: Nathan Marz: “[Trident: A High-Level Abstraction for Realtime Computation](https://blog.twitter.com/2012/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](http://www.slideshare.net/edibice/extremely-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](https://charity.wtf/2016/10/02/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](http://db.cs.berkeley.edu/cs286/papers/isolation-icde2000.pdf),” at *16th International Conference on Data Engineering* (ICDE), February 2000. [doi:10.1109/ICDE.2000.839387](http://dx.doi.org/10.1109/ICDE.2000.839387)
+[^52]: Sudhir Jorwekar, Alan Fekete, Krithi Ramamritham, and S. Sudarshan: “[Automating the Detection of Snapshot Isolation Anomalies](http://www.vldb.org/conf/2007/papers/industrial/p1263-jorwekar.pdf),” at *33rd International Conference on Very Large Data Bases* (VLDB), September 2007.
+[^53]: Kyle Kingsbury: [Jepsen blog post series](https://aphyr.com/tags/jepsen), *aphyr.com*, 2013–2016.
+[^54]: Michael Jouravlev: “[Redirect After Post](http://www.theserverside.com/news/1365146/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](https://groups.csail.mit.edu/ana/Publications/PubPDFs/End-to-End%20Arguments%20in%20System%20Design.pdf),” *ACM Transactions on Computer Systems*, volume 2, number 4, pages 277–288, November 1984. [doi:10.1145/357401.357402](http://dx.doi.org/10.1145/357401.357402)
+[^56]: Peter Bailis, Alan Fekete, Michael J. Franklin, et al.: “[Coordination-Avoiding Database Systems](http://arxiv.org/pdf/1402.2237.pdf),” *Proceedings of the VLDB Endowment*, volume 8, number 3, pages 185–196, November 2014.
+[^57]: Alex Yarmula: “[Strong Consistency in Manhattan](https://blog.twitter.com/2016/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](http://css.csail.mit.edu/6.824/2014/papers/bayou-conflicts.pdf),” at *15th ACM Symposium on Operating Systems Principles* (SOSP), pages 172–182, December 1995. [doi:10.1145/224056.224070](http://dx.doi.org/10.1145/224056.224070)
+[^59]: Jim Gray: “[The Transaction Concept: Virtues and Limitations](http://jimgray.azurewebsites.net/papers/thetransactionconcept.pdf),” at *7th International Conference on Very Large Data Bases* (VLDB), September 1981.
+[^60]: Hector Garcia-Molina and Kenneth Salem: “[Sagas](http://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf),” at *ACM International Conference on Management of Data* (SIGMOD), May 1987. [doi:10.1145/38713.38742](http://dx.doi.org/10.1145/38713.38742)
+[^61]: Pat Helland: “[Memories, Guesses, and Apologies](https://web.archive.org/web/20160304020907/http://blogs.msdn.com/b/pathelland/archive/2007/05/15/memories-guesses-and-apologies.aspx),” *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](https://users.ece.cmu.edu/~yoonguk/papers/kim-isca14.pdf),” at *41st Annual International Symposium on Computer Architecture* (ISCA), June 2014. [doi:10.1145/2678373.2665726](http://dx.doi.org/10.1145/2678373.2665726)
+[^63]: Mark Seaborn and Thomas Dullien: “[Exploiting the DRAM Rowhammer Bug to Gain Kernel Privileges](https://googleprojectzero.blogspot.co.uk/2015/03/exploiting-dram-rowhammer-bug-to-gain.html),” *googleprojectzero.blogspot.co.uk*, March 9, 2015.
+[^64]: Jim N. Gray and Catharine van Ingen: “[Empirical Measurements of Disk Failure Rates and Error Rates](https://www.microsoft.com/en-us/research/publication/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](http://bugs.mysql.com/bug.php?id=73170),” *bugs.mysql.com*, July 2014.
+[^66]: Gary Fredericks: “[Postgres Serializability Bug](https://github.com/gfredericks/pg-serializability-bug),” *github.com*, September 2015.
+[^67]: Xiao Chen: “[HDFS DataNode Scanners and Disk Checker Explained](http://blog.cloudera.com/blog/2016/12/hdfs-datanode-scanners-and-disk-checker-explained/),” *blog.cloudera.com*, December 20, 2016.
+[^68]: Jay Kreps: “[Getting Real About Distributed System Reliability](http://blog.empathybox.com/post/19574936361/getting-real-about-distributed-system-reliability),” *blog.empathybox.com*, March 19, 2012.
+[^69]: Martin Fowler: “[The LMAX Architecture](http://martinfowler.com/articles/lmax.html),” *martinfowler.com*, July 12, 2011.
+[^70]: Sam Stokes: “[Move Fast with Confidence](http://blog.samstokes.co.uk/blog/2016/07/11/move-fast-with-confidence/),” *blog.samstokes.co.uk*, July 11, 2016.
+[^71]: “[Hyperledger Sawtooth documentation](https://web.archive.org/web/20220120211548/https://sawtooth.hyperledger.org/docs/core/releases/latest/introduction.html),” Intel Corporation, *sawtooth.hyperledger.org*, 2017.
+[^72]: Richard Gendal Brown: “[Introducing R3 Corda™: A Distributed Ledger Designed for Financial Services](https://gendal.me/2016/04/05/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](https://www.bigchaindb.com/whitepaper/bigchaindb-whitepaper.pdf),” *bigchaindb.com*, June 8, 2016.
+[^74]: Ralph C. Merkle: “[A Digital Signature Based on a Conventional Encryption Function](https://people.eecs.berkeley.edu/~raluca/cs261-f15/readings/merkle.pdf),” at *CRYPTO '87*, August 1987. [doi:10.1007/3-540-48184-2_32](http://dx.doi.org/10.1007/3-540-48184-2_32)
+[^75]: Ben Laurie: “[Certificate Transparency](http://queue.acm.org/detail.cfm?id=2668154),” *ACM Queue*, volume 12, number 8, pages 10-19, August 2014. [doi:10.1145/2668152.2668154](http://dx.doi.org/10.1145/2668152.2668154)
+[^76]: Mark D. Ryan: “[Enhanced Certificate Transparency and End-to-End Encrypted Mail](https://www.ndss-symposium.org/wp-content/uploads/2017/09/12_2_1.pdf),” at *Network and Distributed System Security Symposium* (NDSS), February 2014. [doi:10.14722/ndss.2014.23379](http://dx.doi.org/10.14722/ndss.2014.23379)
\ No newline at end of file
diff --git a/content/tw/ch14.md b/content/tw/ch14.md
new file mode 100644
index 0000000..dad5570
--- /dev/null
+++ b/content/tw/ch14.md
@@ -0,0 +1,260 @@
+---
+title: "14. 將事情做正確"
+weight: 314
+breadcrumbs: false
+---
+
+
+
+
+
+> *將世界的美好、醜陋與殘酷一起餵給 AI,卻期待它只反映美好的一面,這是一種幻想。*
+>
+> Vinay Uday Prabhu 與 Abeba Birhane,《Large Datasets: A Pyrrhic Win for Computer Vision?》(2020)
+
+在本書最後一章,讓我們退一步看問題。整本書裡,我們考察了各種資料系統架構,評估了它們的利弊,也探討了如何構建可靠、可伸縮、可維護的應用。然而,我們一直略去了討論中一個重要而基礎的部分,現在該補上了。
+
+每個系統都是為了某種目的而建;我們做的每個動作,都有預期後果,也有非預期後果。目的可能只是賺錢,但對世界產生的影響可能遠遠超出這個初始目的。構建這些系統的工程師,有責任認真思考這些後果,並且有意識地決定我們希望生活在怎樣的世界中。
+
+我們常把資料當成抽象事物來談論,但請記住,許多資料集都是關於人的:他們的行為、興趣、身份。我們必須以人性與尊重來對待這樣的資料。使用者也是人,而人的尊嚴至高無上 [^1]。
+
+軟體開發越來越涉及重要的倫理抉擇。確實有一些指南幫助軟體工程師應對這些問題,比如 ACM《倫理與職業行為準則》 [^2],但在實踐中,它們很少被討論、應用與執行。因此,工程師和產品經理有時會對隱私以及產品可能帶來的負面後果抱持一種輕率態度 [^3], [^4]。
+
+技術本身並無善惡,關鍵在於它如何被使用,以及它如何影響人。這一點對搜尋引擎這樣的軟體系統成立,對槍支這樣的武器同樣成立。軟體工程師若只專注技術本身而忽視其後果,是不夠的:倫理責任同樣由我們承擔。倫理推理很難,但它又重要到不能迴避。
+
+不過,什麼算“好”或“壞”並沒有清晰定義,而計算領域的大多數人甚至不討論這個問題 [^5]。與計算領域中的很多概念不同,倫理的核心概念並沒有嚴格且確定的單一含義,它們需要解釋,而解釋可能具有主觀性 [^6]。倫理並不是走一遍檢查清單、確認你“合規”就完事;它是一種參與式、迭代式的反思過程,要與相關人群對話,並對結果負責 [^7]。
+
+## 預測分析 {#id369}
+
+例如,預測分析是人們對大資料和 AI 感到興奮的重要原因之一。用資料分析來預測天氣或疾病傳播是一回事 [^8];預測一個罪犯是否可能再犯、一個貸款申請者是否可能違約,或一個保險客戶是否可能提出高額理賠,又是另一回事 [^9]。後者會直接影響個人的生活。
+
+支付網路當然想防止欺詐交易,銀行想避免壞賬,航空公司想避免劫機,公司想避免僱到低效或不可信的人。從它們的角度看,錯過一筆業務機會的成本較低,而壞賬或問題員工的成本更高,因此機構傾向於謹慎行事完全可以理解。拿不準時,說“不”更穩妥。
+
+然而,隨著演算法決策越來越普遍,一個被某個演算法標記為“高風險”的人(不管標記準確與否),可能會不斷遭遇這種“不”。如果一個人系統性地被排除在工作、航空出行、保險保障、房屋租賃、金融服務以及社會其他關鍵領域之外,這對個體自由構成的約束之大,以至於有人稱之為“演算法監獄” [^10]。在尊重人權的國家,刑事司法講究“未經證明有罪即推定無罪”;但自動化系統卻可能在沒有罪證、幾乎無申訴機會的情況下,系統性且任意地把一個人排除在社會參與之外。
+
+### 偏見與歧視 {#id370}
+
+演算法作出的決策並不必然比人更好,也不必然更差。每個人都可能有偏見,即使他們主動嘗試糾偏也是如此;歧視性做法也可能被文化性地制度化。人們期待基於資料、而非基於人的主觀直覺評估來作決定,可能更公平,也能讓傳統系統中常被忽視的人獲得更好機會 [^11]。
+
+當我們開發預測分析和 AI 系統時,我們並不只是把人的決策“自動化”——即用軟體寫明何時說“是”或“否”的規則;我們甚至把規則本身也交給資料去推斷。然而,這些系統學到的模式往往是不透明的:即使資料中存在某種相關性,我們也未必知道為什麼。如果演算法輸入中存在系統性偏差,系統很可能會在輸出中學習並放大這種偏差 [^12]。
+
+在許多國家,反歧視法禁止依據族裔、年齡、性別、性取向、殘障或信仰等受保護特徵而區別對待他人。一個人的其他資料特徵也許可以分析,但如果這些特徵與受保護特徵相關怎麼辦?例如,在按種族隔離的社群裡,一個人的郵編,甚至其 IP 地址,都可能是種族的強預測因子。這樣一看,認為演算法能把帶偏見的資料作為輸入,卻產出公平中立的結果,幾乎是荒謬的 [^13], [^14]。然而,資料驅動決策的支持者常隱含這種信念,這種態度甚至被諷刺為“機器學習就像給偏見洗錢” [^15]。
+
+預測分析系統只是在外推過去;如果過去有歧視,它們就會把歧視編碼並放大 [^16]。如果我們希望未來比過去更好,就需要道德想象力,而這隻能由人提供 [^17]。資料和模型應當是我們的工具,而不是我們的主宰。
+
+### 責任與問責 {#id371}
+
+自動化決策把責任與問責問題擺到了臺前 [^17]。如果人犯了錯,可以追責,受影響者也可以申訴。演算法同樣會出錯,但如果演算法出了問題,誰來負責 [^18]?自動駕駛汽車造成事故,誰應承擔責任?自動化信用評分演算法如果系統性歧視某一族裔或宗教的人,受害者是否有救濟途徑?如果你的機器學習系統的決策受到司法審查,你能向法官解釋演算法是如何作出該決策的嗎?人不應透過“怪演算法”來逃避自己的責任。
+
+信用評級機構是一個較早的先例:透過收集資料來對人作決策。糟糕的信用評分會讓生活變難,但至少信用分通常基於與借貸歷史直接相關的事實記錄,記錄中的錯誤也可以更正(儘管機構往往不會讓這件事變得容易)。相比之下,基於機器學習的評分演算法通常使用更廣泛的輸入且更不透明,使人更難理解某個具體決策是如何得出的,也更難判斷某人是否受到了不公平或歧視性對待 [^19]。
+
+信用分回答的是“你過去行為如何?”;而預測分析通常基於“誰和你相似,以及像你這樣的人過去行為如何?”。把某人和“相似人群”類比,本質上就是在給人貼群體標籤,比如按居住地(這往往是種族和社會經濟階層的近似代理)來推斷。那被分錯桶的人怎麼辦?此外,如果決策因錯誤資料而出錯,幾乎不可能得到救濟 [^17]。
+
+許多資料本質上是統計性的,這意味著即便總體機率分佈正確,具體個案也可能是錯的。比如,某國平均預期壽命是 80 歲,並不意味著你會在 80 歲生日那天去世。僅憑平均值和機率分佈,我們很難判斷某個具體個體會活到多少歲。同樣,預測系統的輸出是機率性的,在具體個案上完全可能出錯。
+
+盲目相信資料在決策中的至高地位,不僅是錯覺,更是危險。隨著資料驅動決策越來越普遍,我們必須找到辦法讓演算法可問責、可透明,避免強化既有偏見,並在它們不可避免地犯錯時加以糾正。
+
+我們還需要想辦法防止資料被用來傷害人,並實現其積極潛力。比如,分析可以揭示一個人財務和社會生活上的特徵。一方面,這種能力可以用於把援助精準地送到最需要的人手中。另一方面,它有時被掠奪性企業用來識別脆弱人群,並向其兜售高成本貸款、含金量極低的學歷專案等高風險產品 [^17], [^20]。
+
+### 反饋迴路 {#id372}
+
+即便在對人影響沒那麼立竿見影的預測應用中,比如推薦系統,我們也必須直面棘手問題。當服務越來越擅長預測使用者想看什麼內容時,它可能最終只向人們展示他們本就認同的觀點,形成迴音室,讓刻板印象、錯誤資訊和社會極化不斷滋生。我們已經看到社交媒體迴音室對選舉活動的影響。
+
+當預測分析影響人的生活時,自我強化的反饋迴路會帶來尤其惡性的後果。比如,設想僱主用信用分來評估候選人。你原本是一個工作能力不錯、信用也不錯的人,但因某個無法控制的不幸事件突然陷入財務困境。賬單逾期後,你的信用分下降,找到工作的可能性也隨之下降。失業把你推向貧困,反過來讓你的評分更差,進一步降低就業機會 [^17]。這就是一種下行螺旋:有毒假設披著數學嚴謹與資料客觀的偽裝。
+
+反饋迴路還有另一個例子:經濟學家發現,德國加油站引入演算法定價後,競爭反而減弱,消費者價格上升,因為演算法學會了“合謀” [^21]。
+
+我們並不總能預測這些反饋迴路何時出現。不過,很多後果可以透過思考“整個系統”來預見(不僅是計算機化部分,還包括與系統互動的人)——這種方法稱為 **系統思維** [^22]。我們可以嘗試理解資料分析系統對不同行為、結構與特徵的響應。系統是在強化和放大人與人之間既有差異(例如讓富者更富、窮者更窮),還是在努力對抗不公?而且,即便出發點再好,我們也必須警惕非預期後果。
+
+## 隱私與追蹤 {#id373}
+
+除了預測分析的問題——也就是用資料自動化地對人作決策——資料收集本身也有倫理問題。收集資料的組織,與資料被收集的人之間,到底是什麼關係?
+
+當系統只儲存使用者明確輸入的資料,因為使用者希望系統以某種方式儲存和處理它時,系統是在為使用者提供服務:使用者是客戶。但當用戶活動是在做其他事情時被“順帶”追蹤並記錄下來,這種關係就不那麼清晰了。服務不再只是執行使用者指令,而開始擁有自己的利益,而這種利益可能與使用者利益衝突。
+
+行為資料追蹤已成為許多線上服務面向使用者功能的重要組成部分:追蹤搜尋結果點選有助於改進搜尋排序;推薦“喜歡 X 的人也喜歡 Y”可幫助使用者發現有趣且有用的內容;A/B 測試與使用者流程分析可幫助改進使用者介面。這些功能都需要一定程度的使用者行為追蹤,使用者也能從中受益。
+
+然而,取決於公司的商業模式,追蹤往往不會止步於此。如果服務靠廣告資助,那麼廣告主才是真正客戶,使用者利益就會退居次位。追蹤資料會變得更細、分析會更深入、資料會被長期保留,以便為營銷目的構建每個人的精細畫像。
+
+這時,公司與被收集資料的使用者之間的關係,就開始顯著改變了。使用者得到“免費”服務,並被引導儘可能多地參與。對使用者的追蹤,主要服務的並不是這個個體,而是資助服務的廣告主需求。這樣的關係,用一個語義更陰暗的詞來描述更貼切:**監視**。
+
+### 監視 {#id374}
+
+做個思想實驗:把 *data* 一詞替換為 *surveillance*(監視),看看常見說法是否還那麼“好聽” [^23]。例如:“在我們這個監視驅動的組織中,我們收集即時監視流並存入監視倉庫。我們的監視科學家使用先進的分析與監視處理來產出新洞見。”
+
+這個思想實驗對本書來說少見地帶有一點論戰色彩,彷彿書名成了《設計監視密集型應用》(*Designing Surveillance-Intensive Applications*)。但為了強調這一點,我們需要更尖銳的詞。在我們試圖讓軟體“吞噬世界” [^24] 的過程中,我們構建了人類有史以來規模最大的群體監視基礎設施。我們正快速接近這樣一個世界:幾乎每個有人居住的空間都至少有一個聯網麥克風,存在於智慧手機、智慧電視、語音助手裝置、嬰兒監視器,甚至使用雲語音識別的兒童玩具中。許多這類裝置的安全記錄都非常糟糕 [^25]。
+
+與過去相比,新變化在於:數字化讓大規模收集人的資料變得很容易。對我們位置與行動軌跡、社交關係與通訊、購買與支付、健康資訊的監視,幾乎已不可避免。一個監視型組織最終掌握的個人資訊,甚至可能比當事人自己知道的還多——例如,在當事人意識到之前就識別出其疾病或經濟困境。
+
+即便是過去最極權、最壓迫的政權,也只能夢想把麥克風裝進每個房間,並迫使每個人隨身攜帶可追蹤其位置與行動的裝置。可是,由於數字技術帶來的好處太大,我們如今卻自願接受這個全面監視的世界。區別只在於:資料由企業收集以向我們提供服務,而不是由政府機構為控制目的而收集 [^26]。
+
+並非所有資料收集都一定構成監視,但把它放在“監視”的框架下審視,有助於我們理解自己與資料收集者的關係。為什麼我們似乎樂於接受企業監視?也許你覺得自己“沒什麼可隱瞞”——換句話說,你與既有權力結構完全一致,不是邊緣少數群體,也無需擔心被迫害 [^27]。但不是每個人都這麼幸運。又或者,你覺得目的似乎是善意的——不是公開的強制和馴化,而只是更好的推薦與更個性化的營銷。然而,結合上一節對預測分析的討論,這種區分就沒那麼清楚了。
+
+我們已經看到,汽車在未經駕駛員同意的情況下追蹤其駕駛行為,並影響保險費率 [^28];也看到了與佩戴健身追蹤裝置繫結的健康保險保障。當監視被用於決定對生活關鍵方面有重大影響的事項(如保險保障或就業)時,它看起來就不再“無害”。而且,資料分析還能揭示極具侵入性的內容:例如,智慧手錶或健身手環裡的運動感測器可以以相當高的準確率推斷你在輸入什麼(包括密碼) [^29]。感測器精度和分析演算法只會越來越強。
+
+### 同意與選擇自由 {#id375}
+
+我們或許會主張,使用者是自願選擇使用會追蹤其活動的服務,並且他們同意了服務條款和隱私政策,因此他們已同意資料收集。我們甚至可能聲稱,使用者正以其提供的資料換取有價值的服務,而追蹤是提供該服務所必需的。毫無疑問,社交網路、搜尋引擎和各種其他免費線上服務確實對使用者有價值——但這個論證有問題。
+
+首先,我們應當問:追蹤在哪種意義上是“必要的”?有些追蹤形式確實直接用於改進使用者功能:例如,追蹤搜尋結果點選率可提升搜尋排序與相關性;追蹤客戶常一起購買哪些商品,可幫助網店推薦關聯商品。然而,當追蹤使用者互動是為了內容推薦,或為了廣告構建使用者畫像時,這是否真正在使用者利益之中就不那麼清楚了——還是說,它“必要”僅僅因為廣告在為服務買單?
+
+其次,使用者對自己向我們的資料庫“喂入”了哪些資料、這些資料如何被保留與處理,幾乎沒有認知——而多數隱私政策更多是在遮蔽而非闡明。使用者若不瞭解其資料會發生什麼,就無法給出有意義的同意。並且,某個使用者的資料往往也會揭示並非該服務使用者、也未同意任何條款的其他人。我們在本書這部分討論過的那些派生資料集——其中可能把全體使用者資料與行為追蹤及外部資料來源結合——正是使用者不可能形成有意義理解的資料型別。
+
+此外,資料從使用者身上被抽取是單向過程,不是具有真實互惠的關係,也不是公平的價值交換。這裡沒有對話,沒有讓使用者協商“提供多少資料、換取什麼服務”的空間:服務與使用者之間的關係高度不對稱、單向度。規則由服務制定,而非使用者 [^30], [^31]。
+
+在歐盟,《通用資料保護條例》(GDPR)要求同意必須是 “freely given, specific, informed, and unambiguous”,並且使用者必須能夠 “refuse or withdraw consent without detriment”——否則不被視為 “freely given”。任何徵求同意的請求都必須以 “an intelligible and easily accessible form, using clear and plain language” 撰寫。此外,“silence, pre-ticked boxes or inactivity \[do not\] constitute consent” [^32]。除同意外,個人資料處理還可基於其他合法基礎,例如 *legitimate interest*,它允許某些資料用途,如防欺詐 [^33]。
+
+你可能會說,不同意被監視的使用者可以選擇不用這項服務。但這種選擇同樣不自由:如果某項服務流行到“被大多數人視為基本社會參與所必需” [^30],那就不能合理期待人們退出——使用它在事實上成了強制(*de facto* mandatory)。例如,在多數西方社群中,攜帶智慧手機、透過社交網路社交、使用 Google 獲取資訊,已經成為常態。尤其當服務具有網路效應時,選擇 *不* 使用它會付出社會成本。
+
+因為追蹤政策而拒絕使用某服務,說起來容易做起來難。這些平臺本來就是為吸引使用者而設計的。許多平臺使用遊戲機制和賭博常見策略來讓使用者反覆回來 [^34]。即便使用者能克服這一點,拒絕參與也往往只是少數特權人群的選項:他們有時間和知識去理解隱私政策,也有能力承擔潛在代價——比如錯過本可透過該服務獲得的社會參與或職業機會。對於處境更不利的人來說,並不存在真正意義上的選擇自由:監視變得無可逃避。
+
+### 隱私與資料使用 {#id457}
+
+有時有人聲稱“隱私已死”,理由是某些使用者願意在社交媒體上釋出各種生活內容,有些瑣碎,有些極度私密。但這個說法是錯誤的,它建立在對 *privacy* 一詞的誤解之上。
+
+擁有隱私並不意味著把一切都保密;它意味著擁有選擇自由:哪些內容向誰披露、哪些公開、哪些保密。隱私權是一種決策權:它讓每個人在每種情境中,決定自己在“保密”與“透明”光譜上的位置 [^30]。這是個體自由與自主性的重要組成部分。
+
+例如,一個患有罕見疾病的人,可能非常願意把其私密醫療資料提供給研究者,只要這有助於開發治療方法。但關鍵在於,這個人應當有權選擇誰可以訪問這些資料,以及出於什麼目的。如果其病情資訊可能損害其醫療保險、就業或其他重要權益,這個人很可能會更謹慎地共享資料。
+
+當資料透過監視基礎設施從人們身上被抽取時,被侵蝕的未必是隱私權本身,而可能是隱私權的轉移:轉移給資料收集者。獲取資料的公司本質上是在說“相信我們會正確使用你的資料”,這意味著決定“披露什麼、保密什麼”的權利,從個人轉移到了公司。
+
+這些公司反過來會把監視結果中的很大一部分保密,因為一旦公開,會讓人感到毛骨悚然,並傷害其商業模式(該模式依賴於“比其他公司更瞭解你”)。關於使用者的私密資訊通常只以間接方式被暴露,例如透過向特定人群(如患有某種疾病的人)定向投放廣告的工具表現出來。
+
+即便特定使用者無法從某條廣告所面向的人群桶中被個人重識別,他們仍失去了對某些私密資訊披露的主導權。決定“向誰披露什麼”不再基於使用者自己的偏好,而是公司在行使這種隱私權,目標是利潤最大化。
+
+許多公司追求的目標是“不被 *感知* 為令人不適”,迴避“資料收集到底有多侵入”這一問題,轉而專注於管理使用者感知。而且就連這種感知管理也常常做得不好:例如,某些內容也許在事實層面是正確的,但若會觸發痛苦記憶,使用者可能並不想被提醒 [^35]。面對任何資料,我們都應預期它可能出錯、不可取或在某些情況下不合適,並且需要構建機制來處理這些失效。至於什麼算“不可取”或“不合適”,當然屬於人的判斷;演算法除非被我們顯式程式設計去尊重人的需要,否則對這些概念是無感的。作為這些系統的工程師,我們必須保持謙遜,接受並預先規劃這些失效。
+
+線上服務裡的隱私設定,允許使用者控制其資料的哪些方面可被其他使用者看到,這是把部分控制權還給使用者的起點。然而,不管設定如何,服務本身仍可不受限制地訪問這些資料,並可在隱私政策允許範圍內任意使用。即使服務承諾不把資料出售給第三方,通常也會賦予自己在內部處理和分析資料的廣泛權利,而這種處理常常遠遠超出使用者可見範圍。
+
+這種把隱私權從個人大規模轉移到企業的現象,在歷史上前所未有 [^30]。監視並非從未存在,但過去它昂貴且依賴人工,不具備自動化與可伸縮性。信任關係也一直存在,比如病人與醫生、被告與律師之間——但這些關係中的資料使用長期受倫理、法律與監管約束。網際網路服務則讓“在缺乏有意義同意的情況下聚合海量敏感資訊,並在使用者不知情時以大規模方式使用”變得容易得多。
+
+### 資料作為資產與權力 {#id376}
+
+由於行為資料是使用者與服務互動的副產物,它有時被稱為 “data exhaust”(資料尾氣),暗示這些資料是無價值的廢料。照這個角度看,行為分析與預測分析像一種“回收”,從原本會被丟棄的資料中提煉價值。
+
+更準確的看法可能正相反:從經濟學角度看,如果定向廣告在為服務買單,那麼生成行為資料的使用者活動就可被視作一種勞動 [^36]。甚至可以更進一步主張:使用者互動的應用本身,只是引誘使用者不斷向監視基礎設施輸入更多個人資訊的手段 [^30]。線上服務中常見的人類創造力與社會關係,被資料抽取機器以冷酷方式利用。
+
+個人資料是有價值資產,這從資料經紀商行業的存在即可見一斑:這是一個在隱秘中運作、頗為灰暗的行業,購買、聚合、分析、推斷並轉售關於個人的侵入性資料,多數用於營銷 [^20]。初創公司的估值常以使用者數、以“眼球”為基礎——也就是以其監視能力為基礎。
+
+因為資料有價值,很多人都想要它。公司當然想要——這本就是它們收集資料的原因。政府也想拿到:透過秘密交易、脅迫、法律強制,或者直接竊取 [^37]。當公司破產時,其收集的個人資料會作為資產被出售。並且,資料很難徹底保護,洩露事件頻發得令人不安。
+
+這些觀察促使批評者說,資料不只是資產,還是“有毒資產”(*toxic asset*) [^37],或者至少是“危險材料”(*hazardous material*) [^38]。也許資料不是“新黃金”、不是“新石油”,而是“新鈾” [^39]。即使我們認為自己有能力防止資料濫用,每次收集資料時也必須權衡收益與其落入錯誤之手的風險:計算機系統可能被犯罪分子或敵對外國情報機構攻破,資料可能被內部人員洩露,公司可能落入與我們價值觀不一致的管理層手中,或國家可能被一個毫無顧忌、會強迫我們交出資料的政權接管。
+
+收集資料時,我們不僅要考慮今天的政治環境,還要考慮未來所有可能的政府。無法保證未來每一屆政府都會尊重人權與公民自由,因此,“安裝那些未來可能助長警察國家的技術,是糟糕的公民衛生習慣” [^40]。
+
+正如古老格言所說,“知識就是力量”。而且,“審視他人而避免自身被審視,是最重要的權力形式之一” [^41]。這正是極權政府追求監視的原因:它賦予其控制人口的力量。今天的科技公司雖未公開追求政治權力,但它們積累的資料與知識依然賦予其對我們生活的巨大影響力,其中很多是隱蔽的,處在公共監督之外 [^42]。
+
+### 回顧工業革命 {#id377}
+
+資料是資訊時代的決定性特徵。網際網路、資料儲存與處理、軟體驅動自動化,正在深刻影響全球經濟和人類社會。我們的日常生活與社會組織已被資訊科技改變,並且在未來幾十年很可能繼續發生劇烈變化,這很容易讓人聯想到工業革命 [^17], [^26]。
+
+工業革命建立在重大技術與農業進步之上,長期看帶來了持續經濟增長和生活水平顯著改善。但它也伴隨嚴重問題:空氣汙染(煙塵與化工過程)和水汙染(工業與生活廢棄物)都觸目驚心。工廠主生活奢華,城市工人卻常住在惡劣住房裡、長時間在嚴苛條件下勞動。童工普遍存在,包括礦井中危險且低薪的工作。
+
+社會花了很長時間才建立起各種防護措施:環境保護法規、工作場所安全規程、取締童工、食品衛生檢查。毫無疑問,當工廠不再被允許把廢棄物排進河裡、售賣汙染食品、剝削工人時,做生意的成本上升了。但整個社會從這些規制中獲益巨大,今天幾乎沒人願意回到那之前 [^17]。
+
+正如工業革命有其需要被管理的黑暗面一樣,我們向資訊時代的過渡也有重大問題,必須正視並解決 [^43], [^44]。資料的收集與使用就是其中之一。借用 Bruce Schneier 的話 [^26]:
+
+> 資料是資訊時代的汙染問題,而保護隱私是環境挑戰。幾乎所有計算機都會產生資訊。它會長期滯留、不斷髮酵。我們如何處理它——如何圍堵它、如何處置它——對資訊經濟的健康至關重要。正如今天我們回望工業時代的早期幾十年,會疑惑我們的祖先為何在建設工業世界的狂熱中忽視了汙染問題;我們的後代也將回望資訊時代的這些早期幾十年,並以我們如何應對資料收集與濫用的挑戰來評判我們。
+>
+> 我們應努力讓他們感到驕傲。
+
+### 立法與自律 {#sec_future_legislation}
+
+資料保護法也許能夠幫助維護個體權利。例如,歐盟 GDPR 規定,個人資料必須“為特定、明確且合法的目的而收集,不得以與這些目的不相容的方式進一步處理”;並且資料必須“就處理目的而言充分、相關且限於必要範圍” [^32]。
+
+然而,這一 **資料最小化** 原則與大資料哲學正面衝突。大資料強調最大化資料收集,把資料與其他資料集合並,持續實驗與探索,以產生新洞見。探索意味著為預見之外的目的使用資料,這與“特定且明確目的”正相反。儘管 GDPR 對線上廣告行業產生了一些影響 [^45],監管執行總體仍偏弱 [^46],也似乎沒有在更廣泛的科技行業內真正帶來文化與實踐層面的顯著轉變。
+
+那些收集大量個人資料的公司把監管視為負擔和創新阻礙。這種反對在某種程度上也有其合理性。比如共享醫療資料時,隱私風險確實明確存在,但也有潛在機會:如果資料分析能幫助我們實現更好的診斷或找到更好的治療方案,能減少多少死亡 [^47]?過度監管可能會阻礙這類突破。如何平衡機會與風險並不容易 [^41]。
+
+歸根結底,科技行業需要在個人資料問題上完成一次文化轉向。我們應停止把使用者當作可最佳化指標,記住他們是應被尊重、擁有尊嚴與主體性的人。我們應透過自律來約束資料收集與處理實踐,以建立並維繫依賴我們軟體的人們的信任 [^48]。並且,我們應主動教育終端使用者其資料如何被使用,而不是把他們矇在鼓裡。
+
+我們應允許每個個體保有其隱私——也就是對自身資料的控制——而不是透過監視把這種控制偷走。個體對自身資料的控制權,就像國家公園中的自然環境:如果我們不明確保護並照料它,它就會被破壞。這會成為“公地悲劇”,最終所有人都更糟。無處不在的監視並非命中註定——我們仍有機會阻止它。
+
+第一步是不要無限期保留資料,而應在不再需要時儘快清除,並在源頭最小化收集 [^48], [^49]。只要你的資料不存在,它就不會被洩露、被盜,或被政府強制交出。總的來說,這需要文化與態度的改變。作為技術從業者,如果我們不考慮自己工作的社會影響,那就是沒有盡到本職 [^50]。
+
+## 總結 {#id594}
+
+至此,本書接近尾聲。我們已經走過了很長一段路:
+
+- 在 [第 1 章](/tw/ch1#ch_tradeoffs) 中,我們對比了分析型系統與事務型系統,比較了雲與自託管,權衡了分散式與單節點系統,並討論了如何平衡業務需求與使用者需求。
+
+- 在 [第 2 章](/tw/ch2#ch_nonfunctional) 中,我們看到了如何定義非功能性需求,例如效能、可靠性、可伸縮性與可維護性。
+
+- 在 [第 3 章](/tw/ch3#ch_datamodels) 中,我們考察了從關係模型、文件模型到圖模型的一系列資料模型,也討論了事件溯源與 DataFrame。我們還看了多種查詢語言示例,包括 SQL、Cypher、SPARQL、Datalog 與 GraphQL。
+
+- 在 [第 4 章](/tw/ch4#ch_storage) 中,我們討論了面向 OLTP 的儲存引擎(LSM 樹與 B 樹)、面向分析的儲存(列式儲存),以及面向資訊檢索的索引(全文檢索與向量檢索)。
+
+- 在 [第 5 章](/tw/ch5#ch_encoding) 中,我們考察了將資料物件編碼為位元組的不同方式,以及如何在需求變化時支援演化。我們還比較了程序間資料流動的幾種方式:經由資料庫、服務呼叫、工作流引擎或事件驅動架構。
+
+- 在 [第 6 章](/tw/ch6#ch_replication) 中,我們研究了單領導者、多領導者與無主(無領導者)複製之間的權衡,也討論了寫後讀一致性等一致性模型,以及可讓客戶端離線工作的同步引擎。
+
+- 在 [第 7 章](/tw/ch7#ch_sharding) 中,我們深入討論了分片,包括再平衡策略、請求路由與次級索引。
+
+- 在 [第 8 章](/tw/ch8#ch_transactions) 中,我們覆蓋了事務:永續性、各種隔離級別(讀已提交、快照隔離、可序列化)的實現方式,以及如何在分散式事務中保證原子性。
+
+- 在 [第 9 章](/tw/ch9#ch_distributed) 中,我們梳理了分散式系統中的基礎問題(網路失效與延遲、時鐘誤差、程序暫停、崩潰),並看到這些問題如何讓“實現一個看似簡單的鎖”都變得困難。
+
+- 在 [第 10 章](/tw/ch10#ch_consistency) 中,我們深入分析了各種共識形式,以及它所支援的一致性模型(線性一致性)。
+
+- 在 [第 11 章](/tw/ch11#ch_batch) 中,我們深入批處理,從簡單的 Unix 工具鏈一直講到基於分散式檔案系統或物件儲存的大規模分散式批處理系統。
+
+- 在 [第 12 章](/tw/ch12#ch_stream) 中,我們把批處理推廣到流處理,討論了底層訊息代理、資料變更捕獲、容錯機制,以及流連線等處理模式。
+
+- 在 [第 13 章](/tw/ch13#ch_philosophy) 中,我們探討了流式系統的一種哲學,它使異構資料系統更易於整合、系統更易於演化、應用更易於擴充套件。
+
+最後,在本章中,我們後退一步,審視了構建資料密集型應用的一些倫理面向。我們看到,資料雖可為善,也可能造成嚴重傷害:作出深刻影響個人生活卻難以申訴的決策,導致歧視與剝削,使監視常態化,並暴露私密資訊。我們還面臨資料洩露風險,也可能發現某些出於善意的資料使用產生了非預期後果。
+
+隨著軟體與資料對世界產生如此巨大的影響,我們作為工程師必須記住:我們有責任朝著我們希望生活其中的世界努力——一個以人性與尊重對待人的世界。讓我們共同朝這個目標前進。
+
+### 參考文獻 {#references}
+
+[^1]: David Schmudde. [What If Data Is a Bad Idea?](https://schmud.de/posts/2024-08-18-data-is-a-bad-idea.html). *schmud.de*, August 2024. Archived at [perma.cc/ZXU5-XMCT](https://perma.cc/ZXU5-XMCT)
+[^2]: [ACM Code of Ethics and Professional Conduct](https://www.acm.org/code-of-ethics). Association for Computing Machinery, *acm.org*, 2018. Archived at [perma.cc/SEA8-CMB8](https://perma.cc/SEA8-CMB8)
+[^3]: Igor Perisic. [Making Hard Choices: The Quest for Ethics in Machine Learning](https://www.linkedin.com/blog/engineering/archive/making-hard-choices-the-quest-for-ethics-in-machine-learning). *linkedin.com*, November 2016. Archived at [perma.cc/DGF8-KNT7](https://perma.cc/DGF8-KNT7)
+[^4]: John Naughton. [Algorithm Writers Need a Code of Conduct](https://www.theguardian.com/commentisfree/2015/dec/06/algorithm-writers-should-have-code-of-conduct). *theguardian.com*, December 2015. Archived at [perma.cc/TBG2-3NG6](https://perma.cc/TBG2-3NG6)
+[^5]: Ben Green. ["Good" isn't good enough](https://www.benzevgreen.com/wp-content/uploads/2019/11/19-ai4sg.pdf). At *NeurIPS Joint Workshop on AI for Social Good*, December 2019. Archived at [perma.cc/H4LN-7VY3](https://perma.cc/H4LN-7VY3)
+[^6]: Deborah G. Johnson and Mario Verdicchio. [Ethical AI is Not about AI](https://cacm.acm.org/opinion/ethical-ai-is-not-about-ai/). *Communications of the ACM*, volume 66, issue 2, pages 32--34, January 2023. [doi:10.1145/3576932](https://doi.org/10.1145/3576932)
+[^7]: Marc Steen. [Ethics as a Participatory and Iterative Process](https://cacm.acm.org/opinion/ethics-as-a-participatory-and-iterative-process/). *Communications of the ACM*, volume 66, issue 5, pages 27--29, April 2023. [doi:10.1145/3550069](https://doi.org/10.1145/3550069)
+[^8]: Logan Kugler. [What Happens When Big Data Blunders?](https://cacm.acm.org/news/what-happens-when-big-data-blunders/) *Communications of the ACM*, volume 59, issue 6, pages 15--16, June 2016. [doi:10.1145/2911975](https://doi.org/10.1145/2911975)
+[^9]: Miri Zilka. [Algorithms and the criminal justice system: promises and challenges in deployment and research](https://www.cl.cam.ac.uk/research/security/seminars/archive/video/2023-03-07-t196231.html). At *University of Cambridge Security Seminar Series*, March 2023.
+[^10]: Bill Davidow. [Welcome to Algorithmic Prison](https://www.theatlantic.com/technology/archive/2014/02/welcome-to-algorithmic-prison/283985/). *theatlantic.com*, February 2014. Archived at [archive.org](https://web.archive.org/web/20171019201812/https://www.theatlantic.com/technology/archive/2014/02/welcome-to-algorithmic-prison/283985/)
+[^11]: Don Peck. [They're Watching You at Work](https://www.theatlantic.com/magazine/archive/2013/12/theyre-watching-you-at-work/354681/). *theatlantic.com*, December 2013. Archived at [perma.cc/YR9T-6M38](https://perma.cc/YR9T-6M38)
+[^12]: Leigh Alexander. [Is an Algorithm Any Less Racist Than a Human?](https://www.theguardian.com/technology/2016/aug/03/algorithm-racist-human-employers-work) *theguardian.com*, August 2016. Archived at [perma.cc/XP93-DSVX](https://perma.cc/XP93-DSVX)
+[^13]: Jesse Emspak. [How a Machine Learns Prejudice](https://www.scientificamerican.com/article/how-a-machine-learns-prejudice/). *scientificamerican.com*, December 2016. [perma.cc/R3L5-55E6](https://perma.cc/R3L5-55E6)
+[^14]: Rohit Chopra, Kristen Clarke, Charlotte A. Burrows, and Lina M. Khan. [Joint Statement on Enforcement Efforts Against Discrimination and Bias in Automated Systems](https://www.ftc.gov/system/files/ftc_gov/pdf/EEOC-CRT-FTC-CFPB-AI-Joint-Statement%28final%29.pdf). *ftc.gov*, April 2023. Archived at [perma.cc/YY4Y-RCCA](https://perma.cc/YY4Y-RCCA)
+[^15]: Maciej Cegłowski. [The Moral Economy of Tech](https://idlewords.com/talks/sase_panel.htm). *idlewords.com*, June 2016. Archived at [perma.cc/L8XV-BKTD](https://perma.cc/L8XV-BKTD)
+[^16]: Greg Nichols. [Artificial Intelligence in healthcare is racist](https://www.zdnet.com/article/artificial-intelligence-in-healthcare-is-racist/). *zdnet.com*, November 2020. Archived at [perma.cc/3MKW-YKRS](https://perma.cc/3MKW-YKRS)
+[^17]: Cathy O'Neil. *Weapons of Math Destruction: How Big Data Increases Inequality and Threatens Democracy*. Crown Publishing, 2016. ISBN: 978-0-553-41881-1
+[^18]: Julia Angwin. [Make Algorithms Accountable](https://www.nytimes.com/2016/08/01/opinion/make-algorithms-accountable.html). *nytimes.com*, August 2016. Archived at [archive.org](https://web.archive.org/web/20230819055242/https://www.nytimes.com/2016/08/01/opinion/make-algorithms-accountable.html)
+[^19]: Bryce Goodman and Seth Flaxman. [European Union Regulations on Algorithmic Decision-Making and a 'Right to Explanation'](https://arxiv.org/abs/1606.08813). At *ICML Workshop on Human Interpretability in Machine Learning*, June 2016. Archived at [arxiv.org/abs/1606.08813](https://arxiv.org/abs/1606.08813)
+[^20]: [A Review of the Data Broker Industry: Collection, Use, and Sale of Consumer Data for Marketing Purposes](https://www.commerce.senate.gov/services/files/0d2b3642-6221-4888-a631-08f2f255b577). Staff Report, *United States Senate Committee on Commerce, Science, and Transportation*, *commerce.senate.gov*, December 2013. Archived at [perma.cc/32NV-YWLQ](https://perma.cc/32NV-YWLQ)
+[^21]: Stephanie Assad, Robert Clark, Daniel Ershov, and Lei Xu. [Algorithmic Pricing and Competition: Empirical Evidence from the German Retail Gasoline Market](https://economics.yale.edu/sites/default/files/clark_acex_jan_2021.pdf). *Journal of Political Economy*, volume 132, issue 3, pages 723-771, March 2024. [doi:10.1086/726906](https://doi.org/10.1086/726906)
+[^22]: Donella H. Meadows and Diana Wright. *Thinking in Systems: A Primer*. Chelsea Green Publishing, 2008. ISBN: 978-1-603-58055-7
+[^23]: Daniel J. Bernstein. [Listening to a "big data"/"data science" talk. Mentally translating "data" to "surveillance": "\...everything starts with surveillance\..."](https://x.com/hashbreaker/status/598076230437568512) *x.com*, May 2015. Archived at [perma.cc/EY3D-WBBJ](https://perma.cc/EY3D-WBBJ)
+[^24]: Marc Andreessen. [Why Software Is Eating the World](https://a16z.com/why-software-is-eating-the-world/). *a16z.com*, August 2011. Archived at [perma.cc/3DCC-W3G6](https://perma.cc/3DCC-W3G6)
+[^25]: J. M. Porup. ['Internet of Things' Security Is Hilariously Broken and Getting Worse](https://arstechnica.com/information-technology/2016/01/how-to-search-the-internet-of-things-for-photos-of-sleeping-babies/). *arstechnica.com*, January 2016. Archived at [archive.org](https://web.archive.org/web/20250823001716/https://arstechnica.com/information-technology/2016/01/how-to-search-the-internet-of-things-for-photos-of-sleeping-babies/)
+[^26]: Bruce Schneier. [*Data and Goliath: The Hidden Battles to Collect Your Data and Control Your World*](https://www.schneier.com/books/data_and_goliath/). W. W. Norton, 2015. ISBN: 978-0-393-35217-7
+[^27]: The Grugq. [Nothing to Hide](https://grugq.tumblr.com/post/142799983558/nothing-to-hide). *grugq.tumblr.com*, April 2016. Archived at [perma.cc/BL95-8W5M](https://perma.cc/BL95-8W5M)
+[^28]: Federal Trade Commission. [FTC Takes Action Against General Motors for Sharing Drivers' Precise Location and Driving Behavior Data Without Consent](https://www.ftc.gov/news-events/news/press-releases/2025/01/ftc-takes-action-against-general-motors-sharing-drivers-precise-location-driving-behavior-data). *ftc.gov*, January 2025. Archived at [perma.cc/3XGV-3HRD](https://perma.cc/3XGV-3HRD)
+[^29]: Tony Beltramelli. [Deep-Spying: Spying Using Smartwatch and Deep Learning](https://arxiv.org/abs/1512.05616). Masters Thesis, IT University of Copenhagen, December 2015. Archived at *arxiv.org/abs/1512.05616*
+[^30]: Shoshana Zuboff. [Big Other: Surveillance Capitalism and the Prospects of an Information Civilization](https://papers.ssrn.com/sol3/papers.cfm?abstract_id=2594754). *Journal of Information Technology*, volume 30, issue 1, pages 75--89, April 2015. [doi:10.1057/jit.2015.5](https://doi.org/10.1057/jit.2015.5)
+[^31]: Michiel Rhoen. [Beyond Consent: Improving Data Protection Through Consumer Protection Law](https://policyreview.info/articles/analysis/beyond-consent-improving-data-protection-through-consumer-protection-law). *Internet Policy Review*, volume 5, issue 1, March 2016. [doi:10.14763/2016.1.404](https://doi.org/10.14763/2016.1.404)
+[^32]: [Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016](https://eur-lex.europa.eu/eli/reg/2016/679/oj/eng). *Official Journal of the European Union*, L 119/1, May 2016.
+[^33]: UK Information Commissioner's Office. [What is the 'legitimate interests' basis?](https://ico.org.uk/for-organisations/uk-gdpr-guidance-and-resources/lawful-basis/legitimate-interests/what-is-the-legitimate-interests-basis/) *ico.org.uk*. Archived at [perma.cc/W8XR-F7ML](https://perma.cc/W8XR-F7ML)
+[^34]: Tristan Harris. [How a handful of tech companies control billions of minds every day](https://www.ted.com/talks/tristan_harris_how_a_handful_of_tech_companies_control_billions_of_minds_every_day). At *TED2017*, April 2017.
+[^35]: Carina C. Zona. [Consequences of an Insightful Algorithm](https://www.youtube.com/watch?v=YRI40A4tyWU). At *GOTO Berlin*, November 2016.
+[^36]: Imanol Arrieta Ibarra, Leonard Goff, Diego Jiménez Hernández, Jaron Lanier, and E. Glen Weyl. [Should We Treat Data as Labor? Moving Beyond 'Free'](https://www.aeaweb.org/conference/2018/preliminary/paper/2Y7N88na). *American Economic Association Papers Proceedings*, volume 1, issue 1, December 2017.
+[^37]: Bruce Schneier. [Data Is a Toxic Asset, So Why Not Throw It Out?](https://www.schneier.com/essays/archives/2016/03/data_is_a_toxic_asse.html) *schneier.com*, March 2016. Archived at [perma.cc/4GZH-WR3D](https://perma.cc/4GZH-WR3D)
+[^38]: Cory Scott. [Data is not toxic - which implies no benefit - but rather hazardous material, where we must balance need vs. want](https://x.com/cory_scott/status/706586399483437056). *x.com*, March 2016. Archived at [perma.cc/CLV7-JF2E](https://perma.cc/CLV7-JF2E)
+[^39]: Mark Pesce. [Data is the new uranium -- incredibly powerful and amazingly dangerous](https://www.theregister.com/2024/11/20/data_is_the_new_uranium/). *theregister.com*, November 2024. Archived at [perma.cc/NV8B-GYGV](https://perma.cc/NV8B-GYGV)
+[^40]: Bruce Schneier. [Mission Creep: When Everything Is Terrorism](https://www.schneier.com/essays/archives/2013/07/mission_creep_when_e.html). *schneier.com*, July 2013. Archived at [perma.cc/QB2C-5RCE](https://perma.cc/QB2C-5RCE)
+[^41]: Lena Ulbricht and Maximilian von Grafenstein. [Big Data: Big Power Shifts?](https://policyreview.info/articles/analysis/big-data-big-power-shifts) *Internet Policy Review*, volume 5, issue 1, March 2016. [doi:10.14763/2016.1.406](https://doi.org/10.14763/2016.1.406)
+[^42]: Ellen P. Goodman and Julia Powles. [Facebook and Google: Most Powerful and Secretive Empires We've Ever Known](https://www.theguardian.com/technology/2016/sep/28/google-facebook-powerful-secretive-empire-transparency). *theguardian.com*, September 2016. Archived at [perma.cc/8UJA-43G6](https://perma.cc/8UJA-43G6)
+[^43]: Judy Estrin and Sam Gill. [The World Is Choking on Digital Pollution](https://washingtonmonthly.com/2019/01/13/the-world-is-choking-on-digital-pollution/). *washingtonmonthly.com*, January 2019. Archived at [perma.cc/3VHF-C6UC](https://perma.cc/3VHF-C6UC)
+[^44]: A. Michael Froomkin. [Regulating Mass Surveillance as Privacy Pollution: Learning from Environmental Impact Statements](https://repository.law.miami.edu/cgi/viewcontent.cgi?article=1062&context=fac_articles). *University of Illinois Law Review*, volume 2015, issue 5, August 2015. Archived at [perma.cc/24ZL-VK2T](https://perma.cc/24ZL-VK2T)
+[^45]: Pengyuan Wang, Li Jiang, and Jian Yang. [The Early Impact of GDPR Compliance on Display Advertising: The Case of an Ad Publisher](https://openreview.net/pdf?id=TUnLHNo19S). *Journal of Marketing Research*, volume 61, issue 1, April 2023. [doi:10.1177/00222437231171848](https://doi.org/10.1177/00222437231171848)
+[^46]: Johnny Ryan. [Don't be fooled by Meta's fine for data breaches](https://www.economist.com/by-invitation/2023/05/24/dont-be-fooled-by-metas-fine-for-data-breaches-says-johnny-ryan). *The Economist*, May 2023. Archived at [perma.cc/VCR6-55HR](https://perma.cc/VCR6-55HR)
+[^47]: Jessica Leber. [Your Data Footprint Is Affecting Your Life in Ways You Can't Even Imagine](https://www.fastcompany.com/3057514/your-data-footprint-is-affecting-your-life-in-ways-you-cant-even-imagine). *fastcompany.com*, March 2016. Archived at [archive.org](https://web.archive.org/web/20161128133016/https://www.fastcoexist.com/3057514/your-data-footprint-is-affecting-your-life-in-ways-you-cant-even-imagine)
+[^48]: Maciej Cegłowski. [Haunted by Data](https://idlewords.com/talks/haunted_by_data.htm). *idlewords.com*, October 2015. Archived at [archive.org](https://web.archive.org/web/20161130143932/https://idlewords.com/talks/haunted_by_data.htm)
+[^49]: Sam Thielman. [You Are Not What You Read: Librarians Purge User Data to Protect Privacy](https://www.theguardian.com/us-news/2016/jan/13/us-library-records-purged-data-privacy). *theguardian.com*, January 2016. Archived at [archive.org](https://web.archive.org/web/20250828224851/https://www.theguardian.com/us-news/2016/jan/13/us-library-records-purged-data-privacy)
+[^50]: Jez Humble. [It's a cliché that people get into tech to "change the world". So then, you have to actually consider what the impact of your work is on the world. The idea that you can or should exclude societal and political discussions in tech is idiotic. It means you're not doing your job](https://x.com/jezhumble/status/1386758340894597122). *x.com*, April 2021. Archived at [perma.cc/3NYS-MHLC](https://perma.cc/3NYS-MHLC)
\ No newline at end of file
diff --git a/content/tw/ch2.md b/content/tw/ch2.md
index 0528d71..7d9a57d 100644
--- a/content/tw/ch2.md
+++ b/content/tw/ch2.md
@@ -4,32 +4,34 @@ weight: 102
breadcrumbs: false
---
+
+

-> *網際網路做得如此之好,以至於大多數人都把它想象成像太平洋一樣的自然資源,而不是人造的東西。上一次出現這種規模且無差錯的技術是什麼時候?*
+> *網際網路做得太好了,以至於大多數人把它看成像太平洋那樣的自然資源,而不是人造產物。上一次出現這種規模且幾乎無差錯的技術是什麼時候?*
>
> [艾倫・凱](https://www.drdobbs.com/architecture-and-design/interview-with-alan-kay/240003442),
> 在接受 *Dr Dobb's Journal* 採訪時(2012 年)
-如果你正在構建一個應用程式,你將會被一系列需求所驅動。在你的需求列表中,最重要的可能是應用程式必須提供的功能:需要哪些介面和按鈕,以及每個操作應該做什麼,以實現軟體的目的。這些是你的 ***功能性需求***。
+構建一個應用時,你通常會從一張需求清單開始。清單最上面的,往往是應用必須提供的功能:需要哪些頁面和按鈕,每個操作應該完成什麼行為,才能實現軟體的目標。這些就是 ***功能性需求***。
-此外,你可能還有一些 ***非功能性需求***:例如,應用程式應該快速、可靠、安全、合規,並且易於維護。這些需求可能沒有明確寫下來,因為它們看起來有些顯而易見,但它們與應用程式的功能同樣重要:一個慢得讓人無法忍受或不可靠的應用程式還不如不存在。
+此外,你通常還會有一些 ***非功能性需求***:例如,應用應當足夠快、足夠可靠、足夠安全、符合法規,而且易於維護。這些需求可能並沒有明確寫下來,因為它們看起來像是“常識”,但它們與功能需求同樣重要。一個慢得無法忍受、或頻繁出錯的應用,幾乎等於不存在。
-許多非功能性需求,比如安全性,超出了本書的範圍。但我們將考慮一些非功能性需求,本章將幫助你為自己的系統闡明它們:
+許多非功能性需求(比如安全)超出了本書範圍。但本章會討論其中幾項核心要求,並幫助你用更清晰的方式描述自己的系統:
-* 如何定義和衡量系統的 **效能**(參見 ["描述效能"](/tw/ch2#sec_introduction_percentiles));
-* 服務 **可靠** 意味著什麼——即即使出現問題也能繼續正確工作(參見 ["可靠性與容錯"](/tw/ch2#sec_introduction_reliability));
-* 透過在系統負載增長時新增計算能力的有效方法,使系統具有 **可伸縮性**(參見 ["可伸縮性"](/tw/ch2#sec_introduction_scalability));以及
-* 使系統長期更 **易於維護**(參見 ["可維護性"](/tw/ch2#sec_introduction_maintainability))。
+* 如何定義並衡量系統的 **效能**(參見 ["描述效能"](/tw/ch2#sec_introduction_percentiles));
+* 服務 **可靠** 到底意味著什麼:也就是在出錯時仍能持續正確工作(參見 ["可靠性與容錯"](/tw/ch2#sec_introduction_reliability));
+* 如何透過高效增加計算資源,讓系統在負載增長時保持 **可伸縮性**(參見 ["可伸縮性"](/tw/ch2#sec_introduction_scalability));以及
+* 如何讓系統在長期演進中保持 **可維護性**(參見 ["可維護性"](/tw/ch2#sec_introduction_maintainability))。
-本章介紹的術語在後續章節中也很有用,當我們深入研究資料密集型系統的實現細節時。然而,抽象定義可能相當枯燥;為了使這些想法更具體,我們將從一個案例研究開始本章,研究社交網路服務可能如何工作,這將提供效能和可伸縮性的實際案例。
+本章引入的術語,在後續章節深入實現細節時也會反覆用到。不過純定義往往比較抽象。為了把概念落到實處,本章先從一個案例研究開始:看看社交網路服務可能如何實現,並藉此討論效能與可伸縮性問題。
## 案例研究:社交網路首頁時間線 {#sec_introduction_twitter}
-想象一下,你被賦予了實現一個類似 X(前身為 Twitter)風格的社交網路的任務,使用者可以釋出訊息並關注其他使用者。這將是對這種服務實際工作方式的巨大簡化 [^1] [^2] [^3],但它將有助於說明大規模系統中出現的一些問題。
+假設你要實現一個類似 X(原 Twitter)的社交網路:使用者可以發帖,並追隨其他使用者。這會極大簡化真實系統的實現方式 [^1] [^2] [^3],但足以說明大規模系統會遇到的一些關鍵問題。
-假設使用者每天釋出 5 億條帖子,或平均每秒 5,700 條帖子。偶爾,速率可能飆升至每秒 150,000 條帖子 [^4]。我們還假設平均每個使用者關注 200 人並有 200 個粉絲(儘管實際的範圍要大得多:大多數人只有少數粉絲,而少數名人如巴拉克・奧巴馬有超過 1 億粉絲)。
+我們假設:使用者每天發帖 5 億條,平均每秒約 5,700 條;在特殊事件期間,峰值可能衝到每秒 150,000 條 [^4]。再假設平均每位使用者追隨 200 人,並有 200 名追隨者(實際分佈非常不均勻:大多數人只有少量追隨者,少數名人如巴拉克・奧巴馬則有上億追隨者)。
### 表示使用者、帖子與關注關係 {#id20}
@@ -37,7 +39,7 @@ breadcrumbs: false
{{< figure src="/fig/ddia_0201.png" id="fig_twitter_relational" caption="圖 2-1. 社交網路的簡單關係模式,使用者可以相互關注。" class="w-full my-4" >}}
-假設我們的社交網路必須支援的主要讀取操作是 *首頁時間線*,它顯示你關注的人最近釋出的帖子(為簡單起見,我們將忽略廣告、來自你未關注的人的推薦帖子和其他擴充套件)。我們可以編寫以下 SQL 查詢來獲取特定使用者的首頁時間線:
+假設該社交網路最重要的讀操作是 *首頁時間線*:展示你所追隨的人最近釋出的帖子(為簡化起見,我們忽略廣告、未追隨使用者的推薦帖,以及其他擴充套件功能)。獲取某個使用者首頁時間線的 SQL 可能如下:
```sql
SELECT posts.*, users.* FROM posts
@@ -50,42 +52,42 @@ SELECT posts.*, users.* FROM posts
要執行此查詢,資料庫將使用 `follows` 表找到 `current_user` 關注的所有人,查詢這些使用者最近的帖子,並按時間戳排序以獲取被關注使用者的最新 1,000 條帖子。
-帖子應該是及時的,所以假設在某人釋出帖子後,我們希望他們的粉絲能夠在 5 秒內看到它。一種方法是讓使用者的客戶端每 5 秒重複上述查詢(這稱為 *輪詢*)。如果我們假設有 1000 萬用戶同時線上登入,這意味著每秒執行 200 萬次查詢。即使增加輪詢間隔,這也是很大的負載。
+帖子具有時效性。我們假設:某人發帖後,追隨者應在 5 秒內看到。一個做法是客戶端每 5 秒重複執行一次上述查詢(即 *輪詢*)。如果同時線上登入使用者有 1000 萬,就意味著每秒要執行 200 萬次查詢。即使把輪詢間隔調大,這個量也很可觀。
-此外,上述查詢相當昂貴:如果你關注 200 人,它需要獲取這 200 人中每個人的最近帖子列表,併合並這些列表。每秒 200 萬次時間線查詢意味著資料庫需要每秒查詢某個傳送者的最近帖子 4 億次——這是一個巨大的數字。這是平均情況。一些使用者關注數萬個賬戶;對他們來說,這個查詢執行起來非常昂貴,而且很難快速完成。
+此外,這個查詢本身也很昂貴。若你追隨 200 人,系統就要分別抓取這 200 人的近期帖子列表,再把它們歸併。每秒 200 萬次時間線查詢,等價於資料庫每秒要執行約 4 億次“按傳送者查最近帖子”。這還只是平均情況。少數使用者會追隨數萬賬戶,這個查詢對他們尤其昂貴,也更難做快。
### 時間線的物化與更新 {#sec_introduction_materializing}
-我們如何做得更好?首先,與其輪詢,不如伺服器主動向當前線上的任何粉絲推送新帖子。其次,我們應該預先計算上述查詢的結果,以便可以從快取中提供使用者的首頁時間線請求。
+要如何最佳化?第一,與其輪詢,不如由伺服器主動向線上追隨者推送新帖。第二,我們應該預先計算上述查詢結果,讓首頁時間線請求可以直接從快取返回。
-想象一下,我們為每個使用者儲存一個包含其首頁時間線的資料結構,即他們關注的人的最近帖子。每次使用者釋出帖子時,我們查詢他們的所有粉絲,並將該帖子插入到每個粉絲的首頁時間線中——就像向郵箱投遞訊息一樣。現在當用戶登入時,我們可以簡單地給他們這個預先計算的首頁時間線。此外,要接收時間線上任何新帖子的通知,使用者的客戶端只需訂閱新增到其首頁時間線的帖子流。
+設想我們為每個使用者維護一個數據結構,儲存其首頁時間線,也就是其所追隨者的近期帖子。每當使用者發帖,我們就找出其所有追隨者,把這條帖子插入每個追隨者的首頁時間線中,就像往郵箱裡投遞信件。這樣使用者登入時,可以直接讀取預先算好的時間線。若要接收新帖提醒,客戶端只需訂閱“寫入該時間線”的帖子流即可。
-這種方法的缺點是,現在每次使用者釋出帖子時我們需要做更多的工作,因為首頁時間線是需要更新的派生資料。該過程如 [圖 2-2](/tw/ch2#fig_twitter_timelines) 所示。當一個初始請求導致幾個下游請求被執行時,我們使用術語 *扇出* 來描述請求數量增加的因子。
+這種方法的缺點是:每次發帖時都要做更多工作,因為首頁時間線屬於需要持續更新的派生資料。這個過程見 [圖 2-2](/tw/ch2#fig_twitter_timelines)。當一個初始請求觸發多個下游請求時,我們用 *扇出* 描述請求數量被放大的倍數。
-{{< figure src="/fig/ddia_0202.png" id="fig_twitter_timelines" caption="圖 2-2. 扇出:將新帖子傳遞給釋出帖子的使用者的每個粉絲。" class="w-full my-4" >}}
+{{< figure src="/fig/ddia_0202.png" id="fig_twitter_timelines" caption="圖 2-2. 扇出:將新帖子傳遞給釋出帖子的使用者的每個追隨者。" class="w-full my-4" >}}
-以每秒 5,700 條帖子的速率,如果平均帖子到達 200 個粉絲(即扇出因子為 200),我們將需要每秒執行超過 100 萬次首頁時間線寫入。這很多,但與我們本來需要的每秒 4 億次每個傳送者的帖子查詢相比,這仍然是一個顯著的節省。
+按每秒 5,700 條帖子計算,若平均每條帖到達 200 名追隨者(扇出因子 200),則每秒需要略高於 100 萬次首頁時間線寫入。這已經很多,但相比原先每秒 4 億次“按傳送者查帖”,仍是顯著最佳化。
-如果由於某些特殊事件導致帖子速率激增,我們不必立即進行時間線交付——我們可以將它們排隊,並接受帖子在粉絲的時間線中顯示會暫時花費更長時間。即使在這種負載峰值期間,時間線仍然可以快速載入,因為我們只是從快取中提供它們。
+如果遇到特殊事件導致發帖速率激增,我們不必立刻完成時間線投遞。可以先入隊,接受“帖子出現在追隨者時間線中”會暫時變慢。即便在這種峰值期,時間線載入仍然很快,因為讀取仍來自快取。
-這種預先計算和更新查詢結果的過程稱為 *物化*,時間線快取是 *物化檢視* 的一個例子(我們將在 [待補充連結] 中進一步討論這個概念)。物化檢視加速了讀取,但作為交換,我們必須在寫入時做更多的工作。對於大多數使用者來說,寫入成本是適中的,但社交網路還必須考慮一些極端情況:
+這種預先計算並持續更新查詢結果的過程稱為 *物化*。時間線快取就是一種 *物化檢視*(這個概念見 [“維護物化檢視”](/tw/ch12#sec_stream_mat_view))。物化檢視能加速讀取,但代價是寫入側工作量增加。對大多數使用者而言,這個寫入成本仍可接受,但社交網路還要處理一些極端情況:
-* 如果使用者關注非常多的賬戶,並且這些賬戶釋出很多內容,該使用者的物化時間線將有很高的寫入率。然而,在這種情況下,使用者實際上不太可能閱讀其時間線中的所有帖子,因此可以簡單地丟棄其時間線的一些寫入,只向用戶顯示他們關注的賬戶的帖子樣本 [^5]。
-* 當擁有大量粉絲的名人賬戶釋出帖子時,我們必須做大量工作將該帖子插入到他們數百萬粉絲的每個首頁時間線中。在這種情況下,丟棄一些寫入是不可接受的。解決這個問題的一種方法是將名人帖子與其他人的帖子分開處理:我們可以透過將名人帖子單獨儲存並在讀取時與物化時間線合併,來節省將它們新增到數百萬時間線的工作。儘管有這些最佳化,處理社交網路上的名人仍然需要大量基礎設施 [^6]。
+* 如果某使用者追隨了大量賬戶,且這些賬戶發帖頻繁,那麼該使用者的物化時間線寫入率會很高。但在這種場景下,使用者通常也看不完全部帖子,因此可以丟棄部分時間線寫入,只展示其追隨賬戶帖子的一部分樣本 [^5]。
+* 如果一個擁有海量追隨者的名人賬號發帖,我們需要把這條帖子寫入其數百萬追隨者的首頁時間線,工作量極大。此時不能隨意丟寫。常見做法是把名人帖子與普通帖子分開處理:名人帖單獨儲存,讀取時間線時再與物化時間線合併,從而省去寫入數百萬條時間線的成本。即便如此,服務名人賬號仍需大量基礎設施 [^6]。
## 描述效能 {#sec_introduction_percentiles}
-大多數關於軟體效能的討論都考慮兩種主要的度量型別:
+軟體效能通常圍繞兩類指標展開:
響應時間
-: 從使用者發出請求到收到請求應答所經過的時間。測量單位是秒(或毫秒,或微秒)。
+: 從使用者發出請求到收到響應所經歷的時間。單位是秒(或毫秒、微秒)。
吞吐量
-: 系統正在處理的每秒請求數,或每秒資料量。對於給定的硬體資源分配,存在可以處理的 *最大吞吐量*。測量單位是"每秒某物"。
+: 系統每秒可處理的請求數或資料量。對於給定硬體資源,系統存在一個可處理的 *最大吞吐量*。單位是“每秒某種工作量”。
-在社交網路案例研究中,"每秒帖子數"和"每秒時間線寫入數"是吞吐量指標,而"載入首頁時間線所需的時間"或"帖子傳遞給粉絲的時間"是響應時間指標。
+在社交網路案例中,“每秒帖子數”和“每秒時間線寫入數”屬於吞吐量指標;“載入首頁時間線所需時間”或“帖子送達追隨者所需時間”屬於響應時間指標。
-吞吐量和響應時間之間通常存在聯絡;線上服務的這種關係示例如 [圖 2-3](/tw/ch2#fig_throughput) 所示。當請求吞吐量較低時,服務具有較低的響應時間,但隨著負載增加,響應時間也會增加。這是因為 *排隊*:當請求到達高負載系統時,CPU 很可能已經在處理先前的請求,因此傳入請求需要等待先前請求完成。隨著吞吐量接近硬體可以處理的最大值,排隊延遲急劇增加。
+吞吐量和響應時間之間通常相關。線上服務的典型關係如 [圖 2-3](/tw/ch2#fig_throughput):低吞吐量時響應時間較低,負載升高後響應時間上升。原因是 *排隊*。請求到達高負載系統時,CPU 往往已在處理前一個請求,新請求只能等待;當吞吐量逼近硬體上限,排隊延遲會急劇上升。
{{< figure src="/fig/ddia_0203.png" id="fig_throughput" caption="圖 2-3. 隨著服務的吞吐量接近其容量,由於排隊,響應時間急劇增加。" class="w-full my-4" >}}
@@ -93,126 +95,126 @@ SELECT posts.*, users.* FROM posts
> [!TIP] 當過載系統無法恢復時
-如果系統接近過載,吞吐量被推到極限附近,它有時會進入惡性迴圈,變得效率更低,從而更加過載。例如,如果有很長的請求佇列等待處理,響應時間可能會增加到客戶端超時並重新發送請求的程度。這導致請求率進一步增加,使問題變得更糟——*重試風暴*。即使負載再次降低,這樣的系統也可能保持過載狀態,直到重新啟動或以其他方式重置。這種現象稱為 *亞穩態故障(Metastable Failure)*,它可能導致生產系統的嚴重中斷 [^7] [^8]。
+如果系統已接近過載、吞吐量逼近極限,有時會進入惡性迴圈:效率下降,進而更加過載。例如,請求佇列很長時,響應時間可能高到讓客戶端超時並重發請求,導致請求速率進一步上升,問題持續惡化,形成 *重試風暴*。即使負載後來回落,系統也可能仍卡在過載狀態,直到重啟或重置。這種現象叫 *亞穩態故障*(Metastable Failure),可能引發嚴重生產故障 [^7] [^8]。
-為了避免重試使服務過載,你可以在客戶端增加並隨機化連續重試之間的時間(*指數退避* [^9] [^10]),並暫時停止向最近返回錯誤或超時的服務傳送請求(使用 *熔斷器* [^11] [^12] 或 *令牌桶* 演算法 [^13])。伺服器還可以檢測何時接近過載並開始主動拒絕請求(*負載卸除* [^14]),併發送響應要求客戶端減速(*背壓* [^1] [^15])。排隊和負載均衡演算法的選擇也可能產生影響 [^16]。
+為了避免重試把服務拖垮,可以在客戶端拉大並隨機化重試間隔(*指數退避* [^9] [^10]),並臨時停止向近期報錯或超時的服務發請求(例如 *熔斷器* [^11] [^12] 或 *令牌桶* [^13])。服務端也可在接近過載時主動拒絕請求(*負載卸除* [^14]),並透過響應要求客戶端降速(*背壓* [^1] [^15])。此外,排隊與負載均衡演算法的選擇也會影響結果 [^16]。
--------
-就效能指標而言,響應時間通常是使用者最關心的,而吞吐量決定了所需的計算資源(例如,你需要多少伺服器),因此決定了服務特定工作負載的成本。如果吞吐量可能會增長超出當前硬體可以處理的範圍,則需要擴充套件容量;如果系統的最大吞吐量可以透過新增計算資源顯著增加,則稱系統為 *可伸縮的*。
+從效能指標角度看,使用者通常最關心響應時間;而吞吐量決定了所需計算資源(例如伺服器數量),從而決定承載特定工作負載的成本。如果吞吐量增長可能超過當前硬體上限,就必須擴容;若系統可透過增加計算資源顯著提升最大吞吐量,就稱其 *可伸縮*。
-在本節中,我們將主要關注響應時間,我們將在 ["可伸縮性"](/tw/ch2#sec_introduction_scalability) 中回到吞吐量和可伸縮性。
+本節主要討論響應時間;吞吐量與可伸縮性會在 ["可伸縮性"](/tw/ch2#sec_introduction_scalability) 一節再展開。
### 延遲與響應時間 {#id23}
-"延遲"和"響應時間"有時可互換使用,但在本書中我們將以特定方式使用這些術語(如 [圖 2-4](/tw/ch2#fig_response_time) 所示):
+“延遲”和“響應時間”有時會混用,但本書對它們有明確區分(見 [圖 2-4](/tw/ch2#fig_response_time)):
-* *響應時間* 是客戶端看到的;它包括系統中任何地方產生的所有延遲。
-* *服務時間* 是服務主動處理使用者請求的持續時間。
-* *排隊延遲* 可能發生在流程中的幾個點:例如,在收到請求後,它可能需要等待直到 CPU 可用才能被處理;如果同一臺機器上的其他任務通過出站網路介面傳送大量資料,響應資料包可能需要在傳送之前進行緩衝。
-* *延遲* 是一個涵蓋請求未被主動處理時間的總稱,即在此期間它是 *潛在的*。特別是,*網路延遲* 或 *網路延遲* 指的是請求和響應在網路中傳輸所花費的時間。
+* *響應時間* 是客戶端看到的總時間,包含鏈路上各處產生的全部延遲。
+* *服務時間* 是服務主動處理該請求的時間。
+* *排隊延遲* 可發生在流程中的多個位置。例如請求到達後,可能要等 CPU 空出來才能處理;同機其他任務若佔滿出站網絡卡,響應包也可能先在緩衝區等待發送。
+* *延遲* 是對“請求未被主動處理這段時間”的統稱,也就是請求處於 *潛伏(latent)* 狀態的時間。尤其是 *網路延遲*(或網路時延)指請求與響應在網路中傳播所花的時間。
{{< figure src="/fig/ddia_0204.png" id="fig_response_time" caption="圖 2-4. 響應時間、服務時間、網路延遲和排隊延遲。" class="w-full my-4" >}}
-在 [圖 2-4](/tw/ch2#fig_response_time) 中,時間從左到右流動,每個通訊節點顯示為水平線,請求或響應訊息顯示為從一個節點到另一個節點的粗對角箭頭。你將在本書中經常遇到這種風格的圖表。
+在 [圖 2-4](/tw/ch2#fig_response_time) 中,時間從左向右流動。每個通訊節點畫成一條水平線,請求/響應訊息畫成節點間的粗斜箭頭。本書後文會頻繁使用這種圖示風格。
-響應時間可能會因請求而異,即使你一遍又一遍地發出相同的請求。許多因素可能會增加隨機延遲:例如,上下文切換到後臺程序、網路資料包丟失和 TCP 重傳、垃圾回收暫停、強制從磁碟讀取的缺頁錯誤、伺服器機架中的機械振動 [^17],或許多其他原因。我們將在 ["超時與無界延遲"](/tw/ch9#sec_distributed_queueing) 中更詳細地討論這個主題。
+即便反覆傳送同一個請求,響應時間也可能顯著波動。許多因素都會引入隨機延遲:例如切換到後臺程序、網路丟包與 TCP 重傳、垃圾回收暫停、缺頁導致的磁碟讀取、伺服器機架機械振動 [^17] 等。我們會在 ["超時與無界延遲"](/tw/ch9#sec_distributed_queueing) 進一步討論這個問題。
-排隊延遲通常佔響應時間變化的很大一部分。由於伺服器只能並行處理少量事務(例如,受其 CPU 核心數的限制),只需要少量慢請求就可以阻塞後續請求的處理——這種效應稱為 *隊頭阻塞*。即使那些後續請求的服務時間很快,由於等待先前請求完成的時間,客戶端仍會看到緩慢的整體響應時間。排隊延遲不是服務時間的一部分,因此在客戶端測量響應時間很重要。
+排隊延遲常常是響應時間波動的主要來源。伺服器並行處理能力有限(例如受 CPU 核數約束),少量慢請求就可能堵住後續請求,這就是 *頭部阻塞*。即便後續請求本身服務時間很短,客戶端仍會因為等待前序請求而看到較慢的總體響應。排隊延遲不屬於服務時間,因此必須在客戶端側測量響應時間。
-### 平均值、中位數與百分位數 {#id24}
+### 平均值、中位數與百分位點 {#id24}
-因為響應時間因請求而異,我們需要將其視為值的 *分佈*,而不是單個數字。在 [圖 2-5](/tw/ch2#fig_lognormal) 中,每個灰色條表示對服務的請求,其高度顯示該請求花費的時間。大多數請求相當快,但偶爾會有 *異常值* 需要更長時間。網路延遲的變化也稱為 *抖動*。
+由於響應時間會隨請求變化,我們應將其看作一個可測量的 *分佈*,而非單一數字。在 [圖 2-5](/tw/ch2#fig_lognormal) 中,每個灰色柱表示一次請求,柱高是該請求耗時。大多數請求較快,但會有少量更慢的 *異常值*。網路時延波動也常稱為 *抖動*。
-{{< figure src="/fig/ddia_0205.png" id="fig_lognormal" caption="圖 2-5. 說明平均值和百分位數:100 個服務請求的響應時間樣本。" class="w-full my-4" >}}
+{{< figure src="/fig/ddia_0205.png" id="fig_lognormal" caption="圖 2-5. 說明平均值和百分位點:100 個服務請求的響應時間樣本。" class="w-full my-4" >}}
-報告服務的 *平均* 響應時間是常見的(技術上是 *算術平均值*:即,將所有響應時間相加,然後除以請求數)。平均響應時間對於估計吞吐量限制很有用 [^18]。然而,如果你想知道你的"典型"響應時間,平均值不是一個很好的指標,因為它不能告訴你有多少使用者實際經歷了那種延遲。
+報告服務 *平均* 響應時間很常見(嚴格說是 *算術平均值*:總響應時間除以請求數)。平均值對估算吞吐量上限有幫助 [^18]。但若你想知道“典型”響應時間,平均值並不理想,因為它不能反映到底有多少使用者經歷了這種延遲。
-通常使用 *百分位數* 更好。如果你將響應時間列表從最快到最慢排序,那麼 *中位數* 就在中間:例如,如果你的中位響應時間是 200 毫秒,這意味著一半的請求在不到 200 毫秒內返回,一半的請求花費的時間更長。這使得中位數成為了解使用者通常需要等待多長時間的良好指標。中位數也稱為 *第 50 百分位*,有時縮寫為 *p50*。
+通常,*百分位點* 更有意義。把響應時間從快到慢排序,*中位數* 位於中間。例如中位響應時間為 200 毫秒,表示一半請求在 200 毫秒內返回,另一半更慢。因此中位數適合衡量使用者“通常要等多久”。中位數也稱 *第 50 百分位*,常記為 *p50*。
-為了弄清異常值有多糟糕,你可以檢視更高的百分位數:*第 95*、*99* 和 *99.9* 百分位數很常見(縮寫為 *p95*、*p99* 和 *p999*)。它們是 95%、99% 或 99.9% 的請求比該特定閾值快的響應時間閾值。例如,如果第 95 百分位響應時間是 1.5 秒,這意味著 100 個請求中的 95 個花費不到 1.5 秒,100 個請求中的 5 個花費 1.5 秒或更長時間。這在 [圖 2-5](/tw/ch2#fig_lognormal) 中有所說明。
+為了看清異常值有多糟,需要觀察更高百分位點:常見的是 *p95*、*p99*、*p999*。它們表示 95%、99%、99.9% 的請求都快於該閾值。例如 p95 為 1.5 秒,表示 100 個請求裡有 95 個小於 1.5 秒,另外 5 個不小於 1.5 秒。[圖 2-5](/tw/ch2#fig_lognormal) 展示了這一點。
-響應時間的高百分位數,也稱為 *尾部延遲*,很重要,因為它們直接影響使用者的服務體驗。例如,亞馬遜在描述內部服務的響應時間要求時使用第 99.9 百分位,即使它隻影響 1,000 個請求中的 1 個。這是因為請求最慢的客戶通常是那些賬戶上資料最多的客戶,因為他們進行了許多購買——也就是說,他們是最有價值的客戶 [^19]。確保網站對他們來說速度快對於保持這些客戶的滿意度很重要。
+響應時間的高百分位點(也叫 *尾部延遲*)非常重要,因為它直接影響使用者體驗。例如亞馬遜內部服務常以第 99.9 百分位設定響應要求,儘管它隻影響 1/1000 的請求。原因是最慢請求往往來自“賬戶資料最多”的客戶,他們通常也是最有價值客戶 [^19]。讓這批使用者也能獲得快速響應,對業務很關鍵。
-另一方面,最佳化第 99.99 百分位(10,000 個請求中最慢的 1 個)被認為太昂貴,對亞馬遜的目的沒有足夠的好處。在非常高的百分位數上減少響應時間很困難,因為它們很容易受到你無法控制的隨機事件的影響,而且收益遞減。
+另一方面,繼續最佳化到第 99.99 百分位(最慢的萬分之一請求)通常成本過高、收益有限。越到高百分位,越容易受不可控隨機因素影響,也更符合邊際收益遞減規律。
--------
> [!TIP] 響應時間對使用者的影響
-直覺上似乎很明顯,快速服務比慢速服務對使用者更好 [^20]。然而,要獲得可靠的資料來量化延遲對使用者行為的影響是令人驚訝地困難的。
+直覺上,快服務當然比慢服務更好 [^20]。但真正要拿到“延遲如何影響使用者行為”的可靠量化資料,其實非常困難。
-一些經常被引用的統計資料是不可靠的。2006 年,谷歌報告說,搜尋結果從 400 毫秒減慢到 900 毫秒與流量和收入下降 20% 相關 [^21]。然而,2009 年穀歌的另一項研究報告說,延遲增加 400 毫秒導致每天搜尋減少僅 0.6% [^22],同年必應發現載入時間增加兩秒將廣告收入減少 4.3% [^23]。這些公司的較新資料似乎沒有公開。
+一些被頻繁引用的統計並不可靠。2006 年,Google 曾報告:搜尋結果從 400 毫秒變慢到 900 毫秒,與流量和收入下降 20% 相關 [^21]。但 2009 年 Google 另一項研究又稱,延遲增加 400 毫秒僅導致日搜尋量下降 0.6% [^22];同年 Bing 發現,載入時間增加 2 秒會讓廣告收入下降 4.3% [^23]。這些公司的更新資料似乎並未公開。
-Akamai 最近的一項研究 [^24] 聲稱響應時間增加 100 毫秒將電子商務網站的轉化率降低多達 7%;然而,仔細檢查後,同一研究顯示,非常 *快* 的頁面載入時間也與較低的轉化率相關!這個看似矛盾的結果是因為載入最快的頁面通常是那些沒有有用內容的頁面(例如,404 錯誤頁面)。然而,由於該研究沒有努力將頁面內容的影響與載入時間的影響分開,其結果可能沒有意義。
+Akamai 的一項較新研究 [^24] 聲稱:響應時間增加 100 毫秒會讓電商網站轉化率最多下降 7%。但細看可知,同一研究也顯示“載入極快”的頁面同樣和較低轉化率相關。這個看似矛盾的結果,很可能是因為載入最快的頁面往往是“無有效內容”的頁面(如 404)。而該研究並未把“頁面內容影響”和“載入時間影響”區分開,因此結論可能並不可靠。
-雅虎的一項研究 [^25] 比較了快速載入與慢速載入搜尋結果的點選率,控制了搜尋結果的質量。它發現當快速和慢速響應之間的差異為 1.25 秒或更多時,快速搜尋的點選次數增加 20-30%。
+Yahoo 的一項研究 [^25] 在控制搜尋結果質量後,比對了快慢載入對點選率的影響。結果顯示:當快慢響應差異達到 1.25 秒或以上時,快速搜尋的點選量會高出 20%–30%。
--------
### 響應時間指標的應用 {#sec_introduction_slo_sla}
-高百分位數在被多次呼叫作為服務單個終端使用者請求的一部分的後端服務中尤其重要。即使你並行進行呼叫,終端使用者請求仍然需要等待最慢的並行呼叫完成。只需要一個慢呼叫就可以使整個終端使用者請求變慢,如 [圖 2-6](/tw/ch2#fig_tail_amplification) 所示。即使只有一小部分後端呼叫很慢,如果終端使用者請求需要多個後端呼叫,獲得慢呼叫的機會就會增加,因此更高比例的終端使用者請求最終會變慢(這種效應稱為 *尾部延遲放大* [^26])。
+對於“一個終端請求會觸發多次後端呼叫”的服務,高百分位點尤其關鍵。即使並行呼叫,終端請求仍要等待最慢的那個返回。正如 [圖 2-6](/tw/ch2#fig_tail_amplification) 所示,只要一個呼叫慢,就能拖慢整個終端請求。即便慢呼叫比例很小,只要後端呼叫次數變多,撞上慢呼叫的機率就會上升,於是更大比例的終端請求會變慢(稱為 *尾部延遲放大* [^26])。
{{< figure src="/fig/ddia_0206.png" id="fig_tail_amplification" caption="圖 2-6. 當需要幾個後端呼叫來服務請求時,只需要一個慢的後端請求就可以減慢整個終端使用者請求。" class="w-full my-4" >}}
-百分位數通常用於 *服務級別目標*(SLO)和 *服務級別協議*(SLA),作為定義服務預期效能和可用性的方式 [^27]。例如,SLO 可能設定服務的中位響應時間小於 200 毫秒且第 99 百分位低於 1 秒的目標,以及至少 99.9% 的有效請求產生非錯誤響應的目標。SLA 是一份合同,規定如果不滿足 SLO 會發生什麼(例如,客戶可能有權獲得退款)。這至少是基本想法;實際上,為 SLO 和 SLA 定義良好的可用性指標並不簡單 [^28] [^29]。
+百分位點也常用於定義 *服務級別目標*(SLO)和 *服務級別協議*(SLA)[^27]。例如,一個 SLO 可能要求:中位響應時間低於 200 毫秒、p99 低於 1 秒,並且至少 99.9% 的有效請求返回非錯誤響應。SLA 則是“未達成 SLO 時如何處理”的合同條款(例如客戶可獲賠償)。這是基本思路;但在實踐中,為 SLO/SLA 設計合理可用性指標並不容易 [^28] [^29]。
--------
-> [!TIP] 計算百分位數
+> [!TIP] 計算百分位點
-如果你想將響應時間百分位數新增到服務的監控儀表板中,你需要持續有效地計算它們。例如,你可能希望保留過去 10 分鐘內請求的響應時間的滾動視窗。每分鐘,你計算該視窗中值的中位數和各種百分位數,並在圖表上繪製這些指標。
+如果你想在監控面板中展示響應時間百分位點,就需要持續且高效地計算它們。例如,維護“最近 10 分鐘請求響應時間”的滾動視窗,每分鐘計算一次該視窗內的中位數與各百分位點,並繪圖展示。
-最簡單的實現是在時間視窗內保留所有請求的響應時間列表,並每分鐘對該列表進行排序。如果這對你來說效率太低,有一些演算法可以以最小的 CPU 和記憶體成本計算百分位數的良好近似值。開源百分位數估計庫包括 HdrHistogram、t-digest [^30] [^31]、OpenHistogram [^32] 和 DDSketch [^33]。
+最簡單的實現是儲存視窗內全部請求的響應時間,並每分鐘排序一次。若效率不夠,可以用一些低 CPU/記憶體開銷的演算法來近似計算百分位點。常見開源庫包括 HdrHistogram、t-digest [^30] [^31]、OpenHistogram [^32] 和 DDSketch [^33]。
-請注意,平均百分位數,例如,減少時間解析度或組合來自多臺機器的資料,在數學上是沒有意義的——聚合響應時間資料的正確方法是新增直方圖 [^34]。
+要注意,“對百分位點再取平均”(例如降低時間解析度,或合併多機器資料)在數學上沒有意義。聚合響應時間資料的正確方式是聚合直方圖 [^34]。
--------
## 可靠性與容錯 {#sec_introduction_reliability}
-每個人都對某物是否可靠或不可靠有直觀的想法。對於軟體,典型的期望包括:
+每個人對“可靠”與“不可靠”都有直覺。對軟體而言,典型期望包括:
-* 應用程式執行使用者期望的功能。
-* 它可以容忍使用者犯錯誤或以意想不到的方式使用軟體。
-* 在預期的負載和資料量下,其效能足以滿足所需的用例。
-* 系統防止任何未經授權的訪問和濫用。
+* 應用能完成使用者預期的功能。
+* 能容忍使用者犯錯,或以意料之外的方式使用軟體。
+* 在預期負載與資料規模下,效能足以支撐目標用例。
+* 能防止未授權訪問與濫用。
-如果所有這些加在一起意味著"正確工作",那麼我們可以將 *可靠性* 大致理解為"即使出現問題也能繼續正確工作"。為了更準確地說明出現問題,我們將區分 *故障* 和 *失效* [^35] [^36] [^37]:
+如果把這些合起來稱為“正確工作”,那麼 *可靠性* 可以粗略理解為:即使出現問題,系統仍能持續正確工作。為了更精確地描述“出問題”,我們區分 *故障* 與 *失效* [^35] [^36] [^37]:
故障
-: 故障是指系統的某個特定 *部分* 停止正確工作:例如,如果單個硬碟驅動器發生故障,或單臺機器崩潰,或外部服務(系統所依賴的)發生中斷。
+: 指系統某個 *區域性元件* 停止正常工作:例如單個硬碟損壞、單臺機器宕機,或系統依賴的外部服務中斷。
失效
-: 失效是指 *整個* 系統停止向用戶提供所需的服務;換句話說,當它不滿足服務級別目標(SLO)時。
+: 指 *整個系統* 無法繼續向用戶提供所需服務;換言之,系統未滿足服務級別目標(SLO)。
-故障和失效之間的區別可能會令人困惑,因為它們在不同層面上是同一件事。例如,如果硬碟驅動器停止工作,我們說硬碟驅動器已失效:如果系統僅由該一個硬碟驅動器組成,它已停止提供所需的服務。然而,如果你正在談論的系統包含許多硬碟驅動器,那麼從更大系統的角度來看,單個硬碟驅動器的失效只是一個故障,並且更大的系統可能能夠透過在另一個硬碟驅動器上擁有資料副本來容忍該故障。
+“故障”與“失效”的區別容易混淆,因為它們本質上是同一件事在不同層級上的表述。比如一個硬碟壞了,對“硬碟這個系統”來說是失效;但對“由許多硬碟組成的更大系統”來說,它只是一個故障。更大系統若在其他硬碟上有副本,就可能容忍該故障。
### 容錯 {#id27}
如果系統在發生某些故障時仍繼續向用戶提供所需的服務,我們稱系統為 *容錯的*。如果系統不能容忍某個部分變得有故障,我們稱該部分為 *單點故障*(SPOF),因為該部分的故障會升級導致整個系統的失效。
-例如,在社交網路案例研究中,可能發生的故障是在扇出過程中,參與更新物化時間線的機器崩潰或變得不可用。為了使這個過程容錯,我們需要確保另一臺機器可以接管這項任務,而不會錯過任何應該交付的帖子,也不會複製任何帖子。(這個想法被稱為 *精確一次語義*,我們將在 [待補充連結] 中詳細研究它。)
+例如在社交網路案例中,扇出流程裡可能有機器崩潰或不可用,導致物化時間線更新中斷。若要讓該流程具備容錯性,就必須保證有其他機器可接管任務,同時既不漏投帖子,也不重複投遞。(這個思想稱為 *恰好一次語義*,我們會在 [“資料庫的端到端論證”](/tw/ch13#sec_future_end_to_end) 中詳細討論。)
-容錯總是限於某些型別的某些數量的故障。例如,系統可能能夠容忍最多兩個硬碟驅動器同時故障,或最多三個節點中的一個崩潰。如果所有節點都崩潰,沒有什麼可以做的,這沒有意義容忍任何數量的故障。如果整個地球(及其上的所有伺服器)被黑洞吞噬,容忍該故障將需要在太空中進行網路託管——祝你獲得批准該預算專案的好運。
+容錯能力總是“有邊界”的:它只針對某些型別、某個數量以內的故障。例如系統可能最多容忍 2 塊硬碟同時故障,或 3 個節點裡壞 1 個。若全部節點都崩潰,就無計可施,因此“容忍任意數量故障”並無意義。要是地球和上面的伺服器都被黑洞吞噬,那就只能去太空託管了,預算審批祝你好運。
-反直覺地是,在這種容錯系統中,透過故意觸發故障來 *增加* 故障率是有意義的——例如,在沒有警告的情況下隨機殺死單個程序。這稱為 *故障注入*。許多關鍵錯誤實際上是由於錯誤處理不當造成的 [^38];透過故意引發故障,你確保容錯機制不斷得到鍛鍊和測試,這可以增加你對故障自然發生時將被正確處理的信心。*混沌工程* 是一門旨在透過故意注入故障等實驗來提高對容錯機制的信心的學科 [^39]。
+反直覺的是,在這類系統裡,故意 *提高* 故障發生率反而有意義,例如無預警隨機殺死某個程序。這叫 *故障注入*。許多關鍵故障本質上是錯誤處理做得不夠好 [^38]。透過主動注入故障,可以持續演練並驗證容錯機制,提升對“真實故障發生時系統仍能正確處理”的信心。*混沌工程* 就是圍繞這類實驗建立起來的方法論 [^39]。
-儘管我們通常更喜歡容忍故障而不是預防故障,但在預防比治療更好的情況下(例如,因為不存在治療方法)。安全問題就是這種情況:如果攻擊者已經破壞了系統並獲得了對敏感資料的訪問,該事件無法撤消。然而,本書主要涉及可以恢復的故障型別,如以下部分所述。
+儘管我們通常更傾向於“容忍故障”,而非“阻止故障”,但也有“預防優於補救”的場景(例如根本無法補救)。安全問題就是如此:若攻擊者已攻破系統並獲取敏感資料,事件本身無法撤銷。不過,本書主要討論的是可恢復的故障型別。
### 硬體與軟體故障 {#sec_introduction_hardware_faults}
當我們想到系統失效的原因時,硬體故障很快就會浮現在腦海中:
-* 大約 2-5% 的硬磁碟驅動器每年發生故障 [^40] [^41];在擁有 10,000 個磁碟的儲存叢集中,我們因此應該認為平均每天有一個磁碟故障。最近的資料表明磁碟變得更可靠,但故障率仍然很顯著 [^42]。
-* 大約 0.5-1% 的固態硬碟(SSD)每年發生故障 [^43]。少量位錯誤會自動糾正 [^44],但不可糾正的錯誤大約每年每個驅動器發生一次,即使在相當新的驅動器中(即,經歷很少磨損);這個錯誤率高於硬磁碟驅動器 [^45]、[^46]。
+* 機械硬碟每年故障率約為 2%–5% [^40] [^41];在 10,000 盤位的儲存叢集中,平均每天約有 1 塊盤故障。近期資料表明磁碟可靠性在提升,但故障率仍不可忽視 [^42]。
+* SSD 每年故障率約為 0.5%–1% [^43]。少量位元錯誤可自動糾正 [^44],但不可糾正錯誤大約每盤每年一次,即使是磨損較輕的新盤也會出現;該錯誤率高於機械硬碟 [^45]、[^46]。
* 其他硬體元件,如電源、RAID 控制器和記憶體模組也會發生故障,儘管頻率低於硬碟驅動器 [^47] [^48]。
-* 大約千分之一的機器有一個 CPU 核心偶爾計算錯誤的結果,可能是製造缺陷 [^49] [^50] [^51]造成的。在某些情況下,錯誤的計算會導致崩潰,但在其他情況下,它會導致程式簡單地返回錯誤的結果。
-* RAM 中的資料也可能被損壞,要麼是由於宇宙射線等隨機事件,要麼是由於永久性物理缺陷。即使使用糾錯碼(ECC)的記憶體,也有超過 1% 的機器在給定年限內遇到不可糾正的錯誤,這通常會導致機器崩潰和受影響的記憶體模組需要更換 [^52]。此外,某些病態的記憶體訪問模式可能會以很高的機率翻轉位元位 [^53]。
-* 整個資料中心可能變得不可用(例如,由於停電或網路配置錯誤)甚至被永久摧毀(例如,由於火災、洪水或地震 [^54])。太陽風暴,當太陽噴射大量帶電粒子時,會在長距離電線中感應出大電流,可能會損壞電網和海底網路電纜 [^55]。儘管這種大規模故障很少見,但如果服務不能容忍資料中心的丟失,它們的影響可能是災難性的 [^56]。
+* 大約每 1000 臺機器裡就有 1 臺存在“偶發算錯結果”的 CPU 核心,可能由製造缺陷導致 [^49] [^50] [^51]。有時錯誤計算會直接導致崩潰;有時則只是悄悄返回錯誤結果。
+* RAM 資料也可能損壞:要麼來自宇宙射線等隨機事件,要麼來自永久性物理缺陷。即便使用 ECC 記憶體,任意一年內仍有超過 1% 的機器會遇到不可糾正錯誤,通常表現為機器崩潰並需要更換受影響記憶體條 [^52]。此外,某些病態訪問模式還可能以較高機率觸發位元翻轉 [^53]。
+* 整個資料中心也可能不可用(如停電、網路配置錯誤),甚至被永久摧毀(如火災、洪水、地震 [^54])。太陽風暴會在長距離導線中感應大電流,可能損壞電網和海底通訊電纜 [^55]。這類大規模故障雖罕見,但若服務無法容忍資料中心丟失,後果將極其嚴重 [^56]。
-這些事件足夠罕見,你在處理小型系統時通常不需要擔心它們,只要你能夠輕鬆地更換出現故障的硬體。然而,在大規模系統中,硬體故障發生得足夠頻繁,以至於它們成為正常系統執行的一部分。
+這類事件在小系統裡足夠罕見,通常不必過度擔心,只要能方便地更換故障硬體即可。但在大規模系統裡,硬體故障足夠頻繁,已經是“正常執行”的一部分。
#### 透過冗餘容忍硬體故障 {#tolerating-hardware-faults-through-redundancy}
@@ -220,7 +222,7 @@ Akamai 最近的一項研究 [^24] 聲稱響應時間增加 100 毫秒將電子
當元件故障獨立時,冗餘最有效,即一個故障的發生不會改變另一個故障發生的可能性。然而,經驗表明,元件故障之間通常存在顯著的相關性 [^41] [^57] [^58];整個伺服器機架或整個資料中心的不可用仍然比我們預期的更頻繁地發生。
-硬體冗餘增加了單臺機器的正常執行時間;然而,如 ["分散式與單節點系統"](/tw/ch1#sec_introduction_distributed) 中所討論的,使用分散式系統有一些優勢,例如能夠容忍一個數據中心的完全中斷。出於這個原因,雲系統傾向於較少關注單個機器的可靠性,而是旨在透過在軟體級別容忍故障節點來使服務高度可用。雲提供商使用 *可用區* 來識別哪些資源在物理上位於同一位置;同一地方的資源比地理上分離的資源更可能同時發生故障。
+硬體冗餘確實能提升單機可用時間;但正如 ["分散式與單節點系統"](/tw/ch1#sec_introduction_distributed) 所述,分散式系統還具備額外優勢,例如可容忍整個資料中心中斷。因此雲系統通常不再過分追求“單機極致可靠”,而是透過軟體層容忍節點故障來實現高可用。雲廠商使用 *可用區* 標識資源是否物理共址;同一可用區內資源比跨地域資源更容易同時失效。
我們在本書中討論的容錯技術旨在容忍整個機器、機架或可用區的丟失。它們通常透過允許一個數據中心的機器在另一個數據中心的機器發生故障或變得不可達時接管來工作。我們將在 [第 6 章](/tw/ch6)、[第 10 章](/tw/ch10) 以及本書的其他各個地方討論這種容錯技術。
@@ -228,198 +230,174 @@ Akamai 最近的一項研究 [^24] 聲稱響應時間增加 100 毫秒將電子
#### 軟體故障 {#software-faults}
-儘管硬體故障可能是弱相關的,但它們大多仍然是獨立的:例如,如果一個磁碟發生故障,同一臺機器中的其他磁碟很可能在一段時間內還能正常工作。另一方面,軟體故障通常高度相關,因為許多節點執行相同的軟體並因此具有相同的錯誤是常見的 [^59] [^60]。這種故障比不相關的硬體故障更難預料,並且它們往往導致比硬體故障更多的系統失效 [^47]。例如:
+儘管硬體故障可能存在弱相關,但整體上仍相對獨立:例如一塊盤壞了,同機其他盤往往還能再正常工作一段時間。相比之下,軟體故障常常高度相關,因為許多節點運行同一套軟體,也就共享同一批 bug [^59] [^60]。這類故障更難預判,也往往比“相互獨立的硬體故障”造成更多系統失效 [^47]。例如:
* 在特定情況下導致每個節點同時失效的軟體錯誤。例如,2012 年 6 月 30 日,閏秒導致許多 Java 應用程式由於 Linux 核心中的錯誤而同時掛起 [^61]。由於韌體錯誤,某些型號的所有 SSD 在精確執行 32,768 小時(不到 4 年)後突然失效,使其上的資料無法恢復 [^62]。
* 使用某些共享、有限資源(如 CPU 時間、記憶體、磁碟空間、網路頻寬或執行緒)的失控程序 [^63]。例如,處理大請求時消耗過多記憶體的程序可能會被作業系統殺死。客戶端庫中的錯誤可能導致比預期更高的請求量 [^64]。
* 系統所依賴的服務變慢、無響應或開始返回損壞的響應。
-* 不同系統之間的互動導致在隔離測試每個系統時不會發生的緊急行為 [^65]。
+* 不同系統互動後出現“單系統隔離測試中看不到”的湧現行為 [^65]。
* 級聯故障,其中一個元件中的問題導致另一個元件過載和減速,這反過來又導致另一個元件崩潰 [^66] [^67]。
-導致這些型別軟體故障的錯誤通常會潛伏很長時間,直到它們被一組不尋常的環境觸發。在這些情況下,軟體對其環境做出了某種假設——雖然該假設通常是正確的,但它最終由於某種原因不再成立 [^68] [^69]。
+導致這類軟體故障的 bug 往往潛伏很久,直到一組不尋常條件把它觸發出來。這時才暴露出:軟體其實對執行環境做了某些假設,平時大多成立,但終有一天會因某種原因失效 [^68] [^69]。
-軟體中的系統故障沒有快速解決方案。許多小事情可以幫助:仔細考慮系統中的假設和互動;徹底測試;程序隔離;允許程序崩潰和重新啟動;避免反饋迴圈,如重試風暴(參見 ["當過載系統無法恢復時"](/tw/ch2#sidebar_metastable));測量、監控和分析生產中的系統行為。
+軟體系統性故障沒有“速效藥”。但許多小措施都有效:認真審視系統假設與互動、充分測試、程序隔離、允許程序崩潰並重啟、避免反饋環路(如重試風暴,參見 ["當過載系統無法恢復時"](/tw/ch2#sidebar_metastable)),以及在生產環境持續度量、監控和分析系統行為。
### 人類與可靠性 {#id31}
-人類設計和構建軟體系統,保持系統執行的操作員也是人類。與機器不同,人類不只是遵循規則;他們的優勢是創造性和適應性地完成工作。然而,這一特徵也導致了不可預測性,有時會導致失效的錯誤,即使本意是好的。例如,一項對大型網際網路服務的研究發現,操作員的配置更改是中斷的主要原因,而硬體故障(伺服器或網路)僅在 10-25% 的中斷中發揮作用 [^70]。
+軟體系統由人設計、構建和運維。與機器不同,人不會只按規則執行;人的優勢在於創造性和適應性。但這也帶來不可預測性,即使本意是好的,也會犯導致失效的錯誤。例如,一項針對大型網際網路服務的研究發現:運維配置變更是中斷首因,而硬體故障(伺服器或網路)僅佔 10%–25% [^70]。
-人們很自然地傾向於將這類問題歸咎於“人為錯誤”,並希望透過更嚴格的程式和規則遵守來更好地控制人為行為從而解決問題。然而,將錯誤歸咎於人是適得其反的。我們所說的“人為錯誤”並非事件的真實原因,而是人們盡力工作時,社會技術系統中存在問題的徵兆 [^71]。通常,複雜系統具有緊急行為,元件之間的意外互動也可能導致故障 [^72]。
+遇到這類問題,人們很容易歸咎於“人為錯誤”,並試圖透過更嚴格流程和更強規則約束來控制人。但“責怪個人”通常適得其反。所謂“人為錯誤”往往不是事故根因,而是社會技術系統本身存在問題的徵兆 [^71]。複雜系統裡,元件意外互動產生的湧現行為也常導致故障 [^72]。
-各種技術措施可以幫助最小化人為錯誤的影響,包括徹底測試(手寫測試和對大量隨機輸入的 *屬性測試*)[^38]、快速回滾配置更改的回滾機制、新程式碼的漸進部署、詳細和清晰的監控、用於診斷生產問題的可觀測性工具(參見 ["分散式系統的問題"](/tw/ch1#sec_introduction_dist_sys_problems)),以及鼓勵"正確的事情"並阻止"錯誤的事情"的精心設計的介面。
+有多種技術手段可降低人為失誤的影響:充分測試(含手寫測試與大量隨機輸入的 *屬性測試*)[^38]、可快速回滾配置變更的機制、新程式碼漸進發布、清晰細緻的監控、用於排查生產問題的可觀測性工具(參見 ["分散式系統的問題"](/tw/ch1#sec_introduction_dist_sys_problems)),以及鼓勵“正確操作”並抑制“錯誤操作”的良好介面設計。
-然而,這些事情需要時間和金錢的投資,在日常業務的務實現實中,組織通常優先考慮創收活動而不是增加其抵禦錯誤的韌性措施。如果要在更多功能和更多測試之間做出選擇,許多組織會很自然地選擇功能。鑑於這種選擇,當可預防的錯誤不可避免地發生時,責怪犯錯誤的人是沒有意義的——問題在於組織的事項優先順序。
+但這些措施都需要時間和預算。在日常業務壓力下,組織往往優先投入“直接創收”活動,而非提升抗錯韌性的建設。若在“更多功能”和“更多測試”之間二選一,很多組織會自然選擇前者。既然如此,當可預防錯誤最終發生時,責怪個人並無意義,問題本質在於組織的優先順序選擇。
-越來越多的組織正在採用 *無責備事後分析* 的文化:事件發生後,鼓勵相關人員充分分享發生的事情的細節,而不用擔心懲罰,因為這允許組織中的其他人學習如何在未來防止類似的問題 [^73]發生。這個過程可能會發現需要改變業務優先順序、需要投資於被忽視的領域、需要改變相關人員的激勵措施,或者需要引起管理層注意的其他一些系統性問題。
+越來越多組織在實踐 *無責備事後分析*:事故發生後,鼓勵參與者在不擔心懲罰的前提下完整覆盤細節,讓組織其他人也能學習如何避免類似問題 [^73]。這個過程常會揭示出:業務優先順序需要調整、某些長期被忽視的領域需要補投入、相關激勵機制需要改,或其他應由管理層關注的系統性問題。
-作為一般原則,在調查事件時,你應該對簡單化的答案持懷疑態度。"鮑勃在部署該更改時應該更加小心"是沒有意義的,"我們必須用 Haskell 重寫後端"也一樣。相反,管理層應該藉此機會從每天與之合作的人的角度瞭解社會技術系統如何工作的細節,並根據這些反饋採取措施改進它 [^71]。
+一般來說,調查事故時應警惕“過於簡單”的答案。“鮑勃部署時應更小心”沒有建設性,“我們必須用 Haskell 重寫後端”同樣不是。更可行的做法是:管理層藉機從一線人員視角理解社會技術系統的真實執行方式,並據此推動改進 [^71]。
--------
> [!TIP] 可靠性有多重要?
-可靠性不僅僅適用於核電站和空中交通管制——更普通的應用程式也應該可靠地工作。業務應用程式中的錯誤會導致生產力損失(如果資料被不正確地報告,還會有法律風險),而電子商務網站的中斷會因收入損失和聲譽受損而產生巨大的成本。
+可靠性不只適用於核電站或空管系統,普通應用同樣需要可靠。企業軟體中的 bug 會造成生產力損失(若報表錯誤還會帶來法律風險);電商網站故障則會帶來直接收入損失和品牌傷害。
-在許多應用程式中,幾分鐘甚至幾小時的臨時中斷是可以容忍的 [^74],但永久資料丟失或損壞將是災難性的。考慮一位家長在你的照片應用程式中儲存他們孩子的所有照片和影片 [^75]。如果該資料庫突然損壞,他們會有什麼感覺?他們會知道如何從備份中恢復嗎?
+在許多應用裡,幾分鐘乃至幾小時的短暫中斷尚可容忍 [^74];但永久性資料丟失或損壞往往是災難性的。想象一位家長把孩子的全部照片和影片都存在你的相簿應用裡 [^75]。若資料庫突然損壞,他們會怎樣?又是否知道如何從備份恢復?
-作為不可靠軟體如何傷害人們的另一個例子,可以參考郵局“Horizon”醜聞。在 1999 年至 2019 年期間,管理英國郵局分支機構的數百人因會計軟體顯示其賬戶財務漏洞而被判盜竊或欺詐罪。最終真相水落石出,許多這些財務漏洞是由於軟體中的錯誤,許多定罪已被推翻 [^76]。導致這一可能是英國曆史上最大的司法不公的是,英國法律假設計算機正確執行(因此,計算機產生的證據是可靠的),除非有相反的證據 [^77]。軟體工程師可能會嘲笑軟體可能無錯誤的想法,但這對那些因不可靠的計算機系統而被錯誤監禁、宣佈破產甚至自殺的人來說,這幾乎沒什麼安慰。
+另一個“軟體不可靠傷害現實人群”的例子,是英國郵局 Horizon 醜聞。1999 到 2019 年間,數百名郵局網點負責人因會計系統顯示“賬目短缺”被判盜竊或欺詐。後來事實證明,許多“短缺”來自軟體缺陷,且大量判決已被推翻 [^76]。造成這場可能是英國史上最大司法不公的一個關鍵前提,是英國法律預設計算機正常執行(因此其證據可靠),除非有相反證據 [^77]。軟體工程師或許會覺得“軟體無 bug”很荒謬,但這對那些因此被錯判入獄、破產乃至自殺的人來說毫無安慰。
-在某些情況下,我們可能選擇犧牲可靠性以降低開發成本(例如,在為未經證實的市場開發原型產品時)——但我們應該非常清楚何時走捷徑並牢記潛在的後果。
+在某些場景下,我們也許會有意犧牲部分可靠性來降低開發成本(例如做未驗證市場的原型產品)。但應明確知道自己在何處“走捷徑”,並充分評估其後果。
--------
## 可伸縮性 {#sec_introduction_scalability}
-即使系統今天可靠地工作,這並不意味著它將來必然會可靠地工作。降級的一個常見原因是負載增加:也許系統已經從 10,000 個併發使用者增長到 100,000 個併發使用者,或者從 100 萬增長到 1000 萬。也許它正在處理比以前大得多的資料量。
+即便系統今天執行可靠,也不代表將來一定如此。效能退化的常見原因之一是負載增長:比如併發使用者從 1 萬漲到 10 萬,或從 100 萬漲到 1000 萬;也可能是處理的資料規模遠大於從前。
-*可伸縮性* 是我們用來描述系統應對負載增加能力的術語。有時,在討論可伸縮性時,人們會發表評論,如"你不是谷歌或亞馬遜。停止擔心規模,只使用關係資料庫。"這個格言是否適用於你取決於你正在構建的應用程式型別。
+*可伸縮性* 用來描述系統應對負載增長的能力。討論這個話題時,常有人說:“你又不是 Google/Amazon,別擔心規模,直接上關係資料庫。”這句話是否成立,取決於你在做什麼型別的應用。
-如果你正在構建一個目前只有少數使用者的新產品,也許是在初創公司,首要的工程目標通常是保持系統儘可能簡單和靈活,以便你可以在瞭解更多關於客戶需求時輕鬆修改和調整產品的功能 [^78]。在這種環境中,擔心未來可能需要的假設規模是適得其反的:在最好的情況下,對可伸縮性的投資是浪費的努力和過早的最佳化;在最壞的情況下,它們會將你鎖定在不靈活的設計中,並使你的應用程式更難發展。
+如果你在做一個目前使用者很少的新產品(例如創業早期),首要工程目標通常是“儘可能簡單、儘可能靈活”,以便隨著對使用者需求理解加深而快速調整產品功能 [^78]。在這種環境下,過早擔心“未來也許會有”的規模往往適得其反:最好情況是白費功夫、過早最佳化;最壞情況是把自己鎖進僵化設計,反而阻礙演進。
-原因是可伸縮性不是一維標籤:說"X 是可伸縮的"或"Y 不伸縮"是沒有意義的。相反,討論可伸縮性意味著考慮諸如以下問題:
+原因在於,可伸縮性不是一維標籤;“X 可伸縮”或“Y 不可伸縮”這種說法本身意義不大。更有意義的問題是:
-* "如果系統以特定方式增長,我們有什麼選擇來應對增長?"
-* "我們如何增加計算資源來處理額外的負載?"
-* "基於當前的增長預測,我們何時會達到當前架構的極限?"
+* “如果系統按某種方式增長,我們有哪些應對選項?”
+* “我們如何增加計算資源來承載額外負載?”
+* “按當前增長趨勢,現有架構何時會觸頂?”
-如果你成功地使你的應用程式受歡迎,因此處理越來越多的負載,你將瞭解你的效能瓶頸在哪裡,因此你將知道需要沿著哪些維度進行伸縮。那時是開始擔心可伸縮性技術的時候。
+當你的產品真的做起來、負載持續上升時,你自然會看到瓶頸在哪裡,也就知道該沿哪些維度擴充套件。那時再系統性投入可伸縮性技術,通常更合適。
### 描述負載 {#id33}
-首先,我們需要簡潔地描述系統上的當前負載;只有這樣我們才能討論增長問題(如果我們的負載翻倍會發生什麼?)。通常這將是吞吐量的度量:例如,對服務的每秒請求數、每天到達多少千兆位元組的新資料,或每小時購物車結賬的數量。有時你關心某個變數數值的峰值,例如 ["案例研究:社交網路首頁時間線"](/tw/ch2#sec_introduction_twitter) 中同時線上使用者的數量。
+首先要簡明描述系統當前負載,之後才能討論“增長會怎樣”(例如負載翻倍會發生什麼)。最常見的是吞吐量指標:每秒請求數、每天新增資料量(GB)、每小時購物車結賬次數等。有時你關心的是峰值變數,比如 ["案例研究:社交網路首頁時間線"](/tw/ch2#sec_introduction_twitter) 裡的“同時線上使用者數”。
-通常還有其他負載統計特徵,會影響訪問模式,從而影響可擴充套件性要求。例如,你可能需要知道資料庫中的讀寫比率、快取的命中率或每個使用者的資料項數量(例如,社交網路案例研究中的粉絲數量)。也許平均情況對你很重要,或者也許你的瓶頸由少數極端情況主導。這一切都取決於你特定應用程式的細節。
+此外還可能有其他統計特徵會影響訪問模式,進而影響可伸縮性要求。例如資料庫讀寫比、快取命中率、每使用者資料項數量(如社交網路裡的追隨者數)。有時平均情況最關鍵,有時瓶頸由少數極端情況主導,具體取決於你的應用細節。
-一旦你描述了系統上的負載,你就可以調查當負載增加時會發生什麼。你可以從兩個方面來看待它:
+當負載被清楚描述後,就可以分析“負載增加時系統會怎樣”。可從兩個角度看:
-* 當你以某種方式增加負載並保持系統資源(CPU、記憶體、網路頻寬等)不變時,系統的效能如何受到影響?
-* 當你以某種方式增加負載時,如果你想保持效能不變,你需要增加多少資源?
+* 以某種方式增大負載、但保持資源(CPU、記憶體、網路頻寬等)不變時,效能如何變化?
+* 若負載按某種方式增長、但你希望效能不變,需要增加多少資源?
-通常我們的目標是在最小化執行系統成本的同時保持系統性能在 SLA 的要求範圍內(參見 ["響應時間指標的應用"](/tw/ch2#sec_introduction_slo_sla))。所需的計算資源越多,成本就越高。可能某些型別的硬體比其他型別更具成本效益,這些因素可能會隨著新型別硬體的出現而隨時間變化。
+通常目標是:在儘量降低執行成本的同時,讓效能維持在 SLA 要求內(參見 ["響應時間指標的應用"](/tw/ch2#sec_introduction_slo_sla))。所需計算資源越多,成本越高。不同硬體的價效比不同,而且會隨著新硬體出現而變化。
-如果你可以將資源翻倍以處理兩倍的負載,同時保持效能不變,我們說你有 *線性可伸縮性*,這被認為是好事。偶爾,由於規模經濟或峰值負載的更好分佈,可以用不到兩倍的資源處理兩倍的負載 [^79] [^80]。更可能的是,成本增長速度快於線性,並且效率低下可能有許多原因。例如,如果你有大量資料,那麼處理單個寫請求可能涉及比你有少量資料時更多的工作,即使請求的大小相同。
+如果資源翻倍後能承載兩倍負載且效能不變,這稱為 *線性可伸縮性*,通常是理想狀態。偶爾,藉助規模效應或峰值負載更均勻分佈,甚至可用不足兩倍資源處理兩倍負載 [^79] [^80]。但更常見的是成本增長快於線性,低效原因也很多。比如資料量增大後,即使請求大小相同,處理一次寫請求也可能比資料量小時更耗資源。
### 共享記憶體、共享磁碟與無共享架構 {#sec_introduction_shared_nothing}
-增加服務硬體資源的最簡單方法是將其移動到更強大的機器。單個 CPU 核心不再變得顯著更快,但你可以購買一臺機器(或租用雲實例)具有更多 CPU 核心、更多 RAM 和更多磁碟空間。這種方法稱為 *縱向伸縮* 或 *向上擴充套件*。
+增加服務硬體資源的最簡單方式,是遷移到更強的機器。雖然單核 CPU 不再明顯提速,但你仍可購買(或租用)擁有更多 CPU 核心、更多 RAM、更多磁碟的例項。這叫 *縱向伸縮*(scaling up)。
-你可以透過使用多個程序或執行緒在單臺機器上獲得並行性。屬於同一程序的所有執行緒都可以訪問相同的 RAM,因此這種方法也稱為 *共享記憶體架構*。共享記憶體方法的問題是成本增長速度快於線性:具有兩倍硬體資源的高階機器通常成本遠遠超過兩倍。由於瓶頸,兩倍大小的機器通常只能處理不到兩倍的負載。
+在單機上,你可以透過多程序/多執行緒獲得並行性。同一程序內執行緒共享同一塊 RAM,因此這也叫 *共享記憶體架構*。問題是它的成本常常“超線性增長”:硬體資源翻倍的高階機器,價格往往遠超兩倍;且受限於瓶頸,效能提升通常又達不到兩倍。
-另一種方法是 *共享磁碟架構*,它使用幾臺具有獨立 CPU 和 RAM 的機器,但將資料儲存在機器之間共享的磁碟陣列上,這些機器透過快速網路連線:*網路附加儲存*(NAS)或 *儲存區域網路*(SAN)。這種架構傳統上用於本地資料倉庫工作負載,但爭用和鎖定的開銷限制了共享磁碟方法的可伸縮性 [^81]。
+另一種方案是 *共享磁碟架構*:多臺機器各有獨立 CPU 和 RAM,但共享同一組磁碟陣列,透過高速網路連線(NAS 或 SAN)。該架構傳統上用於本地資料倉庫場景,但爭用與鎖開銷限制了其可伸縮性 [^81]。
-相比之下,*無共享架構* [^82](也稱為 *橫向伸縮* 或 *向外擴充套件*)已經獲得了很大的流行。在這種方法中,我們使用具有多個節點的分散式系統,每個節點都有自己的 CPU、RAM 和磁碟。節點之間的任何協調都在軟體級別透過傳統網路完成。
+相比之下,*無共享架構* [^82](即 *橫向伸縮*、scaling out)已廣泛流行。這種方案使用多節點分散式系統,每個節點擁有自己的 CPU、RAM 和磁碟;節點間協作透過常規網路在軟體層完成。
-無共享的優點是它有線性伸縮的潛力,它可以使用提供最佳價效比的任何硬體(特別是在雲中),它可以隨著負載的增加或減少更容易地調整其硬體資源,並且它可以透過在多個數據中心和地區分佈系統來實現更大的容錯。缺點是它需要顯式分片(參見 [第 7 章](/tw/ch7)),並且它會產生分散式系統的所有複雜性([第 9 章](/tw/ch9))。
+無共享的優勢在於:具備線性伸縮潛力、可靈活選用高性價比硬體(尤其在雲上)、更容易隨負載增減調整資源,並可透過跨多個數據中心/地域部署提升容錯。代價是:需要顯式分片(見 [第 7 章](/tw/ch7)),並承擔分散式系統的全部複雜性(見 [第 9 章](/tw/ch9))。
-一些雲原生資料庫系統為儲存和事務執行使用單獨的服務(參見 ["儲存與計算分離"](/tw/ch1#sec_introduction_storage_compute)),多個計算節點共享對同一儲存服務的訪問。這個模型與共享磁碟架構有一些相似之處,但它避免了舊系統的可伸縮性問題:它不是提供檔案系統(NAS)或塊裝置(SAN)抽象,而是儲存服務提供專門為資料庫特定需求設計的 API [^83]。
+一些雲原生資料庫把“儲存”和“事務執行”拆成獨立服務(參見 ["儲存與計算分離"](/tw/ch1#sec_introduction_storage_compute)),由多個計算節點共享同一儲存服務。這種模式與共享磁碟有相似性,但規避了老系統的可伸縮瓶頸:它不暴露 NAS/SAN 那種檔案系統或塊裝置抽象,而是提供面向資料庫場景定製的儲存 API [^83]。
### 可伸縮性原則 {#id35}
-在大規模執行的系統架構通常對應用程式高度特定——沒有通用的、一刀切的可伸縮架構(非正式地稱為 *萬金油*)。例如,設計用於處理每秒 100,000 個請求(每個 1 kB 大小)的系統與設計用於每分鐘 3 個請求(每個 2 GB 大小)的系統看起來非常不同——即使兩個系統具有相同的資料吞吐量(100 MB/秒)。
+能夠大規模執行的系統架構,通常高度依賴具體應用,不存在通用“一招鮮”的可伸縮架構(俗稱 *萬金油*)。例如:面向“每秒 10 萬次請求、每次 1 kB”的系統,與面向“每分鐘 3 次請求、每次 2 GB”的系統,形態會完全不同,儘管二者資料吞吐量都約為 100 MB/s。
-此外,適合一個負載級別的架構不太可能應對 10 倍的負載。如果你正在開發快速增長的服務,因此很可能你需要在每個數量級的負載增加時重新考慮你的架構。由於應用程式的需求可能會演變,通常不值得提前規劃超過一個數量級的未來伸縮需求。
+此外,適合某一級負載的架構,通常難以直接承受 10 倍負載。若你在做高速增長服務,幾乎每跨一個數量級都要重新審視架構。考慮到業務需求本身也會變化,提前規劃超過一個數量級的未來伸縮需求,往往不划算。
-可伸縮性的一個良好通用原則是將系統分解為可以在很大程度上相互獨立執行的較小元件。這是微服務背後的基本原則(參見 ["微服務與無伺服器"](/tw/ch1#sec_introduction_microservices))、分片([第 7 章](/tw/ch7))、流處理([待補充連結])和無共享架構。然而,挑戰在於知道在哪裡劃分應該在一起的事物和應該分開的事物之間的界限。微服務的設計指南可以在其他書籍中找到 [^84],我們在 [第 7 章](/tw/ch7) 中討論無共享系統的分片。
+可伸縮性的一個通用原則,是把系統拆分成儘量可獨立執行的小元件。這也是微服務(參見 ["微服務與無伺服器"](/tw/ch1#sec_introduction_microservices))、分片([第 7 章](/tw/ch7))、流處理([第 12 章](/tw/ch12#ch_stream))和無共享架構的共同基礎。難點在於:哪裡該拆,哪裡該合。微服務設計可參考其他書籍 [^84];無共享系統的分片問題我們會在 [第 7 章](/tw/ch7) 討論。
-另一個好原則是不要讓事情變得比必要的更複雜。如果單機資料庫可以完成工作,它可能比複雜的分散式設定更可取。自動伸縮系統(根據需求自動新增或刪除資源)很酷,但如果你的負載相當可預測,手動伸縮的系統可能會有更少的操作意外(參見 ["操作:自動或手動再平衡"](/tw/ch7#sec_sharding_operations))。具有五個服務的系統比具有五十個服務的系統更簡單。良好的架構通常涉及多種方案的務實混合。
+另一個好原則是:不要把系統做得比必要更複雜。若單機資料庫足夠,就往往優於複雜分散式方案。自動伸縮(按需求自動加減資源)很吸引人,但若負載相對可預測,手動伸縮可能帶來更少運維意外(參見 ["操作:自動或手動再平衡"](/tw/ch7#sec_sharding_operations))。5 個服務的系統通常比 50 個服務更簡單。好架構往往是多種方案的務實組合。
## 可維護性 {#sec_introduction_maintainability}
-軟體不會磨損或遭受材料老化,因此它不會像機械物體那樣損壞。但應用程式的需求經常變化,軟體執行在變化的環境中(例如其依賴項和底層平臺),並且它有需要修復的錯誤。
+軟體不會像機械裝置那樣磨損或材料疲勞,但應用需求會變化,軟體所處環境(依賴項、底層平臺)也會變化,程式碼中還會持續暴露需要修復的缺陷。
-人們普遍認為,軟體的大部分成本不在其初始開發中,而在其持續維護中——修復錯誤、保持其系統執行、調查故障、將其適應新平臺、為新用例修改它、償還技術債務和新增新功能 [^85] [^86]。
+業界普遍認同:軟體成本的大頭不在初始開發,而在後續維護,包括修 bug、保障系統穩定執行、排查故障、適配新平臺、支援新場景、償還技術債,以及持續交付新功能 [^85] [^86]。
-然而,維護也很困難。如果系統已成功執行很長時間,它可能使用如今很少有工程師理解的過時技術(如大型機和 COBOL 程式碼);隨著人員的離開,關於系統如何以及為何以某種特定方式設計的制度性知識可能已經丟失了;可能需要修復其他人的錯誤。此外,計算機系統通常與它支援的人類組織交織在一起,這意味著此類 *遺留* 系統的維護既是人的問題,也是技術問題 [^87]。
+然而維護並不容易。一個長期執行成功的系統,可能仍依賴今天少有人熟悉的舊技術(如大型機和 COBOL);隨著人員流動,系統為何如此設計的組織記憶也可能丟失;維護者往往還要修復前人留下的問題。更重要的是,計算機系統通常與其支撐的組織流程深度耦合,這使得 *遺留* 系統維護既是技術問題,也是人員與組織問題 [^87]。
-如果我們今天建立的每個系統都足夠有價值以長期生存,它有一天將成為遺留系統。為了最小化需要維護我們軟體的未來幾代人的痛苦,我們應該在設計時考慮維護問題。儘管我們不能總是預測哪些決定可能會在未來造成維護難題,但在本書中,我們將注意幾個廣泛適用的原則:
+如果今天構建的系統足夠有價值並長期存活,它終有一天會變成遺留系統。為減少後繼維護者的痛苦,我們應在設計階段就考慮維護性。雖然難以準確預判哪些決策會在未來埋雷,但本書會強調幾條廣泛適用的原則:
可運維性(Operability)
-: 使組織容易保持系統平穩執行。
+: 讓組織能夠更容易地保持系統平穩執行。
簡單性(Simplicity)
-: 透過使用易於理解、一致的模式和結構來實施它,並避免不必要的複雜性,使新工程師容易理解系統。
+: 採用易理解且一致的模式與結構,避免不必要複雜性,讓新工程師也能快速理解系統。
可演化性(Evolvability)
-: 使工程師將來容易對系統進行更改,隨著需求變化而適應和擴充套件它以用於未預料的使用場景。
+: 讓工程師在未來能更容易修改系統,使其隨著需求變化而持續適配並擴充套件到未預料場景。
### 可運維性:讓運維更輕鬆 {#id37}
-我們之前在 ["雲時代的運維"](/tw/ch1#sec_introduction_operations) 中討論了運維的角色,我們看到人類流程對於可靠運維至少與軟體工具一樣重要。
-事實上,有人提出 “良好的運維通常可以解決糟糕(或不完整)軟體的侷限性,但再好的軟體碰上糟糕的運維也難以可靠地執行” [^60]。
+我們在 ["雲時代的運維"](/tw/ch1#sec_introduction_operations) 已討論過運維角色:可靠執行不僅依賴工具,人類流程同樣關鍵。甚至有人指出:“好的運維常能繞過糟糕(或不完整)軟體的侷限;但再好的軟體,碰上糟糕運維也難以可靠執行” [^60]。
-在由數千臺機器組成的大規模系統中,手動維護將是不合理地昂貴的,自動化是必不可少的。然而,自動化可能是一把雙刃劍:
-總會有邊際場景(如罕見的故障場景)需要運維團隊的手動干預。由於無法自動處理的情況是最複雜的問題,更大的自動化需要一個 **更** 熟練的運維團隊來解決這些問題 [^88]。
+在由成千上萬臺機器組成的大規模系統中,純手工維護成本不可接受,自動化必不可少。但自動化也是雙刃劍:總會有邊緣場景(如罕見故障)需要運維團隊人工介入。並且“自動化處理不了”的往往恰恰最複雜,因此自動化越深,越需要 **更** 高水平的運維團隊來兜底 [^88]。
-此外,如果自動化系統出錯,通常比依賴操作員手動執行某些操作的系統更難排除故障。出於這個原因,更多的自動化並不總是對可操作性更好。
-然而,一定程度的自動化很重要,最佳點將取決於你特定應用程式和組織的細節。
+另外,一旦自動化系統本身出錯,往往比“部分依賴人工操作”的系統更難排查。因此自動化並非越多越好。合理自動化程度取決於你所在應用與組織的具體條件。
-良好的可操作性意味著使常規任務變得容易,使運維團隊能夠將精力集中在高價值活動上。
-資料系統可以做各種事情來使常規任務變得容易,包括 [^89]:
+良好的可運維性意味著把日常任務做簡單,讓運維團隊把精力投入到高價值工作。資料系統可以透過多種方式達成這一點 [^89]:
-* 允許監控工具檢查系統的關鍵指標,並支援可觀測性工具(參見 ["分散式系統的問題"](/tw/ch1#sec_introduction_dist_sys_problems))以深入瞭解系統的執行時行為。各種商業和開源工具可以在這方面提供幫助 [^90]。
-* 避免對單個機器的依賴(允許在系統整體持續不間斷執行的同時關閉機器進行維護)
-* 提供良好的文件和易於理解的操作模型("如果我做 X,Y 將會發生")
-* 提供良好的預設行為,但也給管理員在需要時覆蓋預設值的自由
-* 在適當的地方自我修復,但也在需要時給管理員手動控制系統狀態
-* 表現出可預測的行為,最小化意外
+* 讓監控工具能獲取關鍵指標,並支援可觀測性工具(參見 ["分散式系統的問題"](/tw/ch1#sec_introduction_dist_sys_problems))以洞察執行時行為。相關商業/開源工具都很多 [^90]。
+* 避免依賴單機(系統整體不停機的前提下允許下線機器維護)。
+* 提供完善文件和易理解的操作模型(“我做 X,會發生 Y”)。
+* 提供良好預設值,同時允許管理員在需要時覆蓋預設行為。
+* 適當支援自愈,同時在必要時保留管理員對系統狀態的手動控制權。
+* 行為可預測,儘量減少“驚喜”。
### 簡單性:管理複雜度 {#id38}
-小型軟體專案可以有令人愉悅的、簡單而富有表現力的程式碼,但隨著專案變大,它們通常變得非常複雜且難以理解。
-這種複雜性減慢了需要在系統上工作的每個人的效率,進一步增加了維護成本。陷入複雜性的軟體專案有時被描述為 *大泥團* [^91]。
+小型專案往往能保持簡潔、優雅、富有表達力;但專案變大後,程式碼常會迅速變複雜且難理解。這種複雜性會拖慢所有參與者效率,進一步抬高維護成本。陷入這種狀態的軟體專案常被稱為 *大泥團* [^91]。
-當複雜性使維護困難時,預算和時間表經常超支。在複雜軟體中,進行更改時引入錯誤的風險也更大:
-當系統對開發人員來說更難理解和推理時,隱藏的假設、意外的後果和意外的互動更容易被忽視 [^69]。
-相反,降低複雜性極大地提高了軟體的可維護性,因此簡單性應該是我們構建的系統的關鍵目標。
+當複雜性讓維護變難時,預算和進度常常失控。在複雜軟體裡,變更時引入缺陷的風險也更高:系統越難理解和推理,隱藏假設、非預期後果和意外互動就越容易被忽略 [^69]。反過來,降低複雜性能顯著提升可維護性,因此“追求簡單”應是系統設計核心目標之一。
-簡單系統更容易理解,因此我們應該嘗試以儘可能簡單的方式解決給定問題。不幸的是,這說起來容易做起來難。
-某物是否簡單通常是主觀的品味問題,因為沒有客觀的簡單性標準 [^92]。例如,一個系統可能在簡單介面後面隱藏複雜的實現,
-而另一個系統可能有一個向用戶公開更多內部細節的簡單實現——哪一個更簡單?
+簡單系統更容易理解,因此我們應儘可能用最簡單方式解決問題。但“簡單”知易行難。什麼叫簡單,往往帶有主觀判斷,因為不存在絕對客觀的簡單性標準 [^92]。例如,一個系統可能“介面簡單但實現複雜”,另一個可能“實現簡單但暴露更多內部細節”,到底誰更簡單,並不總有標準答案。
-推理複雜性的一種嘗試是將其分為兩類,**本質複雜性** 和 **偶然複雜性** [^93]。
-這個想法是,本質複雜性是應用程式問題域中固有的,而偶然複雜性僅由於我們工具的限制而產生。
-不幸的是,這種區別也有缺陷,因為本質和偶然之間的邊界隨著我們工具的發展而變化 [^94]。
+一種常見分析方法是把複雜性分成兩類:**本質複雜性** 與 **偶然複雜性** [^93]。前者源於業務問題本身,後者源於工具與實現限制。但這種劃分也並不完美,因為隨著工具演進,“本質”和“偶然”的邊界會移動 [^94]。
-我們管理複雜性的最佳工具之一是 **抽象**。良好的抽象可以在乾淨、易於理解的外觀後面隱藏大量實現細節。良好的抽象也可以用於各種不同的應用程式。
-這種重用不僅比多次重新實現類似的東西更有效,而且還能提高軟體質量,因為抽象元件中的質量改進使所有使用它的應用程式受益。
+管理複雜度最重要的工具之一是 **抽象**。好的抽象能在清晰外觀後隱藏大量實現細節,也能被多種場景複用。這種複用不僅比反覆重寫更高效,也能提升質量,因為抽象元件一旦改進,所有依賴它的應用都會受益。
-例如,高階程式語言是隱藏機器碼、CPU 暫存器和系統呼叫的抽象。SQL 是一種隱藏磁碟和記憶體中的複雜資料結構、來自其他客戶端的併發請求以及崩潰後的不一致性的抽象。
-當然,在用高階語言程式設計時,我們仍在使用機器碼;我們只是不 *直接* 使用它,因為程式語言抽象使我們不必考慮它。
+例如,高階語言是對機器碼、CPU 暫存器和系統呼叫的抽象。SQL 則抽象了磁碟/記憶體中的複雜資料結構、來自其他客戶端的併發請求,以及崩潰後的不一致狀態。用高階語言程式設計時,我們仍然在“使用機器碼”,但不再 *直接* 面對它,因為語言抽象替我們遮蔽了細節。
-應用程式程式碼的抽象,旨在降低其複雜性,可以使用諸如 *設計模式* [^95] 和 *領域驅動設計*(DDD)[^96] 等方法建立。
-本書不是關於此類特定於應用程式的抽象,而是關於你可以在其上構建應用程式的通用抽象,例如資料庫事務、索引和事件日誌。如果你想使用像 DDD 這樣的技術,你可以在本書中描述的基礎之上實現它們。
+應用程式碼層面的抽象,常藉助 *設計模式* [^95]、*領域驅動設計*(DDD)[^96] 等方法來構建。本書重點不在這類應用專用抽象,而在你可以拿來構建應用的通用抽象,例如資料庫事務、索引、事件日誌等。若你想採用 DDD 等方法,也可以建立在本書介紹的基礎能力之上。
### 可演化性:讓變化更容易 {#sec_introduction_evolvability}
-你的系統需求將保持不變的可能性極小。它們更可能處於不斷變化中:
-你學習新事實、以前未預料的用例出現、業務優先順序發生變化、使用者請求新功能、
-新平臺取代舊平臺、法律或監管要求發生變化、系統增長迫使架構變化等。
+系統需求永遠不變的機率極低。更常見的是持續變化:你會發現新事實,出現此前未預期用例,業務優先順序會調整,使用者會提出新功能,新平臺會替換舊平臺,法律與監管會變化,系統增長也會倒逼架構調整。
-在組織流程方面,*敏捷* 工作模式為適應變化提供了框架。敏捷社群還開發了在頻繁變化的環境中開發軟體時有用的技術工具和流程,
-例如測試驅動開發(TDD)和重構。在本書中,我們探尋在由具有不同特徵的幾個不同應用程式或服務組成的系統級別增加敏捷性的方法。
+在組織層面,*敏捷* 方法為適應變化提供了框架;敏捷社群也發展出多種適用於高變化環境的技術與流程,如測試驅動開發(TDD)和重構。本書關注的是:如何在“由多個不同應用/服務組成的系統層級”提升這種敏捷能力。
-你可以修改資料系統並使其適應不斷變化的需求的容易程度與其簡單性及其抽象密切相關:松耦合、簡單的系統通常比緊耦合、複雜的系統更容易修改。
-由於這是一個如此重要的概念,我們將使用一個不同的詞來指代資料系統級別的敏捷性:*可演化性* [^97]。
+資料系統對變化的適應難易度,與其簡單性和抽象質量高度相關:松耦合、簡單系統通常比緊耦合、複雜系統更容易修改。由於這一點極其重要,我們把“資料系統層面的敏捷性”單獨稱為 *可演化性* [^97]。
-使大型系統中的變化困難的一個主要因素是某些操作不可逆,因此需要非常謹慎地採取該操作 [^98]。
-例如,假設你正在從一個數據庫遷移到另一個數據庫:如果在新資料庫出現問題時無法切換回舊系統,風險就會高得多,而如果你可以輕鬆返回。最小化不可逆性提高了靈活性。
+大型系統中讓變更困難的一個關鍵因素,是某些操作不可逆,因此執行時必須極其謹慎 [^98]。例如從一個數據庫遷移到另一個:若新庫出問題後無法回切,風險就遠高於可隨時回退。儘量減少不可逆操作,能顯著提升系統靈活性。
## 總結 {#summary}
-在本章中,我們研究了幾個非功能性需求的例子:效能、可靠性、可伸縮性和可維護性。
-透過這些主題,我們還遇到了我們在本書其餘部分需要的原則和術語。我們從社交網路中首頁時間線如何實現的案例研究開始,這說明了在規模擴大時出現的一些挑戰。
+本章討論了幾類核心非功能性需求:效能、可靠性、可伸縮性與可維護性。圍繞這些主題,我們也建立了貫穿全書的一組概念與術語。章節從“社交網路首頁時間線”案例切入,直觀展示了系統在規模增長時會遇到的現實挑戰。
-我們討論了如何衡量效能(例如,使用響應時間百分位數)、系統上的負載(例如,使用吞吐量指標),以及它們如何在 SLA 中使用。
-可伸縮性是一個密切相關的概念:即,在負載增長時確保效能保持不變。我們看到了可伸縮性的一些通用性原則,例如將任務分解為可以獨立執行的較小部分,我們將在以下章節中深入研究可伸縮性技術的技術細節。
+我們討論了如何衡量效能(例如響應時間百分位點)、如何描述系統負載(例如吞吐量指標),以及這些指標如何進入 SLA。與之緊密相關的是可伸縮性:當負載增長時,如何保持效能不退化。我們也給出了若干通用原則,例如將任務拆解為可獨立執行的小元件。後續章節會深入展開相關技術細節。
-為了實現可靠性,你可以使用容錯技術,即使某個元件(例如,磁碟、機器或其他服務)出現故障,系統也可以繼續提供其服務。
-我們看到了可能發生的硬體故障的例子,並將它們與軟體故障區分開來,軟體故障可能更難處理,因為它們通常是強相關的。
-實現可靠性的另一個方面是建立對人類犯錯誤的韌性,我們看到無責備事後分析作為從事件中學習的技術。
+為實現可靠性,可以使用容錯機制,使系統在部分元件(如磁碟、機器或外部服務)故障時仍能持續提供服務。我們區分了硬體故障與軟體故障,並指出軟體故障常更難處理,因為它們往往高度相關。可靠性的另一面是“對人為失誤的韌性”,其中 *無責備事後分析* 是重要學習機制。
-最後,我們研究了可維護性的幾個方面,包括支援運維團隊的工作、管理複雜性以及隨著時間的推移使應用程式功能易於演化。
-實現這些目標沒有簡單的答案,但有一件事可以幫助,那就是使用提供有用抽象的易於理解的構建塊來構建應用程式。本書的其餘部分將涵蓋一系列在實踐中被證明有價值的構建塊。
+最後,我們討論了可維護性的多個維度:支援運維工作、管理複雜度、提升系統可演化性。實現這些目標沒有銀彈,但一個普遍有效的做法是:用清晰、可理解、具備良好抽象的構件來搭建系統。接下來全書會介紹一系列在實踐中證明有效的構件。
-### 參考 {#參考}
+### 參考文獻
[^1]: Mike Cvet. [How We Learned to Stop Worrying and Love Fan-In at Twitter](https://www.youtube.com/watch?v=WEgCjwyXvwc). At *QCon San Francisco*, December 2016.
[^2]: Raffi Krikorian. [Timelines at Scale](https://www.infoq.com/presentations/Twitter-Timeline-Scalability/). At *QCon San Francisco*, November 2012. Archived at [perma.cc/V9G5-KLYK](https://perma.cc/V9G5-KLYK)
diff --git a/content/tw/ch3.md b/content/tw/ch3.md
index 66b78ef..baa1956 100644
--- a/content/tw/ch3.md
+++ b/content/tw/ch3.md
@@ -167,7 +167,7 @@ SELECT users.*, regions.region_name
WHERE users.id = 251;
```
-文件資料庫可以儲存正規化和反正規化的資料,但它們通常與反正規化相關聯 —— 部分是因為 JSON 資料模型使得儲存額外的反正規化欄位變得容易,部分是因為許多文件資料庫中對連線的弱支援使得正規化不方便。一些文件資料庫根本不支援連線,因此你必須在應用程式程式碼中執行它們 —— 也就是說,你首先獲取包含 ID 的文件,然後執行第二個查詢將該 ID 解析為另一個文件。在 MongoDB 中,也可以使用聚合管道中的 `$lookup` 運算子執行連線:
+文件資料庫可以儲存正規化和反正規化的資料,但它們通常與反正規化相關聯 —— 部分是因為 JSON 資料模型使得儲存額外的反正規化欄位變得容易,部分是因為許多文件資料庫中對連線的弱支援使得正規化不方便。一些文件資料庫根本不支援連線,因此你必須在應用程式程式碼中執行它們 —— 也就是說,你首先獲取包含 ID 的文件,然後執行第二個查詢將該 ID 解析為另一個文件。在 MongoDB 中,也可以使用聚合管道中的 `$lookup` 運算元執行連線:
```mongodb-json
db.users.aggregate([
@@ -190,9 +190,9 @@ db.users.aggregate([
* 在反正規化表示中,我們會在每個人的個人資料中包含標誌的影像 URL;這使得 JSON 文件自包含,但如果我們需要更改標誌,就會產生麻煩,因為我們現在需要找到舊 URL 的所有出現並更新它們 [^9]。
* 在正規化表示中,我們將建立一個代表組織或學校的實體,並在該實體上儲存其名稱、標誌 URL 以及可能的其他屬性(描述、新聞提要等)一次。然後,每個提到該組織的簡歷都會簡單地引用其 ID,更新標誌很容易。
-作為一般原則,正規化資料通常寫入更快(因為只有一個副本),但查詢更慢(因為它需要連線);反正規化資料通常讀取更快(連線更少),但寫入更昂貴(更多副本要更新,使用更多磁碟空間)。你可能會發現將反正規化視為派生資料的一種形式很有幫助(["記錄系統和派生資料"](/tw/ch1#sec_introduction_derived)),因為你需要設定一個過程來更新資料的冗餘副本。
+作為一般原則,正規化資料通常寫入更快(因為只有一個副本),但查詢更慢(因為它需要連線);反正規化資料通常讀取更快(連線更少),但寫入更昂貴(更多副本要更新,使用更多磁碟空間)。你可能會發現將反正規化視為派生資料的一種形式很有幫助(["記錄系統與派生資料"](/tw/ch1#sec_introduction_derived)),因為你需要設定一個過程來更新資料的冗餘副本。
-除了執行所有這些更新的成本之外,如果程序在進行更新的過程中崩潰,你還需要考慮資料庫的一致性。提供原子事務的資料庫(參見 ["原子性"](/tw/ch8#sec_transactions_acid_atomicity))使保持一致性變得更容易,但並非所有資料庫都在多個文件之間提供原子性。透過流處理確保一致性也是可能的,我們將在 [待補充連結] 中討論。
+除了執行所有這些更新的成本之外,如果程序在進行更新的過程中崩潰,你還需要考慮資料庫的一致性。提供原子事務的資料庫(參見 ["原子性"](/tw/ch8#sec_transactions_acid_atomicity))使保持一致性變得更容易,但並非所有資料庫都在多個文件之間提供原子性。透過流處理確保一致性也是可能的,我們將在 ["保持系統同步"](/tw/ch12#sec_stream_sync) 中討論。
正規化往往更適合 OLTP 系統,其中讀取和更新都需要快速;分析系統通常使用反正規化資料表現更好,因為它們批次執行更新,只讀查詢的效能是主要關注點。此外,在中小規模的系統中,正規化資料模型通常是最好的,因為你不必擔心保持資料的多個副本相互一致,執行連線的成本是可以接受的。然而,在非常大規模的系統中,連線的成本可能會成為問題。
@@ -200,7 +200,7 @@ db.users.aggregate([
在 ["案例研究:社交網路首頁時間線"](/tw/ch2#sec_introduction_twitter) 中,我們比較了正規化表示([圖 2-1](/tw/ch2#fig_twitter_relational))和反正規化表示(預計算的物化時間線):這裡,`posts` 和 `follows` 之間的連線太昂貴了,物化時間線是該連線結果的快取。將新帖子插入關注者時間線的扇出過程是我們保持反正規化表示一致的方式。
-然而,X(前 Twitter)的物化時間線實現實際上並不儲存每個帖子的實際文字:每個條目實際上只儲存帖子 ID、釋出者的使用者 ID,以及一些額外的資訊來識別轉發和回覆 [^11]。換句話說,它是(大約)以下查詢的預計算結果:
+然而,X(前 Twitter)的物化時間線實現實際上並不儲存每個帖子的實際文字:每個條目實際上只儲存帖子 ID、釋出者的使用者 ID,以及一些額外的資訊來識別轉發和回覆 [^11]。換句話說,它大致是以下查詢的預計算結果:
```sql
SELECT posts.id, posts.sender_id
@@ -211,11 +211,11 @@ SELECT posts.id, posts.sender_id
LIMIT 1000
```
-這意味著每當讀取時間線時,服務仍然需要執行兩個連線:透過 ID 查詢帖子以獲取實際的帖子內容(以及點贊數和回覆數等統計資訊),並透過 ID 查詢傳送者的個人資料(以獲取他們的使用者名稱、個人資料圖片和其他詳細資訊)。這個透過 ID 查詢人類可讀資訊的過程稱為 *hydrating* ID,它本質上是在應用程式程式碼中執行的連線 [^11]。
+這意味著每當讀取時間線時,服務仍然需要執行兩個連線:透過 ID 查詢帖子以獲取實際的帖子內容(以及點贊數和回覆數等統計資訊),並透過 ID 查詢傳送者的個人資料(以獲取他們的使用者名稱、個人資料圖片和其他詳細資訊)。這個將 ID 補全為人類可讀資訊的過程稱為 *hydrating* ID,本質上是在應用程式程式碼中執行的連線 [^11]。
在預計算時間線中僅儲存 ID 的原因是它們引用的資料變化很快:熱門帖子的點贊數和回覆數可能每秒變化多次,一些使用者定期更改他們的使用者名稱或個人資料照片。由於時間線在檢視時應該顯示最新的點贊數和個人資料圖片,因此將此資訊反正規化到物化時間線中是沒有意義的。此外,這種反正規化會顯著增加儲存成本。
-這個例子表明,在讀取資料時必須執行連線並不像有時聲稱的那樣,是建立高效能、可擴充套件服務的障礙。Hydrating 帖子 ID 和使用者 ID 實際上是一個相當容易擴充套件的操作,因為它可以很好地並行化,並且成本不取決於你關注的帳戶數量或你擁有的關注者數量。
+這個例子表明,在讀取資料時必須執行連線並不像有時聲稱的那樣,是建立高效能、可擴充套件服務的障礙。`hydrating` 帖子 ID 和使用者 ID 實際上是一個相當容易擴充套件的操作,因為它可以很好地並行化,並且成本不取決於你關注的賬戶數量或你擁有的關注者數量。
如果你需要決定是否在應用程式中反正規化某些內容,社交網路案例研究表明選擇並不是立即顯而易見的:最可擴充套件的方法可能涉及反正規化某些內容並保持其他內容正規化。你必須仔細考慮資訊更改的頻率以及讀寫成本(這可能由異常值主導,例如在典型社交網路的情況下擁有許多關注/關注者的使用者)。正規化和反正規化本質上並不好或壞 —— 它們只是在讀寫效能以及實施工作量方面的權衡。
@@ -340,7 +340,7 @@ UPDATE users SET first_name = substring_index(name, ' ', 1); -- MySQL
XML 資料庫通常使用 XQuery 和 XPath 查詢,它們旨在允許複雜的查詢,包括跨多個文件的連線,並將其結果格式化為 XML [^28]。JSON Pointer [^29] 和 JSONPath [^30] 為 JSON 提供了等效於 XPath 的功能。
-MongoDB 的聚合管道,我們在 ["正規化、反正規化與連線"](/tw/ch3#sec_datamodels_normalization) 中看到了其用於連線的 `$lookup` 運算子,是 JSON 文件集合查詢語言的一個例子。
+MongoDB 的聚合管道,我們在 ["正規化、反正規化與連線"](/tw/ch3#sec_datamodels_normalization) 中看到了其用於連線的 `$lookup` 運算元,是 JSON 文件集合查詢語言的一個例子。
讓我們看另一個例子來感受這種語言 —— 這次是聚合,這對分析特別需要。想象你是一名海洋生物學家,每次你在海洋中看到動物時,你都會向資料庫新增一條觀察記錄。現在你想生成一份報告,說明你每個月看到了多少條鯊魚。在 PostgreSQL 中,你可能會這樣表達該查詢:
@@ -373,9 +373,9 @@ db.observations.aggregate([
#### 文件和關係資料庫的融合 {#convergence-of-document-and-relational-databases}
-文件資料庫和關係資料庫最初是非常不同的資料管理方法,但隨著時間的推移,它們變得更加相似 [^31]。關係資料庫增加了對 JSON 型別和查詢運算子的支援,以及索引文件內屬性的能力。一些文件資料庫(如 MongoDB、Couchbase 和 RethinkDB)增加了對連線、二級索引和宣告式查詢語言的支援。
+文件資料庫和關係資料庫最初是非常不同的資料管理方法,但隨著時間的推移,它們變得更加相似 [^31]。關係資料庫增加了對 JSON 型別和查詢運算元的支援,以及索引文件內屬性的能力。一些文件資料庫(如 MongoDB、Couchbase 和 RethinkDB)增加了對連線、二級索引和宣告式查詢語言的支援。
-模型的這種融合對應用程式開發人員來說是個好訊息,因為當你可以在同一個資料庫中組合兩者時,關係模型和文件模型效果最好。許多文件資料庫需要對其他文件的關係式引用,許多關係資料庫在模式靈活性有益的部分。關係-文件混合是一個強大的組合。
+模型的這種融合對應用程式開發人員來說是個好訊息,因為當你可以在同一個資料庫中組合兩者時,關係模型和文件模型效果最好。許多文件資料庫需要對其他文件進行關係式引用,許多關係資料庫也有一些場景更適合模式靈活性。關係-文件混合是一個強大的組合。
--------
@@ -531,7 +531,7 @@ RETURN person.name
在我們的示例中,這發生在 Cypher 查詢中的 `() -[:WITHIN*0..]-> ()` 模式中。一個人的 `LIVES_IN` 邊可能指向任何型別的位置:街道、城市、區(district)、地區(region)、州等。一個城市可能在(`WITHIN`)某個地區,該地區在(`WITHIN`)某個州,該州在(`WITHIN`)某個國家,等等。`LIVES_IN` 邊可能直接指向你要查詢的位置頂點,或者它可能在位置層次結構中相距幾個級別。
-在 Cypher 中,`:WITHIN*0..` 非常簡潔地表達了這個事實:它意味著"跟隨 `WITHIN` 邊,零次或多次"。它就像正則表示式中的 `*` 運算子。
+在 Cypher 中,`:WITHIN*0..` 非常簡潔地表達了這個事實:它意味著"跟隨 `WITHIN` 邊,零次或多次"。它就像正則表示式中的 `*` 運算元。
自 SQL:1999 以來,查詢中可變長度遍歷路徑的想法可以使用稱為 *遞迴公用表表達式*(`WITH RECURSIVE` 語法)的東西來表達。[示例 3-6](/tw/ch3#fig_graph_sql_query) 顯示了相同的查詢 —— 查詢從美國移民到歐洲的人的姓名 —— 使用此技術在 SQL 中表達。然而,與 Cypher 相比,語法非常笨拙。
@@ -781,7 +781,7 @@ Cypher 和 SPARQL 直接用 `SELECT` 開始,但 Datalog 一次只邁出一小
在 [示例 3-12](/tw/ch3#fig_datalog_query) 中,我們定義了三個派生表:`within_recursive`、`migrated` 和 `us_to_europe`。虛擬表的名稱和列由每個規則的 `:-` 符號之前出現的內容定義。例如,`migrated(PName, BornIn, LivingIn)` 是一個具有三列的虛擬表:一個人的姓名、他們出生地的名稱和他們居住地的名稱。
-虛擬表的內容由規則的 `:-` 符號之後的部分定義,我們在其中嘗試查詢表中匹配某種模式的行。例如,`person(PersonID, PName)` 匹配行 `person(100, "Lucy")`,變數 `PersonID` 繫結到值 `100`,變數 `PName` 繫結到值 `"Lucy"`。如果系統可以為 `:-` 運算子右側的 *所有* 模式找到匹配項,則規則適用。當規則適用時,就好像 `:-` 的左側被新增到資料庫中(變數被它們匹配的值替換)。
+虛擬表的內容由規則的 `:-` 符號之後的部分定義,我們在其中嘗試查詢表中匹配某種模式的行。例如,`person(PersonID, PName)` 匹配行 `person(100, "Lucy")`,變數 `PersonID` 繫結到值 `100`,變數 `PName` 繫結到值 `"Lucy"`。如果系統可以為 `:-` 運算元右側的 *所有* 模式找到匹配項,則規則適用。當規則適用時,就好像 `:-` 的左側被新增到資料庫中(變數被它們匹配的值替換)。
因此,應用規則的一種可能方式是(如 [圖 3-7](/tw/ch3#fig_datalog_naive) 所示):
@@ -803,7 +803,7 @@ Cypher 和 SPARQL 直接用 `SELECT` 開始,但 Datalog 一次只邁出一小
GraphQL 是一種查詢語言,從設計上講,它比我們在本章中看到的其他查詢語言限制性更強。GraphQL 的目的是允許在使用者裝置上執行的客戶端軟體(如移動應用程式或 JavaScript Web 應用程式前端)請求具有特定結構的 JSON 文件,其中包含渲染其使用者介面所需的欄位。GraphQL 介面允許開發人員快速更改客戶端程式碼中的查詢,而無需更改伺服器端 API。
-GraphQL 的靈活性是有代價的。採用 GraphQL 的組織通常需要工具將 GraphQL 查詢轉換為對內部服務的請求,這些服務通常使用 REST 或 gRPC(參見 [第 5 章](/tw/ch5#ch_encoding))。授權、速率限制和效能挑戰是額外的關注點 [^61]。GraphQL 的查詢語言也受到限制,因為 GraphQL 來自不受信任的來源。該語言不允許任何可能執行成本高昂的操作,否則使用者可能透過執行大量昂貴的查詢對伺服器執行拒絕服務攻擊。特別是,GraphQL 不允許遞迴查詢(與 Cypher、SPARQL、SQL 或 Datalog 不同),並且不允許任意搜尋條件,如"查詢在美國出生並現在居住在歐洲的人"(除非服務所有者特別選擇提供此類搜尋功能)。
+GraphQL 的靈活性是有代價的。採用 GraphQL 的組織通常需要工具將 GraphQL 查詢轉換為對內部服務的請求,這些服務通常使用 REST 或 gRPC(參見 [第 5 章](/tw/ch5#ch_encoding))。授權、速率限制和效能挑戰是額外的關注點 [^61]。GraphQL 的查詢語言也受到限制,因為 GraphQL 查詢來自不受信任的來源。該語言不允許任何可能執行成本高昂的操作,否則使用者可能透過執行大量昂貴的查詢對伺服器執行拒絕服務攻擊。特別是,GraphQL 不允許遞迴查詢(與 Cypher、SPARQL、SQL 或 Datalog 不同),並且不允許任意搜尋條件,如"查詢在美國出生並現在居住在歐洲的人"(除非服務所有者特別選擇提供此類搜尋功能)。
儘管如此,GraphQL 還是很有用的。[示例 3-13](/tw/ch3#fig_graphql_query) 顯示了如何使用 GraphQL 實現 Discord 或 Slack 等群聊應用程式。查詢請求使用者有權訪問的所有頻道,包括頻道名稱和每個頻道中的 50 條最新訊息。對於每條訊息,它請求時間戳、訊息內容以及訊息傳送者的姓名和個人資料圖片 URL。此外,如果訊息是對另一條訊息的回覆,查詢還會請求傳送者姓名和它所回覆的訊息內容(可能以較小的字型呈現在回覆上方,以提供一些上下文)。
@@ -873,17 +873,17 @@ query ChatApp {
在我們迄今為止討論的所有資料模型中,資料以與寫入相同的形式被查詢 —— 無論是 JSON 文件、表中的行,還是圖中的頂點和邊。然而,在複雜的應用程式中,有時很難找到一種能夠滿足所有不同查詢和呈現資料方式的單一資料表示。在這種情況下,以一種形式寫入資料,然後從中派生出針對不同型別讀取最佳化的多種表示形式可能是有益的。
-我們之前在 ["記錄系統和派生資料"](/tw/ch1#sec_introduction_derived) 中看到了這個想法,ETL(參見 ["資料倉庫"](/tw/ch1#sec_introduction_dwh))就是這種派生過程的一個例子。現在我們將進一步深入這個想法。如果我們無論如何都要從一種資料表示派生出另一種,我們可以選擇分別針對寫入和讀取最佳化的不同表示。如果你只想為寫入最佳化資料建模,而不關心高效查詢,你會如何建模?
+我們之前在 ["記錄系統與派生資料"](/tw/ch1#sec_introduction_derived) 中看到了這個想法,ETL(參見 ["資料倉庫"](/tw/ch1#sec_introduction_dwh))就是這種派生過程的一個例子。現在我們將進一步深入這個想法。如果我們無論如何都要從一種資料表示派生出另一種,我們可以選擇分別針對寫入和讀取最佳化的不同表示。如果你只想為寫入最佳化資料建模,而不關心高效查詢,你會如何建模?
也許寫入資料的最簡單、最快速和最具表現力的方式是 *事件日誌*:每次你想寫入一些資料時,你將其編碼為自包含的字串(可能是 JSON),包括時間戳,然後將其追加到事件序列中。此日誌中的事件是 *不可變的*:你永遠不會更改或刪除它們,你只會向日志追加更多事件(這可能會取代早期事件)。事件可以包含任意屬性。
[圖 3-8](/tw/ch3#fig_event_sourcing) 顯示了一個可能來自會議管理系統的示例。會議可能是一個複雜的業務領域:不僅個人參與者可以註冊並用信用卡付款,公司也可以批次訂購座位,透過發票付款,然後再將座位分配給個人。一些座位可能為演講者、贊助商、志願者助手等保留。預訂也可能被取消,與此同時,會議組織者可能透過將其移至不同的房間來更改活動的容量。在所有這些情況發生時,簡單地計算可用座位數量就成為一個具有挑戰性的查詢。
-{{< figure src="/fig/ddia_0308.png" id="fig_event_sourcing" title="圖 3-8. 使用不可變事件日誌作為真相源,並從中派生物化檢視。" class="w-full my-4" >}}
+{{< figure src="/fig/ddia_0308.png" id="fig_event_sourcing" title="圖 3-8. 使用不可變事件日誌作為真相來源(權威資料來源),並從中派生物化檢視。" class="w-full my-4" >}}
在 [圖 3-8](/tw/ch3#fig_event_sourcing) 中,會議狀態的每個變化(例如組織者開放註冊,或參與者進行和取消註冊)首先被儲存為事件。每當事件追加到日誌時,幾個 *物化檢視*(也稱為 *投影* 或 *讀模型*)也會更新以反映該事件的影響。在會議示例中,可能有一個物化檢視收集與每個預訂狀態相關的所有資訊,另一個為會議組織者的儀表板計算圖表,第三個為列印參與者徽章的印表機生成檔案。
-使用事件作為真相源,並將每個狀態變化表達為事件的想法被稱為 *事件溯源* [^62] [^63]。維護單獨的讀最佳化表示並從寫最佳化表示派生它們的原則稱為 *命令查詢責任分離(CQRS)* [^64]。這些術語起源於領域驅動設計(DDD)社群,儘管類似的想法已經存在很長時間了,例如 *狀態機複製*(參見 ["使用共享日誌"](/tw/ch10#sec_consistency_smr))。
+使用事件作為真相來源(權威資料來源),並將每個狀態變化表達為事件的想法被稱為 *事件溯源* [^62] [^63]。維護單獨的讀最佳化表示並從寫最佳化表示派生它們的原則稱為 *命令查詢責任分離(CQRS)* [^64]。這些術語起源於領域驅動設計(DDD)社群,儘管類似的想法已經存在很長時間了,例如 *狀態機複製*(參見 ["使用共享日誌"](/tw/ch10#sec_consistency_smr))。
當用戶的請求進來時,它被稱為 *命令*,首先需要驗證。只有在命令已執行並確定有效(例如,請求的預訂有足夠的可用座位)後,它才成為事實,相應的事件被新增到日誌中。因此,事件日誌應該只包含有效事件,構建物化檢視的事件日誌消費者不允許拒絕事件。
@@ -906,7 +906,7 @@ query ChatApp {
* 事件不可變的要求會在事件包含使用者的個人資料時產生問題,因為使用者可能行使他們的權利(例如,根據 GDPR)請求刪除他們的資料。如果事件日誌是基於每個使用者的,你可以刪除該使用者的整個日誌,但如果你的事件日誌包含與多個使用者相關的事件,這就不起作用了。你可以嘗試將個人資料儲存在實際事件之外,或者使用金鑰對其進行加密,你可以稍後選擇刪除該金鑰,但這也使得在需要時更難重新計算派生狀態。
* 如果存在外部可見的副作用,重新處理事件需要小心 —— 例如,你可能不希望每次重建物化檢視時都重新發送確認電子郵件。
-你可以在任何資料庫之上實現事件溯源,但也有一些專門設計來支援這種模式的系統,例如 EventStoreDB、MartenDB(基於 PostgreSQL)和 Axon Framework。你還可以使用訊息代理(如 Apache Kafka)來儲存事件日誌,流處理器可以使物化檢視保持最新;我們將在 [待補充連結] 中返回這些主題。
+你可以在任何資料庫之上實現事件溯源,但也有一些專門設計來支援這種模式的系統,例如 EventStoreDB、MartenDB(基於 PostgreSQL)和 Axon Framework。你還可以使用訊息代理(如 Apache Kafka)來儲存事件日誌,流處理器可以使物化檢視保持最新;我們將在 ["資料變更捕獲與事件溯源"](/tw/ch12#sec_stream_event_sourcing) 中回到這些主題。
唯一重要的要求是事件儲存系統必須保證所有物化檢視以與它們在日誌中出現的完全相同的順序處理事件;正如我們將在 [第 10 章](/tw/ch10#ch_consistency) 中看到的,這在分散式系統中並不總是容易實現。
@@ -917,7 +917,7 @@ query ChatApp {
資料框是 R 語言、Python 的 Pandas 庫、Apache Spark、ArcticDB、Dask 和其他系統支援的資料模型。它們是資料科學家為訓練機器學習模型準備資料的流行工具,但它們也廣泛用於資料探索、統計資料分析、資料視覺化和類似目的。
-乍一看,資料框類似於關係資料庫中的表或電子表格。它支援對資料框內容執行批次操作的類關係運算符:例如,將函式應用於所有行、基於某些條件過濾行、按某些列對行進行分組並聚合其他列,以及基於某個鍵將一個數據框中的行與另一個數據框連線(關係資料庫稱為 *連線* 的操作在資料框上通常稱為 *合併*)。
+乍一看,資料框類似於關係資料庫中的表或電子表格。它支援對資料框內容執行批次操作的類關係運算元:例如,將函式應用於所有行、基於某些條件過濾行、按某些列對行進行分組並聚合其他列,以及基於某個鍵將一個數據框中的行與另一個數據框連線(關係資料庫稱為 *連線* 的操作在資料框上通常稱為 *合併*)。
資料框通常不是透過宣告式查詢(如 SQL)而是透過一系列修改其結構和內容的命令來操作的。這符合資料科學家的典型工作流程,他們逐步"整理"資料,使其成為能夠找到他們所提問題答案的形式。這些操作通常在資料科學家的資料集私有副本上進行,通常在他們的本地機器上,儘管最終結果可能與其他使用者共享。
diff --git a/content/tw/ch4.md b/content/tw/ch4.md
index bde2b81..3be769a 100644
--- a/content/tw/ch4.md
+++ b/content/tw/ch4.md
@@ -4,6 +4,8 @@ weight: 104
breadcrumbs: false
---
+
+

> *生活的苦惱之一是,每個人對事物的命名都有些偏差。這讓我們理解世界變得比本該有的樣子困難一些,要是命名方式不同就好了。計算機的主要功能並不是傳統意義上的計算,比如算術運算。[……] 它們主要是歸檔系統。*
@@ -19,7 +21,7 @@ breadcrumbs: false
特別是,針對事務型工作負載(OLTP)最佳化的儲存引擎和針對分析型工作負載最佳化的儲存引擎之間存在巨大差異(我們在 ["分析型與事務型系統"](/tw/ch1#sec_introduction_analytics) 中介紹了這種區別)。本章首先研究兩種用於 OLTP 的儲存引擎家族:寫入不可變資料檔案的 *日誌結構* 儲存引擎,以及像 *B 樹* 這樣就地更新資料的儲存引擎。這些結構既用於鍵值儲存,也用於二級索引。
-隨後在 ["分析型資料儲存"](#sec_storage_analytics) 中,我們將討論一系列針對分析最佳化的儲存引擎;在 ["多維索引與全文索引"](#sec_storage_multidimensional) 中,我們將簡要介紹用於更高階查詢(如文字檢索)的索引。
+隨後在 ["分析型資料儲存"](/tw/ch4#sec_storage_analytics) 中,我們將討論一系列針對分析最佳化的儲存引擎;在 ["多維索引與全文索引"](/tw/ch4#sec_storage_multidimensional) 中,我們將簡要介紹用於更高階查詢(如文字檢索)的索引。
## OLTP 系統的儲存與索引 {#sec_storage_oltp}
@@ -39,7 +41,7 @@ db_get () {
這兩個函式實現了一個鍵值儲存。你可以呼叫 `db_set key value`,它將在資料庫中儲存 `key` 和 `value`。鍵和值可以是(幾乎)任何你喜歡的內容 —— 例如,值可以是一個 JSON 文件。然後你可以呼叫 `db_get key`,它會查詢與該特定鍵關聯的最新值並返回它。
-它確實能工作:
+麻雀雖小,五臟俱全:
```bash
$ db_set 12 '{"name":"London","attractions":["Big Ben","London Eye"]}'
@@ -85,7 +87,7 @@ $ cat database
### 日誌結構儲存 {#sec_storage_log_structured}
-首先,讓我們假設你想繼續將資料儲存在 `db_set` 寫入的僅追加檔案中,你只是想加快讀取速度。一種方法是在記憶體中保留一個雜湊對映,其中每個鍵都對映到檔案中可以找到該鍵最新值的位元組偏移量,如 [圖 4-1](#fig_storage_csv_hash_index) 所示。
+首先,讓我們假設你想繼續將資料儲存在 `db_set` 寫入的僅追加檔案中,你只是想加快讀取速度。一種方法是在記憶體中保留一個雜湊對映,其中每個鍵都對映到檔案中可以找到該鍵最新值的位元組偏移量,如 [圖 4-1](/tw/ch4#fig_storage_csv_hash_index) 所示。
{{< figure src="/fig/ddia_0401.png" id="fig_storage_csv_hash_index" caption="圖 4-1. 以類似 CSV 格式儲存鍵值對日誌,使用記憶體雜湊對映建立索引。" class="w-full my-4" >}}
@@ -100,15 +102,15 @@ $ cat database
#### SSTable 檔案格式 {#the-sstable-file-format}
-實際上,雜湊表很少用於資料庫索引,相反,保持資料 *按鍵排序* 的結構更為常見 [^3]。這種結構的一個例子是 *排序字串表*(*Sorted String Table*),簡稱 *SSTable*,如 [圖 4-2](#fig_storage_sstable_index) 所示。這種檔案格式也儲存鍵值對,但它確保它們按鍵排序,每個鍵在檔案中只出現一次。
+實際上,雜湊表很少用於資料庫索引,相反,保持資料 *按鍵排序* 的結構更為常見 [^3]。這種結構的一個例子是 *排序字串表*(*Sorted String Table*),簡稱 *SSTable*,如 [圖 4-2](/tw/ch4#fig_storage_sstable_index) 所示。這種檔案格式也儲存鍵值對,但它確保它們按鍵排序,每個鍵在檔案中只出現一次。
{{< figure src="/fig/ddia_0402.png" id="fig_storage_sstable_index" caption="圖 4-2. 帶有稀疏索引的 SSTable,允許查詢跳轉到正確的塊。" class="w-full my-4" >}}
現在你不需要在記憶體中保留所有鍵:你可以將 SSTable 中的鍵值對分組為幾千位元組的 *塊*,然後在索引中儲存每個塊的第一個鍵。這種只儲存部分鍵的索引稱為 *稀疏* 索引。這個索引儲存在 SSTable 的單獨部分,例如使用不可變 B 樹、字典樹或其他允許查詢快速查詢特定鍵的資料結構 [^4]。
-例如,在 [圖 4-2](#fig_storage_sstable_index) 中,一個塊的第一個鍵是 `handbag`,下一個塊的第一個鍵是 `handsome`。現在假設你要查詢鍵 `handiwork`,它沒有出現在稀疏索引中。由於排序,你知道 `handiwork` 必須出現在 `handbag` 和 `handsome` 之間。這意味著你可以尋找到 `handbag` 的偏移量,然後從那裡掃描檔案,直到找到 `handiwork`(或沒有,如果該鍵不在檔案中)。幾千位元組的塊可以非常快速地掃描。
+例如,在 [圖 4-2](/tw/ch4#fig_storage_sstable_index) 中,一個塊的第一個鍵是 `handbag`,下一個塊的第一個鍵是 `handsome`。現在假設你要查詢鍵 `handiwork`,它沒有出現在稀疏索引中。由於排序,你知道 `handiwork` 必須出現在 `handbag` 和 `handsome` 之間。這意味著你可以尋找到 `handbag` 的偏移量,然後從那裡掃描檔案,直到找到 `handiwork`(或沒有,如果該鍵不在檔案中)。幾千位元組的塊可以非常快速地掃描。
-此外,每個記錄塊都可以壓縮(在 [圖 4-2](#fig_storage_sstable_index) 中用陰影區域表示)。除了節省磁碟空間外,壓縮還減少了 I/O 頻寬使用,代價是使用更多一點的 CPU 時間。
+此外,每個記錄塊都可以壓縮(在 [圖 4-2](/tw/ch4#fig_storage_sstable_index) 中用陰影區域表示)。除了節省磁碟空間外,壓縮還減少了 I/O 頻寬使用,代價是使用更多一點的 CPU 時間。
#### 構建和合並 SSTable {#constructing-and-merging-sstables}
@@ -121,7 +123,7 @@ SSTable 檔案格式在讀取方面比僅追加日誌更好,但它使寫入更
3. 為了讀取某個鍵的值,首先嘗試在記憶體表和最新的磁碟段中找到該鍵。如果沒有找到,就在下一個較舊的段中查詢,依此類推,直到找到鍵或到達最舊的段。如果鍵沒有出現在任何段中,則它不存在於資料庫中。
4. 不時地在後臺執行合併和壓實過程,以合併段檔案並丟棄被覆蓋或刪除的值。
-合併段的工作方式類似於 *歸併排序* 演算法 [^5]。該過程如 [圖 4-3](#fig_storage_sstable_merging) 所示:並排開始讀取輸入檔案,檢視每個檔案中的第一個鍵,將最低的鍵(根據排序順序)複製到輸出檔案,然後重複。如果同一個鍵出現在多個輸入檔案中,只保留較新的值。這會產生一個新的合併段檔案,也按鍵排序,每個鍵只有一個值,並且它使用最少的記憶體,因為我們可以一次遍歷一個鍵的 SSTable。
+合併段的工作方式類似於 *歸併排序* 演算法 [^5]。該過程如 [圖 4-3](/tw/ch4#fig_storage_sstable_merging) 所示:並排開始讀取輸入檔案,檢視每個檔案中的第一個鍵,將最低的鍵(根據排序順序)複製到輸出檔案,然後重複。如果同一個鍵出現在多個輸入檔案中,只保留較新的值。這會產生一個新的合併段檔案,也按鍵排序,每個鍵只有一個值,並且它使用最少的記憶體,因為我們可以一次遍歷一個鍵的 SSTable。
{{< figure src="/fig/ddia_0403.png" id="fig_storage_sstable_merging" caption="圖 4-3. 合併多個 SSTable 段,僅保留每個鍵的最新值。" class="w-full my-4" >}}
@@ -139,15 +141,17 @@ SSTable 檔案格式在讀取方面比僅追加日誌更好,但它使寫入更
具有不可變段檔案也簡化了崩潰恢復:如果在寫出記憶體表或合併段時發生崩潰,資料庫可以刪除未完成的 SSTable 並重新開始。將寫入持久化到記憶體表的日誌如果在寫入記錄的過程中發生崩潰,或者磁碟已滿,可能包含不完整的記錄;這些通常透過在日誌中包含校驗和來檢測,並丟棄損壞或不完整的日誌條目。我們將在 [第 8 章](/tw/ch8#ch_transactions) 中更多地討論永續性和崩潰恢復。
+
+
#### 布隆過濾器 {#bloom-filters}
使用 LSM 儲存,讀取很久以前更新的鍵或不存在的鍵可能會很慢,因為儲存引擎需要檢查多個段檔案。為了加快此類讀取,LSM 儲存引擎通常在每個段中包含一個 *布隆過濾器*(*Bloom filter*)[^13],它提供了一種快速但近似的方法來檢查特定鍵是否出現在特定 SSTable 中。
-[圖 4-4](#fig_storage_bloom) 顯示了一個包含兩個鍵和 16 位的布隆過濾器示例(實際上,它會包含更多的鍵和更多的位)。對於 SSTable 中的每個鍵,我們計算一個雜湊函式,產生一組數字,然後將其解釋為位陣列的索引 [^14]。我們將對應於這些索引的位設定為 1,其餘保持為 0。例如,鍵 `handbag` 雜湊為數字 (2, 9, 4),所以我們將第 2、9 和 4 位設定為 1。然後將點陣圖與鍵的稀疏索引一起儲存為 SSTable 的一部分。這需要一點額外的空間,但與 SSTable 的其餘部分相比,布隆過濾器通常很小。
+[圖 4-4](/tw/ch4#fig_storage_bloom) 顯示了一個包含兩個鍵和 16 位的布隆過濾器示例(實際上,它會包含更多的鍵和更多的位)。對於 SSTable 中的每個鍵,我們計算一個雜湊函式,產生一組數字,然後將其解釋為位陣列的索引 [^14]。我們將對應於這些索引的位設定為 1,其餘保持為 0。例如,鍵 `handbag` 雜湊為數字 (2, 9, 4),所以我們將第 2、9 和 4 位設定為 1。然後將點陣圖與鍵的稀疏索引一起儲存為 SSTable 的一部分。這需要一點額外的空間,但與 SSTable 的其餘部分相比,布隆過濾器通常很小。
{{< figure src="/fig/ddia_0404.png" id="fig_storage_bloom" caption="圖 4-4. 布隆過濾器提供了一種快速的機率檢查,用於判斷特定鍵是否存在於特定 SSTable 中。" class="w-full my-4" >}}
-當我們想知道一個鍵是否出現在 SSTable 中時,我們像以前一樣計算該鍵的相同雜湊,並檢查這些索引處的位。例如,在 [圖 4-4](#fig_storage_bloom) 中,我們查詢鍵 `handheld`,它雜湊為 (6, 11, 2)。其中一個位是 1(即第 2 位),而另外兩個是 0。這些檢查可以使用所有 CPU 都支援的位運算非常快速地進行。
+當我們想知道一個鍵是否出現在 SSTable 中時,我們像以前一樣計算該鍵的相同雜湊,並檢查這些索引處的位。例如,在 [圖 4-4](/tw/ch4#fig_storage_bloom) 中,我們查詢鍵 `handheld`,它雜湊為 (6, 11, 2)。其中一個位是 1(即第 2 位),而另外兩個是 0。這些檢查可以使用所有 CPU 都支援的位運算非常快速地進行。
如果至少有一個位是 0,我們知道該鍵肯定不在 SSTable 中。如果查詢中的位都是 1,那麼該鍵很可能在 SSTable 中,但也有可能是巧合,所有這些位都被其他鍵設定為 1。這種看起來鍵存在但實際上不存在的情況稱為 *假陽性*(*false positive*)。
@@ -170,10 +174,12 @@ SSTable 檔案格式在讀取方面比僅追加日誌更好,但它使寫入更
作為經驗法則,如果你主要有寫入而讀取很少,分層壓實表現更好,而如果你的工作負載以讀取為主,分級壓實表現更好。如果你頻繁寫入少量鍵,而很少寫入大量鍵,那麼分級壓實也可能有優勢 [^18]。
-儘管有許多細微之處,但 LSM 樹的基本思想 —— 保持在後臺合併的 SSTable 級聯 —— 簡單而有效。我們將在 ["比較 B 樹與 LSM 樹"](#sec_storage_btree_lsm_comparison) 中更詳細地討論它們的效能特徵。
+儘管有許多細微之處,但 LSM 樹的基本思想 —— 保持在後臺合併的 SSTable 級聯 —— 簡單而有效。我們將在 ["比較 B 樹與 LSM 樹"](/tw/ch4#sec_storage_btree_lsm_comparison) 中更詳細地討論它們的效能特徵。
--------
+
+
> [!TIP] 嵌入式儲存引擎
許多資料庫作為接受網路查詢的服務執行,但也有 *嵌入式* 資料庫不公開網路 API。相反,它們是在與應用程式程式碼相同的程序中執行的庫,通常讀取和寫入本地磁碟上的檔案,你透過正常的函式呼叫與它們互動。嵌入式儲存引擎的例子包括 RocksDB、SQLite、LMDB、DuckDB 和 KùzuDB [^19]。
@@ -194,21 +200,21 @@ B 樹於 1970 年引入 [^21],不到 10 年後就被稱為"無處不在"[^22]
我們之前看到的日誌結構索引將資料庫分解為可變大小的 *段*,通常為幾兆位元組或更大,寫入一次後就不可變。相比之下,B 樹將資料庫分解為固定大小的 *塊* 或 *頁*,並可能就地覆蓋頁。頁傳統上大小為 4 KiB,但 PostgreSQL 現在預設使用 8 KiB,MySQL 預設使用 16 KiB。
-每個頁都可以使用頁號來標識,這允許一個頁引用另一個頁 —— 類似於指標,但在磁碟上而不是在記憶體中。如果所有頁都儲存在同一個檔案中,將頁號乘以頁大小就給我們檔案中頁所在位置的位元組偏移量。我們可以使用這些頁引用來構建頁樹,如 [圖 4-5](#fig_storage_b_tree) 所示。
+每個頁都可以使用頁號來標識,這允許一個頁引用另一個頁 —— 類似於指標,但在磁碟上而不是在記憶體中。如果所有頁都儲存在同一個檔案中,將頁號乘以頁大小就給我們檔案中頁所在位置的位元組偏移量。我們可以使用這些頁引用來構建頁樹,如 [圖 4-5](/tw/ch4#fig_storage_b_tree) 所示。
{{< figure src="/fig/ddia_0405.png" id="fig_storage_b_tree" caption="圖 4-5. 使用 B 樹索引查詢鍵 251。從根頁開始,我們首先跟隨引用到鍵 200–300 的頁,然後是鍵 250–270 的頁。" class="w-full my-4" >}}
一個頁被指定為 B 樹的 *根*;每當你想在索引中查詢一個鍵時,你就從這裡開始。該頁包含幾個鍵和對子頁的引用。每個子頁負責一個連續的鍵範圍,引用之間的鍵指示這些範圍之間的邊界在哪裡。(這種結構有時稱為 B+ 樹,但我們不需要將其與其他 B 樹變體區分開來。)
-在 [圖 4-5](#fig_storage_b_tree) 的例子中,我們正在查詢鍵 251,所以我們知道我們需要跟隨邊界 200 和 300 之間的頁引用。這將我們帶到一個看起來相似的頁,該頁進一步將 200–300 範圍分解為子範圍。最終我們到達包含單個鍵的頁(*葉頁*),該頁要麼內聯包含每個鍵的值,要麼包含對可以找到值的頁的引用。
+在 [圖 4-5](/tw/ch4#fig_storage_b_tree) 的例子中,我們正在查詢鍵 251,所以我們知道我們需要跟隨邊界 200 和 300 之間的頁引用。這將我們帶到一個看起來相似的頁,該頁進一步將 200–300 範圍分解為子範圍。最終我們到達包含單個鍵的頁(*葉頁*),該頁要麼內聯包含每個鍵的值,要麼包含對可以找到值的頁的引用。
-B 樹的一個頁中對子頁的引用數稱為 *分支因子*。例如,在 [圖 4-5](#fig_storage_b_tree) 中,分支因子為六。實際上,分支因子取決於儲存頁引用和範圍邊界所需的空間量,但通常為幾百。
+B 樹的一個頁中對子頁的引用數稱為 *分支因子*。例如,在 [圖 4-5](/tw/ch4#fig_storage_b_tree) 中,分支因子為六。實際上,分支因子取決於儲存頁引用和範圍邊界所需的空間量,但通常為幾百。
如果你想更新 B 樹中現有鍵的值,你搜索包含該鍵的葉頁,並用包含新值的版本覆蓋磁碟上的該頁。如果你想新增一個新鍵,你需要找到其範圍包含新鍵的頁並將其新增到該頁。如果頁中沒有足夠的空閒空間來容納新鍵,則頁被分成兩個半滿的頁,並更新父頁以說明鍵範圍的新細分。
{{< figure src="/fig/ddia_0406.png" id="fig_storage_b_tree_split" caption="圖 4-6. 透過在邊界鍵 337 上分割頁來增長 B 樹。父頁被更新以引用兩個子頁。" class="w-full my-4" >}}
-在 [圖 4-6](#fig_storage_b_tree_split) 的例子中,我們想插入鍵 334,但範圍 333–345 的頁已經滿了。因此,我們將其分成範圍 333–337(包括新鍵)的頁和 337–344 的頁。我們還必須更新父頁以引用兩個子頁,它們之間的邊界值為 337。如果父頁沒有足夠的空間容納新引用,它也可能需要被分割,分割可以一直持續到樹的根。當根被分割時,我們在它上面建立一個新根。刪除鍵(可能需要合併節點)更複雜 [^5]。
+在 [圖 4-6](/tw/ch4#fig_storage_b_tree_split) 的例子中,我們想插入鍵 334,但範圍 333–345 的頁已經滿了。因此,我們將其分成範圍 333–337(包括新鍵)的頁和 337–344 的頁。我們還必須更新父頁以引用兩個子頁,它們之間的邊界值為 337。如果父頁沒有足夠的空間容納新引用,它也可能需要被分割,分割可以一直持續到樹的根。當根被分割時,我們在它上面建立一個新根。刪除鍵(可能需要合併節點)更複雜 [^5]。
這個演算法確保樹保持 *平衡*:具有 *n* 個鍵的 B 樹始終具有 *O*(log *n*) 的深度。大多數資料庫可以適合三或四層深的 B 樹,所以你不需要跟隨許多頁引用來找到你要查詢的頁。(具有 500 分支因子的 4 KiB 頁的四層樹可以儲存多達 250 TB。)
@@ -249,13 +255,13 @@ B 樹的基本底層寫操作是用新資料覆蓋磁碟上的頁。假設覆蓋
使用 B 樹時,如果應用程式寫入的鍵分散在整個鍵空間中,生成的磁碟操作也會隨機分散,因為儲存引擎需要覆蓋的頁可能位於磁碟的任何位置。另一方面,日誌結構儲存引擎一次寫入整個段檔案(無論是寫出記憶體表還是壓實現有段),這比 B 樹中的頁大得多。
-許多小的、分散的寫入模式(如 B 樹中的)稱為 *隨機寫入*,而較少的大寫入模式(如 LSM 樹中的)稱為 *順序寫入*。磁碟通常具有比隨機寫入更高的順序寫入吞吐量,這意味著日誌結構儲存引擎通常可以在相同硬體上處理比 B 樹更高的寫入吞吐量。這種差異在旋轉磁碟硬碟(HDD)上特別大;在今天大多數資料庫使用的固態硬碟(SSD)上,差異較小,但仍然明顯(參見 ["SSD 上的順序與隨機寫入"](#sidebar_sequential))。
+許多小的、分散的寫入模式(如 B 樹中的)稱為 *隨機寫入*,而較少的大寫入模式(如 LSM 樹中的)稱為 *順序寫入*。磁碟通常具有比隨機寫入更高的順序寫入吞吐量,這意味著日誌結構儲存引擎通常可以在相同硬體上處理比 B 樹更高的寫入吞吐量。這種差異在旋轉磁碟硬碟(HDD)上特別大;在今天大多數資料庫使用的固態硬碟(SSD)上,差異較小,但仍然明顯(參見 ["SSD 上的順序與隨機寫入"](/tw/ch4#sidebar_sequential))。
--------
> [!TIP] SSD 上的順序與隨機寫入
-在旋轉磁碟硬碟(HDD)上,順序寫入比隨機寫入快得多:隨機寫入必須機械地將磁頭移動到新位置,並等待碟片的正確部分經過磁頭下方,這需要幾毫秒 —— 在計算時間尺度上是永恆的。然而,SSD(固態硬碟)包括 NVMe(非易失性記憶體快速,即連線到 PCI Express 匯流排的快閃記憶體)現在已經在許多用例中超越了 HDD,它們不受這種機械限制。
+在旋轉磁碟硬碟(HDD)上,順序寫入比隨機寫入快得多:隨機寫入必須機械地將磁頭移動到新位置,並等待碟片的正確部分經過磁頭下方,這需要幾毫秒 —— 在計算時間尺度上是永恆的。然而,SSD(固態硬碟)包括 NVMe(Non-Volatile Memory Express,即連線到 PCI Express 匯流排的快閃記憶體)現在已經在許多場景中超越了 HDD,它們不受這種機械限制。
儘管如此,SSD 對順序寫入的吞吐量也高於隨機寫入。原因是快閃記憶體可以一次讀取或寫入一頁(通常為 4 KiB),但只能一次擦除一個塊(通常為 512 KiB)。塊中的某些頁可能包含有效資料,而其他頁可能包含不再需要的資料。在擦除塊之前,控制器必須首先將包含有效資料的頁移動到其他塊中;這個過程稱為 *垃圾回收*(GC)[^33]。
@@ -283,7 +289,7 @@ B 樹索引必須至少寫入每條資料兩次:一次寫入預寫日誌,一
B 樹可能會隨著時間的推移變得 *碎片化*:例如,如果刪除了大量鍵,資料庫檔案可能包含許多 B 樹不再使用的頁。對 B 樹的後續新增可以使用這些空閒頁,但它們不能輕易地返回給作業系統,因為它們在檔案的中間,所以它們仍然佔用檔案系統上的空間。因此,資料庫需要一個後臺過程來移動頁以更好地放置它們,例如 PostgreSQL 中的真空過程 [^25]。
-碎片化在 LSM 樹中不太成問題,因為壓實過程無論如何都會定期重寫資料檔案,而且 SSTable 沒有未使用空間的頁。此外,SSTable 中的鍵值對塊可以更好地壓縮,因此通常比 B 樹在磁碟上產生更小的檔案。被覆蓋的鍵和值繼續消耗空間,直到它們被壓實刪除,但使用分級壓即時,這種開銷相當低 [^40] [^41]。分層壓實(參見 ["壓實策略"](#sec_storage_lsm_compaction))使用更多的磁碟空間,特別是在壓實期間臨時使用。
+碎片化在 LSM 樹中不太成問題,因為壓實過程無論如何都會定期重寫資料檔案,而且 SSTable 沒有未使用空間的頁。此外,SSTable 中的鍵值對塊可以更好地壓縮,因此通常比 B 樹在磁碟上產生更小的檔案。被覆蓋的鍵和值繼續消耗空間,直到它們被壓實刪除,但使用分級壓即時,這種開銷相當低 [^40] [^41]。分層壓實(參見 ["壓實策略"](/tw/ch4#sec_storage_lsm_compaction))使用更多的磁碟空間,特別是在壓實期間臨時使用。
在磁碟上有一些資料的多個副本也可能是一個問題,當你需要刪除一些資料,並確信它真的已被刪除(也許是為了遵守資料保護法規)。例如,在大多數 LSM 儲存引擎中,已刪除的記錄可能仍然存在於較高級別中,直到代表刪除的墓碑透過所有壓實級別傳播,這可能需要很長時間。專門的儲存引擎設計可以更快地傳播刪除 [^42]。
@@ -306,7 +312,7 @@ B 樹可能會隨著時間的推移變得 *碎片化*:例如,如果刪除了
* 或者,值可以是對實際資料的引用:要麼是相關行的主鍵(InnoDB 對二級索引這樣做),要麼是對磁碟上位置的直接引用。在後一種情況下,儲存行的地方稱為 *堆檔案*,它以無特定順序儲存資料(它可能是僅追加的,或者它可能跟蹤已刪除的行以便稍後用新資料覆蓋它們)。例如,Postgres 使用堆檔案方法 [^44]。
* 兩者之間的折中是 *覆蓋索引* 或 *包含列的索引*,它在索引中儲存表的 *某些* 列,除了在堆上或主鍵聚簇索引中儲存完整行 [^45]。這允許僅使用索引來回答某些查詢,而無需解析主鍵或檢視堆檔案(在這種情況下,索引被稱為 *覆蓋* 查詢)。這可以使某些查詢更快,但資料的重複意味著索引使用更多的磁碟空間並減慢寫入速度。
-到目前為止討論的索引只將單個鍵對映到值。如果你需要同時查詢表的多個列(或文件中的多個欄位),請參見 ["多維索引與全文索引"](#sec_storage_multidimensional)。
+到目前為止討論的索引只將單個鍵對映到值。如果你需要同時查詢表的多個列(或文件中的多個欄位),請參見 ["多維索引與全文索引"](/tw/ch4#sec_storage_multidimensional)。
當更新值而不更改鍵時,堆檔案方法可以允許記錄就地覆蓋,前提是新值不大於舊值。如果新值更大,情況會更複雜,因為它可能需要移動到堆中有足夠空間的新位置。在這種情況下,要麼所有索引都需要更新以指向記錄的新堆位置,要麼在舊堆位置留下轉發指標 [^2]。
@@ -314,7 +320,7 @@ B 樹可能會隨著時間的推移變得 *碎片化*:例如,如果刪除了
本章到目前為止討論的資料結構都是對磁碟限制的回應。與主記憶體相比,磁碟很難處理。對於磁碟和 SSD,如果你想在讀取和寫入上獲得良好的效能,磁碟上的資料需要仔細布局。然而,我們容忍這種尷尬,因為磁碟有兩個顯著的優勢:它們是持久的(如果斷電,其內容不會丟失),並且它們每千兆位元組的成本比 RAM 低。
-隨著 RAM 變得更便宜,每千兆位元組成本的論點被侵蝕。許多資料集根本不是那麼大,因此將它們完全保留在記憶體中是完全可行的,可能分佈在幾臺機器上。這導致了 *記憶體資料庫* 的發展。
+隨著 RAM 變得更便宜,按每 GB 計價的成本優勢正在減弱。許多資料集根本沒有那麼大,因此將它們完全保留在記憶體中是完全可行的,甚至可以分佈在幾臺機器上。這導致了 *記憶體資料庫* 的發展。
一些記憶體鍵值儲存,例如 Memcached,僅用於快取,如果機器重新啟動,資料丟失是可以接受的。但其他記憶體資料庫旨在實現永續性,這可以透過特殊硬體(例如電池供電的 RAM)、將更改日誌寫入磁碟、將定期快照寫入磁碟或將記憶體狀態複製到其他機器來實現。
@@ -331,7 +337,7 @@ Redis 和 Couchbase 透過非同步寫入磁碟提供弱永續性。
## 分析型資料儲存 {#sec_storage_analytics}
-資料倉庫的資料模型最常見的是關係型,因為 SQL 通常非常適合分析查詢。有許多圖形資料分析工具可以生成 SQL 查詢、視覺化結果,並允許分析師探索資料(透過 *下鑽* 和 *切片切塊* 等操作)。
+資料倉庫的資料模型最常見的是關係型,因為 SQL 通常非常適合分析查詢。有許多圖形化資料分析工具可以生成 SQL 查詢、視覺化結果,並允許分析師探索資料(透過 *下鑽* 和 *切片切塊* 等操作)。
表面上,資料倉庫和關係型 OLTP 資料庫看起來很相似,因為它們都有 SQL 查詢介面。然而,系統的內部可能看起來完全不同,因為它們針對非常不同的查詢模式進行了最佳化。許多資料庫供應商現在專注於支援事務處理或分析工作負載,但不是兩者兼而有之。
@@ -343,16 +349,16 @@ Teradata、Vertica 和 SAP HANA 等資料倉庫供應商既銷售商業許可下
雲資料倉庫往往與其他雲服務更好地整合,並且更具彈性。例如,許多雲倉庫支援自動日誌攝取,並提供與資料處理框架(如 Google Cloud 的 Dataflow 或 Amazon Web Services 的 Kinesis)的輕鬆整合。這些倉庫也更具彈性,因為它們將查詢計算與儲存層解耦 [^54]。資料持久儲存在物件儲存而不是本地磁碟上,這使得可以獨立調整儲存容量和查詢的計算資源,正如我們之前在 ["雲原生系統架構"](/tw/ch1#sec_introduction_cloud_native) 中看到的。
-Apache Hive、Trino 和 Apache Spark 等開源資料倉庫也隨著雲的發展而發展。隨著分析資料儲存轉移到物件儲存上的資料湖,開源倉庫已經開始分解 [^55]。以下元件以前整合在單個系統(如 Apache Hive)中,現在通常作為單獨的元件實現:
+Apache Hive、Trino 和 Apache Spark 等開源資料倉庫也隨著雲的發展而發展。隨著分析資料儲存轉移到物件儲存上的資料湖,開源倉庫也開始解耦拆分 [^55]。以下元件以前整合在單個系統(如 Apache Hive)中,現在通常作為單獨的元件實現:
查詢引擎
-: Trino、Apache DataFusion 和 Presto 等查詢引擎解析 SQL 查詢,將其最佳化為執行計劃,並針對資料執行它們。執行通常需要並行、分散式資料處理任務。一些查詢引擎提供內建任務執行,而其他選擇使用第三方執行框架,如 Apache Spark 或 Apache Flink。
+: Trino、Apache DataFusion 和 Presto 等查詢引擎解析 SQL 查詢,將其最佳化為執行計劃,並在資料上執行這些計劃。執行通常需要並行、分散式的資料處理任務。一些查詢引擎提供內建任務執行,而有些則選擇使用第三方執行框架,如 Apache Spark 或 Apache Flink。
儲存格式
: 儲存格式確定表的行如何編碼為檔案中的位元組,然後通常儲存在物件儲存或分散式檔案系統中 [^12]。然後查詢引擎可以訪問這些資料,但使用資料湖的其他應用程式也可以訪問。此類儲存格式的示例包括 Parquet、ORC、Lance 或 Nimble,我們將在下一節中看到更多關於它們的內容。
表格式
-: 以 Apache Parquet 和類似儲存格式編寫的檔案一旦編寫通常是不可變的。為了支援行插入和刪除,使用 Apache Iceberg 或 Databricks 的 Delta 格式等表格式。表格式指定定義哪些檔案構成表以及表模式的檔案格式。此類格式還提供高階功能,例如時間旅行(查詢表在以前時間點的能力)、垃圾回收,甚至事務。
+: 以 Apache Parquet 和類似儲存格式編寫的檔案一旦寫入通常就是不可變的。為了支援行插入和刪除,通常會使用 Apache Iceberg 或 Databricks Delta 等表格式。表格式規定了哪些檔案構成一張表,以及表模式的定義格式。此類格式還提供高階功能,例如時間旅行(查詢表在過去某個時間點狀態的能力)、垃圾回收,甚至事務。
資料目錄
: 就像表格式定義哪些檔案構成表一樣,資料目錄定義哪些表組成資料庫。目錄用於建立、重新命名和刪除表。與儲存和表格式不同,Snowflake 的 Polaris 和 Databricks 的 Unity Catalog 等資料目錄通常作為可以使用 REST 介面查詢的獨立服務執行。Apache Iceberg 也提供目錄,可以在客戶端內執行或作為單獨的程序執行。查詢引擎在讀取和寫入表時使用目錄資訊。傳統上,目錄和查詢引擎已經整合,但將它們解耦使資料發現和資料治理系統(在 ["資料系統、法律和社會"](/tw/ch1#sec_introduction_compliance) 中討論)也能夠訪問目錄的元資料。
@@ -361,7 +367,7 @@ Apache Hive、Trino 和 Apache Spark 等開源資料倉庫也隨著雲的發展
如 ["星型和雪花型:分析模式"](/tw/ch3#sec_datamodels_analytics) 中所討論的,資料倉庫按照慣例通常使用帶有大型事實表的關係模式,該表包含對維度表的外部索引鍵引用。如果你的事實表中有數萬億行和數 PB 的資料,有效地儲存和查詢它們就成為一個具有挑戰性的問題。維度表通常要小得多(數百萬行),因此在本節中我們將重點關注事實的儲存。
-儘管事實表通常有超過 100 列,但典型的資料倉庫查詢一次只訪問其中的 4 或 5 列(分析很少需要 `"SELECT *"` 查詢)[^52]。以 [示例 4-1](#fig_storage_analytics_query) 中的查詢為例:它訪問大量行(2024 日曆年期間每次有人購買水果或糖果的情況),但它只需要訪問 `fact_sales` 表的三列:`date_key`、`product_sk` 和 `quantity`。查詢忽略所有其他列。
+儘管事實表通常有超過 100 列,但典型的資料倉庫查詢一次只訪問其中的 4 或 5 列(分析很少需要 `"SELECT *"` 查詢)[^52]。以 [示例 4-1](/tw/ch4#fig_storage_analytics_query) 中的查詢為例:它訪問大量行(2024 日曆年期間每次有人購買水果或糖果的情況),但它只需要訪問 `fact_sales` 表的三列:`date_key`、`product_sk` 和 `quantity`。查詢忽略所有其他列。
{{< figure id="fig_storage_analytics_query" title="示例 4-1. 分析人們是否更傾向於購買新鮮水果或糖果,取決於星期幾" class="w-full my-4" >}}
@@ -381,11 +387,11 @@ GROUP BY
我們如何高效地執行這個查詢?
-在大多數 OLTP 資料庫中,儲存是以 *面向行* 的方式佈局的:表中一行的所有值彼此相鄰儲存。文件資料庫類似:整個文件通常作為一個連續的位元組序列儲存。你可以在 [圖 4-1](#fig_storage_csv_hash_index) 的 CSV 示例中看到這一點。
+在大多數 OLTP 資料庫中,儲存是以 *面向行* 的方式佈局的:表中一行的所有值彼此相鄰儲存。文件資料庫類似:整個文件通常作為一個連續的位元組序列儲存。你可以在 [圖 4-1](/tw/ch4#fig_storage_csv_hash_index) 的 CSV 示例中看到這一點。
-為了處理像 [示例 4-1](#fig_storage_analytics_query) 這樣的查詢,你可能在 `fact_sales.date_key` 和/或 `fact_sales.product_sk` 上有索引,告訴儲存引擎在哪裡找到特定日期或特定產品的所有銷售。但是,面向行的儲存引擎仍然需要將所有這些行(每行包含超過 100 個屬性)從磁碟載入到記憶體中,解析它們,並過濾掉不符合所需條件的行。這可能需要很長時間。
+為了處理像 [示例 4-1](/tw/ch4#fig_storage_analytics_query) 這樣的查詢,你可能在 `fact_sales.date_key` 和/或 `fact_sales.product_sk` 上有索引,告訴儲存引擎在哪裡找到特定日期或特定產品的所有銷售。但是,面向行的儲存引擎仍然需要將所有這些行(每行包含超過 100 個屬性)從磁碟載入到記憶體中,解析它們,並過濾掉不符合所需條件的行。這可能需要很長時間。
-*面向列*(或 *列式*)儲存背後的想法很簡單:不要將一行中的所有值儲存在一起,而是將每 *列* 中的所有值儲存在一起 [^56]。如果每列單獨儲存,查詢只需要讀取和解析該查詢中使用的那些列,這可以節省大量工作。[圖 4-7](#fig_column_store) 使用 [圖 3-5](/tw/ch3#fig_dwh_schema) 中事實表的擴充套件版本展示了這一原理。
+*面向列*(或 *列式*)儲存背後的想法很簡單:不要將一行中的所有值儲存在一起,而是將每 *列* 中的所有值儲存在一起 [^56]。如果每列單獨儲存,查詢只需要讀取和解析該查詢中使用的那些列,這可以節省大量工作。[圖 4-7](/tw/ch4#fig_column_store) 使用 [圖 3-5](/tw/ch3#fig_dwh_schema) 中事實表的擴充套件版本展示了這一原理。
--------
@@ -406,13 +412,13 @@ GROUP BY
除了只從磁碟載入查詢所需的那些列之外,我們還可以透過壓縮資料進一步減少對磁碟吞吐量和網路頻寬的需求。幸運的是,面向列的儲存通常非常適合壓縮。
-看看 [圖 4-7](#fig_column_store) 中每列的值序列:它們看起來經常重複,這是壓縮的良好跡象。根據列中的資料,可以使用不同的壓縮技術。在資料倉庫中特別有效的一種技術是 *點陣圖編碼*,如 [圖 4-8](#fig_bitmap_index) 所示。
+看看 [圖 4-7](/tw/ch4#fig_column_store) 中每列的值序列:它們看起來經常重複,這是壓縮的良好跡象。根據列中的資料,可以使用不同的壓縮技術。在資料倉庫中特別有效的一種技術是 *點陣圖編碼*,如 [圖 4-8](/tw/ch4#fig_bitmap_index) 所示。
{{< figure src="/fig/ddia_0408.png" id="fig_bitmap_index" caption="圖 4-8. 單列的壓縮、點陣圖索引儲存。" class="w-full my-4" >}}
通常,列中不同值的數量與行數相比很小(例如,零售商可能有數十億條銷售交易,但只有 100,000 種不同的產品)。我們現在可以將具有 *n* 個不同值的列轉換為 *n* 個單獨的點陣圖:每個不同值一個位圖,每行一位。如果該行具有該值,則該位為 1,否則為 0。
-一種選擇是使用每行一位來儲存這些點陣圖。然而,這些點陣圖通常包含大量零(我們說它們是 *稀疏* 的)。在這種情況下,點陣圖可以另外進行遊程編碼:計算連續零或一的數量並存儲該數字,如 [圖 4-8](#fig_bitmap_index) 底部所示。諸如 *咆哮點陣圖*(*roaring bitmaps*)之類的技術在兩種位圖表示之間切換,使用最緊湊的表示 [^73]。這可以使列的編碼非常高效。
+一種選擇是使用每行一位來儲存這些點陣圖。然而,這些點陣圖通常包含大量零(我們說它們是 *稀疏* 的)。在這種情況下,點陣圖可以另外進行遊程編碼:計算連續零或一的數量並存儲該數字,如 [圖 4-8](/tw/ch4#fig_bitmap_index) 底部所示。諸如 *咆哮點陣圖*(*roaring bitmaps*)之類的技術在兩種位圖表示之間切換,使用最緊湊的表示 [^73]。這可以使列的編碼非常高效。
像這樣的點陣圖索引非常適合資料倉庫中常見的查詢型別。例如:
@@ -439,9 +445,9 @@ GROUP BY
相反,資料需要一次排序整行,即使它是按列儲存的。資料庫管理員可以使用他們對常見查詢的瞭解來選擇表應按哪些列排序。例如,如果查詢經常針對日期範圍(例如上個月),則將 `date_key` 作為第一個排序鍵可能是有意義的。然後查詢可以只掃描上個月的行,這將比掃描所有行快得多。
-第二列可以確定在第一列中具有相同值的任何行的排序順序。例如,如果 `date_key` 是 [圖 4-7](#fig_column_store) 中的第一個排序鍵,那麼 `product_sk` 作為第二個排序鍵可能是有意義的,這樣同一天同一產品的所有銷售都在儲存中分組在一起。這將有助於需要在某個日期範圍內按產品分組或過濾銷售的查詢。
+第二列可以確定在第一列中具有相同值的任何行的排序順序。例如,如果 `date_key` 是 [圖 4-7](/tw/ch4#fig_column_store) 中的第一個排序鍵,那麼 `product_sk` 作為第二個排序鍵可能是有意義的,這樣同一天同一產品的所有銷售都在儲存中分組在一起。這將有助於需要在某個日期範圍內按產品分組或過濾銷售的查詢。
-排序順序的另一個優點是它可以幫助壓縮列。如果主排序列沒有許多不同的值,那麼排序後,它將有很長的序列,其中相同的值在一行中重複多次。簡單的遊程編碼,就像我們在 [圖 4-8](#fig_bitmap_index) 中用於點陣圖的那樣,可以將該列壓縮到幾千位元組 —— 即使表有數十億行。
+排序順序的另一個優點是它可以幫助壓縮列。如果主排序列沒有許多不同的值,那麼排序後,它將有很長的序列,其中相同的值在一行中重複多次。簡單的遊程編碼,就像我們在 [圖 4-8](/tw/ch4#fig_bitmap_index) 中用於點陣圖的那樣,可以將該列壓縮到幾千位元組 —— 即使表有數十億行。
該壓縮效果在第一個排序鍵上最強。第二和第三個排序鍵將更加混亂,因此不會有如此長的重複值執行。排序優先順序較低的列基本上以隨機順序出現,因此它們可能不會壓縮得那麼好。但是,讓前幾列排序仍然是整體上的勝利。
@@ -460,7 +466,7 @@ GROUP BY
用於分析的複雜 SQL 查詢被分解為由多個階段組成的 *查詢計劃*,稱為 *運算元*,這些運算元可能分佈在多臺機器上以並行執行。查詢規劃器可以透過選擇使用哪些運算元、以何種順序執行它們以及在哪裡執行每個運算元來執行大量最佳化。
-在每個運算元內,查詢引擎需要對列中的值執行各種操作,例如查詢值在特定值集中的所有行(可能作為連線的一部分),或檢查值是否大於 15。它還需要檢視同一行的幾列,例如查詢產品是香蕉且商店是特定感興趣商店的所有銷售交易。
+在每個運算元內,查詢引擎需要對列中的值執行各種操作,例如查詢值在特定值集中的所有行(可能作為連線的一部分),或檢查值是否大於 15。它還需要檢視同一行的幾列,例如查詢產品是香蕉且門店是某個特定目標門店的所有銷售交易。
對於需要掃描數百萬行的資料倉庫查詢,我們不僅需要擔心它們需要從磁碟讀取的資料量,還需要擔心執行複雜運算元所需的 CPU 時間。最簡單的運算元型別就像程式語言的直譯器:在遍歷每一行時,它檢查表示查詢的資料結構,以找出需要對哪些列執行哪些比較或計算。不幸的是,這對許多分析目的來說太慢了。高效查詢執行的兩種替代方法已經出現 [^77]:
@@ -470,7 +476,7 @@ GROUP BY
向量化處理
: 查詢被解釋,而不是編譯,但透過批次處理列中的許多值而不是逐行迭代來提高速度。一組固定的預定義運算元內建在資料庫中;我們可以向它們傳遞引數並獲得一批結果 [^50] [^75]。
-例如,我們可以將 `product_sk` 列和"香蕉"的 ID 傳遞給相等運算元,並獲得一個位圖(輸入列中每個值一位,如果是香蕉則為 1);然後我們可以將 `store_sk` 列和感興趣商店的 ID 傳遞給相同的相等運算元,並獲得另一個位圖;然後我們可以將兩個點陣圖傳遞給"按位 AND"運算元,如 [圖 4-9](#fig_bitmap_and) 所示。結果將是一個位圖,包含特定商店中所有香蕉銷售的 1。
+例如,我們可以將 `product_sk` 列和"香蕉"的 ID 傳遞給相等運算元,並獲得一個位圖(輸入列中每個值一位,如果是香蕉則為 1);然後我們可以將 `store_sk` 列和感興趣商店的 ID 傳遞給相同的相等運算元,並獲得另一個位圖;然後我們可以將兩個點陣圖傳遞給"按位 AND"運算元,如 [圖 4-9](/tw/ch4#fig_bitmap_and) 所示。結果將是一個位圖,包含特定商店中所有香蕉銷售的 1。
{{< figure src="/fig/ddia_0409.png" id="fig_bitmap_and" caption="圖 4-9. 兩個點陣圖之間的按位 AND 適合向量化。" class="w-full my-4" >}}
@@ -481,23 +487,23 @@ GROUP BY
* 利用並行性,例如多執行緒和單指令多資料(SIMD)指令 [^79] [^80],以及
* 直接對壓縮資料進行操作,而無需將其解碼為單獨的記憶體表示,這可以節省記憶體分配和複製成本。
-### 物化檢視與多維資料集 {#sec_storage_materialized_views}
+### 物化檢視與資料立方體 {#sec_storage_materialized_views}
我們之前在 ["物化和更新時間線"](/tw/ch2#sec_introduction_materializing) 中遇到了 *物化檢視*:在關係資料模型中,它們是表狀物件,其內容是某些查詢的結果。區別在於物化檢視是查詢結果的實際副本,寫入磁碟,而虛擬檢視只是編寫查詢的快捷方式。當你從虛擬檢視讀取時,SQL 引擎會即時將其擴充套件為檢視的基礎查詢,然後處理擴充套件的查詢。
當基礎資料更改時,物化檢視需要相應更新。一些資料庫可以自動執行此操作,還有像 Materialize 這樣專門從事物化檢視維護的系統 [^81]。執行此類更新意味著寫入時需要更多工作,但物化檢視可以改善在重複需要執行相同查詢的工作負載中的讀取效能。
-*物化聚合* 是一種可以在資料倉庫中有用的物化檢視型別。如前所述,資料倉庫查詢通常涉及聚合函式,例如 SQL 中的 `COUNT`、`SUM`、`AVG`、`MIN` 或 `MAX`。如果許多不同的查詢使用相同的聚合,每次都處理原始資料可能會很浪費。為什麼不快取查詢最常使用的一些計數或總和?*多維資料集* 或 *OLAP 立方體* 透過建立按不同維度分組的聚合網格來做到這一點 [^82]。[圖 4-10](#fig_data_cube) 顯示了一個示例。
+*物化聚合* 是一種可以在資料倉庫中有用的物化檢視型別。如前所述,資料倉庫查詢通常涉及聚合函式,例如 SQL 中的 `COUNT`、`SUM`、`AVG`、`MIN` 或 `MAX`。如果許多不同的查詢使用相同的聚合,每次都處理原始資料可能會很浪費。為什麼不快取查詢最常使用的一些計數或總和?*資料立方體*(*OLAP 立方體*)透過建立按不同維度分組的聚合網格來做到這一點 [^82]。[圖 4-10](/tw/ch4#fig_data_cube) 顯示了一個示例。
-{{< figure src="/fig/ddia_0410.png" id="fig_data_cube" caption="圖 4-10. 多維資料集的兩個維度,透過求和聚合資料。" class="w-full my-4" >}}
+{{< figure src="/fig/ddia_0410.png" id="fig_data_cube" caption="圖 4-10. 資料立方體的兩個維度,透過求和聚合資料。" class="w-full my-4" >}}
-現在假設每個事實只有兩個維度表的外部索引鍵 —— 在 [圖 4-10](#fig_data_cube) 中,這些是 `date_key` 和 `product_sk`。你現在可以繪製一個二維表,日期沿著一個軸,產品沿著另一個軸。每個單元格包含具有該日期-產品組合的所有事實的屬性(例如 `net_price`)的聚合(例如 `SUM`)。然後,你可以沿著每行或列應用相同的聚合,並獲得已減少一個維度的摘要(不管日期的產品銷售,或不管產品的日期銷售)。
+現在假設每個事實只有兩個維度表的外部索引鍵 —— 在 [圖 4-10](/tw/ch4#fig_data_cube) 中,這些是 `date_key` 和 `product_sk`。你現在可以繪製一個二維表,日期沿著一個軸,產品沿著另一個軸。每個單元格包含具有該日期-產品組合的所有事實的屬性(例如 `net_price`)的聚合(例如 `SUM`)。然後,你可以沿著每行或列應用相同的聚合,並獲得已減少一個維度的摘要(不管日期的產品銷售,或不管產品的日期銷售)。
一般來說,事實通常有兩個以上的維度。在 [圖 3-5](/tw/ch3#fig_dwh_schema) 中有五個維度:日期、產品、商店、促銷和客戶。很難想象五維超立方體會是什麼樣子,但原理保持不變:每個單元格包含特定日期-產品-商店-促銷-客戶組合的銷售。然後可以沿著每個維度重複彙總這些值。
-物化多維資料集的優點是某些查詢變得非常快,因為它們已經有效地預先計算了。例如,如果你想知道昨天每個商店的總銷售額,你只需要檢視適當維度的總計 —— 不需要掃描數百萬行。
+物化資料立方體的優點是某些查詢會變得非常快,因為結果已經被預先計算好了。例如,如果你想知道昨天每個商店的總銷售額,你只需要檢視相應維度上的彙總值 —— 不需要掃描數百萬行。
-缺點是多維資料集沒有與查詢原始資料相同的靈活性。例如,沒有辦法計算成本超過 100 美元的商品的銷售比例,因為價格不是維度之一。因此,大多數資料倉庫儘可能多地保留原始資料,並僅將聚合(如多維資料集)用作某些查詢的效能提升。
+缺點是資料立方體不像直接查詢原始資料那樣靈活。例如,沒有辦法計算售價超過 100 美元的商品銷售佔比,因為價格並不是其中一個維度。因此,大多數資料倉庫都會盡可能保留原始資料,只把這類聚合(如資料立方體)當作特定查詢的效能加速手段。
## 多維索引與全文索引 {#sec_storage_multidimensional}
@@ -523,29 +529,29 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
全文檢索允許你透過可能出現在文字中任何位置的關鍵字搜尋文字文件集合(網頁、產品描述等)[^88]。資訊檢索是一個大的專業主題,通常涉及特定於語言的處理:例如,幾種亞洲語言在單詞之間沒有空格或標點符號,因此將文字分割成單詞需要一個指示哪些字元序列構成單詞的模型。全文檢索還經常涉及匹配相似但不相同的單詞(例如拼寫錯誤或單詞的不同語法形式)和同義詞。這些問題超出了本書的範圍。
-然而,在其核心,你可以將全文檢索視為另一種多維查詢:在這種情況下,可能出現在文字中的每個單詞(*詞項*)是一個維度。包含詞項 *x* 的文件在維度 *x* 中的值為 1,不包含 *x* 的文件的值為 0。搜尋提到"紅蘋果"的文件意味著查詢在 *紅* 維度中查詢 1,同時在 *蘋果* 維度中查詢 1。維度數量可能因此非常大。
+然而,在其核心,你可以將全文檢索視為另一種多維查詢:在這種情況下,可能出現在文字中的每個單詞(*詞項*)是一個維度。包含詞項 *x* 的文件在維度 *x* 中的值為 1,不包含 *x* 的文件的值為 0。搜尋提到“紅蘋果”的文件意味著查詢在 *紅* 維度中查詢 1,同時在 *蘋果* 維度中查詢 1。維度數量可能因此非常大。
-許多搜尋引擎用來回答此類查詢的資料結構稱為 *倒排索引*。這是一個鍵值結構,其中鍵是詞項,值是包含該詞項的所有文件的 ID 列表(*倒排列表*)。如果文件 ID 是順序數字,倒排列表也可以表示為稀疏點陣圖,如 [圖 4-8](#fig_bitmap_index):詞項 *x* 的點陣圖中的第 *n* 位是 1,如果 ID 為 *n* 的文件包含詞項 *x* [^89]。
+許多搜尋引擎用來回答此類查詢的資料結構稱為 *倒排索引*。這是一個鍵值結構,其中鍵是詞項,值是包含該詞項的所有文件的 ID 列表(*倒排列表*)。如果文件 ID 是順序數字,倒排列表也可以表示為稀疏點陣圖,如 [圖 4-8](/tw/ch4#fig_bitmap_index):詞項 *x* 的點陣圖中的第 *n* 位是 1,如果 ID 為 *n* 的文件包含詞項 *x* [^89]。
-查詢包含詞項 *x* 和 *y* 的所有文件現在類似於搜尋匹配兩個條件的行的向量化資料倉庫查詢([圖 4-9](#fig_bitmap_and)):載入詞項 *x* 和 *y* 的兩個點陣圖並計算它們的按位 AND。即使點陣圖是遊程編碼的,這也可以非常高效地完成。
+查詢包含詞項 *x* 和 *y* 的所有文件現在類似於搜尋匹配兩個條件的行的向量化資料倉庫查詢([圖 4-9](/tw/ch4#fig_bitmap_and)):載入詞項 *x* 和 *y* 的兩個點陣圖並計算它們的按位 AND。即使點陣圖是遊程編碼的,這也可以非常高效地完成。
例如,Elasticsearch 和 Solr 使用的全文索引引擎 Lucene 就是這樣工作的 [^90]。它將詞項到倒排列表的對映儲存在類似 SSTable 的排序檔案中,這些檔案使用我們在本章前面看到的相同日誌結構方法在後臺合併 [^91]。PostgreSQL 的 GIN 索引型別也使用倒排列表來支援全文檢索和 JSON 文件內的索引 [^92] [^93]。
-除了將文字分解為單詞,另一種選擇是查詢長度為 *n* 的所有子字串,稱為 *n* 元語法。例如,字串 `"hello"` 的三元語法(*n* = 3)是 `"hel"`、`"ell"` 和 `"llo"`。如果我們為所有三元語法構建倒排索引,我們可以搜尋至少三個字元長的任意子字串的文件。三元語法索引甚至允許在搜尋查詢中使用正則表示式;缺點是它們相當大 [^94]。
+除了將文字分解為單詞,另一種選擇是查詢長度為 *n* 的所有子字串,稱為 *n-gram*(*n 元語法*)。例如,字串 `"hello"` 的三元語法(*n* = 3)是 `"hel"`、`"ell"` 和 `"llo"`。如果我們為所有三元語法構建倒排索引,我們就可以搜尋任意至少三個字元長的子字串。三元語法索引甚至允許在搜尋查詢中使用正則表示式;缺點是它們相當大 [^94]。
為了處理文件或查詢中的拼寫錯誤,Lucene 能夠在一定編輯距離內搜尋文字中的單詞(編輯距離為 1 意味著已新增、刪除或替換了一個字母)[^95]。它透過將詞項集儲存為字元上的有限狀態自動機(類似於 *字典樹* [^96])並將其轉換為 *萊文斯坦自動機* 來實現,該自動機支援在給定編輯距離內高效搜尋單詞 [^97]。
### 向量嵌入 {#id92}
-語義搜尋超越了同義詞和拼寫錯誤,試圖理解文件概念和使用者意圖。例如,如果你的幫助頁面包含標題為"取消訂閱"的頁面,使用者在搜尋"如何關閉我的帳戶"或"終止合同"時仍應能夠找到該頁面,即使它們使用完全不同的單詞,但在含義上很接近。
+語義搜尋超越了同義詞和拼寫錯誤,試圖理解文件概念和使用者意圖。例如,如果你的幫助頁面中有一個標題為“取消訂閱”的頁面,使用者在搜尋“如何關閉我的賬戶”或“終止合同”時,仍應能找到這個頁面,即使查詢詞完全不同,但語義非常接近。
為了理解文件的語義 —— 它的含義 —— 語義搜尋索引使用嵌入模型將文件轉換為浮點值向量,稱為 *向量嵌入*。向量表示多維空間中的一個點,每個浮點值表示文件沿著一個維度軸的位置。嵌入模型生成的向量嵌入在(這個多維空間中)彼此接近,當嵌入的輸入文件在語義上相似時。
--------
> [!NOTE]
-> 我們在 ["查詢執行:編譯與向量化"](#sec_storage_vectorized) 中看到了術語 *向量化處理*。語義搜尋中的向量有不同的含義。在向量化處理中,向量指的是可以用特別最佳化的程式碼處理的一批位。在嵌入模型中,向量是表示多維空間中位置的浮點數列表。
+> 我們在 ["查詢執行:編譯與向量化"](/tw/ch4#sec_storage_vectorized) 中看到了術語 *向量化處理*。語義搜尋中的向量有不同的含義。在向量化處理中,向量指的是可以用特別最佳化的程式碼處理的一批位。在嵌入模型中,向量是表示多維空間中位置的浮點數列表。
--------
@@ -566,7 +572,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
: 向量空間被聚類為向量的分割槽(稱為 *質心*),以減少必須比較的向量數量。IVF 索引比平面索引更快,但只能給出近似結果:即使查詢和文件彼此接近,它們也可能落入不同的分割槽。對 IVF 索引的查詢首先定義 *探針*,這只是要檢查的分割槽數。使用更多探針的查詢將更準確,但會更慢,因為必須比較更多向量。
分層可導航小世界(HNSW)
-: HNSW 索引維護向量空間的多個層,如 [圖 4-11](#fig_vector_hnsw) 所示。每一層都表示為一個圖,其中節點表示向量,邊表示與附近向量的接近度。查詢首先在最頂層定位最近的向量,該層具有少量節點。然後查詢移動到下面一層的同一節點,並跟隨該層中的邊,該層連線更密集,尋找更接近查詢向量的向量。該過程繼續直到到達最後一層。與 IVF 索引一樣,HNSW 索引是近似的。
+: HNSW 索引維護向量空間的多個層,如 [圖 4-11](/tw/ch4#fig_vector_hnsw) 所示。每一層都表示為一個圖,其中節點表示向量,邊表示與附近向量的接近度。查詢首先在最頂層定位最近的向量,該層具有少量節點。然後查詢移動到下面一層的同一節點,並跟隨該層中的邊,該層連線更密集,尋找更接近查詢向量的向量。該過程繼續直到到達最後一層。與 IVF 索引一樣,HNSW 索引是近似的。
{{< figure src="/fig/ddia_0411.png" id="fig_vector_hnsw" caption="圖 4-11. 在 HNSW 索引中搜索最接近給定查詢向量的資料庫條目。" class="w-full my-4" >}}
@@ -589,9 +595,9 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
然後我們查看了可以同時搜尋多個條件的索引:多維索引(如 R 樹)可以同時按緯度和經度搜索地圖上的點,全文檢索索引可以搜尋出現在同一文字中的多個關鍵字。最後,向量資料庫用於文字文件和其他媒體的語義搜尋;它們使用具有大量維度的向量,並透過比較向量相似性來查詢相似文件。
-作為應用程式開發人員,如果你掌握了有關儲存引擎內部的這些知識,你就能更好地知道哪種工具最適合你的特定應用程式。如果你需要調整資料庫的調優引數,這種理解使你能夠想象更高或更低的值可能產生什麼影響。
+作為應用開發者,如果你掌握了這些關於儲存引擎內部機制的知識,就能更好地判斷哪種工具最適合你的具體應用。如果你需要調整資料庫的調優引數,這種理解也能幫助你預判引數調高或調低可能帶來的影響。
-儘管本章不能讓你成為調優任何特定儲存引擎的專家,但它希望為你提供了足夠的詞彙和想法,使你能夠理解你選擇的資料庫的文件。
+儘管本章不能讓你成為調優某個特定儲存引擎的專家,但它希望已經為你提供了足夠的術語和思路,使你能夠讀懂所選資料庫的文件。
diff --git a/content/tw/ch5.md b/content/tw/ch5.md
index a4eae50..0d6e1fe 100644
--- a/content/tw/ch5.md
+++ b/content/tw/ch5.md
@@ -5,6 +5,8 @@ math: true
breadcrumbs: false
---
+
+

> *萬物流轉,無物常駐。*
@@ -65,7 +67,7 @@ breadcrumbs: false
這些編碼庫非常方便,因為它們允許用最少的額外程式碼儲存和恢復記憶體物件。然而,它們也有許多深層次的問題:
-* 編碼通常與特定的程式語言繫結,用另一種語言讀取資料非常困難。如果你以這種編碼儲存或傳輸資料,你就將自己承諾於當前的程式語言,可能很長時間,並且排除了與其他組織(可能使用不同語言)的系統整合。
+* 編碼通常與特定程式語言繫結,在另一種語言中讀取會非常困難。如果你以這種編碼儲存或傳輸資料,就等於在相當長時間內把自己繫結在當前程式語言上,也排除了與其他組織(可能使用不同語言)的系統整合。
* 為了以相同的物件型別恢復資料,解碼過程需要能夠例項化任意類。這經常是安全問題的來源 [^1]:如果攻擊者可以讓你的應用程式解碼任意位元組序列,他們可以例項化任意類,這反過來通常允許他們做可怕的事情,例如遠端執行任意程式碼 [^2] [^3]。
* 在這些庫中,資料版本控制通常是事後考慮的:由於它們旨在快速輕鬆地編碼資料,因此它們經常忽略向前和向後相容性的不便問題 [^4]。
* 效率(編碼或解碼所需的 CPU 時間以及編碼結構的大小)通常也是事後考慮的。例如,Java 的內建序列化因其糟糕的效能和臃腫的編碼而臭名昭著 [^5]。
@@ -262,7 +264,7 @@ record Person {
答案取決於 Avro 的使用環境。舉幾個例子:
包含大量記錄的大檔案
-: Avro 的一個常見用途是儲存包含數百萬條記錄的大檔案,所有記錄都使用相同的模式編碼。(我們將在 [Link to Come] 中討論這種情況。)在這種情況下,該檔案的寫入者可以在檔案開頭只包含一次寫入者模式。Avro 指定了一種檔案格式(物件容器檔案)來執行此操作。
+: Avro 的一個常見用途是儲存包含數百萬條記錄的大檔案,所有記錄都使用相同的模式編碼。(我們將在 [第 11 章](/tw/ch11#ch_batch) 討論這種情況。)在這種情況下,該檔案的寫入者可以在檔案開頭只包含一次寫入者模式。Avro 指定了一種檔案格式(物件容器檔案)來執行此操作。
具有單獨寫入記錄的資料庫
: 在資料庫中,不同的記錄可能在不同的時間點使用不同的寫入者模式編寫——你不能假定所有記錄都具有相同的模式。最簡單的解決方案是在每個編碼記錄的開頭包含一個版本號,並在資料庫中保留模式版本列表。讀取者可以獲取記錄,提取版本號,然後從資料庫中獲取該版本號的寫入者模式。使用該寫入者模式,它可以解碼記錄的其餘部分。
@@ -286,7 +288,7 @@ record Person {
### 模式的優點 {#sec_encoding_schemas}
-正如我們所見,Protocol Buffers 和 Avro 都使用模式來描述二進位制編碼格式。它們的模式語言比 XML 模式或 JSON 模式簡單得多,後者支援更詳細的驗證規則(例如,"此欄位的字串值必須與此正則表示式匹配"或"此欄位的整數值必須在 0 到 100 之間")。由於 Protocol Buffers 和 Avro 更簡單實現和使用,它們已經發展到支援相當廣泛的程式語言。
+正如我們所見,Protocol Buffers 和 Avro 都使用模式來描述二進位制編碼格式。它們的模式語言比 XML 模式或 JSON 模式簡單得多,後者支援更詳細的驗證規則(例如,"此欄位的字串值必須與此正則表示式匹配"或"此欄位的整數值必須在 0 到 100 之間")。由於 Protocol Buffers 和 Avro 在實現和使用上都更簡單,它們已經發展到支援相當廣泛的程式語言。
這些編碼所基於的想法絕不是新的。例如,它們與 ASN.1 有很多共同之處,ASN.1 是 1984 年首次標準化的模式定義語言 [^23] [^24]。它用於定義各種網路協議,其二進位制編碼(DER)仍用於編碼 SSL 證書(X.509),例如 [^25]。ASN.1 支援使用標籤號的模式演化,類似於 Protocol Buffers [^26]。然而,它也非常複雜且文件記錄不佳,因此 ASN.1 可能不是新應用程式的好選擇。
@@ -340,7 +342,7 @@ record Person {
由於資料轉儲是一次性寫入的,此後是不可變的,因此像 Avro 物件容器檔案這樣的格式非常適合。這也是將資料編碼為分析友好的列式格式(如 Parquet)的好機會(參見 ["列壓縮"](/tw/ch4#sec_storage_column_compression))。
-在 [Link to Come] 中,我們將更多地討論如何使用歸檔儲存中的資料。
+在 [第 11 章](/tw/ch11#ch_batch) 中,我們將更多地討論如何使用歸檔儲存中的資料。
### 流經服務的資料流:REST 與 RPC {#sec_encoding_dataflow_rpc}
@@ -423,7 +425,7 @@ Web 服務只是透過網路進行 API 請求的一長串技術的最新化身
* 本地函式呼叫是可預測的,要麼成功要麼失敗,僅取決於你控制的引數。網路請求是不可預測的:由於網路問題,請求或響應可能會丟失,或者遠端機器可能速度慢或不可用,而這些問題完全超出了你的控制。網路問題很常見,因此你必須預料到它們,例如透過重試失敗的請求。
* 本地函式呼叫要麼返回結果,要麼丟擲異常,要麼永不返回(因為它進入無限迴圈或程序崩潰)。網路請求有另一種可能的結果:它可能由於 *超時* 而沒有返回結果。在這種情況下,你根本不知道發生了什麼:如果你沒有從遠端服務獲得響應,你無法知道請求是否透過。(我們在 [第 9 章](/tw/ch9#ch_distributed) 中更詳細地討論了這個問題。)
-* 如果你重試失敗的網路請求,可能會發生前一個請求實際上已經透過,只是響應丟失了。在這種情況下,重試將導致操作執行多次,除非你在協議中構建去重機制(*冪等性*)[^40]。本地函式呼叫沒有這個問題。(我們在 [Link to Come] 中更詳細地討論了冪等性。)
+* 如果你重試失敗的網路請求,可能會發生前一個請求實際上已經成功,只是響應丟失了。在這種情況下,重試將導致操作執行多次,除非你在協議中構建去重機制(*冪等性*)[^40]。本地函式呼叫沒有這個問題。(我們在 [“冪等性”](/tw/ch12#sec_stream_idempotence) 中更詳細地討論冪等性。)
* 每次呼叫本地函式時,通常需要大約相同的時間來執行。網路請求比函式呼叫慢得多,其延遲也變化很大:在良好的時候,它可能在不到一毫秒內完成,但當網路擁塞或遠端服務過載時,執行完全相同的操作可能需要許多秒。
* 當你呼叫本地函式時,你可以有效地將引用(指標)傳遞給本地記憶體中的物件。當你發出網路請求時,所有這些引數都需要編碼為可以透過網路傳送的位元組序列。如果引數是不可變的原語,如數字或短字串,那沒問題,但對於更大量的資料和可變物件,它很快就會出現問題。
* 客戶端和服務可能以不同的程式語言實現,因此 RPC 框架必須將資料型別從一種語言轉換為另一種語言。這可能會變得很醜陋,因為並非所有語言都具有相同的型別——例如,回想一下 JavaScript 處理大於 2⁵³ 的數字的問題(參見 ["JSON、XML 及其二進位制變體"](/tw/ch5#sec_encoding_json))。單一語言編寫的單個程序中不存在此問題。
@@ -488,7 +490,7 @@ RPC 方案的向後和向前相容性屬性繼承自它使用的任何編碼:
持久化執行框架已成為構建需要事務性的基於服務的架構的流行方式。在我們的支付示例中,我們希望每筆付款都恰好處理一次。工作流執行期間的故障可能導致信用卡扣費,但沒有相應的銀行賬戶存款。在基於服務的架構中,我們不能簡單地將兩個任務包裝在資料庫事務中。此外,我們可能正在與我們控制有限的第三方支付閘道器進行互動。
-持久化執行框架是為工作流提供 *精確一次語義* 的一種方式。如果任務失敗,框架將重新執行該任務,但會跳過任務在失敗之前成功完成的任何 RPC 呼叫或狀態更改。相反,框架將假裝進行呼叫,但實際上將返回先前呼叫的結果。這是可能的,因為持久化執行框架將所有 RPC 和狀態更改記錄到持久儲存(如預寫日誌)[^45] [^46]。[示例 5-5](/tw/ch5#fig_temporal_workflow) 顯示了使用 Temporal 支援持久化執行的工作流定義示例。
+持久化執行框架是為工作流提供 *恰好一次語義* 的一種方式。如果任務失敗,框架將重新執行該任務,但會跳過任務在失敗之前成功完成的任何 RPC 呼叫或狀態更改。相反,框架將假裝進行呼叫,但實際上將返回先前呼叫的結果。這是可能的,因為持久化執行框架將所有 RPC 和狀態更改記錄到持久儲存(如預寫日誌)[^45] [^46]。[示例 5-5](/tw/ch5#fig_temporal_workflow) 顯示了使用 Temporal 支援持久化執行的工作流定義示例。
{{< figure id="fig_temporal_workflow" title="示例 5-5. [圖 5-7](/tw/ch5#fig_encoding_workflow) 中支付工作流的 Temporal 工作流定義片段。" class="w-full my-4" >}}
@@ -514,7 +516,7 @@ class PaymentWorkflow:
像 Temporal 這樣的框架並非沒有挑戰。外部服務(例如我們示例中的第三方支付閘道器)仍必須提供冪等 API。開發人員必須記住為這些 API 使用唯一 ID 以防止重複執行 [^47]。由於持久化執行框架按順序記錄每個 RPC 呼叫,因此它期望後續執行以相同的順序進行相同的 RPC 呼叫。這使得程式碼更改變得脆弱:你可能僅透過重新排序函式呼叫就引入未定義的行為 [^48]。與其修改現有工作流的程式碼,不如單獨部署新版本的程式碼更安全,以便現有工作流呼叫的重新執行繼續使用舊版本,只有新呼叫使用新程式碼 [^49]。
-同樣,由於持久化執行框架期望以確定性方式重放所有程式碼(相同的輸入產生相同的輸出),因此隨機數生成器或系統時鐘等非確定性程式碼會產生問題 [^48]。框架通常提供此類庫函式的自己的確定性實現,但你必須記住使用它們。在某些情況下,例如 Temporal 的 workflowcheck 工具,框架提供靜態分析工具來確定是否引入了非確定性行為。
+同樣,由於持久化執行框架期望以確定性方式重放所有程式碼(相同的輸入產生相同的輸出),因此隨機數生成器或系統時鐘等非確定性程式碼會產生問題 [^48]。框架通常會為這類庫函式提供自己的確定性實現,但你必須記得使用它們。在某些情況下,例如 Temporal 的 workflowcheck 工具,框架還會提供靜態分析工具來判斷是否引入了非確定性行為。
--------
@@ -539,7 +541,7 @@ class PaymentWorkflow:
#### 訊息代理 {#message-brokers}
-過去,訊息代理的格局由 TIBCO、IBM WebSphere 和 webMethods 等公司的商業企業軟體主導,然後開源實現(如 RabbitMQ、ActiveMQ、HornetQ、NATS 和 Apache Kafka)變得流行。最近,雲服務(如 Amazon Kinesis、Azure Service Bus 和 Google Cloud Pub/Sub)也獲得了採用。我們將在 [Link to Come] 中更詳細地比較它們。
+過去,訊息代理的格局由 TIBCO、IBM WebSphere 和 webMethods 等公司的商業企業軟體主導,然後開源實現(如 RabbitMQ、ActiveMQ、HornetQ、NATS 和 Apache Kafka)變得流行。最近,雲服務(如 Amazon Kinesis、Azure Service Bus 和 Google Cloud Pub/Sub)也獲得了採用。我們將在 [“訊息系統”](/tw/ch12#sec_stream_messaging) 中更詳細地比較它們。
詳細的傳遞語義因實現和配置而異,但通常,最常使用兩種訊息分發模式:
diff --git a/content/tw/ch6.md b/content/tw/ch6.md
index ecb2a5b..f63a380 100644
--- a/content/tw/ch6.md
+++ b/content/tw/ch6.md
@@ -6,11 +6,11 @@ breadcrumbs: false

-> *出錯的事物與不可能出錯的事物之間的主要區別在於,當不可能出錯的事物出錯時,通常會發現它幾乎不可能查詢或修復。*
+> *可能出錯的東西和“不可能”出錯的東西之間,最大的區別在於:後者一旦出錯,往往幾乎無從下手,也難以修復。*
>
-> Douglas Adams,《基本無害》(1992)
+> 道格拉斯·亞當斯,《基本無害》(1992)
-**複製** 指的是透過網路連線的多臺機器上儲存相同資料的副本。如 ["分散式與單節點系統"](/tw/ch1#sec_introduction_distributed) 中所討論的,你可能出於以下幾個原因想要複製資料:
+**複製** 指的是在透過網路連線的多臺機器上保留相同資料的副本。如 ["分散式與單節點系統"](/tw/ch1#sec_introduction_distributed) 中所討論的,你可能出於以下幾個原因希望複製資料:
* 使資料在地理上更接近使用者(從而減少訪問延遲)
* 即使系統的部分元件出現故障,也能讓系統繼續工作(從而提高可用性)
@@ -18,7 +18,7 @@ breadcrumbs: false
本章假設你的資料集足夠小,每臺機器都可以儲存整個資料集的副本。在 [第 7 章](/tw/ch7#ch_sharding) 中,我們將放寬這一假設,討論單臺機器無法容納的、過大資料集的 **分片**(**分割槽**)。在後續章節中,我們將討論複製資料系統中可能發生的各種故障,以及如何處理它們。
-如果需要複製的資料不會隨時間變化,那麼複製就很簡單:只需要將資料複製到每個節點一次就大功告成。處理複製的所有困難都在於處理複製資料的 **變更**,這也是本章的主題。我們將討論三種複製節點間變更的演算法族:**單主**、**多主** 和 **無主** 複製。幾乎所有分散式資料庫都使用這三種方法之一。它們各有利弊,我們將詳細研究。
+如果需要複製的資料不會隨時間變化,那麼複製就很簡單:只需要將資料複製到每個節點一次就大功告成。處理複製的所有困難都在於處理複製資料的 **變更**,這也是本章的主題。我們將討論三類在節點之間複製變更的演算法:**單主**、**多主** 和 **無主** 複製。幾乎所有分散式資料庫都使用這三種方法之一。它們各有利弊,我們將詳細研究。
複製需要考慮許多權衡:例如,是使用同步還是非同步複製,以及如何處理失敗的副本。這些通常是資料庫中的配置選項,儘管不同資料庫的細節有所不同,但許多不同實現的通用原則是相似的。我們將在本章中討論這些選擇的後果。
@@ -40,22 +40,22 @@ breadcrumbs: false
儲存資料庫副本的每個節點稱為 **副本**。有了多個副本,不可避免地會出現一個問題:我們如何確保所有資料最終都出現在所有副本上?
-每次寫入資料庫都需要由每個副本處理;否則,副本將不再包含相同的資料。最常見的解決方案稱為 **基於主節點的複製**、**主備複製** 或 **主動/被動複製**。它的工作原理如下(見 [圖 6-1](/tw/ch6#fig_replication_leader_follower)):
+每次寫入資料庫都需要由每個副本處理;否則,副本將不再包含相同的資料。最常見的解決方案稱為 **基於領導者的複製**、**主備複製** 或 **主動/被動複製**。它的工作原理如下(見 [圖 6-1](/tw/ch6#fig_replication_leader_follower)):
-1. 其中一個副本被指定為 **主節點**(也稱為 **主庫** 或 **源** [^2])。當客戶端想要寫入資料庫時,他們必須將請求傳送給主節點,主節點首先將新資料寫入其本地儲存。
-2. 其他副本稱為 **從節點**(**只讀副本**、**從庫** 或 **熱備**)。每當主節點將新資料寫入其本地儲存時,它也會將資料變更作為 **複製日誌** 或 **變更流** 的一部分發送給所有從節點。每個從節點從主節點獲取日誌,並透過按照與主節點處理相同的順序應用所有寫入來相應地更新其本地資料庫副本。
-3. 當客戶端想要從資料庫讀取時,它可以查詢主節點或任何從節點。然而,只有主節點接受寫入(從客戶端的角度來看,從節點是隻讀的)。
+1. 其中一個副本被指定為 **領導者**(也稱為 **主庫** 或 **源** [^2])。當客戶端想要寫入資料庫時,他們必須將請求傳送給領導者,領導者首先將新資料寫入其本地儲存。
+2. 其他副本稱為 **追隨者**(**只讀副本**、**從庫** 或 **熱備**)。每當領導者將新資料寫入其本地儲存時,它也會將資料變更作為 **複製日誌** 或 **變更流** 的一部分發送給所有追隨者。每個追隨者從領導者獲取日誌,並透過按照與領導者處理相同的順序應用所有寫入來相應地更新其本地資料庫副本。
+3. 當客戶端想要從資料庫讀取時,它可以查詢領導者或任何追隨者。然而,只有領導者接受寫入(從客戶端的角度來看,追隨者是隻讀的)。
-{{< figure src="/fig/ddia_0601.png" id="fig_replication_leader_follower" caption="圖 6-1. 單主複製將所有寫入定向到指定的主節點,該主節點向從副本傳送變更流。" class="w-full my-4" >}}
+{{< figure src="/fig/ddia_0601.png" id="fig_replication_leader_follower" caption="圖 6-1. 單主複製將所有寫入定向到指定的領導者,該領導者向追隨者傳送變更流。" class="w-full my-4" >}}
-如果資料庫是分片的(見 [第 7 章](/tw/ch7#ch_sharding)),每個分片都有一個主節點。不同的分片可能在不同的節點上有其主節點,但每個分片仍必須有一個主節點。在 ["多主複製"](/tw/ch6#sec_replication_multi_leader) 中,我們將討論一種替代模型,其中系統可能同時為同一分片擁有多個主節點。
+如果資料庫是分片的(見 [第 7 章](/tw/ch7#ch_sharding)),每個分片都有一個領導者。不同的分片可能在不同的節點上有其領導者,但每個分片仍必須有一個領導者。在 ["多主複製"](/tw/ch6#sec_replication_multi_leader) 中,我們將討論一種替代模型,其中系統可能同時為同一分片擁有多個領導者。
-單主複製被廣泛使用。它是許多關係資料庫的內建功能,如 PostgreSQL、MySQL、Oracle Data Guard [^3] 和 SQL Server 的 Always On 可用性組 [^4]。它也用於一些文件資料庫,如 MongoDB 和 DynamoDB [^5],訊息代理如 Kafka,複製塊裝置如 DRBD,以及一些網路檔案系統。許多共識演算法(如 Raft)也基於單個主節點,用於 CockroachDB [^6]、TiDB [^7]、etcd 和 RabbitMQ 仲裁佇列(以及其他)中的複製,並在舊主節點失敗時自動選舉新主節點(我們將在 [第 10 章](/tw/ch10#ch_consistency) 中更詳細地討論共識)。
+單主複製被廣泛使用。它是許多關係資料庫的內建功能,如 PostgreSQL、MySQL、Oracle Data Guard [^3] 和 SQL Server 的 Always On 可用性組 [^4]。它也用於一些文件資料庫,如 MongoDB 和 DynamoDB [^5],訊息代理如 Kafka,複製塊裝置如 DRBD,以及一些網路檔案系統。許多共識演算法(如 Raft)也基於單個領導者,用於 CockroachDB [^6]、TiDB [^7]、etcd 和 RabbitMQ 仲裁佇列(以及其他)中的複製,並在舊領導者失敗時自動選舉新領導者(我們將在 [第 10 章](/tw/ch10#ch_consistency) 中更詳細地討論共識)。
--------
> [!NOTE]
-> 在較舊的文件中,你可能會看到術語 **主從複製**。它與基於主節點的複製含義相同,但應該避免使用該術語,因為它被廣泛認為是冒犯性的 [^8]。
+> 在較舊的文件中,你可能會看到術語 **主從複製**。它與基於領導者的複製含義相同,但應該避免使用該術語,因為它被廣泛認為是冒犯性的 [^8]。
--------
@@ -63,40 +63,40 @@ breadcrumbs: false
複製系統的一個重要細節是複製是 **同步** 發生還是 **非同步** 發生。(在關係資料庫中,這通常是一個可配置選項;其他系統通常硬編碼為其中之一。)
-想想 [圖 6-1](/tw/ch6#fig_replication_leader_follower) 中發生的情況,一個網站使用者更新他們的個人資料圖片。在某個時間點,客戶端向主節點發送更新請求;不久之後,主節點收到了它。在某個時間點,主節點將資料變更轉發給從節點。最終,主節點通知客戶端更新成功。[圖 6-2](/tw/ch6#fig_replication_sync_replication) 顯示了時序可能的工作方式。
+想想 [圖 6-1](/tw/ch6#fig_replication_leader_follower) 中發生的情況,一個網站使用者更新他們的個人資料圖片。在某個時間點,客戶端向領導者傳送更新請求;不久之後,領導者收到了它。在某個時間點,領導者將資料變更轉發給追隨者。最終,領導者通知客戶端更新成功。[圖 6-2](/tw/ch6#fig_replication_sync_replication) 顯示了時序可能的工作方式。
-{{< figure src="/fig/ddia_0602.png" id="fig_replication_sync_replication" caption="圖 6-2. 基於主節點的複製,帶有一個同步和一個非同步從節點。" class="w-full my-4" >}}
+{{< figure src="/fig/ddia_0602.png" id="fig_replication_sync_replication" caption="圖 6-2. 基於領導者的複製,帶有一個同步和一個非同步追隨者。" class="w-full my-4" >}}
-在 [圖 6-2](/tw/ch6#fig_replication_sync_replication) 的示例中,對從節點 1 的複製是 **同步的**:主節點等待從節點 1 確認它已收到寫入,然後才向用戶報告成功,並使寫入對其他客戶端可見。對從節點 2 的複製是 **非同步的**:主節點發送訊息,但不等待從節點的響應。
+在 [圖 6-2](/tw/ch6#fig_replication_sync_replication) 的示例中,對追隨者 1 的複製是 **同步的**:領導者等待追隨者 1 確認它已收到寫入,然後才向用戶報告成功,並使寫入對其他客戶端可見。對追隨者 2 的複製是 **非同步的**:領導者傳送訊息,但不等待追隨者的響應。
-圖中顯示,從節點 2 處理訊息之前有相當大的延遲。通常,複製相當快:大多數資料庫系統在不到一秒的時間內將變更應用到從節點。然而,不能保證需要多長時間。在某些情況下,從節點可能落後主節點幾分鐘或更長時間;例如,如果從節點正在從故障中恢復,如果系統正在接近最大容量執行,或者如果節點之間存在網路問題。
+圖中顯示,追隨者 2 處理訊息之前有相當大的延遲。通常,複製相當快:大多數資料庫系統在不到一秒的時間內將變更應用到追隨者。然而,不能保證需要多長時間。在某些情況下,追隨者可能落後領導者幾分鐘或更長時間;例如,如果追隨者正在從故障中恢復,如果系統正在接近最大容量執行,或者如果節點之間存在網路問題。
-同步複製的優點是從節點保證擁有與主節點一致的最新資料副本。如果主節點突然失敗,我們可以確信資料仍然在從節點上可用。缺點是,如果同步從節點沒有響應(因為它已崩潰,或存在網路故障,或任何其他原因),寫入就無法處理。主節點必須阻塞所有寫入並等待同步副本再次可用。
+同步複製的優點是追隨者保證擁有與領導者一致的最新資料副本。如果領導者突然失敗,我們可以確信資料仍然在追隨者上可用。缺點是,如果同步追隨者沒有響應(因為它已崩潰,或存在網路故障,或任何其他原因),寫入就無法處理。領導者必須阻塞所有寫入並等待同步副本再次可用。
-因此,將所有從節點都設為同步是不切實際的:任何一個節點的中斷都會導致整個系統停止。實際上,如果資料庫提供同步複製,通常意味著 **一個** 從節點是同步的,其他的是非同步的。如果同步從節點變得不可用或緩慢,非同步從節點之一將變為同步。這保證了你至少在兩個節點上擁有最新的資料副本:主節點和一個同步從節點。這種配置有時也稱為 **半同步**。
+因此,將所有追隨者都設為同步是不切實際的:任何一個節點的中斷都會導致整個系統停止。實際上,如果資料庫提供同步複製,通常意味著 **一個** 追隨者是同步的,其他的是非同步的。如果同步追隨者變得不可用或緩慢,非同步追隨者之一將變為同步。這保證了你至少在兩個節點上擁有最新的資料副本:領導者和一個同步追隨者。這種配置有時也稱為 **半同步**。
-在某些系統中,**多數**(例如,包括主節點在內的 5 個副本中的 3 個)副本被同步更新,其餘少數是非同步的。這是 **仲裁** 的一個例子,我們將在 ["讀寫仲裁"](/tw/ch6#sec_replication_quorum_condition) 中進一步討論。多數仲裁通常用於使用共識協議進行自動主節點選舉的系統中,我們將在 [第 10 章](/tw/ch10#ch_consistency) 中回到這個話題。
+在某些系統中,**多數**(例如,包括領導者在內的 5 個副本中的 3 個)副本被同步更新,其餘少數是非同步的。這是 **仲裁** 的一個例子,我們將在 ["讀寫仲裁"](/tw/ch6#sec_replication_quorum_condition) 中進一步討論。多數仲裁通常用於使用共識協議進行自動領導者選舉的系統中,我們將在 [第 10 章](/tw/ch10#ch_consistency) 中回到這個話題。
-有時,基於主節點的複製被配置為完全非同步。在這種情況下,如果主節點失敗且無法恢復,任何尚未複製到從節點的寫入都會丟失。這意味著即使已向客戶端確認,寫入也不能保證持久。然而,完全非同步配置的優點是主節點可以繼續處理寫入,即使所有從節點都已落後。
+有時,基於領導者的複製被配置為完全非同步。在這種情況下,如果領導者失敗且無法恢復,任何尚未複製到追隨者的寫入都會丟失。這意味著即使已向客戶端確認,寫入也不能保證持久。然而,完全非同步配置的優點是領導者可以繼續處理寫入,即使所有追隨者都已落後。
-弱化永續性可能聽起來像是一個糟糕的權衡,但非同步複製仍然被廣泛使用,特別是如果有許多從節點或者它們在地理上分佈廣泛 [^9]。我們將在 ["複製延遲的問題"](/tw/ch6#sec_replication_lag) 中回到這個問題。
+弱化永續性可能聽起來像是一個糟糕的權衡,但非同步複製仍然被廣泛使用,特別是如果有許多追隨者或者它們在地理上分佈廣泛 [^9]。我們將在 ["複製延遲的問題"](/tw/ch6#sec_replication_lag) 中回到這個問題。
### 設定新的副本 {#sec_replication_new_replica}
-不時地,你需要設定新的從節點——也許是為了增加副本的數量,或者替換失敗的節點。如何確保新的從節點擁有主節點資料的準確副本?
+不時地,你需要設定新的追隨者——也許是為了增加副本的數量,或者替換失敗的節點。如何確保新的追隨者擁有領導者資料的準確副本?
簡單地將資料檔案從一個節點複製到另一個節點通常是不夠的:客戶端不斷向資料庫寫入,資料總是在變化,所以標準檔案複製會在不同的時間點看到資料庫的不同部分。結果可能沒有任何意義。
-你可以透過鎖定資料庫(使其不可用於寫入)來使磁碟上的檔案保持一致,但這將違揹我們的高可用性目標。幸運的是,設定從節點通常可以在不停機的情況下完成。從概念上講,過程如下所示:
+你可以透過鎖定資料庫(使其不可用於寫入)來使磁碟上的檔案保持一致,但這將違揹我們的高可用性目標。幸運的是,設定追隨者通常可以在不停機的情況下完成。從概念上講,過程如下所示:
-1. 在某個時間點獲取主節點資料庫的一致快照——如果可能,不鎖定整個資料庫。大多數資料庫都有此功能,因為備份也需要它。在某些情況下,需要第三方工具,例如用於 MySQL 的 Percona XtraBackup。
-2. 將快照複製到新的從節點。
-3. 從節點連線到主節點並請求自快照拍攝以來發生的所有資料變更。這要求快照與主節點複製日誌中的確切位置相關聯。該位置有各種名稱:例如,PostgreSQL 稱之為 **日誌序列號**;MySQL 有兩種機制,**binlog 位點** 和 **全域性事務識別符號**(GTID)。
-4. 當從節點處理了自快照以來的資料變更積壓後,我們說它已經 **追上進度**。它現在可以繼續處理主節點發生的資料變更。
+1. 在某個時間點獲取領導者資料庫的一致快照——如果可能,不鎖定整個資料庫。大多數資料庫都有此功能,因為備份也需要它。在某些情況下,需要第三方工具,例如用於 MySQL 的 Percona XtraBackup。
+2. 將快照複製到新的追隨者。
+3. 追隨者連線到領導者並請求自快照拍攝以來發生的所有資料變更。這要求快照與領導者複製日誌中的確切位置相關聯。該位置有各種名稱:例如,PostgreSQL 稱之為 **日誌序列號**;MySQL 有兩種機制,**binlog 位點** 和 **全域性事務識別符號**(GTID)。
+4. 當追隨者處理了自快照以來的資料變更積壓後,我們說它已經 **追上進度**。它現在可以繼續處理領導者發生的資料變更。
-設定從節點的實際步驟因資料庫而異。在某些系統中,該過程是完全自動化的,而在其他系統中,它可能是需要管理員手動執行的有些神秘的多步驟工作流程。
+設定追隨者的實際步驟因資料庫而異。在某些系統中,該過程是完全自動化的,而在其他系統中,它可能是需要管理員手動執行的有些神秘的多步驟工作流程。
-你也可以將複製日誌歸檔到物件儲存;連同物件儲存中整個資料庫的定期快照,這是實現資料庫備份和災難恢復的好方法。你還可以透過從物件儲存下載這些檔案來執行設定新從節點的步驟 1 和 2。例如,WAL-G 為 PostgreSQL、MySQL 和 SQL Server 執行此操作,Litestream 為 SQLite 執行等效操作。
+你也可以將複製日誌歸檔到物件儲存;連同物件儲存中整個資料庫的定期快照,這是實現資料庫備份和災難恢復的好方法。你還可以透過從物件儲存下載這些檔案來執行設定新追隨者的步驟 1 和 2。例如,WAL-G 為 PostgreSQL、MySQL 和 SQL Server 執行此操作,Litestream 為 SQLite 執行等效操作。
--------
@@ -121,53 +121,53 @@ breadcrumbs: false
系統中的任何節點都可能發生故障,可能是由於故障意外發生,但同樣可能是由於計劃維護(例如,重新啟動機器以安裝核心安全補丁)。能夠在不停機的情況下重新啟動單個節點對於操作和維護來說是一個很大的優勢。因此,我們的目標是儘管單個節點發生故障,但保持整個系統執行,並儘可能減小節點中斷的影響。
-如何透過基於主節點的複製實現高可用性?
+如何透過基於領導者的複製實現高可用性?
-#### 從節點故障:追趕恢復 {#follower-failure-catch-up-recovery}
+#### 追隨者故障:追趕恢復 {#follower-failure-catch-up-recovery}
-在其本地磁碟上,每個從節點保留從主節點接收的資料變更日誌。如果從節點崩潰並重新啟動,或者如果主節點和從節點之間的網路暫時中斷,從節點可以很容易地恢復:從其日誌中,它知道在故障發生之前處理的最後一個事務。因此,從節點可以連線到主節點並請求在從節點斷開連線期間發生的所有資料變更。當它應用了這些變更後,它就趕上了主節點,可以像以前一樣繼續接收資料變更流。
+在其本地磁碟上,每個追隨者保留從領導者接收的資料變更日誌。如果追隨者崩潰並重新啟動,或者如果領導者和追隨者之間的網路暫時中斷,追隨者可以很容易地恢復:從其日誌中,它知道在故障發生之前處理的最後一個事務。因此,追隨者可以連線到領導者並請求在追隨者斷開連線期間發生的所有資料變更。當它應用了這些變更後,它就趕上了領導者,可以像以前一樣繼續接收資料變更流。
-儘管從節點恢復在概念上很簡單,但在效能方面可能具有挑戰性:如果資料庫具有高寫入吞吐量,或者如果從節點已離線很長時間,可能有很多寫入需要趕上。在進行這種追趕時,恢復的從節點和主節點(需要將寫入積壓傳送到從節點)都會有高負載。
+儘管追隨者恢復在概念上很簡單,但在效能方面可能具有挑戰性:如果資料庫具有高寫入吞吐量,或者如果追隨者已離線很長時間,可能有很多寫入需要趕上。在進行這種追趕時,恢復的追隨者和領導者(需要將寫入積壓傳送到追隨者)都會有高負載。
-一旦所有從節點都確認已處理了日誌,主節點就可以刪除其寫入日誌,但如果從節點長時間不可用,主節點面臨選擇:要麼保留日誌直到從節點恢復並趕上(冒著主節點磁碟空間耗盡的風險),要麼刪除不可用從節點尚未確認的日誌(在這種情況下,從節點無法從日誌中恢復,並且在它回來時必須從備份中恢復)。
+一旦所有追隨者都確認已處理了日誌,領導者就可以刪除其寫入日誌,但如果追隨者長時間不可用,領導者面臨選擇:要麼保留日誌直到追隨者恢復並趕上(冒著領導者磁碟空間耗盡的風險),要麼刪除不可用追隨者尚未確認的日誌(在這種情況下,追隨者無法從日誌中恢復,並且在它回來時必須從備份中恢復)。
#### 領導者故障:故障轉移 {#leader-failure-failover}
-處理主節點故障更加棘手:其中一個從節點需要被提升為新的主節點,客戶端需要重新配置以將其寫入傳送到新的主節點,其他從節點需要開始從新的主節點消費資料變更。這個過程稱為 **故障轉移**。
+處理領導者故障更加棘手:其中一個追隨者需要被提升為新的領導者,客戶端需要重新配置以將其寫入傳送到新的領導者,其他追隨者需要開始從新的領導者消費資料變更。這個過程稱為 **故障轉移**。
-故障轉移可以手動發生(管理員收到主節點失敗的通知並採取必要步驟來建立新的主節點)或自動發生。自動故障轉移過程通常包括以下步驟:
+故障轉移可以手動發生(管理員收到領導者失敗的通知並採取必要步驟來建立新的領導者)或自動發生。自動故障轉移過程通常包括以下步驟:
-1. **確定主節點已失敗。** 可能會出現許多問題:崩潰、停電、網路問題等。沒有萬無一失的方法來檢測出了什麼問題,所以大多數系統只是使用超時:節點經常相互反彈訊息,如果節點在一段時間內沒有響應——比如 30 秒——它被認為已死。(如果主節點被故意關閉以進行計劃維護,這不適用。)
-2. **選擇新的主節點。** 這可以透過選舉過程完成(其中主節點由剩餘副本的多數選擇),或者新的主節點可以由先前建立的 **控制器節點** 任命 [^13]。領導的最佳候選者通常是具有來自舊主節點的最新資料變更的副本(以最小化任何資料丟失)。讓所有節點就新主節點達成一致是一個共識問題,在 [第 10 章](/tw/ch10#ch_consistency) 中詳細討論。
-3. **重新配置系統以使用新的主節點。** 客戶端現在需要將其寫入請求傳送到新的主節點(我們在 ["請求路由"](/tw/ch7#sec_sharding_routing) 中討論這個問題)。如果舊的主節點恢復,它可能仍然認為自己是主節點,沒有意識到其他副本已經迫使它下臺。系統需要確保舊的主節點成為從節點並識別新的主節點。
+1. **確定領導者已失效。** 可能會出現許多問題:崩潰、停電、網路故障等。沒有萬無一失的方法能準確判斷發生了什麼,所以大多數系統只是依賴超時:節點之間會頻繁來回傳送訊息,如果某個節點在一段時間內(例如 30 秒)沒有響應,就認為它已經失效。(如果是計劃維護而主動下線領導者,則不適用。)
+2. **選擇新的領導者。** 這可以透過選舉過程完成(由剩餘副本中的多數選出領導者),也可以由預先設定的 **控制器節點** 任命 [^13]。最適合擔任領導者的通常是那個擁有舊領導者最新資料變更的副本(以儘量減少資料丟失)。讓所有節點就新領導者達成一致是一個共識問題,我們會在 [第 10 章](/tw/ch10#ch_consistency) 詳細討論。
+3. **將系統重新配置為使用新的領導者。** 客戶端現在需要把寫請求傳送到新領導者(我們在 ["請求路由"](/tw/ch7#sec_sharding_routing) 中討論這個問題)。如果舊領導者恢復,它可能仍然以為自己是領導者,並不知道其他副本已經讓它下臺。系統需要確保舊領導者降級為追隨者,並識別新的領導者。
故障轉移充滿了可能出錯的事情:
-* 如果使用非同步複製,新的主節點可能在失敗之前沒有收到來自舊主節點的所有寫入。如果前主節點在選擇了新主節點後重新加入叢集,那些寫入應該怎麼辦?新的主節點可能同時收到了衝突的寫入。最常見的解決方案是簡單地丟棄舊主節點未複製的寫入,這意味著你認為已提交的寫入實際上並不持久。
-* 如果資料庫之外的其他儲存系統需要與資料庫內容協調,丟棄寫入尤其危險。例如,在 GitHub 的一次事故中 [^14],一個過時的 MySQL 從節點被提升為主節點。資料庫使用自增計數器為新行分配主鍵,但由於新主節點的計數器落後於舊主節點,它重用了舊主節點先前分配的一些主鍵。這些主鍵也在 Redis 儲存中使用,因此主鍵的重用導致 MySQL 和 Redis 之間的不一致,這導致一些私人資料被錯誤地披露給錯誤的使用者。
-* 在某些故障場景中(見 [第 9 章](/tw/ch9#ch_distributed)),可能會發生兩個節點都認為自己是主節點的情況。這種情況稱為 **腦裂**,這是危險的:如果兩個主節點都接受寫入,並且沒有解決衝突的過程(見 ["多主複製"](/tw/ch6#sec_replication_multi_leader)),資料很可能會丟失或損壞。作為安全措施,一些系統在檢測到兩個主節點時有一種機制來關閉一個節點。然而,如果這種機制設計不當,你最終可能會關閉兩個節點 [^15]。此外,當檢測到腦裂並關閉舊節點時,可能為時已晚,資料已經損壞。
-* 在宣佈主節點死亡之前,正確的超時是什麼?更長的超時意味著在主節點失敗的情況下恢復時間更長。然而,如果超時太短,可能會有不必要的故障轉移。例如,臨時負載峰值可能導致節點的響應時間增加到超時以上,或者網路故障可能導致資料包延遲。如果系統已經在高負載或網路問題上掙扎,不必要的故障轉移可能會使情況變得更糟,而不是更好。
+* 如果使用非同步複製,新的領導者可能在失敗之前沒有收到來自舊領導者的所有寫入。如果前領導者在選擇了新領導者後重新加入叢集,那些寫入應該怎麼辦?新的領導者可能同時收到了衝突的寫入。最常見的解決方案是簡單地丟棄舊領導者未複製的寫入,這意味著你認為已提交的寫入實際上並不持久。
+* 如果資料庫之外的其他儲存系統需要與資料庫內容協調,丟棄寫入尤其危險。例如,在 GitHub 的一次事故中 [^14],一個過時的 MySQL 追隨者被提升為領導者。資料庫使用自增計數器為新行分配主鍵,但由於新領導者的計數器落後於舊領導者,它重用了舊領導者先前分配的一些主鍵。這些主鍵也在 Redis 儲存中使用,因此主鍵的重用導致 MySQL 和 Redis 之間的不一致,這導致一些私人資料被錯誤地披露給錯誤的使用者。
+* 在某些故障場景中(見 [第 9 章](/tw/ch9#ch_distributed)),可能會發生兩個節點都認為自己是領導者的情況。這種情況稱為 **腦裂**,這是危險的:如果兩個領導者都接受寫入,並且沒有解決衝突的過程(見 ["多主複製"](/tw/ch6#sec_replication_multi_leader)),資料很可能會丟失或損壞。作為安全措施,一些系統在檢測到兩個領導者時有一種機制來關閉一個節點。然而,如果這種機制設計不當,你最終可能會關閉兩個節點 [^15]。此外,當檢測到腦裂並關閉舊節點時,可能為時已晚,資料已經損壞。
+* 在宣佈領導者死亡之前,正確的超時是什麼?更長的超時意味著在領導者失敗的情況下恢復時間更長。然而,如果超時太短,可能會有不必要的故障轉移。例如,臨時負載峰值可能導致節點的響應時間增加到超時以上,或者網路故障可能導致資料包延遲。如果系統已經在高負載或網路問題上掙扎,不必要的故障轉移可能會使情況變得更糟,而不是更好。
--------
> [!NOTE]
-> 透過限制或關閉舊主節點來防止腦裂被稱為 **柵欄機制**,或者更強調地說,**向頭部開槍**(STONITH)。我們將在 ["分散式鎖和租約"](/tw/ch9#sec_distributed_lock_fencing) 中更詳細地討論柵欄機制。
+> 透過限制或關閉舊領導者來防止腦裂,被稱為 **柵欄機制**(fencing),或者更直白地說,**爆彼之頭**(STONITH)。我們將在 ["分散式鎖和租約"](/tw/ch9#sec_distributed_lock_fencing) 中更詳細地討論柵欄機制。
--------
這些問題沒有簡單的解決方案。因此,一些運維團隊更喜歡手動執行故障轉移,即使軟體支援自動故障轉移。
-故障轉移最重要的是選擇一個最新的從節點作為新的主節點——如果使用同步或半同步複製,這將是舊主節點在確認寫入之前等待的從節點。使用非同步複製,你可以選擇具有最大日誌序列號的從節點。這最小化了故障轉移期間丟失的資料量:丟失幾分之一秒的寫入可能是可以容忍的,但選擇落後幾天的從節點可能是災難性的。
+故障轉移最重要的是選擇一個最新的追隨者作為新的領導者——如果使用同步或半同步複製,這將是舊領導者在確認寫入之前等待的追隨者。使用非同步複製,你可以選擇具有最大日誌序列號的追隨者。這最小化了故障轉移期間丟失的資料量:丟失幾分之一秒的寫入可能是可以容忍的,但選擇落後幾天的追隨者可能是災難性的。
這些問題——節點故障;不可靠的網路;以及圍繞副本一致性、永續性、可用性和延遲的權衡——實際上是分散式系統中的基本問題。在 [第 9 章](/tw/ch9#ch_distributed) 和 [第 10 章](/tw/ch10#ch_consistency) 中,我們將更深入地討論它們。
### 複製日誌的實現 {#sec_replication_implementation}
-基於主節點的複製在底層是如何工作的?讓我們簡要地看看實踐中使用的幾種不同的複製方法。
+基於領導者的複製在底層是如何工作的?讓我們簡要地看看實踐中使用的幾種不同的複製方法。
#### 基於語句的複製 {#statement-based-replication}
-在最簡單的情況下,主節點記錄它執行的每個寫入請求(**語句**)並將該語句日誌傳送給其從節點。對於關係資料庫,這意味著每個 `INSERT`、`UPDATE` 或 `DELETE` 語句都被轉發到從節點,每個從節點解析並執行該 SQL 語句,就像它是從客戶端接收的一樣。
+在最簡單的情況下,領導者記錄它執行的每個寫入請求(**語句**)並將該語句日誌傳送給其追隨者。對於關係資料庫,這意味著每個 `INSERT`、`UPDATE` 或 `DELETE` 語句都被轉發到追隨者,每個追隨者解析並執行該 SQL 語句,就像它是從客戶端接收的一樣。
雖然這聽起來合理,但這種複製方法可能會出現各種問題:
@@ -175,17 +175,17 @@ breadcrumbs: false
* 如果語句使用自增列,或者如果它們依賴於資料庫中的現有資料(例如,`UPDATE … WHERE <某條件>`),它們必須在每個副本上以完全相同的順序執行,否則它們可能會產生不同的效果。當有多個併發執行的事務時,這可能會受到限制。
* 具有副作用的語句(例如,觸發器、儲存過程、使用者定義的函式)可能會導致每個副本上發生不同的副作用,除非副作用是絕對確定的。
-可以解決這些問題——例如,主節點可以在記錄語句時用固定的返回值替換任何非確定性函式呼叫,以便從節點都獲得相同的值。以固定順序執行確定性語句的想法類似於我們之前在 ["事件溯源與 CQRS"](/tw/ch3#sec_datamodels_events) 中討論的事件溯源模型。這種方法也稱為 **狀態機複製**,我們將在 ["使用共享日誌"](/tw/ch10#sec_consistency_smr) 中討論其背後的理論。
+可以解決這些問題——例如,領導者可以在記錄語句時用固定的返回值替換任何非確定性函式呼叫,以便追隨者都獲得相同的值。以固定順序執行確定性語句的想法類似於我們之前在 ["事件溯源與 CQRS"](/tw/ch3#sec_datamodels_events) 中討論的事件溯源模型。這種方法也稱為 **狀態機複製**,我們將在 ["使用共享日誌"](/tw/ch10#sec_consistency_smr) 中討論其背後的理論。
基於語句的複製在 MySQL 5.1 版本之前使用。它今天有時仍在使用,因為它相當緊湊,但預設情況下,如果語句中有任何非確定性,MySQL 現在會切換到基於行的複製(稍後討論)。VoltDB 使用基於語句的複製,並透過要求事務是確定性的來使其安全 [^16]。然而,確定性在實踐中很難保證,因此許多資料庫更喜歡其他複製方法。
#### 預寫日誌(WAL)傳輸 {#write-ahead-log-wal-shipping}
-在 [第 4 章](/tw/ch4#ch_storage) 中,我們看到預寫日誌是使 B 樹儲存引擎健壯所必需的:每個修改首先寫入 WAL,以便在崩潰後可以將樹恢復到一致狀態。由於 WAL 包含將索引和堆恢復到一致狀態所需的所有資訊,我們可以使用完全相同的日誌在另一個節點上構建副本:除了將日誌寫入磁碟外,主節點還透過網路將其傳送給其從節點。當從節點處理此日誌時,它構建了與主節點上找到的完全相同的檔案副本。
+在 [第 4 章](/tw/ch4#ch_storage) 中,我們看到預寫日誌是使 B 樹儲存引擎健壯所必需的:每個修改首先寫入 WAL,以便在崩潰後可以將樹恢復到一致狀態。由於 WAL 包含將索引和堆恢復到一致狀態所需的所有資訊,我們可以使用完全相同的日誌在另一個節點上構建副本:除了將日誌寫入磁碟外,領導者還透過網路將其傳送給其追隨者。當追隨者處理此日誌時,它構建了與領導者上找到的完全相同的檔案副本。
-此複製方法在 PostgreSQL 和 Oracle 等中使用 [^17] [^18]。主要缺點是日誌在非常低的級別描述資料:WAL 包含哪些位元組在哪些磁碟塊中被更改的詳細資訊。這使得複製與儲存引擎緊密耦合。如果資料庫從一個版本更改其儲存格式到另一個版本,通常不可能在主節點和從節點上執行不同版本的資料庫軟體。
+此複製方法在 PostgreSQL 和 Oracle 等中使用 [^17] [^18]。主要缺點是日誌在非常低的級別描述資料:WAL 包含哪些位元組在哪些磁碟塊中被更改的詳細資訊。這使得複製與儲存引擎緊密耦合。如果資料庫從一個版本更改其儲存格式到另一個版本,通常不可能在領導者和追隨者上執行不同版本的資料庫軟體。
-這可能看起來像是一個小的實現細節,但它可能會產生很大的操作影響。如果複製協議允許從節點使用比主節點更新的軟體版本,你可以透過首先升級從節點然後執行故障轉移以使其中一個升級的節點成為新的主節點來執行資料庫軟體的零停機升級。如果複製協議不允許此版本不匹配(如 WAL 傳輸的情況),此類升級需要停機。
+這可能看起來像是一個小的實現細節,但它可能會產生很大的操作影響。如果複製協議允許追隨者使用比領導者更新的軟體版本,你可以透過首先升級追隨者然後執行故障轉移以使其中一個升級的節點成為新的領導者來執行資料庫軟體的零停機升級。如果複製協議不允許此版本不匹配(如 WAL 傳輸的情況),此類升級需要停機。
#### 邏輯(基於行)日誌複製 {#logical-row-based-log-replication}
@@ -199,35 +199,35 @@ breadcrumbs: false
修改多行的事務會生成多個這樣的日誌記錄,後跟指示事務已提交的記錄。MySQL 除了 WAL 之外還保留一個單獨的邏輯複製日誌,稱為 **binlog**(當配置為使用基於行的複製時)。PostgreSQL 透過將物理 WAL 解碼為行插入/更新/刪除事件來實現邏輯複製 [^19]。
-由於邏輯日誌與儲存引擎內部解耦,因此可以更容易地保持向後相容,允許主節點和從節點執行不同版本的資料庫軟體。這反過來又可以以最少的停機時間升級到新版本 [^20]。
+由於邏輯日誌與儲存引擎內部解耦,因此可以更容易地保持向後相容,允許領導者和追隨者執行不同版本的資料庫軟體。這反過來又可以以最少的停機時間升級到新版本 [^20]。
-邏輯日誌格式也更容易供外部應用程式解析。如果你想將資料庫的內容傳送到外部系統(例如用於離線分析的資料倉庫),或者構建自定義索引和快取 [^21],這方面很有用。這種技術稱為 **變更資料捕獲**,我們將在 [Link to Come] 中回到它。
+邏輯日誌格式也更容易被外部應用解析。如果你想把資料庫內容傳送到外部系統(例如用於離線分析的資料倉庫),或者構建自定義索引和快取 [^21],這一點會很有用。這種技術稱為 **資料變更捕獲**,我們將在 ["資料變更捕獲"](/tw/ch12#sec_stream_cdc) 一節再回到它。
## 複製延遲的問題 {#sec_replication_lag}
能夠容忍節點故障只是想要複製的一個原因。如 ["分散式與單節點系統"](/tw/ch1#sec_introduction_distributed) 中所述,其他原因是可伸縮性(處理比單臺機器能夠處理的更多請求)和延遲(將副本在地理上放置得更接近使用者)。
-基於主節點的複製要求所有寫入都透過單個節點,但只讀查詢可以轉到任何副本。對於主要由讀取和只有少量寫入組成的工作負載(這通常是線上服務的情況),有一個有吸引力的選擇:建立許多從節點,並將讀取請求分佈在這些從節點上。這減輕了主節點的負載,並允許附近的副本提供讀取請求。
+基於領導者的複製要求所有寫入都透過單個節點,但只讀查詢可以轉到任何副本。對於主要由讀取和只有少量寫入組成的工作負載(這通常是線上服務的情況),有一個有吸引力的選擇:建立許多追隨者,並將讀取請求分佈在這些追隨者上。這減輕了領導者的負載,並允許附近的副本提供讀取請求。
-在這種 **讀擴充套件** 架構中,你可以透過新增更多從節點來簡單地增加服務只讀請求的容量。然而,這種方法只有在使用非同步複製時才現實可行——如果你試圖同步複製到所有從節點,單個節點故障或網路中斷將使整個系統無法寫入。而且你擁有的節點越多,其中一個節點宕機的可能性就越大,因此完全同步的配置將非常不可靠。
+在這種 **讀擴充套件** 架構中,你可以透過新增更多追隨者來簡單地增加服務只讀請求的容量。然而,這種方法只有在使用非同步複製時才現實可行——如果你試圖同步複製到所有追隨者,單個節點故障或網路中斷將使整個系統無法寫入。而且你擁有的節點越多,其中一個節點宕機的可能性就越大,因此完全同步的配置將非常不可靠。
-不幸的是,如果應用程式從 **非同步** 從節點讀取,如果從節點已落後,它可能會看到過時的資訊。這導致資料庫中出現明顯的不一致:如果你同時在主節點和從節點上執行相同的查詢,你可能會得到不同的結果,因為並非所有寫入都已反映在從節點中。這種不一致只是一種臨時狀態——如果你停止向資料庫寫入並等待一段時間,從節點最終將趕上並與主節點保持一致。因此,這種效果被稱為 **最終一致性** [^22]。
+不幸的是,如果應用程式從 **非同步** 追隨者讀取,如果追隨者已落後,它可能會看到過時的資訊。這導致資料庫中出現明顯的不一致:如果你同時在領導者和追隨者上執行相同的查詢,你可能會得到不同的結果,因為並非所有寫入都已反映在追隨者中。這種不一致只是一種臨時狀態——如果你停止向資料庫寫入並等待一段時間,追隨者最終將趕上並與領導者保持一致。因此,這種效果被稱為 **最終一致性** [^22]。
--------
> [!NOTE]
-> 術語 **最終一致性** 由 Douglas Terry 等人創造 [^23],由 Werner Vogels 推廣 [^24],併成為許多 NoSQL 專案的戰鬥口號。然而,不僅 NoSQL 資料庫是最終一致的:非同步複製的關係資料庫中的從節點具有相同的特徵。
+> 術語 **最終一致性** 由 Douglas Terry 等人創造 [^23],由 Werner Vogels 推廣 [^24],併成為許多 NoSQL 專案的戰鬥口號。然而,不僅 NoSQL 資料庫是最終一致的:非同步複製的關係資料庫中的追隨者具有相同的特徵。
--------
-術語"最終"是故意模糊的:一般來說,副本可以落後多遠沒有限制。在正常操作中,寫入發生在主節點上並反映在從節點上之間的延遲——**複製延遲**——可能只是幾分之一秒,在實踐中不會被注意到。然而,如果系統在接近容量執行或網路中存在問題,延遲可以輕易增加到幾秒甚至幾分鐘。
+術語"最終"是故意模糊的:一般來說,副本可以落後多遠沒有限制。在正常操作中,寫入發生在領導者上並反映在追隨者上之間的延遲——**複製延遲**——可能只是幾分之一秒,在實踐中不會被注意到。然而,如果系統在接近容量執行或網路中存在問題,延遲可以輕易增加到幾秒甚至幾分鐘。
當延遲如此之大時,它引入的不一致不僅僅是一個理論問題,而是應用程式的真正問題。在本節中,我們將重點介紹複製延遲時可能發生的三個問題示例。我們還將概述解決它們的一些方法。
### 讀己之寫 {#sec_replication_ryw}
-許多應用程式讓使用者提交一些資料,然後檢視他們提交的內容。這可能是客戶資料庫中的記錄,或討論執行緒上的評論,或其他類似的東西。提交新資料時,必須將其傳送到主節點,但當用戶檢視資料時,可以從從節點讀取。如果資料經常被檢視但只是偶爾被寫入,這尤其合適。
+許多應用程式讓使用者提交一些資料,然後檢視他們提交的內容。這可能是客戶資料庫中的記錄,或討論執行緒上的評論,或其他類似的東西。提交新資料時,必須將其傳送到領導者,但當用戶檢視資料時,可以從追隨者讀取。如果資料經常被檢視但只是偶爾被寫入,這尤其合適。
使用非同步複製,存在一個問題,如 [圖 6-3](/tw/ch6#fig_replication_read_your_writes) 所示:如果使用者在寫入後不久檢視資料,新資料可能尚未到達副本。對使用者來說,看起來他們提交的資料丟失了,所以他們自然會不高興。
@@ -235,39 +235,39 @@ breadcrumbs: false
在這種情況下,我們需要 **寫後讀一致性**,也稱為 **讀己之寫一致性** [^23]。這是一種保證,如果使用者重新載入頁面,他們將始終看到他們自己提交的任何更新。它不對其他使用者做出承諾:其他使用者的更新可能直到稍後才可見。然而,它向用戶保證他們自己的輸入已正確儲存。
-我們如何在基於主節點的複製系統中實現寫後讀一致性?有各種可能的技術。提及其中幾個:
+我們如何在基於領導者的複製系統中實現寫後讀一致性?有各種可能的技術。下面舉幾個例子:
-* 當讀取使用者可能已修改的內容時,從主節點或同步更新的從節點讀取;否則,從非同步更新的從節點讀取。這要求你有某種方法知道某物是否可能已被修改,而無需實際查詢它。例如,社交網路上的使用者個人資料資訊通常只能由個人資料的所有者編輯,而不能由其他任何人編輯。因此,一個簡單的規則是:始終從主節點讀取使用者自己的個人資料,從從節點讀取任何其他使用者的個人資料。
-* 如果應用程式中的大多數東西都可能被使用者編輯,那種方法將不會有效,因為大多數東西都必須從主節點讀取(否定了讀擴充套件的好處)。在這種情況下,可以使用其他標準來決定是否從主節點讀取。例如,你可以跟蹤上次更新的時間,並在上次更新後的一分鐘內,使所有讀取都來自主節點 [^25]。你還可以監控從節點上的複製延遲,並防止在落後主節點超過一分鐘的任何從節點上進行查詢。
+* 當讀取使用者可能已修改的內容時,從領導者或同步更新的追隨者讀取;否則,從非同步更新的追隨者讀取。這要求你有某種方法知道某物是否可能已被修改,而無需實際查詢它。例如,社交網路上的使用者個人資料資訊通常只能由個人資料的所有者編輯,而不能由其他任何人編輯。因此,一個簡單的規則是:始終從領導者讀取使用者自己的個人資料,從追隨者讀取任何其他使用者的個人資料。
+* 如果應用程式中的大多數東西都可能被使用者編輯,那種方法將不會有效,因為大多數東西都必須從領導者讀取(否定了讀擴充套件的好處)。在這種情況下,可以使用其他標準來決定是否從領導者讀取。例如,你可以跟蹤上次更新的時間,並在上次更新後的一分鐘內,使所有讀取都來自領導者 [^25]。你還可以監控追隨者上的複製延遲,並防止在落後領導者超過一分鐘的任何追隨者上進行查詢。
* 客戶端可以記住其最近寫入的時間戳——然後系統可以確保為該使用者提供任何讀取的副本至少反映該時間戳之前的更新。如果副本不夠最新,則可以由另一個副本處理讀取,或者查詢可以等待直到副本趕上 [^26]。時間戳可以是 **邏輯時間戳**(指示寫入順序的東西,例如日誌序列號)或實際系統時鐘(在這種情況下,時鐘同步變得至關重要;見 ["不可靠的時鐘"](/tw/ch9#sec_distributed_clocks))。
-* 如果你的副本分佈在各個地區(為了地理上接近使用者或為了可用性),還有額外的複雜性。任何需要由主節點提供的請求都必須路由到包含主節點的地區。
+* 如果你的副本分佈在各個地區(為了地理上接近使用者或為了可用性),還有額外的複雜性。任何需要由領導者提供的請求都必須路由到包含領導者的地區。
當同一使用者從多個裝置訪問你的服務時,會出現另一個複雜情況,例如桌面網路瀏覽器和移動應用程式。在這種情況下,你可能希望提供 **跨裝置** 寫後讀一致性:如果使用者在一個裝置上輸入一些資訊,然後在另一個裝置上檢視它,他們應該看到他們剛剛輸入的資訊。
在這種情況下,需要考慮一些額外的問題:
* 需要記住使用者上次更新的時間戳的方法變得更加困難,因為在一個裝置上執行的程式碼不知道在另一個裝置上發生了什麼更新。此元資料將需要集中化。
-* 如果你的副本分佈在不同的地區,則無法保證來自不同裝置的連線將路由到同一地區。(例如,如果使用者的臺式計算機使用家庭寬頻連線,而他們的移動裝置使用蜂窩資料網路,則裝置的網路路由可能完全不同。)如果你的方法需要從主節點讀取,你可能首先需要將來自使用者所有裝置的請求路由到同一地區。
+* 如果你的副本分佈在不同的地區,則無法保證來自不同裝置的連線將路由到同一地區。(例如,如果使用者的臺式計算機使用家庭寬頻連線,而他們的移動裝置使用蜂窩資料網路,則裝置的網路路由可能完全不同。)如果你的方法需要從領導者讀取,你可能首先需要將來自使用者所有裝置的請求路由到同一地區。
--------
-> ![TIP] 地區和可用區
+> [!TIP] 地區和可用區
>
-> 我們使用術語 **地區** 來指代單個地理位置中的一個或多個數據中心。雲提供商在同一地理區域中定位多個數據中心。每個資料中心被稱為 **可用區** 或簡稱 **區域**。因此,單個雲區域由多個區域組成。每個區域是位於獨立物理設施中的獨立資料中心,具有自己的電源、冷卻等。
+> 我們用 **地區**(region)來指代一個地理位置中的一組資料中心。雲服務提供商通常會在同一地區部署多個數據中心,每個資料中心稱為 **可用區**(availability zone,簡稱 AZ)。因此,一個地區由多個可用區組成;每個可用區都是獨立的物理設施,具有自己的供電、製冷等基礎設施。
>
-> 同一地區的區域透過非常高速的網路連線連線。延遲足夠低,以至於大多數分散式系統可以在同一地區的多個區域中執行節點,就好像它們在單個區域中一樣。多區域配置允許分散式系統在一個區域離線的區域中斷中倖存,但它們不能防止所有區域不可用的區域中斷。為了在區域中斷中倖存,分散式系統必須部署在多個地區,這可能導致更高的延遲、更低的吞吐量和增加的雲網絡賬單。我們將在 ["多主複製拓撲"](/tw/ch6#sec_replication_topologies) 中更多地討論這些權衡。現在,只要知道當我們說地區時,我們指的是單個地理位置中的區域/資料中心集合。
+> 同一地區內各可用區通常透過高速網路互聯,延遲足夠低,因此大多數分散式系統可以把同一地區內的多個可用區近似看作一個機房。多可用區部署可以抵禦單個可用區故障,但無法抵禦整個地區不可用。要應對地區級中斷,系統必須跨多個地區部署,這通常會帶來更高延遲、更低吞吐和更高的雲網絡費用。我們將在 ["多主複製拓撲"](/tw/ch6#sec_replication_topologies) 中進一步討論這些權衡。這裡你只需記住:本書所說的“地區”,是同一地理位置內多個可用區(資料中心)的集合。
--------
### 單調讀 {#sec_replication_monotonic_reads}
-從非同步從節點讀取時可能發生的第二個異常示例是,使用者可能會看到事物 **在時間上倒退**。
+從非同步追隨者讀取時可能發生的第二個異常示例是,使用者可能會看到事物 **在時間上倒退**。
-如果使用者從不同的副本進行多次讀取,就可能發生這種情況。例如,[圖 6-4](/tw/ch6#fig_replication_monotonic_reads) 顯示使用者 2345 進行相同的查詢兩次,首先到延遲很小的從節點,然後到延遲更大的從節點。(如果使用者重新整理網頁,並且每個請求都路由到隨機伺服器,這種情況很可能發生。)第一個查詢返回使用者 1234 最近新增的評論,但第二個查詢沒有返回任何內容,因為滯後的從節點尚未獲取該寫入。實際上,第二個查詢觀察到的系統狀態比第一個查詢更早的時間點。如果第一個查詢沒有返回任何內容,這不會那麼糟糕,因為使用者 2345 可能不知道使用者 1234 最近添加了評論。然而,如果使用者 2345 首先看到使用者 1234 的評論出現,然後又看到它消失,這對使用者 2345 來說非常令人困惑。
+如果使用者從不同的副本進行多次讀取,就可能發生這種情況。例如,[圖 6-4](/tw/ch6#fig_replication_monotonic_reads) 顯示使用者 2345 進行相同的查詢兩次,首先到延遲很小的追隨者,然後到延遲更大的追隨者。(如果使用者重新整理網頁,並且每個請求都路由到隨機伺服器,這種情況很可能發生。)第一個查詢返回使用者 1234 最近新增的評論,但第二個查詢沒有返回任何內容,因為滯後的追隨者尚未獲取該寫入。實際上,第二個查詢觀察到的系統狀態比第一個查詢更早的時間點。如果第一個查詢沒有返回任何內容,這不會那麼糟糕,因為使用者 2345 可能不知道使用者 1234 最近添加了評論。然而,如果使用者 2345 首先看到使用者 1234 的評論出現,然後又看到它消失,這對使用者 2345 來說非常令人困惑。
{{< figure src="/fig/ddia_0604.png" id="fig_replication_monotonic_reads" caption="圖 6-4. 使用者首先從新鮮副本讀取,然後從陳舊副本讀取。時間似乎倒退了。為了防止這種異常,我們需要單調讀。" class="w-full my-4" >}}
-**單調讀** [^22] 是一種保證這種異常不會發生的保證。它是比強一致性更弱的保證,但比最終一致性更強的保證。當你讀取資料時,你可能會看到一個舊值;單調讀只意味著如果一個使用者按順序進行多次讀取,他們不會看到時間倒退——即,在之前讀取較新資料後,他們不會讀取較舊的資料。
+**單調讀** [^22] 是一種保證這類異常不會發生的會話保證。它比強一致性弱,但比最終一致性強。當你讀取資料時,仍可能看到舊值;單調讀只保證同一使用者按順序進行多次讀取時,不會出現“時間倒退”——也就是先讀到新值,後又讀到更舊的值。
實現單調讀的一種方法是確保每個使用者始終從同一副本進行讀取(不同的使用者可以從不同的副本讀取)。例如,可以基於使用者 ID 的雜湊選擇副本,而不是隨機選擇。然而,如果該副本失敗,使用者的查詢將需要重新路由到另一個副本。
@@ -283,7 +283,7 @@ Cake 夫人
這兩個句子之間存在因果依賴關係:Cake 夫人聽到了 Poons 先生的問題並回答了它。
-現在,想象第三個人透過從節點聽這個對話。Cake 夫人說的話透過延遲很小的從節點,但 Poons 先生說的話有更長的複製延遲(見 [圖 6-5](/tw/ch6#fig_replication_consistent_prefix))。這個觀察者會聽到以下內容:
+現在,想象第三個人透過追隨者聽這個對話。Cake 夫人說的話透過延遲很小的追隨者,但 Poons 先生說的話有更長的複製延遲(見 [圖 6-5](/tw/ch6#fig_replication_consistent_prefix))。這個觀察者會聽到以下內容:
Cake 夫人
: 通常大約十秒鐘,Poons 先生。
@@ -299,13 +299,13 @@ Poons 先生
這是分片(分割槽)資料庫中的一個特殊問題,我們將在 [第 7 章](/tw/ch7#ch_sharding) 中討論。如果資料庫始終以相同的順序應用寫入,讀取始終會看到一致的字首,因此這種異常不會發生。然而,在許多分散式資料庫中,不同的分片獨立執行,因此沒有全域性的寫入順序:當用戶從資料庫讀取時,他們可能會看到資料庫的某些部分處於較舊狀態,而某些部分處於較新狀態。
-一種解決方案是確保任何因果相關的寫入都寫入同一分片——但在某些應用程式中,這無法有效完成。還有一些演算法明確跟蹤因果依賴關係,這是我們將在 [""先發生"關係與併發"](/tw/ch6#sec_replication_happens_before) 中回到的主題。
+一種解決方案是確保任何因果相關的寫入都寫入同一分片——但在某些應用程式中,這無法有效完成。還有一些演算法明確跟蹤因果依賴關係,這是我們將在 ["先發生關係與併發"](/tw/ch6#sec_replication_happens_before) 中回到的主題。
### 複製延遲的解決方案 {#id131}
-在使用最終一致系統時,值得思考如果複製延遲增加到幾分鐘甚至幾小時,應用程式的行為如何。如果答案是"沒問題",那很好。然而,如果結果對使用者來說是糟糕的體驗,那麼設計系統以提供更強的保證(如寫後讀)很重要。明明是非同步複製,卻假裝它是同步的,這為日後出問題埋下了隱患。
+在使用最終一致系統時,值得思考:如果複製延遲上升到幾分鐘甚至幾小時,應用程式會如何表現。如果答案是“沒問題”,那很好;但如果這會造成糟糕的使用者體驗,就應當設計系統提供更強的保證(如寫後讀一致性)。把非同步複製當作同步複製來假設,往往會在系統承壓時暴露問題。
-如前所述,應用程式可以提供比底層資料庫更強的保證——例如,透過在主節點或同步更新的從節點上執行某些型別的讀取。然而,在應用程式程式碼中處理這些問題很複雜且容易出錯。
+如前所述,應用程式可以提供比底層資料庫更強的保證——例如,透過在領導者或同步更新的追隨者上執行某些型別的讀取。然而,在應用程式程式碼中處理這些問題很複雜且容易出錯。
對於應用程式開發人員來說,最簡單的程式設計模型是選擇一個為副本提供強一致性保證的資料庫,例如線性一致性(見 [第 10 章](/tw/ch10#ch_consistency))和 ACID 事務(見 [第 8 章](/tw/ch8#ch_transactions))。這允許你大部分忽略複製帶來的挑戰,並將資料庫視為只有一個節點。在 2010 年代初期,**NoSQL** 運動推廣了這樣的觀點,即這些功能限制了可伸縮性,大規模系統必須接受最終一致性。
@@ -317,43 +317,43 @@ Poons 先生
## 多主複製 {#sec_replication_multi_leader}
-到目前為止,本章中我們只考慮了使用單個主節點的複製架構。儘管這是一種常見的方法,但還有一些有趣的替代方案。
+到目前為止,本章中我們只考慮了使用單個領導者的複製架構。儘管這是一種常見的方法,但還有一些有趣的替代方案。
-單主複製有一個主要缺點:所有寫入都必須透過一個主節點。如果由於任何原因無法連線到主節點,例如你和主節點之間的網路中斷,你就無法寫入資料庫。
+單主複製有一個主要缺點:所有寫入都必須透過一個領導者。如果由於任何原因無法連線到領導者,例如你和領導者之間的網路中斷,你就無法寫入資料庫。
-單主複製模型的自然擴充套件是允許多個節點接受寫入。複製仍然以相同的方式進行:每個處理寫入的節點必須將該資料變更轉發給所有其他節點。我們稱之為 **多主** 配置(也稱為 **主動/主動** 或 **雙向** 複製)。在這種設定中,每個主節點同時充當其他主節點的從節點。
+單主複製模型的自然擴充套件是允許多個節點接受寫入。複製仍然以相同的方式進行:每個處理寫入的節點必須將該資料變更轉發給所有其他節點。我們稱之為 **多主** 配置(也稱為 **主動/主動** 或 **雙向** 複製)。在這種設定中,每個領導者同時充當其他領導者的追隨者。
-與單主複製一樣,可以選擇使其同步或非同步。假設你有兩個主節點,*A* 和 *B*,你正在嘗試寫入 *A*。如果寫入從 *A* 同步複製到 *B*,並且兩個節點之間的網路中斷,你就無法寫入 *A* 直到網路恢復。同步多主複製因此給你一個非常類似於單主複製的模型,即如果你讓 *B* 成為主節點,*A* 只是將任何寫入請求轉發給 *B* 執行。
+與單主複製一樣,可以選擇使其同步或非同步。假設你有兩個領導者,*A* 和 *B*,你正在嘗試寫入 *A*。如果寫入從 *A* 同步複製到 *B*,並且兩個節點之間的網路中斷,你就無法寫入 *A* 直到網路恢復。同步多主複製因此給你一個非常類似於單主複製的模型,即如果你讓 *B* 成為領導者,*A* 只是將任何寫入請求轉發給 *B* 執行。
-因此,我們不會進一步討論同步多主複製,而只是將其視為等同於單主複製。本節的其餘部分專注於非同步多主複製,其中任何主節點都可以處理寫入,即使其與其他主節點的連線中斷。
+因此,我們不會進一步討論同步多主複製,而只是將其視為等同於單主複製。本節的其餘部分專注於非同步多主複製,其中任何領導者都可以處理寫入,即使其與其他領導者的連線中斷。
### 跨地域執行 {#sec_replication_multi_dc}
在單個地區內使用多主設定很少有意義,因為好處很少超過增加的複雜性。然而,在某些情況下,這種配置是合理的。
-想象你有一個數據庫,在幾個不同的地區有副本(也許是為了能夠容忍整個地區的故障,或者是為了更接近你的使用者)。這被稱為 **地理分散式**、**地域分散式** 或 **地域複製** 設定。使用單主複製,主節點必須在 **一個** 地區,所有寫入都必須透過該地區。
+想象你有一個數據庫,在幾個不同的地區有副本(也許是為了能夠容忍整個地區的故障,或者是為了更接近你的使用者)。這被稱為 **地理分散式**、**地域分散式** 或 **地域複製** 設定。使用單主複製,領導者必須在 **一個** 地區,所有寫入都必須透過該地區。
-在多主配置中,你可以在 **每個** 地區都有一個主節點。[圖 6-6](/tw/ch6#fig_replication_multi_dc) 顯示了這種架構可能的樣子。在每個地區內,使用常規的主從複製(從節點可能在與主節點不同的可用區中);在地區之間,每個地區的主節點將其變更復制到其他地區的主節點。
+在多主配置中,你可以在 **每個** 地區都部署一個領導者。[圖 6-6](/tw/ch6#fig_replication_multi_dc) 展示了這種架構:在每個地區內使用常規單主複製(追隨者可能位於與領導者不同的可用區);在地區之間,每個地區的領導者把變更復制給其他地區的領導者。
{{< figure src="/fig/ddia_0606.png" id="fig_replication_multi_dc" caption="圖 6-6. 跨多個地區的多主複製。" class="w-full my-4" >}}
讓我們比較單主和多主配置在多地區部署中的表現:
效能
-: 在單主配置中,每次寫入都必須透過網際網路到擁有主節點的地區。這可能會給寫入增加顯著的延遲,並可能違背首先擁有多個地區的目的。在多主配置中,每次寫入都可以在本地地區處理,並非同步複製到其他地區。因此,跨地區網路延遲對使用者是隱藏的,這意味著感知效能可能更好。
+: 在單主配置中,每次寫入都必須透過網際網路到擁有領導者的地區。這可能會給寫入增加顯著的延遲,並可能違背首先擁有多個地區的目的。在多主配置中,每次寫入都可以在本地地區處理,並非同步複製到其他地區。因此,跨地區網路延遲對使用者是隱藏的,這意味著感知效能可能更好。
地區故障容忍
-: 在單主配置中,如果擁有主節點的地區變得不可用,故障轉移可以將另一個地區的從節點提升為主節點。在多主配置中,每個地區可以獨立於其他地區繼續執行,並在離線地區恢復上線時趕上覆制。
+: 在單主配置中,如果擁有領導者的地區變得不可用,故障轉移可以將另一個地區的追隨者提升為領導者。在多主配置中,每個地區可以獨立於其他地區繼續執行,並在離線地區恢復上線時趕上覆制。
網路問題容忍
-: 即使有專用連線,地區之間的流量也可能比同一地區內或單個區域內的流量更不可靠。單主配置對這種跨地區鏈路中的問題非常敏感,因為當一個地區的客戶端想要寫入另一個地區的主節點時,它必須透過該鏈路傳送其請求並等待響應才能完成。
+: 即使有專用連線,地區之間的流量也可能比同一地區內或單個區域內的流量更不可靠。單主配置對這種跨地區鏈路中的問題非常敏感,因為當一個地區的客戶端想要寫入另一個地區的領導者時,它必須透過該鏈路傳送其請求並等待響應才能完成。
- 具有非同步複製的多主配置可以更好地容忍網路問題:在臨時網路中斷期間,每個地區的主節點可以繼續獨立處理寫入。
+ 具有非同步複製的多主配置可以更好地容忍網路問題:在臨時網路中斷期間,每個地區的領導者可以繼續獨立處理寫入。
一致性
-: 單主系統可以提供強一致性保證,例如可序列化事務,我們將在 [第 8 章](/tw/ch8#ch_transactions) 中討論。多主系統的最大缺點是它們能夠實現的一致性要弱得多。例如,你不能保證銀行賬戶不會變成負數或使用者名稱是唯一的:不同的主節點總是可能處理單獨沒問題的寫入(從賬戶中支付一些錢,註冊特定使用者名稱),但當與另一個主節點上的另一個寫入結合時違反了約束。
+: 單主系統可以提供強一致性保證,例如可序列化事務,我們將在 [第 8 章](/tw/ch8#ch_transactions) 中討論。多主系統的最大缺點是它們能夠實現的一致性要弱得多。例如,你不能保證銀行賬戶不會變成負數或使用者名稱是唯一的:不同的領導者總是可能處理單獨沒問題的寫入(從賬戶中支付一些錢,註冊特定使用者名稱),但當與另一個領導者上的另一個寫入結合時違反了約束。
- 這只是分散式系統的基本限制 [^28]。如果你需要強制執行此類約束,因此你最好使用單主系統。然而,正如我們將在 ["處理寫入衝突"](/tw/ch6#sec_replication_write_conflicts) 中看到的,多主系統仍然可以實現在不需要此類約束的廣泛應用程式中有用的一致性屬性。
+ 這只是分散式系統的基本限制 [^28]。如果你必須強制執行這類約束,通常應選擇單主系統。不過,正如我們將在 ["處理寫入衝突"](/tw/ch6#sec_replication_write_conflicts) 中看到的,多主系統在不需要這類約束的廣泛應用裡,仍然可以提供有用的一致性屬性。
多主複製不如單主複製常見,但許多資料庫仍然支援它,包括 MySQL、Oracle、SQL Server 和 YugabyteDB。在某些情況下,它是一個外部附加功能,例如在 Redis Enterprise、EDB Postgres Distributed 和 pglogical 中 [^29]。
@@ -361,11 +361,11 @@ Poons 先生
#### 多主複製拓撲 {#sec_replication_topologies}
-**複製拓撲** 描述了寫入從一個節點傳播到另一個節點的通訊路徑。如果你有兩個主節點,如 [圖 6-9](/tw/ch6#fig_replication_write_conflict) 中,只有一種合理的拓撲:主節點 1 必須將其所有寫入傳送到主節點 2,反之亦然。有了兩個以上的主節點,各種不同的拓撲是可能的。[圖 6-7](/tw/ch6#fig_replication_topologies) 中說明了一些示例。
+**複製拓撲** 描述了寫入從一個節點傳播到另一個節點的通訊路徑。如果你有兩個領導者,如 [圖 6-9](/tw/ch6#fig_replication_write_conflict) 中,只有一種合理的拓撲:領導者 1 必須將其所有寫入傳送到領導者 2,反之亦然。有了兩個以上的領導者,各種不同的拓撲是可能的。[圖 6-7](/tw/ch6#fig_replication_topologies) 中說明了一些示例。
{{< figure src="/fig/ddia_0607.png" id="fig_replication_topologies" caption="圖 6-7. 可以設定多主複製的三個示例拓撲。" class="w-full my-4" >}}
-最通用的拓撲是 **全對全**,如 [圖 6-7](/tw/ch6#fig_replication_topologies)(c) 所示,其中每個主節點將其寫入傳送到每個其他主節點。然而,也使用更受限制的拓撲:例如 **環形拓撲**,其中每個節點從一個節點接收寫入並將這些寫入(加上其自己的任何寫入)轉發到另一個節點。另一種流行的拓撲具有 **星形** 形狀:一個指定的根節點將寫入轉發到所有其他節點。星形拓撲可以推廣到樹形。
+最通用的拓撲是 **全對全**,如 [圖 6-7](/tw/ch6#fig_replication_topologies)(c) 所示,其中每個領導者將其寫入傳送到每個其他領導者。然而,也使用更受限制的拓撲:例如 **環形拓撲**,其中每個節點從一個節點接收寫入並將這些寫入(加上其自己的任何寫入)轉發到另一個節點。另一種流行的拓撲具有 **星形** 形狀:一個指定的根節點將寫入轉發到所有其他節點。星形拓撲可以推廣到樹形。
--------
@@ -384,9 +384,9 @@ Poons 先生
{{< figure src="/fig/ddia_0608.png" id="fig_replication_causality" caption="圖 6-8. 使用多主複製,寫入可能以錯誤的順序到達某些副本。" class="w-full my-4" >}}
-在 [圖 6-8](/tw/ch6#fig_replication_causality) 中,客戶端 A 在主節點 1 上向表中插入一行,客戶端 B 在主節點 3 上更新該行。然而,主節點 2 可能以不同的順序接收寫入:它可能首先接收更新(從其角度來看,這是對資料庫中不存在的行的更新),然後才接收相應的插入(應該在更新之前)。
+在 [圖 6-8](/tw/ch6#fig_replication_causality) 中,客戶端 A 在領導者 1 上向表中插入一行,客戶端 B 在領導者 3 上更新該行。然而,領導者 2 可能以不同的順序接收寫入:它可能首先接收更新(從其角度來看,這是對資料庫中不存在的行的更新),然後才接收相應的插入(應該在更新之前)。
-這是一個因果關係問題,類似於我們在 ["一致字首讀"](/tw/ch6#sec_replication_consistent_prefix) 中看到的問題:更新依賴於先前的插入,因此我們需要確保所有節點首先處理插入,然後處理更新。簡單地為每個寫入附加時間戳是不夠的,因為時鐘不能被信任足夠同步以在主節點 2 上正確排序這些事件(見 [第 9 章](/tw/ch9#ch_distributed))。
+這是一個因果關係問題,類似於我們在 ["一致字首讀"](/tw/ch6#sec_replication_consistent_prefix) 中看到的問題:更新依賴於先前的插入,因此我們需要確保所有節點首先處理插入,然後處理更新。簡單地為每個寫入附加時間戳是不夠的,因為時鐘不能被信任足夠同步以在領導者 2 上正確排序這些事件(見 [第 9 章](/tw/ch9#ch_distributed))。
為了正確排序這些事件,可以使用一種稱為 **版本向量** 的技術,我們將在本章後面討論(見 ["檢測併發寫入"](/tw/ch6#sec_replication_concurrent))。然而,許多多主複製系統不使用良好的技術來排序更新,使它們容易受到像 [圖 6-8](/tw/ch6#fig_replication_causality) 中的問題的影響。如果你使用多主複製,值得了解這些問題,仔細閱讀文件,並徹底測試你的資料庫,以確保它真正提供你認為它具有的保證。
@@ -396,7 +396,7 @@ Poons 先生
例如,考慮你的手機、筆記型電腦和其他裝置上的日曆應用程式。你需要能夠隨時檢視你的會議(進行讀取請求)並輸入新會議(進行寫入請求),無論你的裝置當前是否有網際網路連線。如果你在離線時進行任何更改,它們需要在裝置下次上線時與伺服器和你的其他裝置同步。
-在這種情況下,每個裝置都有一個充當主節點的本地資料庫副本(它接受寫入請求),並且在你所有裝置上的日曆副本之間有一個非同步多主複製過程(同步)。複製延遲可能是幾小時甚至幾天,具體取決於你何時有可用的網際網路訪問。
+在這種情況下,每個裝置都擁有一個充當領導者的本地資料庫副本(可接受寫入),並在你所有裝置上的日曆副本之間執行非同步多主複製流程(即同步過程)。複製延遲可能是幾小時甚至幾天,具體取決於你何時能連上網際網路。
從架構的角度來看,這種設定與地區之間的多主複製非常相似,達到了極端:每個裝置是一個"地區",它們之間的網路連線極其不可靠。
@@ -428,37 +428,37 @@ Poons 先生
### 處理寫入衝突 {#sec_replication_write_conflicts}
-多主複製的最大問題——無論是在地域分散式伺服器端資料庫中還是在終端使用者裝置上的本地優先同步引擎中——是不同主節點上的併發寫入可能導致需要解決的衝突。
+多主複製的最大問題——無論是在地域分散式伺服器端資料庫中還是在終端使用者裝置上的本地優先同步引擎中——是不同領導者上的併發寫入可能導致需要解決的衝突。
-例如,考慮一個維基頁面同時被兩個使用者編輯,如 [圖 6-9](/tw/ch6#fig_replication_write_conflict) 所示。使用者 1 將頁面標題從 A 更改為 B,使用者 2 獨立地將標題從 A 更改為 C。每個使用者的更改成功應用於其本地主節點。然而,當更改非同步複製時,檢測到衝突。這個問題在單主資料庫中不會發生。
+例如,考慮一個維基頁面同時被兩個使用者編輯,如 [圖 6-9](/tw/ch6#fig_replication_write_conflict) 所示。使用者 1 將頁面標題從 A 更改為 B,使用者 2 獨立地將標題從 A 更改為 C。每個使用者的更改成功應用於其本地領導者。然而,當更改非同步複製時,檢測到衝突。這個問題在單主資料庫中不會發生。
-{{< figure src="/fig/ddia_0609.png" id="fig_replication_write_conflict" caption="圖 6-9. 兩個主節點併發更新同一記錄導致的寫入衝突。" class="w-full my-4" >}}
+{{< figure src="/fig/ddia_0609.png" id="fig_replication_write_conflict" caption="圖 6-9. 兩個領導者併發更新同一記錄導致的寫入衝突。" class="w-full my-4" >}}
> [!NOTE]
-> 我們說 [圖 6-9](/tw/ch6#fig_replication_write_conflict) 中的兩個寫入是 **併發的**,因為在最初進行寫入時,兩者都不"知道"另一個。寫入是否真的在同一時間發生並不重要;實際上,如果寫入是在離線時進行的,它們實際上可能相隔一段時間。重要的是一個寫入是否發生在另一個寫入已經生效的狀態下。
+> 我們說 [圖 6-9](/tw/ch6#fig_replication_write_conflict) 中的兩個寫入是 **併發的**,因為在最初進行寫入時,兩者都不“知道”對方。寫入是否真的在同一時刻發生並不重要;實際上,如果寫入發生在離線狀態,它們在物理時間上可能相隔很久。關鍵在於:一個寫入是否發生在另一個寫入已經生效的狀態之上。
在 ["檢測併發寫入"](/tw/ch6#sec_replication_concurrent) 中,我們將解決資料庫如何確定兩個寫入是否併發的問題。現在我們假設我們可以檢測衝突,並且我們想找出解決它們的最佳方法。
#### 衝突避免 {#conflict-avoidance}
-衝突的一種策略是首先避免它們發生。例如,如果應用程式可以確保特定記錄的所有寫入都透過同一主節點,那麼即使整個資料庫是多主的,也不會發生衝突。這種方法在同步引擎客戶端離線更新的情況下是不可能的,但在地域複製的伺服器系統中有時是可能的 [^30]。
+衝突的一種策略是首先避免它們發生。例如,如果應用程式可以確保特定記錄的所有寫入都透過同一領導者,那麼即使整個資料庫是多主的,也不會發生衝突。這種方法在同步引擎客戶端離線更新的情況下是不可能的,但在地域複製的伺服器系統中有時是可能的 [^30]。
-例如,在一個使用者只能編輯自己資料的應用程式中,你可以確保來自特定使用者的請求始終路由到同一地區,並使用該地區的主節點進行讀寫。不同的使用者可能有不同的"主"地區(可能基於與使用者的地理接近程度選擇),但從任何一個使用者的角度來看,配置本質上是單主的。
+例如,在一個使用者只能編輯自己資料的應用程式中,你可以確保來自特定使用者的請求始終路由到同一地區,並使用該地區的領導者進行讀寫。不同的使用者可能有不同的"主"地區(可能基於與使用者的地理接近程度選擇),但從任何一個使用者的角度來看,配置本質上是單主的。
-然而,有時你可能想要更改記錄的指定主節點——也許是因為一個地區不可用,你需要將流量重新路由到另一個地區,或者也許是因為使用者已經移動到不同的位置,現在更接近不同的地區。現在存在風險,即使用者在指定主節點更改正在進行時執行寫入,導致必須使用下面的方法之一解決的衝突。因此,如果你允許更改主節點,衝突避免就會失效。
+然而,有時你可能想要更改記錄的指定領導者——也許是因為一個地區不可用,你需要將流量重新路由到另一個地區,或者也許是因為使用者已經移動到不同的位置,現在更接近不同的地區。現在存在風險,即使用者在指定領導者更改正在進行時執行寫入,導致必須使用下面的方法之一解決的衝突。因此,如果你允許更改領導者,衝突避免就會失效。
-衝突避免的另一個例子:想象你想要插入新記錄並基於自增計數器為它們生成唯一 ID。如果你有兩個主節點,你可以設定它們,使得一個主節點只生成奇數,另一個只生成偶數。這樣你可以確保兩個主節點不會同時為不同的記錄分配相同的 ID。我們將在 ["ID 生成器和邏輯時鐘"](/tw/ch10#sec_consistency_logical) 中討論其他 ID 分配方案。
+衝突避免的另一個例子:想象你想要插入新記錄並基於自增計數器為它們生成唯一 ID。如果你有兩個領導者,你可以設定它們,使得一個領導者只生成奇數,另一個只生成偶數。這樣你可以確保兩個領導者不會同時為不同的記錄分配相同的 ID。我們將在 ["ID 生成器和邏輯時鐘"](/tw/ch10#sec_consistency_logical) 中討論其他 ID 分配方案。
-#### 最後寫入者勝(丟棄併發寫入) {#sec_replication_lww}
+#### 最後寫入勝利(丟棄併發寫入) {#sec_replication_lww}
-如果無法避免衝突,解決它們的最簡單方法是為每個寫入附加時間戳,並始終使用具有最大時間戳的值。例如,在 [圖 6-9](/tw/ch6#fig_replication_write_conflict) 中,假設使用者 1 的寫入時間戳大於使用者 2 的寫入時間戳。在這種情況下,兩個主節點都將確定頁面的新標題應該是 B,並丟棄將其設定為 C 的寫入。如果寫入巧合地具有相同的時間戳,可以透過比較值來選擇獲勝者(例如,在字串的情況下,取字母表中較早的那個)。
+如果無法避免衝突,解決它們的最簡單方法是為每個寫入附加時間戳,並始終使用具有最大時間戳的值。例如,在 [圖 6-9](/tw/ch6#fig_replication_write_conflict) 中,假設使用者 1 的寫入時間戳大於使用者 2 的寫入時間戳。在這種情況下,兩個領導者都將確定頁面的新標題應該是 B,並丟棄將其設定為 C 的寫入。如果寫入巧合地具有相同的時間戳,可以透過比較值來選擇獲勝者(例如,在字串的情況下,取字母表中較早的那個)。
-這種方法稱為 **最後寫入者勝**(LWW),因為具有最大時間戳的寫入可以被認為是"最後"的。然而,這個術語是誤導性的,因為當兩個寫入像 [圖 6-9](/tw/ch6#fig_replication_write_conflict) 中那樣併發時,哪個更舊,哪個更新是未定義的,因此併發寫入的時間戳順序本質上是隨機的。
+這種方法稱為 **最後寫入勝利**(LWW),因為具有最大時間戳的寫入可以被認為是"最後"的。然而,這個術語是誤導性的,因為當兩個寫入像 [圖 6-9](/tw/ch6#fig_replication_write_conflict) 中那樣併發時,哪個更舊,哪個更新是未定義的,因此併發寫入的時間戳順序本質上是隨機的。
-因此,LWW 的真正含義是:當同一記錄在不同的主節點上併發寫入時,其中一個寫入被隨機選擇為獲勝者,其他寫入被靜默丟棄,即使它們在各自的主節點上成功處理。這實現了最終所有副本都處於一致狀態的目標,但代價是資料丟失。
+因此,LWW 的真正含義是:當同一記錄在不同的領導者上併發寫入時,其中一個寫入被隨機選擇為獲勝者,其他寫入被靜默丟棄,即使它們在各自的領導者上成功處理。這實現了最終所有副本都處於一致狀態的目標,但代價是資料丟失。
-如果你可以避免衝突——例如,透過只插入具有唯一鍵(如 UUID)的記錄,而從不更新它們——那麼 LWW 沒有問題。但是,如果你更新現有記錄,或者如果不同的主節點可能插入具有相同鍵的記錄,那麼你必須決定丟失的更新對你的應用程式是否是個問題。如果丟失的更新是不可接受的,你需要使用下面描述的衝突解決方法之一。
+如果你可以避免衝突——例如,透過只插入具有唯一鍵(如 UUID)的記錄,而從不更新它們——那麼 LWW 沒有問題。但是,如果你更新現有記錄,或者如果不同的領導者可能插入具有相同鍵的記錄,那麼你必須決定丟失的更新對你的應用程式是否是個問題。如果丟失的更新是不可接受的,你需要使用下面描述的衝突解決方法之一。
LWW 的另一個問題是,如果使用即時時鐘(例如 Unix 時間戳)作為寫入的時間戳,系統對時鐘同步變得非常敏感。如果一個節點的時鐘領先於其他節點,並且你嘗試覆蓋該節點寫入的值,你的寫入可能會被忽略,因為它可能具有較低的時間戳,即使它明顯發生得更晚。這個問題可以透過使用 **邏輯時鐘** 來解決,我們將在 ["ID 生成器和邏輯時鐘"](/tw/ch10#sec_consistency_logical) 中討論。
@@ -466,13 +466,13 @@ LWW 的另一個問題是,如果使用即時時鐘(例如 Unix 時間戳)
如果隨機丟棄你的一些寫入是不可取的,下一個選擇是手動解決衝突。你可能熟悉 Git 和其他版本控制系統中的手動衝突解決:如果兩個不同分支上的提交編輯同一檔案的相同行,並且你嘗試合併這些分支,你將得到一個需要在合併完成之前解決的合併衝突。
-在資料庫中,衝突停止整個複製過程直到人類解決它是不切實際的。相反,資料庫通常儲存給定記錄的所有併發寫入值——例如,[圖 6-9](/tw/ch6#fig_replication_write_conflict) 中的 B 和 C。這些值有時稱為 **兄弟節點**。下次查詢該記錄時,資料庫返回 **所有** 這些值,而不僅僅是最新的值。然後,你可以以任何你想要的方式解決這些值,無論是在應用程式程式碼中自動(例如,你可以將 B 和 C 連線成"B/C"),還是透過詢問使用者。然後,你將新值寫回資料庫以解決衝突。
+在資料庫裡,讓衝突阻塞整個複製流程、直到人工處理,通常並不現實。更常見的是,資料庫會保留某條記錄的所有併發寫入值——例如 [圖 6-9](/tw/ch6#fig_replication_write_conflict) 中的 B 和 C。這些值有時稱為 **兄弟**。下次查詢該記錄時,資料庫會返回 **所有** 這些值,而不只是最新值。隨後你可以按需要解決這些值:要麼在應用程式碼裡自動處理(例如把 B 和 C 合併成 "B/C"),要麼讓使用者參與處理;最後再把新值寫回資料庫以消解衝突。
這種衝突解決方法在某些系統中使用,例如 CouchDB。然而,它也存在許多問題:
* 資料庫的 API 發生變化:例如,以前維基頁面的標題只是一個字串,現在它變成了一組字串,通常包含一個元素,但如果有衝突,有時可能包含多個元素。這可能使應用程式程式碼中的資料難以處理。
-* 要求使用者手動合併兄弟節點是很多工作,無論是對應用程式開發人員(需要構建衝突解決的使用者介面)還是對使用者(可能對他們被要求做什麼以及為什麼感到困惑)。在許多情況下,自動合併比打擾使用者更好。
-* 如果不仔細進行,自動合併兄弟節點可能會導致令人驚訝的行為。例如,亞馬遜的購物車曾經允許併發更新,然後透過保留出現在任何兄弟節點中的所有購物車專案(即,取購物車的集合並集)來合併。這意味著如果客戶在一個兄弟節點中從購物車中刪除了一個專案,但另一個兄弟節點仍然包含該舊專案,刪除的專案會意外地重新出現在客戶的購物車中 [^45]。[圖 6-10](/tw/ch6#fig_replication_amazon_anomaly) 顯示了一個示例,其中裝置 1 從購物車中刪除 Book,併發地裝置 2 刪除 DVD,但合併衝突後兩個專案都重新出現。
+* 要求使用者手動合併兄弟,會帶來很大負擔:開發者需要構建衝突解決介面,使用者也可能不明白自己為何要做這件事。在很多場景下,自動合併比打擾使用者更合適。
+* 如果不夠謹慎,自動合併兄弟也可能產生反直覺行為。例如,亞馬遜購物車曾允許併發更新,並用“並集”策略合併(保留出現在任一兄弟中的所有商品)。這意味著:若使用者在一個兄弟裡刪除了某商品,但另一個兄弟仍保留它,該商品會“復活”回購物車 [^45]。[圖 6-10](/tw/ch6#fig_replication_amazon_anomaly) 就是一個例子:裝置 1 刪除 Book,裝置 2 併發刪除 DVD,衝突合併後兩個商品都回來了。
* 如果多個節點觀察到衝突並併發解決它,衝突解決過程本身可能會引入新的衝突。這些解決方案甚至可能不一致:例如,如果你不小心一致地排序它們,一個節點可能將 B 和 C 合併為"B/C",另一個可能將它們合併為"C/B"。當"B/C"和"C/B"之間的衝突被合併時,它可能導致"B/C/C/B"或類似令人驚訝的東西。
{{< figure src="/fig/ddia_0610.png" id="fig_replication_amazon_anomaly" caption="圖 6-10. 亞馬遜購物車異常的示例:如果購物車上的衝突透過取並集合並,刪除的專案可能會重新出現。" class="w-full my-4" >}}
@@ -484,9 +484,9 @@ LWW 的另一個問題是,如果使用即時時鐘(例如 Unix 時間戳)
LWW 是衝突解決演算法的一個簡單示例。已經為不同型別的資料開發了更複雜的合併演算法,目標是儘可能保留所有更新的預期效果,從而避免資料丟失:
-* 如果資料是文字(例如,維基頁面的標題或正文),我們可以檢測從一個版本到下一個版本插入或刪除了哪些字元。合併的結果然後保留在任何兄弟節點中進行的所有插入和刪除。如果使用者併發地在同一位置插入文字,可以確定性地排序,以便所有節點獲得相同的合併結果。
+* 如果資料是文字(例如維基頁面標題或正文),我們可以檢測每次版本演進中的字元插入和刪除。合併結果會保留任一兄弟中的所有插入和刪除。如果多個使用者併發在同一位置插入文字,還可以用確定性順序來排序,以確保所有節點得到同樣的合併結果。
* 如果資料是專案集合(像待辦事項列表那樣有序,或像購物車那樣無序),我們可以透過跟蹤插入和刪除類似於文字來合併它。為了避免 [圖 6-10](/tw/ch6#fig_replication_amazon_anomaly) 中的購物車問題,演算法跟蹤 Book 和 DVD 被刪除的事實,因此合併的結果是 Cart = {Soap}。
-* 如果資料是表示可以遞增或遞減的計數器的整數(例如,社交媒體帖子上的點贊數),合併演算法可以告訴每個兄弟節點上發生了多少次遞增和遞減,並正確地將它們相加,以便結果不會重複計數也不會丟棄更新。
+* 如果資料是可增可減的整數計數器(例如社交媒體帖子的點贊數),合併演算法可以統計每個兄弟上的遞增和遞減次數,並正確求和,既不重複計數,也不丟更新。
* 如果資料是鍵值對映,我們可以透過將其他衝突解決演算法之一應用於該鍵下的值來合併對同一鍵的更新。對不同鍵的更新可以相互獨立處理。
衝突解決的可能性是有限的。例如,如果你想強制一個列表不包含超過五個專案,並且多個使用者併發地向列表新增專案,使得總共有五個以上,你唯一的選擇是丟棄一些專案。儘管如此,自動衝突解決足以構建許多有用的應用程式。如果你從想要構建協作離線優先或本地優先應用程式的要求開始,那麼衝突解決是不可避免的,自動化它通常是最好的方法。
@@ -515,16 +515,16 @@ OT 最常用於文字的即時協作編輯,例如在 Google Docs 中 [^32],
某些型別的衝突是顯而易見的。在 [圖 6-9](/tw/ch6#fig_replication_write_conflict) 的示例中,兩個寫入併發修改了同一記錄中的同一欄位,將其設定為兩個不同的值。毫無疑問,這是一個衝突。
-其他型別的衝突可能更難以檢測。例如,考慮一個會議室預訂系統:它跟蹤哪個房間由哪組人在什麼時間預訂。此應用程式需要確保每個房間在任何時間只由一組人預訂(即,同一房間不得有任何重疊的預訂)。在這種情況下,如果為同一房間同時建立兩個不同的預訂,可能會出現衝突。即使應用程式在允許使用者進行預訂之前檢查可用性,如果兩個預訂是在兩個不同的主節點上進行的,也可能會發生衝突。
+其他型別的衝突可能更難以檢測。例如,考慮一個會議室預訂系統:它跟蹤哪個房間由哪組人在什麼時間預訂。此應用程式需要確保每個房間在任何時間只由一組人預訂(即,同一房間不得有任何重疊的預訂)。在這種情況下,如果為同一房間同時建立兩個不同的預訂,可能會出現衝突。即使應用程式在允許使用者進行預訂之前檢查可用性,如果兩個預訂是在兩個不同的領導者上進行的,也可能會發生衝突。
-沒有快速現成的答案,但在以下章節中,我們將追蹤通向對這個問題的良好理解的路徑。我們將在 [第 8 章](/tw/ch8#ch_transactions) 中看到更多衝突的例子,並在 [Link to Come] 中討論在複製系統中檢測和解決衝突的可伸縮方法。
+沒有現成的快速答案,不過在後續章節中,我們會逐步建立對這個問題的理解。我們將在 [第 8 章](/tw/ch8#ch_transactions) 看到更多衝突案例,並在 ["透過事件順序捕獲因果關係"](/tw/ch13#sec_future_capture_causality) 中討論在複製系統裡可伸縮地檢測和解決衝突的方法。
## 無主複製 {#sec_replication_leaderless}
-到目前為止,我們在本章中討論的複製方法——單主和多主複製——都基於這樣的想法:客戶端向一個節點(主節點)傳送寫入請求,資料庫系統負責將該寫入複製到其他副本。主節點確定寫入應該處理的順序,從節點以相同的順序應用主節點的寫入。
+到目前為止,我們在本章中討論的複製方法——單主和多主複製——都基於這樣的想法:客戶端向一個節點(領導者)傳送寫入請求,資料庫系統負責將該寫入複製到其他副本。領導者確定寫入應該處理的順序,追隨者以相同的順序應用領導者的寫入。
-一些資料儲存系統採用不同的方法,放棄主節點的概念,並允許任何副本直接接受來自客戶端的寫入。一些最早的複製資料系統是無主的 [^1] [^50],但在關係資料庫主導的時代,這個想法基本上被遺忘了。在亞馬遜於 2007 年將其用於其內部 **Dynamo** 系統後,它再次成為資料庫的時尚架構 [^45]。Riak、Cassandra 和 ScyllaDB 是受 Dynamo 啟發的具有無主複製模型的開源資料儲存,因此這種資料庫也被稱為 **Dynamo 風格**。
+一些資料儲存系統採用不同的方法,放棄領導者的概念,並允許任何副本直接接受來自客戶端的寫入。一些最早的複製資料系統是無主的 [^1] [^50],但在關係資料庫主導的時代,這個想法基本上被遺忘了。在亞馬遜於 2007 年將其用於其內部 **Dynamo** 系統後,它再次成為資料庫的時尚架構 [^45]。Riak、Cassandra 和 ScyllaDB 是受 Dynamo 啟發的具有無主複製模型的開源資料儲存,因此這種資料庫也被稱為 **Dynamo 風格**。
--------
@@ -533,7 +533,7 @@ OT 最常用於文字的即時協作編輯,例如在 Google Docs 中 [^32],
--------
-在某些無主實現中,客戶端直接將其寫入傳送到多個副本,而在其他實現中,協調器節點代表客戶端執行此操作。然而,與主節點資料庫不同,該協調器不強制執行特定的寫入順序。正如我們將看到的,這種設計差異對資料庫的使用方式產生了深遠的影響。
+在某些無主實現中,客戶端直接將其寫入傳送到多個副本,而在其他實現中,協調器節點代表客戶端執行此操作。然而,與領導者資料庫不同,該協調器不強制執行特定的寫入順序。正如我們將看到的,這種設計差異對資料庫的使用方式產生了深遠的影響。
### 當節點故障時寫入資料庫 {#id287}
@@ -548,20 +548,20 @@ OT 最常用於文字的即時協作編輯,例如在 Google Docs 中 [^32],
為了解決這個問題,當客戶端從資料庫讀取時,它不只是將其請求傳送到一個副本:**讀取請求也並行傳送到多個節點**。客戶端可能會從不同的節點獲得不同的響應;例如,從一個節點獲得最新值,從另一個節點獲得陳舊值。
-為了區分哪些響應是最新的,哪些是過時的,寫入的每個值都需要用版本號或時間戳標記,類似於我們在 ["最後寫入者勝(丟棄併發寫入)"](/tw/ch6#sec_replication_lww) 中看到的。當客戶端收到對讀取的多個值響應時,它使用具有最大時間戳的值(即使該值僅由一個副本返回,而其他幾個副本返回較舊的值)。有關更多詳細資訊,請參見 ["檢測併發寫入"](/tw/ch6#sec_replication_concurrent)。
+為了區分哪些響應是最新的,哪些是過時的,寫入的每個值都需要用版本號或時間戳標記,類似於我們在 ["最後寫入勝利(丟棄併發寫入)"](/tw/ch6#sec_replication_lww) 中看到的。當客戶端收到對讀取的多個值響應時,它使用具有最大時間戳的值(即使該值僅由一個副本返回,而其他幾個副本返回較舊的值)。有關更多詳細資訊,請參見 ["檢測併發寫入"](/tw/ch6#sec_replication_concurrent)。
#### 追趕錯過的寫入 {#sec_replication_read_repair}
複製系統應確保最終所有資料都複製到每個副本。在不可用節點恢復上線後,它如何趕上它錯過的寫入?在 Dynamo 風格的資料儲存中使用了幾種機制:
讀修復
-: 當客戶端並行從多個節點進行讀取時,它可以檢測任何陳舊響應。例如,在 [圖 6-12](/tw/ch6#fig_replication_quorum_node_outage) 中,使用者 2345 從副本 3 獲得版本 6 值,從副本 1 和 2 獲得版本 7 值。客戶端看到副本 3 有陳舊值,並將較新的值寫回該副本。這種方法適用於經常讀取的值。
+: 當客戶端並行從多個節點讀取時,它可以檢測任何陳舊響應。例如,在 [圖 6-12](/tw/ch6#fig_replication_quorum_node_outage) 中,使用者 2345 從副本 3 獲得版本 6 的值,從副本 1 和 2 獲得版本 7 的值。客戶端發現副本 3 陳舊後,會把較新的值寫回該副本。這種方法適用於經常被讀取的值。
提示移交
: 如果一個副本不可用,另一個副本可能會以 **提示** 的形式代表其儲存寫入。當應該接收這些寫入的副本恢復時,儲存提示的副本將它們傳送到恢復的副本,然後刪除提示。這個 **移交** 過程有助於使副本保持最新,即使對於從未讀取的值也是如此,因此不由讀修復處理。
反熵
-: 此外,還有一個後臺程序定期查詢副本之間資料的差異,並將任何缺失的資料從一個副本複製到另一個。與基於主節點的複製中的複製日誌不同,這個 **反熵程序** 不以任何特定順序複製寫入,並且在複製資料之前可能會有顯著的延遲。
+: 此外,還有一個後臺程序定期查詢副本之間資料的差異,並將任何缺失的資料從一個副本複製到另一個。與基於領導者的複製中的複製日誌不同,這個 **反熵程序** 不以任何特定順序複製寫入,並且在複製資料之前可能會有顯著的延遲。
#### 讀寫仲裁 {#sec_replication_quorum_condition}
@@ -610,7 +610,7 @@ OT 最常用於文字的即時協作編輯,例如在 Google Docs 中 [^32],
* 在重新平衡正在進行時,其中一些資料從一個節點移動到另一個節點(見 [第 7 章](/tw/ch7#ch_sharding)),節點可能對哪些節點應該持有特定值的 *n* 個副本有不一致的檢視。這可能導致讀取和寫入仲裁不再重疊。
* 如果讀取與寫入操作併發,讀取可能會或可能不會看到併發寫入的值。特別是,一次讀取可能看到新值,而後續讀取看到舊值,正如我們將在 ["線性一致性與仲裁"](/tw/ch10#sec_consistency_quorum_linearizable) 中看到的。
* 如果寫入在某些副本上成功但在其他副本上失敗(例如,因為某些節點上的磁碟已滿),並且總體上在少於 *w* 個副本上成功,它不會在成功的副本上回滾。這意味著如果寫入被報告為失敗,後續讀取可能會或可能不會返回該寫入的值 [^52]。
-* 如果資料庫使用即時時鐘的時間戳來確定哪個寫入更新(如 Cassandra 和 ScyllaDB 所做的),如果另一個具有更快時鐘的節點已寫入同一鍵,寫入可能會被靜默丟棄——我們之前在 ["最後寫入者勝(丟棄併發寫入)"](/tw/ch6#sec_replication_lww) 中看到的問題。我們將在 ["依賴同步時鐘"](/tw/ch9#sec_distributed_clocks_relying) 中更詳細地討論這一點。
+* 如果資料庫使用即時時鐘的時間戳來確定哪個寫入更新(如 Cassandra 和 ScyllaDB 所做的),如果另一個具有更快時鐘的節點已寫入同一鍵,寫入可能會被靜默丟棄——我們之前在 ["最後寫入勝利(丟棄併發寫入)"](/tw/ch6#sec_replication_lww) 中看到的問題。我們將在 ["依賴同步時鐘"](/tw/ch9#sec_distributed_clocks_relying) 中更詳細地討論這一點。
* 如果兩個寫入併發發生,其中一個可能首先在一個副本上處理,另一個可能首先在另一個副本上處理。這導致衝突,類似於我們在多主複製中看到的(見 ["處理寫入衝突"](/tw/ch6#sec_replication_write_conflicts))。我們將在 ["檢測併發寫入"](/tw/ch6#sec_replication_concurrent) 中回到這個主題。
因此,儘管仲裁似乎保證讀取返回最新寫入的值,但實際上並不那麼簡單。Dynamo 風格的資料庫通常針對可以容忍最終一致性的用例進行了最佳化。引數 *w* 和 *r* 允許你調整讀取陳舊值的機率 [^53],但明智的做法是不要將它們視為絕對保證。
@@ -619,24 +619,24 @@ OT 最常用於文字的即時協作編輯,例如在 Google Docs 中 [^32],
從操作角度來看,監控你的資料庫是否返回最新結果很重要。即使你的應用程式可以容忍陳舊讀取,你也需要了解複製的健康狀況。如果它明顯落後,它應該提醒你,以便你可以調查原因(例如,網路中的問題或過載的節點)。
-對於基於主節點的複製,資料庫通常公開復制延遲的指標,你可以將其輸入到監控系統。這是可能的,因為寫入以相同的順序應用於主節點和從節點,每個節點在複製日誌中都有一個位置(它在本地應用的寫入數)。透過從主節點的當前位置減去從節點的當前位置,你可以測量複製延遲的量。
+對於基於領導者的複製,資料庫通常公開復制延遲的指標,你可以將其輸入到監控系統。這是可能的,因為寫入以相同的順序應用於領導者和追隨者,每個節點在複製日誌中都有一個位置(它在本地應用的寫入數)。透過從領導者的當前位置減去追隨者的當前位置,你可以測量複製延遲的量。
然而,在具有無主複製的系統中,沒有固定的寫入應用順序,這使得監控更加困難。副本為移交儲存的提示數量可以是系統健康的一個度量,但很難有用地解釋 [^54]。最終一致性是一個故意模糊的保證,但為了可操作性,能夠量化"最終"很重要。
### 單主與無主複製的效能 {#sec_replication_leaderless_perf}
-基於單個主節點的複製系統可以提供在無主系統中難以或不可能實現的強一致性保證。然而,正如我們在 ["複製延遲的問題"](/tw/ch6#sec_replication_lag) 中看到的,如果你在非同步更新的從節點上進行讀取,基於主節點的複製系統中的讀取也可能返回陳舊值。
+基於單個領導者的複製系統可以提供在無主系統中難以或不可能實現的強一致性保證。然而,正如我們在 ["複製延遲的問題"](/tw/ch6#sec_replication_lag) 中看到的,如果你在非同步更新的追隨者上進行讀取,基於領導者的複製系統中的讀取也可能返回陳舊值。
-從主節點讀取確保最新響應,但它存在效能問題:
+從領導者讀取確保最新響應,但它存在效能問題:
-* 讀取吞吐量受主節點處理請求能力的限制(與讀擴充套件相反,讀擴充套件將讀取分佈在可能返回陳舊值的非同步更新副本上)。
-* 如果主節點失敗,你必須等待檢測到故障,並在繼續處理請求之前完成故障轉移。即使故障轉移過程非常快,使用者也會因為臨時增加的響應時間而注意到它;如果故障轉移需要很長時間,系統在其持續時間內不可用。
-* 系統對主節點上的效能問題非常敏感:如果主節點響應緩慢,例如由於過載或某些資源爭用,增加的響應時間也會立即影響使用者。
+* 讀取吞吐量受領導者處理請求能力的限制(與讀擴充套件相反,讀擴充套件將讀取分佈在可能返回陳舊值的非同步更新副本上)。
+* 如果領導者失敗,你必須等待檢測到故障,並在繼續處理請求之前完成故障轉移。即使故障轉移過程非常快,使用者也會因為臨時增加的響應時間而注意到它;如果故障轉移需要很長時間,系統在其持續時間內不可用。
+* 系統對領導者上的效能問題非常敏感:如果領導者響應緩慢,例如由於過載或某些資源爭用,增加的響應時間也會立即影響使用者。
-無主架構的一大優勢是它對此類問題更有彈性。因為沒有故障轉移,並且請求無論如何都並行傳送到多個副本,一個副本變慢或不可用對響應時間的影響很小:客戶端只是使用響應更快的其他副本的響應。使用最快的響應稱為 **請求對沖**,它可以顯著減少尾部延遲 [^55])。
+無主架構的一大優勢是它對此類問題更有彈性。因為沒有故障轉移,而且請求本來就是並行發往多個副本,所以某個副本變慢或不可用對響應時間影響較小:客戶端只需採用更快副本的響應即可。利用最快響應的做法稱為 **請求對沖**,它可以顯著降低尾部延遲 [^55]。
-從根本上說,無主系統的彈性來自於它不區分正常情況和故障情況的事實。這在處理所謂的 **灰色故障** 時特別有用,其中節點沒有完全宕機,但以降級狀態執行,處理請求異常緩慢 [^56],或者當節點只是過載時(例如,如果節點已離線一段時間,透過提示移交恢復可能會導致大量額外負載)。基於主節點的系統必須決定情況是否足夠糟糕以保證故障轉移(這本身可能會導致進一步的中斷),而在無主系統中,這個問題甚至不會出現。
+從根本上說,無主系統的彈性來自於它不區分正常情況和故障情況的事實。這在處理所謂的 **灰色故障** 時特別有用,其中節點沒有完全宕機,但以降級狀態執行,處理請求異常緩慢 [^56],或者當節點只是過載時(例如,如果節點已離線一段時間,透過提示移交恢復可能會導致大量額外負載)。基於領導者的系統必須決定情況是否足夠糟糕以保證故障轉移(這本身可能會導致進一步的中斷),而在無主系統中,這個問題甚至不會出現。
也就是說,無主系統也可能有效能問題:
@@ -644,7 +644,7 @@ OT 最常用於文字的即時協作編輯,例如在 Google Docs 中 [^32],
* 你擁有的副本越多,你的仲裁就越大,在請求完成之前你必須等待的響應就越多。即使你只等待最快的 *r* 或 *w* 個副本響應,即使你並行發出請求,更大的 *r* 或 *w* 增加了你遇到慢副本的機會,增加了總體響應時間(見 ["響應時間指標的應用"](/tw/ch2#sec_introduction_slo_sla))。
* 大規模網路中斷使客戶端與大量副本斷開連線,可能使形成仲裁變得不可能。一些無主資料庫提供了一個配置選項,允許任何可訪問的副本接受寫入,即使它不是該鍵的通常副本之一(Riak 和 Dynamo 稱之為 **寬鬆仲裁** [^45];Cassandra 和 ScyllaDB 稱之為 **一致性級別 ANY**)。不能保證後續讀取會看到寫入的值,但根據應用程式,它可能仍然比寫入失敗更好。
-多主複製可以提供比無主複製更大的網路中斷彈性,因為讀取和寫入只需要與一個主節點通訊,該主節點可以與客戶端位於同一位置。然而,由於一個主節點上的寫入非同步傳播到其他主節點,讀取可能任意過時。仲裁讀取和寫入提供了一種折衷:良好的容錯性,同時也有很高的可能性讀取最新資料。
+多主複製可以提供比無主複製更大的網路中斷彈性,因為讀取和寫入只需要與一個領導者通訊,該領導者可以與客戶端位於同一位置。然而,由於一個領導者上的寫入非同步傳播到其他領導者,讀取可能任意過時。仲裁讀取和寫入提供了一種折衷:良好的容錯性,同時也有很高的可能性讀取最新資料。
#### 多地區操作 {#multi-region-operation}
@@ -669,9 +669,9 @@ Riak 將客戶端和資料庫節點之間的所有通訊保持在一個地區本
如果每個節點在接收到來自客戶端的寫入請求時只是覆蓋鍵的值,節點將變得永久不一致,如 [圖 6-14](/tw/ch6#fig_replication_concurrency) 中的最終 *get* 請求所示:節點 2 認為 *X* 的最終值是 B,而其他節點認為值是 A。
-為了最終保持一致,副本應該收斂到相同的值。為此,我們可以使用我們之前在 ["處理寫入衝突"](/tw/ch6#sec_replication_write_conflicts) 中討論的任何衝突解決機制,例如最後寫入者勝(由 Cassandra 和 ScyllaDB 使用)、手動解決或 CRDT(在 ["CRDT 與操作變換"](/tw/ch6#sec_replication_crdts) 中描述,並由 Riak 使用)。
+為了最終保持一致,副本應該收斂到相同的值。為此,我們可以使用我們之前在 ["處理寫入衝突"](/tw/ch6#sec_replication_write_conflicts) 中討論的任何衝突解決機制,例如最後寫入勝利(由 Cassandra 和 ScyllaDB 使用)、手動解決或 CRDT(在 ["CRDT 與操作變換"](/tw/ch6#sec_replication_crdts) 中描述,並由 Riak 使用)。
-最後寫入者勝很容易實現:每個寫入都標有時間戳,具有更高時間戳的值總是覆蓋具有較低時間戳的值。然而,時間戳不會告訴你兩個值是否實際上衝突(即,它們是併發寫入的)或不衝突(它們是一個接一個寫入的)。如果你想顯式解決衝突,系統需要更加小心地檢測併發寫入。
+最後寫入勝利很容易實現:每個寫入都標有時間戳,具有更高時間戳的值總是覆蓋具有較低時間戳的值。然而,時間戳不會告訴你兩個值是否實際上衝突(即,它們是併發寫入的)或不衝突(它們是一個接一個寫入的)。如果你想顯式解決衝突,系統需要更加小心地檢測併發寫入。
#### "先發生"關係與併發 {#sec_replication_happens_before}
@@ -686,7 +686,7 @@ Riak 將客戶端和資料庫節點之間的所有通訊保持在一個地區本
--------
-> ![TIP] 併發、時間和相對論
+> [!TIP] 併發、時間和相對論
>
> 似乎兩個操作如果"同時"發生,應該稱為併發——但實際上,它們是否真的在時間上重疊並不重要。由於分散式系統中的時鐘問題,實際上很難判斷兩件事是否恰好在同一時間發生——我們將在 [第 9 章](/tw/ch9#ch_distributed) 中更詳細地討論這個問題。
>
@@ -700,10 +700,10 @@ Riak 將客戶端和資料庫節點之間的所有通訊保持在一個地區本
讓我們看一個確定兩個操作是否併發或一個先發生於另一個的演算法。為了簡單起見,讓我們從只有一個副本的資料庫開始。一旦我們弄清楚如何在單個副本上執行此操作,我們就可以將該方法推廣到具有多個副本的無主資料庫。
-[圖 6-15](/tw/ch6#fig_replication_causality_single) 顯示了兩個客戶端併發地向同一購物車新增專案。(如果這個例子讓你覺得太無聊,想象一下兩個空中交通管制員併發地向他們正在跟蹤的扇區新增飛機。)最初,購物車是空的。客戶端之間向資料庫進行了五次寫入:
+[圖 6-15](/tw/ch6#fig_replication_causality_single) 顯示了兩個客戶端併發地向同一購物車新增專案。(如果這個例子讓你覺得太無聊,想象一下兩個空中交通管制員併發地向他們正在跟蹤的扇區新增飛機。)最初,購物車是空的。兩個客戶端總共向資料庫發起了五次寫入:
1. 客戶端 1 將 `milk` 新增到購物車。這是對該鍵的第一次寫入,因此伺服器成功儲存它併為其分配版本 1。伺服器還將值連同版本號一起回顯給客戶端。
-2. 客戶端 2 將 `eggs` 新增到購物車,不知道客戶端 1 併發地添加了 `milk`(客戶端 2 認為它的 `eggs` 是購物車中的唯一專案)。伺服器為此寫入分配版本 2,並將 `eggs` 和 `milk` 儲存為兩個單獨的值(兄弟節點)。然後,它將 **兩個** 值連同版本號 2 一起返回給客戶端。
+2. 客戶端 2 將 `eggs` 新增到購物車,不知道客戶端 1 併發地添加了 `milk`(客戶端 2 認為它的 `eggs` 是購物車中的唯一專案)。伺服器為此寫入分配版本 2,並將 `eggs` 和 `milk` 儲存為兩個單獨的值(兄弟)。然後,它將 **兩個** 值連同版本號 2 一起返回給客戶端。
3. 客戶端 1,不知道客戶端 2 的寫入,想要將 `flour` 新增到購物車,因此它認為當前購物車內容應該是 `[milk, flour]`。它將此值連同伺服器之前給客戶端 1 的版本號 1 一起傳送到伺服器。伺服器可以從版本號判斷 `[milk, flour]` 的寫入取代了 `[milk]` 的先前值,但它與 `[eggs]` 併發。因此,伺服器將版本 3 分配給 `[milk, flour]`,覆蓋版本 1 值 `[milk]`,但保留版本 2 值 `[eggs]` 並將兩個剩餘值返回給客戶端。
4. 同時,客戶端 2 想要將 `ham` 新增到購物車,不知道客戶端 1 剛剛添加了 `flour`。客戶端 2 在上次響應中從伺服器接收了兩個值 `[milk]` 和 `[eggs]`,因此客戶端現在合併這些值並新增 `ham` 以形成新值 `[eggs, milk, ham]`。它將該值連同先前的版本號 2 一起傳送到伺服器。伺服器檢測到版本 2 覆蓋 `[eggs]` 但與 `[milk, flour]` 併發,因此兩個剩餘值是版本 3 的 `[milk, flour]` 和版本 4 的 `[eggs, milk, ham]`。
5. 最後,客戶端 1 想要新增 `bacon`。它之前從伺服器接收了版本 3 的 `[milk, flour]` 和 `[eggs]`,因此它合併這些,新增 `bacon`,並將最終值 `[milk, flour, eggs, bacon]` 連同版本號 3 一起傳送到伺服器。這覆蓋了 `[milk, flour]`(注意 `[eggs]` 已經在上一步中被覆蓋)但與 `[eggs, milk, ham]` 併發,因此伺服器保留這兩個併發值。
@@ -719,23 +719,23 @@ Riak 將客戶端和資料庫節點之間的所有通訊保持在一個地區本
請注意,伺服器可以透過檢視版本號來確定兩個操作是否併發——它不需要解釋值本身(因此值可以是任何資料結構)。演算法的工作原理如下:
* 伺服器為每個鍵維護一個版本號,每次寫入該鍵時遞增版本號,並將新版本號與寫入的值一起儲存。
-* 當客戶端讀取鍵時,伺服器返回所有兄弟節點,即所有未被覆蓋的值,以及最新的版本號。客戶端必須在寫入之前讀取鍵。
-* 當客戶端寫入鍵時,它必須包含來自先前讀取的版本號,並且必須合併它在先前讀取中收到的所有值,例如使用 CRDT 或透過詢問使用者。寫入請求的響應就像讀取一樣,返回所有兄弟節點,這允許我們像購物車示例中那樣連結多個寫入。
+* 當客戶端讀取鍵時,伺服器返回所有兄弟,即所有未被覆蓋的值,以及最新的版本號。客戶端必須在寫入之前讀取鍵。
+* 當客戶端寫入鍵時,它必須包含來自先前讀取的版本號,並且必須合併它在先前讀取中收到的所有值,例如使用 CRDT 或透過詢問使用者。寫入請求的響應就像讀取一樣,返回所有兄弟,這允許我們像購物車示例中那樣連結多個寫入。
* 當伺服器接收到具有特定版本號的寫入時,它可以覆蓋具有該版本號或更低版本號的所有值(因為它知道它們已合併到新值中),但它必須保留具有更高版本號的所有值(因為這些值與傳入寫入併發)。
當寫入包含來自先前讀取的版本號時,這告訴我們寫入基於哪個先前狀態。如果你在不包含版本號的情況下進行寫入,它與所有其他寫入併發,因此它不會覆蓋任何內容——它只會作為後續讀取的值之一返回。
#### 版本向量 {#version-vectors}
-[圖 6-15](/tw/ch6#fig_replication_causality_single) 中的示例僅使用了單個副本。當有多個副本但沒有主節點時,演算法如何變化?
+[圖 6-15](/tw/ch6#fig_replication_causality_single) 中的示例只使用了單個副本。當存在多個副本、且沒有領導者時,演算法如何變化?
-[圖 6-15](/tw/ch6#fig_replication_causality_single) 使用單個版本號來捕獲操作之間的依賴關係,但當有多個副本併發接受寫入時,這是不夠的。相反,我們需要使用 **每個副本** 以及每個鍵的版本號。每個副本在處理寫入時遞增其自己的版本號,並且還跟蹤它從其他每個副本看到的版本號。此資訊指示要覆蓋哪些值以及保留哪些值作為兄弟節點。
+[圖 6-15](/tw/ch6#fig_replication_causality_single) 使用單個版本號來捕獲操作間依賴關係,但當多個副本併發接受寫入時,這還不夠。我們需要為 **每個副本**、每個鍵分別維護版本號。每個副本在處理寫入時遞增自己的版本號,並追蹤從其他副本看到的版本號。這些資訊決定了哪些值該被覆蓋,哪些值要作為兄弟保留。
-來自所有副本的版本號集合稱為 **版本向量** [^58]。正在使用此想法的幾個變體,但最有趣的可能是 **點化版本向量** [^59] [^60],它在 Riak 2.0 中使用 [^61] [^62]。我們不會詳細介紹,但它的工作方式與我們在購物車示例中看到的非常相似。
+來自所有副本的版本號集合稱為 **版本向量** [^58]。這一思想有若干變體,其中較有代表性的是 **點版本向量** [^59] [^60],Riak 2.0 使用了它 [^61] [^62]。這裡不展開細節,它的工作方式與前面的購物車示例非常相似。
-像 [圖 6-15](/tw/ch6#fig_replication_causality_single) 中的版本號一樣,版本向量在讀取值時從資料庫副本傳送到客戶端,並且在隨後寫入值時需要傳送回資料庫。(Riak 將版本向量編碼為它稱為 **因果上下文** 的字串。)版本向量允許資料庫區分覆蓋和併發寫入。
+和 [圖 6-15](/tw/ch6#fig_replication_causality_single) 裡的版本號一樣,版本向量會在讀取時由資料庫副本返回給客戶端,並在後續寫入時再由客戶端帶回資料庫。(Riak 把版本向量編碼成一個字串,稱為 **因果上下文**。)版本向量讓資料庫能夠區分“覆蓋寫入”和“併發寫入”。
-版本向量還確保從一個副本讀取然後寫回另一個副本是安全的。這樣做可能會導致建立兄弟節點,但只要正確合併兄弟節點,就不會丟失資料。
+版本向量還保證了“從一個副本讀取,再寫回另一個副本”是安全的。這樣做可能會產生兄弟,但只要正確合併兄弟,就不會丟失資料。
--------
@@ -766,17 +766,17 @@ Riak 將客戶端和資料庫節點之間的所有通訊保持在一個地區本
我們討論了三種主要的複製方法:
**單主複製**
-: 客戶端將所有寫入傳送到單個節點(主節點),該節點將資料變更事件流傳送到其他副本(從節點)。讀取可以在任何副本上執行,但從從節點讀取可能是陳舊的。
+: 客戶端將所有寫入傳送到單個節點(領導者),該節點將資料變更事件流傳送到其他副本(追隨者)。讀取可以在任何副本上執行,但從追隨者讀取可能是陳舊的。
**多主複製**
-: 客戶端將每個寫入傳送到幾個主節點之一,任何主節點都可以接受寫入。主節點相互發送資料變更事件流,併發送到任何從節點。
+: 客戶端將每個寫入傳送到幾個領導者之一,任何領導者都可以接受寫入。領導者相互發送資料變更事件流,併發送到任何追隨者。
**無主複製**
: 客戶端將每個寫入傳送到多個節點,並行從多個節點讀取,以檢測和糾正具有陳舊資料的節點。
每種方法都有優缺點。單主複製很受歡迎,因為它相當容易理解,並且提供強一致性。多主和無主複製在存在故障節點、網路中斷和延遲峰值時可以更加健壯——代價是需要衝突解決並提供較弱的一致性保證。
-複製可以是同步的或非同步的,這對系統在出現故障時的行為有深遠的影響。儘管非同步複製在系統平穩執行時可能很快,但重要的是要弄清楚當複製延遲增加和伺服器失敗時會發生什麼。如果主節點失敗並且你將非同步更新的從節點提升為新的主節點,最近提交的資料可能會丟失。
+複製可以是同步的或非同步的,這對系統在出現故障時的行為有深遠的影響。儘管非同步複製在系統平穩執行時可能很快,但重要的是要弄清楚當複製延遲增加和伺服器失敗時會發生什麼。如果領導者失敗並且你將非同步更新的追隨者提升為新的領導者,最近提交的資料可能會丟失。
我們研究了複製延遲可能導致的一些奇怪效果,並討論了一些有助於決定應用程式在複製延遲下應如何表現的一致性模型:
@@ -789,7 +789,7 @@ Riak 將客戶端和資料庫節點之間的所有通訊保持在一個地區本
**一致字首讀**
: 使用者應該看到處於因果意義狀態的資料:例如,按正確順序看到問題及其回覆。
-最後,我們討論了多主和無主複製如何確保所有副本最終收斂到一致狀態:透過使用版本向量或類似演算法來檢測哪些寫入是併發的,並透過使用衝突解決演算法(如 CRDT)來合併併發寫入的值。最後寫入者勝和手動衝突解決也是可能的。
+最後,我們討論了多主和無主複製如何確保所有副本最終收斂到一致狀態:透過使用版本向量或類似演算法來檢測哪些寫入是併發的,並透過使用衝突解決演算法(如 CRDT)來合併併發寫入的值。最後寫入勝利和手動衝突解決也是可能的。
本章假設每個副本都儲存整個資料庫的完整副本,這對於大型資料集是不現實的。在下一章中,我們將研究 **分片**,它允許每臺機器只儲存資料的子集。
diff --git a/content/tw/ch7.md b/content/tw/ch7.md
index 22f7498..bec85d4 100644
--- a/content/tw/ch7.md
+++ b/content/tw/ch7.md
@@ -4,6 +4,8 @@ weight: 207
breadcrumbs: false
---
+
+

> *顯然,我們必須跳出順序計算機指令的窠臼。我們必須敘述定義、提供優先順序和資料描述。我們必須敘述關係,而不是過程。*
@@ -19,9 +21,9 @@ breadcrumbs: false
分片通常與複製結合使用,以便每個分片的副本儲存在多個節點上。這意味著,即使每條記錄屬於恰好一個分片,它仍然可以儲存在多個不同的節點上以提供容錯能力。
-一個節點可能儲存多個分片。如果使用單主複製模型,分片和複製的組合可能看起來像 [圖 7-1](/tw/ch7#fig_sharding_replicas),例如。每個分片的主節點被分配給一個節點,其從節點被分配給其他節點。每個節點可能是某些分片的主節點,同時是其他分片的從節點。
+一個節點可能儲存多個分片。例如,如果使用單領導者複製模型,分片與複製的組合可能如 [圖 7-1](/tw/ch7#fig_sharding_replicas) 所示。每個分片的領導者被分配到一個節點,追隨者被分配到其他節點。每個節點可能是某些分片的領導者,同時又是其他分片的追隨者,但每個分片仍然只有一個領導者。
-{{< figure src="/fig/ddia_0701.png" id="fig_sharding_replicas" caption="圖 7-1. 結合複製和分片:每個節點充當某些分片的主節點,同時充當其他分片的從節點。" class="w-full my-4" >}}
+{{< figure src="/fig/ddia_0701.png" id="fig_sharding_replicas" caption="圖 7-1. 複製與分片結合使用:每個節點對某些分片充當領導者,對另一些分片充當追隨者。" class="w-full my-4" >}}
我們在 [第 6 章](/tw/ch6#ch_replication) 中討論的關於資料庫複製的所有內容同樣適用於分片的複製。由於分片方案的選擇大部分獨立於複製方案的選擇,為了簡單起見,我們將在本章中忽略複製。
@@ -53,7 +55,7 @@ breadcrumbs: false
分片的另一個問題是寫入可能需要更新多個不同分片中的相關記錄。雖然單節點上的事務相當常見(見 [第 8 章](/tw/ch8#ch_transactions)),但確保跨多個分片的一致性需要 *分散式事務*。正如我們將在 [第 8 章](/tw/ch8#ch_transactions) 中看到的,分散式事務在某些資料庫中可用,但它們通常比單節點事務慢得多,可能成為整個系統的瓶頸,有些系統根本不支援它們。
-一些系統即使在單臺機器上也使用分片,通常每個 CPU 核心執行一個單執行緒程序以利用 CPU 中的並行性,或者利用 *非一致性記憶體訪問*(NUMA)架構,其中某些記憶體庫比其他記憶體庫更接近某個 CPU [^5]。例如,Redis、VoltDB 和 FoundationDB 每個核心使用一個程序,並依靠分片在同一臺機器的 CPU 核心之間分散負載 [^6]。
+一些系統即使在單臺機器上也使用分片,通常每個 CPU 核心執行一個單執行緒程序,以利用 CPU 的並行性,或者利用 *非統一記憶體訪問*(NUMA)架構:某些記憶體分割槽比其他分割槽更靠近某個 CPU [^5]。例如,Redis、VoltDB 和 FoundationDB 每個核心使用一個程序,並依靠分片在同一臺機器的 CPU 核心之間分散負載 [^6]。
### 面向多租戶的分片 {#sec_sharding_multitenancy}
@@ -62,10 +64,10 @@ breadcrumbs: false
有時分片用於實現多租戶系統:要麼每個租戶被分配一個單獨的分片,要麼多個小租戶可能被分組到一個更大的分片中。這些分片可能是物理上分離的資料庫(我們之前在 ["嵌入式儲存引擎"](/tw/ch4#sidebar_embedded) 中提到過),或者是更大邏輯資料庫的可單獨管理部分 [^7]。使用分片實現多租戶有幾個優點:
資源隔離
-: 如果一個租戶執行計算密集型操作,如果它們在不同的分片上執行,其他租戶的效能受影響的可能性較小。
+: 如果某個租戶執行計算密集型操作,而它與其他租戶執行在不同分片上,那麼其他租戶效能受影響的可能性更小。
許可權隔離
-: 如果你的訪問控制邏輯中存在錯誤,如果這些租戶的資料集彼此物理分離儲存,你意外地給一個租戶訪問另一個租戶資料的可能性較小。
+: 如果訪問控制邏輯有漏洞,而租戶資料集又是彼此物理隔離儲存的,那麼誤將一個租戶的資料暴露給另一個租戶的機率會更低。
基於單元的架構
: 你不僅可以在資料儲存級別應用分片,還可以為執行應用程式程式碼的服務應用分片。在 *基於單元的架構* 中,特定租戶集的服務和儲存被分組到一個自包含的 *單元* 中,不同的單元被設定為可以在很大程度上彼此獨立執行。這種方法提供了 *故障隔離*:即,一個單元中的故障僅限於該單元,其他單元中的租戶不受影響 [^8]。
@@ -86,7 +88,7 @@ breadcrumbs: false
* 它假設每個單獨的租戶都足夠小,可以適應單個節點。如果情況並非如此,並且你有一個對於一臺機器來說太大的租戶,你將需要在單個租戶內額外執行分片,這將我們帶回到為可伸縮性進行分片的主題 [^12]。
* 如果你有許多小租戶,那麼為每個租戶建立單獨的分片可能會產生太多開銷。你可以將幾個小租戶組合到一個更大的分片中,但隨後你會遇到如何在租戶增長時將其從一個分片移動到另一個分片的問題。
-* 如果你需要支援跨多個租戶連線資料的功能,如果你需要跨多個分片連線資料,這些功能將變得更難實現。
+* 如果你需要支援跨多個租戶關聯資料的功能,那麼在必須跨多個分片做連線時,實現難度會顯著增加。
@@ -96,7 +98,7 @@ breadcrumbs: false
我們進行分片的目標是將資料和查詢負載均勻地分佈在各節點上。如果每個節點承擔公平的份額,那麼理論上——10 個節點應該能夠處理 10 倍的資料量和 10 倍單個節點的讀寫吞吐量(忽略複製)。此外,如果我們新增或刪除節點,我們希望能夠 *再平衡* 負載,使其在新增時均勻分佈在 11 個節點上(或刪除時在剩餘的 9 個節點上)。
-如果分片不公平,使得某些分片比其他分片有更多的資料或查詢,我們稱之為 *傾斜*。傾斜的存在使分片的效果大打折扣。在極端情況下,所有負載可能最終集中在一個分片上,因此 10 個節點中有 9 個處於空閒狀態,你的瓶頸是單個繁忙的節點。具有不成比例高負載的分片稱為 *熱分片* 或 *熱點*。如果有一個鍵具有特別高的負載(例如,社交網路中的名人),我們稱之為 *熱鍵*。
+如果分片不公平,使得某些分片比其他分片承載更多資料或查詢,我們稱之為 *偏斜*。偏斜會顯著削弱分片效果。在極端情況下,所有負載都可能集中在一個分片上,導致 10 個節點中有 9 個處於空閒狀態,而瓶頸落在那一個繁忙節點上。負載明顯高於其他分片的分片稱為 *熱分片* 或 *熱點*。如果某個鍵的負載特別高(例如社交網路中的名人),我們稱之為 *熱鍵*。
因此,我們需要一種演算法,它以記錄的分割槽鍵作為輸入,並告訴我們該記錄在哪個分片中。在鍵值儲存中,分割槽鍵通常是鍵,或鍵的第一部分。在關係模型中,分割槽鍵可能是表的某一列(不一定是其主鍵)。該演算法需要能夠進行再平衡以緩解熱點。
@@ -136,7 +138,7 @@ breadcrumbs: false
鍵範圍分片在你希望具有相鄰(但不同)分割槽鍵的記錄被分組到同一個分片中時很有用;例如,如果是時間戳,這可能就是這種情況。如果你不關心分割槽鍵是否彼此接近(例如,如果它們是多租戶應用程式中的租戶 ID),一種常見方法是先對分割槽鍵進行雜湊,然後將其對映到分片。
-一個好的雜湊函式接受傾斜的資料並使其均勻分佈。假設你有一個 32 位雜湊函式,它接受一個字串。每當你給它一個新字串時,它返回一個介於 0 和 2³² − 1 之間的看似隨機的數字。即使輸入字串非常相似,它們的雜湊值也會均勻分佈在該數字範圍內(但相同的輸入總是產生相同的輸出)。
+一個好的雜湊函式可以把偏斜的資料變得更均勻。假設你有一個 32 位雜湊函式,輸入是字串。每當給它一個新字串,它都會返回一個看似隨機、介於 0 和 2³² − 1 之間的數字。即使輸入字串非常相似,它們的雜湊值也會在這個範圍內均勻分佈(但相同輸入總是產生相同輸出)。
出於分片目的,雜湊函式不需要是密碼學強度的:例如,MongoDB 使用 MD5,而 Cassandra 和 ScyllaDB 使用 Murmur3。許多程式語言都內建了簡單的雜湊函式(因為它們用於雜湊表),但它們可能不適合分片:例如,在 Java 的 `Object.hashCode()` 和 Ruby 的 `Object#hash` 中,相同的鍵在不同的程序中可能有不同的雜湊值,使它們不適合分片 [^16]。
@@ -166,7 +168,7 @@ breadcrumbs: false
如果你發現最初配置的分片數量是錯誤的——例如,如果你已經達到需要比分片更多節點的規模——那麼需要進行昂貴的重新分片操作。它需要分割每個分片並將其寫入新檔案,在此過程中使用大量額外的磁碟空間。一些系統不允許在併發寫入資料庫時進行重新分片,這使得在沒有停機時間的情況下更改分片數量變得困難。
-如果資料集的總大小高度可變(例如,如果它開始很小但可能隨時間增長得更大),選擇正確的分片數量是困難的。由於每個分片包含總資料的固定部分,每個分片的大小與叢集中的總資料量成比例增長。如果分片非常大,再平衡和從節點故障恢復會變得昂貴。但如果分片太小,它們會產生太多開銷。當分片大小"恰到好處"時可以實現最佳效能,既不太大也不太小,如果分片數量固定但資料集大小變化,這可能很難實現。
+如果資料集總大小高度可變(例如起初很小,但會隨時間顯著增長),選擇合適的分片數量就很困難。由於每個分片包含總資料中的固定比例,每個分片的大小會隨叢集總資料量按比例增長。如果分片很大,再平衡和節點故障恢復都會很昂貴;但如果分片太小,又會產生過多管理開銷。最佳效能通常出現在分片大小“恰到好處”時,但在分片數量固定、資料規模又持續變化的情況下,這很難做到。
#### 按雜湊範圍分片 {#sharding-by-hash-range}
@@ -205,15 +207,15 @@ breadcrumbs: false
Cassandra 和 ScyllaDB 使用的分片演算法類似於一致性雜湊的原始定義 [^20],但也提出了其他幾種一致性雜湊演算法 [^21],如 *最高隨機權重*,也稱為 *會合雜湊* [^22],以及 *跳躍一致性雜湊* [^23]。使用 Cassandra 的演算法,如果新增一個節點,少量現有分片會被分割成子範圍;另一方面,使用會合和跳躍一致性雜湊,新節點被分配之前分散在所有其他節點中的單個鍵。哪種更可取取決於應用程式。
-### 傾斜的工作負載與緩解熱點 {#sec_sharding_skew}
+### 偏斜的工作負載與緩解熱點 {#sec_sharding_skew}
-一致性雜湊確保鍵在節點間均勻分佈,但這並不意味著實際負載是均勻分佈的。如果工作負載高度傾斜——即某些分割槽鍵下的資料量遠大於其他鍵,或者對某些鍵的請求率遠高於其他鍵——你仍然可能最終導致某些伺服器過載,而其他伺服器幾乎處於空閒狀態。
+一致性雜湊保證鍵在節點間大致均勻分佈,但這並不等於實際負載也均勻分佈。如果工作負載高度偏斜,即某些分割槽鍵下的資料量遠大於其他鍵,或某些鍵的請求速率遠高於其他鍵,那麼你仍可能出現部分伺服器過載、其他伺服器幾乎空閒的情況。
例如,在社交媒體網站上,擁有數百萬粉絲的名人使用者在做某事時可能會引起活動風暴 [^24]。這個事件可能導致對同一個鍵的大量讀寫(其中分割槽鍵可能是名人的使用者 ID,或者人們正在評論的動作的 ID)。
在這種情況下,需要更靈活的分片策略 [^25] [^26]。基於鍵範圍(或雜湊範圍)定義分片的系統使得可以將單個熱鍵放在自己的分片中,甚至可能為其分配專用機器 [^27]。
-也可以在應用程式級別補償傾斜。例如,如果已知一個鍵非常熱,一個簡單的技術是在鍵的開頭或結尾新增一個隨機數。僅僅一個兩位數的十進位制隨機數就會將對該鍵的寫入均勻分佈在 100 個不同的鍵上,允許這些鍵分佈到不同的分片。
+也可以在應用層補償偏斜。例如,如果已知某個鍵非常熱,一個簡單方法是在鍵的前後附加隨機數。僅用兩位十進位制隨機數,就可以把對該鍵的寫入均勻打散到 100 個不同鍵上,從而將它們分佈到不同分片。
然而,將寫入分散到不同的鍵之後,任何讀取現在都必須做額外的工作,因為它們必須從所有 100 個鍵讀取資料並將其組合。對熱鍵每個分片的讀取量沒有減少;只有寫入負載被分割。這種技術還需要額外的記賬:只對少數熱鍵附加隨機數是有意義的;對於寫入吞吐量低的絕大多數鍵,這將是不必要的開銷。因此,你還需要某種方法來跟蹤哪些鍵正在被分割,以及將常規鍵轉換為特殊管理的熱鍵的過程。
@@ -221,7 +223,7 @@ Cassandra 和 ScyllaDB 使用的分片演算法類似於一致性雜湊的原始
一些系統(特別是為大規模設計的雲服務)有自動處理熱分片的方法;例如,Amazon 稱之為 *熱管理* [^28] 或 *自適應容量* [^17]。這些系統如何工作的細節超出了本書的範圍。
-### 運維:自動/手動再均衡 {#sec_sharding_operations}
+### 運維:自動/手動再平衡 {#sec_sharding_operations}
關於再平衡有一個我們已經忽略的重要問題:分片的分割和再平衡是自動發生還是手動發生?
@@ -267,7 +269,7 @@ Cassandra、ScyllaDB 和 Riak 採用不同的方法:它們在節點之間使
當使用路由層或向隨機節點發送請求時,客戶端仍然需要找到要連線的 IP 地址。這些不像分片到節點的分配那樣快速變化,因此通常使用 DNS 就足夠了。
-這個關於請求路由的討論集中在查詢單個鍵的分片,這對於分片 OLTP 資料庫最相關。分析資料庫通常也使用分片,但它們通常有非常不同型別的查詢執行:查詢通常需要並行聚合和連線來自許多不同分片的資料,而不是在單個分片中執行。我們將在 [連結待定] 中討論這種並行查詢執行的技術。
+上面對請求路由的討論,主要關注如何為單個鍵找到對應分片,這對分片 OLTP 資料庫最相關。分析型資料庫通常也使用分片,但其查詢執行模型很不一樣:查詢往往需要並行聚合並連線來自多個分片的資料,而不是在單個分片內執行。我們將在 ["JOIN 和 GROUP BY"](/tw/ch11#sec_batch_join) 中討論這類並行查詢執行技術。
## 分片與二級索引 {#sec_sharding_secondary_indexes}
@@ -275,17 +277,17 @@ Cassandra、ScyllaDB 和 Riak 採用不同的方法:它們在節點之間使
如果涉及二級索引,情況會變得更加複雜(另見 ["多列和二級索引"](/tw/ch4#sec_storage_index_multicolumn))。二級索引通常不唯一地標識記錄,而是一種搜尋特定值出現的方法:查詢使用者 `123` 的所有操作、查詢包含單詞 `hogwash` 的所有文章、查詢顏色為 `red` 的所有汽車等。
-鍵值儲存通常沒有二級索引,但它們是關係資料庫的基礎,在文件資料庫中也很常見,它們是 Solr 和 Elasticsearch 等搜尋引擎的 *存在理由*。二級索引的問題是它們不能整齊地對映到分片。有兩種主要方法來使用二級索引對資料庫進行分片:本地索引和全域性索引。
+鍵值儲存通常沒有二級索引;但在關係資料庫中,二級索引是基礎能力,在文件資料庫中也很常見,而且它們正是 Solr、Elasticsearch 等全文檢索引擎的 *立身之本*。二級索引的難點在於,它們不能整齊地對映到分片。帶二級索引的分片資料庫主要有兩種做法:本地索引與全域性索引。
### 本地二級索引 {#id166}
例如,假設你正在運營一個出售二手車的網站(如 [圖 7-9](/tw/ch7#fig_sharding_local_secondary) 所示)。每個列表都有一個唯一的 ID——稱之為文件 ID——你使用該 ID 作為分割槽鍵對資料庫進行分片(例如,ID 0 到 499 在分片 0 中,ID 500 到 999 在分片 1 中,等等)。
-如果你想讓使用者搜尋汽車,允許他們按顏色和製造商過濾,你需要在 `color` 和 `make` 上建立二級索引(在文件資料庫中這些是欄位;在關係資料庫中這些是列)。如果你已宣告索引,資料庫可以自動執行索引。例如,每當將紅色汽車新增到資料庫時,資料庫分片會自動將其 ID 新增到索引條目 `color:red` 的文件 ID 列表中。如 [第 4 章](/tw/ch4#ch_storage) 中所討論的,該 ID 列表也稱為 *釋出列表*。
+如果你想讓使用者搜尋汽車,允許他們按顏色和製造商過濾,你需要在 `color` 和 `make` 上建立二級索引(在文件資料庫中這些是欄位;在關係資料庫中這些是列)。如果你已宣告索引,資料庫就可以自動維護索引。例如,每當一輛紅色汽車被寫入資料庫,所在分片會自動將其 ID 加入索引條目 `color:red` 對應的文件 ID 列表。正如 [第 4 章](/tw/ch4#ch_storage) 所述,這個 ID 列表也稱為 *倒排列表*。
{{< figure src="/fig/ddia_0709.png" id="fig_sharding_local_secondary" caption="圖 7-9. 本地二級索引:每個分片只索引其自己分片內的記錄。" class="w-full my-4" >}}
-> [!WARN] 警告
+> [!WARNING] 警告
如果你的資料庫只支援鍵值模型,你可能會嘗試透過在應用程式程式碼中建立從值到文件 ID 的對映來自己實現二級索引。如果你走這條路,你需要格外小心,確保你的索引與底層資料保持一致。競態條件和間歇性寫入失敗(其中某些更改已儲存但其他更改未儲存)很容易導致資料不同步——見 ["多物件事務的需求"](/tw/ch8#sec_transactions_need)。
@@ -297,7 +299,7 @@ Cassandra、ScyllaDB 和 Riak 採用不同的方法:它們在節點之間使
但是,如果你想要所有結果並且事先不知道它們的分割槽鍵,你需要將查詢傳送到所有分片,並組合你收到的結果,因為匹配的記錄可能分散在所有分片中。在 [圖 7-9](/tw/ch7#fig_sharding_local_secondary) 中,紅色汽車出現在分片 0 和分片 1 中。
-這種查詢分片資料庫的方法有時稱為 *分散/聚集*,它可能使二級索引上的讀取查詢相當昂貴。即使並行查詢分片,分散/聚集也容易導致尾部延遲放大(見 ["響應時間指標的使用"](/tw/ch2#sec_introduction_slo_sla))。它還限制了應用程式的可伸縮性:新增更多分片讓你儲存更多資料,但如果每個分片無論如何都必須處理每個查詢,它不會增加你的查詢吞吐量。
+這種查詢分片資料庫的方法有時稱為 *分散/收集*(scatter/gather),它可能使二級索引讀取變得相當昂貴。即使並行查詢各分片,分散/收集也容易導致尾部延遲放大(見 ["響應時間指標的使用"](/tw/ch2#sec_introduction_slo_sla))。它還會限制應用的可伸縮性:增加分片可以提升可儲存資料量,但若每個查詢仍需所有分片參與,查詢吞吐量並不會隨分片數增加而提升。
儘管如此,本地二級索引被廣泛使用 [^31]:例如,MongoDB、Riak、Cassandra [^32]、Elasticsearch [^33]、SolrCloud 和 VoltDB [^34] 都使用本地二級索引。
@@ -313,13 +315,13 @@ Cassandra、ScyllaDB 和 Riak 採用不同的方法:它們在節點之間使
全域性索引使用詞項作為分割槽鍵,因此當你查詢特定詞項或值時,你可以找出需要查詢哪個分片。和以前一樣,分片可以包含連續的詞項範圍(如 [圖 7-10](/tw/ch7#fig_sharding_global_secondary)),或者你可以基於詞項的雜湊將詞項分配給分片。
-全域性索引的優點是具有單個條件的查詢(如 *color = red*)只需要從單個分片讀取以獲取釋出列表。但是,如果你想獲取記錄而不僅僅是 ID,你仍然必須從負責這些 ID 的所有分片中讀取。
+全域性索引的優點是,只有一個查詢條件時(如 *color = red*),只需從一個分片讀取即可獲得倒排列表。但如果你不僅要 ID,還要取回完整記錄,仍然必須去負責這些 ID 的各個分片讀取。
-如果你有多個搜尋條件或詞項(例如,搜尋某種顏色和某種製造商的汽車,或搜尋同一文字中出現的多個單詞),很可能這些詞項將被分配給不同的分片。要計算兩個條件的邏輯 AND,系統需要找到兩個釋出列表中都出現的所有 ID。如果釋出列表很短,這沒問題,但如果它們很長,透過網路傳送它們來計算它們的交集可能會很慢 [^30]。
+如果你有多個搜尋條件或詞項(例如搜尋某種顏色且某個製造商的汽車,或搜尋同一文字中出現的多個單詞),這些詞項很可能會落在不同分片。要計算兩個條件的邏輯 AND,系統需要找出同時出現在兩個倒排列表中的 ID。若倒排列表較短,這沒問題;但若很長,把它們透過網路傳送後再算交集就可能很慢 [^30]。
全域性二級索引的另一個挑戰是寫入比本地索引更複雜,因為寫入單個記錄可能會影響索引的多個分片(文件中的每個詞項可能在不同的分片或不同的節點上)。這使得二級索引與底層資料保持同步更加困難。一種選擇是使用分散式事務來原子地更新儲存主記錄的分片及其二級索引(見 [第 8 章](/tw/ch8#ch_transactions))。
-全域性二級索引被 CockroachDB、TiDB 和 YugabyteDB 使用;DynamoDB 支援本地和全域性二級索引。在 DynamoDB 的情況下,寫入非同步反映在全域性索引中,因此從全域性索引讀取可能是陳舊的(類似於複製延遲,如 ["複製延遲的問題"](/tw/ch6#sec_replication_lag))。儘管如此,如果讀取吞吐量高於寫入吞吐量,並且釋出列表不太長,全域性索引是有用的。
+全域性二級索引被 CockroachDB、TiDB 和 YugabyteDB 使用;DynamoDB 同時支援本地與全域性二級索引。在 DynamoDB 中,寫入會非同步反映到全域性索引,因此從全域性索引讀取到的結果可能是陳舊的(類似複製延遲,見 ["複製延遲的問題"](/tw/ch6#sec_replication_lag))。儘管如此,在讀吞吐量高於寫吞吐量且倒排列表不太長的場景下,全域性索引仍然很有價值。
## 總結 {#summary}
@@ -330,10 +332,13 @@ Cassandra、ScyllaDB 和 Riak 採用不同的方法:它們在節點之間使
我們討論了兩種主要的分片方法:
-* *鍵範圍分片*,其中鍵是有序的,分片擁有從某個最小值到某個最大值的所有鍵。排序的優點是可以進行高效的範圍查詢,但如果應用程式經常訪問排序順序中彼此接近的鍵,則存在熱點風險。
+**鍵範圍分片**
+: 其中鍵是有序的,分片擁有從某個最小值到某個最大值的所有鍵。排序的優點是可以進行高效的範圍查詢,但如果應用程式經常訪問排序順序中彼此接近的鍵,則存在熱點風險。
在這種方法中,當分片變得太大時,通常透過將範圍分成兩個子範圍來動態重新平衡分片。
-* *雜湊分片*,其中對每個鍵應用雜湊函式,分片擁有一個雜湊值範圍(或者可以使用另一種一致性雜湊演算法將雜湊對映到分片)。這種方法破壞了鍵的順序,使範圍查詢效率低下,但可能更均勻地分佈負載。
+
+**雜湊分片**
+: 其中對每個鍵應用雜湊函式,分片擁有一個雜湊值範圍(或者可以使用另一種一致性雜湊演算法將雜湊對映到分片)。這種方法破壞了鍵的順序,使範圍查詢效率低下,但可能更均勻地分佈負載。
當按雜湊分片時,通常預先建立固定數量的分片,為每個節點分配多個分片,並在新增或刪除節點時將整個分片從一個節點移動到另一個節點。像鍵範圍一樣分割分片也是可能的。
@@ -341,17 +346,20 @@ Cassandra、ScyllaDB 和 Riak 採用不同的方法:它們在節點之間使
我們還討論了分片和二級索引之間的互動。二級索引也需要進行分片,有兩種方法:
-* *本地二級索引*,其中二級索引與主鍵和值儲存在同一個分片中。這意味著寫入時只需要更新一個分片,但二級索引的查詢需要從所有分片讀取。
-* *全域性二級索引*,它們基於索引值單獨分片。二級索引中的條目可能引用來自主鍵所有分片的記錄。寫入記錄時,可能需要更新多個二級索引分片;但是,可以從單個分片提供釋出列表的讀取(獲取實際記錄仍需要從多個分片讀取)。
+**本地二級索引**
+: 其中二級索引與主鍵和值儲存在同一個分片中。這意味著寫入時只需要更新一個分片,但二級索引的查詢需要從所有分片讀取。
-最後,我們討論了將查詢路由到適當分片的技術,以及協調服務通常用於跟蹤分片到節點的分配的方式。
+**全域性二級索引**
+: 它們基於索引值單獨分片。二級索引中的條目可能引用來自主鍵所有分片的記錄。寫入記錄時,可能需要更新多個二級索引分片;但讀取倒排列表時,可以由單個分片提供(獲取實際記錄仍需從多個分片讀取)。
-按設計,每個分片主要獨立執行——這就是允許分片資料庫擴充套件到多臺機器的原因。但是,需要寫入多個分片的操作可能會有問題:例如,如果對一個分片的寫入成功,但對另一個分片的寫入失敗,會發生什麼?我們將在以下章節中解決該問題。
+最後,我們討論了將查詢路由到正確分片的技術,以及如何藉助協調服務維護分片到節點的分配資訊。
+
+按設計,每個分片大體獨立執行,這正是分片資料庫能夠擴充套件到多臺機器的原因。然而,凡是需要同時寫多個分片的操作都會變得棘手:例如,一個分片寫入成功、另一個分片寫入失敗時會發生什麼?這個問題將在後續章節中討論。
-### References
+### 參考
[^1]: Claire Giordano. [Understanding partitioning and sharding in Postgres and Citus](https://www.citusdata.com/blog/2023/08/04/understanding-partitioning-and-sharding-in-postgres-and-citus/). *citusdata.com*, August 2023. Archived at [perma.cc/8BTK-8959](https://perma.cc/8BTK-8959)
[^2]: Brandur Leach. [Partitioning in Postgres, 2022 edition](https://brandur.org/fragments/postgres-partitioning-2022). *brandur.org*, October 2022. Archived at [perma.cc/Z5LE-6AKX](https://perma.cc/Z5LE-6AKX)
diff --git a/content/tw/ch8.md b/content/tw/ch8.md
index 9069d34..018c238 100644
--- a/content/tw/ch8.md
+++ b/content/tw/ch8.md
@@ -5,6 +5,8 @@ math: true
breadcrumbs: false
---
+
+

> *有些作者聲稱,支援通用的兩階段提交代價太大,會帶來效能與可用性的問題。我們認為,讓程式設計師來處理過度使用事務導致的效能問題,總比缺少事務程式設計好得多。*
@@ -40,7 +42,7 @@ breadcrumbs: false
在 2000 年代後期,非關係(NoSQL)資料庫開始流行起來。它們旨在透過提供新的資料模型選擇(參見[第 3 章](/tw/ch3#ch_datamodels)),以及預設包含複製([第 6 章](/tw/ch6#ch_replication))和分片([第 7 章](/tw/ch7#ch_sharding))來改進關係型資料庫的現狀。事務是這一運動的主要犧牲品:許多這一代資料庫完全放棄了事務,或者重新定義了這個詞,用來描述比以前理解的更弱的保證集。
-圍繞 NoSQL 分散式資料庫的炒作導致了一種流行的信念,即事務從根本上是不可擴充套件的,任何大規模系統都必須放棄事務以保持良好的效能和高可用性。最近,這種信念被證明是錯誤的。所謂的"NewSQL"資料庫,如 CockroachDB[^5]、TiDB[^6]、Spanner[^7]、FoundationDB[^8] 和 Yugabyte 已經證明,事務系統可以擴充套件到大資料量和高吞吐量。這些系統將分片與共識協議([第 10 章](/tw/ch10#ch_consistency))相結合,以大規模提供強 ACID 保證。
+圍繞 NoSQL 分散式資料庫的炒作導致了一種流行的信念,即事務從根本上不可伸縮,任何大規模系統都必須放棄事務以保持良好的效能和高可用性。最近,這種信念被證明是錯誤的。所謂 "NewSQL" 資料庫,如 CockroachDB[^5]、TiDB[^6]、Spanner[^7]、FoundationDB[^8] 和 YugabyteDB 已經證明,事務系統同樣可以具備很強的可伸縮性,並支援大資料量與高吞吐量。這些系統將分片與共識協議([第 10 章](/tw/ch10#ch_consistency))結合,在大規模下提供強 ACID 保證。
然而,這並不意味著每個系統都必須是事務型的:與任何其他技術設計選擇一樣,事務有優點也有侷限性。為了理解這些權衡,讓我們深入瞭解事務可以提供的保證的細節——無論是在正常操作中還是在各種極端(但現實)的情況下。
@@ -71,9 +73,9 @@ breadcrumbs: false
*一致性*這個詞被嚴重濫用:
* 在[第 6 章](/tw/ch6#ch_replication)中,我們討論了*副本一致性*和非同步複製系統中出現的*最終一致性*問題(參見["複製延遲的問題"](/tw/ch6#sec_replication_lag))。
-* 資料庫的*一致快照*(例如,用於備份)是整個資料庫在某一時刻存在的快照。更準確地說,它與先發生關係(happens-before relation)一致(參見[""先發生"關係和併發"](/tw/ch6#sec_replication_happens_before)):也就是說,如果快照包含在特定時間寫入的值,那麼它也反映了在該值寫入之前發生的所有寫入。
+* 資料庫的*一致快照*(例如,用於備份)是整個資料庫在某一時刻存在的快照。更準確地說,它與先發生關係(happens-before relation)一致(參見["“先發生”關係和併發"](/tw/ch6#sec_replication_happens_before)):也就是說,如果快照包含在特定時間寫入的值,那麼它也反映了在該值寫入之前發生的所有寫入。
* *一致性雜湊*是某些系統用於再平衡的分片方法(參見["一致性雜湊"](/tw/ch7#sec_sharding_consistent_hashing))。
-* 在 CAP 定理中(參見[第 10 章](/tw/ch10#ch_consistency)),*一致性*一詞用於表示*線性一致性*(參見["線性一致性"](/tw/ch10#sec_consistency_linearizability))。
+* 在 CAP定理中(參見[第 10 章](/tw/ch10#ch_consistency)),*一致性*一詞用於表示*線性一致性*(參見["線性一致性"](/tw/ch10#sec_consistency_linearizability))。
* 在 ACID 的上下文中,*一致性*是指應用程式特定的資料庫處於"良好狀態"的概念。
不幸的是,同一個詞至少有五種不同的含義。
@@ -107,6 +109,8 @@ ACID 意義上的*隔離性*意味著同時執行的事務彼此隔離:它們
--------
+
+
> [!TIP] 複製與永續性
歷史上,永續性意味著寫入歸檔磁帶。然後它被理解為寫入磁碟或 SSD。最近,它已經適應為意味著複製。哪種實現更好?
@@ -189,13 +193,13 @@ SELECT COUNT(*) FROM emails WHERE recipient_id = 2 AND unread_flag = true
* 在文件資料模型中,需要一起更新的欄位通常在同一文件內,它被視為單個物件——更新單個文件時不需要多物件事務。然而,缺乏連線功能的文件資料庫也鼓勵反正規化(參見["何時使用哪種模型"](/tw/ch3#sec_datamodels_document_summary))。當需要更新反正規化資訊時,如[圖 8-2](/tw/ch8#fig_transactions_read_uncommitted) 的示例,你需要一次更新多個文件。事務在這種情況下非常有用,可以防止反正規化資料失去同步。
* 在具有二級索引的資料庫中(幾乎除了純鍵值儲存之外的所有資料庫),每次更改值時都需要更新索引。從事務的角度來看,這些索引是不同的資料庫物件:例如,如果沒有事務隔離,記錄可能出現在一個索引中但不在另一個索引中,因為對第二個索引的更新尚未發生(參見["分片和二級索引"](/tw/ch7#sec_sharding_secondary_indexes))。
-這些應用程式仍然可以在沒有事務的情況下實現。然而,沒有原子性的錯誤處理變得更加複雜,缺乏隔離性可能導致併發問題。我們將在["弱隔離級別"](/tw/ch8#sec_transactions_isolation_levels)中討論這些問題,並在[待補充連結]中探索替代方法。
+這些應用程式仍然可以在沒有事務的情況下實現。然而,沒有原子性的錯誤處理變得更加複雜,缺乏隔離性可能導致併發問題。我們將在["弱隔離級別"](/tw/ch8#sec_transactions_isolation_levels)中討論這些問題,並在["派生資料與分散式事務"](/tw/ch13#sec_future_derived_vs_transactions)中探索替代方法。
#### 處理錯誤和中止 {#handling-errors-and-aborts}
事務的一個關鍵特性是,如果發生錯誤,它可以被中止並安全地重試。ACID 資料庫基於這樣的哲學:如果資料庫有違反其原子性、隔離性或永續性保證的危險,它寧願完全放棄事務,也不允許它保持半完成狀態。
-然而,並非所有系統都遵循這種哲學。特別是,具有無領導者複製的資料儲存(參見["無領導者複製"](/tw/ch6#sec_replication_leaderless))更多地基於"盡力而為"的基礎工作,可以總結為"資料庫將盡其所能,如果遇到錯誤,它不會撤消已經完成的操作"——因此,從錯誤中恢復是應用程式的責任。
+然而,並非所有系統都遵循這種哲學。特別是,具有無主(無領導者)複製的資料儲存(參見["無主(無領導者)複製"](/tw/ch6#sec_replication_leaderless))更多地基於"盡力而為"的基礎工作,可以總結為"資料庫將盡其所能,如果遇到錯誤,它不會撤消已經完成的操作"——因此,從錯誤中恢復是應用程式的責任。
錯誤不可避免地會發生,但許多軟體開發人員更願意只考慮快樂路徑,而不是錯誤處理的複雜性。例如,流行的物件關係對映(ORM)框架,如 Rails 的 ActiveRecord 和 Django,不會重試中止的事務——錯誤通常導致異常冒泡到堆疊中,因此任何使用者輸入都被丟棄,使用者收到錯誤訊息。這是一種遺憾,因為中止的全部意義是啟用安全重試。
@@ -288,12 +292,12 @@ SELECT COUNT(*) FROM emails WHERE recipient_id = 2 AND unread_flag = true
然而,使用這個隔離級別時,仍然有很多方式可能出現併發錯誤。例如,[圖 8-6](/tw/ch8#fig_transactions_item_many_preceders) 說明了讀已提交可能發生的問題。
-{{< figure src="/fig/ddia_0806.png" id="fig_transactions_item_many_preceders" caption="圖 8-6. 讀偏斜:Aaliyah 觀察到資料庫處於不一致狀態。" class="w-full my-4" >}}
+{{< figure src="/fig/ddia_0806.png" id="fig_transactions_item_many_preceders" caption="圖 8-6. 讀取偏差:Aaliyah 觀察到資料庫處於不一致狀態。" class="w-full my-4" >}}
假設 Aaliyah 在銀行有 1,000 美元的儲蓄,分成兩個賬戶,每個 500 美元。現在一筆事務從她的一個賬戶轉賬 100 美元到另一個賬戶。如果她不幸在該事務處理的同時檢視她的賬戶餘額列表,她可能會看到一個賬戶餘額在收款到達之前(餘額為 500 美元),另一個賬戶在轉出之後(新余額為 400 美元)。對 Aaliyah 來說,現在她的賬戶總共只有 900 美元——似乎 100 美元憑空消失了。
-這種異常稱為*讀偏斜*,它是*不可重複讀*的一個例子:如果 Aaliyah 在事務結束時再次讀取賬戶 1 的餘額,她會看到與之前查詢中看到的不同的值(600 美元)。讀偏斜在讀已提交隔離下被認為是可接受的:Aaliyah 看到的賬戶餘額確實是在她讀取它們時已提交的。
+這種異常稱為*讀取偏差*,它是*不可重複讀*的一個例子:如果 Aaliyah 在事務結束時再次讀取賬戶 1 的餘額,她會看到與之前查詢中看到的不同的值(600 美元)。讀取偏差在讀已提交隔離下被認為是可接受的:Aaliyah 看到的賬戶餘額確實是在她讀取它們時已提交的。
--------
@@ -351,6 +355,8 @@ SELECT COUNT(*) FROM emails WHERE recipient_id = 2 AND unread_flag = true
長時間執行的事務可能會長時間繼續使用快照,繼續讀取(從其他事務的角度來看)早已被覆蓋或刪除的值。透過永遠不更新原地的值,而是在每次更改值時插入新版本,資料庫可以提供一致的快照,同時只產生很小的開銷。
+
+
#### 索引與快照隔離 {#indexes-and-snapshot-isolation}
索引如何在多版本資料庫中工作?最常見的方法是每個索引條目指向與該條目匹配的行的一個版本(最舊或最新版本)。每個行版本可能包含對下一個最舊或下一個最新版本的引用。使用索引的查詢必須迭代行以找到可見的行,並且值與查詢要查詢的內容匹配。當垃圾收集刪除不再對任何事務可見的舊行版本時,相應的索引條目也可以被刪除。
@@ -455,15 +461,15 @@ UPDATE wiki_pages SET content = 'new content'
在複製資料庫中(參見[第 6 章](/tw/ch6#ch_replication)),防止丟失的更新具有另一個維度:由於它們在多個節點上有資料副本,並且資料可能在不同節點上併發修改,因此需要採取一些額外的步驟來防止丟失的更新。
-鎖和條件寫入操作假設有一個最新的資料副本。然而,具有多領導者或無領導者複製的資料庫通常允許多個寫入併發發生並非同步複製它們,因此它們不能保證有一個最新的資料副本。因此,基於鎖或條件寫入的技術在此上下文中不適用。(我們將在["線性一致性"](/tw/ch10#sec_consistency_linearizability)中更詳細地重新討論這個問題。)
+鎖和條件寫入操作假設有一個最新的資料副本。然而,具有多領導者或無主(無領導者)複製的資料庫通常允許多個寫入併發發生並非同步複製它們,因此它們不能保證有一個最新的資料副本。因此,基於鎖或條件寫入的技術在此上下文中不適用。(我們將在["線性一致性"](/tw/ch10#sec_consistency_linearizability)中更詳細地重新討論這個問題。)
相反,如["處理衝突寫入"](/tw/ch6#sec_replication_write_conflicts)中所討論的,此類複製資料庫中的常見方法是允許併發寫入建立值的多個衝突版本(也稱為*兄弟節點*),並使用應用程式程式碼或特殊資料結構在事後解決和合並這些版本。
如果更新是可交換的(即,你可以在不同副本上以不同順序應用它們,仍然得到相同的結果),合併衝突值可以防止丟失的更新。例如,遞增計數器或向集合新增元素是可交換操作。這就是 CRDT 背後的想法,我們在["CRDT 和操作轉換"](/tw/ch6#sec_replication_crdts)中遇到過。然而,某些操作(如條件寫入)不能成為可交換的。
-另一方面,*最後寫入獲勝*(LWW)衝突解決方法容易丟失更新,如["最後寫入獲勝(丟棄併發寫入)"](/tw/ch6#sec_replication_lww)中所討論的。不幸的是,LWW 是許多複製資料庫中的預設值。
+另一方面,*最後寫入勝利*(LWW)衝突解決方法容易丟失更新,如["最後寫入勝利(丟棄併發寫入)"](/tw/ch6#sec_replication_lww)中所討論的。不幸的是,LWW 是許多複製資料庫中的預設值。
-### 寫偏斜與幻讀 {#sec_transactions_write_skew}
+### 寫偏差與幻讀 {#sec_transactions_write_skew}
在前面的部分中,我們看到了*髒寫*和*丟失更新*,這是當不同事務併發嘗試寫入相同物件時可能發生的兩種競態條件。為了避免資料損壞,需要防止這些競態條件——要麼由資料庫自動防止,要麼透過使用鎖或原子寫操作等手動保護措施。
@@ -473,21 +479,21 @@ UPDATE wiki_pages SET content = 'new content'
現在想象 Aaliyah 和 Bryce 是特定班次的兩位值班醫生。兩人都感覺不舒服,所以他們都決定請假。不幸的是,他們碰巧大約在同一時間點選了下班的按鈕。接下來發生的事情如[圖 8-8](/tw/ch8#fig_transactions_write_skew) 所示。
-{{< figure src="/fig/ddia_0808.png" id="fig_transactions_write_skew" caption="圖 8-8. 寫偏斜導致應用程式錯誤的示例。" class="w-full my-4" >}}
+{{< figure src="/fig/ddia_0808.png" id="fig_transactions_write_skew" caption="圖 8-8. 寫偏差導致應用程式錯誤的示例。" class="w-full my-4" >}}
在每個事務中,你的應用程式首先檢查當前是否有兩個或更多醫生在值班;如果是,它假設一個醫生下班是安全的。由於資料庫使用快照隔離,兩個檢查都返回 `2`,因此兩個事務都繼續到下一階段。Aaliyah 更新她自己的記錄讓自己下班,Bryce 同樣更新他自己的記錄。兩個事務都提交,現在沒有醫生值班。你至少有一個醫生值班的要求被違反了。
-#### 描述寫偏斜 {#characterizing-write-skew}
+#### 寫偏差的特徵 {#characterizing-write-skew}
-這種異常稱為*寫偏斜*[^36]。它既不是髒寫也不是丟失的更新,因為兩個事務正在更新兩個不同的物件(分別是 Aaliyah 和 Bryce 的值班記錄)。這裡發生衝突不太明顯,但這絕對是一個競態條件:如果兩個事務一個接一個地執行,第二個醫生將被阻止下班。異常行為只有在事務併發執行時才可能。
+這種異常稱為*寫偏差*[^36]。它既不是髒寫也不是丟失的更新,因為兩個事務正在更新兩個不同的物件(分別是 Aaliyah 和 Bryce 的值班記錄)。這裡發生衝突不太明顯,但這絕對是一個競態條件:如果兩個事務一個接一個地執行,第二個醫生將被阻止下班。異常行為只有在事務併發執行時才可能。
-你可以將寫偏斜視為丟失更新問題的概括。如果兩個事務讀取相同的物件,然後更新其中一些物件(不同的事務可能更新不同的物件),就會發生寫偏斜。在不同事務更新同一物件的特殊情況下,你會得到髒寫或丟失更新異常(取決於時機)。
+你可以將寫偏差視為丟失更新問題的概括。如果兩個事務讀取相同的物件,然後更新其中一些物件(不同的事務可能更新不同的物件),就會發生寫偏差。在不同事務更新同一物件的特殊情況下,你會得到髒寫或丟失更新異常(取決於時機)。
-我們看到有各種不同的方法可以防止丟失的更新。對於寫偏斜,我們的選擇更受限制:
+我們看到有各種不同的方法可以防止丟失的更新。對於寫偏差,我們的選擇更受限制:
* 原子單物件操作沒有幫助,因為涉及多個物件。
-* 不幸的是,你在某些快照隔離實現中發現的丟失更新的自動檢測也沒有幫助:寫偏斜在 PostgreSQL 的可重複讀、MySQL/InnoDB 的可重複讀、Oracle 的可序列化或 SQL Server 的快照隔離級別中不會自動檢測到[^29]。自動防止寫偏斜需要真正的可序列化隔離(參見["可序列化"](/tw/ch8#sec_transactions_serializability))。
+* 不幸的是,你在某些快照隔離實現中發現的丟失更新的自動檢測也沒有幫助:寫偏差在 PostgreSQL 的可重複讀、MySQL/InnoDB 的可重複讀、Oracle 的可序列化或 SQL Server 的快照隔離級別中不會自動檢測到[^29]。自動防止寫偏差需要真正的可序列化隔離(參見["可序列化"](/tw/ch8#sec_transactions_serializability))。
* 某些資料庫允許你配置約束,然後由資料庫強制執行(例如,唯一性、外部索引鍵約束或對特定值的限制)。但是,為了指定至少有一個醫生必須值班,你需要一個涉及多個物件的約束。大多數資料庫沒有對此類約束的內建支援,但你可能能夠使用觸發器或物化檢視實現它們,如["一致性"](/tw/ch8#sec_transactions_acid_consistency)中所討論的[^12]。
* 如果你不能使用可序列化隔離級別,在這種情況下,第二好的選擇可能是顯式鎖定事務所依賴的行。在醫生示例中,你可以編寫如下內容:
@@ -508,9 +514,9 @@ UPDATE wiki_pages SET content = 'new content'
❶:和以前一樣,`FOR UPDATE` 告訴資料庫鎖定此查詢返回的所有行。
-#### 更多寫偏斜的例子 {#more-examples-of-write-skew}
+#### 寫偏差的更多例子 {#more-examples-of-write-skew}
-寫偏斜起初可能看起來是一個深奧的問題,但一旦你意識到它,你可能會注意到更多可能發生的情況。以下是更多示例:
+寫偏差起初可能看起來是一個深奧的問題,但一旦你意識到它,你可能會注意到更多可能發生的情況。以下是更多示例:
會議室預訂系統
: 假設你想強制同一會議室在同一時間不能有兩個預訂[^55]。當有人想要預訂時,你首先檢查是否有任何衝突的預訂(即,具有重疊時間範圍的同一房間的預訂),如果沒有找到,你就建立會議(參見[例 8-2](/tw/ch8#fig_transactions_meeting_rooms))。
@@ -535,15 +541,15 @@ UPDATE wiki_pages SET content = 'new content'
不幸的是,快照隔離不會阻止另一個使用者併發插入衝突的會議。為了保證你不會出現排程衝突,你再次需要可序列化隔離。
多人遊戲
-: 在[例 8-1](/tw/ch8#fig_transactions_select_for_update) 中,我們使用鎖來防止丟失的更新(即,確保兩個玩家不能同時移動同一個棋子)。但是,鎖不會阻止玩家將兩個不同的棋子移動到棋盤上的同一位置,或者可能做出違反遊戲規則的其他移動。根據你要執行的規則型別,你可能能夠使用唯一約束,但否則你很容易受到寫偏斜的影響。
+: 在[例 8-1](/tw/ch8#fig_transactions_select_for_update) 中,我們使用鎖來防止丟失的更新(即,確保兩個玩家不能同時移動同一個棋子)。但是,鎖不會阻止玩家將兩個不同的棋子移動到棋盤上的同一位置,或者可能做出違反遊戲規則的其他移動。根據你要執行的規則型別,你可能能夠使用唯一約束,但否則你很容易受到寫偏差的影響。
宣告使用者名稱
: 在每個使用者都有唯一使用者名稱的網站上,兩個使用者可能同時嘗試使用相同的使用者名稱建立賬戶。你可以使用事務來檢查名稱是否被佔用,如果沒有,使用該名稱建立賬戶。但是,就像前面的例子一樣,這在快照隔離下是不安全的。幸運的是,唯一約束在這裡是一個簡單的解決方案(嘗試註冊使用者名稱的第二個事務將由於違反約束而被中止)。
防止重複消費
-: 允許使用者花錢或積分的服務需要檢查使用者不會花費超過他們擁有的。你可以透過在使用者賬戶中插入暫定支出專案,列出賬戶中的所有專案,並檢查總和是否為正來實現這一點。有了寫偏斜,可能會發生兩個支出專案併發插入,它們一起導致餘額變為負數,但沒有任何事務注意到另一個。
+: 允許使用者花錢或積分的服務需要檢查使用者不會花費超過他們擁有的。你可以透過在使用者賬戶中插入暫定支出專案,列出賬戶中的所有專案,並檢查總和是否為正來實現這一點。有了寫偏差,可能會發生兩個支出專案併發插入,它們一起導致餘額變為負數,但沒有任何事務注意到另一個。
-#### 導致寫偏斜的幻讀 {#sec_transactions_phantom}
+#### 導致寫偏差的幻讀 {#sec_transactions_phantom}
所有這些例子都遵循類似的模式:
@@ -555,9 +561,9 @@ UPDATE wiki_pages SET content = 'new content'
步驟可能以不同的順序發生。例如,你可以先進行寫入,然後進行 `SELECT` 查詢,最後根據查詢結果決定是中止還是提交。
-在醫生值班示例的情況下,步驟 3 中被修改的行是步驟 1 中返回的行之一,因此我們可以透過鎖定步驟 1 中的行(`SELECT FOR UPDATE`)來使事務安全並避免寫偏斜。但是,其他四個示例是不同的:它們檢查*不存在*匹配某些搜尋條件的行,而寫入*新增*了匹配相同條件的行。如果步驟 1 中的查詢不返回任何行,`SELECT FOR UPDATE` 就無法附加鎖[^56]。
+在醫生值班示例的情況下,步驟 3 中被修改的行是步驟 1 中返回的行之一,因此我們可以透過鎖定步驟 1 中的行(`SELECT FOR UPDATE`)來使事務安全並避免寫偏差。但是,其他四個示例是不同的:它們檢查*不存在*匹配某些搜尋條件的行,而寫入*新增*了匹配相同條件的行。如果步驟 1 中的查詢不返回任何行,`SELECT FOR UPDATE` 就無法附加鎖[^56]。
-這種效果,其中一個事務中的寫入改變另一個事務中搜索查詢的結果,稱為*幻讀*[^4]。快照隔離避免了只讀查詢中的幻讀,但在我們討論的讀寫事務中,幻讀可能導致特別棘手的寫偏斜情況。ORM 生成的 SQL 也容易出現寫偏斜[^50] [^51]。
+這種效果,其中一個事務中的寫入改變另一個事務中搜索查詢的結果,稱為*幻讀*[^4]。快照隔離避免了只讀查詢中的幻讀,但在我們討論的讀寫事務中,幻讀可能導致特別棘手的寫偏差情況。ORM 生成的 SQL 也容易出現寫偏差[^50] [^51]。
#### 物化衝突 {#materializing-conflicts}
@@ -573,7 +579,7 @@ UPDATE wiki_pages SET content = 'new content'
## 可序列化 {#sec_transactions_serializability}
-在本章中,我們已經看到了幾個容易出現競態條件的事務示例。某些競態條件被讀已提交和快照隔離級別所防止,但其他的則沒有。我們遇到了一些特別棘手的寫偏斜和幻讀示例。這是一個令人沮喪的情況:
+在本章中,我們已經看到了幾個容易出現競態條件的事務示例。某些競態條件被讀已提交和快照隔離級別所防止,但其他的則沒有。我們遇到了一些特別棘手的寫偏差和幻讀示例。這是一個令人沮喪的情況:
* 隔離級別很難理解,並且在不同資料庫中的實現不一致(例如,"可重複讀"的含義差異很大)。
* 如果你檢視你的應用程式程式碼,很難判斷在特定隔離級別下執行是否安全——特別是在大型應用程式中,你可能不知道所有可能併發發生的事情。
@@ -676,7 +682,7 @@ VoltDB 還使用儲存過程進行復制:它不是將事務的寫入從一個
* 如果事務 A 已讀取物件而事務 B 想要寫入該物件,B 必須等到 A 提交或中止後才能繼續。(這確保 B 不能在 A 背後意外地更改物件。)
* 如果事務 A 已寫入物件而事務 B 想要讀取該物件,B 必須等到 A 提交或中止後才能繼續。(像[圖 8-4](/tw/ch8#fig_transactions_read_committed) 中那樣讀取物件的舊版本在 2PL 下是不可接受的。)
-在 2PL 中,寫入者不僅阻塞其他寫入者;它們還阻塞讀者,反之亦然。快照隔離有這樣的口號:*讀者永遠不會阻塞寫者,寫者永遠不會阻塞讀者*(參見["多版本併發控制(MVCC)"](/tw/ch8#sec_transactions_snapshot_impl)),這捕捉了快照隔離和兩階段鎖定之間的關鍵區別。另一方面,因為 2PL 提供可序列化,它可以防止早期討論的所有競態條件,包括丟失的更新和寫偏斜。
+在 2PL 中,寫入者不僅阻塞其他寫入者;它們還阻塞讀者,反之亦然。快照隔離有這樣的口號:*讀者永遠不會阻塞寫者,寫者永遠不會阻塞讀者*(參見["多版本併發控制(MVCC)"](/tw/ch8#sec_transactions_snapshot_impl)),這捕捉了快照隔離和兩階段鎖定之間的關鍵區別。另一方面,因為 2PL 提供可序列化,它可以防止早期討論的所有競態條件,包括丟失的更新和寫偏差。
#### 兩階段鎖定的實現 {#implementation-of-two-phase-locking}
@@ -705,7 +711,7 @@ VoltDB 還使用儲存過程進行復制:它不是將事務的寫入從一個
#### 謂詞鎖 {#predicate-locks}
-在前面的鎖描述中,我們掩蓋了一個微妙但重要的細節。在["導致寫偏斜的幻讀"](/tw/ch8#sec_transactions_phantom)中,我們討論了*幻讀*的問題——即一個事務改變另一個事務的搜尋查詢結果。具有可序列化隔離的資料庫必須防止幻讀。
+在前面的鎖描述中,我們掩蓋了一個微妙但重要的細節。在["導致寫偏差的幻讀"](/tw/ch8#sec_transactions_phantom)中,我們討論了*幻讀*的問題——即一個事務改變另一個事務的搜尋查詢結果。具有可序列化隔離的資料庫必須防止幻讀。
在會議室預訂示例中,這意味著如果一個事務已經搜尋了某個時間視窗內某個房間的現有預訂(參見[例 8-2](/tw/ch8#fig_transactions_meeting_rooms)),另一個事務不允許併發插入或更新同一房間和時間範圍的另一個預訂。(併發插入其他房間的預訂,或同一房間不影響擬議預訂的不同時間的預訂是可以的。)
@@ -723,7 +729,7 @@ SELECT * FROM bookings
* 如果事務 A 想要讀取匹配某些條件的物件,就像在該 `SELECT` 查詢中一樣,它必須在查詢條件上獲取共享模式謂詞鎖。如果另一個事務 B 當前對匹配這些條件的任何物件具有獨佔鎖,A 必須等到 B 釋放其鎖後才允許進行查詢。
* 如果事務 A 想要插入、更新或刪除任何物件,它必須首先檢查舊值或新值是否匹配任何現有的謂詞鎖。如果存在事務 B 持有的匹配謂詞鎖,則 A 必須等到 B 提交或中止後才能繼續。
-這裡的關鍵思想是,謂詞鎖甚至適用於資料庫中尚不存在但將來可能新增的物件(幻讀)。如果兩階段鎖定包括謂詞鎖,資料庫將防止所有形式的寫偏斜和其他競態條件,因此其隔離變為可序列化。
+這裡的關鍵思想是,謂詞鎖甚至適用於資料庫中尚不存在但將來可能新增的物件(幻讀)。如果兩階段鎖定包括謂詞鎖,資料庫將防止所有形式的寫偏差和其他競態條件,因此其隔離變為可序列化。
#### 索引範圍鎖 {#sec_transactions_2pl_range}
@@ -738,13 +744,13 @@ SELECT * FROM bookings
無論哪種方式,搜尋條件的近似都附加到其中一個索引。現在,如果另一個事務想要插入、更新或刪除同一房間和/或重疊時間段的預訂,它將必須更新索引的相同部分。在這樣做的過程中,它將遇到共享鎖,並被迫等到鎖被釋放。
-這提供了對幻讀和寫偏斜的有效保護。索引範圍鎖不如謂詞鎖精確(它們可能鎖定比嚴格維護可序列化所需的更大範圍的物件),但由於它們的開銷要低得多,它們是一個很好的折衷。
+這提供了對幻讀和寫偏差的有效保護。索引範圍鎖不如謂詞鎖精確(它們可能鎖定比嚴格維護可序列化所需的更大範圍的物件),但由於它們的開銷要低得多,它們是一個很好的折衷。
如果沒有合適的索引可以附加範圍鎖,資料庫可以退回到整個表的共享鎖。這對效能不利,因為它將阻止所有其他事務寫入表,但這是一個安全的後備位置。
### 可序列化快照隔離(SSI) {#sec_transactions_ssi}
-本章描繪了資料庫併發控制的黯淡畫面。一方面,我們有效能不佳(兩階段鎖定)或擴充套件性不佳(序列執行)的可序列化實現。另一方面,我們有效能良好但容易出現各種競態條件(丟失的更新、寫偏斜、幻讀等)的弱隔離級別。可序列化隔離和良好效能從根本上是對立的嗎?
+本章描繪了資料庫併發控制的黯淡畫面。一方面,我們有效能不佳(兩階段鎖定)或可伸縮性不佳(序列執行)的可序列化實現。另一方面,我們有效能良好但容易出現各種競態條件(丟失的更新、寫偏差、幻讀等)的弱隔離級別。可序列化隔離和良好效能從根本上是對立的嗎?
似乎不是:一種稱為*可序列化快照隔離*(SSI)的演算法提供完全可序列化,與快照隔離相比只有很小的效能損失。SSI 相對較新:它於 2008 年首次描述[^53] [^65]。
@@ -766,7 +772,7 @@ SELECT * FROM bookings
#### 基於過時前提的決策 {#decisions-based-on-an-outdated-premise}
-當我們之前討論快照隔離中的寫偏斜時(參見["寫偏斜與幻讀"](/tw/ch8#sec_transactions_write_skew)),我們觀察到一個反覆出現的模式:事務從資料庫讀取一些資料,檢查查詢結果,並根據它看到的結果決定採取某些行動(寫入資料庫)。但是,在快照隔離下,原始查詢的結果在事務提交時可能不再是最新的,因為資料可能在此期間被修改。
+當我們之前討論快照隔離中的寫偏差時(參見["寫偏差與幻讀"](/tw/ch8#sec_transactions_write_skew)),我們觀察到一個反覆出現的模式:事務從資料庫讀取一些資料,檢查查詢結果,並根據它看到的結果決定採取某些行動(寫入資料庫)。但是,在快照隔離下,原始查詢的結果在事務提交時可能不再是最新的,因為資料可能在此期間被修改。
換句話說,事務基於*前提*(事務開始時為真的事實,例如,"當前有兩名醫生值班")採取行動。後來,當事務想要提交時,原始資料可能已更改——前提可能不再為真。
@@ -781,14 +787,14 @@ SELECT * FROM bookings
回想一下,快照隔離通常由多版本併發控制(MVCC;參見["多版本併發控制(MVCC)"](/tw/ch8#sec_transactions_snapshot_impl))實現。當事務從 MVCC 資料庫中的一致快照讀取時,它會忽略在拍攝快照時尚未提交的任何其他事務所做的寫入。
-在[圖 8-10](/tw/ch8#fig_transactions_detect_mvcc) 中,事務 43 看到 Aaliyah 的 `on_call = true`,因為事務 42(修改了 Aaliyah 的值班狀態)未提交。但是,當事務 43 想要提交時,事務 42 已經提交。這意味著從一致快照讀取時被忽略的寫入現在已生效,事務 43 的前提不再為真。當寫入者插入以前不存在的資料時,事情變得更加複雜(參見["導致寫偏斜的幻讀"](/tw/ch8#sec_transactions_phantom))。我們將在["檢測影響先前讀取的寫入"](/tw/ch8#sec_detecting_writes_affect_reads)中討論為 SSI 檢測幻寫。
+在[圖 8-10](/tw/ch8#fig_transactions_detect_mvcc) 中,事務 43 看到 Aaliyah 的 `on_call = true`,因為事務 42(修改了 Aaliyah 的值班狀態)未提交。但是,當事務 43 想要提交時,事務 42 已經提交。這意味著從一致快照讀取時被忽略的寫入現在已生效,事務 43 的前提不再為真。當寫入者插入以前不存在的資料時,事情變得更加複雜(參見["導致寫偏差的幻讀"](/tw/ch8#sec_transactions_phantom))。我們將在["檢測影響先前讀取的寫入"](/tw/ch8#sec_detecting_writes_affect_reads)中討論為 SSI 檢測幻寫。
{{< figure src="/fig/ddia_0810.png" id="fig_transactions_detect_mvcc" caption="圖 8-10. 檢測事務何時從 MVCC 快照讀取過時值。" class="w-full my-4" >}}
為了防止這種異常,資料庫需要跟蹤事務由於 MVCC 可見性規則而忽略另一個事務的寫入的時間。當事務想要提交時,資料庫會檢查是否有任何被忽略的寫入現在已經提交。如果是,事務必須被中止。
-為什麼要等到提交?為什麼不在檢測到陳舊讀取時立即中止事務 43?好吧,如果事務 43 是隻讀事務,它就不需要被中止,因為沒有寫偏斜的風險。在事務 43 進行讀取時,資料庫還不知道該事務是否稍後會執行寫入。此外,事務 42 可能還會中止,或者在事務 43 提交時可能仍未提交,因此讀取可能最終不是陳舊的。透過避免不必要的中止,SSI 保留了快照隔離對從一致快照進行長時間執行讀取的支援。
+為什麼要等到提交?為什麼不在檢測到陳舊讀取時立即中止事務 43?好吧,如果事務 43 是隻讀事務,它就不需要被中止,因為沒有寫偏差的風險。在事務 43 進行讀取時,資料庫還不知道該事務是否稍後會執行寫入。此外,事務 42 可能還會中止,或者在事務 43 提交時可能仍未提交,因此讀取可能最終不是陳舊的。透過避免不必要的中止,SSI 保留了快照隔離對從一致快照進行長時間執行讀取的支援。
#### 檢測影響先前讀取的寫入 {#sec_detecting_writes_affect_reads}
@@ -920,15 +926,15 @@ SELECT * FROM bookings
資料庫內部事務不必與任何其他系統相容,因此它們可以使用任何協議並應用特定於該特定技術的最佳化。因此,資料庫內部分散式事務通常可以很好地工作。另一方面,跨異構技術的事務更具挑戰性。
-#### 精確一次訊息處理 {#sec_transactions_exactly_once}
+#### 恰好一次訊息處理 {#sec_transactions_exactly_once}
異構分散式事務允許以強大的方式整合各種系統。例如,當且僅當處理訊息的資料庫事務成功提交時,來自訊息佇列的訊息才能被確認為已處理。這是透過在單個事務中原子地提交訊息確認和資料庫寫入來實現的。有了分散式事務支援,即使訊息代理和資料庫是在不同機器上執行的兩種不相關的技術,這也是可能的。
-如果訊息傳遞或資料庫事務失敗,兩者都會中止,因此訊息代理可以稍後安全地重新傳遞訊息。因此,透過原子地提交訊息及其處理的副作用,我們可以確保訊息被*有效地*精確處理一次,即使在成功之前需要幾次重試。中止會丟棄部分完成事務的任何副作用。這被稱為*精確一次語義*。
+如果訊息傳遞或資料庫事務失敗,兩者都會中止,因此訊息代理可以稍後安全地重新傳遞訊息。因此,透過原子地提交訊息及其處理的副作用,我們可以確保訊息在效果上*恰好*處理一次,即使在成功之前需要幾次重試。中止會丟棄部分完成事務的任何副作用。這被稱為*恰好一次語義*。
但是,只有當受事務影響的所有系統都能夠使用相同的原子提交協議時,這種分散式事務才有可能。例如,假設處理訊息的副作用是傳送電子郵件,而電子郵件伺服器不支援兩階段提交:如果訊息處理失敗並重試,可能會發生電子郵件被傳送兩次或更多次。但是,如果處理訊息的所有副作用在事務中止時都會回滾,那麼處理步驟可以安全地重試,就好像什麼都沒有發生一樣。
-我們將在本章後面回到精確一次語義的主題。讓我們首先看看允許此類異構分散式事務的原子提交協議。
+我們將在本章後面回到恰好一次語義的主題。讓我們首先看看允許此類異構分散式事務的原子提交協議。
#### XA 事務 {#xa-transactions}
@@ -972,7 +978,7 @@ XA 假設你的應用程式使用網路驅動程式或客戶端庫與參與者
另一個問題是,由於 XA 需要與各種資料系統相容,它必然是最低公分母。例如,它無法檢測跨不同系統的死鎖(因為這需要系統交換有關每個事務正在等待的鎖的資訊的標準化協議),並且它不適用於 SSI(參見["可序列化快照隔離(SSI)"](/tw/ch8#sec_transactions_ssi)),因為這需要跨不同系統識別衝突的協議。
-這些問題在某種程度上是跨異構技術執行事務所固有的。但是,保持幾個異構資料系統彼此一致仍然是一個真實而重要的問題,因此我們需要為其找到不同的解決方案。這可以做到,我們將在下一節和[待補充連結]中看到。
+這些問題在某種程度上是跨異構技術執行事務所固有的。但是,保持幾個異構資料系統彼此一致仍然是一個真實而重要的問題,因此我們需要為其找到不同的解決方案。這可以做到,我們將在下一節和["派生資料與分散式事務"](/tw/ch13#sec_future_derived_vs_transactions)中看到。
### 資料庫內部的分散式事務 {#sec_transactions_internal}
@@ -991,11 +997,11 @@ XA 的最大問題可以透過以下方式解決:
為分散式事務提供的隔離級別取決於系統,但跨分片的快照隔離和可序列化快照隔離都是可能的。有關其工作原理的詳細資訊,請參見本章末尾引用的論文。
-#### 再談精確一次訊息處理 {#exactly-once-message-processing-revisited}
+#### 再談恰好一次訊息處理 {#exactly-once-message-processing-revisited}
-我們在["精確一次訊息處理"](/tw/ch8#sec_transactions_exactly_once)中看到,分散式事務的一個重要用例是確保某些操作精確生效一次,即使在處理過程中發生崩潰並且需要重試處理。如果你可以跨訊息代理和資料庫原子地提交事務,則當且僅當成功處理訊息並且從處理過程產生的資料庫寫入被提交時,你可以向代理確認訊息。
+我們在["恰好一次訊息處理"](/tw/ch8#sec_transactions_exactly_once)中看到,分散式事務的一個重要用例是確保某些操作恰好生效一次,即使在處理過程中發生崩潰並且需要重試處理。如果你可以跨訊息代理和資料庫原子地提交事務,則當且僅當成功處理訊息並且從處理過程產生的資料庫寫入被提交時,你可以向代理確認訊息。
-但是,你實際上不需要這樣的分散式事務來實現精確一次語義。另一種方法如下,它只需要資料庫中的事務:
+但是,你實際上不需要這樣的分散式事務來實現恰好一次語義。另一種方法如下,它只需要資料庫中的事務:
1. 假設每條訊息都有唯一的 ID,並且在資料庫中有一個已處理訊息 ID 的表。當你開始從代理處理訊息時,你在資料庫上開始一個新事務,並檢查訊息 ID。如果資料庫中已經存在相同的訊息 ID,你知道它已經被處理,因此你可以向代理確認訊息並丟棄它。
2. 如果訊息 ID 尚未在資料庫中,你將其新增到表中。然後你處理訊息,這可能會導致在同一事務中對資料庫進行額外的寫入。完成處理訊息後,你提交資料庫上的事務。
@@ -1004,7 +1010,7 @@ XA 的最大問題可以透過以下方式解決:
如果訊息處理器在提交資料庫事務之前崩潰,事務將被中止,訊息代理將重試處理。如果它在提交後但在向代理確認訊息之前崩潰,它也將重試處理,但重試將在資料庫中看到訊息 ID 並丟棄它。如果它在確認訊息後但在從資料庫中刪除訊息 ID 之前崩潰,你將有一個舊的訊息 ID 留下,除了佔用一點儲存空間外不會造成任何傷害。如果在資料庫事務中止之前發生重試(如果訊息處理器和資料庫之間的通訊中斷,這可能會發生),訊息 ID 表上的唯一性約束應該防止兩個併發事務插入相同的訊息 ID。
-因此,實現精確一次處理只需要資料庫中的事務——跨資料庫和訊息代理的原子性對於此用例不是必需的。在資料庫中記錄訊息 ID 使訊息處理*冪等*,因此可以安全地重試訊息處理而不會重複其副作用。流處理框架(如 Kafka Streams)中使用類似的方法來實現精確一次語義,我們將在[待補充連結]中看到。
+因此,實現恰好一次處理只需要資料庫中的事務——跨資料庫和訊息代理的原子性對於此用例不是必需的。在資料庫中記錄訊息 ID 使訊息處理具備*冪等性*,因此可以安全地重試訊息處理而不會重複其副作用。流處理框架(如 Kafka Streams)中使用類似的方法來實現恰好一次語義,我們將在["容錯"](/tw/ch12#sec_stream_fault_tolerance)中看到。
但是,資料庫內的內部分散式事務對於此類模式的可伸縮性仍然有用:例如,它們將允許訊息 ID 儲存在一個分片上,而訊息處理更新的主資料儲存在其他分片上,並確保跨這些分片的事務提交的原子性。
@@ -1022,7 +1028,7 @@ XA 的最大問題可以透過以下方式解決:
表 8-1. 各種隔離級別可能發生的異常總結
-| 隔離級別 | 髒讀 | 讀偏斜 | 幻讀 | 丟失更新 | 寫偏斜 |
+| 隔離級別 | 髒讀 | 讀取偏差 | 幻讀 | 丟失更新 | 寫偏差 |
|------|------|------|------|-------|------|
| 讀未提交 | ✗ 可能 | ✗ 可能 | ✗ 可能 | ✗ 可能 | ✗ 可能 |
| 讀已提交 | ✓ 防止 | ✗ 可能 | ✗ 可能 | ✗ 可能 | ✗ 可能 |
@@ -1035,17 +1041,17 @@ XA 的最大問題可以透過以下方式解決:
髒寫
: 一個客戶端覆蓋另一個客戶端已寫入但尚未提交的資料。幾乎所有事務實現都防止髒寫。
-讀偏斜
-: 客戶端在不同時間點看到資料庫的不同部分。某些讀偏斜的情況也稱為*不可重複讀*。這個問題最常透過快照隔離來防止,它允許事務從對應於特定時間點的一致快照讀取。它通常使用*多版本併發控制*(MVCC)實現。
+讀取偏差
+: 客戶端在不同時間點看到資料庫的不同部分。某些讀取偏差的情況也稱為*不可重複讀*。這個問題最常透過快照隔離來防止,它允許事務從對應於特定時間點的一致快照讀取。它通常使用*多版本併發控制*(MVCC)實現。
丟失更新
: 兩個客戶端併發執行讀-修改-寫迴圈。一個覆蓋另一個的寫入而不合並其更改,因此資料丟失。某些快照隔離的實現會自動防止此異常,而其他實現需要手動鎖(`SELECT FOR UPDATE`)。
-寫偏斜
+寫偏差
: 事務讀取某些內容,根據它看到的值做出決定,並將決定寫入資料庫。但是,在進行寫入時,決策的前提不再為真。只有可序列化隔離才能防止此異常。
幻讀
-: 事務讀取匹配某些搜尋條件的物件。另一個客戶端進行影響該搜尋結果的寫入。快照隔離防止直接的幻讀,但寫偏斜上下文中的幻讀需要特殊處理,例如索引範圍鎖。
+: 事務讀取匹配某些搜尋條件的物件。另一個客戶端進行影響該搜尋結果的寫入。快照隔離防止直接的幻讀,但寫偏差上下文中的幻讀需要特殊處理,例如索引範圍鎖。
弱隔離級別可以防止某些異常,但讓你(應用程式開發人員)手動處理其他異常(例如,使用顯式鎖定)。只有可序列化隔離可以防止所有這些問題。我們討論了實現可序列化事務的三種不同方法:
@@ -1058,13 +1064,13 @@ XA 的最大問題可以透過以下方式解決:
可序列化快照隔離(SSI)
: 一種相對較新的演算法,避免了前面方法的大部分缺點。它使用樂觀方法,允許事務在不阻塞的情況下進行。當事務想要提交時,它會被檢查,如果執行不可序列化,它將被中止。
-最後,我們研究了當事務分佈在多個節點上時如何實現原子性,使用兩階段提交。如果這些節點都執行相同的資料庫軟體,分散式事務可以很好地工作,但跨不同儲存技術(使用 XA 事務),2PC 是有問題的:它對協調器和驅動事務的應用程式程式碼中的故障非常敏感,並且與併發控制機制的互動很差。幸運的是,冪等性可以確保精確一次語義,而無需跨不同儲存技術的原子提交,我們將在後面的章節中看到更多相關內容。
+最後,我們研究了當事務分佈在多個節點上時如何實現原子性,使用兩階段提交。如果這些節點都執行相同的資料庫軟體,分散式事務可以很好地工作,但跨不同儲存技術(使用 XA 事務),2PC 是有問題的:它對協調器和驅動事務的應用程式程式碼中的故障非常敏感,並且與併發控制機制的互動很差。幸運的是,冪等性可以確保恰好一次語義,而無需跨不同儲存技術的原子提交,我們將在後面的章節中看到更多相關內容。
本章中的示例使用了關係資料模型。但是,如["多物件事務的需求"](/tw/ch8#sec_transactions_need)中所討論的,無論使用哪種資料模型,事務都是有價值的資料庫功能。
-## 參考
+### 參考
[^1]: Steven J. Murdoch. [What went wrong with Horizon: learning from the Post Office Trial](https://www.benthamsgaze.org/2021/07/15/what-went-wrong-with-horizon-learning-from-the-post-office-trial/). *benthamsgaze.org*, July 2021. Archived at [perma.cc/CNM4-553F](https://perma.cc/CNM4-553F)
diff --git a/content/tw/ch9.md b/content/tw/ch9.md
index 0578bba..d83d10d 100644
--- a/content/tw/ch9.md
+++ b/content/tw/ch9.md
@@ -4,9 +4,11 @@ weight: 209
breadcrumbs: false
---
+
+

-> *它們是有趣的東西,意外。在你遇到它們之前,你永遠不會遇到它們。*
+> *意外這東西挺有意思:你沒碰上之前,它就從來不會發生。*
>
> A.A. 米爾恩,《小熊維尼和老灰驢的家》(1928)
@@ -75,11 +77,11 @@ TCP 通常被描述為提供 "可靠" 的交付,從某種意義上說,它檢
那麼,如果 TCP 提供 "可靠性",這是否意味著我們不再需要擔心網路不可靠?不幸的是不是。如果在某個超時時間內沒有收到確認,它會認為資料包一定已經丟失,但 TCP 也無法判斷是出站資料包還是確認丟失了。儘管 TCP 可以重新發送資料包,但它不能保證新資料包也會透過。如果網線被拔掉,TCP 不能為你重新插上它。最終,在可配置的超時後,TCP 放棄並嚮應用程式發出錯誤訊號。
-如果 TCP 連線因錯誤而關閉 —— 也許是因為遠端節點崩潰了,或者是因為網路被中斷了 —— 你不幸地無法知道遠端節點實際處理了多少資料 [^6]。即使 TCP 確認資料包已交付,這僅意味著遠端節點上的作業系統核心收到了它,但應用程式可能在處理該資料之前就崩潰了。如果你想確保請求成功,你需要來自應用程式本身的積極響應 [^7]。
+如果 TCP 連線因錯誤而關閉 —— 也許是因為遠端節點崩潰了,或者是因為網路被中斷了 —— 你不幸地無法知道遠端節點實際處理了多少資料 [^6]。即使 TCP 確認資料包已交付,這也僅意味著遠端節點上的作業系統核心收到了它,但應用程式可能在處理該資料之前就崩潰了。如果你想確保請求成功,你需要應用層返回明確的成功響應 [^7]。
儘管如此,TCP 非常有用,因為它提供了一種方便的方式來發送和接收太大而無法裝入一個數據包的訊息。一旦建立了 TCP 連線,你還可以使用它來發送多個請求和響應。這通常是透過首先發送一個標頭來完成的,該標頭以位元組為單位指示後續訊息的長度,然後是實際訊息。HTTP 和許多 RPC 協議(見 ["透過服務的資料流:REST 和 RPC"](/tw/ch5#sec_encoding_dataflow_rpc))就是這樣工作的。
-### 網路故障的實踐 {#sec_distributed_network_faults}
+### 實踐中的網路故障 {#sec_distributed_network_faults}
我們已經建立計算機網路幾十年了 —— 人們可能希望到現在我們已經弄清楚如何使它們可靠。不幸的是,我們還沒有成功。有一些系統研究和大量軼事證據表明,網路問題可能出人意料地常見,即使在由一家公司運營的受控環境(如資料中心)中也是如此 [^8]:
@@ -108,7 +110,7 @@ TCP 通常被描述為提供 "可靠" 的交付,從某種意義上說,它檢
許多系統需要自動檢測故障節點。例如:
-* 負載均衡器需要停止向已死亡的節點發送請求(即,將其 *移出輪轉*)。
+* 負載均衡器需要停止向已死亡的節點發送請求(即,將其 *從輪詢池中摘除*)。
* 在具有單主複製的分散式資料庫中,如果主節點失效,其中一個從節點需要被提升為新的主節點(見 ["處理節點中斷"](/tw/ch6#sec_replication_failover))。
不幸的是,網路的不確定性使得很難判斷節點是否正常工作。在某些特定情況下,你可能會得到一些明確告訴你某事不工作的反饋:
@@ -134,6 +136,8 @@ TCP 通常被描述為提供 "可靠" 的交付,從某種意義上說,它檢
不幸的是,我們使用的大多數系統都沒有這些保證:非同步網路具有 *無界延遲*(即,它們嘗試儘快交付資料包,但資料包到達所需的時間沒有上限),大多數伺服器實現無法保證它們可以在某個最大時間內處理請求(見 ["響應時間保證"](/tw/ch9#sec_distributed_clocks_realtime))。對於故障檢測,系統大部分時間快速執行是不夠的:如果你的超時很低,往返時間的瞬時峰值就足以使系統失去平衡。
+
+
#### 網路擁塞和排隊 {#network-congestion-and-queueing}
開車時,道路網路上的行駛時間通常因交通擁堵而變化最大。同樣,計算機網路上資料包延遲的可變性最常是由於排隊 [^27]:
@@ -149,6 +153,8 @@ TCP 通常被描述為提供 "可靠" 的交付,從某種意義上說,它檢
--------
+
+
> [!TIP] TCP 與 UDP
>
> 一些對延遲敏感的應用程式,如視訊會議和 IP 語音(VoIP),使用 UDP 而不是 TCP。這是可靠性和延遲可變性之間的權衡:由於 UDP 不執行流量控制並且不重傳丟失的資料包,它避免了網路延遲可變的一些原因(儘管它仍然容易受到交換機佇列和排程延遲的影響)。
@@ -189,6 +195,8 @@ TCP 通常被描述為提供 "可靠" 的交付,從某種意義上說,它檢
--------
+
+
> [!TIP] 延遲和資源利用率
>
> 更一般地說,你可以將可變延遲視為動態資源分割槽的結果。
@@ -417,7 +425,7 @@ while (true) {
-## 知識、真相和謊言 {#sec_distributed_truth}
+## 知識、真相與謊言 {#sec_distributed_truth}
到目前為止,在本章中,我們已經探討了分散式系統與在單臺計算機上執行的程式的不同之處:沒有共享記憶體,只有透過不可靠的網路進行訊息傳遞,具有可變延遲,系統可能會遭受部分失效、不可靠的時鐘和處理暫停。
@@ -471,7 +479,7 @@ while (true) {
術語 *殭屍* 有時用於描述尚未發現失去租約的前租約持有者,並且仍在充當當前租約持有者。由於我們不能完全排除殭屍,我們必須確保它們不能以腦裂的形式造成任何損害。這被稱為 *隔離* 殭屍。
-一些系統試圖透過關閉殭屍來隔離它們,例如透過斷開它們與網路的連線 [^9]、透過雲提供商的管理介面關閉 VM,甚至物理關閉機器 [^87]。這種方法被稱為 *向對方節點頭部開槍* 或 STONITH。不幸的是,它存在一些問題:它不能防範像 [圖 9-5](/tw/ch9#fig_distributed_lease_delay) 中那樣的大網路延遲;可能會發生所有節點相互關閉的情況 [^19];到檢測到殭屍並關閉它時,可能已經太晚了,資料可能已經被損壞。
+一些系統試圖透過關閉殭屍來隔離它們,例如透過斷開它們與網路的連線 [^9]、透過雲提供商的管理介面關閉 VM,甚至物理關閉機器 [^87]。這種方法被稱為 *對端節點爆頭*(STONITH)。不幸的是,它存在一些問題:它不能防範像 [圖 9-5](/tw/ch9#fig_distributed_lease_delay) 中那樣的大網路延遲;可能會發生所有節點相互關閉的情況 [^19];到檢測到殭屍並關閉它時,可能已經太晚了,資料可能已經被損壞。
一個更強大的隔離解決方案,可以防範殭屍和延遲請求,如 [圖 9-6](/tw/ch9#fig_distributed_fencing) 所示。
@@ -487,7 +495,7 @@ while (true) {
--------
-在 [圖 9-6](/tw/ch9#fig_distributed_fencing) 中,客戶端 1 獲得帶有令牌 33 的租約,但隨後進入長時間暫停,租約過期。客戶端 2 獲得帶有令牌 34 的租約(數字總是增加),然後將其寫請求傳送到儲存服務,包括令牌 34。稍後,客戶端 1 恢復生機並將其寫入傳送到儲存服務,包括其令牌值 33。然而,儲存服務記得它已經處理了具有更高令牌編號(34)的寫入,因此它拒絕帶有令牌 33 的請求。剛剛獲得租約的客戶端必須立即向儲存服務進行寫入,一旦該寫入完成,任何殭屍都被隔離了。
+在 [圖 9-6](/tw/ch9#fig_distributed_fencing) 中,客戶端 1 獲得帶有令牌 33 的租約,但隨後進入長時間暫停,租約過期。客戶端 2 獲得帶有令牌 34 的租約(數字總是增加),然後將其寫請求傳送到儲存服務,包括令牌 34。稍後,客戶端 1 恢復執行並將其寫入傳送到儲存服務,包括其令牌值 33。然而,儲存服務記得它已經處理了具有更高令牌編號(34)的寫入,因此它拒絕帶有令牌 33 的請求。剛剛獲得租約的客戶端必須立即向儲存服務進行寫入,一旦該寫入完成,任何殭屍都被隔離了。
如果 ZooKeeper 是你的鎖服務,你可以使用事務 ID `zxid` 或節點版本 `cversion` 作為隔離令牌 [^85]。使用 etcd,修訂號與租約 ID 一起起著類似的作用 [^89]。Hazelcast 中的 FencedLock API 明確生成隔離令牌 [^90]。
@@ -539,6 +547,8 @@ Web 應用程式確實需要預期客戶端在終端使用者控制下的任意
同樣,如果協議可以保護我們免受漏洞、安全妥協和惡意攻擊,那將是很有吸引力的。不幸的是,這也不現實:在大多數系統中,如果攻擊者可以破壞一個節點,他們可能可以破壞所有節點,因為它們可能執行相同的軟體。因此,傳統機制(身份驗證、訪問控制、加密、防火牆等)仍然是防範攻擊者的主要保護。
+
+
#### 弱形式的謊言 {#weak-forms-of-lying}
儘管我們假設節點通常是誠實的,但向軟體新增防範弱形式 "謊言" 的機制可能是值得的 —— 例如,由於硬體問題、軟體錯誤和配置錯誤導致的無效訊息。這種保護機制不是完全的拜占庭容錯,因為它們無法抵禦堅定的對手,但它們仍然是朝著更好可靠性邁出的簡單而務實的步驟。例如:
@@ -671,7 +681,7 @@ DST 要求模擬器能夠控制所有非確定性來源,例如網路延遲。
DST 提供了超越可重放性的幾個優勢。Antithesis 等工具試圖透過在發現不太常見的行為時將測試執行分支為多個子執行來探索應用程式程式碼中的許多不同程式碼路徑。由於確定性測試通常使用模擬時鐘和網路呼叫,因此此類測試可以比掛鐘時間執行得更快。例如,TigerBeetle 的時間抽象允許模擬模擬網路延遲和超時,而實際上不需要觸發超時的全部時間長度。這些技術允許模擬器更快地探索更多程式碼路徑。
-# 確定性的力量
+#### 確定性的力量 {#sidebar_distributed_determinism}
非確定性是我們在本章中討論的所有分散式系統挑戰的核心:併發性、網路延遲、程序暫停、時鐘跳躍和崩潰都以不可預測的方式發生,從系統的一次執行到下一次執行都不同。相反,如果你能使系統確定性,那可以極大地簡化事情。
@@ -691,7 +701,7 @@ DST 提供了超越可重放性的幾個優勢。Antithesis 等工具試圖透
* 節點的時鐘可能與其他節點嚴重不同步(儘管你盡最大努力設定了 NTP),它可能會突然向前或向後跳躍,而依賴它是危險的,因為你很可能沒有一個好的時鐘置信區間度量。
* 程序可能在其執行的任何時刻暫停相當長的時間,被其他節點宣告死亡,然後再次恢復活動而沒有意識到它曾暫停。
-這種 *部分失敗* 可能發生的事實是分散式系統的決定性特徵。每當軟體嘗試做任何涉及其他節點的事情時,都有可能偶爾失敗、隨機變慢或根本沒有響應(並最終超時)。在分散式系統中,我們嘗試將對部分失敗的容忍構建到軟體中,這樣即使某些組成部分出現故障,整個系統也可以繼續執行。
+這種 *部分失效* 可能發生的事實是分散式系統的決定性特徵。每當軟體嘗試做任何涉及其他節點的事情時,都有可能偶爾失敗、隨機變慢或根本沒有響應(並最終超時)。在分散式系統中,我們嘗試將對部分失效的容忍構建到軟體中,這樣即使某些組成部分出現故障,整個系統也可以繼續執行。
要容忍故障,第一步是 *檢測* 它們,但即使這樣也很困難。大多數系統沒有準確的機制來檢測節點是否已失敗,因此大多數分散式演算法依賴超時來確定遠端節點是否仍然可用。然而,超時無法區分網路和節點故障,可變的網路延遲有時會導致節點被錯誤地懷疑崩潰。處理跛行節點(limping nodes)更加困難,這些節點正在響應但速度太慢而無法做任何有用的事情。
diff --git a/content/tw/part-iii.md b/content/tw/part-iii.md
index 4406892..5d2509e 100644
--- a/content/tw/part-iii.md
+++ b/content/tw/part-iii.md
@@ -37,27 +37,35 @@ breadcrumbs: false
## 章節概述
我們將從 [第十一章](/tw/ch11) 開始,研究例如 MapReduce 這樣 **面向批處理(batch-oriented)** 的資料流系統。對於建設大規模資料系統,我們將看到,它們提供了優秀的工具和思想。
-[第十二章](/tw/ch12) 將把這些思想應用到 **流式資料(data streams)** 中,使我們能用更低的延遲完成同樣的任務。[第十三章](/tw/ch13) 將對本書進行總結,探討如何使用這些工具來構建可靠,可伸縮和可維護的應用。
+[第十二章](/tw/ch12) 將把這些思想應用到 **流式資料(data streams)** 中,使我們能用更低的延遲完成同樣的任務。[第十三章](/tw/ch13) 將探討如何使用這些工具來構建可靠、可伸縮和可維護的應用。[第十四章](/ch14) 將以倫理、隱私與社會影響為主題,為全書收束。
-## [第十一章:批處理](/tw/ch11)
-- [使用Unix工具的批處理](/tw/ch11#使用unix工具的批處理)
-- [MapReduce和分散式檔案系統](/tw/ch11#mapreduce和分散式檔案系統)
-- [MapReduce之後](/tw/ch11#mapreduce之後)
-- [本章小結](/tw/ch11#本章小結)
-- [參考文獻](/tw/ch11#參考文獻)
+## 索引
-## [第十二章:流處理](/tw/ch12)
-- [傳遞事件流](/tw/ch12#傳遞事件流)
-- [資料庫與流](/tw/ch12#資料庫與流)
-- [流處理](/tw/ch12#流處理)
-- [本章小結](/tw/ch12#本章小結)
-- [參考文獻](/tw/ch12#參考文獻)
+## [11. 批處理](/tw/ch11)
+- [使用 Unix 工具的批處理](/tw/ch11#sec_batch_unix)
+- [分散式系統中的批處理](/tw/ch11#sec_batch_distributed)
+- [批處理模型](/tw/ch11#id431)
+- [批處理用例](/tw/ch11#sec_batch_output)
+- [本章小結](/tw/ch11#id292)
+- [參考文獻](/tw/ch11#references)
-## [第十三章:資料系統的未來](/tw/ch13)
-- [資料整合](/tw/ch13#資料整合)
-- [分拆資料庫](/tw/ch13#分拆資料庫)
-- [將事情做正確](/tw/ch13#將事情做正確)
-- [做正確的事情](/tw/ch13#做正確的事情)
-- [本章小結](/tw/ch13#本章小結)
-- [參考文獻](/tw/ch13#參考文獻)
\ No newline at end of file
+## [12. 流處理](/tw/ch12)
+- [傳遞事件流](/tw/ch12#sec_stream_transmit)
+- [資料庫與流](/tw/ch12#sec_stream_databases)
+- [流處理](/tw/ch12#sec_stream_processing)
+- [本章小結](/tw/ch12#id332)
+- [參考文獻](/tw/ch12#references)
+
+## [13. 流式系統的哲學](/tw/ch13)
+- [資料整合](/tw/ch13#sec_future_integration)
+- [分拆資料庫](/tw/ch13#sec_future_unbundling)
+- [追求正確性](/tw/ch13#sec_future_correctness)
+- [本章小結](/tw/ch13#id367)
+- [參考文獻](/tw/ch13#references)
+
+## [14. 將事情做正確](/ch14)
+- [預測分析](/ch14#id369)
+- [隱私與追蹤](/ch14#id373)
+- [總結](/ch14#id594)
+- [參考文獻](/ch14#references)
\ No newline at end of file
diff --git a/content/tw/toc.md b/content/tw/toc.md
index 4d99438..5f7f757 100644
--- a/content/tw/toc.md
+++ b/content/tw/toc.md
@@ -86,27 +86,33 @@ breadcrumbs: false
- [共識](/tw/ch10#sec_consistency_consensus)
- [總結](/tw/ch10#summary)
-## [第十一章:批處理](/tw/ch11)
-- [使用Unix工具的批處理](/tw/ch11#使用unix工具的批處理)
-- [MapReduce和分散式檔案系統](/tw/ch11#mapreduce和分散式檔案系統)
-- [MapReduce之後](/tw/ch11#mapreduce之後)
-- [本章小結](/tw/ch11#本章小結)
-- [參考文獻](/tw/ch11#參考文獻)
+## [11. 批處理](/tw/ch11)
+- [使用 Unix 工具的批處理](/tw/ch11#sec_batch_unix)
+- [分散式系統中的批處理](/tw/ch11#sec_batch_distributed)
+- [批處理模型](/tw/ch11#id431)
+- [批處理用例](/tw/ch11#sec_batch_output)
+- [本章小結](/tw/ch11#id292)
+- [參考文獻](/tw/ch11#references)
-## [第十二章:流處理](/tw/ch12)
-- [傳遞事件流](/tw/ch12#傳遞事件流)
-- [資料庫與流](/tw/ch12#資料庫與流)
-- [流處理](/tw/ch12#流處理)
-- [本章小結](/tw/ch12#本章小結)
-- [參考文獻](/tw/ch12#參考文獻)
+## [12. 流處理](/tw/ch12)
+- [傳遞事件流](/tw/ch12#sec_stream_transmit)
+- [資料庫與流](/tw/ch12#sec_stream_databases)
+- [流處理](/tw/ch12#sec_stream_processing)
+- [本章小結](/tw/ch12#id332)
+- [參考文獻](/tw/ch12#references)
-## [第十三章:資料系統的未來](/tw/ch13)
-- [資料整合](/tw/ch13#資料整合)
-- [分拆資料庫](/tw/ch13#分拆資料庫)
-- [將事情做正確](/tw/ch13#將事情做正確)
-- [做正確的事情](/tw/ch13#做正確的事情)
-- [本章小結](/tw/ch13#本章小結)
-- [參考文獻](/tw/ch13#參考文獻)
+## [13. 流式系統的哲學](/tw/ch13)
+- [資料整合](/tw/ch13#sec_future_integration)
+- [分拆資料庫](/tw/ch13#sec_future_unbundling)
+- [追求正確性](/tw/ch13#sec_future_correctness)
+- [本章小結](/tw/ch13#id367)
+- [參考文獻](/tw/ch13#references)
+
+## [14. 將事情做正確](/ch14)
+- [預測分析](/ch14#id369)
+- [隱私與追蹤](/ch14#id373)
+- [總結](/ch14#id594)
+- [參考文獻](/ch14#references)
## [術語表](/tw/glossary)
diff --git a/content/v1_tw/_index.md b/content/v1_tw/_index.md
index 70a77a1..a2fc822 100644
--- a/content/v1_tw/_index.md
+++ b/content/v1_tw/_index.md
@@ -113,6 +113,22 @@ PostgreSQL 專家,資料庫老司機,雲計算泥石流。
| ISSUE & Pull Requests | USER | Title |
|-------------------------------------------------|------------------------------------------------------------|----------------------------------------------------------------|
+| [386](https://github.com/Vonng/ddia/pull/386) | [@uncle-lv](https://github.com/uncle-lv) | ch2: 最佳化一處翻譯 |
+| [384](https://github.com/Vonng/ddia/pull/384) | [@PanggNOTlovebean](https://github.com/PanggNOTlovebean) | docs: 最佳化中文文件的措辭和表達 |
+| [383](https://github.com/Vonng/ddia/pull/383) | [@PanggNOTlovebean](https://github.com/PanggNOTlovebean) | docs: 修正 ch4 中的術語和表達錯誤 |
+| [382](https://github.com/Vonng/ddia/pull/382) | [@uncle-lv](https://github.com/uncle-lv) | ch1: 最佳化一處翻譯 |
+| [381](https://github.com/Vonng/ddia/pull/381) | [@Max-Tortoise](https://github.com/Max-Tortoise) | ch4: 修正一處術語不完整問題 |
+| [377](https://github.com/Vonng/ddia/pull/377) | [@huang06](https://github.com/huang06) | 最佳化翻譯術語 |
+| [375](https://github.com/Vonng/ddia/issues/375) | [@z-soulx](https://github.com/z-soulx) | 對於是否100%全中文翻譯的必要性討論?個人-沒必要100%,特別是“名詞”,有原單詞更加適合it人員 |
+| [371](https://github.com/Vonng/ddia/pull/371) | [@lewiszlw](https://github.com/lewiszlw) | CPU core -> CPU 核心 |
+| [369](https://github.com/Vonng/ddia/pull/369) | [@bbwang-gl](https://github.com/bbwang-gl) | ch7: 可序列化快照隔離檢測一個事務何時修改另一個事務的讀取 |
+| [368](https://github.com/Vonng/ddia/pull/368) | [@yhao3](https://github.com/yhao3) | 更新 zh-tw.py 與 zh-tw 內容 |
+| [367](https://github.com/Vonng/ddia/pull/367) | [@yhao3](https://github.com/yhao3) | 修正拼寫、格式和標點問題 |
+| [366](https://github.com/Vonng/ddia/pull/366) | [@yangshangde](https://github.com/yangshangde) | ch8: 將“電源失敗”改為“電源失效” |
+| [365](https://github.com/Vonng/ddia/pull/365) | [@xyohn](https://github.com/xyohn) | ch1: 最佳化“儲存與計算分離”相關翻譯 |
+| [364](https://github.com/Vonng/ddia/issues/364) | [@xyohn](https://github.com/xyohn) | ch1: 最佳化“儲存與計算分離”相關翻譯 |
+| [363](https://github.com/Vonng/ddia/pull/363) | [@xyohn](https://github.com/xyohn) | #362: 最佳化一處翻譯 |
+| [362](https://github.com/Vonng/ddia/issues/362) | [@xyohn](https://github.com/xyohn) | ch1: 最佳化一處翻譯 |
| [359](https://github.com/Vonng/ddia/pull/359) | [@c25423](https://github.com/c25423) | ch10: 修正一處拼寫錯誤 |
| [358](https://github.com/Vonng/ddia/pull/358) | [@lewiszlw](https://github.com/lewiszlw) | ch4: 修正一處拼寫錯誤 |
| [356](https://github.com/Vonng/ddia/pull/356) | [@lewiszlw](https://github.com/lewiszlw) | ch2: 修正一處標點錯誤 |
diff --git a/hugo.yaml b/hugo.yaml
index ae3b435..1717ab9 100644
--- a/hugo.yaml
+++ b/hugo.yaml
@@ -62,10 +62,20 @@ markup:
table: true # 启用 Markdown 表格
taskList: true # 启用任务列表 [ ] / [x]
typographer: true # 智能排版(引号、破折号等)
+ passthrough:
+ enable: true # 允许将数学定界符透传给 Hextra 的数学渲染器
+ delimiters:
+ block:
+ - ['\[', '\]']
+ - ['$$', '$$']
+ inline:
+ - ['\(', '\)']
parser:
attribute: true # 允许在标题后写 {#id .class key=val},用于显式锚点
autoHeadingID: true # 为标题自动生成 ID(手写 {#id} 会覆盖自动生成)
autoHeadingIDType: github # 自动 ID 规则:github / blackfriday / none
+ renderer:
+ unsafe: true # 允许 Markdown 中的原生 HTML(如 、)按原样渲染
tableOfContents:
startLevel: 2 # ToC 从 h2 开始
endLevel: 4 # ToC 到 h4 结束