update zh-tw contents

This commit is contained in:
Gang Yin 2022-04-20 21:48:01 +08:00
parent 39c8c0ea51
commit ee29a62ffc

View File

@ -18,175 +18,175 @@
本章將假設你的資料集非常小,每臺機器都可以儲存整個資料集的副本。在 [第六章](ch6.md) 中將放寬這個假設,討論對單個機器來說太大的資料集的分割(分片)。在後面的章節中,我們將討論複製資料系統中可能發生的各種故障,以及如何處理這些故障。
如果複製中的資料不會隨時間而改變,那複製就很簡單:將資料複製到每個節點一次就萬事大吉。複製的困難之處在於處理複製資料的 **變更change**,這就是本章所要講的。我們將討論三種流行的變更復制演算法:**單領導者single leader****多領導者multi leader** 和 **無領導者leaderless**。幾乎所有分散式資料庫都使用這三種方法之一。
如果複製中的資料不會隨時間而改變,那複製就很簡單:將資料複製到每個節點一次就萬事大吉。複製的困難之處在於處理複製資料的 **變更change**,這就是本章所要講的。我們將討論三種流行的變更復制演算法:**單領導者single leader,單主****多領導者multi leader,多主** 和 **無領導者leaderless,無主**。幾乎所有分散式資料庫都使用這三種方法之一。
在複製時需要進行許多權衡:例如,使用同步複製還是非同步複製?如何處理失敗的副本?這些通常是資料庫中的配置選項,細節因資料庫而異,但原理在許多不同的實現中都類似。本章會討論這些決策的後果。
資料庫的複製算得上是老生常談了 ——70 年代研究得出的基本原則至今沒有太大變化【1】因為網路的基本約束仍保持不變。然而在研究之外許多開發人員仍然假設一個數據庫只有一個節點。分散式資料庫變為主流只是最近發生的事。許多程式設計師都是這一領域的新手因此對於諸如 **最終一致性eventual consistency** 等問題存在許多誤解。在 “[複製延遲問題](#複製延遲問題)” 一節,我們將更加精確地瞭解最終一致性,並討論諸如 **讀己之寫read-your-writes****單調讀monotonic read** 保證等內容。
資料庫的複製算得上是老生常談了 ——70 年代研究得出的基本原則至今沒有太大變化【1】因為網路的基本約束仍保持不變。然而在研究之外許多開發人員仍然假設一個數據庫只有一個節點。分散式資料庫變為主流只是最近發生的事。許多程式設計師都是這一領域的新手因此對於諸如 **最終一致性eventual consistency** 等問題存在許多誤解。在 “[複製延遲問題](#複製延遲問題)” 一節,我們將更加精確地瞭解最終一致性,並討論諸如 **讀己之寫read-your-writes****單調讀monotonic read** 等內容。
## 領導者與追隨者
儲存資料庫副本的每個節點稱為 **副本replica** 。當存在多個副本時,會不可避免的出現一個問題:如何確保所有資料都落在了所有的副本上?
儲存了資料庫複製的每個節點被稱為 **副本replica** 。當存在多個副本時,會不可避免的出現一個問題:如何確保所有資料都落在了所有的副本上?
每一次向資料庫的寫入操作都需要傳播到所有副本上,否則副本就會包含不一樣的資料。最常見的解決方案被稱為 **基於領導者的複製leader-based replication** (也稱 **主動 / 被動active/passive** 或 **主 / master/slave** 複製),如 [圖 5-1](#fig5-1.png) 所示。它的工作原理如下:
每一次向資料庫的寫入操作都需要傳播到所有副本上,否則副本就會包含不一樣的資料。最常見的解決方案被稱為 **基於領導者的複製leader-based replication** (也稱 **主動/被動active/passive** 複製**主/從master/slave** 複製),如 [圖 5-1](#fig5-1.png) 所示。它的工作原理如下:
1. 副本之一被指定為 **領導者leader**,也稱為 **主庫master|primary** 。當客戶端要向資料庫寫入時,它必須將請求傳送給 **領導者**,領導者會將新資料寫入其本地儲存。
2. 其他副本被稱為 **追隨者followers**,亦稱為 **只讀副本read replicas****從庫slaves****備庫( secondaries****熱備hot-standby**[^i]。每當領導者將新資料寫入本地儲存時,它也會將資料變更傳送給所有的追隨者,稱之為 **複製日誌replication log** 記錄**變更流change stream**。每個跟隨者從領導者拉取日誌,並相應更新其本地資料庫副本,方法是按照領導者處理的相同順序應用所有寫入。
3. 當客戶想要從資料庫中讀取資料時,它可以向領導者或追隨者查詢。 但只有領導者才能接受寫操作(從客戶端的角度來看從庫都是隻讀的)。
1. 其中一個副本被指定為 **領導者leader**,也稱為 **主庫master|primary** 。當客戶端要向資料庫寫入時,它必須將請求傳送給**領導者**,其會將新資料寫入其本地儲存。
2. 其他副本被稱為 **追隨者followers**,亦稱為 **只讀副本read replicas**、**從庫slaves**、**備庫( secondaries** 或 **熱備hot-standby**[^i]。每當領導者將新資料寫入本地儲存時,它也會將資料變更傳送給所有的追隨者,稱之為 **複製日誌replication log****變更流change stream**。每個跟隨者從領導者拉取日誌,並相應更新其本地資料庫副本,方法是按照與領導者相同的處理順序來進行所有寫入。
3. 當客戶想要從資料庫中讀取資料時,它可以向領導者或任一追隨者進行查詢。但只有領導者才能接受寫操作(從客戶端的角度來看從庫都是隻讀的)。
[^i]: 不同的人對 **熱hot****溫warm****冷cold** 備份伺服器有不同的定義。 例如在 PostgreSQL 中,**熱備hot standby** 指的是能接受客戶端讀請求的副本。而 **溫備warm standby** 只是追隨領導者,但不處理客戶端的任何查詢。 就本書而言,這些差異並不重要。
[^i]: 不同的人對 **熱hot**、**溫warm** 和 **冷cold** 備份伺服器有不同的定義。例如在 PostgreSQL 中,**熱備hot standby** 指的是能接受客戶端讀請求的副本。而 **溫備warm standby** 只是追隨領導者,但不處理客戶端的任何查詢。就本書而言,這些差異並不重要。
![](../img/fig5-1.png)
**圖 5-1 基於領導者 (主 - 從) 的複製**
**圖 5-1 基於領導者的(主/從)複製**
這種複製模式是許多關係資料庫的內建功能,如 PostgreSQL從 9.0 版本開始)MySQLOracle Data Guard 【2】和 SQL Server 的 AlwaysOn 可用性組【3】。 它也被用於一些非關係資料庫,包括 MongoDBRethinkDB 和 Espresso 【4】。 最後,基於領導者的複製並不僅限於資料庫:像 Kafka 【5】和 RabbitMQ 高可用佇列【6】這樣的分散式訊息代理也使用它。 某些網路檔案系統,例如 DRBD 這樣的塊複製裝置也與之類似。
這種複製模式是許多關係資料庫的內建功能,如 PostgreSQL從 9.0 版本開始)、MySQL、Oracle Data Guard【2】和 SQL Server 的 AlwaysOn 可用性組【3】。 它也被用於一些非關係資料庫,包括 MongoDB、RethinkDB 和 Espresso【4】。最後,基於領導者的複製並不僅限於資料庫:像 Kafka【5】和 RabbitMQ 高可用佇列【6】這樣的分散式訊息代理也使用它。某些網路檔案系統例如 DRBD 這樣的塊複製裝置也與之類似。
### 同步複製與非同步複製
複製系統的一個重要細節是:複製是 **同步synchronously** 發生還是 **非同步asynchronously** 發生。 (在關係型資料庫中這通常是一個配置項,其他系統通常硬編碼為其中一個)。
複製系統的一個重要細節是:複製是 **同步synchronously** 發生還是 **非同步asynchronously** 發生。(在關係型資料庫中這通常是一個配置項,其他系統通常硬編碼為其中一個)。
想象 [圖 5-1](fig5-1.png) 中發生的情況,網站的使用者更新他們的個人頭像。在某個時間點,客戶向主庫傳送更新請求;不久之後主庫就收到了請求。在某個時刻,主庫又會將資料變更轉發給自己的從庫。最後,主庫通知客戶更新成功。
想象一下 [圖 5-1](fig5-1.png) 中發生的場景,即網站的使用者更新他們的個人頭像。在某個時間點,客戶向主庫傳送更新請求;不久之後主庫就收到了請求。在某個時間點,主庫又會將資料變更轉發給自己的從庫。最終,主庫通知客戶更新成功。
[圖 5-2](../img/fig5-2.png) 顯示了系統各個元件之間的通訊:使用者客戶端,主庫和兩個從庫。時間從左到右流動。請求或響應訊息用粗箭頭表示。
[圖 5-2](../img/fig5-2.png) 顯示了系統各個元件之間的通訊:使用者客戶端、主庫和兩個從庫。時間從左向右流動。請求或響應訊息用粗箭頭表示。
![](../img/fig5-2.png)
**圖 5-2 基於領導者的複製:一個同步從庫和一個非同步從庫**
在 [圖 5-2](../img/fig5-2.png) 的示例中,從庫 1 的複製是同步的:在向用戶報告寫入成功並使結果對其他使用者可見之前,主庫需要等待從庫 1 的確認,確保從庫 1 已經收到寫入操作。以及在使寫入對其他客戶端可見之前接收到寫入。跟隨者 2 的複製是非同步的:主庫傳送訊息,但不等待從庫的響應。
在 [圖 5-2](../img/fig5-2.png) 的示例中,從庫 1 的複製是同步的:在向用戶報告寫入成功並使結果對其他使用者可見之前,主庫需要等待從庫 1 的確認,確保從庫 1 已經收到寫入操作。而從庫 2 的複製是非同步的:主庫傳送訊息,但不等待從庫的響應。
在這幅圖中,從庫 2 處理訊息前存在一個顯著的延遲。通常情況下,複製的速度相當快:大多數資料庫系統能在不到一秒內完成從庫的同步,但它們不能提供複製用時的保證。有些情況下,從庫可能落後主庫幾分鐘或更久;例如:從庫正在從故障中恢復,系統在最大容量附近執行,或者如果節點間存在網路問題
在這幅圖中,從庫 2 處理訊息前存在一個顯著的延遲。通常情況下,複製的速度相當快:大多數資料庫系統能在不到一秒內完成從庫的同步,但它們不能提供複製用時的保證。有些情況下,從庫可能落後主庫幾分鐘或更久,例如:從庫正在從故障中恢復,系統正在最大容量附近執行,或者當節點間存在網路問題時
同步複製的優點是,從庫保證有與主庫一致的最新資料副本。如果主庫突然失效,我們可以確信這些資料仍然能在從庫上找到。缺點是,如果同步從庫沒有響應(比如它已經崩潰,或者出現網路故障,或其它任何原因),主庫就無法處理寫入操作。主庫必須阻止所有寫入,並等待同步副本再次可用。
同步複製的優點是,從庫保證有與主庫一致的最新資料副本。如果主庫突然失效,我們可以確信這些資料仍然能在從庫上找到。缺點是,如果同步從庫沒有響應(比如它已經崩潰,或者出現網路故障,或其它任何原因),主庫就無法處理寫入操作。主庫必須阻止所有寫入,並等待同步副本再次可用。
因此,將所有從庫都設定為同步的是不切實際的:任何一個節點的中斷都會導致整個系統停滯不前。實際上,如果在資料庫上啟用同步複製,通常意味著其中 **一個** 跟隨者是同步的,而其他的則是非同步的。如果同步從庫變得不可用或緩慢,則使一個非同步從庫同步。這保證你至少在兩個節點上擁有最新的資料副本:主庫和同步從庫。 這種配置有時也被稱為 **半同步semi-synchronous**【7】。
因此,將所有從庫都設定為同步的是不切實際的:任何一個節點的中斷都會導致整個系統停滯不前。實際上,如果在資料庫上啟用同步複製,通常意味著其中 **一個** 從庫是同步的,而其他的從庫則是非同步的。如果該同步從庫變得不可用或緩慢,則將一個非同步從庫改為同步執行。這保證你至少在兩個節點上擁有最新的資料副本:主庫和同步從庫。 這種配置有時也被稱為 **半同步semi-synchronous**【7】。
通常情況下,基於領導者的複製都配置為完全非同步。 在這種情況下,如果主庫失效且不可恢復,則任何尚未複製給從庫的寫入都會丟失。 這意味著即使已經向客戶端確認成功,寫入也不能保證 **持久Durable** 然而,一個完全非同步的配置也有優點:即使所有的從庫都落後了,主庫也可以繼續處理寫入。
通常情況下,基於領導者的複製都配置為完全非同步。在這種情況下,如果主庫失效且不可恢復,則任何尚未複製給從庫的寫入都會丟失。這意味著即使已經向客戶端確認成功,寫入也不能保證 **持久Durable** 。然而,一個完全非同步的配置也有優點:即使所有的從庫都落後了,主庫也可以繼續處理寫入。
弱化的永續性可能聽起來像是一個壞的折衷,然而非同步複製已經被廣泛使用了,特別當有很多追隨者,或追隨者異地分佈時。 稍後將在 “[複製延遲問題](#複製延遲問題)” 中回到這個問題。
弱化的永續性可能聽起來像是一個壞的折衷,但非同步複製其實已經被廣泛使用了,特別是在有很多從庫的場景下,或者當從庫在地理上分佈很廣的時候。我們將在討論 “[複製延遲問題](#複製延遲問題)” 時回到這個問題。
> ### 關於複製的研究
>
> 對於非同步複製系統而言,主庫故障時有可能丟失資料。這可能是一個嚴重的問題,因此研究人員仍在研究不丟資料但仍能提供良好效能和可用性的複製方法。 例如,**鏈式複製**【8,9】] 是同步複製的一種變體,已經在一些系統(如 Microsoft Azure 儲存【10,11】中成功實現。
> 對於非同步複製系統而言,主庫故障時會丟失資料可能是一個嚴重的問題,因此研究人員仍在研究不丟資料但仍能提供良好效能和可用性的複製方法。例如,**鏈式複製chain replication**【8,9】是同步複製的一種變體已經在一些系統如 Microsoft Azure Storage【10,11】中成功實現。
>
> 複製的一致性與 **共識**consensus使幾個節點就某個值達成一致之間有著密切的聯絡[第九章](ch9.md) 將詳細地探討這一領域的理論。本章主要討論實踐中資料庫常用的簡單複製形式。
> 複製的一致性與 **共識**consensus使幾個節點就某個值達成一致之間有著密切的聯絡[第九章](ch9.md) 將詳細地探討這一領域的理論。本章主要討論實踐中資料庫常用的簡單複製形式。
>
### 設定新從庫
有時候需要設定一個新的從庫:也許是為了增加副本的數量,或替換失敗的節點。如何確保新的從庫擁有主庫資料的精確副本?
簡單地將資料檔案從一個節點複製到另一個節點通常是不夠的:客戶端不斷向資料庫寫入資料,資料總是在不斷變化,標準的資料副本會在不同的時間點總是不一樣。複製的結果可能沒有任何意義。
簡單地將資料檔案從一個節點複製到另一個節點通常是不夠的:客戶端不斷向資料庫寫入資料,資料總是在不斷地變化,標準的檔案複製會看到資料庫的不同部分在不同的時間點的內容,其結果可能沒有任何意義。
可以透過鎖定資料庫(使其不可用於寫入)來使磁碟上的檔案保持一致,但是這會違背高可用的目標。幸運的是,拉起新的從庫通常並不需要停機。從概念上講,過程如下所示:
可以透過鎖定資料庫(使其不可用於寫入)來使磁碟上的檔案保持一致,但是這會違背高可用的目標。幸運的是,設定新從庫通常並不需要停機。從概念上講,過程如下所示:
1. 在某個時刻獲取主庫的一致性快照(如果可能不必鎖定整個資料庫。大多數資料庫都具有這個功能,因為它是備份必需的。對於某些場景,可能需要第三方工具,例如 MySQL 的 innobackupex 【12】。
1. 在某個時刻獲取主庫的一致性快照(如果可能,不必鎖定整個資料庫。大多數資料庫都具有這個功能,因為它是備份必需的。對於某些場景,可能需要第三方工具,例如用於 MySQL 的 innobackupex【12】。
2. 將快照複製到新的從庫節點。
3. 從庫連線到主庫,並拉取快照之後發生的所有資料變更。這要求快照與主庫複製日誌中的位置精確關聯。該位置有不同的名稱例如PostgreSQL 將其稱為 **日誌序列號log sequence number, LSN**MySQL 將其稱為 **二進位制日誌座標binlog coordinates**
4. 當從庫處理完快照之後積壓的資料變更,我們說它 **趕上caught up** 了主庫。現在它可以繼續處理主庫產生的資料變化了。
3. 從庫連線到主庫,並拉取快照之後發生的所有資料變更。這要求快照與主庫複製日誌中的位置精確關聯。該位置有不同的名稱,例如 PostgreSQL 將其稱為 **日誌序列號log sequence numberLSN**MySQL 將其稱為 **二進位制日誌座標binlog coordinates**
4. 當從庫處理完快照之後積累的資料變更,我們就說它 **趕上caught up** 了主庫,現在它可以繼續及時處理主庫產生的資料變化了。
建立從庫的實際步驟因資料庫而異。在某些系統中,這個過程是完全自動化的,而在另外一些系統中,它可能是一個需要由管理員手動執行的有點神祕的多步驟工作流。
建立從庫的實際步驟因資料庫而異。在某些系統中,這個過程是完全自動化的,而在另外一些系統中,它可能是一個需要由管理員手動執行的有點神祕的多步驟工作流。
### 處理節點宕機
系統中的任何節點都可能宕機,可能因為意外的故障,也可能由於計劃內的維護(例如,重啟機器以安裝核心安全補丁)。對運維而言,能在系統不中斷服務的情況下重啟單個節點好處多多。我們的目標是,即使個別節點失效,也能保持整個系統執行,並儘可能控制節點停機帶來的影響。
如何透過基於主庫的複製實現高可用?
如何透過基於領導者的複製實現高可用?
#### 從庫失效:追趕恢復
在其本地磁碟上,每個從庫記錄從主庫收到的資料變更。如果從庫崩潰並重新啟動,或者,如果主庫和從庫之間的網路暫時中斷,則比較容易恢復:從庫可以從日誌中知道,在發生故障之前處理的最後一個事務。因此,從庫可以連線到主庫,並請求在從庫斷開期間發生的所有資料變更。當應用完所有這些變後,它就趕上了主庫,並可以像以前一樣繼續接收資料變更流。
在其本地磁碟上,每個從庫記錄從主庫收到的資料變更。如果從庫崩潰並重新啟動,或者,如果主庫和從庫之間的網路暫時中斷,則比較容易恢復:從庫可以從日誌中知道,在發生故障之前處理的最後一個事務。因此,從庫可以連線到主庫,並請求在從庫斷開期間發生的所有資料變更。當應用完所有這些變後,它就趕上了主庫,並可以像以前一樣繼續接收資料變更流。
#### 主庫失效:故障切換
主庫失效處理起來相當棘手:其中一個從庫需要被提升為新的主庫,需要重新配置客戶端,以將它們的寫操作傳送給新的主庫,其他從庫需要開始拉取來自新主庫的資料變更。這個過程被稱為 **故障切換failover**
故障切換可以手動進行(通知管理員主庫掛了,並採取必要的步驟來建立新的主庫)或自動進行。自動故障切換過程通常由以下步驟組成:
故障切換可以手動進行(通知管理員主庫掛了,並採取必要的步驟來建立新的主庫)或自動進行。自動故障切換過程通常由以下步驟組成:
1. 確認主庫失效。有很多事情可能會出錯:崩潰,停電,網路問題等等。沒有萬無一失的方法來檢測出現了什麼問題,所以大多數系統只是簡單使用 **超時Timeout** :節點頻繁地相互來回傳遞訊息,並且如果一個節點在一段時間內(例如 30 秒)沒有響應,就認為它掛了(因為計劃內維護而故意關閉主庫不算)。
2. 選擇一個新的主庫。這可以透過選舉過程(主庫由剩餘副本以多數選舉產生)來完成,或者可以由之前選定的 **控制器節點controller node** 來指定新的主庫。主庫的最佳人選通常是擁有舊主庫最新資料副本的從庫(最小化資料損失)。讓所有的節點同意一個新的領導者,是一個 **共識** 問題,將在 [第九章](ch9.md) 詳細討論。
1. 確認主庫失效。有很多事情可能會出錯:崩潰、停電、網路問題等等。沒有萬無一失的方法來檢測出現了什麼問題,所以大多數系統只是簡單使用 **超時Timeout** :節點頻繁地相互來回傳遞訊息,如果一個節點在一段時間內(例如 30 秒)沒有響應,就認為它掛了(因為計劃內維護而故意關閉主庫不算)。
2. 選擇一個新的主庫。這可以透過選舉過程(主庫由剩餘副本以多數選舉產生)來完成,或者可以由之前選定的 **控制器節點controller node** 來指定新的主庫。主庫的最佳人選通常是擁有舊主庫最新資料副本的從庫(最小化資料損失)。讓所有的節點同意一個新的領導者,是一個 **共識** 問題,將在 [第九章](ch9.md) 詳細討論。
3. 重新配置系統以啟用新的主庫。客戶端現在需要將它們的寫請求傳送給新主庫(將在 “[請求路由](ch6.md#請求路由)” 中討論這個問題)。如果舊主庫恢復,可能仍然認為自己是主庫,而沒有意識到其他副本已經讓它失去領導權了。系統需要確保舊主庫意識到新主庫的存在,併成為一個從庫。
故障切換會出現很多大麻煩
故障切換的過程中有很多地方可能出錯
* 如果使用非同步複製,則新主庫可能沒有收到老主庫宕機前最後的寫入操作。在選出新主庫後,如果老主庫重新加入叢集,新主庫在此期間可能會收到衝突的寫入,那這些寫入該如何處理?最常見的解決方案是簡單丟棄老主庫未複製的寫入,這很可能打破客戶對於資料永續性的期望。
* 如果資料庫需要和其他外部儲存相協調,那麼丟棄寫入內容是極其危險的操作。例如在 GitHub 【13】的一場事故中一個過時的 MySQL 從庫被提升為主庫。資料庫使用自增 ID 作為主鍵,因為新主庫的計數器落後於老主庫的計數器,所以新主庫重新分配了一些已經被老主庫分配掉的 ID 作為主鍵。這些主鍵也在 Redis 中使用,主鍵重用使得 MySQL 和 Redis 中資料產生不一致,最後導致一些私有資料洩漏到錯誤的使用者手中。
* 如果資料庫需要和其他外部儲存相協調,那麼丟棄寫入內容是極其危險的操作。例如在 GitHub 【13】的一場事故中一個過時的 MySQL 從庫被提升為主庫。資料庫使用自增 ID 作為主鍵,因為新主庫的計數器落後於老主庫的計數器,所以新主庫重新分配了一些已經被老主庫分配掉的 ID 作為主鍵。這些主鍵也在 Redis 中使用,主鍵重用使得 MySQL 和 Redis 中資料產生不一致,最後導致一些私有資料洩漏到錯誤的使用者手中。
* 發生某些故障時(見 [第八章](ch8.md))可能會出現兩個節點都以為自己是主庫的情況。這種情況稱為 **腦裂 (split brain)**,非常危險:如果兩個主庫都可以接受寫操作,卻沒有衝突解決機制(請參閱 “[多主複製](#多主複製)”),那麼資料就可能丟失或損壞。一些系統採取了安全防範措施:當檢測到兩個主庫節點同時存在時會關閉其中一個節點 [^ii]但設計粗糙的機制可能最後會導致兩個節點都被關閉【14】。
[^ii]: 這種機制稱為 **遮蔽fencing**充滿感情的術語是:**爆彼之頭Shoot The Other Node In The Head, STONITH**。
[^ii]: 這種機制稱為 **屏障fencing**,或者更充滿感情的術語是:**爆彼之頭Shoot The Other Node In The Head, STONITH**。我們將在 “[領導者和鎖](ch8.md#領導者和鎖)” 中對屏障進行詳細討論。
* 主庫被宣告死亡之前的正確超時應該怎麼配置?在主庫失效的情況下,超時時間越長意味著恢復時間也越長。但是如果超時設定太短,又可能會出現不必要的故障切換。例如,臨時負載峰值可能導致節點的響應時間超時,或網路故障可能導致資料包延遲。如果系統已經處於高負載或網路問題的困擾之中,那麼不必要的故障切換可能會讓情況變得更糟糕。
* 主庫被宣告死亡之前的正確超時應該怎麼配置?在主庫失效的情況下,超時時間越長意味著恢復時間也越長。但是如果超時設定太短,又可能會出現不必要的故障切換。例如,臨時負載峰值可能導致節點的響應時間增加到超出超時時間,或網路故障可能導致資料包延遲。如果系統已經處於高負載或網路問題的困擾之中,那麼不必要的故障切換可能會讓情況變得更糟糕。
這些問題沒有簡單的解決方案。因此,即使軟體支援自動故障切換,不少運維團隊還是更願意手動執行故障切換。
節點故障、不可靠的網路、對副本一致性,永續性,可用性和延遲的權衡 ,這些問題實際上是分散式系統中的基本問題。[第八章](ch8.md) 和 [第九章](ch9.md) 將更深入地討論它們。
節點故障、不可靠的網路、對副本一致性、永續性、可用性和延遲的權衡,這些問題實際上是分散式系統中的基本問題。[第八章](ch8.md) 和 [第九章](ch9.md) 將更深入地討論它們。
### 複製日誌的實現
基於主庫的複製底層是如何工作的?實踐中有好幾種不同的複製方式,所以先簡要地看一下。
基於領導者的複製在底層是如何工作的?實踐中有好幾種不同的複製方式,所以先簡要地看一下。
#### 基於語句的複製
在最簡單的情況下,主庫記錄下它執行的每個寫入請求(**語句**,即 statement並將該語句日誌傳送給從庫。對於關係資料庫來說,這意味著每個 `INSERT`、`UPDATE` 或 `DELETE` 語句都被轉發給每個從庫,每個從庫解析並執行該 SQL 語句,就像從客戶端收到一樣。
在最簡單的情況下,主庫記錄下它執行的每個寫入請求(**語句**,即 statement並將該語句日誌傳送給從庫。對於關係資料庫來說這意味著每個 `INSERT`、`UPDATE` 或 `DELETE` 語句都被轉發給每個從庫,每個從庫解析並執行該 SQL 語句,就像直接從客戶端收到一樣。
雖然聽上去很合理,但有很多問題會搞砸這種複製方式:
* 任何呼叫 **非確定性函式nondeterministic** 的語句,可能會在每個副本上生成不同的值。例如,使用 `NOW()` 獲取當前日期時間,或使用 `RAND()` 獲取一個隨機數。
* 如果語句使用了 **自增列auto increment**,或者依賴於資料庫中的現有資料(例如,`UPDATE ... WHERE <某些條件>`),則必須在每個副本上按照完全相同的順序執行它們,否則可能會產生不同的效果。當有多個併發執行的事務時,這可能成為一個限制。
* 有副作用的語句(例如:觸發器、儲存過程、使用者定義的函式)可能會在每個副本上產生不同的副作用,除非副作用是絕對確定的。
* 有副作用的語句(例如:觸發器、儲存過程、使用者定義的函式)可能會在每個副本上產生不同的副作用,除非副作用是絕對確定的。
的確有辦法繞開這些問題 —— 例如,當語句被記錄時,主庫可以用固定的返回值替換任何不確定的函式呼叫,以便從庫獲得相同的值。但是由於邊緣情況實在太多了,現在通常會選擇其他的複製方法。
的確有辦法繞開這些問題 —— 例如,當語句被記錄時,主庫可以用固定的返回值替換任何不確定的函式呼叫,以便所有從庫都能獲得相同的值。但是由於邊緣情況實在太多了,現在通常會選擇其他的複製方法。
基於語句的複製在 5.1 版本前的 MySQL 中使用。因為它相當緊湊現在有時候也還在用。但現在在預設情況下如果語句中存在任何不確定性MySQL 會切換到基於行的複製(稍後討論)。 VoltDB 使用了基於語句的複製但要求事務必須是確定性的以此來保證安全【15】。
基於語句的複製在 5.1 版本前的 MySQL 中使用。因為它相當緊湊現在有時候也還在用。但現在在預設情況下如果語句中存在任何不確定性MySQL 會切換到基於行的複製(稍後討論)。 VoltDB 使用了基於語句的複製但要求事務必須是確定性的以此來保證安全【15】。
#### 傳輸預寫式日誌WAL
在 [第三章](ch3.md) 中,我們討論了儲存引擎如何在磁碟上表示資料,並且我們發現,通常寫操作都是追加到日誌中:
在 [第三章](ch3.md) 中,我們討論了儲存引擎如何在磁碟上表示資料,我們也發現了通常會將寫操作追加到日誌中:
* 對於日誌結構儲存引擎(請參閱 “[SSTables 和 LSM 樹](ch3.md#SSTables和LSM樹)”),日誌是主要的儲存位置。日誌段在後臺壓縮,並進行垃圾回收。
* 對於覆寫單個磁碟塊的 [B 樹](ch3.md#B樹),每次修改都會先寫入 **預寫式日誌Write Ahead Log, WAL**,以便崩潰後索引可以恢復到一個一致的狀態。
在任何一種情況下,日誌都是包含所有資料庫寫入的僅追加位元組序列。可以使用完全相同的日誌在另一個節點上構建副本:除了將日誌寫入磁碟之外,主庫還可以透過網路將其傳送給從庫。
在任何一種情況下,日誌都是包含所有資料庫寫入的僅追加位元組序列。可以使用完全相同的日誌在另一個節點上構建副本:除了將日誌寫入磁碟之外,主庫還可以透過網路將其傳送給從庫。
當從庫應用這個日誌時,它會建立和主庫一模一樣資料結構的副本
透過使用這個日誌,從庫可以構建一個與主庫一模一樣的資料結構複製
PostgreSQL 和 Oracle 等使用這種複製方法【16】。主要缺點是日誌記錄的資料非常底層WAL 包含哪些磁碟塊中的哪些位元組發生了更改。這使複製與儲存引擎緊密耦合。如果資料庫將其儲存格式從一個版本更改為另一個版本,通常不可能在主庫和從庫上執行不同版本的資料庫軟體。
這種複製方法在 PostgreSQL 和 Oracle 等一些產品中被使用到【16】。其主要缺點是日誌記錄的資料非常底層WAL 包含哪些磁碟塊中的哪些位元組發生了更改。這使複製與儲存引擎緊密耦合。如果資料庫將其儲存格式從一個版本更改為另一個版本,通常不可能在主庫和從庫上執行不同版本的資料庫軟體。
看上去這可能只是一個小的實現細節,但卻可能對運維產生巨大的影響。如果複製協議允許從庫使用比主庫更新的軟體版本,則可以先升級從庫,然後執行故障切換,使升級後的節點之一成為新的主庫,從而執行資料庫軟體的零停機升級。如果複製協議不允許版本不匹配(傳輸 WAL 經常出現這種情況),則此類升級需要停機。
看上去這可能只是一個小的實現細節,但卻可能對運維產生巨大的影響。如果複製協議允許從庫使用比主庫更新的軟體版本,則可以先升級從庫,然後執行故障切換,使升級後的節點之一成為新的主庫,從而允許資料庫軟體的零停機升級。如果複製協議不允許版本不匹配(傳輸 WAL 經常出現這種情況),則此類升級需要停機。
#### 邏輯日誌複製(基於行)
另一種方法是,複製和儲存引擎使用不同的日誌格式,這樣可以使複製日誌從儲存引擎內部分離出來。這種複製日誌被稱為邏輯日誌,以將其與儲存引擎的(物理)資料表示區分開來。
另一種方法是對複製和儲存引擎使用不同的日誌格式這樣可以將複製日誌從儲存引擎的內部實現中解耦出來。這種複製日誌被稱為邏輯日誌logical log,以將其與儲存引擎的(物理)資料表示區分開來。
關係資料庫的邏輯日誌通常是以行的粒度描述對資料庫表的寫入記錄序列:
關係資料庫的邏輯日誌通常是以行的粒度描述對資料庫表的寫入記錄序列:
* 對於插入的行,日誌包含所有列的新值。
* 對於刪除的行,日誌包含足夠的資訊來唯一標識已刪除的行。通常是主鍵,但是如果表上沒有主鍵,則需要記錄所有列的舊值。
* 對於更新的行,日誌包含足夠的資訊來唯一標識更新的行,以及所有列的新值(或至少所有已更改的列的新值)。
* 對於刪除的行,日誌包含足夠的資訊來唯一標識被刪除的行,這通常是主鍵,但如果表上沒有主鍵,則需要記錄所有列的舊值。
* 對於更新的行,日誌包含足夠的資訊來唯一標識更新的行,以及所有列的新值(或至少所有已更改的列的新值)。
修改多行的事務會生成多個這樣的日誌記錄,後面跟著一條記錄,指出事務已經提交。 MySQL 的二進位制日誌當配置為使用基於行的複製時使用這種方法【17】。
修改多行的事務會生成多條這樣的日誌記錄,後面跟著一條指明事務已經提交的記錄。 MySQL 的二進位制日誌(當配置為使用基於行的複製時)使用這種方法【17】。
由於邏輯日誌與儲存引擎內部分離,因此可以更容易地保持向後相容,從而使領導者和跟隨者能夠執行不同版本的資料庫軟體甚至不同的儲存引擎。
由於邏輯日誌與儲存引擎的內部實現是解耦的,系統可以更容易地做到向後相容,從而使主庫和從庫能夠執行不同版本的資料庫軟體,或者甚至不同的儲存引擎。
對於外部應用程式來說,邏輯日誌格式也更容易解析。如果要將資料庫的內容傳送到外部系統,這一點很有用,例如複製到資料倉庫進行離線分析或建立自定義索引和快取【18】。 這種技術被稱為 **資料變更捕獲change data capture**[第十一章](ch11.md) 將重新講到它。
對於外部應用程式來說邏輯日誌格式也更容易解析。如果要將資料庫的內容傳送到外部系統例如複製到資料倉庫進行離線分析或建立自定義索引和快取【18】,這一點會很有用。這種技術被稱為 **資料變更捕獲change data capture**[第十一章](ch11.md) 將重新講到它。
#### 基於觸發器的複製
到目前為止描述的複製方法是由資料庫系統實現的,不涉及任何應用程式程式碼。在很多情況下,這就是你想要的。但在某些情況下需要更多的靈活性。例如,如果你只想複製資料的一個子集,或者想從一種資料庫複製到另一種資料庫,或者如果你需要衝突解決邏輯(請參閱 “[處理寫入衝突](#處理寫入衝突)”),則可能需要將複製移到應用程式層。
到目前為止描述的複製方法是由資料庫系統實現的,不涉及任何應用程式程式碼。在很多情況下,這就是你想要的。但在某些情況下需要更多的靈活性。例如,如果你只想複製資料的一個子集,或者想從一種資料庫複製到另一種資料庫,或者如果你需要衝突解決邏輯(請參閱 “[處理寫入衝突](#處理寫入衝突)”),則可能需要將複製操作上移到應用程式層。
一些工具,如 Oracle Golden Gate【19】可以透過讀取資料庫日誌使得其他應用程式可以使用資料。另一種方法是使用許多關係資料庫自帶的功能觸發器和儲存過程。
觸發器允許你將資料更改寫入事務發生時自動執行的自定義應用程式程式碼註冊在資料庫系統中。觸發器有機會將更改記錄到一個單獨的表中使用外部程式讀取這個表再加上一些必要的業務邏輯就可以將資料變更復制到另一個系統去。例如Databus for Oracle【20】和 Bucardo for Postgres【21】就是這樣工作的。
基於觸發器的複製通常比其他複製方法具有更高的開銷,並且比資料庫內建複製更容易出錯,也有很多限制。然而由於其靈活性,仍然是很有用的。
基於觸發器的複製通常比其他複製方法具有更高的開銷,並且比資料庫內建複製更容易出錯,也有很多限制。然而由於其靈活性,仍然是很有用的。
## 複製延遲問題
容忍節點故障只是需要複製的一個原因。正如在 [第二部分](part-ii.md) 的介紹中提到的,另一個原因是可伸縮性(處理比單個機器更多的請求)和延遲(讓副本在地理位置上更接近使用者)。
基於主庫的複製要求所有寫入都由單個節點處理但只讀查詢可以由任何副本處理。所以對於讀多寫少的場景Web 上的常見模式),一個有吸引力的選擇是建立很多從庫,並將讀請求分散到所有的從庫上去。這樣能減小主庫的負載,並允許向最近的副本傳送讀請求。
基於領導者的複製要求所有寫入都由單個節點處理但只讀查詢可以由任何副本處理。所以對於讀多寫少的場景Web 上的常見模式),一個有吸引力的選擇是建立很多從庫,並將讀請求分散到所有的從庫上去。這樣能減小主庫的負載,並允許向最近的副本傳送讀請求。
在這種伸縮體系結構中,只需新增更多的追隨者,就可以提高只讀請求的服務容量。但是,這種方法實際上只適用於非同步複製 —— 如果嘗試同步複製到所有追隨者,則單個節點故障或網路中斷將使整個系統無法寫入。而且越多的節點越有可能會被關閉,所以完全同步的配置是非常不可靠的。
@ -381,7 +381,7 @@ PostgreSQL 和 Oracle 等使用這種複製方法【16】。主要缺點是日
單主資料庫按順序進行寫操作:如果同一個欄位有多個更新,則最後一個寫操作將決定該欄位的最終值。
在多主配置中,沒有明確的寫入順序,所以最終值應該是什麼並不清楚。在 [圖 5-7](../img/fig5-7.png) 中,在主庫 1 中標題首先更新為 B 而後更新為 C在主庫 2 中,首先更新為 C然後更新為 B。兩個順序都不是 “更正確” 的
在多主配置中,沒有明確的寫入順序,所以最終值應該是什麼並不清楚。在 [圖 5-7](../img/fig5-7.png) 中,在主庫 1 中標題首先更新為 B 而後更新為 C在主庫 2 中,首先更新為 C然後更新為 B。兩種順序都不比另一種“更正確”
如果每個副本只是按照它看到寫入的順序寫入,那麼資料庫最終將處於不一致的狀態:最終值將是在主庫 1 的 C 和主庫 2 的 B。這是不可接受的每個複製方案都必須確保資料在所有副本中最終都是相同的。因此資料庫必須以一種 **收斂convergent** 的方式解決衝突,這意味著所有副本必須在所有變更復制完成時收斂至一個相同的最終值。
@ -410,9 +410,9 @@ PostgreSQL 和 Oracle 等使用這種複製方法【16】。主要缺點是日
> #### 自動衝突解決
>
> 衝突解決規則可能很快變得複雜並且自定義程式碼可能容易出錯。亞馬遜是一個經常被引用的例子由於衝突解決處理程式令人意外的效果一段時間以來購物車上的衝突解決邏輯將保留新增到購物車的物品但不包括從購物車中移除的物品。因此顧客有時會看到物品重新出現在他們的購物車中即使他們之前已經被移走【37】。
> 衝突解決規則可能很快變得複雜,並且自定義程式碼可能容易出錯。亞馬遜是一個經常被引用的例子,由於衝突解決處理程式而產生令人意外的效果一段時間以來購物車上的衝突解決邏輯將保留新增到購物車的物品但不包括從購物車中移除的物品。因此顧客有時會看到物品重新出現在他們的購物車中即使他們之前已經被移走【37】。
>
> 已經有一些有趣的研究來自動解決由於資料修改引起的衝突。有幾研究值得一提:
> 已經有一些有趣的研究來自動解決由於資料修改引起的衝突。有幾研究值得一提:
>
> * **無衝突複製資料型別Conflict-free replicated datatypesCRDT**【32,38】是可以由多個使用者同時編輯的集合對映有序列表計數器等的一系列資料結構它們以合理的方式自動解決衝突。一些 CRDT 已經在 Riak 2.0 中實現【39,40】。
> * **可合併的持久資料結構Mergeable persistent data structures**【41】顯式跟蹤歷史記錄類似於 Git 版本控制系統,並使用三向合併功能(而 CRDT 使用雙向合併)。
@ -443,7 +443,7 @@ PostgreSQL 和 Oracle 等使用這種複製方法【16】。主要缺點是日
[^v]: 不要與星型模式混淆(請參閱 “[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”),其中描述了資料模型的結構,而不是節點之間的通訊拓撲。
在環形和星形拓撲中,寫入可能需要在到達所有副本之前透過多個節點。因此,節點需要轉發從其他節點收到的資料更改。為了防止無限複製迴圈,每個節點被賦予一個唯一的識別符號,並且在複製日誌中,每個寫入都被標記了所有已經過的節點的識別符號【43】。當一個節點收到用自己的識別符號標記的資料更改時該資料更改將被忽略因為節點知道它已經被處理過。
在環形和星形拓撲中,寫入可能需要在到達所有副本之前透過多個節點。因此,節點需要轉發從其他節點收到的資料更改。為了防止無限複製迴圈,每個節點被賦予一個唯一的識別符號,並且在複製日誌中,每次寫入都會使用其經過的所有節點的識別符號進行標記【43】。當一個節點收到用自己的識別符號標記的資料更改時該資料更改將被忽略因為節點知道它已經被處理過。
環形和星形拓撲的問題是,如果只有一個節點發生故障,則可能會中斷其他節點之間的複製訊息流,導致它們無法通訊,直到節點修復。拓撲結構可以重新配置為在發生故障的節點上工作,但在大多數部署中,這種重新配置必須手動完成。更密集連線的拓撲結構(例如全部到全部)的容錯性更好,因為它允許訊息沿著不同的路徑傳播,避免單點故障。
@ -476,7 +476,7 @@ PostgreSQL 和 Oracle 等使用這種複製方法【16】。主要缺點是日
假設你有一個帶有三個副本的資料庫,而其中一個副本目前不可用,或許正在重新啟動以安裝系統更新。在基於主機的配置中,如果要繼續處理寫入,則可能需要執行故障切換(請參閱「[處理節點宕機](#處理節點宕機)」)。
另一方面,在無領導配置中,故障切換不存在。[圖 5-10](../img/fig5-10.png) 顯示了發生了什麼事情:客戶端(使用者 1234並行傳送寫入到所有三個副本並且兩個可用副本接受寫入但是不可用副本錯過了它。假設三個副本中的兩個承認寫入是足夠的在使用者 1234 已經收到兩個確定的響應之後,我們認為寫入成功。客戶簡單地忽略了其中一個副本錯過了寫入的事實。
另一方面,在無領導配置中,不存在故障轉移。[圖 5-10](../img/fig5-10.png) 顯示了發生了什麼事情:客戶端(使用者 1234並行傳送寫入到所有三個副本並且兩個可用副本接受寫入但是不可用副本錯過了它。假設三個副本中的兩個承認寫入是足夠的在使用者 1234 已經收到兩個確定的響應之後,我們認為寫入成功。客戶簡單地忽略了其中一個副本錯過了寫入的事實。
![](../img/fig5-10.png)