在啟動MongoDB后,程序會對相應的參數,上次遺留的鎖文件,日志文件等等進行相應的處理,同時也會開啟一些支撐其他部分運行的服務線程,為了精讀MongoDB的代碼,領會其全局設計理念,所以我對這些不是特別核心的部分,也通過博文給自己來做一個總結,方便自己以后查閱。
程序在mian函數里進行了對輸入參數的所有處理,程序使用Boost庫實現了跨平台的命令行參數的兼容性,這部分的代碼非常龐大,也非常的亂,所以也沒有必要太過記載,在main函數的底部進行了initAndListen(cmdLine.port, appsrvPath);調用,這個函數就是我們的重點部分。
在void _initAndListen(int listenPort, const char *appserverLoc = NULL)內函數首先進行了一些運行環境的相關檢驗,如:判斷當前系統是不是32bit系統:bool is32bit = sizeof(int*) == 4;接着輸出一些版本信息(這部分代碼就不貼了)后面進行了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里注冊的信號處理函數就明白了。
log() << "got kill or ctrl-c signal, will terminate after current cmd ends" << endl;
Client::initThread( "ctrlCTerminate" );
exitCleanly( EXIT_KILL );
}
在exitCleanly中 最后調用了shutdownServer函數,負責所有的掃尾工作,對於我們關注的mongo.lock的處理部分如下:
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方法
2 boost::thread t( boost::bind( &FileAllocator::run , this ) );
}
當系統的其他運行部分需要使用它的服務的時候,只需要在那個線程調用 FileAllocator::requestAllocation函數
這個函數會把用戶想要創建的文件名放入list< string > _pending 中存儲,將其文件名和大小對應關系存入mutable map< string, long >中
同時這個函數還是用了boost::condition來進行同步操作(類似於生產者消費者),在run函數中,放文件創建需求列表(_pending)為空時會進行等待
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內部會進行下面的循環
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函數進行需求注冊,服務線程獲取需求,然后工作,其實也可以理解為一種生產者消費者模式,其他的線程生產出需求(文件名,大小),這可以理解為生產者,而服務線程可以理解為消費者,只有需求到達時才運行,否則等待....