原文标题:Changing Fundamental Behavior With Two Lines of Code(用两行代码改变基础行为)

作者:Oren Eini, CEO RavenDB

一直以来我们对 RavenDB 的启动时间并没有太多关注 —— 5 秒或者 15 秒都不是什么问题。但是如果超过 15 秒,甚至长达 3 分钟的话,那我们就必须关注了。

我们的一个用户提供了一个有意思的用例。他们的系统运行在 Azure 上,并充分利用了多设备存储,也就是将数据库临时文件(journals)放在高性能存储服务上,而数据本身放在更大(且相对较慢)的普通存储上。这么做是因为他们的数据量很大,比如某条索引的大小就超过了 256GB。

在他们系统当中,RavenDB 的启动慢到无法接受。我们经过排查发现根本原因在于启动过程中的数据恢复阶段,此时数据库会重新执行临时文件中的最近事务,以保证数据完整性。通常来讲这步花不了多少时间,因为默认情况下,即使数据库负载较大时,临时文件大小也只有 256MB 左右。但我们这位客户的使用场景比较特殊,我们观察到临时文件中一个事务的数据量能达到几个 GB 的大小,再加上临时文件的内容是经过压缩的,所以当数据恢复到主存储当中时,实际的数据量会达到 10GB 以上。虽然那些已经执行过的事务不需要重复执行,但我们必须要先扫描临时文件,来找到哪部分是已经执行过的。

在这个基础上,考虑到数据库的个数,以及每个数据库的索引个数,RavenDB 启动时的整体消耗就变得十分可观了。显然这不是我们所要的。如果我们遇到系统崩溃,那么目前还没有什么好的办法来避免重新执行这些事务;但问题是就算没有遇到崩溃,就算是正常关闭,重新启动的过程还是一样的缓慢。

这个问题本质上是因为 RavenDB 没有在临时文件中标记哪个位置是已经同步过的,所以在启动过程中我们必须扫描整个临时文件,来找到需要重新执行的部分。当然我们可以在临时文件中补上这个标记,但是我们不能直接去更改客户现有系统的数据文件格式,这种解决方案不但感觉别扭,而且可能带来兼容性问题。

我们也考虑修改数据库的行为,使得在对某个大数据量的事务同步成功后,更加主动的切换到另一个临时文件。今天我在查看相关代码时,发现了一个有问题的地方。每当我们处理一个大事务(事务大小超过临时文件最大大小)时,为了有足够的空间,我们会在磁盘上扩充临时文件的大小,而我们分配空间的计算方式很有意思:

图片描述

如图所示,如果当前的临时文件大小小于需要的最小空间,我们会要增加其空间;同时为了避免过于频繁的扩充文件操作,我们还会考虑给下一个事务预留足够的空间。现在我们假设当前临时文件大小为 256MB(也是系统预设的最大值),而事务大小为 1.56GB。

这种情况下,临时文件将会扩充到 2GB 大小,而其中只有 1.56GB 用到了。本来剩余的空间我们是可以继续利用的,除非下一个事务很大,比如有 800MB,我们就会要重建一个新的 1GB 大小的文件。

这个时候问题来了。对于这个 2GB 大小的临时文件,假设我们已经成功将其内容同步到了主存储,那么剩下还有 440MB 的空间可用,我们就会保留这个临时文件用它来存储下一个事务。如果在这个点上数据库进行了重启,那么在启动阶段就必须扫描整个 2GB 的临时文件来确保没有丢失数据。要修复这个问题也超简单:

图片描述

我们要做的就是当扩充后的大小大于临时文件最大大小时,取临时文件最大大小和实际需要的大小之间的最大值作为最终的实际大小。这样扩充后的临时文件就刚好只能容纳一个大事务。当这个事务成功同步后,因为没有额外的空间再容纳另一个事务,于是 Voron 便会马上清理这个文件。这样临时文件中就不存在残留的大事务数据。这个改动既简洁又有效,非常棒,我非常喜欢。


捏造的信仰
2.8k 声望272 粉丝

Java 开发人员


引用和评论

0 条评论