Python:慢读&为数百万个小文件写

时间:2010-06-13 08:31:16

标签: python file io

  

结论:   似乎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分钟。

你怎么建议我解决这个问题?我是否更改了目录树结构?

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

7 个答案:

答案 0 :(得分:6)

两个建议:

首先,涉及子目录32深度嵌套的结构本质上存在缺陷。假设你真的有“大约1000万个文件”,那么一个级别的子目录应该是足够的(假设你使用的是现代文件系统)。

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

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

答案 1 :(得分:6)

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

您需要使用HDF5等内容进行研究。它仅适用于具有数百万个单独数据点的分层数据类型。

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

答案 2 :(得分:5)

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

如果您正在进行大量I / O操作,您还可以在问题上投入更多硬件并使用SSD或将所有数据保存在RAM中(显式或通过缓存)。仅使用硬盘驱动器就无法在这种情况下获得良好的性能。

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

答案 3 :(得分:2)

  1. 磁盘受每秒可读/写的字节数限制,也受到第二次执行的操作的限制。
  2. 在缓存小文件时,操作速度明显快于未缓存文件。
  3. 看起来你正在解决这两个问题,

    • 进行太多的i / o操作
    • 用完了缓存

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

答案 4 :(得分:2)

解决所有这些子目录需要时间。你对文件系统过度征税。

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

/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

...当然,你仍然会有问题,因为现在你的所有1000万个文件都在一个目录中,而根据我的经验(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

...依此类推。看起来你似乎浪费了所有那些冗长标题的空间,但就磁盘而言,这是一个更节省空间的策略,而不是为所有这些小片段提供单独的文件。这个小例子对三个记录使用了230个字节(包括换行符),所以你可能会尝试在每个文件中放入大约16个记录(记住每个文件略少于4096个字节要好于略多于4096,浪费了一整个额外的磁盘块。)

无论如何,祝你好运!

答案 5 :(得分:1)

你正在复制一个文件,打开它来阅读,关闭它,然后重新打开它进行写入,然后重新复制它。一次性完成它会更快。

编辑:当数字位数小于当前位数时(例如,如果您减去或添加负数),以前的版本有一个错误;此版本修复了它,时间结果几乎不受影响

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

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

答案 6 :(得分:0)

如果您使用Linux并获得大容量内存(64GB +),请尝试tmpfs,它的确能像挂载磁盘一样工作,您无需更改代码或购买其他SSD。