MongoDB新存儲引擎WiredTiger實現(事務篇)

fzgd8551 8年前發布 | 41K 次閱讀 MongoDB 數據庫 NoSQL數據庫

導語:計算機硬件在飛速發展,數據規模在急速膨脹,但是數據庫仍然使用是十年以前的架構體系,WiredTiger 嘗試打破這一切,充分利用多核與大內存時代,開發一種真正滿足未來大數據管理所需的數據庫。本文由袁榮喜向「高可用架構」投稿,介紹對 WiredTiger 源代碼學習過程中對數據庫設計的感悟。

袁榮喜,學霸君工程師,2015年加入學霸君,負責學霸君的網絡實時傳輸和分布式系統的架構設計和實現,專注于基礎技術領域,在網絡傳輸、數據庫內核、分布式系統和并發編程方面有一定了解。

WiredTiger 從被 MongoDB 收購到成為 MongoDB 的默認存儲引擎的一年半,得到了迅猛的發展,也逐步被外部熟知。

現代計算機近 20 年來 CPU 的計算能力和內存容量飛速發展,但磁盤的訪問速度并沒有得到相應的提高,WT 就是在這樣的一個情況下研發出來,它設計了充分利用 CPU 并行計算的內存模型的無鎖并行框架,使得 WT 引擎在多核 CPU 上的表現優于其他存儲引擎。

針對磁盤存儲特性,WT 實現了一套基于 BLOCK/Extent 的友好的磁盤訪問算法,使得 WT 在數據壓縮和磁盤 I/O 訪問上優勢明顯。實現了基于 snapshot 技術的 ACID 事務,snapshot 技術大大簡化了 WT 的事務模型,摒棄了傳統的事務鎖隔離又同時能保證事務的 ACID。WT 根據現代內存容量特性實現了一種基于 Hazard Pointer 的 LRU cache 模型,充分利用了內存容量的同時又能擁有很高的事務讀寫并發。

在本文中,我們主要針對 WT 引擎的事務來展開分析,來看看它的事務是如何實現的。說到數據庫事務,必然先要對事務這個概念和 ACID 簡單的介紹。

基本概念:事務與 ACID

什么是事務?

事務就是通過一系列操作來完成一件事情,在進行這些操作的過程中,要么這些操作完全執行,要么這些操作全不執行,不存在中間狀態,事務分為事務執行階段和事務提交階段。一般說到事務,就會想到它的特性— ACID,那么什么是 ACID 呢?我們先用一個現實中的例子來說明:AB 兩同學賬號都有 1,000 塊錢,A 通過銀行轉賬向 B 轉了 100,這個事務分為兩個操作,即從 A 同學賬號扣除 100,向 B 同學賬號增加 100。

原子性(Atomicity)

組成事務的系列操作是一個整體,要么全執行,要么不執行。通過上面例子就是從 A 同學扣除錢和向 B 同學增加 100 是一起發生的,不可能出現扣除了 A 的錢,但沒增加 B 的錢的情況。

一致性(Consistency):

在事務開始之前和事務結束以后,數據庫的完整性和狀態沒有被破壞。這個怎么理解呢?就是 A、B 兩人在轉賬錢的總和是 2,000,轉賬后兩人的總和也必須是 2,000。不會因為這次轉賬事務破壞這個狀態。

隔離性(Isolation):

多個事務在并發執行時,事務執行的中間狀態是其他事務不可訪問的。A 轉出 100 但事務沒有確認提交,這時候銀行人員對其賬號查詢時,看到的應該還是 1,000,不是 900。

持久性(Durability)

事務一旦提交生效,其結果將永久保存,不受任何故障影響。A 轉賬一但完成,那么 A 就是 900,B 就是 1,100,這個結果將永遠保存在銀行的數據庫中,直到他們下次交易事務的發生。

WT 如何實現事務

知道了基本的事務概念和 ACID 后,來看看 WT 引擎是怎么來實現事務和 ACID。要了解實現先要知道它的事務的構造和使用相關的技術,WT 在實現事務的時使用主要是使用了三個技術:

  • snapshot(事務快照)
  • MVCC(多版本并發控制)
  • redo log(重做日志)

為了實現這三個技術,它還定義了一個基于這三個技術的事務對象和全局事務管理器。事務對象描述如下

