MongoDB 儲存引擎與內部原理

碼洞發表於2022-12-05

MongoDB 儲存引擎與內部原理

一、儲存引擎(Storage)

MongoDB 儲存引擎與內部原理

mongodb 3.0預設儲存引擎為MMAPV1,還有一個新引擎wiredTiger可選,或許可以提高一定的效能。

mongodb中有多個databases,每個database可以建立多個collections,collection是底層資料分割槽(partition)的單位,每個collection都有多個底層的資料檔案組成。(參見下文data files儲存原理)

MongoDB 儲存引擎與內部原理

wiredTiger引擎:3.0新增引擎,官方宣稱在read、insert和複雜的update下具有更高的效能。所以後續版本,我們建議使用wiredTiger。所有的write請求都基於“文件級別”的lock,因此多個客戶端可以同時更新一個colleciton中的不同文件,這種更細顆粒度的lock,可以支撐更高的讀寫負載和併發量。因為對於production環境,更多的CPU可以有效提升wireTiger的效能,因為它是的IO是多執行緒的。

MongoDB 儲存引擎與內部原理

wiredTiger不像MMAPV1引擎那樣儘可能的耗盡記憶體,它可以透過在配置檔案中指定“cacheSizeGB”引數設定引擎使用的記憶體量,此記憶體用於快取工作集資料(索引、namespace,未提交的write,query緩衝等)。

MongoDB 儲存引擎與內部原理

journal就是一個預寫事務日誌,來確保資料的永續性,wiredTiger每隔60秒(預設)或者待寫入的資料達到2G時,mongodb將對journal檔案提交一個checkpoint(檢測點,將記憶體中的資料變更flush到磁碟中的資料檔案中,並做一個標記點,表示此前的資料表示已經持久儲存在了資料檔案中,此後的資料變更存在於記憶體和journal日誌)。對於write操作,首先被持久寫入journal,然後在記憶體中儲存變更資料,條件滿足後提交一個新的檢測點,即檢測點之前的資料只是在journal中持久儲存,但並沒有在mongodb的資料檔案中持久化,延遲持久化可以提升磁碟效率,如果在提交checkpoint之前,mongodb異常退出,此後再次啟動可以根據journal日誌恢復資料。journal日誌預設每個100毫秒同步磁碟一次,每100M資料生成一個新的journal檔案,journal預設使用了snappy壓縮,檢測點建立後,此前的journal日誌即可清除。mongod可以禁用journal,這在一定程度上可以降低它帶來的開支;對於單點mongod,關閉journal可能會在異常關閉時丟失checkpoint之間的資料(那些尚未提交到磁碟資料檔案的資料);對於replica set架構,永續性的保證稍高,但仍然不能保證絕對的安全(比如replica set中所有節點幾乎同時退出時)。

MongoDB 儲存引擎與內部原理

MMAPv1引擎:mongodb原生的儲存引擎,比較簡單,直接使用系統級的記憶體對映檔案機制(memory mapped files),一直是mongodb的預設儲存引擎,對於insert、read和in-place update(update不導致文件的size變大)效能較高;不過MMAPV1在lock的併發級別上,支援到collection級別,所以對於同一個collection同時只能有一個write操作執行,這一點相對於wiredTiger而言,在write併發性上就稍弱一些。對於production環境而言,較大的記憶體可以使此引擎更加高效,有效減少“page fault”頻率,但是因為其併發級別的限制,多核CPU並不能使其受益。此引擎將不會使用到swap空間,但是對於wiredTiger而言需要一定的swap空間。(核心:對於大檔案MAP操作,比較忌諱的就是在檔案的中間修改資料,而且導致檔案長度增長,這會涉及到索引引用的大面積調整)

