【问题标题】:Python Running Increasingly Slower, Garbage Collection Issue?Python 运行越来越慢,垃圾回收问题?
【发布时间】:2015-09-19 05:19:56
【问题描述】:

所以我的代码可以从最初包含超过 1400 万个文件的目录中获取文件列表。这是一台运行 Ubuntu 14.04 桌面的具有 20 GB RAM 的六核机器,仅获取文件列表需要数小时 - 我实际上还没有计时。

在过去一周左右的时间里,我运行的代码只不过是收集这个文件列表,打开每个文件以确定它的创建时间,然后根据它的月份和年份将它移动到一个目录被创建。 (这些文件已经过 scp'd 和 rsync'd,因此操作系统提供的时间戳在这一点上毫无意义,因此打开文件。)

当我第一次开始运行这个循环时,它在大约 90 秒内移动了 1000 个文件。然后像这样经过几个小时后,90 秒变成了 2.5 分钟,然后是 4,然后是 5,然后是 9,最后是 15 分钟。所以我把它关了重新开始。

我注意到,今天一旦完成收集超过 900 万个文件的列表,移动 1000 个文件就需要 15 分钟。我只是再次关闭进程并重新启动机器,因为移动 1000 个文件的时间已经攀升到 90 多分钟。

我曾希望找到一些方法来执行while + list.pop() 风格的策略,以在循环进行时释放内存。然后发现几个 SO 帖子说可以使用 for i in list: ... list.remove(...) 完成,但这是一个糟糕的主意。

代码如下:

from basicconfig.startup_config import *

arc_dir = '/var/www/data/visits/'

def step1_move_files_to_archive_dirs(files):
  """

  :return:
  """

  cntr = 0
  for f in files:
      cntr += 1

      if php_basic_files.file_exists(f) is False:
          continue

      try:
          visit = json.loads(php_basic_files.file_get_contents(f))
      except:
          continue

      fname = php_basic_files.basename(f)

      try:
          dt = datetime.fromtimestamp(visit['Entrance Time'])
      except KeyError:
          continue

      mYr = dt.strftime("%B_%Y")

      # Move the lead to Monthly archive
      arc_path = arc_dir + mYr + '//'
      if not os.path.exists(arc_path):
          os.makedirs(arc_path, 0777)

      if not os.path.exists(arc_path):
          print "Directory: {} was not created".format(arc_path)
      else:
          # Move the file to the archive
          newFile = arc_path + fname
          #print "File moved to {}".format(newFile)
          os.rename(f, newFile)

      if cntr % 1000 is 0:
          print "{} files moved ({})".format(cntr, datetime.fromtimestamp(time.time()).isoformat())

def step2_combine_visits_into_1_file():
  """

  :return:
  """

  file_dirs = php_basic_files.glob(arc_dir + '*')

  for fd in file_dirs:
    arc_files = php_basic_files.glob(fd + '*.raw')
    arc_fname = arc_dir + php_basic_str.str_replace('/', '', php_basic_str.str_replace(arc_dir, '', fd)) + '.arc'

    try:
      arc_file_data = php_basic_files.file_get_contents(arc_fname)
    except:
      arc_file_data = {}

    for f in arc_files:
      uniqID = moduleName = php_adv_str.fetchBefore('.', php_basic_files.basename(f))

      if uniqID not in arc_file_data:
        visit = json.loads(php_basic_files.file_get_contents(f))
        arc_file_data[uniqID] = visit

    php_basic_files.file_put_contents(arc_fname, json.dumps(arc_file_data))


def main():
  """

  :return:
  """

  files = php_basic_files.glob('/var/www/html/ver1/php/VisitorTracking/data/raw/*')
  print "Num of Files: {}".format(len(files))

  step1_move_files_to_archive_dirs(files)
  step2_combine_visits_into_1_file()

注意事项:

basicconfig 本质上是我为环境和一些常用库(如所有 php_basic_* 库)所拥有的一堆常量。 (在接触 Python 之前,我使用 PHP 多年,所以我构建了一个库来模仿我使用的更常见的函数,以便更快地启动和运行 Python。)

