本篇文章主要介紹MongoDB的日誌模組以及資料持久化儲存模組的代碼實現方式。大家也許會驚訝,為什麼日誌模組和持久化儲存模組會放到一篇文章來總結。嘿嘿,在別的系統,可能這兩個模組聯絡不是特別大,可是這MongoDB ,這兩個模組還真不能分開來講。這是怎麼回事呢?請聽我娓娓道來…
通常說來,MongoDB具有三個日誌模組,
Log: 位於 log.h,它主要負責使用者記錄檔,這和我們普通系統的日誌系統沒有什麼區別,作用也就是記錄系統的一些重要流程,然後持久化到log檔案。這個log檔案可以通過系統啟動參數"--logpath".
Journal: 位於dur.h,通過啟動參數"--dur"啟動該模組功能。主要用於解決因系統宕機時,記憶體中的資料未寫入磁碟而造成的資料丟失(為什麼資料會被放到記憶體做儲存而不是直接對外存上的檔案進行操作呢?這一點與MongoDB的儲存機制有關,稍後會講到)。其機制主要是通過log方式定時將動作記錄(對資料庫有更改的操作,查詢不在記錄範圍之類)記錄到dbpath的命名為journal檔案夾下,這樣當系統再次重啟時從該檔案夾下恢複丟失的資料。
Oplog :當部署應用於生產的健壯的伺服器時,需要對伺服器進行同步備份,MongoDB為解決這一問題提出了複製集(Replica sets)模式,而Oplog 的作用則主要是負責記錄寫伺服器(一個複製集內只有一台伺服器可寫,多台備份伺服器可讀)上所有對資料的更改(查詢等對資料庫不產生更改的操作不會被記錄),這樣,複製集內的其他讀擴充(即用於備份的機器和分散讀壓力的伺服器)的伺服器通過擷取Oplog 就可以進行差異同步了。
本文主要是介紹日誌和持久化儲存,以及他們之間的關係。所以本文就不對Oplog做過多的說明,後續文章講到複製集模組時,我一定會寫上。本文的主要重點還是分析Journal以及持久化的實現,所以,對於Log模組,我也就只是簡單的概括一下了。
Log模組:
當我們啟動MongoDB,對Log模組的調用流程如下:
Main(...)->addWindowsOptions(...)->initLogging(...)->loggingManager.start(...);
之後會調用這樣的代碼來設定stdout的輸出目標
FILE* tmp = freopen(_path.c_str(), (_append ? "a" : "w"), stdout);
又因為static的logfile指標指向stdout
FILE* Logstream::logfile = stdout;
所以在Logstream內最後資料會被flush到stdout,即系統所指定的目的地.
在log.h下有如下定義:
1 enum LogLevel { LL_DEBUG , LL_INFO , LL_NOTICE , LL_WARNING , LL_ERROR , LL_SEVERE };
2 inline Nullstream& log( int level ) {
3 if ( level > logLevel )
4 return nullstream;
5 return Logstream::get().prolog();
6 }
7
8 inline Nullstream& log() {
9 return Logstream::get().prolog();
10 }
又因為Logstream重載了一些基本的流符號:
Logstream& operator<<(const char *x) { ss << x; return *this; }
Logstream& operator<<(const string& x) { ss << x; return *this; }
Logstream& operator<<(const StringData& x) { ss << x.data(); return *this; }
Logstream& operator<<(char *x) { ss << x; return *this; }
…
Logstream& operator<< (ostream& ( *_endl )(ostream&)) {
ss << '\n';
flush(0);
return *this;
}
Logstream& operator<< (ios_base& (*_hex)(ios_base&)) {
ss << _hex;
return *this;
}
所以,我們可以輕鬆的使用下面的操作來記錄我們的日誌。
log() << "WARNING: alloc() failed after allocating new extent. lenWHdr: "<<endl;
我不知道大家是否喜歡我這樣的分析模式,我是挺喜歡的,節奏快,很直接,很靠譜!
這裡做一下簡要說明<<重載運算子方法將要輸出的日誌放入stringstream的,接著調用<<endl時,觸發 上面列出來的Logstream& operator<< (ostream& ( *_endl )(ostream&))方法,間接調用flush(0)
void Logstream::flush(Tee *t)方法的職責就是將stringstream內緩衝的所有日誌進行持久化到logfile.因為flush這部分的代碼也非常的簡潔易懂,所以這裡就不貼了。至此使用者日誌也被寫到了外存上,準系統已經完成。
在flush中另外值得注意是與Tee相關的代碼
if( t ) t->write(logLevel,out);
if ( globalTees ) {
for ( unsigned i=0; i<globalTees->size(); i++ )
(*globalTees)[i]->write(logLevel,out);
}
關於Tee的定義:
class Tee {
public:
virtual ~Tee() {}
virtual void write(LogLevel level , const string& str) = 0;
};
所以我們可以很清晰的認識到,實際上Tee的職責是訂閱日誌資訊(觀察者設計模式),任何Tee的衍生類別都可以實現在不影響現有日誌的情景下將日誌額外的記錄到其他任何地方。例如遠程日誌.或者在伺服器很多的情況下,收集各個伺服器的使用者日誌放入資料庫,以供管理員查看.
Journal模組:
實際上在MongoDB中,Journal\Durability是一個很大的模組,牽扯到的東西也是非常之多,他的設計初衷是為了使用日誌的方式來提高單機資料的可靠性,在1.7版本的最新分支上首次出現了這個部分.具體他的職責可以用一句話來概括:
通過log方式定時將動作記錄(對資料庫有更改的操作,查詢不在記錄範圍之類)記錄到dbpath的命名為journal檔案夾下,這樣當系統再次重啟時從該檔案夾下恢複丟失的資料。
根據其完成的功能,我們可以將這個部分的實現概括為以下幾個問題:
- 何時調用
- 如何記錄使用者操作
- 如何序列化使用者操作並持久化
- 如何根據現有Journal日誌恢複資料
下面我們來一一根據源碼分析其重要步驟:
一.何時調用
當我們需要更改資料庫時,需要記錄下使用者的操作以及使用者更改後的資料,這些記錄的資料將是進行恢複時的資料來源。舉一個例子,我們向資料庫插入一條記錄的時候,我們需要記錄使用者的操作以及操作的資料,我截取了這部分代碼,如下:
r = (Record*) getDur().writingPtr(r, lenWHdr);//持久化插入記錄資訊
...
if( obuf )
memcpy(r->data, obuf, len);//直接拷貝資料到記錄欄位
我們來看DurableImpl內的幾個重要方法:
//告訴系統我正在往x位置寫入資料(更改或插入時丟會調用)
void* writingPtr(void *x, unsigned len);
//告訴系統我建立了一個檔案
void createdFile(string filename, unsigned long long len);
其實調用上兩個函數的潛台詞就是,我現在幹了什麼事,你給我把他完整的記錄下來,如果我這件事沒有最終被儲存到磁碟的話,我就需要你這個負責記錄的模組拿出原來所做的記錄,我來進行恢複操作,以確保資料萬無一失!
二.如何記錄使用者操作
在這個模組,使用者的操作類型實際上可以歸類為兩種,一種是基本寫操作,一種是非基本寫操作。對資料的新增和修改等都可以認為是寫操作,而類似與建立檔案(FileCreatedOp),刪除資料庫(DropDbOp)操作都是非基本寫操作,這類操作建模為DurOp.最終這兩種操作都會在CommitJob:: note()與 CommitJob::noteOp()進行記憶體儲存。
基本寫操作會被D結構體封裝,我們來看下他的結構:
struct D {
void *p;//使用者更改的資料來源首地址
unsigned len;//使用者更改的資料長度
static void go(const D& d);
};
基本寫記錄會被儲存到Writes類在CommitJob類的執行個體_wi,繼而儲存到TaskQueue<D>在Writes類的執行個體_deferred.
非基本寫記錄會被儲存到Writes類的vector< shared_ptr<DurOp> > _ops;
這個流程有很多個類參與,下面用兩張順序圖來總結這一流程。
調用getDur().writingPtr的時序圖
調用getDur().createdFile的時序圖
至此為止,使用者動作記錄在記憶體的記錄操作就完成了。接下來要講到如何序列化記憶體中的記錄,以備持久化到磁碟.
今日至此,下面兩點下次結合其他的文章一起寫。
如何序列化使用者操作並持久化
如何根據現有Journal日誌恢複資料
後會有期......