為了確保資料的安全性,mongodb將所有的變更操作寫入journal並間歇性的持久到磁碟上,對於實際資料檔案將延遲寫入,和wiredTiger一樣journal也是用於資料恢復。所有的記錄在磁碟上連續儲存,當一個document尺寸變大時,mongodb需要重新分配一個新的記錄(舊的record標記刪除,新的記record在檔案尾部重新分配空間),這意味著mongodb同時還需要更新此文件的索引(指向新的record的offset),與in-place update相比,將消耗更多的時間和儲存開支。由此可見,如果你的mongodb的使用場景中有大量的這種update,那麼或許MMAPv1引擎並不太適合,同時也反映出如果document沒有索引,是無法保證document在read中的順序(即自然順序)。3.0之後,mongodb預設採用“Power of 2 Sized Allocations”,所以每個document對應的record將有實際資料和一些padding組成,這padding可以允許document的尺寸在update時適度的增長,以最小化重新分配record的可能性。此外重新分配空間,也會導致磁碟碎片(舊的record空間)。

Power of 2 Sized Allocations:預設情況下,MMAPv1中空間分配使用此策略,每個document的size是2的次冪,比如32、64、128、256...2MB,如果文件尺寸大於2MB,則空間為2MB的倍數(2M,4M,6M等)。這種策略有2種優勢,首先那些刪除或者update變大而產生的磁碟碎片空間(尺寸變大,意味著開闢新空間儲存此document,舊的空間被mark為deleted)可以被其他insert重用,再者padding可以允許文件尺寸有限度的增長,而無需每次update變大都重新分配空間。此外,mongodb還提供了一個可選的“No padding Allocation”策略(即按照實際資料尺寸分配空間),如果你確信資料絕大多數情況下都是insert、in-place update,極少的delete,此策略將可以有效的節約磁碟空間,看起來資料更加緊湊,磁碟利用率也更高。

備註:mongodb 3.2+之後,預設的儲存引擎為“wiredTiger”,大量最佳化了儲存效能,建議升級到3.2+版本。

MongoDB 儲存引擎與內部原理

二、Capped Collections

一種特殊的collection,其尺寸大小是固定值,類似於一個可迴圈使用的buffer,如果空間被填滿之後,新的插入將會覆蓋最舊的文件,我們通常不會對Capped進行刪除或者update操作,所以這種型別的collection能夠支撐較高的write和read,通常情況下我們不需要對這種collection構建索引,因為insert是append(insert的資料儲存是嚴格有序的)、read是iterator方式,幾乎沒有隨機讀;在replica set模式下,其oplog就是使用這種colleciton實現的。Capped Collection的設計目的就是用來儲存“最近的”一定尺寸的document。

MongoDB 儲存引擎與內部原理

Capped Collection在語義上,類似於“FIFO”佇列,而且是有界佇列。適用於資料快取,訊息型別的儲存。

MongoDB 儲存引擎與內部原理

Capped支援update,但是我們通常不建議,如果更新導致document的尺寸變大,操作將會失敗,只能使用in-place update,而且還需要建立合適的索引。在capped中使用remove操作是允許的。autoIndex屬性表示預設對_id欄位建立索引,我們推薦這麼做。在上文中我們提到了Tailable Cursor,就是為Capped而設計的,效果類似於“tail -f ”。

MongoDB 儲存引擎與內部原理

三、資料模型(Data Model)

上文已經描述過,mongodb是一個模式自由的NOSQL,不像其他RDBMS一樣需要預先定義Schema而且所有的資料都“整齊劃一”,mongodb的document是BSON格式,鬆散的,原則上說任何一個Colleciton都可以儲存任意結構的document,甚至它們的格式千差萬別,不過從應用角度考慮,包括業務資料分類和查詢最佳化機制等,我們仍然建議每個colleciton中的document資料結構應該比較接近。

對於有些update,比如對array新增元素等,會導致document尺寸的增加,無論任何儲存系統包括MYSQL、Hbase等,對於這種情況都需要額外的考慮,這歸結於磁碟空間的分配是連續的(連續意味著讀取效能將更高,儲存檔案空間通常是預分配固定尺寸,我們需要儘可能的利用磁碟IO的這種優勢)。對於MMAPV1引擎,如果文件尺寸超過了原分配的空間(上文提到Power of 2 Allocate),mongodb將會重新分配新的空間來儲存整個文件(舊文件空間回收,可以被後續的insert重用)。

document模型的設計與儲存,需要兼顧應用的實際需要,否則可能會影響效能。mongodb支援內嵌document,即document中一個欄位的值也是一個document,可以形成類似於RDBMS中的“one-to-one”、“one-to-many”,只需要對reference作為一個內嵌文件儲存即可。這種情況就需要考慮mongodb儲存引擎的機制了,如果你的內嵌文件(即reference文件)尺寸是動態的,比如一個user可以有多個card,因為card數量無法預估,這就會導致document的尺寸可能不斷增加以至於超過“Power of 2 Allocate”,從而觸發空間重新分配,帶來效能開銷,這種情況下,我們需要將內嵌文件單獨儲存到一個額外的collection中,作為一個或者多個document儲存,比如把card列表儲存在card collection中。“one-to-one”的情況也需要個別考慮,如果reference文件尺寸較小,可以內嵌,如果尺寸較大,建議單獨儲存。此外內嵌文件還有個優點就是write的原子性,如果使用reference的話,就無法保證了。

MongoDB 儲存引擎與內部原理

索引:提高查詢效能,預設情況下_id欄位會被建立唯一索引;因為索引不僅需要佔用大量記憶體而且也會佔用磁碟,所以我們需要建立有限個索引,而且最好不要建立重複索引;每個索引需要8KB的空間,同時update、insert操作會導致索引的調整,會稍微影響write的效能,索引只能使read操作收益,所以讀寫比高的應用可以考慮建立索引。

大集合拆分:比如一個用於儲存log的collection,log分為有兩種“dev”、“debug”,結果大致為{"log":"dev","content":"...."},{"log":"debug","content":"....."}。這兩種日誌的document個數比較接近,對於查詢時,即使給log欄位建立索引,這個索引也不是高效的,所以可以考慮將它們分別放在2個Collection中,比如:log_dev和log_debug。

MongoDB 儲存引擎與內部原理

資料生命週期管理:mongodb提供了expire機制,即可以指定文件儲存的時長,過期後自動刪除,即TTL特性,這個特性在很多場合將是非常有用的,比如“驗證碼保留15分鐘有效期”、“訊息儲存7天”等等,mongodb會啟動一個後臺執行緒來刪除那些過期的document。需要對一個日期欄位建立“TTL索引”,比如插入一個文件:{"check_code":"101010",$currentDate:{"created":true}}},其中created欄位預設值為系統時間Date;然後我們對created欄位建立TTL索引。

我們向collection中insert文件時,created的時間為系統當前時間,其中在creatd欄位上建立了“TTL”索引,索引TTL為15分鐘,mongodb後臺執行緒將會掃描並檢測每條document的(created時間 + 15分鐘)與當前時間比較,如果發現過期,則刪除索引條目(連帶刪除document)。

某些情況下,我們可能需要實現“在某個指定的時刻過期”,我們只需要將上述文件和索引變通改造即可,即created指定為“目標時間”,expiredAfter指定為0。

四、架構模式

MongoDB 儲存引擎與內部原理

Replica set:複製集,mongodb的架構方式之一 ,通常是三個對等的節點構成一個“複製集”叢集,有“primary”和secondary等多角色(稍後詳細介紹),其中primary負責讀寫請求,secondary可以負責讀請求,這由配置決定,其中secondary緊跟primary並應用write操作;如果primay失效,則叢集進行“多數派”選舉,選舉出新的primary,即failover機制,即HA架構。複製集解決了單點故障問題,也是mongodb垂直擴充套件的最小部署單位,當然sharding cluster中每個shard節點也可以使用Replica set提高資料可用性。

MongoDB 儲存引擎與內部原理

Sharding cluster:分片叢集,資料水平擴充套件的手段之一;replica set這種架構的缺點就是“叢集資料容量”受限於單個節點的磁碟大小,如果資料量不斷增加,對它進行擴容將會非常苦難的事情,所以我們需要採用Sharding模式來解決這個問題。將整個collection的資料將根據sharding key被sharding到多個mongod節點上,即每個節點持有collection的一部分資料,這個叢集持有全部資料,原則上sharding可以支撐數TB的資料。

