【问题标题】:Python: slow read & write for millions of small filesPython:数百万个小文件的缓慢读写
【发布时间】:2010-06-13 08:31:16
【问题描述】:

结论: 看来 HDF5 是我的目的。基本上,“HDF5 是一种用于存储和管理数据的数据模型、库和文件格式。”旨在处理大量数据。它有一个名为 python-tables 的 Python 模块。 (链接在下面的答案中)

HDF5 在节省大量数据方面做得更好 1000%。不过,从 2 亿行中读取/修改数据很痛苦,因此这是下一个要解决的问题。


我正在构建包含大量子目录和文件的目录树。大约有 1000 万个文件分布在十万个目录中。每个文件下有 32 个子目录。

我有一个 python 脚本来构建这个文件系统并读写这些文件。问题是当我达到超过一百万个文件时,读写方法变得非常慢。

这是我拥有的函数,它读取文件的内容(文件包含一个整数字符串),向其中添加一定的数字,然后将其写回原始文件。

def addInFile(path, scoreToAdd):
    num = scoreToAdd
    try:
        shutil.copyfile(path, '/tmp/tmp.txt')
        fp = open('/tmp/tmp.txt', 'r')
        num += int(fp.readlines()[0])
        fp.close()
    except:
        pass
    fp = open('/tmp/tmp.txt', 'w')
    fp.write(str(num))
    fp.close()
    shutil.copyfile('/tmp/tmp.txt', path)
  • 关系数据库访问这些数据似乎太慢了,所以我选择了文件系统方法。
  • 我之前尝试过为这些命令执行 linux 控制台命令,但速度较慢。
  • 我先将文件复制到临时文件,然后访问/修改它,然后再复制回来,因为我发现这比直接访问文件要快。
  • 将所有文件放入 1 个目录(以 reiserfs 格式)导致访问文件时速度过慢。

我认为速度变慢的原因是因为有大量文件。执行此功能 1000 次不到一秒.. 但现在已达到 1 分钟。

你建议我如何解决这个问题?我要更改我的目录树结构吗?

我只需要快速访问这个庞大的文件池中的每个文件*

【问题讨论】:

  • 256^32!!!那是1E77!!!仅用于目录就需要 4E68 1TB 磁盘驱动器!!!如果您的意思是“2^32”,那么您很幸运,您只需要大约 4 PB 的存储空间。
  • 请检查您的数字,您的磁盘上不可能有 256^32 个目录,甚至是当前磁盘的 2^32 个目录,并且仍然在寻求帮助。换句话说,你不会在不知道如何正确处理它们的情况下购买这么多或这么大的磁盘,所以数字显然是错误的。
  • 另外,您是说每个文件只有几个字节长,但抱怨当文件为 1GB 时处理需要时间?
  • 从描述中,我了解到 OP 正在制作 256 个目录,每个目录包含 256 个目录,...,32 层深度。最后一级包含 256 个小文件。
  • 我猜还是有些混乱。假设 Adrien 对拥有 32 深 256 树的解释,您将“略”超过 10 ^ 77 个目录,或者只是“略”高于 2 ^ 256。不幸的是,没有 256 位文件系统,所以你必须等到那些成为时尚(提示:他们不会)。一旦你有了这样一个 256 位的文件系统,你只需要克服boil the oceans 的小障碍,两次,然后你就设置好了你的目录结构。

标签: python file io


【解决方案1】:

我知道这不是您问题的直接答案,但它是您问题的直接解决方案。

您需要使用HDF5 之类的东西进行研究。它专为具有数百万个单独数据点的分层数据类型而设计。

你真的很幸运,因为有很棒的用于 HDF5 的 Python 绑定,称为pytables。 我以非常相似的方式使用它并取得了巨大的成功。

