MongoDB源碼概述——啟動處理


       在啟動MongoDB后,程序會對相應的參數,上次遺留的鎖文件,日志文件等等進行相應的處理,同時也會開啟一些支撐其他部分運行的服務線程,為了精讀MongoDB的代碼,領會其全局設計理念,所以我對這些不是特別核心的部分,也通過博文給自己來做一個總結,方便自己以后查閱。

 

  程序在mian函數里進行了對輸入參數的所有處理,程序使用Boost庫實現了跨平台的命令行參數的兼容性,這部分的代碼非常龐大,也非常的亂,所以也沒有必要太過記載,在main函數的底部進行了initAndListen(cmdLine.port, appsrvPath);調用,這個函數就是我們的重點部分。

 

  在void _initAndListen(int listenPort, const char *appserverLoc = NULL)內函數首先進行了一些運行環境的相關檢驗,如:判斷當前系統是不是32bit系統:bool is32bit = sizeof(int*) == 4;接着輸出一些版本信息(這部分代碼就不貼了)后面進行了acquirePathLock()方法的調用。

   void acquirePathLock() {

    string name = ( boost::filesystem::path( dbpath ) / "mongod.lock" ).native_file_string();

        bool oldFile = false;
        //oldFile為true的概念是存在mongod.lock並且它的filesize大於0,。 正常結束的進程會將mongodb.lock大小設置為0(包括CTRL+C)
        if ( boost::filesystem::exists( name ) && boost::filesystem::file_size( name ) > 0 ) {
            oldFile = true;
        }
        // we check this here because we want to see if we can get the lock
        
// if we can't, then its probably just another mongod running

        lockFile = open( name.c_str(), O_RDWR | O_CREAT , S_IRWXU | S_IRWXG | S_IRWXO );
        if( lockFile <= 0 ) {
            uasserted( 10309 , str::stream() << "Unable to create / open lock file for lockfilepath: " << name << ' ' << errnoWithDescription());
        }
        if (flock( lockFile, LOCK_EX | LOCK_NB ) != 0) {   //man 2 flock 查看參數
            close ( lockFile );//若另一個mongod程序鎖住了該文件,則flock LOCK_EX會失敗(即返回結果!=0),拋出下面的異常,此處保證了一個dbpath不能被兩個mongod程序同時打開
            lockFile = 0;
            uassert( 10310 ,  "Unable to acquire lock for lockfilepath: " + name,  0 );
        }
        
        if ( oldFile ) {
            //若上次異常結束
            string errmsg;
            if (cmdLine.dur) {
                //若開啟了-dur屬性
                if (!dur::haveJournalFiles()) {
                    
                    vector<string> dbnames;
                    getDatabaseNames( dbnames );//獲取同一目錄下的所有xx.ns的xx,存入dbnames
                    
                    if ( dbnames.size() == 0 ) {//在沒有任何xx.ns的情況下,不需要修復,所以也就不需要處理了,直接讓程序繼續運行
                        
// this means that mongod crashed
                        
// between initial startup and when journaling was initialized
                        
// it is safe to continue
                    }
                    else {
                        errmsg = str::stream()
                            << "************** \n"
                            << "old lock file: " << name << ".  probably means unclean shutdown,\n"
                            << "but there are no journal files to recover.\n"
                            << "this is likely human error or filesystem corruption.\n"
                            << "found " << dbnames.size() << " dbs.\n"
                            << "see: http://dochub.mongodb.org/core/repair for more information\n"
                            << "*************";
                    }


                }
            }
            else {
                //若沒有開啟了-dur屬性又存在.lock文件,則給出錯誤信息到errmsg
                errmsg = str::stream()
                         << "************** \n"
                         << "old lock file: " << name << ".  probably means unclean shutdown\n"
                         << "recommend removing file and running --repair\n"
                         << "see: http://dochub.mongodb.org/core/repair for more information\n"
                         << "*************";
            }
            //若有上面errmsg有任何錯誤信息,則停止程序
            if (!errmsg.empty()) {
                cout << errmsg << endl;
                close ( lockFile );

                lockFile = 0;
                uassert( 12596 , "old lock file" , 0 );
            }
        }

        // Not related to lock file, but this is where we handle unclean shutdown
        if( !cmdLine.dur && dur::haveJournalFiles() ) {//如果竟不是-dur模式啟動,但又有Journal日志文件,則異常退出,因為無法處理未持久化的數據
            cout << "**************" << endl;
            cout << "Error: journal files are present in journal directory, yet starting without --dur enabled." << endl;
            cout << "It is recommended that you start with journaling enabled so that recovery may occur." << endl;
            cout << "Alternatively (not recommended), you can backup everything, then delete the journal files, and run --repair" << endl;
            cout << "**************" << endl;
            uasserted(13597"can't start without --dur enabled when journal/ files are present");
        }
        uassert( 13342"Unable to truncate lock file", ftruncate(lockFile, 0) == 0);
        writePid( lockFile );//將進程號寫入了mongod.lock文件
        fsync( lockFile );

    }

 對於這段代碼的邏輯,我這里就不羅嗦了,我畫了一張流程圖,我相信圖片比代碼更容易讓人理解。 

 

 oldFile為true的概念是存在mongod.lock並且它的filesize大於0,對於這一點可能大家不太容易理解filesize在這個特定語境的意思,我們來看在db.cpp里注冊的信號處理函數就明白了。

   void  ctrlCTerminate() {    

 log() << "got kill or ctrl-c signal, will terminate after current cmd ends" << endl;
        Client::initThread( "ctrlCTerminate" );
        exitCleanly( EXIT_KILL );
    }