系統配置:

  1. 建議mongodb部署在linux系統上,較高版本,選擇合適的底層檔案系統(ext4),開啟合適的swap空間

  2. 無論是MMAPV1或者wiredTiger引擎,較大的記憶體總能帶來直接收益。

  3. 對資料儲存檔案關閉“atime”(檔案每次access都會更改這個時間值,表示檔案最近被訪問的時間),可以提升檔案訪問效率。

  4. ulimit引數調整,這個在基於網路IO或者磁碟IO操作的應用中,通常都會調整,上調系統允許開啟的檔案個數(ulimit -n 65535)。

MongoDB 儲存引擎與內部原理

五、資料檔案儲存原理(Data Files storage,MMAPV1引擎)

1、Data Files

mongodb的資料將會儲存在底層檔案系統中,比如我們dbpath設定為“/data/db”目錄,我們建立一個database為“test”,collection為“sample”,然後在此collection中插入數條documents。我們檢視dbpath下生成的檔案列表:

ls -lh
-rw-------  1 mongo  mongo    16M 11  6 17:24 test.0
-rw-------  1 mongo  mongo    32M 11  6 17:24 test.1
-rw-------  1 mongo  mongo    64M 11  6 17:24 test.2
-rw-------  1 mongo  mongo   128M 11  6 17:24 test.3
-rw-------  1 mongo  mongo   256M 11  6 17:24 test.4
-rw-------  1 mongo  mongo   512M 11  6 17:24 test.5
-rw-------  1 mongo  mongo   512M 11  6 17:24 test.6
-rw-------  1 mongo  mongo    16M 11  6 17:24 test.ns

可以看到test這個資料庫目前已經有6個資料檔案(data files),每個檔案以“database”的名字 + 序列數字組成,序列號從0開始,逐個遞增,資料檔案從16M開始,每次擴張一倍(16M、32M、64M、128M...),在預設情況下單個data file的最大尺寸為2G,如果設定了smallFiles屬性(配置檔案中)則最大限定為512M;mongodb中每個database最多支援16000個資料檔案,即約32T,如果設定了smallFiles則單個database的最大資料量為8T。如果你的database中的資料檔案很多,可以使用directoryPerDB配置項將每個db的資料檔案放置在各自的目錄中。當最後一個data file有資料寫入後,mongodb將會立即預分配下一個data file,可以透過“--nopreallocate”啟動命令引數來關閉此選項。

MongoDB 儲存引擎與內部原理

一個database中所有的collections以及索引資訊會分散儲存在多個資料檔案中,即mongodb並沒有像SQL資料庫那樣,每個表的資料、索引分別儲存;資料分塊的單位為extent(範圍,區域),即一個data file中有多個extents組成,extent中可以儲存collection資料或者indexes

MongoDB 儲存引擎與內部原理

資料,一個extent只能儲存同一個collection資料不同的collections資料分佈在不同的extents中,indexes資料也儲存在各自的extents中;最終,一個collection有一個或者多個extents構成,最小size為8K,最大可以為2G,依次增大;它們分散在多個data files中。對於一個data file而言,可能包含多個collection的資料,即由多個不同collections的extents、index extents混合構成。每個extent包含多條documents(或者index entries),每個extent的大小可能不相等,但一個extent不會跨越2個data files。

MongoDB 儲存引擎與內部原理

有人肯定疑問:一個collection中有哪些extents,這種資訊mongodb存在哪裡?在每個database的namespace檔案中,比如test.ns檔案中,每個collection只儲存了第一個extent的位置資訊,並不儲存所有的extents列表,但每個extent都維護者一個連結串列關係,即每個extent都在其header資訊中記錄了此extent的上一個、下一個extent的位置資訊,這樣當對此collection進行scan操作時(比如全表掃描),可以提供很大的便利性。

我們可以透過db.stats()指令檢視當前database中extents的資訊:

