mongodb持久化

先上一張圖(根據此處重畫),看完下面的內容應該能夠理解。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

查看代碼會發現大量的相似調用,這就是保存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;
};
ThreadLocalIntents

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;
UncommittedBytesLimit
  • 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
View Code

執行remap時,須要LockMongoFiles鎖。win32下,這把鎖是排他鎖;而其餘平臺下(linux等)是共享鎖。write view刷新到磁盤的時候,也須要LockMongoFiles共享鎖。這樣,在win32下,若是在執行磁盤刷新操做,則remap操做會被阻塞;而在執行remap以前,已經得到了'W'鎖,這樣會阻塞全部的讀寫操做。所以,在win32平臺下,太多的寫操做(寫操做越多,remap越頻繁)會致使整個數據庫讀寫阻塞。

在win32和linux下作了一個測試,不停的插入大小爲10k的記錄。結果顯示以下:上圖win32平臺,下圖爲linux平臺;橫座標爲時間軸,從0開始;縱座標爲每秒的插入次數。很明顯的,linux平臺的性能比win32好不少。

相關文章
相關標籤/搜索