最近工作需要開始研究mongoDB,我準備從其原始碼角度,對於mongod和mongos服務的架構、sharding策略、replicaset策略、資料同步容災、索引等機製做一個本質性的瞭解。其代碼約20萬行(我研究的是 2.0.6版本源碼),本篇先從mongod的啟動流程說起,它本是一個多線程程式,所以本文在於說明mongod有多少個線程,每個線程的意義所在。希望大家閱讀本文時關注在mongod的外圍架構,暫不涉及資料檔案的組織、索引B樹的組織等,僅focus in在網路架構、執行緒模式上。
弄清楚這點的好處很明顯:之後就可以有的放矢的研究mongod某個模組究竟是如何?的,可以快速的跳到相應的類中閱讀源碼,解決我們在產品中的實際問題。我認為這是研究其龐大源碼一個好的開始。
在說明mongod前,須瞭解mongoDB大量代碼是基於boost庫構建的,因此這裡先行對boost庫建立線程做個簡單的瞭解。
1、boost庫如何建立線程
boost::thread是boost中跨平台的多線程庫,mongoDB建立線程時大多數情況下是使用thread庫的(少量情況直接調用pthread_create方法),主要使用了以下兩種方式:
(1)直接運行讓線程運行func
例如durThread線程:
void durThread() {
while( !inShutdown() ) { ... }
}
boost::thread t(durThread);
(2)在類中定義靜態run方法,調用thread建立線程
class FileAllocator : boost::noncopyable {
static void run( FileAllocator * fa );
void FileAllocator::start() {
boost::thread t( boost::bind( &FileAllocator::run , this ) );
}
};
2、mongod的入口
mongod的入口main函數在src/mongo/db/db.cpp檔案中,我畫了個簡單的活動圖表簡要介紹其啟動流程:
如所示,這裡出現了12個固定線程,還沒有包括mongod運行以後處理請求時派生出來的線程,如下所示:
– interruptThread
– DataFileSync::run
– FileAllocator::run
– durThread
– SnapshotThread::run
– ClientCursorMonitor::run
– PeriodicTask::Runner::run
– TTLMonitor::run
– replSlaveThread
– replMasterThread
– webServerThread
– 處理資料庫請求的主線程
如果不屬於任何replica set,那麼至少有10個固定線程(去除 replSlaveThread和 replMasterThread)。
下面我們先討論這10個固定的線程,再討論效能非常弱的監聽web事件的線程是怎樣處理請求的,最後討論效能稍好一點的主服務線程是怎樣處理請求的。
3、5個基於BackgroundJob類實現的背景工作執行緒
這5個線程分別是DataFileSync,SnapshotThread, ClientCursorMonitor, TTLMonitor, PeriodicTask,類圖如下所示:
上面這5個類也是用boost::threadfunction方法建立線程啟動並執行,它們繼承了BackgroundJob類,使用go方法啟動線程執行jobBody就是在啟動線程執行run方法,如下所示:
BackgroundJob& BackgroundJob::go() { boost::thread t( boost::bind( &BackgroundJob::jobBody , this, _status ) ); return *this; } void BackgroundJob::jobBody( boost::shared_ptr<JobStatus> status ) { ... run(); ... }
這些線程的意義如下:
DataFileSync主要在調用MemoryMappedFile::flush方法將記憶體中的資料刷到磁碟上。 我們知道,mongodb是調用mmap把磁碟中的資料對應到記憶體中的,所以必須有一個機制時刻的刷資料到硬碟才能保證可靠性,多久刷一次是與syncdelay參數相關的。
SnapshotThread將產生快照檔案協助快速恢複。
ClientCursorMonitor將系統管理使用者的遊標,每4秒調用一次idleTimeReport()方法,每一分鐘調用sayMemoryStatus()方法。
TTLMonitor管理TTL,通過調用doTTLForDB()方法檢查所有db。
PeriodicTask將從動態數組std::vector<PeriodicTask* > _tasks中擷取週期性任務執行。
4、5個直接提供全域方法執行的線程
FileAllocator用於分配新檔案,它決定分配檔案的大小,例如用翻倍的方式。
interruptThread只處理訊號量。
durThread做批量提交和復原工作。
replSlaveThread是當前結點作為secondary時的同步線程。
replMasterThread是當前結點作為master時的同步線程。
5、web監聽線程
mongod是如何處理web請求的呢?它是通過網路架構中的核心類Listerner實現的,類圖如下所示:
怎麼理解這幅類圖呢?
首先看 Listener類,它負責監聽、建立新串連,其工作步驟如下:
a、建立socket控制代碼,綁定連接埠,監聽
b、調用select檢測新串連事件
c、對偵測到的事件調用accept建立新串連
d、調用void Listener::acceptedMP(MessagingPort*mp)方法處理新串連,誰重新實現acceptedMP方法誰決定處理方式
這個Listener類既用於處理web請求,也用於處理普通的資料庫請求。
OK,現在我們看web請求是如何處理的。MiniWebServer類繼承了Listener類,它重新實現了acceptedMP方法,開始接收TCP流,解析HTTP協議,同時還會負責組裝HTTP響應包並發送TCP流到用戶端。那麼實際完成http請求的類是誰呢?它是繼承了MiniWebServer類的DbWebServer類。這個類重新實現了doRequest方法,它會在完整接收到HTTP請求後被調用,HTTP請求的處理過程不在本篇的討論範圍內,這裡略過。但我們清楚了,這個線程採用同步的阻塞的方式處理請求,它意味著它同一時刻只能處理一個web請求,並發能力超級弱,還好web請求只是mongod的副業,僅用於查詢狀態。
6、主監聽線程和資料請求的處理線程
處理資料庫請求的是中的PortMessageServer 類,它運行在主線程中。
我們先看看PortMessageServer 類是如何?acceptedMP方法的:
virtual voidacceptedMP(MessagingPort * p) {if ( !connTicketHolder.tryAcquire() ) {sleepmillis(2); // otherwisewe'll hard loopreturn;} …int failed =pthread_create(&thread, &attrs, (void*(*)(void*)) &pms::threadRun,p);…}
很清晰,它開啟了一個線程獨立的執行這個請求。雖然這種方式依然效能極差:大量的進程間環境切換在等著我們,但總比web請求處理要好多了,而且mongod的並發能力本來就不是它的長項。
對於每個新串連,都會有類封裝成對象,如下:
接下來pms::threadRun方法是在處理MessagingPort對象。
下面看看pms::threadRun方法中做了些什麼:
void threadRun( MessagingPort *inPort) {TicketHolderReleaserconnTicketReleaser( &connTicketHolder );Message m;try {LastError * le = newLastError();lastError.reset( le ); //lastError now has ownershiphandler->connected( p.get());while ( ! inShutdown() ) {if ( ! p->recv(m) ) {p->shutdown();break;}handler->process( m ,p.get() , le );}}handler->disconnected( p.get());}
可以看到,它會在這個串連上接收完整的請求,之後會調用handler的process方法。這個handler又是什麼呢?如所示:
所以,普通的資料庫請求是由MyMessageHandler的process方法處理的。這個方法裡也只是個封裝,真正處理業務的是全域方法assembleResponse。
assembleResponse方法中會按照8種操作方式分別的調用DataFileMgr中的方法處理實際檔案,例如:
enum Operations {opReply = 1, /* reply. responseTo is set. */dbMsg = 1000, /* generic msg command followed by a string */dbUpdate = 2001, /* update object */dbInsert = 2002,//dbGetByOID = 2003,dbQuery = 2004,dbGetMore = 2005,dbDelete = 2006,dbKillCursors = 2007};
在方法中有類似這樣的代碼在調用實際的業務類處理操作:
else if ( op == dbInsert ) { receivedInsert(m, currentOp); } else if ( op == dbUpdate ) { receivedUpdate(m, currentOp); } else if ( op == dbDelete ) { receivedDelete(m, currentOp); }
當然本篇志不在此,下篇我們再討論索引和資料檔案的操作。