資料存放區:
之前在介紹Journal的時候有說到為什麼MongoDB會先把資料放入記憶體,而不是直接持久化到資料庫隱藏檔,這與MongoDB對資料庫記錄檔案的儲存管理操作有關。MongoDB採用作業系統底層提供的記憶體檔案對應(MMap)的方式來實現對資料庫記錄檔案的訪問,MMAP可以把磁碟檔案的全部內容直接映射到進程的記憶體空間,這樣檔案中的每條資料記錄就會在記憶體中有對應的地址,這時對檔案的讀寫可以直接通過操作記憶體來完成(而不是fread,fwrite之輩).
這裡順便提一句,MMAP的只是將檔案對應到進程空間,而不是直接全部map到實體記憶體,只有訪問到這塊資料時才會被作業系統以Page的方式換到實體記憶體。這部分的管理工作由作業系統完成,對於MongoDB的開發人員而言,也是透明的.其實我們所能用的所有函數,包括系統核心裡的實現函數,操作的統統都是虛擬記憶體,也就是每個進程所謂的4GB(32位系統)的虛擬位址空間.實體記憶體對於使用者是不可見的,不可操作的。這也就是為什麼MongoDB可以儲存比記憶體更大的資料,但是卻不建議熱資料超過記憶體大小的原因。因為熱資料大於記憶體的話,作業系統需要頻繁的換入換出實體記憶體中的資料,會嚴重影響MongoDB的效能。
32位作業系統進程虛擬記憶體表圖:
使用這種記憶體管理方式極大的減輕了MongoDB開發人員的負擔,把大量的記憶體管理的工作交由作業系統來完成,在寫這篇文章的時候我自個兒我總結了下她的特點,可是後面發現有本書上有總結,於是直接貼上來(加了幾個底線),沒辦法,人家比我總結得好。
• MongoDB’s code for managing memory is small and clean, because most of that work is pushed to the operating system.
• The virtual size of a MongoDB server process is often very large, exceeding the size of the entire data set. This is OK, because the operating system will handle keeping the amount of data resident in memory contained.
• MongoDB cannot control the order that data is written to disk, which makes it
impossible to use a writeahead log to provide single-server durability. Work is
ongoing on an alternative storage engine for MongoDB to provide single-server
durability.
• 32-bit MongoDB servers are limited to a total of about 2GB of data per mongod.
This is because all of the data must be addressable using only 32 bits.
(如果你想瞭解更多MMAP相關的東東,可以翻閱《Unix網路編程 卷二》的12.2節)
好了,抽象的東西講述完畢,下面來點硬貨!!!
儲存源碼分析:
在MongoMMF類的定義(momgommf.h 29)中需要注意一下幾個方法:
void* map(const char *filename, unsigned long long &length, int options = 0 );//將檔案filename以MMAP的方式映射到進程的空間(稱之為視圖),返回在記憶體中的首地址//如果檔案不存在,會通過mmap_win裡的CreateFile建立檔案void flush(bool sync);//將映射到進程空間的資料Flush到磁碟void* getView() const//擷取視圖首地址
關於這三個方法的內部實現,自然我們可以想到是對作業系統的API的調用,對於不同的作業系統,方法簽名以及參數還有變化,在這裡我就不羅嗦了,各個系統的API都查得到。所以我們這裡也並不會貼出其內部調用的系統API.
究竟MongoDB是什麼時候map資料庫檔案到記憶體的呢?又是何時將記憶體中映射的資料flush到磁碟進行持久化的呢?下面我們來分析一下這兩個問題。
map資料庫檔案到記憶體:
在我們第一次向一個未建立的資料庫插入一條記錄時,調用的函數會由如下流程:
DataFileMgr::insert()——》Database::allocExtent()——》Database::suitableFile()——》 Database::getFile()——》MongoDataFile::open()——》 MongoMMF::create()
DataFileMgr::insert()之前有些方法我已經省略了,這個調用流程比較長,但是最終會調用到MongoMMF::create()來建立第一個資料庫檔案
bool MongoMMF::create(string fname, unsigned long long& len, bool sequentialHint) { setPath(fname); _view_write = map(fname.c_str(), len, sequentialHint ? SEQUENTIAL : 0);//如果檔案不存在,會通過mmap_win裡的CreateFile建立檔案,MemoryMappedFile::map方法 return finishOpening(); }
觀察代碼後我們發現create方法直接調用了map,而map的內部,就有檔案建立功能,建立完後就map到記憶體了。
若是向現有資料庫插入記錄,則在Database構造的期間會調用openAllFiles(),進入上面流程的Database::getFile()部分
終上所述兩種情況,我們明白了MongoDB何時將資料庫記錄檔案map到記憶體.
Flush資料進行持久化:
MongoDB中預設每分鐘Flush一次進行持久化儲存,當然這個間歇可以通過"--syncdelay"啟動參數來進行設定.執行流程為main()——》dataFileSync.go()。DataFileSync派生自BackgroundJob,其go()方法會建立一個新的線程來運行虛函數run()。
void run() { if( cmdLine.syncdelay == 0 ) log() << "warning: --syncdelay 0 is not recommended and can have strange performance" << endl; else if( cmdLine.syncdelay == 1 ) log() << "--syncdelay 1" << endl; else if( cmdLine.syncdelay != 60 )//預設是60 log(1) << "--syncdelay " << cmdLine.syncdelay << endl; int time_flushing = 0; while ( ! inShutdown() ) { flushDiagLog(); if ( cmdLine.syncdelay == 0 ) { // in case at some point we add an option to change at runtime sleepsecs(5); continue; } sleepmillis( (long long) std::max(0.0, (cmdLine.syncdelay * 1000) - time_flushing) ); if ( inShutdown() ) { // occasional issue trying to flush during shutdown when sleep interrupted break; } Date_t start = jsTime();//當前dataFileSync的任務就是在一段時間後(cmdLine.syncdelay)將記憶體中的資料flush到磁碟上(因為mongodb使用mmap方式將資料先放入記憶體中) int numFiles = MemoryMappedFile::flushAll( true ); time_flushing = (int) (jsTime() - start); globalFlushCounters.flushed(time_flushing); log(1) << "flushing mmap took " << time_flushing << "ms " << " for " << numFiles << " files" << endl; } }
Run()最後調用MemoryMappedFile::flushAll方法對所有的對應檔進行flush操作,將更改持久化到磁碟.前面在介紹MongoMMF的時候就介紹過此方法.這裡不再累述。
這裡順便提一句,其實mmap不調用fsync強刷到磁碟,作業系統也是會幫我們自動刷到磁碟的,linux有個dirty_writeback_centisecs參數用於定義髒資料在記憶體停留的時間(預設為500,即5秒),過了這個timeout時間就會被系統刷到磁碟上。在這個自動刷的過程中是會阻塞所有的IO操作的,如果要刷的資料特別多的話,容易產生一些長耗時的操作,例如有些使用mmap的程式每隔一段時間就會出現有逾時操作,一般的最佳化手段是考慮修改系統參數dirty_writeback_centisecs,加快髒頁刷寫頻率來減少長耗時。mongodb是定時強刷,不會有此問題。
問題的出現:
弄清楚了MongoDB的儲存引擎何時將資料庫記錄檔案map到進程的記憶體空間以及何時flush到原檔案時,不知道您發現了問題沒有?持久化的flush過程是每分鐘調用一次,而寫資料是時時刻刻進行的,若還沒有到一分鐘,在59秒的時候伺服器斷電了怎麼辦?是不是這59秒內對資料庫的所有操作都不會提交到持久化的資料庫檔案?丟失59秒的資料,這還不是最可怕的. 如果在60秒後,在進行flushAll的過程中系統宕機,則會造成資料檔案錯亂,一部分是新資料,一部分是舊資料,這種情況下,有可能我們的資料庫就不能用了。
不知道為什麼,MongoDB在正確的退出流程中(調用dbexit(EXIT_CLEAN)),非"--dur模式啟動 也並沒有調用MemoryMappedFile::flushAll來進行持久化操作,這令我非常費解.一開始我以為是我這個版本的代碼沒有完善,立馬又查閱了2.2版本的源碼,發現也並沒有在非"--dur"調用flush方法。都僅僅是調用MemoryMappedFile::closeAllFiles.
我個人的理解是,在生產環境下一定會開啟"--dur",甚至在新版本中在64位運行環境下預設開啟,所以給非dur模式下來一次flush就不那麼必要了.
如果您在使用MongoDB的windows版本進行調試的以驗證我上面的描述的話,您會得到相反的結果,可能你的第一感覺就會是我完全的搞錯了。的確,一般的人都會這樣認為,我們來進行一次簡單的測試流程:
- 以非"--dur"模式啟動Mongod,啟動時最好調整一下--syncdelay,設定一個較大值如600
- 使用mogo對資料庫的資料進行修改(如修改刪除)
- 使用工作管理員強制結束進程mongod(類比系統宕機)
- 刪除掉mongod.lock(類比宕機一定會留下這個),重新啟動非"--dur"模式的Mongod
- 使用mongo進行db.collectiob.find()觀察第一次的更改是否已經生效
使用上述測試流程,您會驚奇的發現,我們的任何更改都已經持久化了,這樣是不是就說明我前面所提到的都是胡扯呢?起初我自己也有點懷疑這個結果,反覆的測試了很多遍,並進行了跟蹤調試,我發現即便MongoDB沒有運行過一次flushAll,並且連任何一個MongoMMF類的對象(代表一個資料庫記錄檔案)也不曾調用flush()方法,所做的更改仍然能被持久化。至此,我開始懷疑Windows上並不是顯示調用flush才會持久化,而是memcopy更改時就會被持久化,搜尋了一下網上,發現了別人在Windows也遇到了相同的問題.(CSDN上命名為 "記憶體映射,沒有FlushViewOfFile,也可以儲存到檔案"的貼子也遇到了相同的問題).
對於Windows這個特例,我也就不再深究了,大家知道是這個地方的問題就OK了,其實在它的這種機制下,整個用於flush資料到磁碟的DataFileSync線程都不用,對於Linux,Unix,我上面的總結還是正確的.
問題的解決:
事實上曾經有人就是因為上面提到的問題丟失了所有資料,所以MongoDB的團隊成員才在1.7版本的最新分支上開始對單機高可靠性的提升,這就是引入的Journal\durability模組,著重解決這個問題。(導火索見文章"MongoDB的資料可靠性,單機可靠性有望在1.8版本後增強“)
在MongoDB源碼概述——日誌 一文中也提到這個Journal\durability模組,不過最後還有一部分沒有講完,下次將會有專門的博文介紹後續問題。