在啟動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函數進行需求註冊,服務線程擷取需求,然後工作,其實也可以理解為一種生產者消費者模式,其他的線程生產出需求(檔案名稱,大小),這可以理解為生產者,而服務線程可以理解為消費者,只有需求到達時才運行,否則等待....