數據庫

MongoDB 存儲引擎與內部原理

廣告
廣告

一、存儲引擎(Storage)

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

mongodb中有多個databases,每個database可以創建多個collections,collection是底層數據分區(partition)的單位,每個collection都有多個底層的數據文件組成。(參見下文data files存儲原理)

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

wiredTiger不像MMAPV1引擎那樣盡可能的耗盡內存,它可以通過在配置文件中指定“cacheSizeGB”參數設定引擎使用的內存量,此內存用于緩存工作集數據(索引、namespace,未提交的write,query緩沖等)。

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中所有節點幾乎同時退出時)。

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+版本。

二、Capped Collections

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

Capped Collection在語義上,類似于“FIFO”隊列,而且是有界隊列。適用于數據緩存,消息類型的存儲。

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

三、數據模型(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的話,就無法保證了。

索引:提高查詢性能,默認情況下_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提供了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。

四、架構模式

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

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)。

五、數據文件存儲原理(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”啟動命令參數來關閉此選項。

一個database中所有的collections以及索引信息會分散存儲在多個數據文件中,即mongodb并沒有像SQL數據庫那樣,每個表的數據、索引分別存儲;數據分塊的單位為extent(范圍,區域),即一個data file中有多個extents組成,extent中可以保存collection數據或者indexes

數據,一個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。

有人肯定疑問:一個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

從上文中我們已經得知,刪除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文件。

對于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復制集最終我們需要在“寫入性能”和“數據一致性”兩個方面權衡,即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)

我還沒有學會寫個人說明!

穿過AI、BI、ML等技術術語迷霧,企業數字化路上需要什么?

上一篇

從Oracle到PostgreSQL,某保險公司遷移實踐

下一篇

你也可能喜歡

MongoDB 存儲引擎與內部原理

長按儲存圖像,分享給朋友

ITPUB 每周精要將以郵件的形式發放至您的郵箱


微信掃一掃

微信掃一掃
重庆时时后一8码方法 体彩排列五走势图 宝鸡做什么赚钱 一件代发衣服赚钱吗 章鱼彩票游戏 卖什么小吃容易赚钱 浙江20选5杀号预测 夺宝阁计划官方网站 体育彩票大乐透走势图带坐标 金沙棋牌官网手机版 滴答怎么赚钱 江西新时时彩中奖qq群 重庆快乐10分钟走势图 排列3和值遗漏 拍摄抖音能赚钱吗 手机赚钱app封号是否上黑名单 6场半全场14092期分析