wt_transaction{

transaction_id:    本次事務的全局唯一的ID,用于標示事務修改數據的版本號

snapshot_object:   當前事務開始或者操作時刻其他正在執行且并未提交的事務集合,用于事務隔離

operation_array:   本次事務中已執行的操作列表,用于事務回滾。

redo_log_buf:      操作日志緩沖區。用于事務提交后的持久化

State:             事務當前狀態

}

WT 的多版本并發控制

WT 中的 MVCC 是基于 key/value 中 value 值的鏈表,這個鏈表單元中存儲有當先版本操作的事務 ID 和操作修改后的值。描述如下:

wt_mvcc{

transaction_id:    本次修改事務的ID

value:             本次修改后的值

}

WT 中的數據修改都是在這個鏈表中進行 append 操作,每次對值做修改都是 append 到鏈表頭上,每次讀取值的時候讀是從鏈表頭根據值對應的修改事務 transaction_id 和本次讀事務的 snapshot 來判斷是否可讀,如果不可讀,向鏈表尾方向移動,直到找到讀事務能都的數據版本。樣例如下:

圖1,點擊圖片可以全屏縮放

上圖中,事務 T0 發生的時刻最早,T5 發生的時刻最晚。T1/T2/T4 是對記錄做了修改。那么在 MVCC list 當中就會增加 3 個版本的數據,分別是 11/12/14。如果事務都是基于 snapshot 級別的隔離,T0 只能看到 T0 之前提交的值 10,讀事務 T3 訪問記錄時它能看到的值是 11,T5 讀事務在訪問記錄時,由于 T4 未提交,它也只能看到 11 這個版本的值。這就是 WT 的 MVCC 基本原理。

WT 事務 snapshot

上面多次提及事務的 snapshot,那到底什么是事務的 snapshot 呢?其實就是 事務開始或者進行操作之前對整個 WT 引擎內部正在執行或者將要執行的事務進行一次快照 ,保存當時整個引擎所有事務的狀態,確定哪些事務是對自己見的,哪些事務都自己是不可見。說白了就是一些列事務 ID 區間。WT 引擎整個事務并發區間示意圖如下:

圖2,點擊圖片可以全屏縮放

WT 引擎中的 snapshot_oject 是有一個最小執行事務 snap_min、一個最大事務 snap max 和一個處于 [snap_min, snap_max] 區間之中所有正在執行的寫事務序列組成。如果上圖在 T6 時刻對系統中的事務做一次 snapshot,那么產生的

snapshot_object = {

snap_min=T1,

snap_max=T5,

snap_array={T1, T4, T5},

};

T6 能訪問的事務修改有兩個區間:所有小于 T1 事務的修改 [0, T1) 和 [snap_min, snap_max] 區間已經提交的事務 T2 的修改。換句話說,凡是出現在 snap_array 中或者事務 ID 大于 snap_max 的事務的修改對事務 T6 是不可見的。如果 T1 在建立 snapshot 之后提交了,T6 也是不能訪問到 T1 的修改。這個就是 snapshot 方式隔離的基本原理。

全局事務管理器

通過上面的 snapshot 的描述,我們可以知道要創建整個系統事務的快照截屏,就需要一個全局的事務管理來進行事務快照時的參考,在 WT 引擎中是如何定義這個全局事務管理器的呢?在 CPU 多核多線程下,它是如何來管理事務并發的呢?下面先來分析它的定義:

wt_txn_global{

current_id:       全局寫事務ID產生種子,一直遞增

oldest_id:        系統中最早產生且還在執行的寫事務ID

transaction_array: 系統事務對象數組,保存系統中所有的事務對象

scan_count:  正在掃描transaction_array數組的線程事務數,用于建立snapshot過程的無鎖并發

}

transaction_array 保存的是圖 2 正在執行事務的區間的事務對象序列。在建立 snapshot 時,會對整個 transaction_array 做掃描,確定 snap_min/snap_max/snap_array 這三個參數和更新 oldest_id,在掃描的過程中,凡是 transaction_id 不等于 WT_TNX_NONE 都認為是在執行中且有修改操作的事務,直接加入到 snap_array 當中。整個過程是一個無鎖操作過程,這個過程如下:

