Mongodb源碼分析--日誌及持久化

      在本系列的第一篇文章(主函數入口 ) 中,介紹了mongodb會在系統啓動同時,初始化了日誌持久化服務,該功能貌似是1.7版本後引入到系統中的,主要用於解決因系統宕機時,內存中的數據 未寫入磁盤而造成的數據丟失。其機制主要是通過log方式定時將操作日誌(如cud操作等)記錄到db的journal文件夾下,這樣當系統再次重啓時從 該文件夾下恢復丟失的(內存)數據。也就是在_initAndListen()函數體(db.cpp文件第511行)中下面這一行代碼:
   dur::startup();


    今天就以這個函數爲起點,看一下mongodb的日誌持久化的流程,及實現方式。

    在Mongodb中,提供持久化的類一般都以dur開頭,比如下面幾個:

  dur.cpp:封裝持久化主要方法和實現,以便外部使用
  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),如下:

/* * at startup, recover, and then start the journal threads  */
    
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使用類似接口方式,從而約定不同的持久化方式實現,如下:

    class  DurableInterface : boost::noncopyable {
    
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  NonDurableImpl :  public  DurableInterface{  /* 非持久化,基於內存臨時存儲 */
    }

    
class  DurableImpl :  public  DurableInterface {  /* 持久化,支持磁盤存儲 */
    }

 
    再回到startup函數最後一行:boost::thread t(durThread);

    該行代碼會創建一個線程來運行durThread方法,該方法就是持久化線程,如下:

void  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()的實現代碼:

         static   void  go() {
            
if ! commitJob.hasWritten() ){  /* hasWritten一般在CUD操作時會變爲true,後面會加以介紹 */
                commitJob.notifyCommitted();
/* 發送信息已存儲到磁盤的通知 */
                
return ;
            }
            {
                readlocktry lk(
"" 1000 ); /* 聲明讀鎖 */
                
if ( lk.got() ) {
                    groupCommit();
/* 提交一組操作 */
                    
return ;
                }
            }

            
//  當未取到讀鎖時,可能獲取讀鎖比較慢,則直接使用寫鎖,不過寫鎖會用更多的RAM
            writelock lk;
            groupCommit();
        }
  /* * locking: in read lock when called.  */
        
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行):

   DiskLoc DataFileMgr::insert( const   char   * ns,  const   void   * obuf,  int  len,  bool  god,  const  BSONElement  & writeId,  bool  mayAddIndex) {
    ......

    r 
=  (Record * ) getDur().writingPtr(r, lenWHdr); // 位於1588行


     該方法用於將客戶端提交的數據(信息)寫入到持久化隊列(defer)中去,如下(按函數調用順序):

void *  DurableImpl::writingPtr( void   * x, unsigned len) {
        
void   * =  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方法定義如下:

     void  insertWriteIntent( void *  p,  int  len) {
        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設置很計巧的代碼做一下簡要分析,代碼如下:

    CodeBlock::Within w(durThreadMain);


    它的作爲就是一個對多線程訪問指定代碼塊加鎖的功能,其類定義如下(位於race.h):

  class  CodeBlock {
        
volatile   int  n;
        unsigned tid;
        
void  fail() {
            log() 
<<   " synchronization (race condition) failure "   <<  endl;
            printStackTrace();
            abort();
/**/
        }
        
void  enter() {
            
if ++ !=   1  ) fail();  /* 當已有線程執行該代碼塊時,則執行fail */
#if  defined(_WIN32)
            tid 
=  GetCurrentThreadId();
#endif
        }
        
void  leave() {  /* 只有調用 leave 操作,纔會--n,即在線程執行完該代碼塊時調用 */
            
if -- !=   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結束之後,該進程內只允許一個線程執行該代碼塊,呵呵。

 

 

    原文鏈接:http://www.cnblogs.com/daizhj/archive/2011/03/21/1990344.html
    作者: daizhj, 代震軍   
    微博: http://t.sina.com.cn/daizhj
    Tags: mongodb,c++

 

 

發佈了366 篇原創文章 · 獲贊 1 · 訪問量 74萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章