MongoDB 基础浅谈
作者:hazenweng,騰訊 QQ 音樂后臺開發工程師
MongoDB 作為一款優秀的基于分布式文件存儲的 NoSQL 數據庫,在業界有著廣泛的應用。下文對 MongoDB 的一些基礎概念進行簡單介紹。
1 MongoDB 特點
面向集合存儲:MongoDB 是面向集合的,數據以 collection 分組存儲。每個 collection 在數據庫中都有唯一的名稱。
模式自由:集合的概念類似 MySQL 里的表,但它不需要定義任何模式。
結構松散:對于存儲在數據庫中的文檔,不需要設置相同的字段,并且相同的字段不需要相同的數據類型,不同結構的文檔可以存在同一個 collection 里。
高效的二進制存儲:存儲在集合中的文檔,是以鍵值對的形式存在的。鍵用于唯一標識一個文檔,一般是 ObjectId 類型,值是以 BSON 形式存在的。BSON = Binary JSON, 是在 JSON 基礎上加了一些類型及元數據描述的格式。
支持索引:可以在任意屬性上建立索引,包含內部對象。MongoDB 的索引和 MySQL 的索引基本一樣,可以在指定屬性上創建索引以提高查詢的速度。除此之外,MongoDB 還提供創建基于地理空間的索引的能力。
支持 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)組成的, 每個管道的工作流程是:
接受一系列原始數據文檔
對這些文檔進行一系列運算
結果文檔輸出給下一個 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 的寫操作的關聯序列具有因果關系。應用程序必須確保一次只有一個線程在客戶端會話中執行這些操作。
對于因果相關的操作:
客戶端開啟客戶端會話,需滿足以下條件:read concern 為 majority,數據已被大多數復制集成員確認并且是持久化的;write concern 為 majority,確認該操作已應用于復制集中大多數可投票成員。
當客戶端發出 read concern 為 majority 的讀操作和 write concern 為 majority 的寫操作的序列時,客戶端將會話信息包含在每個操作中。
對于與會話相關聯的每個 read concern 為 majority 的讀操作和 write concern 為 majority 的寫操作,即使操作出錯,MongoDB 也會返回操作時間和集群時間。
相關的客戶端會話會跟蹤這兩個時間字段。
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 等。
騰訊程序員視頻號最新視頻
歡迎點贊
總結
以上是生活随笔為你收集整理的MongoDB 基础浅谈的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微信搜一搜在线检索技术演进复盘
- 下一篇: 速抢中秋月饼和红包封面!