MongoDB 基礎淺談

語言: CN / TW / HK

作者:hazenweng,騰訊 QQ 音樂後臺開發工程師

MongoDB 作為一款優秀的基於分散式檔案儲存的 NoSQL 資料庫,在業界有著廣泛的應用。下文對 MongoDB 的一些基礎概念進行簡單介紹。

1 MongoDB 特點

  • 面向集合儲存:MongoDB 是面向集合的,資料以 collection 分組儲存。每個 collection 在資料庫中都有唯一的名稱。

  • 模式自由:集合的概念類似 MySQL 裡的表,但它不需要定義任何模式。

  • 結構鬆散:對於儲存在資料庫中的文件,不需要設定相同的欄位,並且相同的欄位不需要相同的資料型別,不同結構的文件可以存在同一個 collection 裡。

  • 高效的二進位制儲存:儲存在集合中的文件,是以鍵值對的形式存在的。鍵用於唯一標識一個文件,一般是 ObjectId 型別,值是以 BSON 形式存在的。BSON = Binary JSON, 是在 JSON 基礎上加了一些型別及元資料描述的格式。

  • 支援索引: 可以在任意屬性上建立索引,包含內部物件。 MongoDB 的索引和 MySQL 的索引基本一樣,可以在指定屬性上建立索引以提高查詢的速度。 除此之外, Mon goDB 還提供建立基於地理空間的索引的能力。

  • 支援 mapreduce:通過分治的方式完成複雜的聚合任務。

  • 支援 failover:通過主從複製機制,可以實現資料備份、故障恢復、讀擴充套件等功能。基於複製集的複製機制提供了自動故障恢復的功能,確保了叢集資料不會丟失。

  • 支援分片:MongoDB 支援叢集自動切分資料,可以使叢集儲存更多的資料,實現更大的負載,在資料插入和更新時,能夠自動路由和儲存。

  • 支援儲存大檔案:MongoDB 中 BSON 物件最大不能超過 16 MB。對於大檔案的儲存,BSON 格式無法滿足。GridFS 機制提供了一個儲存大檔案的機制,可以將一個大檔案分割成為多個較小的文件進行儲存。

2 MongoDB 要素

  • database: 資料庫。

  • collection: 資料集合,相當於 MySQL 的 table。

  • document: 資料記錄行,相當於 MySQL 的 row。

  • field: 資料域,相當於 MySQL 的 column。

  • index: 索引。

  • primary key: 主鍵。

3 MongoDB 資料庫

一個 MongoDB 例項可以建立多個 database。連線時如果沒開啟免認證模式的話,需要連線到 admin 庫進行認證。如果開啟免認證模式,若不指定 database 進行連線,預設連線一個叫 db 的資料庫,該資料庫儲存在 data 目錄中。通過 show dbs 命令可以檢視所有的資料庫。資料庫名不能包含空字元。資料庫名不能為空並且必須小於 64 個字元。

MongoDB 預留了幾個特殊的 database。

  • admin: admin 資料庫主要是儲存 root 使用者和角色。例如,system.users 表儲存使用者,system.roles 表儲存角色。一般不建議使用者直接操作這個資料庫。將一個使用者新增到這個資料庫,且使它擁有 admin 庫上的名為 dbAdminAnyDatabase 的角色許可權,這個使用者自動繼承所有資料庫的許可權。一些特定的伺服器端命令也只能從這個資料庫執行,比如關閉伺服器。

  • local: local 資料庫是不會被複制到其他分片的,因此可以用來儲存本地單臺伺服器的任意 collection。一般不建議使用者直接使用 local 庫儲存任何資料,也不建議進行 CRUD 操作,因為資料無法被正常備份與恢復。

  • config: 當 MongoDB 使用分片設定時,config 資料庫可用來儲存分片的相關資訊。

一個 MongoDB 例項的資料結構如下圖:

4 MongoDB 集合

MongoDB 集合存在於資料庫中,沒有固定的結構,可以往集合插入不同格式和型別的資料。集合不需要事先建立。當第一個文件插入,或者第一個索引建立時,集合就會被建立。集合名必須以下劃線或者字母符號開始,並且不能包含 $,不能為空字串(比如 ""),不能包含空字元,且不能以 system. 為字首。

capped collection 是固定大小的集合,支援高吞吐的插入操作和查詢操作。它的工作方式與迴圈緩衝區類似,當一個集合填滿了被分配的空間,則通過覆蓋最早的文件來為新的文件騰出空間。和標準的 collection 不同,capped collection 需要顯式建立,指定大小,單位是位元組。capped collection 可以按照文件的插入順序儲存到集合中,而且這些文件在磁碟上存放位置也是按照插入順序來儲存的,所以更新 capped collection 中的文件,不可以超過之前文件的大小,以便確保所有文件在磁碟上的位置一直保持不變。

5 MongoDB 檢視

