数据存储:
之前在介绍Journal的时候有说到为什么MongoDB会先把数据放入内存,而不是直接持久化到数据库存储文件,这与MongoDB对数据库记录文件的存储管理操作有关。MongoDB采用操作系统底层提供的内存文件映射(MMap)的方式来实现对数据库记录文件的访问,MMAP可以把磁盘文件的全部内容直接映射到进程的内存空间,这样文件中的每条数据记录就会在内存中有对应的地址,这时对文件的读写可以直接通过操作内存来完成(而不是fread,fwrite之辈).
这里顺便提一句,MMAP的只是将文件映射到进程空间,而不是直接全部map到物理内存,只有访问到这块数据时才会被操作系统以Page的方式换到物理内存。这部分的管理工作由操作系统完成,对于MongoDB的开发者而言,也是透明的.其实我们所能用的所有函数,包括系统内核里的实现函数,操作的统统都是虚拟内存,也就是每个进程所谓的4GB(32位系统)的虚拟地址空间.物理内存对于用户是不可见的,不可操作的。这也就是为什么MongoDB可以存储比内存更大的数据,但是却不建议热数据超过内存大小的原因。因为热数据大于内存的话,操作系统需要频繁的换入换出物理内存中的数据,会严重影响MongoDB的性能。
32位操作系统进程虚拟内存表图:
使用这种内存管理方式极大的减轻了MongoDB开发者的负担,把大量的内存管理的工作交由操作系统来完成,在写这篇文章的时候我自个儿我总结了下她的特点,可是后面发现有本书上有总结,于是直接贴上来(加了几个下划线),没办法,人家比我总结得好。
• MongoDB’s code for managing memory is small and clean, because most of that work is pushed to the operating system.
• The virtual size of a MongoDB server process is often very large, exceeding the size of the entire data set. This is OK, because the operating system will handle keeping the amount of data resident in memory contained.
• MongoDB cannot control the order that data is written to disk, which makes it
impossible to use a writeahead log to provide single-server durability. Work is
ongoing on an alternative storage engine for MongoDB to provide single-server
durability.
• 32-bit MongoDB servers are limited to a total of about 2GB of data per mongod.
This is because all of the data must be addressable using only 32 bits.
(如果你想了解更多MMAP相关的东东,可以翻阅《Unix网络编程 卷二》的12.2节)
好了,抽象的东西讲述完毕,下面来点硬货!!!
存储源码分析:
在MongoMMF类的定义(momgommf.h 29)中需要注意一下几个方法:
void* map(const char *filename, unsigned long long &length, int options = 0 );
//将文件filename以MMAP的方式映射到进程的空间(称之为视图),返回在内存中的首地址
//如果文件不存在,会通过mmap_win里的CreateFile创建文件
void flush(bool sync);
//将映射到进程空间的数据Flush到磁盘
void* getView() const
//获取视图首地址
关于这三个方法的内部实现,自然我们可以想到是对操作系统的API的调用,对于不同的操作系统,方法签名以及参数还有变化,在这里我就不罗嗦了,各个系统的API都查得到。所以我们这里也并不会贴出其内部调用的系统API.
究竟MongoDB是什么时候map数据库文件到内存的呢?又是何时将内存中映射的数据flush到磁盘进行持久化的呢?下面我们来分析一下这两个问题。
map数据库文件到内存:
在我们第一次向一个未创建的数据库插入一条记录时,调用的函数会由如下流程:
DataFileMgr::insert()——》Database::allocExtent()——》Database::suitableFile()——》 Database::getFile()——》MongoDataFile::open()——》 MongoMMF::create()
DataFileMgr::insert()之前有些方法我已经省略了,这个调用流程比较长,但是最终会调用到MongoMMF::create()来创建第一个数据库文件
bool MongoMMF::create(string fname, unsigned long long& len, bool sequentialHint) {
setPath(fname);
_view_write = map(fname.c_str(), len, sequentialHint ? SEQUENTIAL : 0);
//如果文件不存在,会通过mmap_win里的CreateFile创建文件,MemoryMappedFile::map方法
return finishOpening();
}
观察代码后我们发现create方法直接调用了map,而map的内部,就有文件创建功能,创建完后就map到内存了。
若是向现有数据库插入记录,则在Database构造的期间会调用openAllFiles(),进入上面流程的Database::getFile()部分
终上所述两种情况,我们明白了MongoDB何时将数据库记录文件map到内存.
Flush数据进行持久化:
MongoDB中默认每分钟Flush一次进行持久化存储,当然这个间歇可以通过"--syncdelay"启动参数来进行设置.执行流程为main()——》dataFileSync.go()。DataFileSync派生自BackgroundJob,其go()方法会创建一个新的线程来运行虚函数run()。
void run() {
if( cmdLine.syncdelay == 0 )
log() << "warning: --syncdelay 0 is not recommended and can have strange performance" << endl;
else if( cmdLine.syncdelay == 1 )
log() << "--syncdelay 1" << endl;
else if( cmdLine.syncdelay != 60 )//默认是60
log(1) << "--syncdelay " << cmdLine.syncdelay << endl;
int time_flushing = 0;
while ( ! inShutdown() ) {
flushDiagLog();
if ( cmdLine.syncdelay == 0 ) {
// in case at some point we add an option to change at runtime
sleepsecs(5);
continue;
}
sleepmillis( (long long) std::max(0.0, (cmdLine.syncdelay * 1000) - time_flushing) );
if ( inShutdown() ) {
// occasional issue trying to flush during shutdown when sleep interrupted
break;
}
Date_t start = jsTime();
//当前dataFileSync的任务就是在一段时间后(cmdLine.syncdelay)将内存中的数据flush到磁盘上(因为mongodb使用mmap方式将数据先放入内存中)
int numFiles = MemoryMappedFile::flushAll( true );
time_flushing = (int) (jsTime() - start);
globalFlushCounters.flushed(time_flushing);
log(1) << "flushing mmap took " << time_flushing << "ms " << " for " << numFiles << " files" << endl;
}
}
Run()最后调用MemoryMappedFile::flushAll方法对所有的映射文件进行flush操作,将更改持久化到磁盘.前面在介绍MongoMMF的时候就介绍过此方法.这里不再累述。
这里顺便提一句,其实mmap不调用fsync强刷到磁盘,操作系统也是会帮我们自动刷到磁盘的,linux有个dirty_writeback_centisecs参数用于定义脏数据在内存停留的时间(默认为500,即5秒),过了这个timeout时间就会被系统刷到磁盘上。在这个自动刷的过程中是会阻塞所有的IO操作的,如果要刷的数据特别多的话,容易产生一些长耗时的操作,例如有些使用mmap的程序每隔一段时间就会出现有超时操作,一般的优化手段是考虑修改系统参数dirty_writeback_centisecs,加快脏页刷写频率来减少长耗时。mongodb是定时强刷,不会有此问题。
问题的出现:
弄清楚了MongoDB的存储引擎何时将数据库记录文件map到进程的内存空间以及何时flush到原文件时,不知道您发现了问题没有?持久化的flush过程是每分钟调用一次,而写数据是时时刻刻进行的,若还没有到一分钟,在59秒的时候服务器断电了怎么办?是不是这59秒内对数据库的所有操作都不会提交到持久化的数据库文件?丢失59秒的数据,这还不是最可怕的. 如果在60秒后,在进行flushAll的过程中系统宕机,则会造成数据文件错乱,一部分是新数据,一部分是旧数据,这种情况下,有可能我们的数据库就不能用了。
不知道为什么,MongoDB在正确的退出流程中(调用dbexit(EXIT_CLEAN)),非"--dur模式启动 也并没有调用MemoryMappedFile::flushAll来进行持久化操作,这令我非常费解.一开始我以为是我这个版本的代码没有完善,立马又查阅了2.2版本的源码,发现也并没有在非"--dur"调用flush方法。都仅仅是调用MemoryMappedFile::closeAllFiles.
我个人的理解是,在生产环境下一定会开启"--dur",甚至在新版本中在64位运行环境下默认开启,所以给非dur模式下来一次flush就不那么必要了.
如果您在使用MongoDB的windows版本进行调试的以验证我上面的描述的话,您会得到相反的结果,可能你的第一感觉就会是我完全的搞错了。的确,一般的人都会这样认为,我们来进行一次简单的测试流程:
- 以非"--dur"模式启动Mongod,启动时最好调整一下--syncdelay,设置一个较大值如600
- 使用mogo对数据库的数据进行修改(如修改删除)
- 使用任务管理器强制结束进程mongod(模拟系统宕机)
- 删除掉mongod.lock(模拟宕机一定会留下这个),重新启动非"--dur"模式的Mongod
- 使用mongo进行db.collectiob.find()观察第一次的更改是否已经生效
使用上述测试流程,您会惊奇的发现,我们的任何更改都已经持久化了,这样是不是就说明我前面所提到的都是胡扯呢?起初我自己也有点怀疑这个结果,反复的测试了很多遍,并进行了跟踪调试,我发现即便MongoDB没有运行过一次flushAll,并且连任何一个MongoMMF类的对象(代表一个数据库记录文件)也不曾调用flush()方法,所做的更改仍然能被持久化。至此,我开始怀疑Windows上并不是显示调用flush才会持久化,而是memcopy更改时就会被持久化,搜索了一下网上,发现了别人在Windows也遇到了相同的问题.(CSDN上命名为 "内存映射,没有FlushViewOfFile,也可以保存到文件"的贴子也遇到了相同的问题).
对于Windows这个特例,我也就不再深究了,大家知道是这个地方的问题就OK了,其实在它的这种机制下,整个用于flush数据到磁盘的DataFileSync线程都不用,对于Linux,Unix,我上面的总结还是正确的.
问题的解决:
事实上曾经有人就是因为上面提到的问题丢失了所有数据,所以MongoDB的团队成员才在1.7版本的最新分支上开始对单机高可靠性的提升,这就是引入的Journal\durability模块,着重解决这个问题。(导火索见文章"MongoDB的数据可靠性,单机可靠性有望在1.8版本后增强“)
在MongoDB源码概述——日志 一文中也提到这个Journal\durability模块,不过最后还有一部分没有讲完,下次将会有专门的博文介绍后续问题。