鵝廠程序員爆肝整理,萬字長文講透MongoDB中的鎖

?? 導讀
MongoDB 作為世界領(lǐng)先的文檔型數(shù)據(jù)庫廣受開發(fā)者的喜愛,而 MongoDB 中的鎖又為數(shù)據(jù)庫高并發(fā)的讀寫提供了保障。本文從 MongoDB 的慢日志引入 MongoDB 中的鎖,通過介紹 MongoDB 中的資源分類、鎖分類、鎖結(jié)構(gòu)、鎖實現(xiàn)以及鎖的使用情況與查詢方法,深入淺出地介紹 MongoDB 中鎖的相關(guān)技術(shù)。 長文干貨,建議先點贊收藏再細細閱讀~?? 目錄
1 引子:從 MongoDB 的慢日志引入 2 MongoDB 的鎖與資源分類 3 MongoDB 的鎖矩陣 4 淺入:MongoDB 的鎖實現(xiàn)01
引子:從 MongoDB 的慢日志引入
在我們?nèi)粘5臄?shù)據(jù)庫使用中,經(jīng)常會與慢日志打交道。而在使用 MongoDB 時,慢日志也常作為 MongoDB 讀寫性能的衡量標志之一。筆者剛?cè)肼殨r,自己也剛開始接觸數(shù)據(jù)庫中的慢日志概念,不經(jīng)會發(fā)出疑問:
什么是慢日志?
慢日志的全稱為“Slow Query Log”,正如其英文直譯,代表著對返回較慢的查詢請求的日志記錄,最開始是 MySQL 中對執(zhí)行較慢查詢的統(tǒng)計,主要用于記錄 MySQL 中執(zhí)行時間超過指定時間的 SQL 語句;通過查詢慢日志,我們可以查找出哪些語句的執(zhí)行效率較低,并進行針對性的查詢優(yōu)化。
MongoDB 中慢日志的概念與 MySQL 中的相同,一般將執(zhí)行時間大于 100ms 的請求稱為慢請求,內(nèi)核會在執(zhí)行時統(tǒng)計請求的執(zhí)行時間,并記錄下執(zhí)行時間大于 100ms 的請求相關(guān)信息,打印至內(nèi)核運行日志中,記錄為慢日志。通過查看 MongoDB 的慢日志,我們可以獲得對應(yīng)請求與內(nèi)核當前運行狀態(tài)的諸多信息,并可以依次為依據(jù)做出對應(yīng)的優(yōu)化策略。
MongoDB 也可以通過多種方式采集、記錄慢請求的相關(guān)信息。
如在 MongoDB 中,可以通過以下語句設(shè)定 Database Profiler 用于過濾、采集請求,用于慢操作的分析。
# 查看Databaseprofiler配置db.getProfilingStatus()
# 設(shè)置Databaseprofiler用于采集慢請求db.setProfilingLevel(<level>, <options>)
其中 level 代表 profiling 的等級,有如下三個等級:
-
0:不開啟 profiler(默認不開啟);
-
1:開啟 profiler 采集慢請求(默認采集 100ms 以上);
-
2:開啟 profiler 采集所有的操作。
options 則包含以下選項:
-
slowMs:慢請求判斷毫秒數(shù),只有大于 slowMs 的請求才會被 profiler 標記并記錄為慢請求,默認 100ms;
-
sampleRate:采集慢操作的采樣率;
-
filter:采樣的過濾規(guī)則。
在設(shè)置 Profiler 后,滿足條件的慢請求將會被記錄在 system.profile 表中,該表為一個 capped collection,可以通過 db.system.profile.find() 來過濾與查詢慢請求的記錄,舉個例子:
>db.system.profile.find().pretty(){"op" : "query", # 操作類型,可為command、count、distinct、geoNear、getMore、group、insert、mapReduce、query、remove、update"ns" : "test.report", # 操作的目標namespace庫表"command" : { # 操作的具體command"find" : "report",......},"cursorid" : 33629063128, # query與getMore使用的cursor id"keysExamined" : 101, # 為執(zhí)行操作掃描的索引鍵數(shù)量"docsExamined" : 101, # 為執(zhí)行操作掃描的文檔數(shù)據(jù)"fromMultiPlanner" : true,"numYield" : 2, # 執(zhí)行操作時讓其他操作完成的次數(shù)"nreturned" : 101, # 返回的文檔數(shù)量"queryHash" : "811451DD", # 查詢的hash"planCacheKey" : "759981BA", # 查詢計劃的key"locks" : { # 鎖信息"Global" : { # 全局鎖"acquireCount" : {"r" : NumberLong(3),"w" : NumberLong(3)}},"Database" : { # Database鎖"acquireCount" : { "r" : NumberLong(3) },"acquireWaitCount" : { "r" : NumberLong(1) },"timeAcquiringMicros" : { "r" : NumberLong(69130694) }},"Collection" : { # Collection鎖"acquireCount" : { "r" : NumberLong(3) }}},"storage" : { # 存儲情況"data" : {"bytesRead" : NumberLong(14736), # 從磁盤中讀取的數(shù)據(jù)大小"timeReadingMicros" : NumberLong(17) # 從磁盤讀取數(shù)據(jù)的耗時}},"responseLength" : 1305014, # response的大小"protocol" : "op_msg", # 消息的協(xié)議"millis" : 69132, # 執(zhí)行時間,milliseconds"planSummary" : "IXSCAN { a: 1, _id: -1 }", # 執(zhí)行計劃,這里代表走索引查詢"execStats" : { # 操作執(zhí)行的詳細步驟信息"stage" : "FETCH", # 操作類型,如COLLSCAN、IXSCAN、FETCH"nReturned" : 101, # 返回文檔數(shù)......},"ts" : ISODate("2019-01-14T16:57:33.450Z"), # 時間戳"client" : "127.0.0.1", # 客戶端信息"appName" : "MongoDB Shell", # appName"allUsers" : [{"user" : "someuser","db" : "admin"}],"user" : "someuser@admin" # 執(zhí)行的用戶信息}
上述信息記錄了慢操作的詳細的一些信息,可方便的用于慢操作的查詢與分析,如以下幾點:
-
command:記錄的請求的內(nèi)容,可以幫助我們分析慢請求的原因;
-
locks:請求與鎖的相關(guān)信息,執(zhí)行請求需要獲取的鎖,以及鎖排隊等待、獲取時長等信息;
-
writeConflicts(只有寫請求):寫沖突,在同時寫一個文檔時即會造成些沖突;
-
millis:慢請求最終執(zhí)行時長;
-
planSummary:如執(zhí)行 COLLSCAN 全表掃就會比執(zhí)行 IXSCAN 索引話費更多的資源與時間;
-
execStats:執(zhí)行計劃的具體執(zhí)行情況,方便得知請求的執(zhí)行全貌。
從以上幾個方面分析,可以大致得知請求的執(zhí)行情況,用于分析慢請求產(chǎn)生的原因。一般而言,慢請求的產(chǎn)生無非以下幾點原因:
-
CPU負載高:如頻繁的認證/建鏈接會使大量CPU消耗,導致請求執(zhí)行慢;
-
等鎖/鎖沖突:一些請求需要獲取鎖,而如果有其他請求拿到鎖未釋放,則會導致請求執(zhí)行慢;
-
全表掃描:查詢未走索引,導致全表掃描,會導致請求執(zhí)行慢;
-
內(nèi)存排序:與上述情況類似,未走索引的情況下內(nèi)存排序?qū)е抡埱髨?zhí)行慢;
-
但開啟分析器 Profiler 是需要一些代價的(如影響內(nèi)核性能),且一般來說默認關(guān)閉,故在處理線上問題時,我們往往只能拿到內(nèi)核日志中記錄的慢日志信息。
一條典型的 MongoDB 慢日志舉例如下:
{"t": { # 時間戳"$date": "2020-05-20T20:10:08.731+00:00"},"s": "I", # 日志等級,分為F、E、W、I,分別為Fatal、Error、Warning、Info"c": "COMMAND", # 執(zhí)行請求類型,分為ACCESS、COMMAND、CONTROL、ELECTION、FTDC、GEO、INDEX等"id": 51803, # 慢日志的日志id"ctx": "conn281", # 鏈接的信息"msg": "Slow query", # 慢日志信息,代表是慢請求"attr": { # 參數(shù)詳情"type": "command", # 請求類型"ns": "stocks.trades", # Namespace,請求的庫表"appName": "MongoDB Shell", # client的app name"command": { # 請求詳情"aggregate": "trades", # 請求為aggregate,且在trades表上執(zhí)行......},"planSummary": "COLLSCAN", # 請求執(zhí)行了COLLSCAN"cursorid": 1912190691485054700,"keysExamined": 0,"docsExamined": 1000001, # 查詢的文檔數(shù),由于是COLLSCAN所以查詢較多"hasSortStage": true,"usedDisk": true,"numYields": 1002, # 查詢讓渡其他請求執(zhí)行數(shù)"nreturned": 101, # 返回的文檔數(shù),遠遠小于docsExamined,全表掃消耗了大量時間"reslen": 17738,"locks": { # 鎖信息"ReplicationStateTransition": {"acquireCount": {"w": 1119}},"Global": {"acquireCount": {"r": 1119}},"Database": {"acquireCount": {"r": 1119}},"Collection": {"acquireCount": {"r": 1119}},"Mutex": {"acquireCount": {"r": 117}}},"storage": {......},"remote": "192.168.14.15:37666","protocol": "op_msg","durationMillis": 22427}}
從上述慢請求中,一些信息是比較直觀的,如 planSummary 為 COLLSCAN 代表查詢走了全表掃,而全表掃描一般意味著性能較差;如 docsExamied 若遠大于 nreturned ,則代表著請求執(zhí)行的效率較低,性能較差。而也有一些信息不那么直觀,如locks雖然代表了請求的鎖信息,但其中又分為不少子項目,第一眼看上去不禁會讓人感到疑惑:
-
ReplicationStateTransation、Global、Database、Collection、Mutex 分別代表什么意思?
-
W、R、w、r 分別代表什么含義?
-
鎖數(shù)量的大小意味著什么?
-
不同鎖之間是什么關(guān)系?有什么聯(lián)系?
-
MongoDB 中的鎖是如何實現(xiàn)的?結(jié)構(gòu)如何?
-
我們帶著以上問題開始逐步了解 MongoDB 中的鎖。
02
MongoDB 的鎖與資源分類
MongoDB 支持并發(fā)讀寫操作,故需要用鎖來確保并發(fā)時的數(shù)據(jù)一致性。在 MongoDB 中,不同的請求會對不同的資源執(zhí)行請求,故加鎖的操作也是在不同的資源上進行加鎖。通過對資源進行分層與層級管理,以及使用不同類型的鎖執(zhí)行鎖機制來避免并發(fā)沖突。
2.1 資源分類
在 MongoDB 中對資源進行了層級劃分,鎖本身的 lock_manager 并不區(qū)分自己屬于哪個資源,且不同層級的資源之間永不互斥(不會互相影響)。一般而言,MongoDB 中的資源按以下類型進行分類,且從上到下優(yōu)先級依次降低。
在4.4版本之前(包括),資源分類如下:
enum ResourceType {RESOURCE_INVALID = 0,
RESOURCE_PBWM, // 并發(fā)批量寫資源鎖
RESOURCE_RSTL, // 副本集成員狀態(tài)轉(zhuǎn)換鎖,狀態(tài)包括如STARTUP、PRIMARY、SECONDARY、RECOVERING等
RESOURCE_GLOBAL, // global操作的資源鎖,如reIndex、renameCollection、replSetResizeOplog等操作都會acquire global W 鎖
RESOURCE_DATABASE, // database級別操作的資源鎖,如cloneCollectionCapped、collMod、compact、convertToCapped等操作都會acquire databsae W 鎖RESOURCE_COLLECTION, // collection級別操作的資源鎖,如createCollection、createIndexes、drop等操作都會acquire collection W 鎖RESOURCE_METADATA, // 元數(shù)據(jù)相關(guān)的鎖
RESOURCE_MUTEX, // 剩余的不與存儲層相關(guān)的其他資源的鎖
ResourceTypesCount};
在4.4版本之后,資源分類如下:
// 資源類型大致與4.4版本之前一致,只是global資源有所變化enum ResourceType {RESOURCE_INVALID = 0,RESOURCE_GLOBAL,RESOURCE_DATABASE,RESOURCE_COLLECTION,RESOURCE_METADATA,RESOURCE_MUTEX,ResourceTypesCount};
// 使用枚舉來表示所有的global資源enum class ResourceGlobalId : uint8_t {kParallelBatchWriterMode,kFeatureCompatibilityVersion,kReplicationStateTransitionLock,kGlobal,
kNumIds};
從5.0開始,將 RESOURCE_PBWM、RESOURCE_RSTL、RESOURCE_GLOBAL 全部歸為了 RESOURCE_GLOBAL,且使用一個 enum 對其進行劃分。從5.0開始,還新增了一批 lock-free read 操作,這些操作在其他操作持有同 collection 的排他寫鎖時也不會被阻塞,如 find、count、distinct、aggregate、listCollections、listIndexes 等,其中 aggregate 中包含對 collection 的寫入時,會持有 collection 的意向排他鎖。
以上資源層級自上而下優(yōu)先級依次降低。為了防止出現(xiàn)死鎖,一般而言在對低優(yōu)先級的資源加鎖時,都需要先對更高優(yōu)先級的資源加意向鎖。如:在對 RESOURCE_COLLECTION 加排它鎖之前,需要對 RESOURCE_DATABASE以及 RESOURCE_GLOBAL 加意向排他鎖。
故該操作的鎖獲取情況為:
{"locks": {"Global": {"acquireCount": {"w": 1}},"Database": {"acquireCount": {"w": 1}},"Collection": {"acquireCount": {"W": 1}},}}
2.2 鎖分類
MongoDB 中不僅對資源進行了層級劃分,還對鎖的類型進行了劃分,如上文中提到的意向鎖、排它鎖、共享鎖等。本節(jié)講述在同一層級資源中,通過劃分不同的鎖類型,來高效地解決并發(fā)關(guān)系。
在 MongoDB 中為了提高并發(fā)效率,提供了類似讀寫鎖的模式,即共享鎖(Shared, S)(讀鎖)以及排他鎖(Exclusive, X)(寫鎖),同時,為了解決多層級資源之間的互斥關(guān)系,提高多層級資源請求的效率,還在此基礎(chǔ)上提供了意向鎖(Intent Lock)。即鎖可以劃分為4中類型:
enum LockMode {MODE_NONE = 0,MODE_IS = 1, //意向共享鎖,意向讀鎖,rMODE_IX = 2, // 意向排他鎖,意向?qū)戞i,wMODE_S = 3, // 共享鎖,讀鎖,RMODE_X = 4, // 排它鎖,寫鎖,W
LockModesCount};
有了讀寫鎖,為什么還要再劃分意向鎖?我們知道在多層級資源加鎖過程中,對低層級資源加鎖時還需要對高層級資源添加意向鎖。由于往往高層級的資源對低層級的資源是包含關(guān)系,故加意向鎖的操作目的是:在對低層級資源加鎖時,通過對上一級資源加意向鎖告訴外界,在高級資源中有某個低級資源被添加了鎖。
意向鎖有什么用呢?百科上的解釋為:
如果另一個任務(wù)企圖在某表級別上應(yīng)用共享或排他鎖,則受由第一個任務(wù)控制的表級別意向鎖的阻塞,第二個任務(wù)在鎖定該表前不需要檢查各個頁或行鎖,而只需檢查表上的意向鎖。
話說的比較繞,舉個例子:
-
假如有一個操作 A 鎖住了某一個 collectionX,此時有另一個操作 B 需要對 DB1 進行操作,而 collectionX 又屬于 DB1,此時在操作 B 加鎖之前,需要進行以下步驟的 Check:
-
檢查 DB1 的庫鎖是否被其他操作持有;
-
依次檢查 DB1 下所有的 collection,確認是否有其他操作持有其中之一的 collection 鎖;

這里需要遍歷所有的次級資源鎖進行判斷,若某 DB 下有很多的 collection,則遍歷拿鎖的時間線性增長,故引入了意向鎖的概念。當添加意向鎖后,操作 A 再給 collectionX 加鎖前,還需要給 DB1 加一把意向鎖;這樣在操作 B 在給 DB1 加鎖前,上述 check 步驟變?yōu)槿缦拢?/span>
-
檢查 DB1 的庫鎖是否被其他操作持有;
-
檢查 DB1 的意向鎖是否被其他操作持有。

