MongoDB 4.0 事務實現解析

摘要: 雲數據庫 MongoDB 版 基於飛天分佈式系統和高性能存儲,提供三節點副本集的高可用架構,容災切換,故障遷移徹底透明化。java

雲數據庫 MongoDB 版
基於飛天分佈式系統和高性能存儲,提供三節點副本集的高可用架構,容災切換,故障遷移徹底透明化。並提供專業的數據庫在線擴容、備份回滾、性能優化等解決方案。
瞭解更多
上個月底 MongoDB Wolrd 宣佈發佈 MongoDB 4.0, 支持複製集多文檔事務,阿里雲數據庫團隊 研發工程師第一時間對事務功能的時間進行了源碼分析,解析事務實現機制。python

MongoDB 4.0 引入的事務功能,支持多文檔ACID特性,例如使用 mongo shell 進行事務操做mongodb

s = db.getMongo().startSession()
session { "id" : UUID("3bf55e90-5e88-44aa-a59e-a30f777f1d89") }
s.startTransaction()
db.coll01.insert({x: 1, y: 1})
WriteResult({ "nInserted" : 1 })
db.coll02.insert({x: 1, y: 1})
WriteResult({ "nInserted" : 1 })
s.commitTransaction() (或者 s.abortTransaction()回滾事務)
支持 MongoDB 4.0 的其餘語言 Driver 也封裝了事務相關接口,用戶須要建立一個 Session,而後在 Session 上開啓事務,提交事務。例如

python 版本shell

with client.start_session() as s:
    s.start_transaction()
    collection_one.insert_one(doc_one, session=s)
    collection_two.insert_one(doc_two, session=s)
    s.commit_transaction()

java 版本數據庫

try (ClientSession clientSession = client.startSession()) {
   clientSession.startTransaction();
   collection.insertOne(clientSession, docOne);
   collection.insertOne(clientSession, docTwo);
   clientSession.commitTransaction();
}

Session性能優化

Session 是 MongoDB 3.6 版本引入的概念,引入這個特性主要就是爲實現多文檔事務作準備。Session 本質上就是一個「上下文」。網絡

在之前的版本,MongoDB 只管理單個操做的上下文,mongod 服務進程接收到一個請求,爲該請求建立一個上下文 (源碼裏對應 OperationContext),而後在服務整個請求的過程當中一直使用這個上下文,內容包括,請求耗時統計、請求佔用的鎖資源、請求使用的存儲快照等信息。有了 Session 以後,就可讓多個請求共享一個上下文,讓多個請求產生關聯,從而有能力支持多文檔事務。session

每一個 Session 包含一個惟一的標識 lsid,在 4.0 版本里,用戶的每一個請求能夠指定額外的擴展字段,主要包括:架構

lsid: 請求所在 Session 的 ID, 也稱 logic session id
txnNmuber: 請求對應的事務號,事務號在一個 Session 內必須單調遞增
stmtIds: 對應請求裏每一個操做(以insert爲例,一個insert命令能夠插入多個文檔)操做ID
實際上,用戶在使用事務時,是不須要理解這些細節,MongoDB Driver 會自動處理,Driver 在建立 Session 時分配 lsid,接下來這個 Session 裏的因此操做,Driver 會自動爲這些操做加上 lsid,若是是事務操做,會自動帶上 txnNumber。併發

值得一提的是,Session lsid 能夠經過調用 startSession 命令讓 server 端分配,也能夠客戶端本身分配,這樣能夠節省一次網絡開銷;而事務的標識,MongoDB 並無提供一個單獨的 startTransaction的命令,txnNumber 都是直接由 Driver 來分配的,Driver 只需保證一個 Session 內,txnNumber 是遞增的,server 端收到新的事務請求時,會主動的開始一個新事務。

MongoDB 在 startSession 時,能夠指定一系列的選項,用於控制 Session 的訪問行爲,主要包括:

causalConsistency: 是否提供 causal consistency 的語義,若是設置爲true,不論從哪一個節點讀取,MongoDB 會保證 "read your own write" 的語義。參考 causal consistency
readConcern:參考 MongoDB readConcern 原理解析
writeConcern:參考 MongoDB writeConcern 原理解析
readPreference: 設置讀取時選取節點的規則,參考 read preference
retryWrites:若是設置爲true,在複製集場景下,MongoDB 會自動重試發生從新選舉的場景; 參考retryable write

ACID

Atomic
針對多文檔的事務操做,MongoDB 提供 "All or nothing" 的原子語義保證。

Consistency
太難解釋了,還有拋棄 Consistency 特性的數據庫?

Isolation
MongoDB 提供 snapshot 隔離級別,在事務開始建立一個 WiredTiger snapshot,而後在整個事務過程當中使用這個快照提供事務讀。

