Mongodb在1.8版本之后开始支持journal,就是我们常说的redo log,用于故障恢复和持久化。
持久化为了保证数据永久保存不丢失。MongoDB具有高度可配置的持久化设置,从完全没有任何保证到完全持久化。下面介绍一下内容:
1. MongoDB是如何保证持久化的
2. 如何配置应用程序和服务器持久化等级来满足你的需求
3. 没有启用journal的影响
4. 什么是MongoDB保证不了的
MongoDB可以保证当服务器崩溃或服务器硬关机或硬盘故障时的数据完整性。在关系型数据库中通常是使用事务来保证数据的持久性。MongoDB是不支持事务的,那么是如何来保证的呢?
1. Journaling是干什么的
MongoDB journaling 工作原理
当执行写操作时,MongoDB创建一个journal来包含确切磁盘位置和改变的字节。因此,如果服务器突然崩溃,启动时,journal会重放崩溃前并没有刷新到磁盘上的任何写操作。
数据文件每隔60s刷新到磁盘上,默认情况下,因此journal只需要持有60s内的写入数据。journal预分配了几个空文件用于此目的,位于/data/db/journal,命名为_j.0,j.1等等。
MongoDB运行很长时间情况下,在journal目录下,你会看到类似于_j.6217,_j.6218和_j.6219文件。这些文件是当前的journal文件,如果MongoDB一直运行,这些数字会持续增加。当正常关闭MongoDB时,这些文件将被清除,因为正常关机不在需要这些日志的。
如果服务器崩溃或kill -9, mongodb再次启动时,会重放journal文件,会输出冗长难懂的检验行,这表明在正常的恢复。
1.1 有规划的批提交
默认情况下,MongoDB每隔100ms写入一次journal日志,几兆字节的数据被写入。这意味着mongodb批量提交更改,每个写不立即刷新到磁盘,但是默认设置,你不可能失去写入超过100ms的数据,在每个崩溃事件中。
然而,这种保证对某些应用不是足够强,所以有几种方法可以得到更强的持久化保证。你可以通过把j选项传递给getLastError来确保写入已经写入持久化。getLastError会等待上一次的写操作写入到journal和journaling只会等待30ms而不是100ms,journal下一批写。
> db.foo.insert({"x" : 1})
> db.runCommand({"getLastError" : 1, "j" : true})
> // The {"x" : 1} document is now safely on disk
请注意:如果每次写使用 "j" : true,意味着写的速度将基本控制在 33 writes/sec。(1 write/30ms) × (1000ms/second) = 33.3 writes/second
这一般并不需要太长的时间来刷新写入到磁盘,因此你会发觉写性能提高了,如果你允许mongodb批量大多数写,而不是一个一个的提交。该选项用在重要的写入操作。
提交之前已经提交过的写也是一样的,因此,如果有50个重要的写操作,可以使用“normal” getLastError 不带j选项,然后在最后的一个写操作用j选项,如果成功的,说明50个写操作被安全的刷新到磁盘了。
如果连接了多个写连接,可以使用j选项来并行写入,提高吐吞量,即使延迟高。
1.2 设置提交间隔
为了journaling侵入程度降低的另一种选择是,缩短或延长两者之间的journal提交。执行setParameter 命令来设置journalCommitInterval 值为2ms到500ms之间的值。下面设置journal每隔10ms提交:
> db.adminCommand({"setParameter" : 1, "journalCommitInterval" : 10})
该选项也可以在启动时设置 --journalCommitInterval。
不管间隔设置,调用getLastError "j" : true 时间将减少到三分之一。
如果客户端尝试写入速度比journal刷新快,mongodb将被阻塞,直到journal完成写入磁盘。
在生产环境下,我们强烈推荐开启journal。但是,在某些情况下,可能就希望将其关闭掉。journal影响mongodb的写入速度,即使没有j选项。如果可以容忍数据丢失或更着重速度,那么就禁用journal。
禁用journal日志记录会有个问题,mongodb崩溃后数据的完整性没法保证了。没有journal情况下崩溃,数据可能被破坏了,必须进行修复或更换了。也可能该台的数据没法使用了,或使用过程中突然停止工作了,某些数据损坏丢失了。
如果希望崩溃后,可以继续正常工作,有以下方法:
1. 更换数据文件
这是最好的选择。删除所有的数据目录文件,从备份中恢复,从一个干净的成员中做个快照,或从复制集重新复制,得到一份新的数据。
如果有一个复制集并且是少量的数据,从新复制或许是最好的选择,停止这台,删除数据目录文件,并重新启动复制。
2. 修复数据文件
如果没有备份,没有副本,没有复制集,尽一切的办法补救数据,能恢复多少就是多少了,死马当活马医了。所以,一定要备份数据且保证备份数据可用,这是最后的救命稻草了。
这个情况下,需要使用repair命令了,该命令会删除任何损坏的数据。mongod附带两种修复工具:mongod本身内嵌的和mongodump内嵌的。
mongodump修复可能会发现更多的数据,但是需要很长的时间。此外,如果使用mongodump修复,仍然需要再次启动之前恢复数据的。因此,应该判定多少时间恢复数据是可以接受的。
使用mongod内置的修复,运行mongod加上--repair选项:
# mongod --dbpath /path/to/corrupt/data --repair
当运行在修复状态下,mongodb无法启动监听端口27017,但是可以查看日志,看看在做什么。
请注意:修复过程需要占用大量的磁盘空间,确保磁盘可用空间多于数据大小。如80G的数据需要80G的可用空间。如果当前磁盘空间不够,可通过--repairpath选项指定到挂载的新盘上。
# mongod --dbpath /path/to/corrupt/data --repair --repairpath /media/external-hd/data/db
如果在修复过程中被killed或磁盘空间不足而退出,不会有任何影响的。因为,修复的所有输出是写入到新的文件中,不会更改原始文件直到最后一刻。
mongodump使用repair选项:
# mongodump --repair
3. mongod.lock文件
mongodb数据目录下有一个特殊的文件,就是mongod.lock文件。当运行在禁用journal情况下,它是很重要的。
当正常关闭mongod时,会清除mongod.lock文件,下次启动时知道上次是完全关闭的。相反,如果lock文件没有被清除,mongod没有正常的关闭。
如果mongod检测到没有正常的关闭,不会让你再次启动,需要你复制一份数据。然而,有些人已经意识到,可以通过删除这个lock文件来绕过这个检查。但是,请不要怎么干。在启动时删除lock文件意味着你不知道或不关心你的数据是否已经损坏。除非是这种情况下,请尊重lock文件。如果阻止你启动mongod,修复你的数据,而不是删除lock文件。
4. 异常关机
不要删除锁定文件的一个重要原因是,你甚至可能不会注意到硬盘崩溃。假设重启服务器,初始化脚本在服务器关闭前停止mongod,然而,初始化会尝试优雅关闭进程,如果关不掉就会硬杀掉它。在繁忙的系统,mongodb可能需要更长的时间来关闭,init脚本不会等待它关闭,很粗暴的硬关机。
1. 载体崩溃
当出现硬件问题或文件系统出错,特别是硬盘损坏了,就没法保护数据安全了。另外,不同品牌的硬件和软件具有不同的持久保证的。
总而言之,如果硬件或文件系统破坏了数据,MongoDB是无法保护数据的,这已经属于底层存储的事情了。可以使用复制来避免这个问题,其实就是单点问题了。
2. 检查损坏
validate命令用来检测一个集合的损坏,如:
在上面输出底部,有个"valid" : true 说明没有损坏,如果不是,会找到一些损坏的细节。大部分输出是描述集合内部的结构,对于调试不是特别有用的。
只能检测集合,不能检测索引。
如果出现无效的BSONbj,通常就是损坏了。最糟糕的是出现pdfile,pdfile基本上是mongodb的数据存储核心,几乎可以判断数据文件已经损坏了。
如果出现损坏,会看到下面的日志信息:
Tue Dec 20 01:12:09 [initandlisten] Assertion: 10334:
Invalid BSONObj size: 285213831 (0x87040011)
first element: _id: ObjectId('4e5efa454b4ae20fa6000013')
如果第一个元素显示的是垃圾,不需要做什么。如果第一个元素是可见的,可以移除损坏的文档。如:
> db.remove({_id: ObjectId('4e5efa454b4ae20fa6000013')})
如果损坏并不局限于该文档,这种技术可能无法工作,你仍然需要进行修复。
3. 复制的持久性
复制集一个写可被回滚,直到它被写入到一个大多数的集合中。
> db.runCommand({"getLastError" : 1, "j" : true, "w" : "majority"})
只能保证primary写入已经持久化,secondary未必持久化。
Mongodb在1.8版本之后开始支持journal,就是我们常说的redo log,用于故障恢复和持久化。
1. 启动
启动journal功能使用mongod --journal选项,也可以关闭--nojournal,在2.0之后的版本,journal都是默认打开的,以确保数据安全。在version < 2.0 或者32位的系统上都是默认关闭的 。因为打开journal会使用更多的内存(下面会详细介绍),而32位系统支持的内存太小,所以关闭了。
由于Mongodb会事先初始化journal空间,而且在初始化完成之前是不会打开监听端口的,所以启动后可能会有一段时间连不上,不用紧张,查看日志,待journal初始化完成之后再连接。这里也建议,尽量使用ext4或者xfs等文件系统,诸如ext3这样的文件系统,初始化磁盘会非常慢,你会看到启动mongod之后,很长一段时间都停留在打印日志的状态,而用ext4会瞬间完成。而且Mongodb在运行时对db的空间也采用预分配的机制,所以使用更高级的文件系统是很有帮助的,防止磁盘引起的高并发下拥堵问题。
2. 文件、恢复和备份
journal存放在数据文件的/journal/文件夹下,运行时的文件一般是这样的
其中j.32,j.33是使用中的journal文件,当单个文件达到1GB的时候,就会创建一个新的文件,旧文件不会循环使用,自动删除。
lsn保存最后使用的journal序列号,是个2进制文件,它实际保存的是系统启动到现在的一个时间戳。
prealloc.2是还未使用的初始化的journal文件。使用db.shutdownServer()和kill -2关闭的系统,也就是clean shutdown,journal文件夹下除prealloc.*文件 都会被删除。
如果系统掉电或者运行时死机,再启动时,mongo就会使用journal进行恢复,不用运行repair。
我们可以将journal,oplog,data做快照备份,在数据丢失的时候,可以恢复到最近的状态,保证安全。盛大的云计算系统就是这样做的,同时使用go语言做异步备份,有机会可以跟他们交流。
3. 批量提交
journal除了故障恢复的作用之外,还可以提高写入的性能,批量提交(batch-commit),journal一般默认100ms刷新一次,在这个过程中,所有的写入都可以一次提交,是单事务的,全部成功或者全部失败。关于刷新时间,它是可以更改,范围是2-300ms,但是这并不是绝对的。mongodb提供了journal延迟测试的函数:
> db.runCommand("journalLatencyTest")
在实际运行中,刷新时间是--journalCommitInterval设置和延迟测试中较大的一个。
不得不吐槽一下,有的服务器磁盘有cache却没有电池,情何以堪,在不走cache的情况下,延迟相当大,图中就是不走cache的情况。mongo也是支持ssd的,有条件可以使用。在比较繁忙的系统上,当journal和data放在一个volume上的时候,这个值也会比较大。
查看journal运行情况:
> db.serverStatus():
commits:在journalCommitInterval时间内提交的操作数。
journaledMB:在journalCommitInterval时间内写到journal文件中的数据量 。
writeToDataFilesMB:在journalCommitInterval时间内从journal刷新到磁盘的数据量 。
compression:v>2.0,表示客户端提交写入到journal的数据的压缩比率,注意,写入到journal的数据并不是全部的数据。( journaled_size_of_data / uncompressed_size_of_data ) 。
commitsInWriteLock:在有写锁的情况下提交的数量,这表示写的压力很大。
earlyCommits:表示在journalCommitInterval之前的时间,mongod请求提交的次数。用这个参数确定journalCommitInterval是不是设置的过长。
dur.timeMS.prepLogBuffer:从privateView映射到Logbuffer的时间。
dur.timeMS.writeToJournal:从logbuffer刷新到journalfile 的时间。
dur.timeMS.writeToDataFiles:从journalbuffer映射到MMF,然后从MMF刷新到磁盘的时间,文件系统和磁盘会影响写入性能。
dur.timeMS.remapPrivateView:重新映射数据到PrivateView的时间,越小性能越好。这个之后会介绍,这也是为什么journal会使用更多内存的原因,因为journal会另外使用一个叫PrivateView的内存区域。
4. 总结:
mongodb在使用journal之后,备份,容灾得到保障,批量提交也使得写入更加快速(不持久化的不算)。我们也需要选用较高级的文件系统和磁盘还有更多的内存来保障journal的良好运行。
当系统启动时,mongodb会将数据文件映射到一块内存区域,称之为Shared view,在不开启journal的系统中,数据直接写入shared view,然后返回,系统每60s刷新这块内存到磁盘,这样,如果断电或down机,就会丢失很多内存中未持久化的数据。
当系统开启了journal功能,系统会再映射一块内存区域供journal使用,称之为private view,mongodb默认每100ms刷新privateView到journal,也就是说,断电或宕机,有可能丢失这100ms数据,一般都是可以忍受的,如果不能忍受,那就用程序写log吧。这也是为什么开启journal后mongod使用的虚拟内存是之前的两倍。
Mongodb的隔离级别是read_uncommitted,不管使用不使用journal,都是以内存中的数据为准,只不过,不开启journal,数据从shared view读取,开启journal,数据从private view读取。
在开启journal的系统中,写操作从请求到写入磁盘共经历5个步骤,在serverStatus()中已经列出各个步骤消耗的时间。
①、Write to privateView
②、prepLogBuffer
③、WritetoJournal
④、WritetoDataFile
⑤、RemaptoPrivateView
下面详细介绍每个步骤的过程:
流程图:
1、preplogbuffer:
Private view(PV) 中的数据并不是直接刷新到journal文件,而是通过一个中间内存块(journalbuffer,或者alogned buffer)一部分一部分的刷新到journal,这样可以提高并发。
preplogbuffer即是将PV中的数据写入到aligned buffer中的过程。这个过程有两部分,basic write 操作和非 basic write操作(e.g.create file)。
一次preplogbuffer是以一个commitJob为一个单位,可能会有很多个commitJob写入到aligned buffer,然后提交。
一个commitJob中包含多个basic write 和非basic write 操作,basic write是存在Writeintent结构体中的,Writeintent记录了写操作的地址信息。非basic write 操作存在一个vector中。
具体结构如下。
Aligned buffer 有自己的结构,这也是写入到journalfile中的结构。包含Jheader,JsectHeader lsn,Durop,JSectFooter:
每个JsectHeader之间的Durop是属于一个事务范围,一起提交,一起成功,一起失败,即all-or-nothing.上篇文章中介绍的lsn文件,就是记录这个lsn号。
2、WritetoJournal:
writetoJournal操作是将alignedbuffer刷新到JournalFile的过程。默认100ms刷新一次,由--journalCommitInterval 参数控制。writetoJournal会做一些checksum验证,将alignedbuffer进行压缩,然后将压缩过后的alignedbuffer写入到磁盘。写入磁盘后将删除已经满的Journal文件,更新lsn号到lsn文件。写操作到这一步就是安全的了,因为数据已经在磁盘上,如果使用getlasterror(j=true),这一步即可返回。
3、WritetoDataFile:
WritetoDataFile是将未压缩的aligned buffer写入到shared view的过程,然后由操作系统刷新到磁盘文件中。WritetoDataFile首先会对aligned buffer进行严格的验证,确保没有改变过,然后解析aligned buffer,通过memcpy函数拷贝到shareview
4、RemaptoprivateView:
RemaptoprivateView会将持久化的数据重新映射到PV,以减小PV的大小,防止它不断扩大,按照源码上说,RemaptoprivateView会两秒钟重新映射一次,大约有1000个view,不是一次全做完,而是一部分一部分的做。由于读操作是读取PV,所以在映射完成之后会有短暂的时间读取磁盘。
经过这四步,一个写操作就完成了,journal提高了数据的安全性,并不像想象中的会丢数据,重要的是如何使用和维护。
以上均参考自mongo官方文档和源码,有理解不对的地方也请大家指正。