這樣在請求上級資源的鎖時,只需要 check 上級資源的意向鎖是否被占用,如被占用則意味著有次級資源的鎖被占用,這里不必再去遍歷所有次級資源的鎖占用情況,使鎖獲取的判斷更加高效。
03
MongoDB 的鎖矩陣
在有了共享/排他鎖與意向鎖的分類后,MongoDB 的鎖可以被分為4類;同時,不同類型的鎖與其他類型鎖之間有不同的排他性,通過這種排他性關(guān)系可以實現(xiàn)提升鎖的效率。這種特殊的排他性可以被歸納為鎖矩陣。
MongoDB 的鎖矩陣如下:
/*** MongoDB鎖矩陣,可以根據(jù)鎖矩陣快速查詢當前想要加的鎖與已經(jīng)加鎖的類型是否沖突** | Requested Mode | Granted Mode |* |----------------|:------------:|:-------:|:--------:|:------:|:--------:|* | | MODE_NONE | MODE_IS | MODE_IX | MODE_S | MODE_X |* | MODE_IS | + | + | + | + | |* | MODE_IX | + | + | + | | |* | MODE_S | + | + | | + | |* | MODE_X | + | | | | |*/
根據(jù)鎖矩陣,可以根據(jù)請求所需的鎖類型以及當前請求對應(yīng)資源加鎖的類型來直接得出結(jié)論是否可以加鎖,同樣舉個例子:
如請求 A 在對 collection2 執(zhí)行讀操作,此時需要獲取 collection2 的 IS 鎖(Intent Shard Lock),由上述資源層級優(yōu)先級關(guān)系,需要向上依次獲得 DB2 的 IS 鎖以及 Global 的 IS 鎖:

此時請求 B 需要對 DB2 執(zhí)行 drop 操作,需要獲取 DB2 的 X 鎖(Exclusive Lock),由上述資源層級優(yōu)先關(guān)系,需要向上獲得 Global 的 IX 鎖,根據(jù) MongoDB 的鎖矩陣:
-
Global:IX 鎖與 IS 鎖兼容,故可以獲取到 Global 的 IX 鎖;
-
DB2:X 鎖與 IS 鎖不兼容,故不能獲取到 DB2 的 X 鎖,需要等待 DB2 的 IS 鎖釋放;
故此時操作 B 對 DB2 的 drop 操作將無法執(zhí)行,因為加鎖不成功,會等待
此時請求 C 需要對 collection2 執(zhí)行寫入操作,需要獲取 collection2 的 IX 鎖(Intent Exclusive Lock),由上述資源層級優(yōu)先關(guān)系,需要依次向上獲得 DB2 的 IX 鎖,Global 的 IX 鎖,根據(jù) MongoDB 的鎖矩陣:
● Global:IX 鎖與 IS 鎖兼容,故可以獲取到 Global 的 IX 鎖;
● DB2:IX 鎖與 IS 鎖兼容,故可以獲取到 DB2l 的 IX 鎖;
● Collection2:IX 鎖與 IS 鎖兼容,故可以獲取到 Collection2l 的 IX 鎖;
故此操作可以拿到鎖,執(zhí)行成功。
通過以上對資源的層級分類,以及通過使用意向鎖、讀寫鎖的分類,可以相對高效的通過鎖做到高并發(fā)。
以上提到了 Global、Database、Collection 三個資源級別以及對應(yīng)的鎖,而 MongoDB 最小粒度的資源為 Document,而 Document 粒度的鎖則使用的是 WT 引擎里的鎖,在 MongoDB 中,操作一般為樂觀并發(fā)控制,如寫操作,會先假設(shè)沒有沖突對數(shù)據(jù)進行修改,而只有真正修改數(shù)據(jù)時才會加鎖,而 Document 鎖加失敗時則會遭遇寫沖突(WriteConflict),而寫沖突時 MongoDB 會自動重試,這里不多做討論。
04
淺入:MongoDB 的鎖實現(xiàn)
了解了 MongoDB 中資源類型與對應(yīng)鎖的行為后,我們從代碼與實現(xiàn)層面來分析一下 MongoDB 鎖的實現(xiàn)與調(diào)用。MongoDB 中粒度最細的資源鎖為 collection 級別的鎖,我們看下,在 MongoDB 的代碼中如何獲取 collection 的鎖呢?
在 catalog_raii.h 中有與資源相關(guān)的定義與 RAII-style 的實現(xiàn),有關(guān) RAII-style 可以參考: wiki-RAII ,全稱為“資源獲取即初始化”,本質(zhì)是 C++ 這類面向?qū)ο缶幊田L格中將資源的獲取與生命周期與對象的生命周期強綁定的一種方式,說得直白一點為:以對象代表需要獲取的資源,在對象初始化時完成資源的獲取(構(gòu)造函數(shù)),在對象析構(gòu)的時候完成資源的釋放(析構(gòu))。
可以看到對于 Collection 資源,定義了以下類:
// catalog_raii.h
/*** RAII-style的類,在獲取collection鎖時會按照以下矩陣依次獲取更高資源層級的鎖** | modeColl | Global Lock Result | DB Lock Result | Collection Lock Result |* |----------+--------------------+----------------+------------------------|* | MODE_IX | MODE_IX | MODE_IX | MODE_IX |* | MODE_X | MODE_IX | MODE_IX | MODE_X |* | MODE_IS | MODE_IS | MODE_IS | MODE_IS |* | MODE_S | MODE_IS | MODE_IS | MODE_S |*/class AutoGetCollection {...
public:// 構(gòu)造函數(shù),用于調(diào)用獲取資源AutoGetCollection(......);
......
protected:boost::optional<AutoGetDb> _autoDb;std::vector<Lock::CollectionLock> _collLocks;......};
在 AutoGetCollection 的實現(xiàn)中,也可以看到需要先獲取 global/RSTL 與對應(yīng)的 Database 鎖,再會去獲取 collection locks:
// catalog_raii.cpp
AutoGetCollection::AutoGetCollection(......) {invariant(!opCtx->isLockFreeReadsOp());
......
// 獲取global/RSTL鎖以及所有對應(yīng)的DB鎖_autoDb.emplace(opCtx,!nsOrUUID.dbname().empty() ? nsOrUUID.dbname() : nsOrUUID.nss()->db(),isSharedLockMode(modeColl) ? MODE_IS : MODE_IX,deadline,secondaryDbNames);......
// 獲取collection鎖if (secondaryDbNames.empty()) {uassertStatusOK(nsOrUUID.isNssValid());_collLocks.emplace_back(opCtx, nsOrUUID, modeColl, deadline);} else {acquireCollectionLocksInResourceIdOrder(opCtx, nsOrUUID, modeColl, deadline, secondaryNssOrUUIDs, &_collLocks);}......}
如上文中的定義,AutoGetCollection 中如果要獲取某 mode 的 collection 鎖,需要依次獲取上層資源的意向鎖;在定義中可以看到,有一個std::vector<CollectionLock>代表需要獲取的 collection 對應(yīng)的鎖,而boost::optional<AutoGetDb>則代表了對應(yīng)上級的 Database 資源。獲取 collection 鎖之前,會根據(jù)獲取的 mode 進行轉(zhuǎn)換,如果獲取的是 S 或 IS,則會獲取對應(yīng) Database 的 IS 鎖;如果獲取的是 X 或 IX,則會獲取對應(yīng) Database 的 IX 鎖。
獲取 Collection 資源前需要先獲取對應(yīng)的 Database 資源,有以下定義:
// catalog_raii.h
// RAII-style class, 可獲取DB級別的資源鎖class AutoGetDb {......
public:// 構(gòu)造函數(shù),用于調(diào)用獲取資源AutoGetDb(......);......private:std::string _dbName;Lock::DBLock _dbLock;Database* _db;std::vector<Lock::DBLock> _secondaryDbLocks;};
其中 Lock::DBLock 即為 Database 持有的資源鎖,在獲取 Database 級別的鎖之前,首先要獲取其中包含的 Global 級別的鎖。
4.1 鎖的分類實現(xiàn)
上述流程中描述了獲取低級別的資源鎖前需要先獲取高級別的資源鎖,分別包括:
-
GlobalLock:代表著全局的資源鎖;
-
DBLock:代表著 Database 資源鎖;
-
CollectionLock:代表著 Collection 資源鎖。
以上鎖的定義都在 d_concurrency.h 文件中,我們分別來看其實現(xiàn):
// d_concurrency.h
/*** Collection 級別的鎖* 該鎖支持以下幾種類型:* MODE_IS: concurrent collection access, requiring read locks* MODE_IX: concurrent collection access, requiring read or write locks* MODE_S: shared read access to the collection, blocking any writers* MODE_X: exclusive access to the collection, blocking all other readers and writers** 在獲取collection鎖之前需要獲取對應(yīng)的意向DB鎖*/class CollectionLock {CollectionLock(const CollectionLock&) = delete;CollectionLock& operator=(const CollectionLock&) = delete;
public:CollectionLock(OperationContext* opCtx,const NamespaceStringOrUUID& nssOrUUID,LockMode mode,Date_t deadline = Date_t::max());
CollectionLock(CollectionLock&&);~CollectionLock();
private:ResourceId _id;OperationContext* _opCtx;};
從注釋中可以看到,在獲取 Collection 鎖之前需要獲取對應(yīng)的 Database 鎖。在 private 域中 ResourceId 代表了需要加鎖的 Collection 對應(yīng)的 ResourceId,在 MongoDB 所有的資源都通過 Resource 標識;而 OperationContext 中則有真正要加鎖的內(nèi)容,可以從 CollectionLock 的構(gòu)造函數(shù)實現(xiàn)中看到具體的加鎖過程:
// d_concurrency.cpp
Lock::CollectionLock::CollectionLock(......): _opCtx(opCtx) {if (nssOrUUID.nss()) {auto& nss = *nssOrUUID.nss();_id = {RESOURCE_COLLECTION, nss.ns()};
invariant(nss.coll().size(), str::stream() << "expected non-empty collection name:" << nss);dassert(_opCtx->lockState()->isDbLockedForMode(nss.db(),isSharedLockMode(mode) ? MODE_IS : MODE_IX));
_opCtx->lockState()->lock(_opCtx, _id, mode, deadline);return;}......bool locked = false;NamespaceString prevResolvedNss;do {if (locked) {_opCtx->lockState()->unlock(_id);}
_id = ResourceId(RESOURCE_COLLECTION, nss.ns());_opCtx->lockState()->lock(_opCtx, _id, mode, deadline);locked = true;
prevResolvedNss = nss;nss = CollectionCatalog::get(opCtx)->resolveNamespaceStringOrUUID(opCtx, nssOrUUID);} while (nss != prevResolvedNss);}
以上,是通過_opCtx->lockStat()->lock()獲取對應(yīng)的資源鎖。而鎖的定義在 OperationContext 中是這樣定義的:
// operation_context.h
class OperationContext : public Interruptible, public Decorable<OperationContext> {OperationContext(const OperationContext&) = delete;OperationContext& operator=(const OperationContext&) = delete;public:......
// Interface for locking. Caller DOES NOT own pointer.Locker* lockState() const {return _locker.get();}
......
private:......
std::unique_ptr<Locker> _locker;......};
其中 _locker 為 Locker 的一個 unique 指針,其中 Locker 為一個虛類作為 Interface,定義鎖的結(jié)構(gòu)及行為,定義在 locker.h 文件中,其一般的實現(xiàn)為 LockerImpl,定義在 lock_state.h 文件中。現(xiàn)先不討論 Locker 的具體結(jié)構(gòu)、繼承以及實現(xiàn),只需要先知道是通過調(diào)用 OperationConetext->lockState()->lock() 接口來進行的鎖的獲取。
獲取 Collection 鎖之前需要先獲取對應(yīng)的 Database 鎖,實現(xiàn)方式:
// d_concurrency.h/*** Database資源鎖.** 該鎖支持以下幾種類型::* MODE_IS: concurrent database access, requiring further collection read locks* MODE_IX: concurrent database access, requiring further collection read or write locks* MODE_S: shared read access to the database, blocking any writers* MODE_X: exclusive access to the database, blocking all other readers and writers** 在獲取DB鎖前需要獲取對應(yīng)類型的global鎖*/class DBLock {public:DBLock(OperationContext* opCtx,StringData db,LockMode mode,Date_t deadline = Date_t::max(),bool skipGlobalAndRSTLLocks = false);......
private:const ResourceId _id;OperationContext* const _opCtx;......
// Acquires the global lock on our behalf.boost::optional<GlobalLock> _globalLock;};
在 DBLock 的定義中,以 ResourceId 作為資源標識,同樣通過 OperationContext 來訪問真正的資源鎖,但要注意,在 DBLock 的定義中多了 boost::optional<GlobalLock>,表示 global 級別的鎖,即在為每個 Database 基本鎖加鎖前,理論上都需要先為 Global 鎖加鎖,可以看到 DBLock() 構(gòu)造函數(shù)實現(xiàn)如下:
// concurrency.cpp
Lock::DBLock::DBLock(......): _id(RESOURCE_DATABASE, db), _opCtx(opCtx), _result(LOCK_INVALID), _mode(mode) {
if (!skipGlobalAndRSTLLocks) {_globalLock.emplace(opCtx,isSharedLockMode(_mode) ? MODE_IS : MODE_IX,deadline,InterruptBehavior::kThrow);}massert(28539, "need a valid database name", !db.empty() && nsIsDbOnly(db));
_opCtx->lockState()->lock(_opCtx, _id, _mode, deadline);_result = LOCK_OK;}
如果設(shè)定了 skipGlobalAndRSLocks,則無需獲取 Global 鎖,否則都需要獲取對應(yīng) mode 的 Global 鎖。同時通過 OperationContext->lockState()->lock() 接口獲取對應(yīng)的 Database 資源鎖。
獲取 Database 鎖之前需要先獲取 Global 級別的鎖,我們繼續(xù)看 Global 鎖的實現(xiàn)如下:
// d_concurrency.h
// Global級別的資源鎖.class GlobalLock {public:GlobalLock(OperationContext* opCtx, LockMode lockMode): GlobalLock(opCtx, lockMode, Date_t::max(), InterruptBehavior::kThrow) {}
// A GlobalLock with a deadline requires the interrupt behavior to be explicitly defined.GlobalLock(OperationContext* opCtx,LockMode lockMode,Date_t deadline,InterruptBehavior behavior,bool skipRSTLLock = false);......
private:......OperationContext* const _opCtx;LockResult _result;ResourceLock _pbwm;ResourceLock _fcvLock;......};// Global exclusive lockclass GlobalWrite : public GlobalLock {};// Global shared lockclass GlobalRead : public GlobalLock {};
除了 GlobalLock 外,還有 GlobalRead 與 GlobalWrite,這里不詳細討論,僅討論 GlobalLock。可以看到,GlobalLock 中定義了兩個接口:
-
_takeGlobalLockOnly():僅占用 Global 鎖;
-
_takeGlobalAndRSTLLocks():同時占用 RSTL 與 Global 鎖;
同時可以看到還有 _pbwm 鎖以及 _fcv 鎖,這兩個鎖均以 ResourceLock 對象的形式實現(xiàn)。
// General purpose RAII wrapper for a resource managed by the lock managerclass ResourceLock {ResourceLock(const ResourceLock&) = delete;ResourceLock& operator=(const ResourceLock&) = delete;
public:......// Acquires lock on this specified resource in the specified mode.void lock(OperationContext* opCtx, LockMode mode, Date_t deadline = Date_t::max());void unlock();bool isLocked() const;
private:const ResourceId _rid;Locker* const _locker;LockResult _result;};
ResourceLock 中同樣有代表資源的 ResourceId,以及代表實際鎖的 Locker。下面來看 GlobalLock 的構(gòu)造函數(shù)的實現(xiàn)中是如何獲取鎖的:
Lock::GlobalLock::GlobalLock(OperationContext* opCtx,LockMode lockMode,Date_t deadline,InterruptBehavior behavior,bool skipRSTLLock): _opCtx(opCtx),_result(LOCK_INVALID),_pbwm(opCtx->lockState(), resourceIdParallelBatchWriterMode),_fcvLock(opCtx->lockState(), resourceIdFeatureCompatibilityVersion),_interruptBehavior(behavior),_skipRSTLLock(skipRSTLLock),_isOutermostLock(!opCtx->lockState()->isLocked()) {_opCtx->lockState()->getFlowControlTicket(_opCtx, lockMode);
try {......_result = LOCK_INVALID;if (skipRSTLLock) {_takeGlobalLockOnly(lockMode, deadline);} else {_takeGlobalAndRSTLLocks(lockMode, deadline);}......} catch (const ExceptionForCat<ErrorCategory::Interruption>&) {......}......}
void Lock::GlobalLock::_takeGlobalLockOnly(LockMode lockMode, Date_t deadline) {_opCtx->lockState()->lockGlobal(_opCtx, lockMode, deadline);}
void Lock::GlobalLock::_takeGlobalAndRSTLLocks(LockMode lockMode, Date_t deadline) {_opCtx->lockState()->lock(_opCtx, resourceIdReplicationStateTransitionLock, MODE_IX, deadline);ScopeGuard unlockRSTL([this] { _opCtx->lockState()->unlock(resourceIdReplicationStateTransitionLock); });
_opCtx->lockState()->lockGlobal(_opCtx, lockMode, deadline);
unlockRSTL.dismiss();}
可以看到最終還是通過 OperationContext->lockState()->lockGlobal() 接口來獲取全局鎖。
4.2 鎖結(jié)構(gòu)
上文可知,無論是 CollectionLock、DBLock、GlobalLock 還是 ResourceLock,其最終都是通過Locker類的對象來實現(xiàn)鎖的獲取與釋放。Locker 類定義了鎖的獲取、釋放等行為,以一個虛類的形式存在作為 MongoDB 中 lock 概念的 Interface,其定義在 locker.h 文件中,類的接口比較多,我們挑重點來看:
// locker.h
// Interface for acquiring locks. One of those objects will have to be instantiated for each request (transaction).class Locker {Locker(const Locker&) = delete;Locker& operator=(const Locker&) = delete;
friend class UninterruptibleLockGuard;
public:virtual ~Locker() {}/*** This is what the lock modes on the global lock mean:* IX - Regular write operation* IS - Regular read operation* S - Stops all *write* activity. Used for administrative operations (repl, etc).* X - Stops all activity. Used for administrative operations (repl state changes,* shutdown, etc).*/virtual void lockGlobal(OperationContext* opCtx,LockMode mode,Date_t deadline = Date_t::max()) = 0;virtual bool unlockGlobal() = 0;/*** Requests the RSTL to be acquired in the requested mode (typically mode X) . This should only* be called inside ReplicationStateTransitionLockGuard.*/virtual LockResult lockRSTLBegin(OperationContext* opCtx, LockMode mode) = 0;virtual void lockRSTLComplete(OperationContext* opCtx, LockMode mode, Date_t deadline) = 0;/*** Acquires lock on the specified resource in the specified mode and returns the outcome* of the operation. See the details for LockResult for more information on what the* different results mean.*/virtual void lock(OperationContext* opCtx,ResourceId resId,LockMode mode,Date_t deadline = Date_t::max()) = 0;virtual void lock(ResourceId resId, LockMode mode, Date_t deadline = Date_t::max()) = 0;virtual bool unlock(ResourceId resId) = 0;void skipAcquireTicket();void setAcquireTicket();shouldAcquireTicket();protected:......private:......};
其中 lockGlobal() 與 unlockGlobal() 是對 Global 基本資源的獲取/釋放鎖,lockRSTLBegin() 與 lockRSTLComplete() 為對 RSTL 鎖的獲取/釋放,lock() 與 unlock() 接口則是對其他指定 ResourceId 對應(yīng)資源鎖的獲取/釋放。
Locker 僅定義了一組 interface,而一般我們在 MongoDB 中使用的鎖,都是通過 LockerImpl 實現(xiàn)的,其定義在 lock_state.h 中:
// lock_state.h
/*** Interface for acquiring locks. One of those objects will have to be instantiated for each request (transaction).*/class LockerImpl : public Locker {public:......private:/*** Allows for lock requests to be requested in a non-blocking way. There can be only one* outstanding pending lock request per locker object.** _lockBegin posts a request to the lock manager for the specified lock to be acquired,* which either immediately grants the lock, or puts the requestor on the conflict queue* and returns immediately with the result of the acquisition. The result can be one of:** LOCK_OK - Nothing more needs to be done. The lock is granted.* LOCK_WAITING - The request has been queued up and will be granted as soon as the lock* is free. If this result is returned, typically _lockComplete needs to be called in* order to wait for the actual grant to occur. If the caller no longer needs to wait* for the grant to happen, unlock needs to be called with the same resource passed* to _lockBegin.*/LockResult _lockBegin(OperationContext* opCtx, ResourceId resId, LockMode mode);void _lockComplete(OperationContext* opCtx, ResourceId resId, LockMode mode, Date_t deadline);/*** Acquires a ticket for the Locker under 'mode'.* Returns true if a ticket is successfully acquired.* false if it cannot acquire a ticket within 'deadline'.* It may throw an exception when it is interrupted.*/bool _acquireTicket(OperationContext* opCtx, LockMode mode, Date_t deadline);......};
LockerImpl 實現(xiàn)了 Locker 的接口,同時新增了許多 private 的接口,上文中重點挑出了三個接口,分別是:
-
_lockBegin:通過 lockManager 來獲取鎖或者在隊列中等待鎖;
-
_lockComplete:等待鎖的獲取;
-
_acquireTicket:獲取 ticket。
為什么把他們?nèi)齻€單獨拎出來,看看 LockerImpl 對于兩個重要接口 lockGlobal() 與 lock() 的實現(xiàn)便知:
// lock_state.cpp
void LockerImpl::lockGlobal(OperationContext* opCtx, LockMode mode, Date_t deadline) {dassert(isLocked() == (_modeForTicket != MODE_NONE));if (_modeForTicket == MODE_NONE) {if (_uninterruptibleLocksRequested) {// Ignore deadline and _maxLockTimeout.invariant(_acquireTicket(opCtx, mode, Date_t::max()));} else {auto beforeAcquire = Date_t::now();deadline = std::min(deadline,_maxLockTimeout ? beforeAcquire + *_maxLockTimeout : Date_t::max());uassert(ErrorCodes::LockTimeout,str::stream() << "Unable to acquire ticket with mode '" << mode<< "' within a max lock request timeout of '"<< Date_t::now() - beforeAcquire << "' milliseconds.",_acquireTicket(opCtx, mode, deadline));}_modeForTicket = mode;}
const LockResult result = _lockBegin(opCtx, resourceIdGlobal, mode);// Fast, uncontended pathif (result == LOCK_OK)return;
invariant(result == LOCK_WAITING);_lockComplete(opCtx, resourceIdGlobal, mode, deadline);}
void LockerImpl::lock(OperationContext* opCtx, ResourceId resId, LockMode mode, Date_t deadline) {// `lockGlobal` must be called to lock `resourceIdGlobal`.invariant(resId != resourceIdGlobal);
const LockResult result = _lockBegin(opCtx, resId, mode);
// Fast, uncontended pathif (result == LOCK_OK)return;
invariant(result == LOCK_WAITING);_lockComplete(opCtx, resId, mode, deadline);}
由以上實現(xiàn)可知:
無論是 lockGlobal() 還是 lock() 最終都是通過調(diào)用 _lockBegin() 與 _lockComplete() 通過 lockManager 來獲取對應(yīng)級別的資源鎖;
Global 鎖在獲取前還需要調(diào)用 _acquireTicket() 來獲取 Ticket。
于是我們分別討論 ticket 與 lockManager。
4.3 有關(guān) ticket
MongoDB 中有兩種類型的 Ticket,一種是開啟流控(FlowControl)時,在獲取 Global 鎖之前,需要調(diào)用 getFlowContrlTicket() 來獲取流控的 ticket,本質(zhì)是通過漏桶、令牌桶等方式執(zhí)行限流操作。另一種則是在獲取 Global 鎖時,需要先通過 _acquireTicket() 接口獲取對應(yīng)的 ticket,MongoDB 通過 ticket 來控制請求的并發(fā)度,理論上大多數(shù)請求(除非設(shè)置了 skipAcquireTicket)都需要獲取 Global 鎖(IX、IS、X),故所有請求都需要獲取 Ticket,_acquireTicket() 的實現(xiàn)如下:
// lock_state.cpp
bool LockerImpl::_acquireTicket(OperationContext* opCtx, LockMode mode, Date_t deadline) {const bool reader = isSharedLockMode(mode);auto holder = shouldAcquireTicket() ? _ticketHolders->getTicketHolder(mode) : nullptr;if (holder) {_clientState.store(reader ? kQueuedReader : kQueuedWriter);
// If the ticket wait is interrupted, restore the state of the client.ScopeGuard restoreStateOnErrorGuard([&] { _clientState.store(kInactive); });
// Acquiring a ticket is a potentially blocking operation.if (opCtx)invariant(!opCtx->recoveryUnit()->isTimestamped());
auto waitMode = _uninterruptibleLocksRequested ? TicketHolder::WaitMode::kUninterruptible: TicketHolder::WaitMode::kInterruptible;if (deadline == Date_t::max()) {_ticket = holder->waitForTicket(opCtx, &_admCtx, waitMode);} else if (auto ticket = holder->waitForTicketUntil(opCtx, &_admCtx, deadline, waitMode)) {_ticket = std::move(*ticket);} else {return false;}restoreStateOnErrorGuard.dismiss();}_clientState.store(reader ? kActiveReader : kActiveWriter);return true;}
通過 _ticketHolders 來獲取一個 ticketHolder 對象,再通過 ticketHolder->waitForTicket() 或 ticketHolder->waitForTicketUntil() 來獲取 ticket。MongoDB 通過 TicketHolders 來管理 TicketHolder,分別通過 _openWriteTransaction 與 _openReadTransaction 來管理寫與讀的 ticket。
// ticketHolders.h
class TicketHolders {public:......
static TicketHolders& get(ServiceContext* svcCtx);static TicketHolders& get(ServiceContext& svcCtx);/*** Sets the TicketHolder implementation to use to obtain tickets from 'reading' (for MODE_S and* MODE_IS), and from 'writing' (for MODE_IX) in order to throttle database access. There is no* throttling for MODE_X, as there can only ever be a single locker using this mode. The* throttling is intended to defend against large drops in throughput under high load due to too* much concurrency.*/void setGlobalThrottling(std::unique_ptr<TicketHolder> reading,std::unique_ptr<TicketHolder> writing);
TicketHolder* getTicketHolder(LockMode mode);
private:std::unique_ptr<TicketHolder> _openWriteTransaction;std::unique_ptr<TicketHolder> _openReadTransaction;};
// ticketHolders.cpp
TicketHolder* TicketHolders::getTicketHolder(LockMode mode) {switch (mode) {case MODE_S:case MODE_IS:return _openReadTransaction.get();case MODE_IX:return _openWriteTransaction.get();default:return nullptr;}}
在 MongoDB 啟動時會調(diào)用 initializeStorageEngine(),而在其中則會初始化 TicketHolder 的類型以及數(shù)量:
// storage_engine_init.cpp
StorageEngine::LastShutdownState initializeStorageEngine(OperationContext* opCtx,const StorageEngineInitFlags initFlags) {......
// This should be set once during startup.if (storageGlobalParams.engine != "ephemeralForTest" &&(initFlags & StorageEngineInitFlags::kForRestart) == StorageEngineInitFlags{}) {auto readTransactions = gConcurrentReadTransactions.load();static constexpr auto DEFAULT_TICKETS_VALUE = 128;readTransactions = readTransactions == 0 ? DEFAULT_TICKETS_VALUE : readTransactions;auto writeTransactions = gConcurrentWriteTransactions.load();writeTransactions = writeTransactions == 0 ? DEFAULT_TICKETS_VALUE : writeTransactions;
auto svcCtx = opCtx->getServiceContext();auto& ticketHolders = TicketHolders::get(svcCtx);if (feature_flags::gFeatureFlagExecutionControl.isEnabledAndIgnoreFCV()) {LOGV2_DEBUG(5190400, 1, "Enabling new ticketing policies");switch (gTicketQueueingPolicy) {case QueueingPolicyEnum::Semaphore:LOGV2_DEBUG(6382201, 1, "Using Semaphore-based ticketing scheduler");ticketHolders.setGlobalThrottling(std::make_unique<SemaphoreTicketHolder>(readTransactions, svcCtx),std::make_unique<SemaphoreTicketHolder>(writeTransactions, svcCtx));break;case QueueingPolicyEnum::FifoQueue:LOGV2_DEBUG(6382200, 1, "Using FIFO queue-based ticketing scheduler");ticketHolders.setGlobalThrottling(std::make_unique<FifoTicketHolder>(readTransactions, svcCtx),std::make_unique<FifoTicketHolder>(writeTransactions, svcCtx));break;}} else {ticketHolders.setGlobalThrottling(std::make_unique<SemaphoreTicketHolder>(readTransactions, svcCtx),std::make_unique<SemaphoreTicketHolder>(writeTransactions, svcCtx));}}
......}
從代碼中可知,默認的讀與寫的 ticket 數(shù)目均為128個,且 TicketHolder 分為兩種類型:
-
SemaphoreTicketHolder:通過信號量控制同時持有 ticket 數(shù)目的TicketHolder,當請求數(shù)大于128時,未拿到 ticket 的線程將由于信號量控制而阻塞,當有多余資源被釋放時則通過信號量中斷調(diào)用獲取資源;
-
FifoTicketHolder:通過 FIFO 隊列來控制同時持有ticket數(shù)目的TicketHolder,當請求數(shù)大于128時,未拿到 ticket 的線程將進入一個先進先出的等待隊列等待 ticket 釋放。
兩個 TicketHolder 的 waitForTicket() 接口本質(zhì)最終都是調(diào)用 waitForTicketUntil() 接口,具體可以看 SemaphoreTicketHolder::waitForTicketUntil() 與 FifoTicketHolder::waitForTicketUntil() 的實現(xiàn),均在 ticketHolder.cpp 文件中。
我們得到的結(jié)論是:MongoDB 在獲取 Global 鎖之前,要先獲取 Ticket, Ticket 是 MongoDB 用于控制并發(fā)度的工具,在初始化 StorageEngine 時會初始化 TicketHolder,并初始化讀寫 Ticket 各128個(默認值,可改),同時 TicketHolder 有兩種類型,分別是使用信號量中斷通知等待線程的 SemaphoreTicketHolder,以及通過 FIFO 隊列使等待線程獲取 Ticket 的 FifoTicketHolder,兩種 Holder 的主要區(qū)別在于如何通知等待獲取資源的線程。
通過排隊/立即獲取完 Ticket 后,便可通過 _lockBegin 與 _lockComplete 獲取鎖。
4.4 鎖排隊&防餓死機制
下面我們來一起討論 _lockBegin() 與 _lockComplete() 兩個接口,首先看 _lockBegin():
// lock_state.cpp
MONGO_TSAN_IGNORELockResult LockerImpl::_lockBegin(OperationContext* opCtx, ResourceId resId, LockMode mode) {dassert(!getWaitingResource().isValid());......
// Making this call here will record lock re-acquisitions and conversions as well.globalStats.recordAcquisition(_id, resId, mode);_stats.recordAcquisition(resId, mode);
// Give priority to the full modes for Global, PBWM, and RSTL resources so we don't stall global// operations such as shutdown or stepdown.const ResourceType resType = resId.getType();if (resType == RESOURCE_GLOBAL) {if (mode == MODE_S || mode == MODE_X) {request->enqueueAtFront = true;request->compatibleFirst = true;}} else if (resType != RESOURCE_MUTEX) {// This is all sanity checks that the global locks are always be acquired// before any other lock has been acquired and they must be in sync with the nesting.if (kDebugBuild) {const LockRequestsMap::Iterator itGlobal = _requests.find(resourceIdGlobal);invariant(itGlobal->recursiveCount > 0);invariant(itGlobal->mode != MODE_NONE);};}
// The notification object must be cleared before we invoke the lock manager, because// otherwise we might reset state if the lock becomes granted very fast._notify.clear();
LockResult result = isNew ? getGlobalLockManager()->lock(resId, request, mode): getGlobalLockManager()->convert(resId, request, mode);
......
return result;}
在 _lockBegin() 中,會通過 globalStats 與 _stats 來記錄 ResId 對應(yīng)的資源請求以及鎖的 mode;同時,還會根據(jù) resType 以及 mode 的類型來設(shè)置排隊的優(yōu)先級,以實現(xiàn)防餓死機制。在獲取完 ticket 后,在獲取鎖的時候依然會進行排隊,這里通過為 S 與 X 的 Global 鎖設(shè)置了更高的排隊優(yōu)先級,通過 request->enqueueAtFront=true 與 request->compatibleFirst=true 來保證 Global 級別的 S 與 X 鎖在鎖排隊時有更高的優(yōu)先級,以確保如 shutdown 或 stepdown 這類的請求不會被阻塞很久。我們這里先有個印象,即 MongoDB 在獲取鎖時依舊有一個排隊隊列的情況,后續(xù)再詳細講解如何排隊,以及如何防餓死。
最后,通過 getGlobalLockManager()->lock() 與 getGlobalManager()->convert() 接口來對鎖進行獲取,并返回獲取的結(jié)果:
// lock_manager_defs.h
/*** Return values for the locking functions of the lock manager.*/enum LockResult {
// 成功獲取到鎖LOCK_OK,
// 存在鎖沖突,等待獲取鎖LOCK_WAITING,
// 獲取鎖超時LOCK_TIMEOUT,
// 初始化值,不應(yīng)被使用LOCK_INVALID};
可知,結(jié)果為 LOCK_OK 則代表成功獲取了對應(yīng)資源的鎖,而結(jié)果 LOCK_WAITING 則代表了當前需要獲取的鎖已被其他操作占有,進入鎖沖突等待隊列;LOCK_TIMEOUT 則意味著在等待隊列中等待的時間已經(jīng)超時。
這里繼續(xù)看 _lockComplete() 的實現(xiàn):
// lock_state.cpp
void LockerImpl::_lockComplete(OperationContext* opCtx,ResourceId resId,LockMode mode,Date_t deadline) {......
while (true) {if (opCtx && _uninterruptibleLocksRequested == 0) {result = _notify.wait(opCtx, waitTime);} else {result = _notify.wait(waitTime);}
// Account for the time spent waiting on the notification objectconst uint64_t curTimeMicros = curTimeMicros64();const uint64_t elapsedTimeMicros = curTimeMicros - startOfCurrentWaitTime;startOfCurrentWaitTime = curTimeMicros;
globalStats.recordWaitTime(_id, resId, mode, elapsedTimeMicros);_stats.recordWaitTime(resId, mode, elapsedTimeMicros);
if (result == LOCK_OK)break;
// If infinite timeout was requested, just keep waitingif (timeout == Milliseconds::max()) {continue;}
......}
invariant(result == LOCK_OK);unlockOnErrorGuard.dismiss();_setWaitingResource(ResourceId());}
如上述代碼,只有在調(diào)用 _lockBegin() 返回的結(jié)果不是 LOCK_OK 時才會繼續(xù)調(diào)用 _lockComplete() 進行等待,在 _lockComplete() 中、將會執(zhí)行等待,如果等待的鎖被釋放且成功拿到,則會通過 _notifyWait() 進行通知,同時 _lockComplete() 中還會統(tǒng)計等鎖的時間等信息。
上述文章描述了是如何等待與獲取鎖的,最終都是通過 LockManager->lock() 來執(zhí)行的鎖獲取與排隊等待,下面我們分析 LockManager 的結(jié)構(gòu)以及是如何防止鎖餓死的。
LockManager 的結(jié)構(gòu)如下:
// lock_manager.h
/*** Entry point for the lock manager scheduling functionality. Don't use it directly, but* instead go through the Locker interface.*/class LockManager {......
public:......
/*** Acquires lock on the specified resource in the specified mode and returns the outcome* of the operation. See the details for LockResult for more information on what the* different results mean.*/LockResult lock(ResourceId resId, LockRequest* request, LockMode mode);LockResult convert(ResourceId resId, LockRequest* request, LockMode newMode);bool unlock(LockRequest* request);......
private:// The lockheads need access to the partitionsfriend struct LockHead;
// These types describe the locks hash tablestruct LockBucket {SimpleMutex mutex;typedef stdx::unordered_map<ResourceId, LockHead*> Map;Map data;LockHead* findOrInsert(ResourceId resId);};
struct Partition {PartitionedLockHead* find(ResourceId resId);PartitionedLockHead* findOrInsert(ResourceId resId);typedef stdx::unordered_map<ResourceId, PartitionedLockHead*> Map;SimpleMutex mutex;Map data;};
LockBucket* _getBucket(ResourceId resId) const;Partition* _getPartition(LockRequest* request) const;......static const unsigned _numLockBuckets;LockBucket* _lockBuckets;
static const unsigned _numPartitions;Partition* _partitions;};
在LockManager中,比較重要的接口即lock()、convert()、unlock(),以及其中一些比較重要的結(jié)構(gòu)對象:
-
LockHead:結(jié)構(gòu)體用于真正的獲取鎖,其中包括了 grantedList 與等待的 connflictList;
-
LockBucket:結(jié)構(gòu)體用于定義 ResourceId-->LockHead 的哈希表;
-
_lockBuckets:為一個由 _numLockBuckets 定義長度的 LockBucket 數(shù)組;
-
_numLockBuckets:定義了 LockBucket 的長度,代碼中定死了為128。
上述共同組成了 LockManager 的結(jié)構(gòu),可以用如下圖來表述
下面分別討論:首先 LockManager 中存在一個長度為128的 BucketArray,其可以無鎖訪問,在每一個 lock() 操作中都首先通過 resId % 128來找到對應(yīng)的 bucket,這里利用了 ResourceId 彼此之間的無關(guān)性執(zhí)行了分桶操作,提高了并發(fā)。
// lock_manager.cpp
LockResult LockManager::lock(ResourceId resId, LockRequest* request, LockMode mode) {......// Use regular LockHead, maybe start partitioningLockBucket* bucket = _getBucket(resId);......LockHead* lock = bucket->findOrInsert(resId);
// Start a partitioned lock if possibleif (request->partitioned && !(lock->grantedModes & (~intentModes)) && !lock->conflictModes) {Partition* partition = _getPartition(request);stdx::lock_guard<SimpleMutex> scopedLock(partition->mutex);PartitionedLockHead* partitionedLock = partition->findOrInsert(resId);invariant(partitionedLock);lock->partitions.push_back(partition);partitionedLock->newRequest(request);return LOCK_OK;}
......}
// Have more buckets than CPUs to reduce contention on lock and cachesconst unsigned LockManager::_numLockBuckets(128);
LockManager::LockBucket* LockManager::_getBucket(ResourceId resId) const {return &_lockBuckets[resId % _numLockBuckets];}
每個 bucket 中存儲了一張 <ResourceId,LockHead> 的哈希 map,方便快速定位,且該哈希表受該 bucket 中的 mutex 鎖保護。當獲取 ResourceId 對應(yīng)的 LockHead 時,先通過 stdx::lock_guard<SimpleMutex> 獲取鎖,再通過 LockHead* lock = bucket->findOrInsert(resId) 獲取對應(yīng)的 LockHead。
// lock_manager.h
struct LockBucket {SimpleMutex mutex;typedef stdx::unordered_map<ResourceId, LockHead*> Map;Map data;LockHead* findOrInsert(ResourceId resId);};
LockHead 則是 MongoDB 中鎖的具象化對象,其簡要結(jié)構(gòu)如下:
// lock_manager.cpp
/*** There is one of these objects for each resource that has a lock request. Empty objects (i.e.* LockHead with no requests) are allowed to exist on the lock manager's hash table.*/struct LockHead {....../*** Finish creation of request and put it on the LockHead's conflict or granted queues. Returns* LOCK_WAITING for conflict case and LOCK_OK otherwise.*/LockResult newRequest(LockRequest* request) {invariant(!request->partitionedLock);request->lock = this;
// New lock request. Queue after all granted modes and after any already requested conflicting modesif (conflicts(request->mode, grantedModes) ||(!compatibleFirstCount && conflicts(request->mode, conflictModes))) {request->status = LockRequest::STATUS_WAITING;
// Put it on the conflict queue. Conflicts are granted front to back.if (request->enqueueAtFront) {conflictList.push_front(request);} else {conflictList.push_back(request);}
incConflictModeCount(request->mode);
return LOCK_WAITING;}
// No conflict, new requestrequest->status = LockRequest::STATUS_GRANTED;
grantedList.push_back(request);incGrantedModeCount(request->mode);
if (request->compatibleFirst) {compatibleFirstCount++;}
return LOCK_OK;}
// Id of the resource which is protected by this lock. Initialized at construction time and does not change.ResourceId resourceId;
// Granted queueLockRequestList grantedList;uint32_t grantedCounts[LockModesCount];uint32_t grantedModes;
// Conflict queueLockRequestList conflictList;uint32_t conflictCounts[LockModesCount];uint32_t conflictModes;
......};
LockHead 是對應(yīng)于某個 ResourceId的鎖對象,維護著所有對該 ResourceId 的鎖請求。LockHead 中有兩個重要的組成部分:ConflictList 與 GrantList 為兩個雙向鏈表,分別代表著鎖的等待隊列與當前鎖的持有隊列,其中 ConflictList 為一個 FIFO 的隊列,同時還有 ConflictCounts 與 GrantCounts 來維護等待隊列與持有隊列的長度,GrantedModes 與 ConflictModes 則作為 bit-mask 來標識當前隊列中存在鎖的 mode 類型。為什么要這么做?試想當有一個新的請求到達時,需要遍歷所有 GrantList 中的元素來檢測請求中的 mode 與隊列中是否沖突,這樣做時間復雜度為 O(n),并不高效。
// 偽代碼
def lock(newNode):foreach node in GrandList:if conflict(node.mode, newNode.mode):return ConflictList.add(newNode);return GrantList.add(newNode);
為了解決這個問題,MongoDB 為 ConflictList 與 GrantList 增加了引用計數(shù)的數(shù)組,在將一個對象添加到 GrantList 中時,同時需要對 GrantCounts[mode] 進行累加,如果 GrantCounts[mode] 是從0到1的變化,則需要將 GrantModes 對應(yīng) mode 的 bitMask 設(shè)置為1。從 GrantList 中刪除對象時則是一個逆向的對稱操作。這樣,GrantCounts[mode] 表示了每一個 mode 對應(yīng)的在 GrantList 中的數(shù)量,而 GrantModes 則表示當前 GrantList 中是否存在對應(yīng) mode 的鎖持有。由此,在判斷某個 mode 是否與當前 GrantList 中已有對象沖突時,只需將待加節(jié)點的 mode 與 GrantModes 中對應(yīng)的 bitMask 進行比較,時間復雜度從 O(n) 降低到 0(1)。
// lock_manager.cpp
uint_32 conflictCounts[LockModesCount];uint_32 conflictModes;
// Methods to maintain the conflict queuevoid incConflictModeCount(LockMode mode) {invariant(conflictCounts[mode] >= 0);if (++conflictCounts[mode] == 1) {invariant((conflictModes & modeMask(mode)) == 0);conflictModes |= modeMask(mode); // 算出mode的bit-map,再進行按位或賦值}}
void decConflictModeCount(LockMode mode) {invariant(conflictCounts[mode] >= 1);if (--conflictCounts[mode] == 0) {invariant((conflictModes & modeMask(mode)) == modeMask(mode));conflictModes &= ~modeMask(mode); // 算出mode的bit-map,再進行取反按位與取消賦值}}enum LockMode {MODE_NONE = 0,MODE_IS = 1,MODE_IX = 2,MODE_S = 3,MODE_X = 4,
LockModesCount};uint32_t modeMask(LockMode mode) {return 1 << mode; // 對1左移mode代表的位構(gòu)建bit-mask}// Helper functions for the lock modesbool conflicts(LockMode newMode, uint32_t existingModesMask) {return (LockConflictsTable[newMode] & existingModesMask) != 0;}// Map of conflicts.static const int LockConflictsTable[] = {0, // MODE_NONE(1 << MODE_X), // MODE_IS(1 << MODE_S) | (1 << MODE_X), // MODE_IX(1 << MODE_IX) | (1 << MODE_X), // MODE_S(1 << MODE_S) | (1 << MODE_X) | (1 << MODE_IS) | (1 << MODE_IX), // MODE_X};
上述代碼解決了意向鎖中獲取與排隊等待的問題,并提高了獲取鎖的效率。對于一個鎖請求,如果與當前 GrantList 中的請求類型無沖突,就將其添加到 GrantList 中加鎖成功,否則將其添加到 ConflictList 中,并等待 grantedModes 變更時,從 ConflictList 中選擇一批與 grantedModes 兼容的加鎖請求進入 GrantList。但是上述策略會有一個問題:
-
試想以下場景:如果 ConflictList 中有 X 鎖在等待,而 GrantList 中的 IS/IX 鎖請求源源不斷的進來,那么 X 鎖就會一直無法被調(diào)度,即鎖會被餓死。
為了避免這種排它鎖被共享鎖餓死的情況,在 ConflictList 的 FIFO 隊列基礎(chǔ)上,引入了排隊優(yōu)先級概念。MongoDB 通過添加 enqueueAtFront 與 compatibleFirst 這兩個參數(shù)來解決排它鎖餓死的問題。其位于在獲取 lock 時傳入的 LockRequest 中:
// lock_manager_defs.h
/*** There is one of those entries per each request for a lock. They hang on a linked list off* the LockHead or off a PartitionedLockHead and also are in a map for each Locker. This* structure is not thread-safe.*/struct LockRequest {enum Status {STATUS_NEW,STATUS_GRANTED,STATUS_WAITING,STATUS_CONVERTING,
// Counts the rest. Always insert new status types above this entry.StatusCount};
......
// If the request cannot be granted right away, whether to put it at the front or at the end of// the queue. By default, requests are put at the back. If a request is requested to be put at// the front, this effectively bypasses fairness. Default is FALSE.bool enqueueAtFront;
// When this request is granted and as long as it is on the granted queue, the particular// resource's policy will be changed to "compatibleFirst". This means that even if there are// pending requests on the conflict queue, if a compatible request comes in it will be granted// immediately. This effectively turns off fairness.bool compatibleFirst;
......};
如,在_lockBegin()中,就會將 Global 級別的 X 鎖設(shè)置高優(yōu)先級:
// lock_state.cpp
MONGO_TSAN_IGNORELockResult LockerImpl::_lockBegin(OperationContext* opCtx, ResourceId resId, LockMode mode) {......// Give priority to the full modes for Global, PBWM, and RSTL resources so we don't stall global// operations such as shutdown or stepdown.const ResourceType resType = resId.getType();if (resType == RESOURCE_GLOBAL) {if (mode == MODE_S || mode == MODE_X) {request->enqueueAtFront = true;request->compatibleFirst = true;}} else if (resType != RESOURCE_MUTEX) {// This is all sanity checks that the global locks are always be acquired// before any other lock has been acquired and they must be in sync with the nesting.if (kDebugBuild) {const LockRequestsMap::Iterator itGlobal = _requests.find(resourceIdGlobal);invariant(itGlobal->recursiveCount > 0);invariant(itGlobal->mode != MODE_NONE);};}......}
其中 enqueueAtFront 參數(shù)決定了當當前請求的 mode 沖突時,是將請求插入 ConflictList 的最前面還是最后面,即設(shè)置了 enqueueAtFront 參數(shù)的請求將會在等待隊列的最前面插隊:
// lock_manager.cpp
LockResult newRequest(LockRequest* request) {......
// New lock request. Queue after all granted modes and after any already requested conflicting modesif (conflicts(request->mode, grantedModes) ||(!compatibleFirstCount && conflicts(request->mode, conflictModes))) {request->status = LockRequest::STATUS_WAITING;
// Put it on the conflict queue. Conflicts are granted front to back.if (request->enqueueAtFront) {conflictList.push_front(request);} else {conflictList.push_back(request);}
incConflictModeCount(request->mode);
return LOCK_WAITING;}// No conflict, new requestrequest->status = LockRequest::STATUS_GRANTED;
grantedList.push_back(request);incGrantedModeCount(request->mode);
if (request->compatibleFirst) {compatibleFirstCount++;}......}
而 compatibleFrist 參數(shù)則是配合 enqueueAtFront 一起配合防止排它鎖餓死,上述代碼所示:
-
如果鎖請求與當前 GrantedModes 沖突,則進入ConflictList等待,且根據(jù) enqueueAtFront 來判斷是插入等待隊列的隊頭/隊尾;
-
如果鎖請求與當前 GrantedModes 不沖突,也未必能加鎖成功,還需要檢測當前 GrantList 持有鎖的資源中的 complatiblecFristCount,即:GrantList 中 compatibleFist=true 的鎖請求的個數(shù),如果 GrantList 中無 complatibleFirst 的鎖請求,且請求的鎖 mode 與當前 ConflictList 中的請求mode沖突,則依舊要將新的請求加入 ConflictList 等待隊列進行等待,這里保證了當有排它鎖在 ConflictList 中等待時,新的共享鎖不會不斷的進入 GrantList 獲取鎖而導致排它鎖餓死。
-
如果獲取鎖成功,則將鎖請求加入 GrantList 中,并將 compatibleFristCount++;
-
現(xiàn)在我們來分析有 Global 的 X 鎖進入排隊的情況:
-
當有 Global 的 X 鎖請求時,MongoDB 會為當前請求設(shè)置 enqueueAtFirst=true 以及 compatibleFirst=true;
-
此時 GrantList 中的鎖請求 mode 均為 IX/IS 類型;
-
請求到達時,由于 mode 與 GrantList 沖突,Global 的 X 鎖請求被加入 ConflictList 隊列等待,且由于 enqueueAtFirst=true,請求被直接加到 ConflictList 的隊頭;
-
再有新的 IX/IS 請求到達時,由于此時 compatibleFristCount==0,且請求的 IX/IS 類型鎖與 ConflictList 中的 Global 的 X 鎖類型沖突,導致新的 IX/IS 鎖請求也依舊進入 ConflictList 隊尾進行等待。
-
由于新的請求不斷的進入 ConflictList 進行等待,且 Global 的 X 鎖請求位于 ConflictList 的 FIFO 隊列第一位,防止了排它鎖被源源不斷的共享鎖餓死。
本文主要從 MongoDB 的慢日志引入,為你詳細拆解了 MongoDB 的鎖與相關(guān)實現(xiàn)問題。在下一篇中,我們將對 MongoDB 的操作和鎖使用進行深入的闡述,敬請期待。

你為什么選擇 MongoDB,以及它有什么優(yōu)缺點? 歡迎評論留言。我們將選取1則優(yōu)質(zhì)的評論,送出騰訊Q哥公仔 1個 (見下圖)。2月29日中午12點開獎。

????歡迎加入騰訊云開發(fā)者社群,享前沿資訊、大咖干貨,找興趣搭子,交同城好友,更有鵝廠招聘機會、限量周邊好禮等你來~

(長按圖片立即掃碼)