檢視基於已有的集合進行建立,是隻讀的,不實際儲存硬碟,通過檢視進行寫操作會報錯。檢視使用其上游集合的索引。由於索引是基於集合的,所以你不能基於檢視建立、刪除或重建索引,也不能獲取檢視的索引列表。如果檢視依賴的集合是分片的, 那麼檢視也視為分片的。檢視是實時計算並讀取的。

6 MongoDB 索引

MongoDB 支援豐富的索引方式。如果沒有索引,讀操作就必須掃描集合中的每個文件並篩選符合查詢條件的記錄。索引能夠在很大程度上提高查詢速度。

  • 單欄位索引:有三種方式,(1)在單個欄位上建立索引;(2)在嵌入式欄位上建立索引;(3)在內嵌文件上建立索引。

  • 複合索引:支援在多個欄位上匹配的查詢。對任何複合索引施加 32 個欄位的限制。對於複合索引,MongoDB 可以使用索引來支援對索引字首的查詢。

  • 多鍵索引:為了索引包含陣列值的欄位,MongoDB 為陣列中的每個元素建立一個索引鍵。這些多鍵索引支援對陣列欄位的高效查詢。

  • 文字索引:支援對字串內容的文字搜尋查詢。文字索引可以包含任何值為字串或字串元素陣列的欄位。一個集合最多可以有一個文字索引。

  • 萬用字元索引:支援針對未知或任意欄位的查詢。例如:db.collection.createIndex( {"a.$**" : 1 } ) 可支援諸如 db.collection.find({ "a.b" : 1 })、db.collection.find({ "a.c" : { $lt : 2 } }) 等查詢,提高查詢效率。不能使用萬用字元索引來分片集合。不能為萬用字元建立複合索引。

  • 萬用字元文字索引:萬用字元文字索引不同於萬用字元索引。萬用字元索引不支援使用 $text操作符的查詢。萬用字元文字索引為集合中每個文件中包含字串資料的每個欄位建立索引。索引的建立方式示例:db.collection.createIndex( { "$**": "text" } )。

  • 2dsphere 索引:支援球體上的地理空間查詢:包含、相交和鄰近度查詢。

  • hashed 索引:支援使用雜湊的分片鍵進行分片。基於雜湊的分片使用欄位的雜湊索引作為分片鍵,以便跨分片叢集對資料進行分割槽。MongoDB 支援任何單個欄位的雜湊索引,但不支援建立具有多個雜湊欄位的複合索引,也不能在索引上指定唯一雜湊索引。

  • ttl 索引:一種特殊的單欄位索引,支援在一定的時間或特定的期限後自動從集合中刪除文件。TTL 索引不能保證過期資料在過期時立即刪除。預設每 60 秒執行一次刪除過期文件的後臺程序。capped collection 不支援 ttl 索引。

  • 唯一索引:確保索引欄位不會儲存重複值。如果集合已經存在了違反索引的唯一約束的文件,則後臺建立唯一索引會失敗。

  • 部分索引:只索引集合中滿足指定篩選器表示式的文件。例如:db.collection.createIndex({ a:1 },{ partialFilterExpression: { b: { $lt: 100 } } }) 表示只對集合中 b 欄位小於 100 的文進行索引,大於等於 100 的文件不會被索引。這可以有效提高儲存效率。

  • 稀疏索引:只包含有索引欄位的文件的條目,即使索引欄位包含空值。索引會跳過任何缺少索引欄位的文件。非稀疏索引包含集合中的所有文件,為那些不包含索引欄位的文件儲存空值。

7 MongoDB ObjectId

ObjectId 可以快速生成並排序,長度為 12 個位元組,包括:

  • 一個 4 位元組的時間戳,表示 unix 時間戳

  • 5 位元組隨機值

  • 3 位元組遞增計數器,初始化為隨機值

在 MongoDB 中,儲存在集合中的每個文件都需要一個唯一的 _id 欄位作為主鍵。如果插入的文件省略了 _id 欄位,則自動為文件生成一個 _id。

8 MongoDB 複製集

MongoDB 的複製集又稱為副本集(Replica Set),是一組維護相同資料集合的 mongod 程序。複製集包含多個數據節點和一個可選的仲裁節點(arbiter)。在資料節點中,有且僅有一個成員為主節點(primary),其他節點為從節點(secondary)。

一個典型的複製集架構圖如下:

8.1 複製集節點型別

  • 主節點:接收所有的寫操作,並將集合所有的變化記錄到操作日誌中,即 oplog。

  • 從節點:通過複製主節點的操作來維護一個相同的資料集。從節點有幾個選配項:v 引數決定是否具有投票權;priority 引數決定節點選主過程時的優先順序;hidden 引數 決定是否對客戶端可見;slaveDelay 引數表示複製 n 秒之前的資料,保持與主節點的時間差。從節點可以配置成 0 優先順序,阻止它在選舉中成為主節點,適用於將該節點部署在備用資料中心,或者將它作為一個冷節點;可以配置為隱藏複製集,防止應用程式從它讀取資料,適用於在該節點上執行需要與正常流量分離的程式;可以配置為延遲複製集,保持一個歷史快照,以便做按特定時間的故障恢復。

  • 仲裁節點:如果將一個 mongod 例項作為仲裁節點新增到一個複製集中,該節點可以參與主節點選舉,但不儲存資料。仲裁節點永遠只能是仲裁節點。

