MongoDB的日志记录是否保证数据持久性?

11
即使开启了日志记录,MongoDB仍有可能丢失写入吗?
“默认情况下,未记录在日志中的最大丢失写入是在最后100毫秒内进行的。”这是来自管理日志记录的内容,这表明您可能会失去自上次将日志刷新到磁盘以来所做的写入。
如果想要更多的耐久性,“要强制mongod更频繁地提交到日志,请指定j:true。 当具有j:true的写入操作挂起时,mongod将减少journalCommitInterval到设置值的三分之一。”
即使在这种情况下,似乎将日志刷新到磁盘是异步的,因此仍有可能丢失写入。我是否忽略了确保不丢失写入的方法?

是的,从技术上讲,在1/1000秒(我认为是这样,可能是1/10000秒)内您可以丢失数据,我想您只需要考虑是否真的会有足够的流量来使其成为问题,个人认为这是不可能的。 - Sammaye
值得一提的是,日志记录与耐久性无关,它确保在计划外关闭后数据文件的一致性。这就是它的工作。磁盘写入与耐久性更相关,在MongoDB中,这恰好是日志记录的作用。但即使在这种情况下,它也不是“直接写入磁盘”。 - Sammaye
5个回答

20

为了澄清问题,我发布了一个新答案。我进行了测试并再次阅读了源代码,我确定烦恼来自于写关注文档中的一句不幸的话。启用日志记录并使用j:true写关注时,写操作是耐久的,并且没有神秘的数据丢失窗口。

即使启用了日志记录,MongoDB仍然有可能丢失写操作吗?

是的,因为耐久性也取决于单个操作的写关注。

"默认情况下,最大程度上丢失的写入(即未被写入日志的写入)是在最后100毫秒内进行的。"

这是从管理日志中得出的结论,它表明您可能会失去自上次将日志刷新到磁盘以来所做的写入。

没错。日志由单独的线程异步刷新,因此您可能会失去自上次刷新以来所有内容。

如果我想要更高的耐久性,“要强制mongod更频繁地提交日志,可以指定j:true。当带有j:true的写操作正在等待时,mongod将把journalCommitInterval减少到设置值的三分之一。”

这也让我感到烦恼。这是它的意思:

当您发送带有j:true的写操作时,它不会立即触发磁盘刷新,也不会在网络线程上触发。这是有道理的,因为可能有数十个应用程序与同一个mongod实例交谈。如果每个应用程序都大量使用日志记录,则db将非常缓慢,因为它一直在fsync。

相反,发生的情况是,“耐久性线程”将获取所有待处理的日志提交并将其刷新到磁盘。该线程的实现方式如下(我的评论):

sleepmillis(oneThird); //dur.cpp, line 801
for( unsigned i = 1; i <= 2; i++ ) {
  // break, if any j:true write is pending
  if( commitJob._notify.nWaiting() )
    break;
  // or the number of bytes is greater than some threshold
  if( commitJob.bytes() > UncommittedBytesLimit / 2  )
    break;
  // otherwise, sleep another third
  sleepmillis(oneThird);
}

// fsync all pending writes                                      
durThreadGroupCommit();

因此,待处理的j:true操作将导致日志提交线程比通常情况下更早地提交,并将提交所有未设置j:true的挂起写入到日志中。

即使在这种情况下,似乎将日志刷新到磁盘是异步的,因此仍有可能丢失写入。我是否缺少了确保不丢失写入的方法?

使用j:true日志记录写入关注点进行的写入(或getLastError命令)将等待持久性线程完成同步,因此不存在数据丢失的风险(只要操作系统和硬件可以保证)。

句子“但是,在日志提交之间存在一个时间窗口,写入操作尚未完全持久”可能指的是启用了日志记录的mongod接受了不使用j:true写入关注点的写入。在这种情况下,由于上次日志提交以来,写入可能会丢失。

我已经为此提交了文档错误报告


1
文档已更新,内容如下:“如果 j: true,请求确认指定在 w: <value> 中的 mongod 实例已写入到磁盘日志。” - undefined

