MongoDB源碼概述——內存管理和存儲引擎

原文鏈接

數據存儲:

  之前在介紹Journal的時候有說到爲什麼MongoDB會先把數據放入內存,而不是直接持久化到數據庫存儲文件,這與MongoDB對數據庫記錄文件的存儲管理操作有關。MongoDB採用操作系統底層提供的內存文件映射(MMap)的方式來實現對數據庫記錄文件的訪問,MMAP可以把磁盤文件的全部內容直接映射到進程的內存空間,這樣文件中的每條數據記錄就會在內存中有對應的地址,這時對文件的讀寫可以直接通過操作內存來完成(而不是fread,fwrite之輩).

  這裏順便提一句,MMAP的只是將文件映射到進程空間,而不是直接全部map到物理內存,只有訪問到這塊數據時纔會被操作系統以Page的方式換到物理內存。這部分的管理工作由操作系統完成,對於MongoDB的開發者而言,也是透明的.其實我們所能用的所有函數,包括系統內核裏的實現函數,操作的統統都是虛擬內存,也就是每個進程所謂的4GB(32位系統)的虛擬地址空間.物理內存對於用戶是不可見的,不可操作的。這也就是爲什麼MongoDB可以存儲比內存更大的數據,但是卻不建議熱數據超過內存大小的原因。因爲熱數據大於內存的話,操作系統需要頻繁的換入換出物理內存中的數據,會嚴重影響MongoDB的性能。

clip_image001

32位操作系統進程虛擬內存表圖:

clip_image002  使用這種內存管理方式極大的減輕了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)中需要注意一下幾個方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
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數據庫文件到內存:

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

1
DataFileMgr::insert()——》Database::allocExtent()——》Database::suitableFile()——》 Database::getFile()——》MongoDataFile::open()——》 MongoMMF::create()

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

1
2
3
4
5
6
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()。

  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模塊,不過最後還有一部分沒有講完,下次將會有專門的博文介紹後續問題。


發佈了52 篇原創文章 · 獲贊 23 · 訪問量 26萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章