8.2 複製集選主

MongoDB 的副本集協議(又稱為 pv1),是一種 raft-like 協議,即基於 raft 協議的理論思想實現,並且對之進行了一些擴充套件。當往復制集新增一個節點,或當主節點無法和叢集中其他節點通訊的時間超過引數 electionTimeoutMillis 配置的期限時,從節點會嘗試通過 pv1 協議發起選舉來推薦自己成為新主節點。

在選舉前具有投票權的節點之間兩兩互相傳送心跳,以偵測節點是否存活。複製集節點每兩秒向彼此傳送心跳。如果心跳未在 10 秒內返回,則傳送心跳的一方將被髮送方標記為不可訪問,也就是說,預設當 5 次心跳未收到時判斷為節點失聯。如果失聯的是主節點,從節點會發起選舉,選出新的主節點;如果失聯的是從節點則不會產生新的選舉。選舉基於 raft 一致性演算法實現,在大多數投票節點存活下選舉出主節點。只有能夠與多數節點建立連線且具有較新的 oplog 的節點才可能被選舉為主節點,如果叢集裡的節點配置了優先順序,那麼具有較高的優先順序的節點更可能被選舉為主節點。

複製集中最多可以有 50 個節點,但具有投票權的節點最多 7 個。

8.3 複製集作用

  • 主節點發生故障時自動選舉出一個新的主節點,以實現 failover。

  • 將資料從一個數據中心複製到另一個數據中心,減少另一個數據中心的讀延遲。

  • 實現讀寫分離。

  • 實現容災,可以在資料中心故障時快速切換到同城或異地的資料中心。

9 MongoDB 分片集

MongoDB 支援通過分片技術來支援海量資料儲存。解決資料增長的擴充套件方式有兩種:垂直擴充套件和水平擴充套件。垂直擴充套件通過增加單個伺服器的能力來實現,比如磁碟空間、記憶體容量、CPU 數量等;水平擴充套件則通過將資料儲存到多個伺服器上來實現。MongoDB 通過分片實現水平擴充套件。

一個典型的分片叢集架構如下:

9.1 分片集元件

  • shard:每個分片上可以儲存一個集合的子集,所有分片上的子集的資料互不相交,構成完整的集合。每個分片可以被部署為複製集架構。最大為 1024 個分片。

  • mongos:充當查詢路由器,在客戶端和分片集之間提供讀寫介面。mongos 提供叢集單一入口,轉發應用端請求,選擇合適的資料節點進行讀寫,合併多個數據節點的返回。mongos 是無狀態的,分片叢集一般需要配置至少 2 個 mongos。

  • config server:儲存分片集的相關配置資訊。

9.2 分片鍵

MongoDB 集合若要採用分片,必須要指定分片鍵(shard key)。分片鍵由文件中的一個或多個欄位組成。分片集合必須具有支援分片鍵的索引,索引可以是分片鍵的索引,也可以是以分片鍵是索引字首的複合索引。要對已填充的集合進行分片,該集合必須具有以分片鍵開頭的索引;分片一個空集合時,如果該集合還沒有包含指定分片鍵的索引,則 MongoDB 會預設給分片鍵建立索引。

對於一個即將要分片的集合,如果該集合具有其他唯一索引,則無法分片該集合。

對於已分片的集合,不能在其他欄位上建立唯一索引。

4.2 版本開始可以更改文件的分片鍵值,除非分片鍵欄位為不可變的 _id 欄位。更新分片鍵時必須在事務中或以可重試寫入的方式在 mongos 上執行,不能直接在分片上執行操作。在此之前文件的分片鍵欄位值是不可變的。

4.4 版本開始,可以向現有片鍵中新增一個或多個字尾欄位以優化集合的片鍵。

5.0 版本開始,實現了實時重新分片(live resharding),可以實現分片鍵的完全重新選擇。live resharding 機制下,資料將根據新的分片規則進行遷移,不過有一些限制,比如一個例項中有且只能有一個集合在相同的時間下 resharding 等。

資料庫可以混合使用分片和未分片集合。分片集合被分割槽並分佈在叢集中的各個分片中。而未分片集合僅儲存在主分片中。

設定 shard key 時應該充分考慮取值基數和取值分佈。分片鍵應被儘可能多的業務場景用到。儘可能避免使用單調遞增或遞減的欄位作為分片鍵。

9.3 分片策略

