mongodb持久化


先上一張圖(根據此處重畫),看完下面的內容應該可以理解。

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

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

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;
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好很多。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM