一、存儲引擎(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的資料。
系統配置:
- 建議mongodb部署在linux系統上,較高版本,選擇合适的底層檔案系統(ext4),開啟合适的swap空間
- 無論是MMAPV1或者wiredTiger引擎,較大的記憶體總能帶來直接收益。
- 對資料存儲檔案關閉“atime”(檔案每次access都會更改這個時間值,表示檔案最近被通路的時間),可以提升檔案通路效率。
- 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的資訊:
清單資訊中有幾個字段簡單介紹一下:
- 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空間。
- storageSize:所有collection的documents占用總空間,包括那些已經删除的documents所占的空間,為存儲documents的extents所占空間總和。文檔的删除或者收縮不會導緻storageSize變小。
- indexSize:所用collection的索引資料的大小,為存儲indexes的extents所占空間的總和。
- fileSize:為底層所有data files的大小總和,但不包括namespace檔案。為storageSize、indexSize、以及一些尚未使用的空間等等。當删除database、collections時會導緻此值變小。
此外,如果你想檢視一個collection中extents的配置設定情況,可以使用
db..stats(),結構與上述類似;如果你希望更細緻的了解collection中extents的全部資訊,則可以使用db..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: ''})。
如果你的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分别使用不同的磁盤驅動器)。
如果你希望資料盡可能的不丢失,可以考慮:
- 減小commitIntervalMs的值
- 每個write指定“write concern”中指定“j”參數為true
- 最佳手段就是采用“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,檢視原文請點選「閱讀原文」