MongoDB 將分片資料拆分成塊。每個分塊都有一個基於分片鍵的上下限範圍 。分片策略包括雜湊分片、範圍分片和自定義 zone 分片。

  • 雜湊分片會計算分片鍵欄位的雜湊值,這個值被用作片鍵,然後根據雜湊值的雜湊為每個塊分配一個範圍。

  • 範圍分片根據分片鍵的值將資料劃分為多個連續範圍。,然後基於分片鍵的值分配每個塊的範圍。當片鍵的基數大、頻率低且值非單調變更時,範圍分片更高效。

  • 自定義 zone 分片基於 shard key 建立。每個 zone 與叢集中的一個或者更多分片關聯。一個分片可以和任意數目的非衝突 zone 相關聯。

10 MongoDB 聚合

MongoDB 聚合框架(Aggregation Framework)是一個計算框架,功能是:

  • 作用在一個或幾個集合上。

  • 對集合中的資料進行的一系列運算。

  • 將這些資料轉化為期望的形式。

MongoDB 提供了三種執行聚合的方法:聚合管道,map-reduce 和單一目的聚合方法(如 count、distinct 等方法)。

10.1 聚合管道

在聚合管道中,整個聚合運算過程稱為管道(pipeline),它是由多個步驟(stage)組成的, 每個管道的工作流程是:

  1. 接受一系列原始資料文件

  2. 對這些文件進行一系列運算

  3. 結果文件輸出給下一個 stage

聚合計算基本格式如下:

pipeline = [$stage1, $stage2, ...$stageN];     db.collection.aggregate( pipeline, { options } )

10.2 map-reduce

map-reduce 操作包括兩個階段:map 階段處理每個文件並將 key 與 value 傳遞給 reduce 函式進行處理,reduce 階段將 map 操作的輸出組合在一起。map-reduce 可使用自定義 JavaScript 函式來執行 map 和 reduce 操作,以及可選的 finalize 操作。通常情況下效率比聚合管道低。

10.3 單一目的聚合方法

主要包括以下三個:

  • db.collection.estimatedDocumentCount()

  • db.collection.count()

  • db.collection.distinct()

11 MongoDB 一致性

分散式系統有個 PACELC 理論。根據 CAP,在一個存在網路分割槽(P)的分散式系統中,要面臨在可用性(A)和一致性(C)之間的權衡,除此之外(E),即使沒有網路分割槽的存在,在實際系統中,我們也要面臨在訪問延遲(L)和一致性(C)之間的權衡。MongoDB 的一致性模型對讀寫操作 L 和 C 的選擇提供了豐富的選項。

11.1 因果一致性

單節點的資料庫由於為讀寫操作提供了順序保證,因此實現了因果一致性。分散式系統同樣可以提供這些保證,但必須對所有節點上的相關事件進行協調和排序。

以下是一個不遵循因果一致性的例子:

為了保持因果一致性,必須有以下保證:

實現因果一致性的單號讀寫應遵循以下流程:

為了建立複製集和分片集事件的全域性偏序關係,MongoDB 實現了一個邏輯時鐘,稱為 lamport logical clock。每個寫操作在應用於主節點時都會被分配一個時間值。這個值可以在副本和分片之間進行比較。從驅動到查詢路由器再到資料承載節點,分片叢集中的每個成員都必須在每條訊息中跟蹤和傳送其最新時間值,從而允許分片之間的每個節點在最新時間保持一致。主節點將最新的時間值賦值給後續的寫入,這為任何一系列相關操作建立了一個因果順序。節點可以使用這個因果順序在執行所需的讀或寫之前等待,以確保它在另一個操作之後發生。

從 MongoDB 3.6 開始,在客戶端會話中開啟因果一致性,保證 read concern 為 majority 的讀操作和 write concern 為 majority 的寫操作的關聯序列具有因果關係。應用程式必須確保一次只有一個執行緒在客戶端會話中執行這些操作。

對於因果相關的操作:

  1. 客戶端開啟客戶端會話,需滿足以下條件:read concern 為 majority,資料已被大多數複製整合員確認並且是持久化的;write concern 為 majority,確認該操作已應用於複製集中大多數可投票成員。

  2. 當客戶端發出 read concern 為 majority 的讀操作和 write concern 為 majority 的寫操作的序列時,客戶端將會話資訊包含在每個操作中。

  3. 對於與會話相關聯的每個 read concern 為 majority 的讀操作和 write concern 為 majority 的寫操作,即使操作出錯,MongoDB 也會返回操作時間和叢集時間。

  4. 相關的客戶端會話會跟蹤這兩個時間欄位。

11.2 線性一致性

線性一致性又被稱為強一致性。CAP 中的 C 指的就是線性一致性。順序一致性中程序只關心各自的順序一樣就行,不需要與全域性時鐘一致。線性一致性是順序一致性的進化版,要求順序一致性的這種偏序(partial order)要達到全序(total order)。

在實現了線性一致性的系統中,任何操作在該系統生效的時刻都對應時間軸上的一個點。把這些時刻連線成一條線,則這條線會一直沿時間軸向前,不會反向。任何操作都需要互相比較決定發生的順序。