3
也许是的。是的,它等待数据被写入,但根据文档,有一个'日志提交之间存在一个窗口,该写操作未完全持久化', 不知道这是什么意思。我找不到他们所指的内容。
我在此处留下编辑后的答案,但我来回反复,所以有点烦人。
这有点棘手,因为你可以拉很多杠杆:
你的 MongoDB 设置
假设启用了日志记录(64 位默认值),则日志将在定期间隔中提交。如果日志和数据文件在同一块设备上,则 journalCommitInterval 的默认值为 100ms,否则为 30ms(因此最好将日志放在单独的磁盘上)。
您还可以将 journalCommitInterval 更改为至少 2ms,但它会增加写操作的数量并降低总体写入性能。
写入关注点
您需要指定写入关注点,告诉驱动程序和数据库等待数据写入磁盘。 然而,这不会等待数据实际写入磁盘,因为在默认设置下,在恶劣情况下可能需要 100ms。

因此,最好的情况下,有一个2毫秒的时间窗口可以丢失数据。然而,这对于许多应用程序来说是不足够的。

fsync命令会强制刷新所有数据文件到磁盘,但如果您使用日志记录,则这是不必要的,而且效率低下。

现实生活中的耐久性

即使您每次写入都进行日志记录,如果数据中心管理员在您的硬件上使用电锯或硬件本身出现故障,那么这有什么好处呢?

在许多情况下,不像RAID那样在块设备级别上进行冗余存储,而是在更高的级别上进行冗余存储,是更好的选择:将数据放置在不同的位置或至少在不同的机器上使用副本集,并使用启用了日志记录的w:majority写关注(尽管日志记录仅适用于主服务器)。在单个机器上使用RAID以增加您的运气。

这提供了性能、耐久性和一致性的最佳平衡。此外,它允许您为每个写操作调整写入关注度,并具有良好的可用性。如果数据在三台不同的机器上排队等待下一个fsync,那么在任何一台机器上进行下一个日志提交可能仍需要30毫秒(最坏情况),但是在30毫秒间隔内三台机器崩溃的可能性可能比电锯大屠杀管理员场景低百万倍。

证据

简而言之:我认为我上面的答案是正确的。

文档可能有点烦人,特别是关于 wtimeout 的部分,因此我查看了源代码。虽然我不是 mongo 源代码的专家,但请谨慎对待以下内容:

write_concern.cpp 中,我们发现(为简洁起见进行了编辑):