圖3,點擊圖片可以全屏縮放

創建 snapshot 快照的過程在 WT 引擎內部是非常頻繁,尤其是在大量自動提交型的短事務執行的情況下,由創建 snapshot 動作引起的 CPU 競爭是非常大的開銷,所以這里 WT 并沒有使用 spin lock,而是采用了上圖的一個無鎖并發設計,這種設計遵循了我們開始說的并發設計原則。

事務 ID

從 WT 引擎創建事務 snapshot 的過程中,現在可以確定,snapshot 的對象是有寫操作的事務,純讀事務是不會被 snapshot 的,因為 snapshot 的目的是隔離 MVCC list 中的記錄,通過 MVCC 中 value 的事務 ID 與讀事務的 snapshot 進行版本讀取,與讀事務本身的 ID 是沒有關系。

在 WT 引擎中,開啟事務時,引擎會將一個 WT_TNX_NONE(= 0) 的事務 ID 設置給開啟的事務,當它第一次對事務進行寫時,會在數據修改前通過全局事務管理器中的 current_id 來分配一個全局唯一的事務 ID。這個過程也是通過 CPU 的 CAS_ADD 原子操作完成的無鎖過程。

WT 的事務過程

一般事務是兩個階段:事務執行和事務提交。在事務執行前,我們需要先創建事務對象并開啟它,然后才開始執行,如果執行遇到沖突和或者執行失敗,我們需要回滾事務(rollback)。如果執行都正常完成,最后只需要提交(commit)它即可。

從上面的描述可以知道事務過程有:創建開啟、執行、提交和回滾。從這幾個過程中來分析 WT 是怎么實現這幾個過程的。

事務開啟

WT 事務開啟過程中,首先會為事務創建一個事務對象并把這個對象加入到全局事務管理器當中,然后通過事務配置信息確定事務的隔離級別和 redo log 的刷盤方式并將事務狀態設為執行狀態,最后判斷如果隔離級別是 ISOLATION_SNAPSHOT(snapshot 級的隔離),在本次事務執行前創建一個系統并發事務的 snapshot。至于為什么要在事務執行前創建一個 snapshot,在后面 WT 事務隔離章節詳細介紹。

事務執行

事務在執行階段,如果是讀操作,不做任何記錄,因為讀操作不需要回滾和提交。如果是寫操作,WT 會對每個寫操作做詳細的記錄。在上面介紹的事務對象(wt_transaction)中有兩個成員,一個是操作 operation_array,一個是 redo_log_buf。這兩個成員是來記錄修改操作的詳細信息,在 operation_array 的數組單元中,包含了一個指向 MVCC list 對應修改版本值的指針。詳細的更新操作流程如下:

  1. 創建一個 MVCC list 中的值單元對象(update)
  2. 根據事務對象的 transaction id 和事務狀態判斷是否為本次事務創建了寫的事務 ID,如果沒有,為本次事務分配一個事務 ID,并將事務狀態設成 HAS_TXN_ID 狀態。
  3. 將本次事務的 ID 設置到 update 單元中作為 MVCC 版本號。
  4. 創建一個 operation 對象,并將這個對象的值指針指向 update,并將這個 operation 加入到本次事務對象的 operation_array。
  5. 將 update 單元加入到 MVCC list 的鏈表頭上。
  6. 寫入一條 redo log 到本次事務對象的 redo_log_buf 當中。

示意圖如下:

圖4,點擊圖片可以全屏縮放

事務提交

WT 引擎對事務的提交過程比較簡單,先將要提交的事務對象中的 redo_log_buf 中的數據寫入到 redo log file(重做日志文件)中,并將 redo log file 持久化到磁盤上。清除提交事務對象的 snapshot object,再將提交的事務對象中的 transaction_id 設置為 WT_TNX_NONE,保證其他事務在創建系統事務 snapshot 時本次事務的狀態是已提交的狀態。

事務回滾

WT 引擎對事務的回滾過程也比較簡單,先遍歷整個operation_array,對每個數組單元對應 update 的事務 id 設置以為一個 WT_TXN_ABORTED(= uint64_max) ,標示 MVCC 對應的修改單元值被回滾,在其他讀事務進行 MVCC 讀操作的時候,跳過這個放棄的值即可。整個過程是一個無鎖操作,高效、簡潔。

WT 的事務隔離

傳統的數據庫事務隔離分為:

  • Read-Uncommited(未提交讀)
  • Read-Commited(提交讀)
  • Repeatable-Read(可重復讀)
  • Serializable(串行化)

WT 引擎并沒有按照傳統的事務隔離實現這四個等級,而是基于 snapshot 的特點實現了自己的 Read-Uncommited、Read-Commited 和一種叫做 snapshot-Isolation(快照隔離)的事務隔離方式。

在 WT 中不管是選用的是那種事務隔離方式,它都是基于系統中執行事務的快照來實現的。那來看看 WT 是怎么實現上面三種方式?

圖5,點擊圖片可以全屏縮放

Read-uncommited

Read-Uncommited(未提交讀)隔離方式的事務在讀取數據時總是讀取到系統中最新的修改,哪怕是這個修改事務還沒有提交一樣讀取,這其實就是一種臟讀。WT 引擎在實現這個隔方式時,就是將事務對象中的 snap_object.snap_array 置為空即可,在讀取 MVCC list 中的版本值時,總是讀取到 MVCC list 鏈表頭上的第一個版本數據。

舉例說明,在圖 5 中,如果 T0/T3/T5 的事務隔離級別設置成 Read-uncommited 的話,T1/T3/T5 在 T5 時刻之后讀取系統的值時,讀取到的都是 14。一般數據庫不會設置成這種隔離方式,它違反了事務的 ACID 特性。可能在一些注重性能且對臟讀不敏感的場景會采用,例如網頁 cache。

Read-Commited

Read-Commited(提交讀)隔離方式的事務在讀取數據時總是讀取到系統中最新提交的數據修改,這個修改事務一定是提交狀態。這種隔離級別可能在一個長事務多次讀取一個值的時候前后讀到的值可能不一樣,這就是經常提到的“幻象讀”。在 WT 引擎實現 read-commited 隔離方式就是事務在執行每個操作前都對系統中的事務做一次快照,然后在這個快照上做讀寫。

還是來看圖 5,T5 事務在 T4 事務提交之前它進行讀取前做事務

snapshot={

snap_min=T2,

snap_max=T4,

snap_array={T2,T4},

};

在讀取 MVCC list 時,12 和 14 修改對應的事務 T2/T4 都出現在 snap_array 中,只能再向前讀取 11,11 是 T1 的修改,而且 T1 沒有出現在 snap_array,說明 T1 已經提交,那么就返回 11 這個值給 T5。

之后事務 T2 提交,T5 在它提交之后再次讀取這個值,會再做一次

snapshot={

snap_min=T4,

snap_max=T4,

snap_array={T4},

},

這時在讀取 MVCC list 中的版本時,就會讀取到最新的提交修改 12。

Snapshot-Isolation

Snapshot-Isolation(快照隔離)隔離方式是讀事務開始時看到的最后提交的值版本修改,這個值在整個讀事務執行過程只會看到這個版本,不管這個值在這個讀事務執行過程被其他事務修改了幾次,這種隔離方式不會出現“幻象讀”。WT 在實現這個隔離方式很簡單,在事務開始時對系統中正在執行的事務做一個 snapshot,這個 snapshot 一直沿用到事務提交或者回滾。還是來看圖 5, T5 事務在開始時,對系統中的執行的寫事務做

snapshot={

snap_min=T2,

snap_max=T4,

snap_array={T2,T4}

},

在他讀取值時讀取到的是 11。即使是 T2 完成了提交,但 T5 的 snapshot 執行過程不會更新,T5 讀取到的依然是 11。

這種隔離方式的寫比較特殊,就是如果有對事務看不見的數據修改,事務嘗試修改這個數據時會失敗回滾,這樣做的目的是防止忽略不可見的數據修改。

通過上面對三種事務隔離方式的分析,WT 并沒有使用傳統的事務獨占鎖和共享訪問鎖來保證事務隔離,而是通過對系統中寫事務的 snapshot 來實現。這樣做的目的是在保證事務隔離的情況下又能提高系統事務并發的能力。

內存設計如何保證 Durability:事務日志