以下是一個線性一致性的系統示例:

在以上系統中,寫操作生效之前的任何時刻,讀取值均為 1,生效後均為 2。也就是說,任何讀操作都能讀到某個資料的最近一次寫的資料。系統中的所有程序看到的操作順序,都遵循全域性時鐘的順序。

11.3 read concern

read concern 是針對讀操作的配置。它控制讀取資料的新近度和永續性。read concern 選項控制資料讀取的一致性,分為 local、available、majority、linearizable 四種,它們對一致性的承諾依次由弱到強。其中 linearizable 表示線性一致性,另外 3 種級別代表了 MongoDB 在實現最終一致性時,對訪問延遲和一致性的取捨。

  • local/available: 語義基本一致,都是讀操作直接讀取本地最新的資料,但不保證該資料已被寫入大多數複製整合員。資料可能會被回滾。預設是針對主節點讀。如果讀取操作與因果一致的會話相關聯,則針對副節點讀。唯一的區別在於,avaliable 在分片叢集場景下,為了保證效能,可能返回孤兒文件。

  • majority:讀取 majority committed 的資料,可以保證讀取的資料不會被回滾,但是並不能保證讀到本地最新的資料。受限於不同節點的複製進度,可能會讀取到更舊的值。當寫操作對應的 write concern 配置中 w 的值越大,則寫操作在擴散到更多的複製集節點上之後才返回寫成功,這時通過 read concern 被配置為 majority 的讀操作進行讀取資料,就有更大的概率讀取到最新的資料。

  • linearizable:讀取 majority committed 的資料,但會等待在讀之前所有的 majority committed 確認。它承諾線性一致性,要求讀寫順序和操作真實發生的時間完全一致,既保證能讀取到最新的資料,也保證讀到資料不會被回滾。只對讀取單個文件時有效,且可能導致非常慢的讀,因此總是建議配合使用 maxTimeMS 使用。linearizable 只能用在主節點的讀操作上,考慮到寫操作也只能發生在主節點上,相當於說 MongoDB 的線性一致性被限定在單機環境下實現。實現 linearizable,讀取的資料應該是被 write concern 為 majority 的寫操作寫入到 MongoDB 叢集中的、且持久化到日誌中的資料。如果資料寫入到多數節點後,沒有在日誌中持久化,當這些節點發生重啟恢復,那麼之前通過配置 read concern 為 linearizable 的讀操作讀取到的資料就可能丟失。可以通過 writeConcernMajorityJournalDefault 選項保證指定 write concern 為 majority 的寫操作在日誌中是否持久化。如果寫操作持久化到了日誌中,但是沒有複製到多數節點,在重新選主後,同樣可能會發生資料丟失,違背一致性承諾。

  • snapshot: 與關係型資料庫中的快照隔離級別語義一致。最高隔離級別,接近於 serializable。是伴隨著 MongoDB 4.0 版本中新出現的多文件事務而設計的,只能用在顯式開啟的多文件事務中。如果事務是因果一致會話的一部分,且 write concern 為 majority,則在事務提交後,讀操作可以保證已從多數提交資料的快照中讀取,該快照提供與該事務開始之前的操作的因果一致性。它讀取 majority committed 的資料,但可能讀不到最新的已提交資料。snapshot 保證在事務中的讀不出現髒讀、不可重複讀和幻讀。因為所有的讀都將使用同一個快照,直到事務提交為止該快照才被釋放。

下面借用一張圖展示 majority 和 linearizable 的區別:

11.4 write concern

write concern 是針對寫操作的配置,表示寫請求對獨立 mongod 例項或複製集或分片集進行寫操作的確認級別。它主要是控制資料寫入的永續性。包含三個選項:

  • w:指定了寫操作需要複製並應用到多少個複製整合員才能返回成功,可以為數字或 majority。

    • w:0 表示客戶端不需要收到任何有關寫操作是否執行成功的確認,就直接返回成功,具有最高效能。

    • w:1 表示寫主成功則返回。

    • w: majority 需要收到多數節點(含主節點)關於操作執行成功的確認,具體個數由 MongoDB 根據複製集配置自動得出。w 值越大,對客戶端來說,資料的永續性保證越強,寫操作的延遲越大。w:1 要求事務只要在本地成功提交即可,而 w: majority 要求事務在複製集的多數派節點提交成功。

    • w:all 表示全部節點確認才返回成功。

  • j:表示寫操作對應的修改是否要被持久化到儲存引擎日誌中,只能選填 true 或 false。

    • j:false 表示寫操作到達記憶體即算作成功。

    • j:true 表示寫操作落到 journal 檔案中才算成功。w:0 如果指定 j:true,則優先使用 j:true 來請求獨立或複製集主副本的確認。j:true 本身並不能保證不會因複製集主故障轉移而回滾寫操作。

  • wtimeout:主節點在等待足夠數量的確認時的超時時間,單位為毫秒。超時返回錯誤,但並不代表寫操作已經執行失敗。跟 w 有關,比如:w 是 1,則是帶主節點確認的超時時間;w 為 0,則永不返回錯誤;w 為 majority,表示多數節點確認的超時時間。

