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好不少。

相关文章
相关标签/搜索