通過上面的分析可以知道 WT 在事務的修改都是在內存中完成的,事務提交時也不會將修改的 MVCC list 當中的數據刷入磁盤,WT 是怎么保證事務提交的結果永久保存呢?

WT 引擎在保證事務的持久可靠問題上是通過 redo log(重做操作日志)的方式來實現的,在本文的事務執行和事務提交階段都有提到寫操作日志。WT 的操作日志是一種基于 K/V 操作的邏輯日志,它的日志不是基于 btree page 的物理日志。說的通俗點就是將修改數據的動作記錄下來,例如:插入一個 key = 10, value = 20 的動作記錄在成:

{

Operation = insert,(動作)

Key = 10,

Value = 20

};

將動作記錄的數據以 append 追加的方式寫入到 wt_transaction 對象中 redo_log_buf 中,等到事務提交時將這個 redo_log_buf 中的數據已同步寫入的方式寫入到 WT 的重做日志的磁盤文件中。如果數據庫程序發生異常或者崩潰,可以通過上一個 checkpoint(檢查點)位置重演磁盤上這個磁盤文件來恢復已經提交的事務來保證事務的持久性。

如何通過操作日志實現 Durability?

根據上面的描述,有幾個問題需要搞清楚:

1、操作日志格式怎么設計?

2、在事務并發提交時,各個事務的日志是怎么寫入磁盤的?

3、日志是怎么重演的?它和 checkpoint 的關系是怎樣的?

在分析這三個問題前先來看 WT 是怎么管理重做日志文件的,在 WT 引擎中定義一個叫做 LSN 序號結構,操作日志對象是通過 LSN 來確定存儲的位置的,LSN 就是 Log Sequence Number(日志序列號),它在 WT 的定義是文件序號加文件偏移,

wt_lsn{

file:      文件序號,指定是在哪個日志文件中

offset:    文件內偏移位置,指定日志對象文件內的存儲文開始位置

}

WT 就是通過這個 LSN 來管理重做日志文件的。

日志格式設計

WT 引擎的操作日志對象(以下簡稱為 logrec)對應的是提交的事務,事務的每個操作被記錄成一個 logop 對象,一個 logrec 包含多個 logop,logrec 是一個通過精密序列化事務操作動作和參數得到的一個二進制 buffer,這個 buffer的數據是通過事務和操作類型來確定其格式的。

WT 中的日志分為 4 類,分別是:

  • 建立 checkpoint 的操作日志(LOGREC_CHECKPOINT)
  • 普通事務操作日志(LOGREC_COMMIT)
  • btree page 同步刷盤的操作日志(LOGREC_FILE_SYNC)
  • 提供給引擎外部使用的日志(LOGREC_MESSAGE)

這里介紹和執行事務密切先關的 LOGREC_COMMIT,這類日志里面由根據 K/V 的操作方式分為:

  • LOG_PUT(增加或者修改K/V操作)
  • LOG_REMOVE(單 KEY 刪除操作)
  • 范圍刪除日志

這幾種操作都會記錄操作時的 key,根據操作方式填寫不同的其他參數,例如:update 更新操作,就需要將 value 填上。除此之外,日志對象還會攜帶 btree 的索引文件 ID、提交事務的 ID 等,整個 logrec 和 logop 的關系結構圖如下:

圖6,點擊圖片可以全屏縮放

對于上圖中的 logrec essay-header 中的為什么會出現兩個長度字段:logrec 磁盤上的空間長度和在內存中的長度,因為 logrec 在刷入磁盤之前會進行空間壓縮,磁盤上的長度和內存中的長度就不一樣。壓縮是根據系統配置可選的。

WAL 與無鎖設計的日志寫并發

WT 引擎在采用 WAL(Write-Ahead Log)方式寫入日志,WAL 通俗點說就是說在事務所有修改提交前需要將其對應的操作日志寫入磁盤文件。在事務執行的介紹小節中我們介紹是在什么時候寫日志的,這里我們來分析事務日志是怎么寫入到磁盤上的,整個寫入過程大致分為下面幾個階段:

1、事務在執行第一個寫操作時,先會在事務對象(wt_transaction)中的 redo_log_buf 的緩沖區上創建一個 logrec 對象,并將 logrec 中的事務類型設置成 LOGREC_COMMIT。

2、然后在事務執行的每個寫操作前生成一個 logop 對象,并加入到事務對應的 logrec 中。

3、在事務提交時,把 logrec 對應的內容整體寫入到一個全局 log 對象的 slot buffer 中并等待寫完成信號。

4、Slot buffer 會根據并發情況合并同時發生的提交事務的 logrec,然后將合并的日志內容同步刷入磁盤(sync file),最后告訴這個 slot buffer 對應所有的事務提交刷盤完成。

5、提交事務的日志完成,事務的執行結果也完成了持久化。

整個過程的示意圖如下:

圖7,點擊圖片可以全屏縮放

WT 為了減少日志刷盤造成寫 IO,對日志刷盤操作做了大量的優化,實現一種類似 MySQL 組提交的刷盤方式。

這種刷盤方式會將同時發生提交的事務日志合并到一個 slot buffer 中,先完成合并的事務線程會同步等待一個完成刷盤信號,最后完成日志數據合并的事務線程將 slot buffer 中的所有日志數據 sync 到磁盤上并通知在這個 slot buffer 中等待其他事務線程刷盤完成。

并發事務的 logrec 合并到 slot buffer 中的過程是一個 完全無鎖 的過程,這減少了必要的 CPU 競爭和操作系統上下文切換。為了這個無鎖設計 WT 在全局的 log 管理中定義了一個 acitve_ready_slot 和一個 slot_pool 數組結構,大致如下定義:

wt_log{

. . .

active_slot:準備就緒且可以作為合并logrec的slot buffer對象

slot_pool:系統所有slot buffer對象數組,包括:正在合并的、準備合并和閑置的slot buffer。

}

slot buffer 對象是一個動態二進制數組,可以根據需要進行擴大。定義如下:

wt_log_slot{

. . .

state:          當前 slot 的狀態,ready/done/written/free 這幾個狀態

buf: 緩存合并 logrec 的臨時緩沖區

group_size: 需要提交的數據長度

slot_start_offset: 合并的logrec存入log file中的偏移位置

. . .

}

通過一個例子來說明這個無鎖過程,假如在系統中 slot_pool 中的 slot 個數為16,設置的 slot buffer 大小為 4KB,當前 log 管理器中的 active_slot 的 slot_start_offset=0 ,有 4 個事務(T1、T2、T3、T4)同時發生提交,他們對應的日志對象分別是 logrec1、logrec2、logrec3 和 logrec4。

Logrec1 size = 1KB,  logrec2 szie = 2KB, logrec3 size = 2KB, logrec4 size = 5KB 。他們合并和寫入的過程如下:

1、T1事 務在提交時,先會從全局的 log 對象中的 active_slot 發起一次 JOIN 操作,join 過程就是向 active_slot 申請自己的合并位置和空間, logrec1_size + slot_start_offset < slot_size 并且 slot 處于 ready 狀態,那 T1 事務的合并位置就是 active_slot[0, 1KB],slot_group_size = 1KB

2、這是 T2 同時發生提交也要合并 logrec,也重復第 1 部 JOIN 操作,它申請到的位置就是 active_slot [1KB, 3KB], slot_group_size = 3KB

3、在T1事務 JOIN 完成后,它會判斷自己是第一個 JOIN 這個 active_slot 的事務,判斷條件就是返回的寫入位置 slot_offset=0 。如果是第一個它立即會將 active_slot 的狀態從 ready 狀態置為 done 狀態,并未后續的事務從 slot_pool 中獲取一個空閑的 active_slot_new 來頂替自己合并數據的工作。

4、與此同時 T2 事務 JOIN 完成之后,它也是進行這個過程的判斷,T2 發現自己不是第一個,它將會等待 T1 將 active_slot 置為 done.

5、T1 和 T2 都獲取到了自己在 active_slot 中的寫入位置,active_slot 的狀態置為 done 時,T1 和 T2 分別將自己的 logrec 寫入到對應 buffer 位置。假如在這里 T1 比 T2 先將數據寫入完成,T1 就會等待一個 slot_buffer 完全刷入磁盤的信號,而 T2 寫入完成后會將 slot_buffer 中的數據寫入 log 文件,并對 log 文件做 sync 刷入磁盤的操作,最高發送信號告訴 T1 同步刷盤完成,T1 和 T2 各自返回,事務提交過程的日志刷盤操作完成。

