先上一張圖(根據此處重畫),看完下面的內容應該能夠理解。html
mongodb使用內存映射的方式來訪問和修改數據庫文件,內存由操做系統來管理。開啓journal的狀況,數據文件映射到內存2個view:private view和write view。對write view的更新會刷新到磁盤,而對private view的更新不刷新到磁盤。寫操做先修改private view,而後批量提交(groupCommit),修改write view。linux
WriteIntent
發生寫操做時,會記錄修改的內存地址和大小,由結構WriteIntent表示。mongodb
/** Declaration of an intent to write to a region of a memory mapped view * We store the end rather than the start pointer to make operator< faster * since that is heavily used in set lookup. */ struct WriteIntent { /* copyable */ void *p; // intent to write up to p unsigned len; // up to this len void* end() const { return p; } bool operator < (const WriteIntent& rhs) const { return end() < rhs.end(); } // 用於排序 };
查看代碼會發現大量的相似調用,這就是保存WriteIntent。數據庫
getDur().writing(..)
getDur().writingPtr(...)數組
CommitJob
CommitJob保存未批量提交的WriteIntent和DurOp,目前只使用一個全局對象commitJob。對於不修改數據庫文件的操做,如建立文件(FileCreatedOp)、刪除庫(DropDbOp),不記錄WriteIntent,而是記錄DurOp。多線程
ThreadLocalIntents
因爲mongodb是多線程程序,同時操做CommitJob須要加鎖(groupCommitMutex)。爲了不頻繁加鎖,使用了線程局部變量app
/** so we don't have to lock the groupCommitMutex too often */ class ThreadLocalIntents { enum { N = 21 }; std::vector<dur::WriteIntent> intents; };
WriteIntent先存放到intents裏,當intents的大小達到N時,就添加到CommitJob裏,這時候要才須要加鎖。添加intents到CommitJob時,會對重疊的內存地址段進行合併,減小WriteIntent的數量。固然,CommitJob也會對添加的WriteIntent進行檢查是否重複添加。這裏有一個問題,若是intents的大小沒有達到N,是否是永遠都不添加到CommitJob裏呢?不會。由於每次寫操做,必須先得到'w'鎖(庫的寫鎖)或者'W'鎖(全局寫鎖),當釋放鎖的時候,也會把intents添加到全局的數組裏。ide
什麼時候groupCommit
寫操做會先修改private view,並保存WriteIntent到CommitJob。可是private view是不持久化的,CommitJob保存的WriteIntent什麼時候groupCommit?函數
const unsigned UncommittedBytesLimit = (sizeof(void*)==4) ? 50 * 1024 * 1024 : 100 * 1024 * 1024;
groupCommit的過程性能
1.PREPLOGBUFFER
首先是生成寫操做日誌(redo log)。對WriteIntent從小到大排序,這樣能夠對先後的WriteIntent進行重疊、重複的合併。對每一個WriteIntent的地址,和每一個數據文件的private view的基地址進行比較(private view的基地址已經排序,查找很快),找出其隸屬的數據文件的標號。WriteIntent的地址減掉private view的基地址獲得偏移,再從private view把修改的數據複製下來。這樣數據文件標號、偏移、數據,造成一個JEntry。
2.WRITETOJOURNAL
把寫操做日誌壓縮並寫入journal文件。這一步完成以後,即便mongodb異常退出,數據也不會丟失了,由於能夠根據journal文件中的寫操做日誌重建數據。關於journal文件能夠參見這裏。
3.WRITETODATAFILES
把全部寫操做更新到write view中。後臺線程DataFileSync會按期把write view刷新到磁盤中,默認是60秒,由syncdelay選項指定。
4.REMAPPRIVATEVIEW
private view是copy on write的,即在發生寫時開闢新的內存,不然是和write view共用一塊內存的。若是寫操做很頻繁,則private view會申請不少的內存,因此private view會remap,防止佔用內存過多。並非每次groupCommit都會remap,只有持有'W'鎖的狀況下才會remap。
durThread線程的按期groupCommit有三種狀況會remap
調用commitIfNeeded發生的groupCommit,若是持有持有'W'鎖則remap。
remap的一個問題
在_REMAPPRIVATEVIEW()函數中,有這樣一段代碼
#if defined(_WIN32) || defined(__sunos__) // Note that this negatively affects performance. // We must grab the exclusive lock here because remapPrivateView() on Windows and // Solaris need to grab it as well, due to the lack of an atomic way to remap a // memory mapped file. // See SERVER-5723 for performance improvement. // See SERVER-5680 to see why this code is necessary on Windows. // See SERVER-8795 to see why this code is necessary on Solaris. LockMongoFilesExclusive lk; #else LockMongoFilesShared lk; #endif
執行remap時,須要LockMongoFiles鎖。win32下,這把鎖是排他鎖;而其餘平臺下(linux等)是共享鎖。write view刷新到磁盤的時候,也須要LockMongoFiles共享鎖。這樣,在win32下,若是在執行磁盤刷新操做,則remap操做會被阻塞;而在執行remap以前,已經得到了'W'鎖,這樣會阻塞全部的讀寫操做。所以,在win32平臺下,太多的寫操做(寫操做越多,remap越頻繁)會致使整個數據庫讀寫阻塞。
在win32和linux下作了一個測試,不停的插入大小爲10k的記錄。結果顯示以下:上圖win32平臺,下圖爲linux平臺;橫座標爲時間軸,從0開始;縱座標爲每秒的插入次數。很明顯的,linux平臺的性能比win32好不少。