【讨论】:

  • 这看起来很有希望!我也会对此进行测试。最好的一点是它用于科学数据,并且使用 POSIX 格式。
  • 如果您知道“表”中的偏移量或使用 pytables 索引,访问将非常快。更新只是你有多少内存和你的磁盘有多快,就像其他任何事情一样。如果您有大量存储空间,而不是进行就地更新,您可能会考虑复制到一个新的“表”中,并确保同时打开压缩。
  • 我已经阅读了有关 HDF5 的手册。事实上,如果您知道行的偏移量,或者如果您只是对其中的一部分进行采样,它会很快,但我想搜索所有数据。我将不得不重新考虑数据的输入,这样我就不必再进行行更新了。但毫无疑问,HDF5 会处理我的数百万个小文件
【解决方案2】:

两个建议:

首先,涉及 32 深嵌套子目录的结构本身就有缺陷。假设您确实有“大约 1000 万个文件”,那么一级子目录绝对足够(假设您使用现代文件系统)。

第二:您说您有“大约 1000 万个文件”并且每个文件“包含一个整数字符串”。假设这些是 32 位整数并且您将它们直接存储而不是作为字符串存储,那么数据集的总大小为 40MiB(10M 个文件 * 每个文件 4 个字节)。假设每个文件名的长度为 32 个字节,则为该数据添加另外 320MiB 的“密钥”。

因此,您将能够轻松地将整个数据集放入内存中。我建议这样做,并对主存储器中保存的数据进行操作。除非有任何理由需要详细的目录结构,否则我进一步建议将数据存储在单个文件中。

【讨论】:

  • 根据使用的文件系统,一级子目录可能会自找麻烦。并非所有文件系统都能在目录中包含数千个文件时表现良好。
  • @Mattias:是的,但这就是为什么你会在 serverfault 上询问部署这类东西的最佳 FS。
  • 是的,现在每个人都知道这可能是一个问题,以及在哪里寻求帮助。 :)
  • 马蒂亚斯,谢谢。我的讨论基于这样一个假设,即如果想要基于 FS 存储数据,则使用现代 FS。我为此添加了一条注释。
【解决方案3】:

我建议您重新考虑您的方法,使用大量极小的文件势必会给您带来严重的性能问题。根据程序的目的,某种数据库可能效率更高。

如果您正在执行大量 I/O,您也可以将更多硬件用于解决问题并使用 SSD 或将所有数据保存在 RAM 中(显式地或通过缓存)。在这种情况下,仅使用硬盘驱动器就没有机会获得良好的性能。

我从未使用过它,但例如Redis 是一个持久键值存储,应该非常快。如果您的数据适合这个模型,我肯定会尝试这个或类似的东西。您可以在此 article 中找到一些性能数据,这些数据应该让您了解可以达到的速度。

【讨论】:

  • 所以你的意思是因为我正在读/写数十亿个小文件,硬盘驱动器是一个很大的问题?我无法使用 SSD 或 RAM,因为我现在生成的估计数据量约为 22Gb。
  • 22 GB 可轻松安装在经济实惠的 SSD 上,您只需不到 200 欧元即可获得 64 GB。我仍然建议您研究导致更少 I/O 的其他方法。
  • 对使用传统 HDD 减少 IO 有什么建议吗?
  • 像什么?让其中的 100 个 IO 仍然比 SSD 少?坏消息 - 大型数据仓库就是这样做的。数百个或更多 15k RPM SAS 驱动器,只是为了获得 IO 速度。一张光盘适用于 x 百 IOPS - 300 左右的光盘。您需要更多 IO - 获得更多心轴;)或 SSD(RealSSD = 40.000 IOPS)。
  • 减少 IO 数量的建议:合并文件!
【解决方案4】:
  1. 磁盘受到每秒可以读取/写入的字节数以及每秒可以执行的操作数量的限制。
  2. 当您的小文件被缓存时,操作速度明显快于未缓存文件。

看起来你遇到了这两个问题,

  • 执行过多的 i/o 操作
  • 缓存不足

我建议重新审视您正在使用的结构,并使用较小的文件。保持在 minf 中(根据经验),小于 128K 的 I/O 操作运行时成本或多或少等于 1byte 的 I/O!

