在Mac OS X Snow Leopard上进行磁盘I/O时,C程序陷入了不可中断的等待状态。

16
背景:我是Redis,一种NoSQL数据库的开发人员。我正在实现的一个新功能是虚拟内存,因为Redis将所有数据存储在内存中。由于有了VM,Redis能够将很少使用的对象从内存转移到磁盘上,这样做比让操作系统为我们进行交换要好得多(redis对象由许多分配在非连续位置的小对象构建,在由Redis序列化到磁盘时,它们占用的空间比它们所在的内存页面少10倍等等)。
现在我有一个alpha实现,它在Linux上完美运行,但在Mac OS X Snow Leopard上运行得不太好。有时,当Redis尝试将页面从内存移动到磁盘时,redis进程会进入不可中断的等待状态数分钟。我无法调试此问题,但这可能发生在对fseeko()fwrite()的调用中。数分钟后,调用最终返回,Redis继续正常工作:没有崩溃。
传输的数据量非常小,大约256字节左右。因此,这不应该是执行了非常多的I/O的问题。
但是有关写操作的交换文件有一个有趣的细节。它是一个大文件(26 GB),通过使用fopen()打开文件然后使用ftruncate()扩大文件。最后,文件被unlink(),以便Redis继续引用它,但我们确信当Redis进程退出时,操作系统将真正释放交换文件。

好的,这就是全部内容,但如果需要进一步了解,请告诉我。顺便说一句,您甚至可以在Redis git中找到实际代码,但是考虑到它是一个相当复杂的系统,在五分钟内理解并不容易。

非常感谢任何帮助。


更多信息:现在使用较小的交换文件(256 MB)尝试,错误消失了,即使数据恰好写入相同的位置和相同数量的页面。考虑到答案中的其他猜测,似乎发生的情况很像操作系统在几次写入后似乎尝试在文件系统中物理分配巨大的文件,这需要几分钟的时间。我可以通过在启动时写入一些随机字节来“修复”此问题,以便尽快强制进行物理分配,至少作为一个选项。非常感谢。我会在这里发布更新。 - antirez
5个回答

11
据我所知,HFS+对于稀疏文件的支持非常差。因此,您的写入可能会触发文件扩展,导致大部分文件被初始化/实例化。例如,我知道在HFS+下,将一个新的大空文件映射到内存并在几个随机位置写入会产生一个非常大的磁盘文件。这相当令人恼火,因为mmap和稀疏文件是一种极其方便的处理数据的方式,而且几乎所有其他平台/文件系统都可以优雅地处理它们。
交换文件是否按线性方式写入?也就是说,我们要么替换现有块,要么在末尾写入新块并增加自由空间指针?如果是这样,那么做更频繁的小型ftruncate调用以扩展文件可能会导致更短的暂停。
另外,我很好奇为什么Redis VM不使用mmap,然后只是移动块,试图将热块集中到热页中。

你好,Jason。是的,这也是我的想法:由于某种原因,在ftruncate()和几次写操作之后,HFS+实现会认为是时候将文件的大部分内容实体化了。页面是逐步分配的。我使用类似于Linux内核的算法。我尝试逐步分配一定数量的页面,然后不时返回文件开头搜索空闲的连续块。因此,增量ftruncate()是一个好主意。我考虑过它,但避免在需要完整磁盘的启动时告诉“空间已满”。 - antirez
我想知道,即使在支持稀疏文件的系统上,ftruncate()是否实际上会保留文件空间?另外:我听说苹果已经开始开发一个新的文件系统,不是从HFS派生的。在他们这样做之前,OSX将永远无法用于服务器,并且对于部署到linux/solaris等系统的开发人员来说也很麻烦。 - Jason Watkins
尝试使用较小的文件后,错误消失了。因此我认为你的答案是正确的,在ftruncate之后,第一次写入可能会实现文件。鉴于每个人都在Linux上运行Redis进行生产,这不是一个大问题,但最好知道 :) 谢谢 - antirez

1

antirez,我不确定我能提供多少帮助,因为我的苹果经验仅限于 Apple ][,但我会尽力而为。

首先是一个问题。我认为,对于虚拟内存来说,操作速度应该比磁盘空间更重要(特别是对于 NoSQL 数据库来说,速度是整个重点,否则你会使用 SQL,不是吗?)。但是,如果您的交换文件大小为 26G,也许不是这样的 :-)