Durability
事務使用 WriteConcern {j: ture} 時,MongoDB 必定會保證事務日誌提交才返回,即便發生 crash,MongoDB 也能根據事務日誌來恢復;而若是沒有指定 {j: true} 級別,即便事務提交成功了,在 crash recovery 以後,事務的也可能被回滾掉。

事務與複製
複製集配置下,MongoDB 整個事務在提交時,會記錄一條 oplog(oplog 是一個普通的文檔,因此目前版本里事務的修改加起來不能超過文檔大小 16MB的限制),包含事務裏全部的操做,備節點拉取oplog,並在本地重放事務操做。

事務 oplog 示例,包含事務操做的 lsid,txnNumber,以及事務內全部的操做日誌(applyOps字段)

"ts" : Timestamp(1530696933, 1), "t" : NumberLong(1), "h" : NumberLong("4217817601701821530"), "v" : 2, "op" : "c", "ns" : "admin.$cmd", "wall" : ISODate("2018-07-04T09:35:33.549Z"), "lsid" : { "id" : UUID("e675c046-d70b-44c2-ad8d-3f34f2019a7e"), "uid" : BinData(0,"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=") }, "txnNumber" : NumberLong(0), "stmtId" : 0, "prevOpTime" : { "ts" : Timestamp(0, 0), "t" : NumberLong(-1) }, "o" : { "applyOps" : [ { "op" : "i", "ns" : "test.coll2", "ui" : UUID("a49ccd80-6cfc-4896-9740-c5bff41e7cce"), "o" : { "_id" : ObjectId("5b3c94d4624d615ede6097ae"), "x" : 20000 } }, { "op" : "i", "ns" : "test.coll3", "ui" : UUID("31d7ae62-fe78-44f5-ba06-595ae3b871fc"), "o" : { "_id" : ObjectId("5b3c94d9624d615ede6097af"), "x" : 20000 } } ] } }

整個重放過程以下:

獲取當前 Batch (後臺不斷拉取 oplog 放入 Batch)
設置 OplogTruncateAfterPoint 時間戳爲 Batch裏第一條 oplog 時間戳 (存儲在 local.replset.oplogTruncateAfterPoint 集合)
寫入 Batch 裏全部的 oplog 到 local.oplog.rs 集合,根據 oplog 條數,若是數量較多,會併發寫入加速
清理 OplogTruncateAfterPoint, 標識 oplog 徹底成功寫入;若是在本步驟完成前 crash,重啓恢復時,發現 oplogTruncateAfterPoint 被設置,會將 oplog 截短到該時間戳,以恢復到一致的狀態點。
將 oplog 劃分到到多個線程併發重放,爲了提高併發效率,事務產生的 oplog 包含的全部修改操做,跟一條普通單條操做的 oplog 同樣,會據文檔ID劃分到多個線程。
更新 ApplyThrough 時間戳爲 Batch 裏最後一條 oplog 時間戳,標識下一次重啓後,從該位置從新同步,若是本步驟以前失敗,重啓恢復時,會從 ApplyThrough 上一次的值(上一個 Batch 最後一條 oplog)拉取 oplog。
更新 oplog 可見時間戳,若是有其餘節點從該備節點同步,此時就能讀到這部分新寫入的 oplog
更新本地 Snapshot(時間戳),新的寫入將對用戶可見。

事務與存儲引擎

事務時序統一
WiredTiger 很早就支持事務,在 3.x 版本里,MongoDB 就經過 WiredTiger 事務,來保證一條修改操做,對數據、索引、oplog 三者修改的原子性。但實際上 MongoDB 通過多個版本的迭代,才提供了事務接口,核心難點就是時序問題。

MongoDB 經過 oplog 時間戳來標識全局順序,而 WiredTiger 經過內部的事務ID來標識全局順序,在實現上,2者沒有任何關聯。這就致使在併發狀況下, MongoDB 看到的事務提交順序與 WiredTiger 看到的事務提交順序不一致。

爲解決這個問題,WiredTier 3.0 引入事務時間戳(transaction timestamp)機制,應用程序能夠經過 WT_SESSION::timestamp_transaction 接口顯式的給 WiredTiger 事務分配 commit timestmap,而後就能夠實現指定時間戳讀(read "as of" a timestamp)。有了 read "as of" a timestamp 特性後,在重放 oplog 時,備節點上的讀就不會再跟重放 oplog 有衝突了,不會因重放 oplog 而阻塞讀請求,這是4.0版本一個巨大的提高。

/*

  • __wt_txn_visible --
  • Can the current transaction see the given ID / timestamp?

*/
static inline bool
__wt_txn_visible(

WT_SESSION_IMPL *session, uint64_t id, const wt_timestamp_t *timestamp)

