在MongoDB源碼概述——內存管理和存儲引擎一文的最后,我們留下了一個問題,在使用MongoDB的內存管理與存儲引擎時,因為其依仗操作系統的MMAP方式,將磁盤上的文件映射到進程的內存空間,這給MongoDB帶來了極大的便利,可也給我們帶來了不小的問題。到底隔多久一次將映射的在內存的視圖持久化硬盤才能保證我們服務器在宕機時丟失的數據最少呢?針對flushAll過程中宕機有可能造成的數據錯亂,有沒有什么好的解決方案呢?
MongoDB的團隊成員1.7版本的最新分支上開始對單機高可靠性的提升,這就是引入的Journal\Durability模塊,這個模塊主要解決上面提出來的問題,對提高單機數據的可靠性,起了決定性的作用.其機制主要是通過log方式定時將操作日志(對數據庫有更改的操作,查詢不在記錄范圍之類)記錄到dbpath的命名為journal文件夾下,這樣當系統再次重啟時從該文件夾下恢復丟失的數據。
下面我們針對源碼,對他進行簡要分析
Journal記錄模塊
Journal\durability模塊的調用路徑如下:
Main()——》initAndListen()——》_initAndListen()——》dur::startup();
Startup()的代碼如下:
void startup() {
if( !cmdLine.dur )
return;
//DurableInterface的工廠模式 用DurableImpl來實例化getDur獲取的對象
DurableInterface::enableDurability();
journalMakeDir();//確認日志目錄
try {
recover();//修復模式
}
catch(...) {
log() << "exception during recovery" << endl;
throw;
}
//預分配兩個日志文件
preallocateFiles();
boost::thread t(durThread);
}
上述代碼中,DurableInterface::enableDurability()確保系統使用DurableImpl來實例化內部_impl變量指針,此指針默認指向一個NonDurableImpl實例。他們的關系如下:
NonDurableImpl不會持久化任何的journal,而DurableImpl,提供journal的持久化。
journalMakeDir()函數會檢查日志的目錄是否存在,若不存在,則負責創建。
recover()函數則負責檢查現有的journal持久化文件,若有相關文件,則意味着上次系統宕機,需要根據journal恢復數據,這部分內容將會在本文的后面講到。
preallocateFiles(),給持久化journal提供存儲文件,系統會根據當前環境來判定到底需不需要預分配.
接着系統開啟了一個新的線程來運行durThread(),為了極大的減少文中粘貼的代碼數量,我還是描述一下流程,說幾個重要的步驟吧。畢竟我覺得貼代碼沒意思,文章膨脹,可實際有用的內容又少的可憐。這也就是為什么我喜歡叫我的文章為源碼概述而不是源碼分析的緣故.
durThread主要負責每90毫秒commit一次journal(記錄用戶對數據庫更改的操作,查詢操作不再記錄范圍),他是一個單獨的線程,而記錄接口,在內存中存儲journal這兩大部分則是在用戶調用journal接口時完成的,這部分的內容我在 MongoDB源碼概述——日志 一文中已經完成,
具體可以分為以下幾個過程:
- 記錄最后一次MMAP的Flush時間,清理不再需要的日志文件
調用journalRotate()會更新lsn文件,此文件用於記錄最后一次MMAP文件Flush到磁盤的時間,此數據來源於lastFlushTime屬性,而與此屬性相關的賦值如下:
void Journal::init() {
assert( _curLogFile == 0 );
MongoFile::notifyPreFlush = preFlush;//兩個指向函數的指針
MongoFile::notifyPostFlush = postFlush;//用於模擬事件通知
}
void Journal::preFlush() {
j._preFlushTime = Listener::getElapsedTimeMillis();//獲取系統啟動后的初略時間
}
void Journal::postFlush() {
j._lastFlushTime = j._preFlushTime;
j._writeToLSNNeeded = true;
}
至此,我們知道其lastFlushTime是存儲着在Listener類一個初略估計系統啟動時間的數值,且這個數值會隨着MMAP的視圖Flush到磁盤的時候通知lastFlushTime更新值(函數指針通知)。另外,此次調用還會檢查是否已經寫滿了journal存儲文件,系統給32位和64位的環境設定了不同的最大值
DataLimit = (sizeof(void*)==4) ? 256 * 1024 * 1024 : 1 * 1024 * 1024 * 1024;
若當前寫的位置超出了最大值范圍,會相繼調用
closeCurrentJournalFile();
removeUnneededJournalFiles();
這兩個函數的代碼我就不貼了,其實他就是關閉當前已經寫滿的Journal記錄文件,刪除掉那些在最后一次FlushTime之前的記錄文件(同時存在多個Journal記錄文件)。因為這部分記錄的更改已經順利持久化了,不再需要Journal記錄之前的操作了.
- 序列化用戶操作並持久化
序列化之前,系統需要調用commitJob.wi()._deferred.invoke(),此函數將遍歷TaskQueue<D>內存有的D(記錄用戶操作那步存下來的),逐個運行D::go(),最后將所有D內的數據封裝為WriteIntent存到Writes :: _writes中(set<WriteIntent>),細看WriteIntent與D結構體的區別,D存儲數據源的首地址,而WriteIntent存儲數據源的首地址,官方的解釋是這樣做能夠讓我們在_writes(set<WriteIntent> 內部實現是紅黑樹)運行重載符” < “更快.我實在對他這種做法很費解,為什么這些東西不能由一個D來完成呢?非得弄個WriteIntent,干擾閱讀代碼的人的視線.
好了 至此為止,所有的WriteIntent在_writes(set<WriteIntent>整裝待發,正准備的系統對他進行序列化,就像砧板上的肉,洗干凈了身子正准備等待主人來切.
在_groupCommit內調用PREPLOGBUFFER(),開始了journal的序列化操作
AlignedBuilder& bb = commitJob._ab;//可以將其理解為一個Buf
...
for( vector< shared_ptr<DurOp> >::iterator i = commitJob.ops().begin(); i != commitJob.ops().end(); ++i ) {
(*i)->serialize(bb);
}
…
for( set<WriteIntent>::iterator i = commitJob.writes().begin(); i != commitJob.writes().end(); i++ ) {
prepBasicWrite_inlock(bb, &(*i), lastDbPath);
}
通過上面代碼我們可以得知,DurOp的序列化是自己的serialize方法完成的,他們的序列化操作不牽扯到被修改數據,所以序列化結果可以很簡潔。例如一個DropDbOp,卸載掉某個數據庫,如果需要恢復,指向需要再運行一次卸載過程即可,所以只需要用一個東西(甚至代碼數字也可以)來標識就行了。而BasicWrites就不一樣了,例如新插入了一個Record,我們需要記錄下整個Record作為恢復的數據源,沒錯,這就是上面沒有解釋的代碼prepBasicWrite_inlock所干的事情。
JEntry e;
...
bb.appendStruct(e);
bb.appendBuf(i->start(), e.len)
對於AlignedBuilder,我們可以理解為我們序列化過程中的Buf,存儲着已經序列化好的待持久化的數據,appendBuf將會將參數指定位置的數據進行memcopy,實際上這里說是序列化還有些問題,像這些數據源,存儲在journal日志文件時就是二進制。好了,不糾結這個名稱了,AlignedBuilder除了放入了數據源之外,還放入了JEntry來表示一些基本屬性,JEntry與WriteIntent是1:1的關系,也只有這樣,讀取的時候才能正確的尋址.
在全部序列化之后,系統調用WRITETOJOURNAL(commitJob._ab)來將AlignedBuilder持久化到journal日志文件,最終通過調用LogFile::synchronousAppend負責向外部存儲文件寫入。接着系統調用WRITETODATAFILES(),事實上我在第一次看源碼的時候我非常的不解,舉個例子,我們在插入數據的時候,已經將將用戶要插入的數據memcopy過一次了,已經存到內存里面的視圖(View)上了,為什么這里的WRITETODATAFILES還需要memcopy一次呢?我在這個問題上也糾結了很久,最后才找到了答案。這個奧秘就在於如果啟用了dur模式,對於每個MemoryMappedFile實際上會產生兩個視圖,一個_view_private,一個_view_write(對於未開啟dur模式的mongodb在32bit系統上運行,官方說db數據不能超過2.5G,現在通過這個原理我們可以看到他的水分,實際上最優的大小也就1G)。代碼如下:
bool MongoMMF::finishOpening() {
if( _view_write ) {//_view_write先創建
if( cmdLine.dur ) {
_view_private = createPrivateMap();//創建 _view_private
if( _view_private == 0 ) {
massert( 13636 , "createPrivateMap failed (look in log for error)" , false );
}
privateViews.add(_view_private, this); // note that testIntent builds use this, even though it points to view_write then...
}
else {//若不允許dur 則只用一個view
_view_private = _view_write;
}
return true;
}
return false;
}
MongoMMF內的兩個視圖,只有一個能被Flush到磁盤,那就是第一個創建的視圖,_view_write一定是第一個創建的,所以只有他才能真正持久化。
void MemoryMappedFile::flush(bool sync) {
uassert(13056, "Async flushing not supported on windows", sync);
if( !views.empty() ) {
WindowsFlushable f( views[0] , fd , filename() , _flushMutex);
f.flush();
}
}
我們現在還只是知道dur模式有兩次memcopy,可是為什么會有兩次呢?從此模式下有兩個不同的視圖出發,你有沒有想到什么?沒錯,我們在Insert方法中(pdfile.cpp 1596行)調用memcopy是將內容復制到_view_private上(pdfile.cpp 1596行可知recordAt使用的是p,p=》_mb=》 _mb = mmf.getView();所以,實際上那個record在view_private上),不是可以被持久化的_view_write,所以在WRITETODATAFILES需要在復制一次,而此時,數據源則就是view_private上被復制過來的數據.
通過上面兩段的代碼,我們還能發現,在非dur模式下,_view_private與_view_write實際上是同一個東西。這也就解釋了為什么非dur模式不需要兩次memcopy就能很好的完成工作(非dur模式不運行WRITETODATAFILES)。
好,至此為止,我們所有的結論都已經對接上了。
最后用一張非常蹩腳的時序圖來描述這一過程(過程非完全面向對象)。
Journal恢復模塊
此模塊在系統啟動時運行,他完成對上次宕機遺留的Journal文件進行解讀(也是通過MMAP的方式)並將沒有Flush到數據庫記錄文件的記錄重新通過memcopy的方式放入_view_write中。以備存儲引擎線程執行持久化。
若系統上次是正常退出,則在退出流程中會進行最后的Flush(僅dur模式),並清理現有的Journal文件,所以正常退出是不會遺留任何的Journal文件的.
這個部分的操作也非常的簡單,因為時間的關系,本文就不再詳細闡述了,時序圖如下:
不早了. 洗洗睡了!!!
另尋找熱愛底層技術(C/C++ linux)的朋友一起研究和創造有意思的東西!