step1 def 是到目前为止程序所能达到的。 step2 def 可以并且很可能应该并行运行。但是,我认为 I/O 是瓶颈,并行执行更多操作可能会大大降低所有功能的速度。 (我很想将存档目录 rsync 到另一台机器进行聚合,从而获得没有 I/O 瓶颈的并行速度,但我认为 rsync 也会很慢。)

文件本身都是 3 Kb,所以不是很大。

----- 最后的想法-------

就像我说的,至少在我看来,每次打开文件时都不会存储任何数据。因此内存应该不是问题。但是,我确实注意到现在只使用了 1.2 GB 的 RAM,而之前使用了超过 12 GB 的 RAM。这 12 个文件中的很大一部分可以存储 1400 万个文件名和路径。我刚刚再次开始处理,所以在接下来的几个小时里,python 将收集一个文件列表,而该列表还没有在内存中。

所以我想知道是否存在垃圾收集问题或我遗漏的其他问题。为什么它在循环中进行时会变慢?

【问题讨论】:

  • 您是否尝试过不一次将 1400 万个对象全部粘贴到内存中?也许发电机更适合这里?
  • 像这样的程序不太可能出现垃圾收集问题。不过,您可以使用import gc; gc.disable() 禁用自动收集。 (仍然会使用引用计数,但它不会引入扫描收集器的增量减速特性。)另外,我建议去掉 PHP 仿真层,它会减慢执行速度,并使 Python 专家更难审查程序。
  • glob 确实抓取了所有 1400 万(现在是 900 万)并将它们作为一个巨大的列表返回。重新启动机器已恢复到快速状态。在半夜,脚本在不到一分钟的时间内移动了 1000 个文件。此后,移动 1000 个文件的时间已攀升至 90 多秒。在重新启动之前,移动 1000 个文件需要 2 个多小时。 GC 只是一个猜测,我也想知道操作系统和 I/O 问题是否在起作用。对于关于 14 M 列表的所有 cmets,它在步骤 1 的执行期间不会增长,因此该列表不应减慢 step1 的执行速度,因为它会在列表中进行。
  • 对不起,缺少 cmets 的代码。通常我会写很多 cmets,但这段代码实际上只是为了重新组织文件和目录结构一次。然后较新的文件将获得一些类似的代码来重新组织它们,或者我首先将它们保存到正确的存档文件夹中。
  • 我还没有使用过生成器,所以我很难看到在这种情况下如何使用生成器。谷歌搜索没有帮助,但也许我问错了问题。当我要求从此目录中的文件列表但由于某种原因 Python 没有时,操作系统会爆炸。

标签: python performance garbage-collection


【解决方案1】:

step1_move_files_to_archive_dirs

以下是第 1 步可能比您预期花费的时间更长的一些原因...

对步骤 1 中的任何异常的响应是 continue 到下一个文件。如果您有任何损坏的数据文件,它们将永远保留在文件系统中,从而增加此函数下次必须执行的工作量(以及下一次,以及下一次......)。

您正在读取每个文件并将其从 JSON 转换为 dict,只是为了提取一个日期。所以 everything 至少被读取和转换一次。如果您控制这些文件的创建,则可能值得将此值存储在文件名或单独的索引/日志中,这样您以后不必再次搜索该值。

如果输入目录和输出/归档目录位于不同的文件系统上,os.rename(f, newFile) 不能只是重命名文件,而是必须复制文件中的每个字节源文件系统到目标文件系统。因此,要么每个文件都近乎瞬间重命名,要么每个输入文件都被慢慢复制。

PS:奇怪的是,这个函数会仔细检查输入文件是否仍然存在,或者os.makedirs 是否有效,但随后允许来自os.rename 的任何异常导致您在循环中崩溃。

step2_combine_visits_into_1_file

