天天看點

MongoDB源碼概述——記憶體管理和存儲引擎

資料存儲:

  之前在介紹Journal的時候有說到為什麼MongoDB會先把資料放入記憶體,而不是直接持久化到資料庫存儲檔案,這與MongoDB對資料庫記錄檔案的存儲管理操作有關。MongoDB采用作業系統底層提供的記憶體檔案映射(MMap)的方式來實作對資料庫記錄檔案的通路,MMAP可以把磁盤檔案的全部内容直接映射到程序的記憶體空間,這樣檔案中的每條資料記錄就會在記憶體中有對應的位址,這時對檔案的讀寫可以直接通過操作記憶體來完成(而不是fread,fwrite之輩).

  這裡順便提一句,MMAP的隻是将檔案映射到程序空間,而不是直接全部map到實體記憶體,隻有通路到這塊資料時才會被作業系統以Page的方式換到實體記憶體。這部分的管理工作由作業系統完成,對于MongoDB的開發者而言,也是透明的.其實我們所能用的所有函數,包括系統核心裡的實作函數,操作的統統都是虛拟記憶體,也就是每個程序所謂的4GB(32位系統)的虛拟位址空間.實體記憶體對于使用者是不可見的,不可操作的。這也就是為什麼MongoDB可以存儲比記憶體更大的資料,但是卻不建議熱資料超過記憶體大小的原因。因為熱資料大于記憶體的話,作業系統需要頻繁的換入換出實體記憶體中的資料,會嚴重影響MongoDB的性能。

<a target="_blank" href="http://images.cnblogs.com/cnblogs_com/Creator/201211/201211041945301465.png"></a>

32位作業系統程序虛拟記憶體表圖:

• 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)中需要注意一下幾個方法:

1

2

3

4

5

6

7

8

9

10

11

12

13

<code>void</code><code>* map(</code><code>const</code> <code>char</code> <code>*filename, unsigned </code><code>long</code> <code>long</code> <code>&amp;length, </code><code>int</code> <code>options = 0 );</code>

<code>//将檔案filename以MMAP的方式映射到程序的空間(稱之為視圖),傳回在記憶體中的首位址</code>

<code>//如果檔案不存在,會通過mmap_win裡的CreateFile建立檔案</code>

<code>void</code> <code>flush(</code><code>bool</code> <code>sync);</code>

<code>//将映射到程序空間的資料Flush到磁盤</code>

<code>void</code><code>* getView() </code><code>const</code>

<code>//擷取視圖首位址</code>

  關于這三個方法的内部實作,自然我們可以想到是對作業系統的API的調用,對于不同的作業系統,方法簽名以及參數還有變化,在這裡我就不羅嗦了,各個系統的API都查得到。是以我們這裡也并不會貼出其内部調用的系統API.

  究竟MongoDB是什麼時候map資料庫檔案到記憶體的呢?又是何時将記憶體中映射的資料flush到磁盤進行持久化的呢?下面我們來分析一下這兩個問題。

map資料庫檔案到記憶體:

  在我們第一次向一個未建立的資料庫插入一條記錄時,調用的函數會由如下流程:

<code>DataFileMgr::insert()——》Database::allocExtent()——》Database::suitableFile()——》 Database::getFile()——》MongoDataFile::open()——》 MongoMMF::create()</code>

  DataFileMgr::insert()之前有些方法我已經省略了,這個調用流程比較長,但是最終會調用到MongoMMF::create()來建立第一個資料庫檔案

<code>bool</code> <code>MongoMMF::create(string fname, unsigned </code><code>long</code> <code>long</code><code>&amp; len, </code><code>bool</code> <code>sequentialHint) {</code>

<code>        </code><code>setPath(fname);</code>

<code>        </code><code>_view_write = map(fname.c_str(), len, sequentialHint ? SEQUENTIAL : 0);</code>

<code>        </code><code>//如果檔案不存在,會通過mmap_win裡的CreateFile建立檔案,MemoryMappedFile::map方法</code>

<code>        </code><code>return</code> <code>finishOpening();</code>

<code>    </code><code>}</code>

  觀察代碼後我們發現create方法直接調用了map,而map的内部,就有檔案建立功能,建立完後就map到記憶體了。

  若是向現有資料庫插入記錄,則在Database構造的期間會調用openAllFiles(),進入上面流程的Database::getFile()部分

  終上所述兩種情況,我們明白了MongoDB何時将資料庫記錄檔案map到記憶體.

Flush資料進行持久化:

  MongoDB中預設每分鐘Flush一次進行持久化存儲,當然這個間歇可以通過"--syncdelay"啟動參數來進行設定.執行流程為main()——》dataFileSync.go()。DataFileSync派生自BackgroundJob,其go()方法會建立一個新的線程來運作虛函數run()。

<a target="_blank" href="http://www.cnblogs.com/Creator/archive/2012/11/04/2754110.html#">+ View Code</a>

  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版本後增強“)