if ( cmdObj["j"].trueValue() ) {
    if( !getDur().awaitCommit() ) {
        // --journal is off
        result->append("jnote", "journaling not enabled on this server");
    } // ...
}
else if ( cmdObj["fsync"].trueValue() ) {
    if( !getDur().awaitCommit() ) {
        // if get here, not running with --journal
        log() << "fsync from getlasterror" << endl;
        result->append( "fsyncFiles" , MemoryMappedFile::flushAll( true ) );
    }

请注意,如果设置了fsync,则调用MemoryMappedFile :: flushAll(true)。此调用显然不在第一个分支中。否则,耐久性在单独的线程上处理(相关文件前缀为dur_)。
这解释了wtimeout的作用:它是指等待从节点的时间,并与服务器上的I/O或fsync无关。

1
MongoDB的弱点恰恰也是它的优势,特别是在使用数据库时,它可以在单个服务器之外的预期环境中跨多个分区实现耐久性。此外,很多人认为MongoDB不够耐用,但这些观点都是基于理论而言,在实际应用中这些理论是站不住脚的... - Sammaye
2
这不是一个弱点,事实上它是一个特性:MySQL的InnoDB在5.1版中引入了自适应刷新(http://dev.mysql.com/doc/innodb/1.1/en/glossary.html) 作为一项新功能。我同意,纯CS对耐用性的定义很难应用到现实生活中,因为它没有考虑高层次的问题。 - mnemosyn
1
你确定使用 j:1 选项的 writeconcern 不会等待更新写入日志文件吗? - attish
窗口时间在30-100毫秒之间。我曾经向10gen提出过这个问题,要求他们更具体地解释日志记录的工作原理,但似乎他们还没有解决这个问题...此外,使用j驱动程序时,等待时间永远不会超过journalCommitInterval/3。 - Sammaye
在主分支中,mnemosyn 在 dur.cpp 的第 655 行调用 WRITETOJOURNAL,第 659 行提交 getlasterror 命令。如果我没理解错,在 dur_journal.cpp 中的 WRITETOJOURNAL 实现之前,在第 699 行有一个注释:“将我们构建的缓冲区写入(追加)到日志和 fsync 它。” 对我来说,这意味着从这个角度来看没有丢失数据的窗口,而命令 fsync 也是如此。 - attish
显示剩余3条评论

2

日志记录是为了使特定的mongod上的数据保持一致性状态,即使在出现极端情况下也能够恢复,但是通过客户端设置写关注(writeconcern),可以强制实现耐久性。有关写关注的详细信息请查看此文档

有一个选项j:1,您可以在此处了解更多信息,它可以确保特定的写操作等待确认,直到它被写入磁盘上的日志文件(而不仅仅是内存映射中)。然而,这文档表述相反。:) 我会选择第一种情况,因为它让我感觉更加舒适。

如果你使用这个选项运行很多命令,MongoDB会自动调整日志的提交间隔大小以加速处理速度,您可以在此处了解更多信息,您也提到了这点,正如其他人已经说过的那样,您可以指定2-300ms之间的间隔。

在我看来,与w:2选项相比,耐久性得到了更好的保证,因为如果更新/写操作被复制集中的两个成员确认,则在同一分钟内失去两者的可能性非常小(数据文件刷新间隔),但不是不可能的。

同时使用这两个选项将导致这样一种情况,即当数据库集群确认操作时,该操作将存在于两个不同的框箱中,并且在其中一个框箱中它将位于一致可恢复的磁盘位置。


0

我同意Sammaye的观点,日志记录与持久性关系不大。然而,如果你想得到一个答案,是否可以真正信任mongodb以良好的一致性存储你的数据,那么我建议你阅读这篇博客文章。这篇文章有10gen对该帖子的回复,以及作者对10gen帖子的回复。我建议你仔细阅读,做出明智的决定。我自己花了一些时间才理解所有细节,但这篇文章已经涵盖了基础知识。

这里是mongodb制造公司10gen对博客文章的回应。

在这篇文章上,教授对回应进行了回应。

它详细解释了Mongodb如何分片数据,它的实际功能以及如果添加额外的安全锁会带来的性能影响。我强烈想说,这三篇文章是目前最好的东西,也是关于mongodb利弊的最全面的东西,如果你认为它是片面的,请看看评论,还要看看人们的意见,因为如果某个东西得到了软件制造公司的回复,那么它肯定至少提出了一些好的观点。


那篇博客文章有些片面,并且有时不准确。该作者实际上是 MongoDB 的竞争对手公司的开发人员,他写这篇文章是为了挑起口水战,并将其发布在 MongoDB 的维基百科文章中以引导流量。尽管他确实提到了关于日志仅在主要成员上被确认而不在其他成员上被确认的一个好点,但对于在分区数据库上必须确认日志是否有意义,这是一个观点问题。 - Sammaye
1
@Sammaye:我同意(除了不准确的部分),但问题在于它触及到广泛的问题。我不会说他有时不准确,我只是认为他有一种倾向,将ACID和一致性作为NoSQL数据库的一部分。其次,10gen有一个回应,否定了他所做的一些声明,然后又有一个反驳。我认为这是评判Mongo最好的方法,因为两方都有陈述。 - Games Brainiac
1
不知道在 SO 上的投票有时非常奇怪,可以说单一来源对于 SO 的回答并不够好,有些人讨厌单一来源的答案;当我说有时候时,我的意思是总是这样。 - Sammaye
抱歉,我不小心点击了平板电脑。已经更正 :) - Boris B.
@BorisB。没关系,谢谢你。如果我的回答有什么问题,我通常会立即修正它。 - Games Brainiac

0

一般来说,在每个系统中,只要存在缓存、延迟写入等与系统运行时和永久(非易失性)存储介质之间的处理,即使在操作系统级别(例如写入后缓存),都会出现数据丢失问题。因此,即使您使用的具体提供商(如MongoDB)提供了事务耐久性功能,但最终写入数据的还是底层操作系统,甚至在设备级别也存在缓存...而这只是更低层次的问题,如果将系统高度并发、分布式且高性能,则问题会更加严重。

简而言之,没有绝对的耐久性,只有实际/最终/盼望着有耐久性,特别是对于像Mongo这样的NoSQL存储,它并不是为了一致性和耐久性而设计的。


我会称之为考虑因素而非问题。尽管最常用的写入磁盘的方法经过了通常的缓存优化,但操作系统甚至硬件接口提供了手段,使得当数据安全地存储在底层存储介质上时,写入才会成功。存在 fsync 和 barriers 直接与你第一段的论点相矛盾;即你无法完全控制缓存并且不知道数据是否安全地存储在存储介质上。 - Greg

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接