先上一张图(根据此处重画),看完下面的内容应该能够理解。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好不少。