mirror of
https://github.com/Vonng/ddia.git
synced 2025-01-05 15:30:06 +08:00
update zh-tw content
This commit is contained in:
parent
fb150bf5e3
commit
9696b06285
@ -12,7 +12,7 @@
|
||||
|
||||
在本書的前兩部分中,我們討論了很多關於**請求**和**查詢**以及相應的**響應**或**結果**。許多現有資料系統中都採用這種資料處理方式:你傳送請求指令,一段時間後(我們期望)系統會給出一個結果。資料庫、快取、搜尋索引、Web伺服器以及其他一些系統都以這種方式工作。
|
||||
|
||||
像這樣的**線上(online)**系統,無論是瀏覽器請求頁面還是呼叫遠端API的服務,我們通常認為請求是由人類使用者觸發的,並且正在等待響應。他們不應該等太久,所以我們非常關注系統的響應時間(請參閱“[描述效能](ch1.md#描述效能)”)。
|
||||
像這樣的**線上(online)** 系統,無論是瀏覽器請求頁面還是呼叫遠端API的服務,我們通常認為請求是由人類使用者觸發的,並且正在等待響應。他們不應該等太久,所以我們非常關注系統的響應時間(請參閱“[描述效能](ch1.md#描述效能)”)。
|
||||
|
||||
Web和越來越多的基於HTTP/REST的API使互動的請求/響應風格變得如此普遍,以至於很容易將其視為理所當然。但我們應該記住,這不是構建系統的唯一方式,其他方法也有其優點。我們來看看三種不同型別的系統:
|
||||
|
||||
@ -22,11 +22,11 @@
|
||||
|
||||
***批處理系統(離線系統)***
|
||||
|
||||
一個批處理系統有大量的輸入資料,跑一個**作業(job)**來處理它,並生成一些輸出資料,這往往需要一段時間(從幾分鐘到幾天),所以通常不會有使用者等待作業完成。相反,批次作業通常會定期執行(例如,每天一次)。批處理作業的主要效能衡量標準通常是吞吐量(處理特定大小的輸入所需的時間)。本章中討論的就是批處理。
|
||||
一個批處理系統有大量的輸入資料,跑一個**作業(job)** 來處理它,並生成一些輸出資料,這往往需要一段時間(從幾分鐘到幾天),所以通常不會有使用者等待作業完成。相反,批次作業通常會定期執行(例如,每天一次)。批處理作業的主要效能衡量標準通常是吞吐量(處理特定大小的輸入所需的時間)。本章中討論的就是批處理。
|
||||
|
||||
***流處理系統(準實時系統)***
|
||||
|
||||
流處理介於線上和離線(批處理)之間,所以有時候被稱為**準實時(near-real-time)**或**準線上(nearline)**處理。像批處理系統一樣,流處理消費輸入併產生輸出(並不需要響應請求)。但是,流式作業在事件發生後不久就會對事件進行操作,而批處理作業則需等待固定的一組輸入資料。這種差異使流處理系統比起批處理系統具有更低的延遲。由於流處理基於批處理,我們將在[第十一章](ch11.md)討論它。
|
||||
流處理介於線上和離線(批處理)之間,所以有時候被稱為**準實時(near-real-time)** 或**準線上(nearline)** 處理。像批處理系統一樣,流處理消費輸入併產生輸出(並不需要響應請求)。但是,流式作業在事件發生後不久就會對事件進行操作,而批處理作業則需等待固定的一組輸入資料。這種差異使流處理系統比起批處理系統具有更低的延遲。由於流處理基於批處理,我們將在[第十一章](ch11.md)討論它。
|
||||
|
||||
正如我們將在本章中看到的那樣,批處理是構建可靠、可伸縮和可維護應用程式的重要組成部分。例如,2004年釋出的批處理演算法Map-Reduce(可能被過分熱情地)被稱為“造就Google大規模可伸縮性的演算法”【2】。隨後在各種開源資料系統中得到應用,包括Hadoop,CouchDB和MongoDB。
|
||||
|
||||
@ -269,7 +269,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
單個MapReduce作業可以解決的問題範圍很有限。以日誌分析為例,單個MapReduce作業可以確定每個URL的頁面瀏覽次數,但無法確定最常見的URL,因為這需要第二輪排序。
|
||||
|
||||
因此將MapReduce作業連結成為**工作流(workflow)**中是極為常見的,例如,一個作業的輸出成為下一個作業的輸入。 Hadoop MapReduce框架對工作流沒有特殊支援,所以這個鏈是透過目錄名隱式實現的:第一個作業必須將其輸出配置為HDFS中的指定目錄,第二個作業必須將其輸入配置為從同一個目錄。從MapReduce框架的角度來看,這是兩個獨立的作業。
|
||||
因此將MapReduce作業連結成為**工作流(workflow)** 中是極為常見的,例如,一個作業的輸出成為下一個作業的輸入。 Hadoop MapReduce框架對工作流沒有特殊支援,所以這個鏈是透過目錄名隱式實現的:第一個作業必須將其輸出配置為HDFS中的指定目錄,第二個作業必須將其輸入配置為從同一個目錄。從MapReduce框架的角度來看,這是兩個獨立的作業。
|
||||
|
||||
因此,被連結的MapReduce作業並沒有那麼像Unix命令管道(它直接將一個程序的輸出作為另一個程序的輸入,僅用一個很小的記憶體緩衝區)。它更像是一系列命令,其中每個命令的輸出寫入臨時檔案,下一個命令從臨時檔案中讀取。這種設計有利也有弊,我們將在“[物化中間狀態](#物化中間狀態)”中討論。
|
||||
|
||||
@ -353,7 +353,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
如果連線的輸入存在熱鍵,可以使用一些演算法進行補償。例如,Pig中的**偏斜連線(skewed join)**方法首先執行一個抽樣作業(Sampling Job)來確定哪些鍵是熱鍵【39】。連線實際執行時,Mapper會將熱鍵的關聯記錄**隨機**(相對於傳統MapReduce基於鍵雜湊的確定性方法)傳送到幾個Reducer之一。對於另外一側的連線輸入,與熱鍵相關的記錄需要被複制到**所有**處理該鍵的Reducer上【40】。
|
||||
|
||||
這種技術將處理熱鍵的工作分散到多個Reducer上,這樣可以使其更好地並行化,代價是需要將連線另一側的輸入記錄複製到多個Reducer上。 Crunch中的**分片連線(sharded join)**方法與之類似,但需要顯式指定熱鍵而不是使用抽樣作業。這種技術也非常類似於我們在“[負載偏斜與熱點消除](ch6.md#負載偏斜與熱點消除)”中討論的技術,使用隨機化來緩解分割槽資料庫中的熱點。
|
||||
這種技術將處理熱鍵的工作分散到多個Reducer上,這樣可以使其更好地並行化,代價是需要將連線另一側的輸入記錄複製到多個Reducer上。 Crunch中的**分片連線(sharded join)** 方法與之類似,但需要顯式指定熱鍵而不是使用抽樣作業。這種技術也非常類似於我們在“[負載偏斜與熱點消除](ch6.md#負載偏斜與熱點消除)”中討論的技術,使用隨機化來緩解分割槽資料庫中的熱點。
|
||||
|
||||
Hive的偏斜連線最佳化採取了另一種方法。它需要在表格元資料中顯式指定熱鍵,並將與這些鍵相關的記錄單獨存放,與其它檔案分開。當在該表上執行連線時,對於熱鍵,它會使用Map端連線(請參閱下一節)。
|
||||
|
||||
@ -458,7 +458,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
MapReduce作業的輸出處理遵循同樣的原理。透過將輸入視為不可變且避免副作用(如寫入外部資料庫),批處理作業不僅實現了良好的效能,而且更容易維護:
|
||||
|
||||
- 如果在程式碼中引入了一個錯誤,而輸出錯誤或損壞了,則可以簡單地回滾到程式碼的先前版本,然後重新執行該作業,輸出將重新被糾正。或者,甚至更簡單,你可以將舊的輸出儲存在不同的目錄中,然後切換回原來的目錄。具有讀寫事務的資料庫沒有這個屬性:如果你部署了錯誤的程式碼,將錯誤的資料寫入資料庫,那麼回滾程式碼將無法修復資料庫中的資料。 (能夠從錯誤程式碼中恢復的概念被稱為**人類容錯(human fault tolerance)**【50】)
|
||||
- 由於回滾很容易,比起在錯誤意味著不可挽回的傷害的環境,功能開發進展能快很多。這種**最小化不可逆性(minimizing irreversibility)**的原則有利於敏捷軟體開發【51】。
|
||||
- 由於回滾很容易,比起在錯誤意味著不可挽回的傷害的環境,功能開發進展能快很多。這種**最小化不可逆性(minimizing irreversibility)** 的原則有利於敏捷軟體開發【51】。
|
||||
- 如果Map或Reduce任務失敗,MapReduce框架將自動重新排程,並在同樣的輸入上再次執行它。如果失敗是由程式碼中的錯誤造成的,那麼它會不斷崩潰,並最終導致作業在幾次嘗試之後失敗。但是如果故障是由於臨時問題導致的,那麼故障就會被容忍。因為輸入不可變,這種自動重試是安全的,而失敗任務的輸出會被MapReduce框架丟棄。
|
||||
- 同一組檔案可用作各種不同作業的輸入,包括計算指標的監控作業並且評估作業的輸出是否具有預期的性質(例如,將其與前一次執行的輸出進行比較並測量差異) 。
|
||||
- 與Unix工具類似,MapReduce作業將邏輯與佈線(配置輸入和輸出目錄)分離,這使得關注點分離,可以重用程式碼:一個團隊可以專注實現一個做好一件事的作業;而其他團隊可以決定何時何地執行這項作業。
|
||||
@ -469,7 +469,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
正如我們所看到的,Hadoop有點像Unix的分散式版本,其中HDFS是檔案系統,而MapReduce是Unix程序的怪異實現(總是在Map階段和Reduce階段執行`sort`工具)。我們瞭解瞭如何在這些原語的基礎上實現各種連線和分組操作。
|
||||
|
||||
當MapReduce論文發表時【1】,它從某種意義上來說 —— 並不新鮮。我們在前幾節中討論的所有處理和並行連線演算法已經在十多年前所謂的**大規模並行處理(MPP, massively parallel processing)**資料庫中實現了【3,40】。比如Gamma database machine,Teradata和Tandem NonStop SQL就是這方面的先驅【52】。
|
||||
當MapReduce論文發表時【1】,它從某種意義上來說 —— 並不新鮮。我們在前幾節中討論的所有處理和並行連線演算法已經在十多年前所謂的**大規模並行處理(MPP, massively parallel processing)** 資料庫中實現了【3,40】。比如Gamma database machine,Teradata和Tandem NonStop SQL就是這方面的先驅【52】。
|
||||
|
||||
最大的區別是,MPP資料庫專注於在一組機器上並行執行分析SQL查詢,而MapReduce和分散式檔案系統【19】的組合則更像是一個可以執行任意程式的通用作業系統。
|
||||
|
||||
@ -548,7 +548,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
將這個中間狀態寫入檔案的過程稱為**物化(materialization)**。 (在“[聚合:資料立方體和物化檢視](ch3.md#聚合:資料立方體和物化檢視)”中已經在物化檢視的背景中遇到過這個術語。它意味著對某個操作的結果立即求值並寫出來,而不是在請求時按需計算)
|
||||
|
||||
作為對照,本章開頭的日誌分析示例使用Unix管道將一個命令的輸出與另一個命令的輸入連線起來。管道並沒有完全物化中間狀態,而是隻使用一個小的記憶體緩衝區,將輸出增量地**流(stream)**向輸入。
|
||||
作為對照,本章開頭的日誌分析示例使用Unix管道將一個命令的輸出與另一個命令的輸入連線起來。管道並沒有完全物化中間狀態,而是隻使用一個小的記憶體緩衝區,將輸出增量地**流(stream)** 向輸入。
|
||||
|
||||
與Unix管道相比,MapReduce完全物化中間狀態的方法存在不足之處:
|
||||
|
||||
@ -587,7 +587,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
Spark,Flink和Tez避免將中間狀態寫入HDFS,因此它們採取了不同的方法來容錯:如果一臺機器發生故障,並且該機器上的中間狀態丟失,則它會從其他仍然可用的資料重新計算(在可行的情況下是先前的中間狀態,要麼就只能是原始輸入資料,通常在HDFS上)。
|
||||
|
||||
為了實現這種重新計算,框架必須跟蹤一個給定的資料是如何計算的 —— 使用了哪些輸入分割槽?應用了哪些運算元? Spark使用**彈性分散式資料集(RDD,Resilient Distributed Dataset)**的抽象來跟蹤資料的譜系【61】,而Flink對運算元狀態存檔,允許恢復執行在執行過程中遇到錯誤的運算元【66】。
|
||||
為了實現這種重新計算,框架必須跟蹤一個給定的資料是如何計算的 —— 使用了哪些輸入分割槽?應用了哪些運算元? Spark使用**彈性分散式資料集(RDD,Resilient Distributed Dataset)** 的抽象來跟蹤資料的譜系【61】,而Flink對運算元狀態存檔,允許恢復執行在執行過程中遇到錯誤的運算元【66】。
|
||||
|
||||
在重新計算資料時,重要的是要知道計算是否是**確定性的**:也就是說,給定相同的輸入資料,運算元是否始終產生相同的輸出?如果一些丟失的資料已經發送給下游運算元,這個問題就很重要。如果運算元重新啟動,重新計算的資料與原有的丟失資料不一致,下游運算元很難解決新舊資料之間的矛盾。對於不確定性運算元來說,解決方案通常是殺死下游運算元,然後再重跑新資料。
|
||||
|
||||
@ -609,7 +609,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
批處理上下文中的圖也很有趣,其目標是在整個圖上執行某種離線處理或分析。這種需求經常出現在機器學習應用(如推薦引擎)或排序系統中。例如,最著名的圖形分析演算法之一是PageRank 【69】,它試圖根據連結到某個網頁的其他網頁來估計該網頁的流行度。它作為配方的一部分,用於確定網路搜尋引擎呈現結果的順序。
|
||||
|
||||
> 像Spark,Flink和Tez這樣的資料流引擎(請參閱“[物化中間狀態](#物化中間狀態)”)通常將運算元作為**有向無環圖(DAG)**的一部分安排在作業中。這與圖處理不一樣:在資料流引擎中,**從一個運算元到另一個運算元的資料流**被構造成一個圖,而資料本身通常由關係型元組構成。在圖處理中,資料本身具有圖的形式。又一個不幸的命名混亂!
|
||||
> 像Spark,Flink和Tez這樣的資料流引擎(請參閱“[物化中間狀態](#物化中間狀態)”)通常將運算元作為**有向無環圖(DAG)** 的一部分安排在作業中。這與圖處理不一樣:在資料流引擎中,**從一個運算元到另一個運算元的資料流**被構造成一個圖,而資料本身通常由關係型元組構成。在圖處理中,資料本身具有圖的形式。又一個不幸的命名混亂!
|
||||
|
||||
許多圖演算法是透過一次遍歷一條邊來表示的,將一個頂點與近鄰的頂點連線起來,以傳播一些資訊,並不斷重複,直到滿足一些條件為止 —— 例如,直到沒有更多的邊要跟進,或直到一些指標收斂。我們在[圖2-6](../img/fig2-6.png)中看到一個例子,它透過重複跟進標明地點歸屬關係的邊,生成了資料庫中北美包含的所有地點列表(這種演算法被稱為**傳遞閉包(transitive closure)**)。
|
||||
|
||||
@ -667,7 +667,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
與硬寫執行連線的程式碼相比,指定連線關係運算元的優點是,框架可以分析連線輸入的屬性,並自動決定哪種上述連線演算法最適合當前任務。 Hive,Spark和Flink都有基於代價的查詢最佳化器可以做到這一點,甚至可以改變連線順序,最小化中間狀態的數量【66,77,78,79】。
|
||||
|
||||
連線演算法的選擇可以對批處理作業的效能產生巨大影響,而無需理解和記住本章中討論的各種連線演算法。如果連線是以**宣告式(declarative)**的方式指定的,那這就這是可行的:應用只是簡單地說明哪些連線是必需的,查詢最佳化器決定如何最好地執行連線。我們以前在“[資料查詢語言](ch2.md#資料查詢語言)”中見過這個想法。
|
||||
連線演算法的選擇可以對批處理作業的效能產生巨大影響,而無需理解和記住本章中討論的各種連線演算法。如果連線是以**宣告式(declarative)** 的方式指定的,那這就這是可行的:應用只是簡單地說明哪些連線是必需的,查詢最佳化器決定如何最好地執行連線。我們以前在“[資料查詢語言](ch2.md#資料查詢語言)”中見過這個想法。
|
||||
|
||||
但MapReduce及其資料流後繼者在其他方面,與SQL的完全宣告式查詢模型有很大區別。 MapReduce是圍繞著回撥函式的概念建立的:對於每條記錄或者一組記錄,呼叫一個使用者定義的函式(Mapper或Reducer),並且該函式可以自由地呼叫任意程式碼來決定輸出什麼。這種方法的優點是可以基於大量已有庫的生態系統創作:解析、自然語言分析、影象分析以及執行數值或統計演算法等。
|
||||
|
||||
|
@ -10,17 +10,17 @@
|
||||
|
||||
[TOC]
|
||||
|
||||
在[第十章](ch10.md)中,我們討論了批處理技術,它讀取一組檔案作為輸入,並生成一組新的檔案作為輸出。輸出是**衍生資料(derived data)**的一種形式;也就是說,如果需要,可以透過再次執行批處理過程來重新建立資料集。我們看到了如何使用這個簡單而強大的想法來建立搜尋索引、推薦系統、做分析等等。
|
||||
在[第十章](ch10.md)中,我們討論了批處理技術,它讀取一組檔案作為輸入,並生成一組新的檔案作為輸出。輸出是**衍生資料(derived data)** 的一種形式;也就是說,如果需要,可以透過再次執行批處理過程來重新建立資料集。我們看到了如何使用這個簡單而強大的想法來建立搜尋索引、推薦系統、做分析等等。
|
||||
|
||||
然而,在[第十章](ch10.md)中仍然有一個很大的假設:即輸入是有界的,即已知和有限的大小,所以批處理知道它何時完成輸入的讀取。例如,MapReduce核心的排序操作必須讀取其全部輸入,然後才能開始生成輸出:可能發生這種情況:最後一條輸入記錄具有最小的鍵,因此需要第一個被輸出,所以提早開始輸出是不可行的。
|
||||
|
||||
實際上,很多資料是**無界限**的,因為它隨著時間的推移而逐漸到達:你的使用者在昨天和今天產生了資料,明天他們將繼續產生更多的資料。除非你停業,否則這個過程永遠都不會結束,所以資料集從來就不會以任何有意義的方式“完成”【1】。因此,批處理程式必須將資料人為地分成固定時間段的資料塊,例如,在每天結束時處理一天的資料,或者在每小時結束時處理一小時的資料。
|
||||
|
||||
日常批處理中的問題是,輸入的變更只會在一天之後的輸出中反映出來,這對於許多急躁的使用者來說太慢了。為了減少延遲,我們可以更頻繁地執行處理 —— 比如說,在每秒鐘的末尾 —— 或者甚至更連續一些,完全拋開固定的時間切片,當事件發生時就立即進行處理,這就是**流處理(stream processing)**背後的想法。
|
||||
日常批處理中的問題是,輸入的變更只會在一天之後的輸出中反映出來,這對於許多急躁的使用者來說太慢了。為了減少延遲,我們可以更頻繁地執行處理 —— 比如說,在每秒鐘的末尾 —— 或者甚至更連續一些,完全拋開固定的時間切片,當事件發生時就立即進行處理,這就是**流處理(stream processing)** 背後的想法。
|
||||
|
||||
一般來說,“流”是指隨著時間的推移逐漸可用的資料。這個概念出現在很多地方:Unix的stdin和stdout,程式語言(惰性列表)【2】,檔案系統API(如Java的`FileInputStream`),TCP連線,透過網際網路傳送音訊和影片等等。
|
||||
|
||||
在本章中,我們將把**事件流(event stream)**視為一種資料管理機制:無界限,增量處理,與上一章中的批次資料相對應。我們將首先討論怎樣表示、儲存、透過網路傳輸流。在“[資料庫與流](#資料庫與流)”中,我們將研究流和資料庫之間的關係。最後在“[流處理](#流處理)”中,我們將研究連續處理這些流的方法和工具,以及它們用於應用構建的方式。
|
||||
在本章中,我們將把**事件流(event stream)** 視為一種資料管理機制:無界限,增量處理,與上一章中的批次資料相對應。我們將首先討論怎樣表示、儲存、透過網路傳輸流。在“[資料庫與流](#資料庫與流)”中,我們將研究流和資料庫之間的關係。最後在“[流處理](#流處理)”中,我們將研究連續處理這些流的方法和工具,以及它們用於應用構建的方式。
|
||||
|
||||
|
||||
## 傳遞事件流
|
||||
@ -50,11 +50,11 @@
|
||||
|
||||
在這個**釋出/訂閱**模式中,不同的系統採取各種各樣的方法,並沒有針對所有目的的通用答案。為了區分這些系統,問一下這兩個問題會特別有幫助:
|
||||
|
||||
1. **如果生產者傳送訊息的速度比消費者能夠處理的速度快會發生什麼?**一般來說,有三種選擇:系統可以丟掉訊息,將訊息放入緩衝佇列,或使用**背壓(backpressure)**(也稱為**流量控制(flow control)**;即阻塞生產者,以免其傳送更多的訊息)。例如Unix管道和TCP就使用了背壓:它們有一個固定大小的小緩衝區,如果填滿,傳送者會被阻塞,直到接收者從緩衝區中取出資料(請參閱“[網路擁塞和排隊](ch8.md#網路擁塞和排隊)”)。
|
||||
1. **如果生產者傳送訊息的速度比消費者能夠處理的速度快會發生什麼?** 一般來說,有三種選擇:系統可以丟掉訊息,將訊息放入緩衝佇列,或使用**背壓(backpressure)**(也稱為**流量控制(flow control)**;即阻塞生產者,以免其傳送更多的訊息)。例如Unix管道和TCP就使用了背壓:它們有一個固定大小的小緩衝區,如果填滿,傳送者會被阻塞,直到接收者從緩衝區中取出資料(請參閱“[網路擁塞和排隊](ch8.md#網路擁塞和排隊)”)。
|
||||
|
||||
如果訊息被快取在佇列中,那麼理解佇列增長會發生什麼是很重要的。當佇列裝不進記憶體時系統會崩潰嗎?還是將訊息寫入磁碟?如果是這樣,磁碟訪問又會如何影響訊息傳遞系統的效能【6】?
|
||||
|
||||
2. **如果節點崩潰或暫時離線,會發生什麼情況? —— 是否會有訊息丟失?**與資料庫一樣,永續性可能需要寫入磁碟和/或複製的某種組合(請參閱“[複製與永續性](ch7.md#複製與永續性)”),這是有代價的。如果你能接受有時訊息會丟失,則可能在同一硬體上獲得更高的吞吐量和更低的延遲。
|
||||
2. **如果節點崩潰或暫時離線,會發生什麼情況? —— 是否會有訊息丟失?** 與資料庫一樣,永續性可能需要寫入磁碟和/或複製的某種組合(請參閱“[複製與永續性](ch7.md#複製與永續性)”),這是有代價的。如果你能接受有時訊息會丟失,則可能在同一硬體上獲得更高的吞吐量和更低的延遲。
|
||||
|
||||
是否可以接受訊息丟失取決於應用。例如,對於週期傳輸的感測器讀數和指標,偶爾丟失的資料點可能並不重要,因為更新的值會在短時間內發出。但要注意,如果大量的訊息被丟棄,可能無法立刻意識到指標已經不正確了【7】。如果你正在對事件計數,那麼它們能夠可靠送達是更重要的,因為每個丟失的訊息都意味著使計數器的錯誤擴大。
|
||||
|
||||
@ -79,7 +79,7 @@
|
||||
|
||||
透過將資料集中在代理上,這些系統可以更容易地容忍來來去去的客戶端(連線,斷開連線和崩潰),而永續性問題則轉移到代理的身上。一些訊息代理只將訊息儲存在記憶體中,而另一些訊息代理(取決於配置)將其寫入磁碟,以便在代理崩潰的情況下不會丟失。針對緩慢的消費者,它們通常會允許無上限的排隊(而不是丟棄訊息或背壓),儘管這種選擇也可能取決於配置。
|
||||
|
||||
排隊的結果是,消費者通常是**非同步(asynchronous)**的:當生產者傳送訊息時,通常只會等待代理確認訊息已經被快取,而不等待訊息被消費者處理。向消費者遞送訊息將發生在未來某個未定的時間點 —— 通常在幾分之一秒之內,但有時當訊息堆積時會顯著延遲。
|
||||
排隊的結果是,消費者通常是**非同步(asynchronous)** 的:當生產者傳送訊息時,通常只會等待代理確認訊息已經被快取,而不等待訊息被消費者處理。向消費者遞送訊息將發生在未來某個未定的時間點 —— 通常在幾分之一秒之內,但有時當訊息堆積時會顯著延遲。
|
||||
|
||||
#### 訊息代理與資料庫的對比
|
||||
|
||||
@ -299,14 +299,14 @@
|
||||
|
||||
與變更資料捕獲類似,事件溯源涉及到**將所有對應用狀態的變更**儲存為變更事件日誌。最大的區別是事件溯源將這一想法應用到了一個不同的抽象層次上:
|
||||
|
||||
* 在變更資料捕獲中,應用以**可變方式(mutable way)**使用資料庫,可以任意更新和刪除記錄。變更日誌是從資料庫的底層提取的(例如,透過解析複製日誌),從而確保從資料庫中提取的寫入順序與實際寫入的順序相匹配,從而避免[圖11-4](../img/fig11-4.png)中的競態條件。寫入資料庫的應用不需要知道CDC的存在。
|
||||
* 在變更資料捕獲中,應用以**可變方式(mutable way)** 使用資料庫,可以任意更新和刪除記錄。變更日誌是從資料庫的底層提取的(例如,透過解析複製日誌),從而確保從資料庫中提取的寫入順序與實際寫入的順序相匹配,從而避免[圖11-4](../img/fig11-4.png)中的競態條件。寫入資料庫的應用不需要知道CDC的存在。
|
||||
* 在事件溯源中,應用邏輯顯式構建在寫入事件日誌的不可變事件之上。在這種情況下,事件儲存是僅追加寫入的,更新與刪除是不鼓勵的或禁止的。事件被設計為旨在反映應用層面發生的事情,而不是底層的狀態變更。
|
||||
|
||||
事件溯源是一種強大的資料建模技術:從應用的角度來看,將使用者的行為記錄為不可變的事件更有意義,而不是在可變資料庫中記錄這些行為的影響。事件溯源使得應用隨時間演化更為容易,透過更容易理解事情發生的原因來幫助除錯的進行,並有利於防止應用Bug(請參閱“[不可變事件的優點](#不可變事件的優點)”)。
|
||||
|
||||
例如,儲存“學生取消選課”事件以中性的方式清楚地表達了單個行為的意圖,而其副作用“從登記表中刪除了一個條目,而一條取消原因的記錄被新增到學生反饋表“則嵌入了很多有關稍後對資料的使用方式的假設。如果引入一個新的應用功能,例如“將位置留給等待列表中的下一個人” —— 事件溯源方法允許將新的副作用輕鬆地從現有事件中脫開。
|
||||
|
||||
事件溯源類似於**編年史(chronicle)**資料模型【45】,事件日誌與星型模式中的事實表之間也存在相似之處(請參閱“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”) 。
|
||||
事件溯源類似於**編年史(chronicle)** 資料模型【45】,事件日誌與星型模式中的事實表之間也存在相似之處(請參閱“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”) 。
|
||||
|
||||
諸如Event Store【46】這樣的專業資料庫已經被開發出來,供使用事件溯源的應用使用,但總的來說,這種方法獨立於任何特定的工具。傳統的資料庫或基於日誌的訊息代理也可以用來構建這種風格的應用。
|
||||
|
||||
@ -643,7 +643,7 @@ GROUP BY follows.follower_id
|
||||
|
||||
微批次也隱式提供了一個與批次大小相等的滾動視窗(按處理時間而不是事件時間戳分窗)。任何需要更大視窗的作業都需要顯式地將狀態從一個微批次轉移到下一個微批次。
|
||||
|
||||
Apache Flink則使用不同的方法,它會定期生成狀態的滾動存檔點並將其寫入持久儲存【92,93】。如果流運算元崩潰,它可以從最近的存檔點重啟,並丟棄從最近檢查點到崩潰之間的所有輸出。存檔點會由訊息流中的**壁障(barrier)**觸發,類似於微批次之間的邊界,但不會強制一個特定的視窗大小。
|
||||
Apache Flink則使用不同的方法,它會定期生成狀態的滾動存檔點並將其寫入持久儲存【92,93】。如果流運算元崩潰,它可以從最近的存檔點重啟,並丟棄從最近檢查點到崩潰之間的所有輸出。存檔點會由訊息流中的**壁障(barrier)** 觸發,類似於微批次之間的邊界,但不會強制一個特定的視窗大小。
|
||||
|
||||
在流處理框架的範圍內,微批次與存檔點方法提供了與批處理一樣的**恰好一次語義**。但是,只要輸出離開流處理器(例如,寫入資料庫,向外部訊息代理髮送訊息,或傳送電子郵件),框架就無法拋棄失敗批次的輸出了。在這種情況下,重啟失敗任務會導致外部副作用發生兩次,只有微批次或存檔點不足以阻止這一問題。
|
||||
|
||||
|
@ -197,7 +197,7 @@
|
||||
|
||||
**聯合資料庫:統一讀取**
|
||||
|
||||
可以為各種各樣的底層儲存引擎和處理方法提供一個統一的查詢介面 —— 一種稱為**聯合資料庫(federated database)**或**多型儲存(polystore)**的方法【18,19】。例如,PostgreSQL的**外部資料包裝器(foreign data wrapper)**功能符合這種模式【20】。需要專用資料模型或查詢介面的應用程式仍然可以直接訪問底層儲存引擎,而想要組合來自不同位置的資料的使用者可以透過聯合介面輕鬆完成操作。
|
||||
可以為各種各樣的底層儲存引擎和處理方法提供一個統一的查詢介面 —— 一種稱為**聯合資料庫(federated database)** 或**多型儲存(polystore)** 的方法【18,19】。例如,PostgreSQL的**外部資料包裝器(foreign data wrapper)** 功能符合這種模式【20】。需要專用資料模型或查詢介面的應用程式仍然可以直接訪問底層儲存引擎,而想要組合來自不同位置的資料的使用者可以透過聯合介面輕鬆完成操作。
|
||||
|
||||
聯合查詢介面遵循著單一整合系統的關係型傳統,帶有高階查詢語言和優雅的語義,但實現起來非常複雜。
|
||||
|
||||
@ -373,7 +373,7 @@
|
||||
|
||||
最近用於開發有狀態的客戶端與使用者介面的工具,例如如Elm語言【30】和Facebook的React,Flux和Redux工具鏈,已經透過訂閱表示使用者輸入或伺服器響應的事件流來管理客戶端的內部狀態,其結構與事件溯源相似(請參閱“[事件溯源](ch11.md#事件溯源)”)。
|
||||
|
||||
將這種程式設計模型擴充套件為:允許伺服器將狀態變更事件推送到客戶端的事件管道中,是非常自然的。因此,狀態變化可以透過**端到端(end-to-end)**的寫路徑流動:從一個裝置上的互動觸發狀態變更開始,經由事件日誌,並穿過幾個衍生資料系統與流處理器,一直到另一臺裝置上的使用者介面,而有人正在觀察使用者介面上的狀態變化。這些狀態變化能以相當低的延遲傳播 —— 比如說,在一秒內從一端到另一端。
|
||||
將這種程式設計模型擴充套件為:允許伺服器將狀態變更事件推送到客戶端的事件管道中,是非常自然的。因此,狀態變化可以透過**端到端(end-to-end)** 的寫路徑流動:從一個裝置上的互動觸發狀態變更開始,經由事件日誌,並穿過幾個衍生資料系統與流處理器,一直到另一臺裝置上的使用者介面,而有人正在觀察使用者介面上的狀態變化。這些狀態變化能以相當低的延遲傳播 —— 比如說,在一秒內從一端到另一端。
|
||||
|
||||
一些應用(如即時訊息傳遞與線上遊戲)已經具有這種“實時”架構(在低延遲互動的意義上,不是在“[響應時間保證](ch8.md#響應時間保證)”中的意義上)。但我們為什麼不用這種方式構建所有的應用?
|
||||
|
||||
@ -465,7 +465,7 @@ COMMIT;
|
||||
|
||||
#### 操作識別符號
|
||||
|
||||
要在通過幾跳的網路通訊上使操作具有冪等性,僅僅依賴資料庫提供的事務機制是不夠的 —— 你需要考慮**端到端(end-to-end)**的請求流。
|
||||
要在通過幾跳的網路通訊上使操作具有冪等性,僅僅依賴資料庫提供的事務機制是不夠的 —— 你需要考慮**端到端(end-to-end)** 的請求流。
|
||||
例如,你可以為操作生成一個唯一的識別符號(例如UUID),並將其作為隱藏表單欄位包含在客戶端應用中,或透過計算所有表單相關欄位的雜湊來生成操作ID 【3】。如果Web瀏覽器提交了兩次POST請求,這兩個請求將具有相同的操作ID。然後,你可以將該操作ID一路傳遞到資料庫,並檢查你是否曾經使用給定的ID執行過一個操作,如[例12-2]()中所示。
|
||||
|
||||
**例12-2 使用唯一ID來抑制重複請求**
|
||||
@ -635,7 +635,7 @@ COMMIT;
|
||||
1. 資料流系統可以維持衍生資料的完整性保證,而無需原子提交、線性一致性或者同步的跨分割槽協調。
|
||||
2. 雖然嚴格的唯一性約束要求及時性和協調,但許多應用實際上可以接受寬鬆的約束:只要整個過程保持完整性,這些約束可能會被臨時違反並在稍後被修復。
|
||||
|
||||
總之這些觀察意味著,資料流系統可以為許多應用提供無需協調的資料管理服務,且仍能給出很強的完整性保證。這種**無協調(coordination-avoiding)**的資料系統有著很大的吸引力:比起需要執行同步協調的系統,它們能達到更好的效能與更強的容錯能力【56】。
|
||||
總之這些觀察意味著,資料流系統可以為許多應用提供無需協調的資料管理服務,且仍能給出很強的完整性保證。這種**無協調(coordination-avoiding)** 的資料系統有著很大的吸引力:比起需要執行同步協調的系統,它們能達到更好的效能與更強的容錯能力【56】。
|
||||
|
||||
例如,這種系統可以使用多領導者配置運維,跨越多個數據中心,在區域間非同步複製。任何一個數據中心都可以持續獨立執行,因為不需要同步的跨區域協調。這樣的系統的及時性保證會很弱 —— 如果不引入協調它是不可能是線性一致的 —— 但它仍然可以提供有力的完整性保證。
|
||||
|
||||
@ -711,7 +711,7 @@ COMMIT;
|
||||
|
||||
我對這些技術的拜占庭容錯方面有些懷疑(請參閱“[拜占庭故障](ch8.md#拜占庭故障)”),而且我發現**工作證明(proof of work)** 技術非常浪費(比如,比特幣挖礦)。比特幣的交易吞吐量相當低,儘管更多是出於政治與經濟原因而非技術上的原因。不過,完整性檢查的方面是很有趣的。
|
||||
|
||||
密碼學審計與完整性檢查通常依賴**默克爾樹(Merkle tree)**【74】,這是一顆雜湊值的樹,能夠用於高效地證明一條記錄出現在一個數據集中(以及其他一些特性)。除了炒作的沸沸揚揚的加密貨幣之外,**證書透明性(certificate transparency)**也是一種依賴Merkle樹的安全技術,用來檢查TLS/SSL證書的有效性【75,76】。
|
||||
密碼學審計與完整性檢查通常依賴**默克爾樹(Merkle tree)**【74】,這是一顆雜湊值的樹,能夠用於高效地證明一條記錄出現在一個數據集中(以及其他一些特性)。除了炒作的沸沸揚揚的加密貨幣之外,**證書透明性(certificate transparency)** 也是一種依賴Merkle樹的安全技術,用來檢查TLS/SSL證書的有效性【75,76】。
|
||||
|
||||
我可以想象,那些在證書透明度與分散式賬本中使用的完整性檢查和審計演算法,將會在通用資料系統中得到越來越廣泛的應用。要使得這些演算法對於沒有密碼學審計的系統同樣可伸縮,並儘可能降低效能損失還需要一些工作。 但我認為這是一個值得關注的有趣領域。
|
||||
|
||||
@ -766,7 +766,7 @@ COMMIT;
|
||||
|
||||
當預測性分析影響人們的生活時,自我強化的反饋迴圈會導致非常有害的問題。例如,考慮僱主使用信用分來評估候選人的例子。你可能是一個信用分不錯的好員工,但因不可抗力的意外而陷入財務困境。由於不能按期付賬單,你的信用分會受到影響,進而導致找到工作更為困難。失業使你陷入貧困,這進一步惡化了你的分數,使你更難找到工作【87】。在資料與數學嚴謹性的偽裝背後,隱藏的是由惡毒假設導致的惡性迴圈。
|
||||
|
||||
我們無法預測這種反饋迴圈何時發生。然而透過對整個系統(不僅僅是計算機化的部分,而且還有與之互動的人)進行整體思考,許多後果是可以夠預測的 —— 一種稱為**系統思維(systems thinking)**的方法【92】。我們可以嘗試理解資料分析系統如何響應不同的行為,結構或特性。該系統是否加強和增大了人們之間現有的差異(例如,損不足以奉有餘,富者愈富,貧者愈貧),還是試圖與不公作鬥爭?而且即使有著最好的動機,我們也必須當心意想不到的後果。
|
||||
我們無法預測這種反饋迴圈何時發生。然而透過對整個系統(不僅僅是計算機化的部分,而且還有與之互動的人)進行整體思考,許多後果是可以夠預測的 —— 一種稱為**系統思維(systems thinking)** 的方法【92】。我們可以嘗試理解資料分析系統如何響應不同的行為,結構或特性。該系統是否加強和增大了人們之間現有的差異(例如,損不足以奉有餘,富者愈富,貧者愈貧),還是試圖與不公作鬥爭?而且即使有著最好的動機,我們也必須當心意想不到的後果。
|
||||
|
||||
### 隱私和追蹤
|
||||
|
||||
@ -806,7 +806,7 @@ COMMIT;
|
||||
|
||||
#### 隱私與資料使用
|
||||
|
||||
有時候,人們聲稱“隱私已死”,理由是有些使用者願意把各種關於他們生活的事情釋出到社交媒體上,有時是平凡俗套,但有時是高度私密的。但這種說法是錯誤的,而且是對**隱私(privacy)**一詞的誤解。
|
||||
有時候,人們聲稱“隱私已死”,理由是有些使用者願意把各種關於他們生活的事情釋出到社交媒體上,有時是平凡俗套,但有時是高度私密的。但這種說法是錯誤的,而且是對**隱私(privacy)** 一詞的誤解。
|
||||
|
||||
擁有隱私並不意味著保密一切東西;它意味著擁有選擇向誰展示哪些東西的自由,要公開什麼,以及要保密什麼。**隱私權是一項決定權**:在從保密到透明的光譜上,隱私使得每個人都能決定自己想要在什麼地方位於光譜上的哪個位置【99】。這是一個人自由與自主的重要方面。
|
||||
|
||||
@ -868,13 +868,13 @@ COMMIT;
|
||||
|
||||
## 本章小結
|
||||
|
||||
在本章中,我們討論了設計資料系統的新方式,而且也包括了我的個人觀點,以及對未來的猜測。我們從這樣一種觀察開始:沒有單種工具能高效服務所有可能的用例,因此應用必須組合使用幾種不同的軟體才能實現其目標。我們討論瞭如何使用批處理與事件流來解決這一**資料整合(data integration)**問題,以便讓資料變更在不同系統之間流動。
|
||||
在本章中,我們討論了設計資料系統的新方式,而且也包括了我的個人觀點,以及對未來的猜測。我們從這樣一種觀察開始:沒有單種工具能高效服務所有可能的用例,因此應用必須組合使用幾種不同的軟體才能實現其目標。我們討論瞭如何使用批處理與事件流來解決這一**資料整合(data integration)** 問題,以便讓資料變更在不同系統之間流動。
|
||||
|
||||
在這種方法中,某些系統被指定為記錄系統,而其他資料則透過轉換衍生自記錄系統。透過這種方式,我們可以維護索引,物化檢視,機器學習模型,統計摘要等等。透過使這些衍生和轉換操作非同步且鬆散耦合,能夠防止一個區域中的問題擴散到系統中不相關部分,從而增加整個系統的穩健性與容錯性。
|
||||
|
||||
將資料流表示為從一個數據集到另一個數據集的轉換也有助於演化應用程式:如果你想變更其中一個處理步驟,例如變更索引或快取的結構,則可以在整個輸入資料集上重新執行新的轉換程式碼,以便重新衍生輸出。同樣,出現問題時,你也可以修復程式碼並重新處理資料以便恢復。
|
||||
|
||||
這些過程與資料庫內部已經完成的過程非常類似,因此我們將資料流應用的概念重新改寫為,**分拆(unbundling)**資料庫元件,並透過組合這些鬆散耦合的元件來構建應用程式。
|
||||
這些過程與資料庫內部已經完成的過程非常類似,因此我們將資料流應用的概念重新改寫為,**分拆(unbundling)** 資料庫元件,並透過組合這些鬆散耦合的元件來構建應用程式。
|
||||
|
||||
衍生狀態可以透過觀察底層資料的變更來更新。此外,衍生狀態本身可以進一步被下游消費者觀察。我們甚至可以將這種資料流一路傳送至顯示資料的終端使用者裝置,從而構建可動態更新以反映資料變更,並在離線時能繼續工作的使用者介面。
|
||||
|
||||
|
@ -559,7 +559,7 @@
|
||||
* 如果寫操作與讀操作同時發生,寫操作可能僅反映在某些副本上。在這種情況下,不確定讀取是返回舊值還是新值。
|
||||
* 如果寫操作在某些副本上成功,而在其他節點上失敗(例如,因為某些節點上的磁碟已滿),在小於w個副本上寫入成功。所以整體判定寫入失敗,但整體寫入失敗並沒有在寫入成功的副本上回滾。這意味著如果一個寫入雖然報告失敗,後續的讀取仍然可能會讀取這次失敗寫入的值【47】。
|
||||
* 如果攜帶新值的節點失敗,需要讀取其他帶有舊值的副本。並且其資料從帶有舊值的副本中恢復,則儲存新值的副本數可能會低於w,從而打破法定人數條件。
|
||||
* 即使一切工作正常,有時也會不幸地出現關於**時序(timing)** 的邊緣情況,我們將在**“[線性一致性和法定人數](ch9.md#線性一致性和法定人數)”中看到這點。
|
||||
* 即使一切工作正常,有時也會不幸地出現關於**時序(timing)** 的邊緣情況,我們將在“[線性一致性和法定人數](ch9.md#線性一致性和法定人數)”中看到這點。
|
||||
|
||||
因此,儘管法定人數似乎保證讀取返回最新的寫入值,但在實踐中並不那麼簡單。 Dynamo風格的資料庫通常針對可以忍受最終一致性的用例進行最佳化。允許透過引數w和r來調整讀取陳舊值的概率,但把它們當成絕對的保證是不明智的。
|
||||
|
||||
|
@ -29,7 +29,7 @@
|
||||
|
||||
怎樣知道你是否需要事務?為了回答這個問題,首先需要確切理解事務可以提供的安全保障,以及它們的代價。儘管乍看事務似乎很簡單,但實際上有許多微妙但重要的細節在起作用。
|
||||
|
||||
本章將研究許多出錯案例,並探索資料庫用於防範這些問題的演算法。尤其會深入**併發控制**的領域,討論各種可能發生的競爭條件,以及資料庫如何實現**讀已提交(read committed)**,**快照隔離(snapshot isolation)**和**可序列化(serializability)**等隔離級別。
|
||||
本章將研究許多出錯案例,並探索資料庫用於防範這些問題的演算法。尤其會深入**併發控制**的領域,討論各種可能發生的競爭條件,以及資料庫如何實現**讀已提交(read committed)**,**快照隔離(snapshot isolation)** 和**可序列化(serializability)** 等隔離級別。
|
||||
|
||||
本章同時適用於單機資料庫與分散式資料庫;在[第八章](ch8.md)中將重點討論僅出現在分散式系統中的特殊挑戰。
|
||||
|
||||
|
38
zh-tw/ch8.md
38
zh-tw/ch8.md
@ -39,7 +39,7 @@
|
||||
|
||||
當你編寫執行在多臺計算機上的軟體時,情況有本質上的區別。在分散式系統中,我們不再處於理想化的系統模型中,我們別無選擇,只能面對現實世界的混亂現實。而在現實世界中,各種各樣的事情都可能會出現問題【4】,如下面的軼事所述:
|
||||
|
||||
> 在我有限的經驗中,我已經和很多東西打過交道:單個**資料中心(DC)**中長期存在的網路分割槽,配電單元PDU故障,交換機故障,整個機架的意外重啟,整個資料中心主幹網路故障,整個資料中心的電源故障,以及一個低血糖的司機把他的福特皮卡撞在資料中心的HVAC(加熱,通風和空調)系統上。而且我甚至不是一個運維。
|
||||
> 在我有限的經驗中,我已經和很多東西打過交道:單個**資料中心(DC)** 中長期存在的網路分割槽,配電單元PDU故障,交換機故障,整個機架的意外重啟,整個資料中心主幹網路故障,整個資料中心的電源故障,以及一個低血糖的司機把他的福特皮卡撞在資料中心的HVAC(加熱,通風和空調)系統上。而且我甚至不是一個運維。
|
||||
>
|
||||
> ——柯達黑爾
|
||||
|
||||
@ -59,9 +59,9 @@
|
||||
|
||||
在本書中,我們將重點放在實現網際網路服務的系統上,這些系統通常與超級計算機看起來有很大不同:
|
||||
|
||||
* 許多與網際網路有關的應用程式都是**線上(online)**的,因為它們需要能夠隨時以低延遲服務使用者。使服務不可用(例如,停止叢集以進行修復)是不可接受的。相比之下,像天氣模擬這樣的離線(批處理)工作可以停止並重新啟動,影響相當小。
|
||||
* 許多與網際網路有關的應用程式都是**線上(online)** 的,因為它們需要能夠隨時以低延遲服務使用者。使服務不可用(例如,停止叢集以進行修復)是不可接受的。相比之下,像天氣模擬這樣的離線(批處理)工作可以停止並重新啟動,影響相當小。
|
||||
|
||||
* 超級計算機通常由專用硬體構建而成,每個節點相當可靠,節點透過共享記憶體和**遠端直接記憶體訪問(RDMA)**進行通訊。另一方面,雲服務中的節點是由商用機器構建而成的,由於規模經濟,可以以較低的成本提供相同的效能,而且具有較高的故障率。
|
||||
* 超級計算機通常由專用硬體構建而成,每個節點相當可靠,節點透過共享記憶體和**遠端直接記憶體訪問(RDMA)** 進行通訊。另一方面,雲服務中的節點是由商用機器構建而成的,由於規模經濟,可以以較低的成本提供相同的效能,而且具有較高的故障率。
|
||||
|
||||
* 大型資料中心網路通常基於IP和乙太網,以CLOS拓撲排列,以提供更高的對分(bisection)頻寬【9】。超級計算機通常使用專門的網路拓撲結構,例如多維網格和Torus網路 【10】,這為具有已知通訊模式的HPC工作負載提供了更好的效能。
|
||||
|
||||
@ -82,7 +82,7 @@
|
||||
> 您可能想知道這是否有意義——直觀地看來,系統只能像其最不可靠的元件(最薄弱的環節)一樣可靠。事實並非如此:事實上,從不太可靠的潛在基礎構建更可靠的系統是計算機領域的一個古老思想【11】。例如:
|
||||
>
|
||||
> * 糾錯碼允許數字資料在通訊通道上準確傳輸,偶爾會出現一些錯誤,例如由於無線網路上的無線電干擾【12】。
|
||||
> * **網際網路協議(Internet Protocol, IP)**不可靠:可能丟棄,延遲,重複或重排資料包。 傳輸控制協議(Transmission Control Protocol, TCP)在網際網路協議(IP)之上提供了更可靠的傳輸層:它確保丟失的資料包被重新傳輸,消除重複,並且資料包被重新組裝成它們被髮送的順序。
|
||||
> * **網際網路協議(Internet Protocol, IP)** 不可靠:可能丟棄,延遲,重複或重排資料包。 傳輸控制協議(Transmission Control Protocol, TCP)在網際網路協議(IP)之上提供了更可靠的傳輸層:它確保丟失的資料包被重新傳輸,消除重複,並且資料包被重新組裝成它們被髮送的順序。
|
||||
>
|
||||
> 雖然這個系統可以比它的底層部分更可靠,但它的可靠性總是有限的。例如,糾錯碼可以處理少量的單位元錯誤,但是如果你的訊號被幹擾所淹沒,那麼透過通道可以得到多少資料,是有根本性的限制的【13】。 TCP可以隱藏資料包的丟失,重複和重新排序,但是它不能神奇地消除網路中的延遲。
|
||||
>
|
||||
@ -211,13 +211,13 @@
|
||||
|
||||
如果資料中心網路和網際網路是電路交換網路,那麼在建立電路時就可以建立一個受保證的最大往返時間。但是,它們並不是:乙太網和IP是**分組交換協議**,不得不忍受排隊的折磨,及其導致的網路無限延遲。這些協議沒有電路的概念。
|
||||
|
||||
為什麼資料中心網路和網際網路使用分組交換?答案是,它們針對**突發流量(bursty traffic)**進行了最佳化。一個電路適用於音訊或影片通話,在通話期間需要每秒傳送相當數量的位元。另一方面,請求網頁,傳送電子郵件或傳輸檔案沒有任何特定的頻寬要求——我們只是希望它儘快完成。
|
||||
為什麼資料中心網路和網際網路使用分組交換?答案是,它們針對**突發流量(bursty traffic)** 進行了最佳化。一個電路適用於音訊或影片通話,在通話期間需要每秒傳送相當數量的位元。另一方面,請求網頁,傳送電子郵件或傳輸檔案沒有任何特定的頻寬要求——我們只是希望它儘快完成。
|
||||
|
||||
如果想透過電路傳輸檔案,你得預測一個頻寬分配。如果你猜的太低,傳輸速度會不必要的太慢,導致網路容量閒置。如果你猜的太高,電路就無法建立(因為如果無法保證其頻寬分配,網路不能建立電路)。因此,將電路用於突發資料傳輸會浪費網路容量,並且使傳輸不必要地緩慢。相比之下,TCP動態調整資料傳輸速率以適應可用的網路容量。
|
||||
|
||||
已經有一些嘗試去建立同時支援電路交換和分組交換的混合網路,比如ATM[^iii]。InfiniBand有一些相似之處【35】:它在鏈路層實現了端到端的流量控制,從而減少了在網路中排隊的需要,儘管它仍然可能因鏈路擁塞而受到延遲【36】。透過仔細使用**服務質量(quality of service,)**(QoS,資料包的優先順序和排程)和**准入控制(admission control)**(限速傳送器),可以在分組網路上類比電路交換,或提供統計上的**有限延遲**【25,32】。
|
||||
|
||||
[^iii]: **非同步傳輸模式(Asynchronous Transfer Mode, ATM)**在20世紀80年代是乙太網的競爭對手【32】,但在電話網核心交換機之外並沒有得到太多的採用。它與自動櫃員機(也稱為自動取款機)無關,儘管共用一個縮寫詞。或許,在一些平行的世界裡,網際網路是基於像ATM這樣的東西,因此它們的網際網路影片通話可能比我們的更可靠,因為它們不會遭受包的丟失和延遲。
|
||||
[^iii]: **非同步傳輸模式(Asynchronous Transfer Mode, ATM)** 在20世紀80年代是乙太網的競爭對手【32】,但在電話網核心交換機之外並沒有得到太多的採用。它與自動櫃員機(也稱為自動取款機)無關,儘管共用一個縮寫詞。或許,在一些平行的世界裡,網際網路是基於像ATM這樣的東西,因此它們的網際網路影片通話可能比我們的更可靠,因為它們不會遭受包的丟失和延遲。
|
||||
|
||||
但是,目前在多租戶資料中心和公共雲或透過網際網路[^iv]進行通訊時,此類服務質量尚未啟用。當前部署的技術不允許我們對網路的延遲或可靠性作出任何保證:我們必須假設網路擁塞,排隊和無限的延遲總是會發生。因此,超時時間沒有“正確”的值——它需要透過實驗來確定。
|
||||
|
||||
@ -279,7 +279,7 @@
|
||||
|
||||
在具有多個CPU插槽的伺服器上,每個CPU可能有一個單獨的計時器,但不一定與其他CPU同步。作業系統會補償所有的差異,並嘗試嚮應用執行緒表現出單調鐘的樣子,即使這些執行緒被排程到不同的CPU上。當然,明智的做法是不要太把這種單調性保證當回事【40】。
|
||||
|
||||
如果NTP協議檢測到計算機的本地石英鐘比NTP伺服器要更快或更慢,則可以調整單調鍾向前走的頻率(這稱為**偏移(skewing)**時鐘)。預設情況下,NTP允許時鐘速率增加或減慢最高至0.05%,但NTP不能使單調時鐘向前或向後跳轉。單調時鐘的解析度通常相當好:在大多數系統中,它們能在幾微秒或更短的時間內測量時間間隔。
|
||||
如果NTP協議檢測到計算機的本地石英鐘比NTP伺服器要更快或更慢,則可以調整單調鍾向前走的頻率(這稱為**偏移(skewing)** 時鐘)。預設情況下,NTP允許時鐘速率增加或減慢最高至0.05%,但NTP不能使單調時鐘向前或向後跳轉。單調時鐘的解析度通常相當好:在大多數系統中,它們能在幾微秒或更短的時間內測量時間間隔。
|
||||
|
||||
在分散式系統中,使用單調鍾測量**經過時間(elapsed time)**(比如超時)通常很好,因為它不假定不同節點的時鐘之間存在任何同步,並且對測量的輕微不準確性不敏感。
|
||||
|
||||
@ -399,14 +399,14 @@ while (true) {
|
||||
假設一個執行緒可能會暫停很長時間,這是瘋了嗎?不幸的是,這種情況發生的原因有很多種:
|
||||
|
||||
* 許多程式語言執行時(如Java虛擬機器)都有一個垃圾收集器(GC),偶爾需要停止所有正在執行的執行緒。這些“**停止所有處理(stop-the-world)**”GC暫停有時會持續幾分鐘【64】!甚至像HotSpot JVM的CMS這樣的所謂的“並行”垃圾收集器也不能完全與應用程式程式碼並行執行,它需要不時地停止所有處理【65】。儘管通常可以透過改變分配模式或調整GC設定來減少暫停【66】,但是如果我們想要提供健壯的保證,就必須假設最壞的情況發生。
|
||||
* 在虛擬化環境中,可以**掛起(suspend)**虛擬機器(暫停執行所有程序並將記憶體內容儲存到磁碟)並恢復(恢復記憶體內容並繼續執行)。這個暫停可以在程序執行的任何時候發生,並且可以持續任意長的時間。這個功能有時用於虛擬機器從一個主機到另一個主機的實時遷移,而不需要重新啟動,在這種情況下,暫停的長度取決於程序寫入記憶體的速率【67】。
|
||||
* 在虛擬化環境中,可以**掛起(suspend)** 虛擬機器(暫停執行所有程序並將記憶體內容儲存到磁碟)並恢復(恢復記憶體內容並繼續執行)。這個暫停可以在程序執行的任何時候發生,並且可以持續任意長的時間。這個功能有時用於虛擬機器從一個主機到另一個主機的實時遷移,而不需要重新啟動,在這種情況下,暫停的長度取決於程序寫入記憶體的速率【67】。
|
||||
* 在終端使用者的裝置(如膝上型電腦)上,執行也可能被暫停並隨意恢復,例如當用戶關閉膝上型電腦的蓋子時。
|
||||
* 當作業系統上下文切換到另一個執行緒時,或者當管理程式切換到另一個虛擬機器時(在虛擬機器中執行時),當前正在執行的執行緒可能在程式碼中的任意點處暫停。在虛擬機器的情況下,在其他虛擬機器中花費的CPU時間被稱為**竊取時間(steal time)**。如果機器處於沉重的負載下(即,如果等待執行的執行緒佇列很長),暫停的執行緒再次執行可能需要一些時間。
|
||||
* 如果應用程式執行同步磁碟訪問,則執行緒可能暫停,等待緩慢的磁碟I/O操作完成【68】。在許多語言中,即使程式碼沒有包含檔案訪問,磁碟訪問也可能出乎意料地發生——例如,Java類載入器在第一次使用時惰性載入類檔案,這可能在程式執行過程中隨時發生。 I/O暫停和GC暫停甚至可能合謀組合它們的延遲【69】。如果磁碟實際上是一個網路檔案系統或網路塊裝置(如亞馬遜的EBS),I/O延遲進一步受到網路延遲變化的影響【29】。
|
||||
* 如果作業系統配置為允許交換到磁碟(頁面交換),則簡單的記憶體訪問可能導致**頁面錯誤(page fault)**,要求將磁碟中的頁面裝入記憶體。當這個緩慢的I/O操作發生時,執行緒暫停。如果記憶體壓力很高,則可能需要將另一個頁面換出到磁碟。在極端情況下,作業系統可能花費大部分時間將頁面交換到記憶體中,而實際上完成的工作很少(這被稱為**抖動(thrashing)**)。為了避免這個問題,通常在伺服器機器上禁用頁面排程(如果你寧願幹掉一個程序來釋放記憶體,也不願意冒抖動風險)。
|
||||
* 可以透過傳送SIGSTOP訊號來暫停Unix程序,例如透過在shell中按下Ctrl-Z。 這個訊號立即阻止程序繼續執行更多的CPU週期,直到SIGCONT恢復為止,此時它將繼續執行。 即使你的環境通常不使用SIGSTOP,也可能由運維工程師意外發送。
|
||||
|
||||
所有這些事件都可以隨時**搶佔(preempt)**正在執行的執行緒,並在稍後的時間恢復執行,而執行緒甚至不會注意到這一點。這個問題類似於在單個機器上使多執行緒程式碼執行緒安全:你不能對時序做任何假設,因為隨時可能發生上下文切換,或者出現並行執行。
|
||||
所有這些事件都可以隨時**搶佔(preempt)** 正在執行的執行緒,並在稍後的時間恢復執行,而執行緒甚至不會注意到這一點。這個問題類似於在單個機器上使多執行緒程式碼執行緒安全:你不能對時序做任何假設,因為隨時可能發生上下文切換,或者出現並行執行。
|
||||
|
||||
當在一臺機器上編寫多執行緒程式碼時,我們有相當好的工具來實現執行緒安全:互斥量,訊號量,原子計數器,無鎖資料結構,阻塞佇列等等。不幸的是,這些工具並不能直接轉化為分散式系統操作,因為分散式系統沒有共享記憶體,只有透過不可靠網路傳送的訊息。
|
||||
|
||||
@ -416,7 +416,7 @@ while (true) {
|
||||
|
||||
在許多程式語言和作業系統中,執行緒和程序可能暫停一段無限制的時間,正如討論的那樣。如果你足夠努力,導致暫停的原因是**可以**消除的。
|
||||
|
||||
某些軟體的執行環境要求很高,不能在特定時間內響應可能會導致嚴重的損失:控制飛機、火箭、機器人、汽車和其他物體的計算機必須對其感測器輸入做出快速而可預測的響應。在這些系統中,軟體必須有一個特定的**截止時間(deadline)**,如果截止時間不滿足,可能會導致整個系統的故障。這就是所謂的**硬實時(hard real-time)**系統。
|
||||
某些軟體的執行環境要求很高,不能在特定時間內響應可能會導致嚴重的損失:控制飛機、火箭、機器人、汽車和其他物體的計算機必須對其感測器輸入做出快速而可預測的響應。在這些系統中,軟體必須有一個特定的**截止時間(deadline)**,如果截止時間不滿足,可能會導致整個系統的故障。這就是所謂的**硬實時(hard real-time)** 系統。
|
||||
|
||||
> #### 實時是真的嗎?
|
||||
>
|
||||
@ -456,17 +456,17 @@ while (true) {
|
||||
|
||||
### 真相由多數所定義
|
||||
|
||||
設想一個具有不對稱故障的網路:一個節點能夠接收發送給它的所有訊息,但是來自該節點的任何傳出訊息被丟棄或延遲【19】。即使該節點執行良好,並且正在接收來自其他節點的請求,其他節點也無法聽到其響應。經過一段時間後,其他節點宣佈它已經死亡,因為他們沒有聽到節點的訊息。這種情況就像夢魘一樣:**半斷開(semi-disconnected)**的節點被拖向墓地,敲打尖叫道“我沒死!” ——但是由於沒有人能聽到它的尖叫,葬禮隊伍繼續以堅忍的決心繼續行進。
|
||||
設想一個具有不對稱故障的網路:一個節點能夠接收發送給它的所有訊息,但是來自該節點的任何傳出訊息被丟棄或延遲【19】。即使該節點執行良好,並且正在接收來自其他節點的請求,其他節點也無法聽到其響應。經過一段時間後,其他節點宣佈它已經死亡,因為他們沒有聽到節點的訊息。這種情況就像夢魘一樣:**半斷開(semi-disconnected)** 的節點被拖向墓地,敲打尖叫道“我沒死!” ——但是由於沒有人能聽到它的尖叫,葬禮隊伍繼續以堅忍的決心繼續行進。
|
||||
|
||||
在一個稍微不那麼夢魘的場景中,半斷開的節點可能會注意到它傳送的訊息沒有被其他節點確認,因此意識到網路中必定存在故障。儘管如此,節點被其他節點錯誤地宣告為死亡,而半連線的節點對此無能為力。
|
||||
|
||||
第三種情況,想象一個經歷了一個長時間**停止所有處理垃圾收集暫停(stop-the-world GC Pause)**的節點。節點的所有執行緒被GC搶佔並暫停一分鐘,因此沒有請求被處理,也沒有響應被髮送。其他節點等待,重試,不耐煩,並最終宣佈節點死亡,並將其丟到靈車上。最後,GC完成,節點的執行緒繼續,好像什麼也沒有發生。其他節點感到驚訝,因為所謂的死亡節點突然從棺材中抬起頭來,身體健康,開始和旁觀者高興地聊天。GC後的節點最初甚至沒有意識到已經經過了整整一分鐘,而且自己已被宣告死亡。從它自己的角度來看,從最後一次與其他節點交談以來,幾乎沒有經過任何時間。
|
||||
第三種情況,想象一個經歷了一個長時間**停止所有處理垃圾收集暫停(stop-the-world GC Pause)** 的節點。節點的所有執行緒被GC搶佔並暫停一分鐘,因此沒有請求被處理,也沒有響應被髮送。其他節點等待,重試,不耐煩,並最終宣佈節點死亡,並將其丟到靈車上。最後,GC完成,節點的執行緒繼續,好像什麼也沒有發生。其他節點感到驚訝,因為所謂的死亡節點突然從棺材中抬起頭來,身體健康,開始和旁觀者高興地聊天。GC後的節點最初甚至沒有意識到已經經過了整整一分鐘,而且自己已被宣告死亡。從它自己的角度來看,從最後一次與其他節點交談以來,幾乎沒有經過任何時間。
|
||||
|
||||
這些故事的寓意是,節點不一定能相信自己對於情況的判斷。分散式系統不能完全依賴單個節點,因為節點可能隨時失效,可能會使系統卡死,無法恢復。相反,許多分散式演算法都依賴於法定人數,即在節點之間進行投票(請參閱“[讀寫的法定人數](ch5.md#讀寫的法定人數)“):決策需要來自多個節點的最小投票數,以減少對於某個特定節點的依賴。
|
||||
|
||||
這也包括關於宣告節點死亡的決定。如果法定數量的節點宣告另一個節點已經死亡,那麼即使該節點仍感覺自己活著,它也必須被認為是死的。個體節點必須遵守法定決定並下臺。
|
||||
|
||||
最常見的法定人數是超過一半的絕對多數(儘管其他型別的法定人數也是可能的)。多數法定人數允許系統繼續工作,如果單個節點發生故障(三個節點可以容忍單節點故障;五個節點可以容忍雙節點故障)。系統仍然是安全的,因為在這個制度中只能有一個多數——不能同時存在兩個相互衝突的多數決定。當我們在[第九章](ch9.md)中討論**共識演算法(consensus algorithms)**時,我們將更詳細地討論法定人數的應用。
|
||||
最常見的法定人數是超過一半的絕對多數(儘管其他型別的法定人數也是可能的)。多數法定人數允許系統繼續工作,如果單個節點發生故障(三個節點可以容忍單節點故障;五個節點可以容忍雙節點故障)。系統仍然是安全的,因為在這個制度中只能有一個多數——不能同時存在兩個相互衝突的多數決定。當我們在[第九章](ch9.md)中討論**共識演算法(consensus algorithms)** 時,我們將更詳細地討論法定人數的應用。
|
||||
|
||||
#### 領導者和鎖
|
||||
|
||||
@ -522,7 +522,7 @@ while (true) {
|
||||
>
|
||||
> 拜占庭是後來成為君士坦丁堡的古希臘城市,現在在土耳其的伊斯坦布林。沒有任何歷史證據表明拜占庭將軍比其他地方更容易出現陰謀和陰謀。相反,這個名字來源於拜占庭式的過度複雜,官僚,迂迴等意義,早在計算機之前就已經在政治中被使用了【79】。Lamport想要選一個不會冒犯任何讀者的國家,他被告知將其稱為阿爾巴尼亞將軍問題並不是一個好主意【80】。
|
||||
|
||||
當一個系統在部分節點發生故障、不遵守協議、甚至惡意攻擊、擾亂網路時仍然能繼續正確工作,稱之為**拜占庭容錯(Byzantine fault-tolerant)**的,在特定場景下,這種擔憂在是有意義的:
|
||||
當一個系統在部分節點發生故障、不遵守協議、甚至惡意攻擊、擾亂網路時仍然能繼續正確工作,稱之為**拜占庭容錯(Byzantine fault-tolerant)** 的,在特定場景下,這種擔憂在是有意義的:
|
||||
|
||||
* 在航空航天環境中,計算機記憶體或CPU暫存器中的資料可能被輻射破壞,導致其以任意不可預知的方式響應其他節點。由於系統故障非常昂貴(例如,飛機撞毀和炸死船上所有人員,或火箭與國際空間站相撞),飛行控制系統必須容忍拜占庭故障【81,82】。
|
||||
* 在多個參與組織的系統中,一些參與者可能會試圖欺騙或欺騙他人。在這種情況下,節點僅僅信任另一個節點的訊息是不安全的,因為它們可能是出於惡意的目的而被髮送的。例如,像比特幣和其他區塊鏈一樣的對等網路可以被認為是讓互不信任的各方同意交易是否發生的一種方式,而不依賴於中心機構(central authority)【83】。
|
||||
@ -553,11 +553,11 @@ while (true) {
|
||||
|
||||
***同步模型***
|
||||
|
||||
**同步模型(synchronous model)**假設網路延遲、程序暫停和和時鐘誤差都是受限的。這並不意味著完全同步的時鐘或零網路延遲;這隻意味著你知道網路延遲、暫停和時鐘漂移將永遠不會超過某個固定的上限【88】。同步模型並不是大多數實際系統的現實模型,因為(如本章所討論的)無限延遲和暫停確實會發生。
|
||||
**同步模型(synchronous model)** 假設網路延遲、程序暫停和和時鐘誤差都是受限的。這並不意味著完全同步的時鐘或零網路延遲;這隻意味著你知道網路延遲、暫停和時鐘漂移將永遠不會超過某個固定的上限【88】。同步模型並不是大多數實際系統的現實模型,因為(如本章所討論的)無限延遲和暫停確實會發生。
|
||||
|
||||
***部分同步模型***
|
||||
|
||||
**部分同步(partial synchronous)**意味著一個系統在大多數情況下像一個同步系統一樣執行,但有時候會超出網路延遲,程序暫停和時鐘漂移的界限【88】。這是很多系統的現實模型:大多數情況下,網路和程序表現良好,否則我們永遠無法完成任何事情,但是我們必須承認,在任何時刻都存在時序假設偶然被破壞的事實。發生這種情況時,網路延遲、暫停和時鐘錯誤可能會變得相當大。
|
||||
**部分同步(partial synchronous)** 意味著一個系統在大多數情況下像一個同步系統一樣執行,但有時候會超出網路延遲,程序暫停和時鐘漂移的界限【88】。這是很多系統的現實模型:大多數情況下,網路和程序表現良好,否則我們永遠無法完成任何事情,但是我們必須承認,在任何時刻都存在時序假設偶然被破壞的事實。發生這種情況時,網路延遲、暫停和時鐘錯誤可能會變得相當大。
|
||||
|
||||
***非同步模型***
|
||||
|
||||
@ -568,17 +568,17 @@ while (true) {
|
||||
|
||||
***崩潰-停止故障***
|
||||
|
||||
在**崩潰停止(crash-stop)**模型中,演算法可能會假設一個節點只能以一種方式失效,即透過崩潰。這意味著節點可能在任意時刻突然停止響應,此後該節點永遠消失——它永遠不會回來。
|
||||
在**崩潰停止(crash-stop)** 模型中,演算法可能會假設一個節點只能以一種方式失效,即透過崩潰。這意味著節點可能在任意時刻突然停止響應,此後該節點永遠消失——它永遠不會回來。
|
||||
|
||||
***崩潰-恢復故障***
|
||||
|
||||
我們假設節點可能會在任何時候崩潰,但也許會在未知的時間之後再次開始響應。在**崩潰-恢復(crash-recovery)**模型中,假設節點具有穩定的儲存(即,非易失性磁碟儲存)且會在崩潰中保留,而記憶體中的狀態會丟失。
|
||||
我們假設節點可能會在任何時候崩潰,但也許會在未知的時間之後再次開始響應。在**崩潰-恢復(crash-recovery)** 模型中,假設節點具有穩定的儲存(即,非易失性磁碟儲存)且會在崩潰中保留,而記憶體中的狀態會丟失。
|
||||
|
||||
***拜占庭(任意)故障***
|
||||
|
||||
節點可以做(絕對意義上的)任何事情,包括試圖戲弄和欺騙其他節點,如上一節所述。
|
||||
|
||||
對於真實系統的建模,具有**崩潰-恢復故障(crash-recovery)**的**部分同步模型(partial synchronous)**通常是最有用的模型。分散式演算法如何應對這種模型?
|
||||
對於真實系統的建模,具有**崩潰-恢復故障(crash-recovery)** 的**部分同步模型(partial synchronous)** 通常是最有用的模型。分散式演算法如何應對這種模型?
|
||||
|
||||
#### 演算法的正確性
|
||||
|
||||
|
72
zh-tw/ch9.md
72
zh-tw/ch9.md
@ -90,7 +90,7 @@
|
||||
|
||||
* 客戶端A的第一個讀操作,完成於寫操作開始之前,因此必須返回舊值 `0`。
|
||||
* 客戶端A的最後一個讀操作,開始於寫操作完成之後。如果資料庫是線性一致性的,它必然返回新值 `1`:因為讀操作和寫操作一定是在其各自的起止區間內的某個時刻被處理。如果在寫入結束後開始讀取,則必須在寫入之後處理讀取,因此它必須看到寫入的新值。
|
||||
* 與寫操作在時間上重疊的任何讀操作,可能會返回 `0` 或 `1` ,因為我們不知道讀取時,寫操作是否已經生效。這些操作是**併發(concurrent)**的。
|
||||
* 與寫操作在時間上重疊的任何讀操作,可能會返回 `0` 或 `1` ,因為我們不知道讀取時,寫操作是否已經生效。這些操作是**併發(concurrent)** 的。
|
||||
|
||||
但是,這還不足以完全描述線性一致性:如果與寫入同時發生的讀取可以返回舊值或新值,那麼讀者可能會在寫入期間看到數值在舊值和新值之間來回翻轉。這不是我們所期望的模擬“單一資料副本”的系統。[^ii]
|
||||
|
||||
@ -202,7 +202,7 @@
|
||||
|
||||
***單主複製(可能線性一致)***
|
||||
|
||||
在具有單主複製功能的系統中(請參閱“[領導者與追隨者](ch5.md#領導者與追隨者)”),主庫具有用於寫入的資料的主副本,而追隨者在其他節點上保留資料的備份副本。如果從主庫或同步更新的從庫讀取資料,它們**可能(potential)**是線性一致性的[^iv]。然而,實際上並不是每個單主資料庫都是線性一致性的,無論是因為設計的原因(例如,因為使用了快照隔離)還是因為在併發處理上存在錯誤【10】。
|
||||
在具有單主複製功能的系統中(請參閱“[領導者與追隨者](ch5.md#領導者與追隨者)”),主庫具有用於寫入的資料的主副本,而追隨者在其他節點上保留資料的備份副本。如果從主庫或同步更新的從庫讀取資料,它們**可能(potential)** 是線性一致性的[^iv]。然而,實際上並不是每個單主資料庫都是線性一致性的,無論是因為設計的原因(例如,因為使用了快照隔離)還是因為在併發處理上存在錯誤【10】。
|
||||
|
||||
[^iv]: 對單主資料庫進行分割槽(分片),使得每個分割槽有一個單獨的領導者,不會影響線性一致性,因為線性一致性只是對單一物件的保證。 交叉分割槽事務是一個不同的問題(請參閱“[分散式事務與共識](#分散式事務與共識)”)。
|
||||
|
||||
@ -287,7 +287,7 @@
|
||||
|
||||
在分散式系統中有更多有趣的“不可能”的結果【41】,且CAP定理現在已經被更精確的結果取代【2,42】,所以它現在基本上成了歷史古蹟了。
|
||||
|
||||
[^vi]: 正如“[真實世界的網路故障](ch8.md#真實世界的網路故障)”中所討論的,本書使用**分割槽(partition)**指代將大資料集細分為小資料集的操作(分片;請參閱[第六章](ch6.md))。與之對應的是,**網路分割槽(network partition)**是一種特定型別的網路故障,我們通常不會將其與其他型別的故障分開考慮。但是,由於它是CAP的P,所以這種情況下我們無法避免混亂。
|
||||
[^vi]: 正如“[真實世界的網路故障](ch8.md#真實世界的網路故障)”中所討論的,本書使用**分割槽(partition)** 指代將大資料集細分為小資料集的操作(分片;請參閱[第六章](ch6.md))。與之對應的是,**網路分割槽(network partition)** 是一種特定型別的網路故障,我們通常不會將其與其他型別的故障分開考慮。但是,由於它是CAP的P,所以這種情況下我們無法避免混亂。
|
||||
|
||||
#### 線性一致性和網路延遲
|
||||
|
||||
@ -310,7 +310,7 @@
|
||||
**順序(ordering)**這一主題在本書中反覆出現,這表明它可能是一個重要的基礎性概念。讓我們簡要回顧一下其它曾經出現過**順序**的上下文:
|
||||
|
||||
* 在[第五章](ch5.md)中我們看到,領導者在單主複製中的主要目的就是,在複製日誌中確定**寫入順序(order of write)**——也就是從庫應用這些寫入的順序。如果不存在一個領導者,則併發操作可能導致衝突(請參閱“[處理寫入衝突](ch5.md#處理寫入衝突)”)。
|
||||
* 在[第七章](ch7.md)中討論的**可序列化**,是關於事務表現的像按**某種先後順序(some sequential order)**執行的保證。它可以字面意義上地以**序列順序(serial order)**執行事務來實現,或者允許並行執行,但同時防止序列化衝突來實現(透過鎖或中止事務)。
|
||||
* 在[第七章](ch7.md)中討論的**可序列化**,是關於事務表現的像按**某種先後順序(some sequential order)** 執行的保證。它可以字面意義上地以**序列順序(serial order)** 執行事務來實現,或者允許並行執行,但同時防止序列化衝突來實現(透過鎖或中止事務)。
|
||||
* 在[第八章](ch8.md)討論過的在分散式系統中使用時間戳和時鐘(請參閱“[依賴同步時鐘](ch8.md#依賴同步時鐘)”)是另一種將順序引入無序世界的嘗試,例如,確定兩個寫入操作哪一個更晚發生。
|
||||
|
||||
事實證明,順序、線性一致性和共識之間有著深刻的聯絡。儘管這個概念比本書其他部分更加理論化和抽象,但對於明確系統的能力範圍(可以做什麼和不可以做什麼)而言是非常有幫助的。我們將在接下來的幾節中探討這個話題。
|
||||
@ -322,21 +322,21 @@
|
||||
* 在“[一致字首讀](ch5.md#一致字首讀)”([圖5-5](../img/fig5-5.png))中,我們看到一個例子:一個對話的觀察者首先看到問題的答案,然後才看到被回答的問題。這是令人困惑的,因為它違背了我們對**因(cause)**與**果(effect)**的直覺:如果一個問題被回答,顯然問題本身得先在那裡,因為給出答案的人必須先看到這個問題(假如他們並沒有預見未來的超能力)。我們認為在問題和答案之間存在**因果依賴(causal dependency)**。
|
||||
* [圖5-9](../img/fig5-9.png)中出現了類似的模式,我們看到三位領導者之間的複製,並注意到由於網路延遲,一些寫入可能會“壓倒”其他寫入。從其中一個副本的角度來看,好像有一個對尚不存在的記錄的更新操作。這裡的因果意味著,一條記錄必須先被建立,然後才能被更新。
|
||||
* 在“[檢測併發寫入](ch5.md#檢測併發寫入)”中我們觀察到,如果有兩個操作A和B,則存在三種可能性:A發生在B之前,或B發生在A之前,或者A和B**併發**。這種**此前發生(happened before)**關係是因果關係的另一種表述:如果A在B前發生,那麼意味著B可能已經知道了A,或者建立在A的基礎上,或者依賴於A。如果A和B是**併發**的,那麼它們之間並沒有因果聯絡;換句話說,我們確信A和B不知道彼此。
|
||||
* 在事務快照隔離的上下文中(“[快照隔離和可重複讀](ch7.md#快照隔離和可重複讀)”),我們說事務是從一致性快照中讀取的。但此語境中“一致”到底又是什麼意思?這意味著**與因果關係保持一致(consistent with causality)**:如果快照包含答案,它也必須包含被回答的問題【48】。在某個時間點觀察整個資料庫,與因果關係保持一致意味著:因果上在該時間點之前發生的所有操作,其影響都是可見的,但因果上在該時間點之後發生的操作,其影響對觀察者不可見。**讀偏差(read skew)**意味著讀取的資料處於違反因果關係的狀態(不可重複讀,如[圖7-6](../img/fig7-6.png)所示)。
|
||||
* 事務之間**寫偏差(write skew)**的例子(請參閱“[寫入偏斜與幻讀](ch7.md#寫入偏斜與幻讀)”)也說明了因果依賴:在[圖7-8](../img/fig7-8.png)中,愛麗絲被允許離班,因為事務認為鮑勃仍在值班,反之亦然。在這種情況下,離班的動作因果依賴於對當前值班情況的觀察。[可序列化快照隔離](ch7.md#可序列化快照隔離)透過跟蹤事務之間的因果依賴來檢測寫偏差。
|
||||
* 在事務快照隔離的上下文中(“[快照隔離和可重複讀](ch7.md#快照隔離和可重複讀)”),我們說事務是從一致性快照中讀取的。但此語境中“一致”到底又是什麼意思?這意味著**與因果關係保持一致(consistent with causality)**:如果快照包含答案,它也必須包含被回答的問題【48】。在某個時間點觀察整個資料庫,與因果關係保持一致意味著:因果上在該時間點之前發生的所有操作,其影響都是可見的,但因果上在該時間點之後發生的操作,其影響對觀察者不可見。**讀偏差(read skew)** 意味著讀取的資料處於違反因果關係的狀態(不可重複讀,如[圖7-6](../img/fig7-6.png)所示)。
|
||||
* 事務之間**寫偏差(write skew)** 的例子(請參閱“[寫入偏斜與幻讀](ch7.md#寫入偏斜與幻讀)”)也說明了因果依賴:在[圖7-8](../img/fig7-8.png)中,愛麗絲被允許離班,因為事務認為鮑勃仍在值班,反之亦然。在這種情況下,離班的動作因果依賴於對當前值班情況的觀察。[可序列化快照隔離](ch7.md#可序列化快照隔離)透過跟蹤事務之間的因果依賴來檢測寫偏差。
|
||||
* 在愛麗絲和鮑勃看球的例子中([圖9-1](../img/fig9-1.png)),在聽到愛麗絲驚呼比賽結果後,鮑勃從伺服器得到陳舊結果的事實違背了因果關係:愛麗絲的驚呼因果依賴於得分宣告,所以鮑勃應該也能在聽到愛麗斯驚呼後查詢到比分。相同的模式在“[跨通道的時序依賴](#跨通道的時序依賴)”一節中,以“影象大小調整服務”的偽裝再次出現。
|
||||
|
||||
因果關係對事件施加了一種**順序**:因在果之前;訊息傳送在訊息收取之前。而且就像現實生活中一樣,一件事會導致另一件事:某個節點讀取了一些資料然後寫入一些結果,另一個節點讀取其寫入的內容,並依次寫入一些其他內容,等等。這些因果依賴的操作鏈定義了系統中的因果順序,即,什麼在什麼之前發生。
|
||||
|
||||
如果一個系統服從因果關係所規定的順序,我們說它是**因果一致(causally consistent)**的。例如,快照隔離提供了因果一致性:當你從資料庫中讀取到一些資料時,你一定還能夠看到其因果前驅(假設在此期間這些資料還沒有被刪除)。
|
||||
如果一個系統服從因果關係所規定的順序,我們說它是**因果一致(causally consistent)** 的。例如,快照隔離提供了因果一致性:當你從資料庫中讀取到一些資料時,你一定還能夠看到其因果前驅(假設在此期間這些資料還沒有被刪除)。
|
||||
|
||||
|
||||
|
||||
#### 因果順序不是全序的
|
||||
|
||||
**全序(total order)**允許任意兩個元素進行比較,所以如果有兩個元素,你總是可以說出哪個更大,哪個更小。例如,自然數集是全序的:給定兩個自然數,比如說5和13,那麼你可以告訴我,13大於5。
|
||||
**全序(total order)** 允許任意兩個元素進行比較,所以如果有兩個元素,你總是可以說出哪個更大,哪個更小。例如,自然數集是全序的:給定兩個自然數,比如說5和13,那麼你可以告訴我,13大於5。
|
||||
|
||||
然而數學集合並不完全是全序的:`{a, b}` 比 `{b, c}` 更大嗎?好吧,你沒法真正比較它們,因為二者都不是對方的子集。我們說它們是**無法比較(incomparable)**的,因此數學集合是**偏序(partially order)**的:在某些情況下,可以說一個集合大於另一個(如果一個集合包含另一個集合的所有元素),但在其他情況下它們是無法比較的[^譯註i]。
|
||||
然而數學集合並不完全是全序的:`{a, b}` 比 `{b, c}` 更大嗎?好吧,你沒法真正比較它們,因為二者都不是對方的子集。我們說它們是**無法比較(incomparable)** 的,因此數學集合是**偏序(partially order)** 的:在某些情況下,可以說一個集合大於另一個(如果一個集合包含另一個集合的所有元素),但在其他情況下它們是無法比較的[^譯註i]。
|
||||
|
||||
[^譯註i]: 設R為非空集合A上的關係,如果R是自反的、反對稱的和可傳遞的,則稱R為A上的偏序關係。簡稱偏序,通常記作≦。一個集合A與A上的偏序關係R一起叫作偏序集,記作$(A,R)$或$(A, ≦)$。全序、偏序、關係、集合,這些概念的精確定義可以參考任意一本離散數學教材。
|
||||
|
||||
@ -354,11 +354,11 @@
|
||||
|
||||
併發意味著時間線會分岔然後合併 —— 在這種情況下,不同分支上的操作是無法比較的(即併發操作)。在[第五章](ch5.md)中我們看到了這種現象:例如,[圖5-14](../img/fig5-14.png) 並不是一條直線的全序關係,而是一堆不同的操作併發進行。圖中的箭頭指明瞭因果依賴 —— 操作的偏序。
|
||||
|
||||
如果你熟悉像Git這樣的分散式版本控制系統,那麼其版本歷史與因果關係圖極其相似。通常,一個**提交(Commit)**發生在另一個提交之後,在一條直線上。但是有時你會遇到分支(當多個人同時在一個專案上工作時),**合併(Merge)**會在這些併發建立的提交相融合時建立。
|
||||
如果你熟悉像Git這樣的分散式版本控制系統,那麼其版本歷史與因果關係圖極其相似。通常,一個**提交(Commit)** 發生在另一個提交之後,在一條直線上。但是有時你會遇到分支(當多個人同時在一個專案上工作時),**合併(Merge)** 會在這些併發建立的提交相融合時建立。
|
||||
|
||||
#### 線性一致性強於因果一致性
|
||||
|
||||
那麼因果順序和線性一致性之間的關係是什麼?答案是線性一致性**隱含著(implies)**因果關係:任何線性一致的系統都能正確保持因果性【7】。特別是,如果系統中有多個通訊通道(如[圖9-5](../img/fig9-5.png) 中的訊息佇列和檔案儲存服務),線性一致性可以自動保證因果性,系統無需任何特殊操作(如在不同元件間傳遞時間戳)。
|
||||
那麼因果順序和線性一致性之間的關係是什麼?答案是線性一致性**隱含著(implies)** 因果關係:任何線性一致的系統都能正確保持因果性【7】。特別是,如果系統中有多個通訊通道(如[圖9-5](../img/fig9-5.png) 中的訊息佇列和檔案儲存服務),線性一致性可以自動保證因果性,系統無需任何特殊操作(如在不同元件間傳遞時間戳)。
|
||||
|
||||
線性一致性確保因果性的事實使線性一致系統變得簡單易懂,更有吸引力。然而,正如“[線性一致性的代價](#線性一致性的代價)”中所討論的,使系統線性一致可能會損害其效能和可用性,尤其是在系統具有嚴重的網路延遲的情況下(例如,如果系統在地理上散佈)。出於這個原因,一些分散式資料系統已經放棄了線性一致性,從而獲得更好的效能,但它們用起來也更為困難。
|
||||
|
||||
@ -390,7 +390,7 @@
|
||||
|
||||
這樣的序列號或時間戳是緊湊的(只有幾個位元組大小),它提供了一個全序關係:也就是說每個操作都有一個唯一的序列號,而且總是可以比較兩個序列號,確定哪一個更大(即哪些操作後發生)。
|
||||
|
||||
特別是,我們可以使用**與因果一致(consistent with causality)**的全序來生成序列號[^vii]:我們保證,如果操作 A 因果地發生在操作 B 前,那麼在這個全序中 A 在 B 前( A 具有比 B 更小的序列號)。並行操作之間可以任意排序。這樣一個全序關係捕獲了所有關於因果的資訊,但也施加了一個比因果性要求更為嚴格的順序。
|
||||
特別是,我們可以使用**與因果一致(consistent with causality)** 的全序來生成序列號[^vii]:我們保證,如果操作 A 因果地發生在操作 B 前,那麼在這個全序中 A 在 B 前( A 具有比 B 更小的序列號)。並行操作之間可以任意排序。這樣一個全序關係捕獲了所有關於因果的資訊,但也施加了一個比因果性要求更為嚴格的順序。
|
||||
|
||||
[^vii]: 與因果關係不一致的全序很容易建立,但沒啥用。例如你可以為每個操作生成隨機的UUID,並按照字典序比較UUID,以定義操作的全序。這是一個有效的全序,但是隨機的UUID並不能告訴你哪個操作先發生,或者操作是否為併發的。
|
||||
|
||||
@ -447,7 +447,7 @@
|
||||
|
||||
乍看之下,似乎操作的全序關係足以解決這一問題(例如使用蘭伯特時間戳):如果建立了兩個具有相同使用者名稱的帳戶,選擇時間戳較小的那個作為勝者(第一個抓到使用者名稱的人),並讓帶有更大時間戳者失敗。由於時間戳上有全序關係,所以這個比較總是可行的。
|
||||
|
||||
這種方法適用於事後確定勝利者:一旦你收集了系統中的所有使用者名稱建立操作,就可以比較它們的時間戳。然而當某個節點需要實時處理使用者建立使用者名稱的請求時,這樣的方法就無法滿足了。節點需要**馬上(right now)**決定這個請求是成功還是失敗。在那個時刻,節點並不知道是否存在其他節點正在併發執行建立同樣使用者名稱的操作,罔論其它節點可能分配給那個操作的時間戳。
|
||||
這種方法適用於事後確定勝利者:一旦你收集了系統中的所有使用者名稱建立操作,就可以比較它們的時間戳。然而當某個節點需要實時處理使用者建立使用者名稱的請求時,這樣的方法就無法滿足了。節點需要**馬上(right now)** 決定這個請求是成功還是失敗。在那個時刻,節點並不知道是否存在其他節點正在併發執行建立同樣使用者名稱的操作,罔論其它節點可能分配給那個操作的時間戳。
|
||||
|
||||
為了確保沒有其他節點正在使用相同的使用者名稱和較小的時間戳併發建立同名賬戶,你必須檢查其它每個節點,看看它在做什麼【56】。如果其中一個節點由於網路問題出現故障或不可達,則整個系統可能被拖至停機。這不是我們需要的那種容錯系統。
|
||||
|
||||
@ -499,7 +499,7 @@
|
||||
|
||||
如 [圖9-4](../img/fig9-4.png) 所示,線上性一致的系統中,存在操作的全序。這是否意味著線性一致與全序廣播一樣?不盡然,但兩者之間有著密切的聯絡[^x]。
|
||||
|
||||
[^x]: 從形式上講,線性一致讀寫暫存器是一個“更容易”的問題。 全序廣播等價於共識【67】,而共識問題在非同步的崩潰-停止模型【68】中沒有確定性的解決方案,而線性一致的讀寫暫存器**可以**在這種模型中實現【23,24,25】。 然而,支援諸如**比較並設定(CAS, compare-and-set)**,或**自增並返回(increment-and-get)**的原子操作使它等價於共識問題【28】。 因此,共識問題與線性一致暫存器問題密切相關。
|
||||
[^x]: 從形式上講,線性一致讀寫暫存器是一個“更容易”的問題。 全序廣播等價於共識【67】,而共識問題在非同步的崩潰-停止模型【68】中沒有確定性的解決方案,而線性一致的讀寫暫存器**可以**在這種模型中實現【23,24,25】。 然而,支援諸如**比較並設定(CAS, compare-and-set)**,或**自增並返回(increment-and-get)** 的原子操作使它等價於共識問題【28】。 因此,共識問題與線性一致暫存器問題密切相關。
|
||||
|
||||
全序廣播是非同步的:訊息被保證以固定的順序可靠地傳送,但是不能保證訊息**何時**被送達(所以一個接收者可能落後於其他接收者)。相比之下,線性一致性是新鮮性的保證:讀取一定能看見最新的寫入值。
|
||||
|
||||
@ -555,7 +555,7 @@
|
||||
|
||||
***原子提交***
|
||||
|
||||
在支援跨多節點或跨多分割槽事務的資料庫中,一個事務可能在某些節點上失敗,但在其他節點上成功。如果我們想要維護事務的原子性(就ACID而言,請參閱“[原子性](ch7.md#原子性)”),我們必須讓所有節點對事務的結果達成一致:要麼全部中止/回滾(如果出現任何錯誤),要麼它們全部提交(如果沒有出錯)。這個共識的例子被稱為**原子提交(atomic commit)**問題[^xii]。
|
||||
在支援跨多節點或跨多分割槽事務的資料庫中,一個事務可能在某些節點上失敗,但在其他節點上成功。如果我們想要維護事務的原子性(就ACID而言,請參閱“[原子性](ch7.md#原子性)”),我們必須讓所有節點對事務的結果達成一致:要麼全部中止/回滾(如果出現任何錯誤),要麼它們全部提交(如果沒有出錯)。這個共識的例子被稱為**原子提交(atomic commit)** 問題[^xii]。
|
||||
|
||||
|
||||
[^xii]: 原子提交的形式化與共識稍有不同:原子事務只有在**所有**參與者投票提交的情況下才能提交,如果有任何參與者需要中止,則必須中止。 共識則允許就**任意一個**被參與者提出的候選值達成一致。 然而,原子提交和共識可以相互簡化為對方【70,71】。 **非阻塞**原子提交則要比共識更為困難 —— 請參閱“[三階段提交](#三階段提交)”。
|
||||
@ -568,7 +568,7 @@
|
||||
>
|
||||
> 因此,雖然FLP是關於共識不可能性的重要理論結果,但現實中的分散式系統通常是可以達成共識的。
|
||||
|
||||
在本節中,我們將首先更詳細地研究**原子提交**問題。具體來說,我們將討論**兩階段提交(2PC, two-phase commit)**演算法,這是解決原子提交問題最常見的辦法,並在各種資料庫、訊息佇列和應用伺服器中被實現。事實證明2PC是一種共識演算法,但不是一個非常好的共識演算法【70,71】。
|
||||
在本節中,我們將首先更詳細地研究**原子提交**問題。具體來說,我們將討論**兩階段提交(2PC, two-phase commit)** 演算法,這是解決原子提交問題最常見的辦法,並在各種資料庫、訊息佇列和應用伺服器中被實現。事實證明2PC是一種共識演算法,但不是一個非常好的共識演算法【70,71】。
|
||||
|
||||
透過對2PC的學習,我們將繼續努力實現更好的一致性演算法,比如ZooKeeper(Zab)和etcd(Raft)中使用的演算法。
|
||||
|
||||
@ -616,10 +616,10 @@
|
||||
|
||||
2PC使用一個通常不會出現在單節點事務中的新元件:**協調者(coordinator)**(也稱為**事務管理器(transaction manager)**)。協調者通常在請求事務的相同應用程序中以庫的形式實現(例如,嵌入在Java EE容器中),但也可以是單獨的程序或服務。這種協調者的例子包括Narayana、JOTM、BTM或MSDTC。
|
||||
|
||||
正常情況下,2PC事務以應用在多個數據庫節點上讀寫資料開始。我們稱這些資料庫節點為**參與者(participants)**。當應用準備提交時,協調者開始階段 1 :它傳送一個**準備(prepare)**請求到每個節點,詢問它們是否能夠提交。然後協調者會跟蹤參與者的響應:
|
||||
正常情況下,2PC事務以應用在多個數據庫節點上讀寫資料開始。我們稱這些資料庫節點為**參與者(participants)**。當應用準備提交時,協調者開始階段 1 :它傳送一個**準備(prepare)** 請求到每個節點,詢問它們是否能夠提交。然後協調者會跟蹤參與者的響應:
|
||||
|
||||
* 如果所有參與者都回答“是”,表示它們已經準備好提交,那麼協調者在階段 2 發出**提交(commit)**請求,然後提交真正發生。
|
||||
* 如果任意一個參與者回覆了“否”,則協調者在階段2 中向所有節點發送**中止(abort)**請求。
|
||||
* 如果所有參與者都回答“是”,表示它們已經準備好提交,那麼協調者在階段 2 發出**提交(commit)** 請求,然後提交真正發生。
|
||||
* 如果任意一個參與者回覆了“否”,則協調者在階段2 中向所有節點發送**中止(abort)** 請求。
|
||||
|
||||
這個過程有點像西方傳統婚姻儀式:司儀分別詢問新娘和新郎是否要結婚,通常是從兩方都收到“我願意”的答覆。收到兩者的回覆後,司儀宣佈這對情侶成為夫妻:事務就提交了,這一幸福事實會廣播至所有的參與者中。如果新娘與新郎之一沒有回覆”我願意“,婚禮就會中止【73】。
|
||||
|
||||
@ -644,7 +644,7 @@
|
||||
|
||||
我們已經討論了在2PC期間,如果參與者之一或網路發生故障時會發生什麼情況:如果任何一個**準備**請求失敗或者超時,協調者就會中止事務。如果任何提交或中止請求失敗,協調者將無條件重試。但是如果協調者崩潰,會發生什麼情況就不太清楚了。
|
||||
|
||||
如果協調者在傳送**準備**請求之前失敗,參與者可以安全地中止事務。但是,一旦參與者收到了準備請求並投了“是”,就不能再單方面放棄 —— 必須等待協調者回答事務是否已經提交或中止。如果此時協調者崩潰或網路出現故障,參與者什麼也做不了只能等待。參與者的這種事務狀態稱為**存疑(in doubt)**的或**不確定(uncertain)**的。
|
||||
如果協調者在傳送**準備**請求之前失敗,參與者可以安全地中止事務。但是,一旦參與者收到了準備請求並投了“是”,就不能再單方面放棄 —— 必須等待協調者回答事務是否已經提交或中止。如果此時協調者崩潰或網路出現故障,參與者什麼也做不了只能等待。參與者的這種事務狀態稱為**存疑(in doubt)** 的或**不確定(uncertain)** 的。
|
||||
|
||||
情況如[圖9-10](../img/fig9-10.png) 所示。在這個特定的例子中,協調者實際上決定提交,資料庫2 收到提交請求。但是,協調者在將提交請求傳送到資料庫1 之前發生崩潰,因此資料庫1 不知道是否提交或中止。即使**超時**在這裡也沒有幫助:如果資料庫1 在超時後單方面中止,它將最終與執行提交的資料庫2 不一致。同樣,單方面提交也是不安全的,因為另一個參與者可能已經中止了。
|
||||
|
||||
@ -657,9 +657,9 @@
|
||||
|
||||
#### 三階段提交
|
||||
|
||||
兩階段提交被稱為**阻塞(blocking)**原子提交協議,因為存在2PC可能卡住並等待協調者恢復的情況。理論上,可以使一個原子提交協議變為**非阻塞(nonblocking)**的,以便在節點失敗時不會卡住。但是讓這個協議能在實踐中工作並沒有那麼簡單。
|
||||
兩階段提交被稱為**阻塞(blocking)**- 原子提交協議,因為存在2PC可能卡住並等待協調者恢復的情況。理論上,可以使一個原子提交協議變為**非阻塞(nonblocking)** 的,以便在節點失敗時不會卡住。但是讓這個協議能在實踐中工作並沒有那麼簡單。
|
||||
|
||||
作為2PC的替代方案,已經提出了一種稱為**三階段提交(3PC)**的演算法【13,80】。然而,3PC假定網路延遲有界,節點響應時間有限;在大多數具有無限網路延遲和程序暫停的實際系統中(見[第八章](ch8.md)),它並不能保證原子性。
|
||||
作為2PC的替代方案,已經提出了一種稱為**三階段提交(3PC)** 的演算法【13,80】。然而,3PC假定網路延遲有界,節點響應時間有限;在大多數具有無限網路延遲和程序暫停的實際系統中(見[第八章](ch8.md)),它並不能保證原子性。
|
||||
|
||||
通常,非阻塞原子提交需要一個**完美的故障檢測器(perfect failure detector)**【67,71】—— 即一個可靠的機制來判斷一個節點是否已經崩潰。在具有無限延遲的網路中,超時並不是一種可靠的故障檢測機制,因為即使沒有節點崩潰,請求也可能由於網路問題而超時。出於這個原因,2PC仍然被使用,儘管大家都清楚可能存在協調者故障的問題。
|
||||
|
||||
@ -679,7 +679,7 @@
|
||||
|
||||
***異構分散式事務***
|
||||
|
||||
在**異構(heterogeneous)**事務中,參與者是由兩種或兩種以上的不同技術組成的:例如來自不同供應商的兩個資料庫,甚至是非資料庫系統(如訊息代理)。跨系統的分散式事務必須確保原子提交,儘管系統可能完全不同。
|
||||
在**異構(heterogeneous)** 事務中,參與者是由兩種或兩種以上的不同技術組成的:例如來自不同供應商的兩個資料庫,甚至是非資料庫系統(如訊息代理)。跨系統的分散式事務必須確保原子提交,儘管系統可能完全不同。
|
||||
|
||||
資料庫內部事務不必與任何其他系統相容,因此它們可以使用任何協議,並能針對特定技術進行特定的最佳化。因此資料庫內部的分散式事務通常工作地很好。另一方面,跨異構技術的事務則更有挑戰性。
|
||||
|
||||
@ -687,15 +687,15 @@
|
||||
|
||||
異構的分散式事務處理能夠以強大的方式整合不同的系統。例如:訊息佇列中的一條訊息可以被確認為已處理,當且僅當用於處理訊息的資料庫事務成功提交。這是透過在同一個事務中原子提交**訊息確認**和**資料庫寫入**兩個操作來實現的。藉由分散式事務的支援,即使訊息代理和資料庫是在不同機器上執行的兩種不相關的技術,這種操作也是可能的。
|
||||
|
||||
如果訊息傳遞或資料庫事務任意一者失敗,兩者都會中止,因此訊息代理可能會在稍後安全地重傳訊息。因此,透過原子提交**訊息處理及其副作用**,即使在成功之前需要幾次重試,也可以確保訊息被**有效地(effectively)**恰好處理一次。中止會拋棄部分完成事務所導致的任何副作用。
|
||||
如果訊息傳遞或資料庫事務任意一者失敗,兩者都會中止,因此訊息代理可能會在稍後安全地重傳訊息。因此,透過原子提交**訊息處理及其副作用**,即使在成功之前需要幾次重試,也可以確保訊息被**有效地(effectively)** 恰好處理一次。中止會拋棄部分完成事務所導致的任何副作用。
|
||||
|
||||
然而,只有當所有受事務影響的系統都使用同樣的**原子提交協議(atomic commit protocl)**時,這樣的分散式事務才是可能的。例如,假設處理訊息的副作用是傳送一封郵件,而郵件伺服器並不支援兩階段提交:如果訊息處理失敗並重試,則可能會發送兩次或更多次的郵件。但如果處理訊息的所有副作用都可以在事務中止時回滾,那麼這樣的處理流程就可以安全地重試,就好像什麼都沒有發生過一樣。
|
||||
然而,只有當所有受事務影響的系統都使用同樣的**原子提交協議(atomic commit protocol)** 時,這樣的分散式事務才是可能的。例如,假設處理訊息的副作用是傳送一封郵件,而郵件伺服器並不支援兩階段提交:如果訊息處理失敗並重試,則可能會發送兩次或更多次的郵件。但如果處理訊息的所有副作用都可以在事務中止時回滾,那麼這樣的處理流程就可以安全地重試,就好像什麼都沒有發生過一樣。
|
||||
|
||||
在[第十一章](ch11.md)中將再次回到“恰好一次”訊息處理的主題。讓我們先來看看允許這種異構分散式事務的原子提交協議。
|
||||
|
||||
#### XA事務
|
||||
|
||||
*X/Open XA*(**擴充套件架構(eXtended Architecture)**的縮寫)是跨異構技術實現兩階段提交的標準【76,77】。它於1991年推出並得到了廣泛的實現:許多傳統關係資料庫(包括PostgreSQL,MySQL,DB2,SQL Server和Oracle)和訊息代理(包括ActiveMQ,HornetQ,MSMQ和IBM MQ) 都支援XA。
|
||||
*X/Open XA*(**擴充套件架構(eXtended Architecture)** 的縮寫)是跨異構技術實現兩階段提交的標準【76,77】。它於1991年推出並得到了廣泛的實現:許多傳統關係資料庫(包括PostgreSQL,MySQL,DB2,SQL Server和Oracle)和訊息代理(包括ActiveMQ,HornetQ,MSMQ和IBM MQ) 都支援XA。
|
||||
|
||||
XA不是一個網路協議——它只是一個用來與事務協調者連線的C API。其他語言也有這種API的繫結;例如在Java EE應用的世界中,XA事務是使用**Java事務API(JTA, Java Transaction API)**實現的,而許多使用**Java資料庫連線(JDBC, Java Database Connectivity)**的資料庫驅動,以及許多使用**Java訊息服務(JMS)**API的訊息代理都支援**Java事務API(JTA)**。
|
||||
|
||||
@ -717,13 +717,13 @@
|
||||
|
||||
#### 從協調者故障中恢復
|
||||
|
||||
理論上,如果協調者崩潰並重新啟動,它應該乾淨地從日誌中恢復其狀態,並解決任何存疑事務。然而在實踐中,**孤立(orphaned)**的存疑事務確實會出現【89,90】,即無論出於何種理由,協調者無法確定事務的結果(例如事務日誌已經由於軟體錯誤丟失或損壞)。這些事務無法自動解決,所以它們永遠待在資料庫中,持有鎖並阻塞其他事務。
|
||||
理論上,如果協調者崩潰並重新啟動,它應該乾淨地從日誌中恢復其狀態,並解決任何存疑事務。然而在實踐中,**孤立(orphaned)** 的存疑事務確實會出現【89,90】,即無論出於何種理由,協調者無法確定事務的結果(例如事務日誌已經由於軟體錯誤丟失或損壞)。這些事務無法自動解決,所以它們永遠待在資料庫中,持有鎖並阻塞其他事務。
|
||||
|
||||
即使重啟資料庫伺服器也無法解決這個問題,因為在2PC的正確實現中,即使重啟也必須保留存疑事務的鎖(否則就會冒違反原子性保證的風險)。這是一種棘手的情況。
|
||||
|
||||
唯一的出路是讓管理員手動決定提交還是回滾事務。管理員必須檢查每個存疑事務的參與者,確定是否有任何參與者已經提交或中止,然後將相同的結果應用於其他參與者。解決這個問題潛在地需要大量的人力,並且可能發生在嚴重的生產中斷期間(不然為什麼協調者處於這種糟糕的狀態),並很可能要在巨大精神壓力和時間壓力下完成。
|
||||
|
||||
許多XA的實現都有一個叫做**啟發式決策(heuristic decisions)**的緊急逃生艙口:允許參與者單方面決定放棄或提交一個存疑事務,而無需協調者做出最終決定【76,77,91】。要清楚的是,這裡**啟發式**是**可能破壞原子性(probably breaking atomicity)**的委婉說法,因為它違背了兩階段提交的系統承諾。因此,啟發式決策只是為了逃出災難性的情況而準備的,而不是為了日常使用的。
|
||||
許多XA的實現都有一個叫做**啟發式決策(heuristic decisions)** 的緊急逃生艙口:允許參與者單方面決定放棄或提交一個存疑事務,而無需協調者做出最終決定【76,77,91】。要清楚的是,這裡**啟發式**是**可能破壞原子性(probably breaking atomicity)** 的委婉說法,因為它違背了兩階段提交的系統承諾。因此,啟發式決策只是為了逃出災難性的情況而準備的,而不是為了日常使用的。
|
||||
|
||||
#### 分散式事務的限制
|
||||
|
||||
@ -732,7 +732,7 @@
|
||||
* 如果協調者沒有複製,而是隻在單臺機器上執行,那麼它是整個系統的失效單點(因為它的失效會導致其他應用伺服器阻塞在存疑事務持有的鎖上)。令人驚訝的是,許多協調者實現預設情況下並不是高可用的,或者只有基本的複製支援。
|
||||
* 許多伺服器端應用都是使用無狀態模式開發的(受HTTP的青睞),所有持久狀態都儲存在資料庫中,因此具有應用伺服器可隨意按需新增刪除的優點。但是,當協調者成為應用伺服器的一部分時,它會改變部署的性質。突然間,協調者的日誌成為持久系統狀態的關鍵部分—— 與資料庫本身一樣重要,因為協調者日誌是為了在崩潰後恢復存疑事務所必需的。這樣的應用伺服器不再是無狀態的了。
|
||||
* 由於XA需要相容各種資料系統,因此它必須是所有系統的最小公分母。例如,它不能檢測不同系統間的死鎖(因為這將需要一個標準協議來讓系統交換每個事務正在等待的鎖的資訊),而且它無法與SSI(請參閱[可序列化快照隔離](ch7.md#可序列化快照隔離 ))協同工作,因為這需要一個跨系統定位衝突的協議。
|
||||
* 對於資料庫內部的分散式事務(不是XA),限制沒有這麼大 —— 例如,分散式版本的SSI是可能的。然而仍然存在問題:2PC成功提交一個事務需要所有參與者的響應。因此,如果系統的**任何**部分損壞,事務也會失敗。因此,分散式事務又有**擴大失效(amplifying failures)**的趨勢,這又與我們構建容錯系統的目標背道而馳。
|
||||
* 對於資料庫內部的分散式事務(不是XA),限制沒有這麼大 —— 例如,分散式版本的SSI是可能的。然而仍然存在問題:2PC成功提交一個事務需要所有參與者的響應。因此,如果系統的**任何**部分損壞,事務也會失敗。因此,分散式事務又有**擴大失效(amplifying failures)** 的趨勢,這又與我們構建容錯系統的目標背道而馳。
|
||||
|
||||
這些事實是否意味著我們應該放棄保持幾個系統相互一致的所有希望?不完全是 —— 還有其他的辦法,可以讓我們在沒有異構分散式事務的痛苦的情況下實現同樣的事情。我們將在[第十一章](ch11.md) 和[第十二章](ch12.md) 回到這些話題。但首先,我們應該概括一下關於**共識**的話題。
|
||||
|
||||
@ -740,13 +740,13 @@
|
||||
|
||||
### 容錯共識
|
||||
|
||||
非正式地,共識意味著讓幾個節點就某事達成一致。例如,如果有幾個人**同時(concurrently)**嘗試預訂飛機上的最後一個座位,或劇院中的同一個座位,或者嘗試使用相同的使用者名稱註冊一個帳戶。共識演算法可以用來確定這些**互不相容(mutually incompatible)**的操作中,哪一個才是贏家。
|
||||
非正式地,共識意味著讓幾個節點就某事達成一致。例如,如果有幾個人**同時(concurrently)** 嘗試預訂飛機上的最後一個座位,或劇院中的同一個座位,或者嘗試使用相同的使用者名稱註冊一個帳戶。共識演算法可以用來確定這些**互不相容(mutually incompatible)** 的操作中,哪一個才是贏家。
|
||||
|
||||
共識問題通常形式化如下:一個或多個節點可以**提議(propose)**某些值,而共識演算法**決定(decides)**採用其中的某個值。在座位預訂的例子中,當幾個顧客同時試圖訂購最後一個座位時,處理顧客請求的每個節點可以**提議**將要服務的顧客的ID,而**決定**指明瞭哪個顧客獲得了座位。
|
||||
|
||||
在這種形式下,共識演算法必須滿足以下性質【25】:[^xiii]
|
||||
|
||||
[^xiii]: 這種共識的特殊形式被稱為**統一共識(uniform consensus)**,相當於在具有不可靠故障檢測器的非同步系統中的**常規共識(regular consensus)**【71】。學術文獻通常指的是**程序(process)**而不是節點,但我們在這裡使用**節點(node)**來與本書的其餘部分保持一致。
|
||||
[^xiii]: 這種共識的特殊形式被稱為**統一共識(uniform consensus)**,相當於在具有不可靠故障檢測器的非同步系統中的**常規共識(regular consensus)**【71】。學術文獻通常指的是**程序(process)** 而不是節點,但我們在這裡使用**節點(node)** 來與本書的其餘部分保持一致。
|
||||
|
||||
***一致同意(Uniform agreement)***
|
||||
|
||||
@ -771,7 +771,7 @@
|
||||
|
||||
共識的系統模型假設,當一個節點“崩潰”時,它會突然消失而且永遠不會回來。(不像軟體崩潰,想象一下地震,包含你的節點的資料中心被山體滑坡所摧毀,你必須假設節點被埋在30英尺以下的泥土中,並且永遠不會重新上線)在這個系統模型中,任何需要等待節點恢復的演算法都不能滿足**終止**屬性。特別是,2PC不符合終止屬性的要求。
|
||||
|
||||
當然如果**所有**的節點都崩潰了,沒有一個在執行,那麼所有演算法都不可能決定任何事情。演算法可以容忍的失效數量是有限的:事實上可以證明,任何共識演算法都需要至少佔總體**多數(majority)**的節點正確工作,以確保終止屬性【67】。多數可以安全地組成法定人數(請參閱“[讀寫的法定人數](ch5.md#讀寫的法定人數)”)。
|
||||
當然如果**所有**的節點都崩潰了,沒有一個在執行,那麼所有演算法都不可能決定任何事情。演算法可以容忍的失效數量是有限的:事實上可以證明,任何共識演算法都需要至少佔總體**多數(majority)** 的節點正確工作,以確保終止屬性【67】。多數可以安全地組成法定人數(請參閱“[讀寫的法定人數](ch5.md#讀寫的法定人數)”)。
|
||||
|
||||
因此**終止**屬性取決於一個假設,**不超過一半的節點崩潰或不可達**。然而即使多數節點出現故障或存在嚴重的網路問題,絕大多數共識的實現都能始終確保安全屬性得到滿足—— 一致同意,完整性和有效性【92】。因此,大規模的中斷可能會阻止系統處理請求,但是它不能透過使系統做出無效的決定來破壞共識系統。
|
||||
|
||||
@ -792,7 +792,7 @@
|
||||
* 由於**有效性**屬性,訊息不會被損壞,也不能憑空編造。
|
||||
* 由於**終止**屬性,訊息不會丟失。
|
||||
|
||||
檢視戳複製,Raft和Zab直接實現了全序廣播,因為這樣做比重複**一次一值(one value a time)**的共識更高效。在Paxos的情況下,這種最佳化被稱為Multi-Paxos。
|
||||
檢視戳複製,Raft和Zab直接實現了全序廣播,因為這樣做比重複**一次一值(one value a time)** 的共識更高效。在Paxos的情況下,這種最佳化被稱為Multi-Paxos。
|
||||
|
||||
#### 單領導者複製與共識
|
||||
|
||||
@ -814,7 +814,7 @@
|
||||
|
||||
在任何領導者被允許決定任何事情之前,必須先檢查是否存在其他帶有更高紀元編號的領導者,它們可能會做出相互衝突的決定。領導者如何知道自己沒有被另一個節點趕下臺?回想一下在“[真相由多數所定義](ch8.md#真相由多數所定義)”中提到的:一個節點不一定能相信自己的判斷—— 因為只有節點自己認為自己是領導者,並不一定意味著其他節點接受它作為它們的領導者。
|
||||
|
||||
相反,它必須從**法定人數(quorum)**的節點中獲取選票(請參閱“[讀寫的法定人數](ch5.md#讀寫的法定人數)”)。對領導者想要做出的每一個決定,都必須將提議值傳送給其他節點,並等待法定人數的節點響應並贊成提案。法定人數通常(但不總是)由多數節點組成【105】。只有在沒有意識到任何帶有更高紀元編號的領導者的情況下,一個節點才會投票贊成提議。
|
||||
相反,它必須從**法定人數(quorum)** 的節點中獲取選票(請參閱“[讀寫的法定人數](ch5.md#讀寫的法定人數)”)。對領導者想要做出的每一個決定,都必須將提議值傳送給其他節點,並等待法定人數的節點響應並贊成提案。法定人數通常(但不總是)由多數節點組成【105】。只有在沒有意識到任何帶有更高紀元編號的領導者的情況下,一個節點才會投票贊成提議。
|
||||
|
||||
因此,我們有兩輪投票:第一次是為了選出一位領導者,第二次是對領導者的提議進行表決。關鍵的洞察在於,這兩次投票的**法定人群**必須相互**重疊(overlap)**:如果一個提案的表決透過,則至少得有一個參與投票的節點也必須參加過最近的領導者選舉【105】。因此,如果在一個提案的表決過程中沒有出現更高的紀元編號。那麼現任領導者就可以得出這樣的結論:沒有發生過更高時代的領導選舉,因此可以確定自己仍然在領導。然後它就可以安全地對提議值做出決定。
|
||||
|
||||
@ -830,7 +830,7 @@
|
||||
|
||||
共識系統總是需要嚴格多數來運轉。這意味著你至少需要三個節點才能容忍單節點故障(其餘兩個構成多數),或者至少有五個節點來容忍兩個節點發生故障(其餘三個構成多數)。如果網路故障切斷了某些節點同其他節點的連線,則只有多數節點所在的網路可以繼續工作,其餘部分將被阻塞(請參閱“[線性一致性的代價](#線性一致性的代價)”)。
|
||||
|
||||
大多數共識演算法假定參與投票的節點是固定的集合,這意味著你不能簡單的在叢集中新增或刪除節點。共識演算法的**動態成員擴充套件(dynamic membership extension)**允許叢集中的節點集隨時間推移而變化,但是它們比靜態成員演算法要難理解得多。
|
||||
大多數共識演算法假定參與投票的節點是固定的集合,這意味著你不能簡單的在叢集中新增或刪除節點。共識演算法的**動態成員擴充套件(dynamic membership extension)** 允許叢集中的節點集隨時間推移而變化,但是它們比靜態成員演算法要難理解得多。
|
||||
|
||||
共識系統通常依靠超時來檢測失效的節點。在網路延遲高度變化的環境中,特別是在地理上散佈的系統中,經常發生一個節點由於暫時的網路問題,錯誤地認為領導者已經失效。雖然這種錯誤不會損害安全屬性,但頻繁的領導者選舉會導致糟糕的效能表現,因系統最後可能花在權力傾紮上的時間要比花在建設性工作的多得多。
|
||||
|
||||
@ -848,7 +848,7 @@
|
||||
|
||||
***線性一致性的原子操作***
|
||||
|
||||
使用原子CAS操作可以實現鎖:如果多個節點同時嘗試執行相同的操作,只有一個節點會成功。共識協議保證了操作的原子性和線性一致性,即使節點發生故障或網路在任意時刻中斷。分散式鎖通常以**租約(lease)**的形式實現,租約有一個到期時間,以便在客戶端失效的情況下最終能被釋放(請參閱“[程序暫停](ch8.md#程序暫停)”)。
|
||||
使用原子CAS操作可以實現鎖:如果多個節點同時嘗試執行相同的操作,只有一個節點會成功。共識協議保證了操作的原子性和線性一致性,即使節點發生故障或網路在任意時刻中斷。分散式鎖通常以**租約(lease)** 的形式實現,租約有一個到期時間,以便在客戶端失效的情況下最終能被釋放(請參閱“[程序暫停](ch8.md#程序暫停)”)。
|
||||
|
||||
***操作的全序排序***
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user