在exitCleanly中 最后調用了shutdownServer函數,負責所有的掃尾工作,對於我們關注的mongo.lock的處理部分如下:

        if  ( lockFile ) { 

    log() << "shutdown: removing fs lock..." << endl;
            if( ftruncate( lockFile , 0 ) )  //man  ftruncate  設置文件大小為0
                log() << "couldn't remove fs lock " << errnoWithDescription() << endl;
            flock( lockFile, LOCK_UN );
        }

 也就是是,只要是正常退出(包括CTRL+C),mongo.lock的大小就會為0,所以可以通過它來判斷上次服務此是怎么結束的。

在_initAndListen 方法中還進行了如下調用:

FileAllocator::get()->start();

一看到這種調用入口,就可以推測出這里肯定使用的是單例模式

在我們調用的start方法中,FileAllocator創建了一個單獨的線程來專門運行其run方法

1  void FileAllocator::start() {
2         boost::thread t( boost::bind( &FileAllocator::run ,  this ) );

 

  FileAllocator::run方法類似於一個單獨的服務的線程,專門提供創建指定大小文件的任務。

 當系統的其他運行部分需要使用它的服務的時候,只需要在那個線程調用 FileAllocator::requestAllocation函數

這個函數會把用戶想要創建的文件名放入list< string >  _pending 中存儲,將其文件名和大小對應關系存入mutable map< string, long >中

同時這個函數還是用了boost::condition來進行同步操作(類似於生產者消費者),在run函數中,放文件創建需求列表(_pending)為空時會進行等待

   if ( fa->_pending.size() == 0 )

        fa->_pendingUpdated.wait( lk.boost() ); 

這樣就防止了無限循環while所帶來了損耗CPU時間的弊端 

若需求列表中有需要創建的item了就會調用系統API進行文件創建 

long fd = open(name.c_str(), O_CREAT | O_RDWR | O_NOATIME, S_IRUSR | S_IWUSR); 

 

通過上面的調用, 文件是被創建了,可是需求卻沒有得到滿足,因為我們創建的文件不是指定大小的,這里有必要解釋一下為什么需要先創建指定大小的文件,因為對於數據庫文件來說,操作很有可能是非常頻繁,這就對我存數據的文件提出了要求,我們總是希望數據庫文件在磁盤上能得到一塊連續的空間(就像內存分配一樣),這樣對文件進行讀寫的時候,文件的fragments比較少,讀起來也會比較快(系統API級別的,不可控的),所以一般希望能夠Handles allocation of contiguous files on disk

 下面來看mongoDB是怎么處理這個問題的:

在調用open創建文件后會進行 ensureLength( fd , size )調用,在ensureLength內部會進行下面的循環

 1              char* buf = buf_holder. get();
 2             memset(buf,  0, z);
 3              long left = size;
 4              while ( left >  0 ) {
 5                  long towrite = left;
 6                  if ( towrite > z )
 7                     towrite = z;
 8 
 9                  int written = write( fd , buf , towrite );
10                 uassert(  10443 , errnoWithPrefix( " FileAllocator: file write failed " ), written >  0 );
11                 left -= written;

看完 之后你會不會很驚訝,居然創建指定大小的文件是通過while循環來做的,沒錯,不管你有沒有驚訝,反正我驚訝了。

后來仔細一想,其實這也是一個還不錯的方案,畢竟暫時我還沒有發現任何可以創建指定大小,空間連續的文件的方法,雖然用上面的方法不能保證空間就一定連續,但是集中在一個時間在占用文件大小,比需要時每次再去寫一下,fragments會少很多。

 

Ok,現在總結一下,FileAllocator的職責就是盡量創建fragments少的指定大小的文件,實現方法為,一個線程專門用於提供創建文件和填充到指定大小的服務,其他的線程調用requestAllocation函數進行需求注冊,服務線程獲取需求,然后工作,其實也可以理解為一種生產者消費者模式,其他的線程生產出需求(文件名,大小),這可以理解為生產者,而服務線程可以理解為消費者,只有需求到達時才運行,否則等待....

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM