本篇文章主要介紹MongoDB的日志模塊以及數據持久化存儲模塊的代碼實現方式。大家也許會驚訝,為什么日志模塊和持久化存儲模塊會放到一篇文章來總結。嘿嘿,在別的系統,可能這兩個模塊聯系不是特別大,可是這MongoDB ,這兩個模塊還真不能分開來講。這是怎么回事呢?請聽我娓娓道來…
通常說來,MongoDB具有三個日志模塊,
- Log
- Journal
- Oplog
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模塊的調用流程如下:
之后會調用這樣的代碼來設置stdout的輸出目標
FILE* tmp = freopen(_path.c_str(), (_append ? " a " : " w " ), stdout);
又因為static的logfile指針指向stdout
FILE* Logstream::logfile = stdout;
所以在Logstream內最后數據會被flush到stdout,即系統所指定的目的地.
在log.h下有如下定義:
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 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 ( 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日志恢復數據
下面我們來一一根據源碼分析其重要步驟:
一.何時調用
當我們需要更改數據庫時,需要記錄下用戶的操作以及用戶更改后的數據,這些記錄的數據將是進行恢復時的數據源。舉一個例子,我們向數據庫插入一條記錄的時候,我們需要記錄用戶的操作以及操作的數據,我截取了這部分代碼,如下:
...
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結構體封裝,我們來看下他的結構:
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的時序圖
至此為止,用戶操作日志在內存的記錄操作就完成了。接下來要講到如何序列化內存中的記錄,以備持久化到磁盤.
今日至此,下面兩點下次結合其他的文章一起寫。
后會有期......