摘要:?云數(shù)據(jù)庫 MongoDB 版 基于飛天分布式系統(tǒng)和高性能存儲,提供三節(jié)點副本集的高可用架構,容災切換,故障遷移完全透明化。
基于飛天分布式系統(tǒng)和高性能存儲,提供三節(jié)點副本集的高可用架構,容災切換,故障遷移完全透明化。并提供專業(yè)的數(shù)據(jù)庫在線擴容、備份回滾、性能優(yōu)化等解決方案。
上個月底 MongoDB Wolrd 宣布發(fā)布 MongoDB 4.0, 支持復制集多文檔事務,阿里云數(shù)據(jù)庫團隊?研發(fā)工程師第一時間對事務功能的時間進行了源碼分析,解析事務實現(xiàn)機制。
MongoDB 4.0 引入的事務功能,支持多文檔ACID特性,例如使用?mongo shell?進行事務操作
>?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 也封裝了事務相關接口,用戶需要創(chuàng)建一個?Session,然后在?Session?上開啟事務,提交事務。例如
python 版本
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 版本引入的概念,引入這個特性主要就是為實現(xiàn)多文檔事務做準備。Session?本質上就是一個「上下文」。
在以前的版本,MongoDB 只管理單個操作的上下文,mongod?服務進程接收到一個請求,為該請求創(chuàng)建一個上下文 (源碼里對應?OperationContext),然后在服務整個請求的過程中一直使用這個上下文,內容包括,請求耗時統(tǒng)計、請求占用的鎖資源、請求使用的存儲快照等信息。有了?Session之后,就可以讓多個請求共享一個上下文,讓多個請求產生關聯(lián),從而有能力支持多文檔事務。
每個?Session?包含一個唯一的標識 lsid,在 4.0 版本里,用戶的每個請求可以指定額外的擴展字段,主要包括:
lsid: 請求所在 Session 的 ID, 也稱 logic session id
txnNmuber: 請求對應的事務號,事務號在一個 Session 內必須單調遞增
stmtIds: 對應請求里每個操作(以insert為例,一個insert命令可以插入多個文檔)操作ID
實際上,用戶在使用事務時,是不需要理解這些細節(jié),MongoDB Driver 會自動處理,Driver 在創(chuàng)建?Session?時分配 lsid,接下來這個?Session?里的所以操作,Driver 會自動為這些操作加上 lsid,如果是事務操作,會自動帶上 txnNumber。
值得一提的是,Session?lsid 可以通過調用?startSession?命令讓 server 端分配,也可以客戶端自己分配,這樣可以節(jié)省一次網(wǎng)絡開銷;而事務的標識,MongoDB 并沒有提供一個單獨的?startTransaction的命令,txnNumber 都是直接由 Driver 來分配的,Driver 只需保證一個 Session 內,txnNumber 是遞增的,server 端收到新的事務請求時,會主動的開始一個新事務。
MongoDB 在?startSession?時,可以指定一系列的選項,用于控制?Session?的訪問行為,主要包括:
causalConsistency: 是否提供?causal consistency?的語義,如果設置為true,不論從哪個節(jié)點讀取,MongoDB 會保證 "read your own write" 的語義。參考?causal consistency
readConcern:參考?MongoDB readConcern 原理解析
writeConcern:參考?MongoDB writeConcern 原理解析
readPreference: 設置讀取時選取節(jié)點的規(guī)則,參考?read preference
retryWrites:如果設置為true,在復制集場景下,MongoDB 會自動重試發(fā)生重新選舉的場景; 參考retryable write
ACID
Atomic
針對多文檔的事務操作,MongoDB 提供 "All or nothing" 的原子語義保證。
Consistency
太難解釋了,還有拋棄 Consistency 特性的數(shù)據(jù)庫?
Isolation
MongoDB 提供 snapshot 隔離級別,在事務開始創(chuàng)建一個 WiredTiger snapshot,然后在整個事務過程中使用這個快照提供事務讀。
Durability
事務使用 WriteConcern?{j: ture}?時,MongoDB 一定會保證事務日志提交才返回,即使發(fā)生 crash,MongoDB 也能根據(jù)事務日志來恢復;而如果沒有指定?{j: true}?級別,即使事務提交成功了,在 crash recovery 之后,事務的也可能被回滾掉。
事務與復制
復制集配置下,MongoDB 整個事務在提交時,會記錄一條 oplog(oplog 是一個普通的文檔,所以目前版本里事務的修改加起來不能超過文檔大小 16MB的限制),包含事務里所有的操作,備節(jié)點拉取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 集合,根據(jù) oplog 條數(shù),如果數(shù)量較多,會并發(fā)寫入加速
清理?OplogTruncateAfterPoint, 標識 oplog 完全成功寫入;如果在本步驟完成前 crash,重啟恢復時,發(fā)現(xiàn)?oplogTruncateAfterPoint?被設置,會將 oplog 截短到該時間戳,以恢復到一致的狀態(tài)點。
將 oplog 劃分到到多個線程并發(fā)重放,為了提升并發(fā)效率,事務產生的 oplog 包含的所有修改操作,跟一條普通單條操作的 oplog 一樣,會據(jù)文檔ID劃分到多個線程。
更新?ApplyThrough?時間戳為 Batch 里最后一條 oplog 時間戳,標識下一次重啟后,從該位置重新同步,如果本步驟之前失敗,重啟恢復時,會從?ApplyThrough?上一次的值(上一個 Batch 最后一條 oplog)拉取 oplog。
更新 oplog 可見時間戳,如果有其他節(jié)點從該備節(jié)點同步,此時就能讀到這部分新寫入的 oplog
更新本地 Snapshot(時間戳),新的寫入將對用戶可見。
事務與存儲引擎
事務時序統(tǒng)一
WiredTiger 很早就支持事務,在 3.x 版本里,MongoDB 就通過 WiredTiger 事務,來保證一條修改操作,對數(shù)據(jù)、索引、oplog 三者修改的原子性。但實際上 MongoDB 經(jīng)過多個版本的迭代,才提供了事務接口,核心難點就是時序問題。
MongoDB 通過 oplog 時間戳來標識全局順序,而 WiredTiger 通過內部的事務ID來標識全局順序,在實現(xiàn)上,2者沒有任何關聯(lián)。這就導致在并發(fā)情況下, MongoDB 看到的事務提交順序與 WiredTiger 看到的事務提交順序不一致。
為解決這個問題,WiredTier 3.0?引入事務時間戳(transaction timestamp)機制,應用程序可以通過?WT_SESSION::timestamp_transaction?接口顯式的給 WiredTiger 事務分配 commit timestmap,然后就可以實現(xiàn)指定時間戳讀(read "as of" a timestamp)。有了?read "as of" a timestamp?特性后,在重放 oplog 時,備節(jié)點上的讀就不會再跟重放 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}
從上面的代碼可以看到,再引入事務時間戳之后,在可見性判斷時,還會額外檢查時間戳,上層讀取時指定了時間戳讀,則只能看到該時間戳以前的數(shù)據(jù)。而 MongoDB 在提交事務時,會將 oplog 時間戳跟事務關聯(lián),從而達到 MongoDB Server 層時序與 WiredTiger 層時序一致的目的。
事務對 cache 的影響
WiredTiger(WT) 事務會打開一個快照,而快照的存在的 WiredTiger cache evict 是有影響的。一個 WT page 上,有N個版本的修改,如果這些修改沒有全局可見(參考?__wt_txn_visible_all),這個 page 是不能 evict 的(參考?__wt_page_can_evict)。
在 3.x 版本里,一個寫請求對數(shù)據(jù)、索引、oplog的修改會放到一個 WT 事務里,事務的提交由 MongoDB 自己控制,MongoDB 會盡可能快的提交事務,完成寫清求;但 4.0 引入事務之后,事務的提交由應用程序控制,可能出現(xiàn)一個事務修改很多,并且很長時間不提交,這會給 WT cache evict 造成很大的影響,如果大量內存無法 evict,最終就會進入 cache stuck 狀態(tài)。
為了盡量減小 WT cache 壓力,MongoDB 4.0 事務功能有一些限制,但事務資源占用超過一定閾值時,會自動 abort 來釋放資源。規(guī)則包括
事務的生命周期不能超過?transactionLifetimeLimitSeconds?(默認60s),該配置可在線修改
事務修改的文檔數(shù)不能超過 1000 ,不可修改
事務修改產生的 oplog 不能超過 16mb,這個主要是 MongoDB 文檔大小的限制, oplog 也是一個普通的文檔,也必須遵守這個約束。
Read as of a timestamp 與 oldest timestamp
Read as of a timestamp?依賴 WiredTiger 在內存里維護多版本,每個版本跟一個時間戳關聯(lián),只要 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 層面完成,但節(jié)點需要回滾時,會根據(jù)要回滾的 oplog 不斷應用相反的操作,或從回滾源上讀取最新的版本,整個回滾操作效率很低。
4.0 版本實現(xiàn)了存儲引擎層的回滾機制,當復制集節(jié)點需要回滾時,直接調用 WiredTiger 接口,將數(shù)據(jù)回滾到某個穩(wěn)定版本(實際上就是一個 Checkpoint),這個穩(wěn)定版本則依賴于?stable timestamp。WiredTiger 會確保?stable timestamp?之后的數(shù)據(jù)不會寫到 Checkpoint里,MongoDB 根據(jù)復制集的同步狀態(tài),當數(shù)據(jù)已經(jīng)同步到大多數(shù)節(jié)點時(Majority commited),會更新?stable timestamp,因為這些數(shù)據(jù)已經(jīng)提交到大多數(shù)節(jié)點了,一定不會發(fā)生 ROLLBACK,這個時間戳之前的數(shù)據(jù)就都可以寫到 Checkpoint 里了。
MongoDB 需要確保頻繁(及時)的更新?stable timestamp,否則影響 WT Checkpoint 行為,導致很多內存無法釋放。
分布式事務
MongoDB 4.0 支持副本集多文檔事務,并計劃在 4.2 版本支持分片集群事務功能。下圖是從 MongoDB 3.0 引入 WiredTiger 到 4.0 支持多文檔事務的功能迭代圖,可以發(fā)現(xiàn)一盤大棋即將上線,敬請期待。
基于飛天分布式系統(tǒng)和高性能存儲,提供三節(jié)點副本集的高可用架構,容災切換,故障遷移完全透明化。并提供專業(yè)的數(shù)據(jù)庫在線擴容、備份回滾、性能優(yōu)化等解決方案。
本文為云棲社區(qū)原創(chuàng)內容,未經(jīng)允許不得轉載
評論
查看更多