12 MongoDB WiredTiger 引擎

從 3.2 版本開始,預設使用 WiredTiger 儲存引擎,每個被建立的表和索引,都對應各自獨立的 WiredTiger 表。為了保證 MongoDB 中資料的永續性,使用 WiredTiger 的寫操作會先寫入 cache,並持久化到 WAL(write ahead log),每 60s 或日誌檔案達到 2 GB,就會做一次 checkpoint,定期將快取資料刷到磁碟,將當前的資料持久化產生一個新的快照。

12.1 WiredTiger 資料結構

MongoDB 採用外掛式儲存引擎架構,實現了服務層和儲存引擎層的解耦,可支援使用多種儲存引擎。除此之外,底層的 WiredTiger 引擎還支援使用 B+ 樹和 LSM 兩種資料結構進行資料管理和儲存,預設使用 B+ 樹結構做儲存。使用 B+ 樹時,WiredTiger 以 page 為單位往磁碟讀寫資料,B+ 樹的每個節點為一個 page,包含三種類型的 page,即 root page、internal page 和 leaf page。

以下是 B+ 樹的結構示意圖:

  • root page 是 B+ 樹的根節點。

  • internal page 是不實際儲存資料的中間索引節點。

  • leaf page 是真正儲存資料的葉子節點,包含頁頭(page header)、塊頭(block header)和真正的資料(key-value 對)。page header 定義了頁的型別、頁儲存的記錄條數等資訊;塊頭定義了頁的校驗和 checksum、塊在磁碟上的定址位置等資訊。真正的資料由一個 WT_ROW 結構的陣列變數進行儲存,每一條記錄還有一個 cell_offset 變數,表示這條記錄在 page 上的偏移量。WiredTiger 有一個用來為 page 分配 block 的塊裝置管理模組。定位文件位置時,先計算 block 的位置,通過 block 的位置找到它對應的 page,再通過 page 找到文件行資料的相對位置。leaf page 為了實現 MVCC,還會維護一個 WT_UPDATE 結構的陣列變數,每條記錄對應一個數組元素,每個元素是一個連結串列,將所有修改值以連結串列形式儲存。

12.2 WiredTiger 壓縮

WiredTiger 支援在記憶體和磁碟上對索引進行壓縮,通過字首壓縮的方式減少 RAM 的使用。

12.3 WiredTiger 一致性原理

WiredTiger 使用了二級快取 WiredTiger Cache 和 File System Cache 來保證 Disk 上 Database File 資料的最終一致性。

  • WiredTiger Cache:通過 B+ 樹快取未壓縮的資料,並通過淘汰演算法確保記憶體佔用在合理範圍內。

  • File System Cache:由作業系統管理,快取壓縮後的資料。

  • Database File:儲存壓縮後的資料。每個 WiredTiger 表對應一個獨立的磁碟檔案。磁碟檔案劃分成多個按 4 KB 對齊的 extent,並通過 3 個連結串列來管理:available list(可分配的 extent 列表) ,discard list(廢棄的 extent 列表)和 allocate list(當前已分配的 extent 列表)

12.4 WiredTiger MVCC

WiredTiger 使用 MVCC 進行寫操作,多個客戶端可以併發同時修改集合的不同文件。事務開始時,WiredTiger 為操作提供反映記憶體資料的一致檢視的時間點快照。MVCC 通過非鎖機制進行讀寫操作,是一種樂觀併發控制模式。WiredTiger 僅在全域性、資料庫和集合級別使用意向鎖。當儲存引擎檢測到兩個操作之間存在衝突時,將引發寫衝突,從而導致 MongoDB 自動重試該操作。

使用 WiredTiger,如果沒有 journal 記錄,MongoDB 能且僅能從最後一個檢查點恢復。如果需要恢復最後一次 checkpoint 之後所做的更改,那麼開啟日誌是必要的。

13 MongoDB 資料讀寫

13.1 讀偏好 ReadPerference

預設情況下,客戶端讀取複製集主節點上的資料。但客戶端可以指定一個 read perference 改變讀取行為,以便對複製集上的其他節點進行直接讀操作。可選值包括:

13.2 在複製集上進行讀寫操作

讀操作由客戶端指定的 read prefenence 選項決定。

