Python运行越来越慢,垃圾收集问题?

时间:2015-09-19 05:19:57

标签: python performance garbage-collection

所以我有代码从最初拥有超过1400万个文件的目录中获取文件列表。这是一个六核机器,20 GB内存运行Ubuntu 14.04桌面,只需抓取一个文件列表需要几个小时 - 我还没有实际计时。

在过去一周左右的时间里,我运行的代码只是收集这个文件列表,打开每个文件以确定它何时被创建,并根据月份和年份将其移动到目录中创建了。 (这些文件都是scp'd和rsync'd所以操作系统提供的时间戳此时没有意义,因此打开文件。)

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

我注意到今天一旦完成了收集超过9百万个文件的列表,那么移动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的更常用函​​数。)

步骤1 def是程序到目前为止。 step2 def可以并且可能应该并行运行。但是,我认为I / O是瓶颈,并行执行更多功能可能会使所有功能降低更多。 (我一直试图将存档目录rsync到另一台机器进行聚合,从而在没有I / O瓶颈的情况下获得并行速度,但认为rsync也会非常慢。)

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

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

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

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

1 个答案:

答案 0 :(得分:0)

<强> step1_move_files_to_archive_dirs

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

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

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

如果输入目录和输出/存档目录位于不同的文件系统上,os.rename(f, newFile)不能重命名该文件,但必须复制每个字节从源文件系统到目标文件系统。因此,要么几乎每个文件都被即时重命名,要么每个输入文件被慢慢复制。

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

<强> step2_combine_visits_into_1_file

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

功能代码主要由注释替换:

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,转换为巨型字符串,成长(可能),然后写回文件系统。这是一个很大的I / O,即使平均.arc文件不是很大(只有在批次的情况下才会发生)。

为什么要这么做呢?在步骤2开始时,您已经为每个.raw输入文件获得了一个唯一的ID,并且它已经是文件名中的 ---所以为什么不使用文件系统本身来存储/arch_dir/subdir/unique_id.json

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

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

PS:我错过了什么,或者是arc_files *.raw个文件名列表。不应该是raw_files

其他评论

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

最后,你提到从del my_list[0]弹出文件名(虽然我在你的代码中没有看到)...插入或删除第一个巨大的时间代价大型列表的元素--- my_list.pop(0)my_list.insert(0, something)"SELECT * FROM [music_data] INNER JOIN [music_junc] ON [music_data].[music_d_id]=[music_junc].[music_d_id]) WHERE ([profile_id] = 2)" ---因为项目1到n-1都必须将一个索引复制到0.这就变成了O( n )操作到O( n ** 2)...再次,如果你的代码在任何地方。