如果可能,可以尝试一些事情。

1. 尝试将问题实际隔离到seek或write上。我很难相信seek可能需要那么长时间,因为最坏的情况下,它只是一个缓冲指针的变化。不过,我没有写过OSX,所以不能确定。
2. 尝试调整交换文件的大小,看看是否会导致问题。
3. 您是否会动态扩展交换文件(而不是预分配)?如果是这样,可能就是这个问题的原因。
4. 您总是尽量写入文件的最低处吗?也许创建一个26G的文件实际上并不会填满它,但如果您创建了它然后写入最后一个字节,操作系统可能需要在此之前将字节清零(延迟初始化,如果有的话)。
5. 如果只是预分配整个文件(写入每个字节),而不删除它,会发生什么情况?换句话说,在您的程序运行期间保留文件(当然,如果它不存在,则创建它)。然后在Redis的启动代码中,只需初始化文件(指针等)。这可能会消除上述第4点类似的任何问题。
6. 在各种BSD网站上提问。我不确定苹果在底层做了多少更改,但OSX实际上只是BSD的最底层(Pax躲避掩护)。
7. 还考虑在苹果网站上提问(如果您还没有这样做的话)。

这就是我的小贡献,希望能有所帮助。祝你的项目好运。


你好,你的评论很棒!非常感谢。关于大小,确实整个重点在于速度,但有许多数据集通常只使用整个数据集的5%,因此大型交换文件有时会很方便。在Redis中,用户可以配置交换文件大小(页面大小和实际页面数)以及Redis可以使用的RAM量,因此这是为您的数据集非常好地调整系统的问题。顺便说一句:1)好主意。 2)确实,这可能会确认是否为实际文件分配时间。 3)空间不足很难恢复,但是... - antirez
我也会尝试尝试使用4。5)启动时间可能太长了,我可以逐步完成它,并且考虑到Linux的工作方式,它是第一个部署平台... 6和7)也是好主意。非常棒的评论和帮助。谢谢!目前我的最佳猜测就是mac os x尝试在少数写入后将文件分配到磁盘上,而既然这是一个26 GB的文件,那就需要很长时间。 - antirez
antirez,关于“5)启动时间可能太长”的问题:我建议您在第一次运行程序时执行此操作,并在运行之间保留交换文件。这样,后续的运行就不必创建文件了。它们仍然需要初始化,但希望这只是将几个指针类型值或零计数写入其开头的情况。 - paxdiablo
这样,当程序启动时,您总是会有一个交换文件 - 操作系统不会在您使用它时惰性创建文件的某些部分。如果空间不足,仍然需要代码来扩展交换,但无论如何都是这种情况。 交换保持了其曾经达到的最大大小(如果要减小它,请在 Redis 之外删除文件,以便在运行时重新创建它,或者具有让 Redis 在启动时重新创建交换文件的选项,无论该文件是否存在)。 - paxdiablo
哦,明白了,这也是可行的。交换文件根本不需要任何初始化,因为页面表已经在内存中以提高性能。所以实际上Redis只需要检查文件是否存在即可。而且当前的方法很糟糕,因为它会在/tmp中创建交换文件。用户希望它在快速驱动器(如SSD或类似物品)所在的位置创建。因此...实际上,“命名”交换文件更好。vm-swap-file <...>。感谢您提供的好主意! - antirez

0

你是否已经关闭了文件缓存?例如 fcntl(fd, F_GLOBAL_NOCACHE, 1)


不,如果系统中有空闲内存,操作系统缓存文件是个好主意。实际上,其中一个合法的用途是通过交换CPU周期来获取内存,因为存储在虚拟内存中的数据更小但访问速度较慢。所以理论上它应该是一个普通的文件,但如果你认为这可能是问题所在,我可以尝试一下。我会报告我的发现。感谢您的回答。 - antirez

0

我尝试使用dtruss来查看调用,但并不成功,没有任何提示为什么会这么慢。可能是操作系统正在执行某些阻塞操作,例如在ftruncate之后将文件的一部分实现到磁盘上?我会继续尝试,并感谢提供链接和答案。 - antirez

-1
正如Linus曾经在Git邮件列表上说过的那样:
“我意识到OS X的人们很难接受,但是OS X文件系统通常是彻头彻尾的垃圾 - 比Windows还要糟糕。”

2
有趣,但没有帮助的答案。 - sbooth

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