今天就以這個函數爲起點,看一下mongodb的日誌持久化的流程,及實現方式。
dur_commitjob.cpp:持久化任務工作(單元),封裝延時隊列TaskQueue < D > ,操作集合vector < shared_ptr < DurOp > > 等
dur_journal.cpp:提供日誌文件 / 路徑,創建,遍歷等操作
dur_journalformat.h:日誌文件格式定義
dur_preplogbuffer.cpp:構造用於輸出的日誌buffer
dur_recover.h:日誌恢復類(後臺任務方式BackgroupJob)
dur_stats.h:統計類,包括提交 / 同步數據次數等
dur_writetodatafiles.cpp:封裝寫入數據文件mongofile方法
durop.h:持久化操作類,提供序列化,創建操作(FileCreatedOp),DROP操作(DropDbOp)
首先我們看一下dur::startup() 方法實現(dur.cpp),如下:
void startup() {
if ( ! cmdLine.dur ) /* 判斷命令行啓動參數是否爲持久化 */
return ;
DurableInterface::enableDurability(); // 對持久化變量 _impl 設置爲DurableImpl方式
journalMakeDir(); /* 構造日誌文件所要存儲的路徑:dur_journal.cpp */
try {
recover(); /* 從上一次系統crash中恢復數據日誌信息:dur_recover.cpp */
}
catch (...) {
log() << " exception during recovery " << endl;
throw ;
}
preallocateFiles();
boost::thread t(durThread);
}
注意:上面的DurableInterface,因爲mongodb使用類似接口方式,從而約定不同的持久化方式實現,如下:
virtual void * writingPtr( void * x, unsigned len) = 0 ;
virtual void createdFile( string filename, unsigned long long len) = 0 ;
virtual void declareWriteIntent( void * x, unsigned len) = 0 ;
virtual void * writingAtOffset( void * buf, unsigned ofs, unsigned len) = 0 ;
....
}
接口定義了寫文件的方式及方法等等。
並且mongodb包括了兩種實現方式,即:
}
class DurableImpl : public DurableInterface { /* 持久化,支持磁盤存儲 */
}
再回到startup函數最後一行:boost::thread t(durThread);
該行代碼會創建一個線程來運行durThread方法,該方法就是持久化線程,如下:
Client::initThread( " dur " );
const int HowOftenToGroupCommitMs = 90 ; /* 多少時間提交一組信息,單位:毫秒 */
// 注:commitJob對象用於封裝並執行提交一組操作
while ( ! inShutdown() ) {
sleepmillis( 10 );
CodeBlock::Within w(durThreadMain); /* 定義代碼塊鎖,該設計很討巧,接下來會介紹 */
try {
int millis = HowOftenToGroupCommitMs;
{
stats.rotate(); // 統計最新的_lastRotate信息
{
Timer t; /* 聲明定時器 */
/* 遍歷日誌文件夾下的文件並更新文件的“最新更新時間”標誌位並移除無效或關閉之前使用的日誌文件:dur_journal.cpp */
journalRotate();
millis -= t.millis(); /* 線程睡眠時間爲90減去遍歷時間 */
assert( millis <= HowOftenToGroupCommitMs );
if ( millis < 5 )
millis = 5 ;
}
// we do this in a couple blocks, which makes it a tiny bit faster (only a little) on throughput,
// but is likely also less spiky on our cpu usage, which is good:
sleepmillis(millis / 2 );
// 從commitJob的defer任務隊列中獲取任務並執行,詳情參見: taskqueue.h的invoke() 和 dur_commitjob.cpp 的
// Writes::D::go(const Writes::D& d)方法(用於非延遲寫入信息操作)
commitJob.wi()._deferred.invoke();
sleepmillis(millis / 2 );
// 按mongodb開發者的理解,通過將休眠時間減少一半(millis/2)並緊跟着繼續從隊列中取任務,
// 以此小幅提升讀取隊列系統的吞吐量
commitJob.wi()._deferred.invoke();
}
go(); // 執行提交一組信息操作
}
catch (std::exception & e) { /* 服務如果突然crash */
log() << " exception in durThread causing immediate shutdown: " << e.what() << endl;
abort(); // based on myTerminate()
}
}
cc().shutdown(); // 關閉當前線程,Client::initThread("dur")
}
下面是go()的實現代碼:
if ( ! commitJob.hasWritten() ){ /* hasWritten一般在CUD操作時會變爲true,後面會加以介紹 */
commitJob.notifyCommitted(); /* 發送信息已存儲到磁盤的通知 */
return ;
}
{
readlocktry lk( "" , 1000 ); /* 聲明讀鎖 */
if ( lk.got() ) {
groupCommit(); /* 提交一組操作 */
return ;
}
}
// 當未取到讀鎖時,可能獲取讀鎖比較慢,則直接使用寫鎖,不過寫鎖會用更多的RAM
writelock lk;
groupCommit();
}
static void _groupCommit() {
stats.curr -> _commits ++ ; /* 提交次數加1 */
......
// 預定義頁對齊的日誌緩存對象,該對象對會commitJob.ops()的返回值(該返回值類型vector< shared_ptr<DurOp> >)進行對象序列化
// 並保存到commitJob._ab中,供下面方法調用,位於dur_preplogbuffer.cpp-->_PREPLOGBUFFER()方法
PREPLOGBUFFER();
// todo : write to the journal outside locks, as this write can be slow.
// however, be careful then about remapprivateview as that cannot be done
// if new writes are then pending in the private maps.
WRITETOJOURNAL(commitJob._ab); /* 寫入journal信息,最終操作位於dur_journal.cpp的 Journal::journal(const AlignedBuilder& b)方法 */
// data is now in the journal, which is sufficient for acknowledging getLastError.
// (ok to crash after that)
commitJob.notifyCommitted();
WRITETODATAFILES(); /* 寫信息到mongofile文件中 */
commitJob.reset(); /* 重置當前任務操作 */
// REMAPPRIVATEVIEW
// remapping 私有視圖必須在 WRITETODATAFILES 方法之後調用,否則無法讀出新寫入的數據
DEV assert( ! commitJob.hasWritten() );
if ( ! dbMutex.isWriteLocked() ) {
// this needs done in a write lock (as there is a short window during remapping when each view
// might not exist) thus we do it on the next acquisition of that instead of here (there is no
// rush if you aren't writing anyway -- but it must happen, if it is done, before any uncommitted
// writes occur). If desired, perhpas this can be eliminated on posix as it may be that the remap
// is race-free there.
//
dbMutex._remapPrivateViewRequested = true ;
}
else {
stats.curr -> _commitsInWriteLock ++ ;
// however, if we are already write locked, we must do it now -- up the call tree someone
// may do a write without a new lock acquisition. this can happen when MongoMMF::close() calls
// this method when a file (and its views) is about to go away.
//
REMAPPRIVATEVIEW();
}
}
到這裏只是知道mongodb會定時從任務隊列中獲取相應任務並統一寫入,寫入journal和mongofile文件後再重置任務隊列及遞增相應統計計數信息(如privateMapBytes用於REMAPPRIVATEVIEW)。
但任務隊列中的操作信息又是如何生成的呢?這個比較簡單,我們只要看一下相應的cud數據操作時的代碼即可,這裏以插入(insert)數據爲例:
我們找到pdfile.cpp文件的插入記錄方法,如下(1467行):
......
r = (Record * ) getDur().writingPtr(r, lenWHdr); // 位於1588行
該方法用於將客戶端提交的數據(信息)寫入到持久化隊列(defer)中去,如下(按函數調用順序):
void * p = x;
declareWriteIntent(p, len);
return p;
}
void DurableImpl::declareWriteIntent( void * p, unsigned len) {
commitJob.note(p, len);
}
void CommitJob::note( void * p, int len) {
DEV dbMutex.assertWriteLocked();
dassert( cmdLine.dur );
if ( ! _wi._alreadyNoted.checkAndSet(p, len) ) {
MemoryMappedFile::makeWritable(p, len); /* 設置可寫入mmap文件的信息 */
if ( ! _hasWritten ) {
assert( ! dbMutex._remapPrivateViewRequested );
// 設置寫信息標誌位, 用於進行_groupCommit(上面提到)時進行判斷
_hasWritten = true ;
}
......
// 向defer任務隊列中加入操作信息
_wi.insertWriteIntent(p, len);
wassert( _wi._writes.size() < 2000000 );
assert( _wi._writes.size() < 20000000 );
......
}
其中insertWriteIntent方法定義如下:
D d;
d.p = p; /* 操作記錄record類型 */
d.len = len; /* 記錄長度 */
_deferred.defer(d); /* 延期任務隊列:TaskQueue<D>類型 */
}
到這裏總結一下,mongodb在啓動時,專門初始化一個線程不斷循環(除非應用crash掉),用於在一定時間週期內來從defer隊列中獲取要持久化
的數據並寫入到磁盤的journal(日誌)和mongofile(數據)處,當然因爲它不是在用戶添加記錄時就寫到磁盤上,所以按mongodb開發者
說,它不會造成性能上的損耗,因爲看過代碼發現,當進行CUD操作時,記錄(Record類型)都被放入到defer隊列中以供延時批量
(groupcommit)提交寫入,但相信其中時間週期參數是個要認真考量的參數,系統爲90毫秒,如果該值更低的話,可能會造成頻繁磁盤操作,過高又
會造成系統宕機時數據丟失過多。
最後對文中那個mongodb設置很計巧的代碼做一下簡要分析,代碼如下:
它的作爲就是一個對多線程訪問指定代碼塊加鎖的功能,其類定義如下(位於race.h):
volatile int n;
unsigned tid;
void fail() {
log() << " synchronization (race condition) failure " << endl;
printStackTrace();
abort(); /**/
}
void enter() {
if ( ++ n != 1 ) fail(); /* 當已有線程執行該代碼塊時,則執行fail */
#if defined(_WIN32)
tid = GetCurrentThreadId();
#endif
}
void leave() { /* 只有調用 leave 操作,纔會--n,即在線程執行完該代碼塊時調用 */
if ( -- n != 0 ) fail();
}
public :
CodeBlock() : n( 0 ) { }
class Within {
CodeBlock & _s;
public :
Within(CodeBlock & s) : _s(s) { _s.enter(); }
~ Within() { _s.leave(); }
};
void assertWithin() {
assert( n == 1 );
#if defined(_WIN32)
assert( GetCurrentThreadId() == tid );
#endif
}
};
#else
通過其內部類Within的構造函數和析構函數,分別調用了_s.enter,_s.leave()方法,這樣只要在一個代碼塊之前定義一個該類實例,則從下一行開始到codeblock結束之後,該進程內只允許一個線程執行該代碼塊,呵呵。
作者: daizhj, 代震軍
微博: http://t.sina.com.cn/daizhj
Tags: mongodb,c++