先上一張圖(根據此處重畫),看完下面的內容應該可以理解。
mongodb使用內存映射的方式來訪問和修改數據庫文件,內存由操作系統來管理。開啟journal的情況,數據文件映射到內存2個view:private view和write view。對write view的更新會刷新到磁盤,而對private view的更新不刷新到磁盤。寫操作先修改private view,然后批量提交(groupCommit),修改write view。
WriteIntent
發生寫操作時,會記錄修改的內存地址和大小,由結構WriteIntent表示。

/** 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)。為了避免頻繁加鎖,使用了線程局部變量

/** 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添加到全局的數組里。
何時groupCommit
寫操作會先修改private view,並保存WriteIntent到CommitJob。但是private view是不持久化的,CommitJob保存的WriteIntent何時groupCommit?

const unsigned UncommittedBytesLimit = (sizeof(void*)==4) ? 50 * 1024 * 1024 : 100 * 1024 * 1024;
- durThread線程定期groupCommit,間隔時間可以由journalCommitInterval選項指定。默認是100毫秒(journal文件所在硬盤分區和數據文件所在硬盤相同)或者30毫秒。另外,如果有線程在等待groupCommit完成,或者未commit的字節數大於UncommittedBytesLimit / 2,會提前commit。
- 調用commitIfNeeded。如果未commit的字節數不小於UncommittedBytesLimit,或者是強制groupCommit,則執行groupCommit。
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
- privateMapBytes >= UncommittedBytesLimit
- 前面9次groupCommit都沒有ramap
- durOptions選項指定了DurAlwaysRemap
調用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好很多。