所有的寫操作都在集合的主節點上執行。主節點執行寫操作並將操作記錄在操作日誌或 oplog 上。oplog 是 local 資料庫的一個集合,叫 local.oplog.rs。這是一個 capped collection,是固定大小,迴圈使用的。oplog 是對資料集的可重複操作序列,其記錄的每個操作都是冪等的,也就是說,對目標資料集應用一次或多次 oplog 操作都會產生相同的結果。從節點從上一次結束時間點建立 tailable cursor,不斷的從同步源拉取 oplog 並重放應用到自身,且嚴格按照原始的寫順序對給定的文件執行寫操作。mongodb 使用多執行緒批量執行寫操作來提高併發,根據文件 id 進行分批執行。MongoDB 為了提升同步效率,將拉取 oplog 以及重放 oplog 分到了不同的執行緒來執行。

大致的寫流程如下:

  • producer thread 不斷的從主節點上拉取 oplog,並把它加入到一個 blockQueue 裡,blockQueue 不是無限容量的,當超過最大儲存容量,producer thread 就必須等到 oplog 被 replBatcher thread 從佇列裡取出後才能繼續拉取 oplog。

  • replBatcher thread 不斷從 producer thread 對應的 blockQueue 裡取出 oplog,放到自己的記憶體佇列裡,記憶體佇列也不是無限容量,一旦滿了,就需要等待被 oplogApplication thread 消費。

  • oplogApplication thread 不斷取出 replBatch thread 記憶體佇列裡的所有元素,分散到不同的 replWriter thread,由 replWriter thread 根據 oplog 進行寫操作。等待所有 oplog 都應用完畢,oplogApplication hread 將所有的 oplog 順序寫入到 local.oplog.rs 集合。

13.3 在分片叢集上進行讀寫操作

對於分片叢集,需要一個 mongos 例項提供客戶端應用程式和分片叢集之間的介面。在客戶端看來,該 mongos 例項的行為與其他 MongoDB 例項是相同的。客戶端向路由節點 mongos 傳送請求,由該節點決定往哪個分片進行讀寫。對於讀取操作,若能定向到特定分片時,效率最高。一般而言,分片集合的查詢應包含集合的分片鍵,以避免低效的全分片查詢。在這種情況下,mongos 可以使用配置資料庫 config 中的叢集元資料資訊,將查詢路由到分片。如果查詢不包含分片鍵,則 mongos 節點必須將查詢定向到叢集中的所有分片,然後在 mongos 上聚合所有分片的查詢結果,返回給客戶端。

對於寫操作, mongos 定向到負責資料集特定部分的分片,config 資料庫上有集合相關的分片鍵資訊,mongos 從中讀取配置,並路由寫操作到適當的分片。

14 MongoDB 事務

14.1 ACID 特性

MongoDB 在一定程度上支援了事務的 ACID 特性。MongoDB 4.0 版本開始支援複製集上的多文件事務,4.2 版本引入了分散式事務,它增加了對分片群集上多文件事務的支援。

  • 原子性:成功提交事務時,事務中所有資料更新將完全進行成功,並在事務外部可見。在提交事務之前,事務外部看不到在事務中進行的任何資料更新。當事務被打斷或終止時,事務中進行的所有資料更新都將被丟棄,對事務外部完全不可見。但是當事務寫入多個分片時,並非所有事務外的讀操作都需要等待事務提交後所有分片上資料完全可見。

  • 隔離性:MongoDB 提供 snapshot 隔離級別,在事務開始建立一個 WiredTiger snapshot,然後在整個事務過程中,便可以使用這個快照提供事務讀。

  • 永續性:事務使用 write concern 指定 {j: true} 時,MongoDB 會保證事務日誌提交才返回,即使發生 crash,也能根據事務日誌來恢復;而如果沒有指定 {j: true} 級別,即使事務提交成功了,在故障恢復之後,事務的也可能被回滾掉。

  • 一致性:參考前文提到的 MongoDB 一致性。

14.2 事務的使用限制

  • 僅 WiredTiger 引擎支援事務。

  • 對集合的建立和刪除操作,不能出現在事務中。

  • 對索引的建立和刪除操作,不能出現在事務中。

  • 不能對系統級別的資料庫和集合進行操作。

  • 預設情況下,事務大小的限制在 16 MB。

  • 預設情況下,事務操作整體不允許超過 60 秒。

  • 事務不能在 session 外執行。

  • 一個 session 只能執行一個事務,多個 session 可以並行執行事務。

  • 不能對 capped collection 進行操作。

  • 不能使用 explain 操作做查詢分析。

14.3 事務與 read concern

事務中的操作使用事務級別的 read concern。事務內部忽略在集合和資料庫級別設定的任何 read concern。事務支援設定 read concern 為 local、majority 和 snapshot 其中之一。

  • 當 read concern 為 local 時,可讀取節點可用的最新資料,但資料可能回滾。對於分片群集上的事務,local 不能保證資料是從整個分片的同一快照檢視獲取。

  • 當 read concern 為 majority 時,如果在提交事務時指定了 write concern 為 majority 級別,則返回大多數副本成員已確認的資料(即無法回滾資料)。如果事務未指定 write concern 為 majority 級別,則不保證讀操作可以讀取多數提交的資料。對於分片群集上的事務,不能保證資料是從整個分片的同一快照檢視中獲取。

  • 當 read concern 為 snapshot 時,如果在提交事務時指定了 write concern 為 majority 級別,則從大多數已提交資料的快照中返回資料。如果事務未指定 write concern 為 majority 級別,則不保證讀操作使用了 majority commited 的資料的快照。對於分片群集上的事務,snapshot 跨分片同步。