【讨论】:

  • 对于您的#1,我只按顺序执行读/写,我不会将其线程化以执行多次。这还会是个问题吗?
  • 这不是问题,但是,如果磁盘被限制为每秒 100 次 IO 操作,您将无法读取/写入超过 70 个文件(因为 1 写入成本是 (经验法则)等于 4 次读取的成本)
【解决方案5】:

解析所有这些子目录需要时间。您对文件系统负担过重。

也许您可以不使用目录树,而是将路径信息编码到文件名中,这样就不用创建具有如下路径的文件:

/parent/00/01/02/03/04/05/06/07
       /08/09/0A/0B/0C/0D/0E/0F
       /10/11/12/13/14/15/16/17
       /18/19/1A/1B/1C/1D/1E/1F.txt

...您可以使用如下路径创建文件:

/parent/00_01_02_03_04_05_06_07_
        08_09_0A_0B_0C_0D_0E_0F_
        10_11_12_13_14_15_16_17_
        18_19_1A_1B_1C_1D_1E_1F.txt

...当然,您仍然会遇到问题,因为现在您所有的一千万个文件都将位于一个目录中,而根据我的经验 (NTFS),一个包含数千个文件的目录它仍然使文件系统负担过重。

您可以提出一种混合方法:

/parent/00_01_02_03/04_05_06_07
       /08_09_0A_0B/0C_0D_0E_0F
       /10_11_12_13/14_15_16_17
       /18_19_1A_1B/1C_1D_1E_1F.txt

但是,如果您详尽地创建所有这些目录,那仍然会给您带来问题。即使这些目录中的大多数是“空的”(因为它们不包含任何文件),操作系统仍然必须为每个目录创建一个 INODE 记录,这会占用磁盘空间。

相反,您应该只在有文件可放入目录时才创建目录。此外,如果您删除任何给定目录中的所有文件,则删除空目录。

您应该创建多少级目录层次结构?在我的小示例中,我将您的 32 级层次结构转换为 8 级层次结构,但是在进行一些测试之后,您可能会决定使用稍微不同的映射。这实际上取决于您的数据,以及这些路径在组合解决方案空间中分布的均匀程度。您需要优化具有两个约束的解决方案:

1) 尽量减少创建的目录数量,知道每个目录都会成为底层文件系统中的一个 INODE,创建太多目录会使文件系统不堪重负。

2) 尽量减少每个目录中的文件数量,因为每个目录有太多文件(根据我的经验,超过 1000 个)会使文件系统不堪重负。

还有一个注意事项需要牢记:磁盘上的存储空间是使用“块”寻址和分配的。如果您创建的文件小于最小块大小,它仍然会占用整个块,从而浪费磁盘空间。在 NTFS 中,这些块由它们的“簇大小”定义(部分由卷的整体大小决定),通常默认为 4kB:

http://support.microsoft.com/kb/140365

所以如果你创建一个只有一个字节数据的文件,它仍然会消耗 4kB 的磁盘空间,浪费 4095 个字节。

在您的示例中,您说您有大约 1000 万个文件,其中包含大约 1gB 的数据。如果这是真的,那么您的每个文件只有大约 100 个字节长。如果集群大小为 4096,则空间浪费率约为 98%。

如果可能,请尝试合并其中的一些文件。我不知道它们包含什么样的数据,但如果是文本格式,您可以尝试这样做:

[id:01_23_45_67_89_AB_CD_EF]
lorem ipsum dolor sit amet consectetur adipiscing elit
[id:fe_dc_ba_98_76_54_32_10]
ut non lorem quis quam malesuada lacinia
[id:02_46_81_35_79_AC_DF_BE]
nulla semper nunc id ligula eleifend pulvinar

...等等等等。看起来你用所有那些冗长的头文件在浪费空间,但就磁盘而言,这是一种比为所有这些小 sn-ps 单独文件更节省空间的策略。这个小例子正好为三个记录使用了 230 个字节(包括换行符),因此您可以尝试在每个文件中放入大约 16 个记录(请记住,每个文件的字节数略少于 4096 字节比略多于4096,浪费了整个额外的磁盘块)。

无论如何,祝你好运!