那這里有幾種其他的情況,假如在第 2 步運行的完成后,T3 也進行 JOIN 操作,這個時候 slot_size(4KB) < slot_group_size(3KB)+ logrec_size(2KB) ,T3 不 JOIN 當時的 active_slot,而是自旋等待 active_slot_new 頂替 active_slot 后再 JOIN 到 active_slot_new。

如果在第 2 步時,T4 也提交,因為 logrec4(5KB) > slot_size(4KB) ,T4 就不會進行 JOIN 操作,而是直接將自己的 logrec 數據寫入 log 文件,并做 sync 刷盤返回。在返回前因為發現有 logrec4 大小的日志數據無法合并,全局 log 對象會試圖將 slot buffer 的大小放大兩倍,這樣做的目的是盡量讓下面的事務提交日志能進行 slot 合并寫。

WT 引擎之所以引入 slot 日志合并寫的原因就是為了 減少磁盤的 I/O 訪問 ,通過無鎖的操作,減少全局日志緩沖區的競爭。

事務恢復

從上面關于事務日志和 MVCC list 相關描述我們知道, 事務的 redo log 主要是防止內存中已經提交的事務修改丟失 ,但如果所有的修改都存在內存中,隨著時間和寫入的數據越來越多,內存就會不夠用,這個時候就需要將內存中的修改數據寫入到磁盤上。

一般在 WT 中是將整個 BTREE 上的 page 做一次 checkpoint 并寫入磁盤。WT 中的 checkpoint 是 append 方式管理,也就是說 WT 會保存多個 checkpoint 版本。不管從哪個版本的 checkpoint 開始都可以通過重演 redo log 來恢復內存中已提交的事務修改。整個重演過程就是就是簡單的對 logrec 中各個操作的執行。

這里值得提一下的是因為 WT 保存多個版本的 checkpoint,那么它會將 checkpoint 做為一種元數據寫入到元數據表中,元數據表也會有自己的 checkpoint 和 redo log,但是保存元數據表的 checkpoint 是保存在 WiredTiger.wt 文件中,系統重演普通表的提交事務之前,先會重演元數據事務提交修改。后文會單獨用一個篇幅來說明 btree、checkpoint 和元數據表的關系和實現。

WT 的 redo log 是通過配置開啟或者關閉的,MongoDB 并沒有使用 WT 的 redo log 來保證事務修改不丟,而是采用了 WT 的 checkpoint 和 MongoDB 復制集的功能結合來保證數據的完整性。

大致的細節是如果某個 MongoDB 實例宕機了,重啟后通過 MongoDB 的復制協議將自己最新 checkpoint 后面的修改從其他的 MongoDB 實例復制過來。

后記

雖然 WT 實現了多操作事務模型,然而 MongoDB 并沒有提供事務,這或許和 MongoDB 本身的架構和產品定位有關系。但是 MongoDB 利用了 WT 的短事務的隔離性實現了文檔級行鎖 ,對 MongoDB 來說這是大大的進步。

可以說 WT 在事務的實現上另辟蹊徑,整個事務系統的實現沒有用繁雜的事務鎖,而是 使用 snapshot 和 MVCC 這兩個技術輕松的而實現了事務的 ACID ,這種實現也大大提高了事務執行的并發性。

除此之外,WT 在各個事務模塊的實現多采用無鎖并發 ,充分利用 CPU 的多核能力來減少資源競爭和 I/O 操作,可以說 WT 在實現上是有很大創新的。通過對 WiredTiger 的源碼分析和測試,也讓我獲益良多,不僅僅了解了數據庫存儲引擎的最新技術,也對 CPU 和內存相關的并發編程有了新的理解,很多的設計模式和并發程序架構可以直接借鑒到現實中的項目和產品中。

后續的工作是繼續對 Wiredtiger 做更深入的分析、研究和測試,并把這些工作的心得體會分享出來,讓更多的工程師和開發者了解這個優秀的存儲引擎。(小編:請留意高可用架構后續 WiredTiger 文章)

 

來自: http://h2ex.com/1120

 

 本文由用戶 fzgd8551 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!