14.4 事務與 write concern

事務使用事務級別的 write concern 來進行寫操作提交,可以通過配置 w 選項設定節點個數,來決定事務寫入是否成功,預設情況下為 1。

  • w:0 表示事務寫入不關注是否成功,預設為成功。

  • w:1 表示事務寫入到主節點就開始往客戶端傳送確認寫入成功。

  • w:majority 表示大多數節點成功原則,例如一個複製集 3 個節點,2 個節點成功就認為本次事務寫入成功。

  • w:all 表示所有節點都寫入成功,才認為事務提交成功。

  • j:false 表示寫操作到達記憶體就算事務成功。

  • j:true 表示寫操作只有記錄到日誌檔案才算事務成功。

  • wtimeout: 寫入超時時間,過期表示事務失敗。

15 MongoDB Change Stream

15.1 變更流使用場景

MongoDB 3.6 引入了 change stream(變更流)。它的使用場景包括:

  • 資料同步:多個 MongoDB 叢集之間的增量資料同步。

  • 審計:對 MongoDB 操作進行審計、監控。

  • 資料訂閱:外部程式訂閱 MongoDB 的資料變更,可離線資料同步、計算或分析等。

15.2 變更流特點

change stream 允許外部程式訪問實時資料更改,而不會增加 MongoDB 基礎操作的複雜性,也不會導致 oplog 延遲的風險。應用程式可以使用 change stream 來訂閱單個集合、資料庫或整個叢集中的所有資料變更。若要開啟 change stream,必須使用 WiredTiger 儲存引擎。

change stream 可應用於複製集和分片集。應用於複製集時,可以在複製集中任意一個節點上開啟監聽;應用於分片集時,則只能在 mongos 上開啟監聽。在 mongos 上發起監聽,是利用全域性邏輯時鐘提供了整個分片上變更的總體排序,確保監聽事件可以按接收到的順序安全地解釋。mongos 會一直檢查每個分片,檢視每個分片是否存在最新的變更。如果多個分片上一直很少出現變更,則可能會對 change stream 的響應時間產生負面影響,因為 mongos 仍必須檢查這些冷分片保持總體有序。

15.3 變更流監聽事件型別

從 change stream 中能監聽到的變更事件包括:insert、update、replace、delete、drop、rename、dropDatabase 和 invalidate。

15.4 變更流故障恢復

MongoDB 4.0 之後,可以通過指定 startAtOperationTime 來控制從某個特定的時間點開啟監聽,但該時間點必須在所選擇節點的有效 oplog 時間範圍內。change stream 監聽返回的欄位中有個 _id 欄位,表示的是 resume token,這是唯一標誌 change stream 流中的位置的欄位。

如果 change stream 監聽比中止後需要繼續監聽,那麼可指定 resumeAfter 恢復訂閱。指定 resumeAfter 為 change stream 中斷處的 _id 欄位即可。

當監聽的集合發生 rename、drop 或 dropDatabase 事件,就會導致 invalidate 事件;當監聽的資料庫出現 dropDatabase 事件,也會導致無效事件。invalidate 事件後 change stream 的遊標會被關閉,這時就需要使用 resumeAfter 選項來恢復 change stream 的監聽,在 4.2 版本後也可以通過 startAfter 選項建立新的更改流來恢復監聽。

15.5 變更流使用限制

  • change stream 無法配置到系統庫或者 system.xxx 表上。

  • change stream 依賴於 oplog,因此中斷時間不可超過 oplog 回收的最大時間窗。

16 MongoDB 效能問題定位方式

  • 可以為 mongod 例項啟用資料庫分析。資料庫分析器既可以在例項上啟用,也可以在單個數據庫層面上啟用。它收集在例項上執行的 CRUD 操作、遊標、命令、配置等詳細資訊,並將它收集的所有資料寫到 system.profile 集合。這是一個 capped collection,預設情況下,system.profile 容量大小為 4M。開啟實時資料庫分析往往伴隨著副作用,請謹慎使用。

  • 使用 db.currentOp() 操作。它返回一個文件,其中包含有關資料庫例項正在進行的操作的資訊。

  • 使用 db.serverStatus() 命令。它返回一個文件,提供資料庫狀態的概述,通過它可以收集有關該例項的統計資訊。

  • 使用 explain 來評估查詢效能,例如 cursor.explain() 或 db.collection.explain() 方法可以用來返回關於查詢執行的資訊。

  • 借用一些商業工具,比如 MongoDB Ops Manager、Percona 等。

騰訊程式設計師影片號最新影片

歡迎點贊