【讨论】:

  • 好点。事实上,我有很多浪费的空间。如果我在每个文件中有 1 个整数(值 0-255),我可以将 256 个整数放入 1 个文件中,以利用 reiserfs 文件系统的 4096 块大小。我做了一个测试来检查哪个更快:很多小文件与 1 个包含大量内容的文件。对于测试 1,我有 65k 个文件,每个文件有 1 个整数,然后访问和修改它们 50k 次。对于测试 2,我在 1 个文件中有 65k 行,每行有 1 个整数,然后访问和修改它们 50k 次。结果:大量小文件:7.4 秒大量内容:5.6 秒
  • 我检查了我的测试代码,似乎大量小文件测试的 2 秒延迟是由于打开和关闭不同文件造成的。当同一个文件一直被打开/关闭时,会发生某种缓存。
  • 如果您的整数值在 0 到 255 的范围内,则可以将其存储在一个字节中,因此如果您想容纳一个文件,则可以将其中的 4096 个(!) 4KB 块大小。
  • 是的,它们很好地填充了块大小。请阅读我对 Lie Ryan 建议的第二条评论
【解决方案6】:

您正在复制一个文件,打开它以读取,关闭它,然后重新打开它以写入,然后重新复制它。一口气完成会更快。

编辑:以前的版本在位数变得小于当前位数时存在错误(例如,如果您要减去或添加负数);这个版本修复了,计时结果几乎不受影响

def addInFile(path, scoreToAdd):
    try:
        fp = open(path, 'r+')
    except IOError as e:
        print e
    else:
        num = str(scoreToAdd + int(fp.read()))
        fp.seek(0)
        fp.write(num)
        fp.truncate(len(num))
    finally:
        fp.close()

或者,如果你想避免文件丢失和写入缓存,你应该一次性完成复制和求和,然后在另一个步骤中进行覆盖舞蹈:

def addInFile(path, scoreToAdd):
    try:
        orig = open(path, 'r')
        tmp = open('/home/lieryan/junks/tmp.txt', 'w')
    except IOError as e:
        print e
    else:
        num = int(orig.read())
        tmp.write(str(scoreToAdd + num))
    finally:
        orig.close()
        tmp.close()
    try:
        # make sure /tmp/ and path is in the same partition
        # otherwise the fast shutil.move become a slow shutil.copy
        shutil.move(path, '/home/lieryan/junks/backup.txt')
        shutil.move('/home/lieryan/junks/tmp.txt', path)
        os.remove('/home/lieryan/junks/backup.txt')
    except (IOError, shutil.Error) as e:
        print e

另外,不要使用裸异常。

或者,如何将最低叶中的所有 256 个文件分组为一个更大的文件?然后,您可以在一个缓存中一口气读取多个数字。如果你使用了一个固定宽度的文件,那么你可以快速使用 seek() 在 O(1) 中找到文件中的任何条目。

一些计时,在同一个文件上写1000次:

  • 你原来的方法:1.87690401077
  • 我的第一种方法(使用 rw+ 打开):0.0926730632782
  • 我的第二种方法,复制到同一个分区:0.464048147202

(所有函数在其错误处理路径上未经测试)

【讨论】:

  • 让我试试你的第一种方法。我会在几个小时后回来评论结果。当文件太多时,我会尝试解决瓶颈。是的,我不会使用纯例外,但要明白这是一种概念证明
  • 第一种方法确实更快。但是在总文件数超过一百万,总文件大小超过 1Gb 后,即使文件大小适当地调整为 FS 的块大小,性能也会急剧下降
【解决方案7】:

如果你在 linux 下并且有大内存(64GB+),试试tmpfs,它真的像挂载的磁盘一样工作,你不需要更改代码或购买另一个 SSD。

【讨论】:

    猜你喜欢
    • 2016-08-22
    • 1970-01-01
    • 1970-01-01
    • 2013-02-04
    • 1970-01-01
    • 2021-03-16
    • 2011-08-11
    • 1970-01-01
    • 2020-03-16
    相关资源
    最近更新 更多