{

if (!__txn_visible_id(session, id))
    return (false);

/* Transactions read their writes, regardless of timestamps. */
if (F_ISSET(&session->txn, WT_TXN_HAS_ID) && id == session->txn.id)
    return (true);

ifdef HAVE_TIMESTAMPS

{
WT_TXN *txn = &session->txn;

/* Timestamp check. */
if (!F_ISSET(txn, WT_TXN_HAS_TS_READ) || timestamp == NULL)
    return (true);

return (__wt_timestamp_cmp(timestamp, &txn->read_timestamp) <= 0);
}

else

WT_UNUSED(timestamp);
return (true);

endif

}
從上面的代碼能夠看到,再引入事務時間戳以後,在可見性判斷時,還會額外檢查時間戳,上層讀取時指定了時間戳讀,則只能看到該時間戳之前的數據。而 MongoDB 在提交事務時,會將 oplog 時間戳跟事務關聯,從而達到 MongoDB Server 層時序與 WiredTiger 層時序一致的目的。

事務對 cache 的影響

WiredTiger(WT) 事務會打開一個快照,而快照的存在的 WiredTiger cache evict 是有影響的。一個 WT page 上,有N個版本的修改,若是這些修改沒有全局可見(參考 __wt_txn_visible_all),這個 page 是不能 evict 的(參考 __wt_page_can_evict)。

在 3.x 版本里,一個寫請求對數據、索引、oplog的修改會放到一個 WT 事務裏,事務的提交由 MongoDB 本身控制,MongoDB 會盡量快的提交事務,完成寫清求;但 4.0 引入事務以後,事務的提交由應用程序控制,可能出現一個事務修改不少,而且很長時間不提交,這會給 WT cache evict 形成很大的影響,若是大量內存沒法 evict,最終就會進入 cache stuck 狀態。

爲了儘可能減少 WT cache 壓力,MongoDB 4.0 事務功能有一些限制,但事務資源佔用超過必定閾值時,會自動 abort 來釋放資源。規則包括

事務的生命週期不能超過 transactionLifetimeLimitSeconds (默認60s),該配置可在線修改
事務修改的文檔數不能超過 1000 ,不可修改
事務修改產生的 oplog 不能超過 16mb,這個主要是 MongoDB 文檔大小的限制, oplog 也是一個普通的文檔,也必須遵照這個約束。

Read as of a timestamp 與 oldest timestamp
Read as of a timestamp 依賴 WiredTiger 在內存裏維護多版本,每一個版本跟一個時間戳關聯,只要 MongoDB 層可能須要讀的版本,引擎層就必須維護這個版本的資源,若是保留的版本太多,也會對 WT cache 產生很大的壓力。

WiredTiger 提供設置 oldest timestamp 的功能,容許由 MongoDB 來設置該時間戳,含義是Read as of a timestamp 不會提供更小的時間戳來進行一致性讀,也就是說,WiredTiger 無需維護 oldest timestamp 以前的全部歷史版本。MongoDB 層須要頻繁(及時)更新 oldest timestamp,避免讓 WT cache 壓力太大。

引擎層 Rollback 與 stable timestamp
在 3.x 版本里,MongoDB 複製集的回滾動做是在 Server 層面完成,但節點須要回滾時,會根據要回滾的 oplog 不斷應用相反的操做,或從回滾源上讀取最新的版本,整個回滾操做效率很低。

4.0 版本實現了存儲引擎層的回滾機制,當複製集節點須要回滾時,直接調用 WiredTiger 接口,將數據回滾到某個穩定版本(實際上就是一個 Checkpoint),這個穩定版本則依賴於 stable timestamp。WiredTiger 會確保 stable timestamp 以後的數據不會寫到 Checkpoint裏,MongoDB 根據複製集的同步狀態,當數據已經同步到大多數節點時(Majority commited),會更新 stable timestamp,由於這些數據已經提交到大多數節點了,必定不會發生 ROLLBACK,這個時間戳以前的數據就均可以寫到 Checkpoint 裏了。

MongoDB 須要確保頻繁(及時)的更新 stable timestamp,不然影響 WT Checkpoint 行爲,致使不少內存沒法釋放。

分佈式事務
MongoDB 4.0 支持副本集多文檔事務,並計劃在 4.2 版本支持分片集羣事務功能。下圖是從 MongoDB 3.0 引入 WiredTiger 到 4.0 支持多文檔事務的功能迭代圖,能夠發現一盤大棋即將上線,敬請期待。

圖片描述

雲數據庫 MongoDB 版
基於飛天分佈式系統和高性能存儲,提供三節點副本集的高可用架構,容災切換,故障遷移徹底透明化。並提供專業的數據庫在線擴容、備份回滾、性能優化等解決方案。
瞭解更多

原文連接本文爲雲棲社區原創內容,未經容許不得轉載

相關文章
相關標籤/搜索