在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)的朋友一起研究和創造有意思的東西!