> use test
switched to db test
> db.stats();
{
   "db" : "test",
   "collections" : 3,  ##collection的個數
   "objects" : 1000006, ##documents總條數
   "avgObjSize" : 495.9974400153599, ##record的平均大小,單位byte
   "dataSize" : 496000416, ##document所佔空間的總量
   "storageSize" : 629649408, ##
   "numExtents" : 18,  ##extents個數
   "indexes" : 2,
   "indexSize" : 108282944,
   "fileSize" : 1006632960,
   "nsSizeMB" : 16, ##namespace檔案大小
   "extentFreeList" : {   ##尚未使用(已分配尚未使用、已刪除但尚未被重用)的extent列表
       "num" : 0,
       "totalSize" : 0
   },
   "dataFileVersion" : {
       "major" : 4,
       "minor" : 22
   },
   "ok" : 1

列表資訊中有幾個欄位簡單介紹一下:

  1. dataSize:documents所佔的空間總量,mongodb將會為每個document分配一定空間用於儲存資料,每個document所佔空間包括“文件實際大小” + “padding”,對於MMAPV1引擎,mongodb預設採用了“Power of 2 Sized Allocations”策略,這也意味著通常會有padding,不過如果你的document不會被update(或者update為in-place方式,不會導致文件尺寸變大),可以在在createCollection是指定noPadding屬性為true,這樣dataSize的大小就是documents實際大小;當documents被刪除後,將導致dataSize減小;不過如果在原有document的空間內(包括其padding空間)update(或者replace),則不會導致dataSize的變大,因為mongodb並沒有分配任何新的document空間。

  2. storageSize:所有collection的documents佔用總空間,包括那些已經刪除的documents所佔的空間,為儲存documents的extents所佔空間總和。文件的刪除或者收縮不會導致storageSize變小。

  3. indexSize:所用collection的索引資料的大小,為儲存indexes的extents所佔空間的總和。

  4. fileSize:為底層所有data files的大小總和,但不包括namespace檔案。為storageSize、indexSize、以及一些尚未使用的空間等等。當刪除database、collections時會導致此值變小。

此外,如果你想檢視一個collection中extents的分配情況,可以使用

db.<collection名稱>.stats(),結構與上述類似;如果你希望更細緻的瞭解collection中extents的全部資訊,則可以使用db.<collection名稱>.validate(),此方法接收一個boolean值,表示是否檢視明細,這個指令會scan全部的data files,因此比較耗時:

 > db.sample.validate(true);
{
   "ns" : "test.sample",
   "datasize" : 496000000,
   "nrecords" : 1000000,
   "lastExtentSize" : 168742912,
   "firstExtent" : "0:5000 ns:test.sample",
   "lastExtent" : "3:a05f000 ns:test.sample",
   "extentCount" : 16,
   "extents" : [
       {
           "loc" : "0:5000",
           "xnext" : "0:49000",
           "xprev" : "null",
           "nsdiag" : "test.sample",
           "size" : 8192,
           "firstRecord" : "0:50b0",
           "lastRecord" : "0:6cb0"
       },
       ...
       ]
       ...
}

可以看到extents在邏輯上是連結串列形式,以及每個extent的資料量、以及所在data file的offset位置。具體參見validate - MongoDB Manual 3.6

MongoDB 儲存引擎與內部原理

從上文中我們已經得知,刪除document會導致磁碟碎片,有些update也會導致磁碟碎片,比如update導致文件尺寸變大,進而超過原來分配的空間;當有新的insert操作時,mongodb會檢測現有的extents中是否合適的碎片空間可以被重用,如果有,則重用這些fragment,否則分配新的儲存空間。磁碟碎片,對write操作有一定的效能影響,而且會導致磁碟空間浪費;如果你需要刪除某個collection中大部分資料,則可以考慮將有效資料先轉存到新的collection,然後直接drop()原有的collection。或者使用db.runCommand({compact: '<collection>'})。

如果你的database已經執行一段時間,資料已經有很大的磁碟碎片(storageSize與dataSize比較),可以透過mongodump將指定database的所有資料匯出,然後將原有的db刪除,再透過mongorestore指令將資料重新匯入。(同compact,這種操作需要停機維護)

mongod中還有2個預設的database,系統級的,“admin”和“local”;它們的儲存原理同上,其中“admin”用於儲存“使用者授權資訊”,比如每個database中使用者的role、許可權等;“local”即為本地資料庫,我們常說的oplog(replication架構中使用,類似與binlog)即儲存在此資料庫中。

2、Namespace檔案

對於namespace檔案,比如“test.ns”檔案,預設大小為16M,此檔案中主要用於儲存“collection”、index的命名資訊,比如collection的“屬性”資訊、每個索引的屬性型別等,如果你的database中需要儲存大量的collection(比如每一小時生成一個collection,在資料分析應用中),那麼我們可以透過配置檔案“nsSize”選項來指定。

3、journal檔案

journal日誌為mongodb提供了資料保障能力,它本質上與mysql binlog沒有太大區別,用於當mongodb異常crash後,重啟時進行資料恢復;這歸結於mongodb的資料持久寫入磁碟是滯後的。預設情況下,“journal”特性是開啟的,特別在production環境中,我們沒有理由來關閉它。(除非,資料丟失對應用而言,是無關緊要的)

一個mongodb例項中所有的databases共享journal檔案。

MongoDB 儲存引擎與內部原理

對於write操作而言,首先寫入journal日誌,然後將資料在記憶體中修改(mmap),此後後臺執行緒間歇性的將記憶體中變更的資料flush到底層的data files中,時間間隔為60秒(參見配置項“syncPeriodSecs”);write操作在journal檔案中是有序的,為了提升效能,write將會首先寫入journal日誌的記憶體buffer中,當buffer資料達到100M或者每隔100毫秒,buffer中的資料將會flush到磁碟中的journal檔案中;如果mongodb異常退出,將可能導致最多100M資料或者最近100ms內的資料丟失,flush磁碟的時間間隔配置項“commitIntervalMs”決定,預設為100毫秒。mongodb之所以不能對每個write都將journal同步磁碟,這也是對效能的考慮,mysql的binlog也採用了類似的權衡方式。開啟journal日誌功能,將會導致write效能有所降低,可能降低5~30%,因為它直接加劇了磁碟的寫入負載,我們可以將journal日誌單獨放置在其他磁碟驅動器中來提高寫入併發能力(與data files分別使用不同的磁碟驅動器)。

如果你希望資料儘可能的不丟失,可以考慮:

  1. 減小commitIntervalMs的值

  2. 每個write指定“write concern”中指定“j”引數為true

  3. 最佳手段就是採用“replica set”架構模式,透過資料備份方式解決,同時還需要在“write concern”中指定“w”選項,且保障級別不低於“majority”。

MongoDB 儲存引擎與內部原理

參見mongodb複製集最終我們需要在“寫入效能”和“資料一致性”兩個方面權衡,即CAP理論。


根據write併發量,journal日誌檔案為1G,如果指定了smallFiles配置項,則最大為128M,和data files一樣journal檔案也採用了“preallocated”方式,journal日誌儲存在dbpath下“journal”子目錄中,一般會有三個journal檔案,每個journal檔案格式類似於“j._<序列數字>”。並不是每次buffer flush都生成一個新的journal日誌,而是當前journal檔案即將滿時會預建立一個新的檔案,journal檔案中儲存了write操作的記錄,每條記錄中包含write操作內容之外,還包含一個“lsn”(last sequence number),表示此記錄的ID;此外我們會發現在journal目錄下,還有一個“lsn”檔案,這個檔案非常小,只儲存了一個數字,當write變更的資料被flush到磁碟中的data files後,也意味著這些資料已經持久化了,那麼它們在“異常恢復”時也不需要了,那麼其對應的journal日誌將可以刪除,“lsn”檔案中記錄的就是write持久化的最後一個journal記錄的ID,此ID之前的write操作已經被持久寫入data files,此ID之前的journal在“異常恢復”時則不需要關注;如果某個journal檔案中最大 ID小於“lsn”,則此journal可以被刪除或者重用。

本文轉載自 ITeye(https://www.iteye.com/blog/shift-alt-ctrl-2255580)

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31561269/viewspace-2663773/,如需轉載,請註明出處,否則將追究法律責任。

相關文章