您的所有文件 I/O 都隐藏在 PHP 库中,但在这个 PHP 局外人看来,就像您试图将每个子目录中所有文件的 内容 存储在 RAM 中一样。然后,您将所有这些内容累积在一些较少数量的存档文件中,同时保留(大部分?)已经存在的数据。这不仅一开始可能很慢,而且随着时间的推移会越来越慢。

功能代码大多被cmets替代:

file_dirs =  # arch_dir/* --- Maybe lots, maybe only a few.
for fd in file_dirs:
    arc_files =  # arch_dir/subdir*.raw or maybe arch_dir/subdir/*.raw.
    arc_fname =  # subdir.arc
    arc_file_data =  # Contents of JSON file subdir.arc, as a dict.
    for f in arc_files:  # The *.raw files.
        uniqID =  # String based on f's filename.
        if uniqID not in arc_file_data:
            # Add to arc_file_data the uniqID key, and the
            # _ entire contents_ of the .raw file as its value.
    php_basic_files.file_put_contents  # (...)
    # Convert the arc_file_data dict into one _massive_ string,
    # and replace the contents of the subdir.arc file.

除非您有一些定期修剪 *.arc 文件的维护工作,否则您最终将在 *.arc 文件中拥有所有 1400 万个文件(以及任何旧文件)的全部内容。这些.arc 文件中的每一个都被读入dict,转换为巨型字符串,增长(可能),然后写回文件系统。即使.arc 的平均文件不是很大(只有 很多 个文件才会发生这种情况),这也是大量的 I/O。

为什么要这么做?在第 2 步开始时,您已经为每个 .raw 输入文件获得了一个唯一 ID,并且它已经在 文件名中 --- 那么为什么不使用文件系统本身来存储 @987654335 @?

如果您确实需要在几个庞大的档案中保存所有这些数据,那应该不需要太多的工作。 .arc 文件只不过是 .raw 文件的未更改内容,它们之间有一些 JSON 字典。一个简单的 shell 脚本可以将它们组合在一起,而无需解释 JSON 本身。

(如果值不只是 JSON,而是 quoted JSON,则您必须将读取 .arc 文件的任何内容更改为 not 取消引用这些值。但是现在我只是在猜测,因为我只能看到正在发生的一些事情。)

PS:我是否遗漏了什么,或者arc_files*.raw 文件名的列表。不应该是raw_files吗?

其他评论

正如其他人所指出的,如果您的文件通配函数返回一个包含 1400 万个文件名的巨型列表,那么作为一次可以yield 一个文件名的生成器,它的内存效率会大大提高。

最后,您提到了从 list 中弹出文件名(尽管我在您的代码中没有看到)...插入或删除一个大列表 --- del my_list[0]my_list.pop(0)my_list.insert(0, something) --- 因为项目 1 到 n-1 都必须复制一个索引到 0。这变成了 O(n ) 操作到 O(n**2)... 再次,如果它在你的代码中的任何地方。

【讨论】:

  • step2 尚未运行。它仅在 step1 完成且 step 从未运行完成后运行。但是,step2 的 arc_file_data 旨在聚合给定 month_year 存档目录中的所有文件。所以它不会只包含来自给定月份_年组合的文件的全部 14 M。看起来它仍然可以像 1 M 一样大,所以我正在寻找减小尺寸的方法。特别是通过创建冗余数据列表 - 用户代理字符串 - 并在每次访问时用整数替换该数据。
  • 就第 1 步而言,我让代码在 os.rename 上失败,因为我遇到了 os.rename 在 Ubuntu 14.04 中删除文件的问题。它通常会默默地执行此操作,因此让它崩溃该功能并不是什么大问题。 step2 背后的主要目的是允许我压缩存档目录,同时仍然可以访问这些文件中的数据。就未来而言,这 14 M 文件已经积累了几年。如果我只是每小时运行一次这个归档脚本,它将在几秒钟内完成,所以我不关心重命名文件。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-03-05
  • 2021-12-11
  • 2020-01-23
  • 2023-03-12
  • 1970-01-01
相关资源
最近更新 更多