【问题标题】:subprocess.Popen() performance degrades as process count risessubprocess.Popen() 性能随着进程数的增加而降低
【发布时间】:2018-09-22 23:13:04
【问题描述】:

我有一个使用subprocess.Popen() 运行和管理许多服务的应用程序。这些服务中的每一个都会运行,直到被明确告知要关闭。我注意到从subprocess.Popen() 调用返回的时间以相当线性的速度增加,因为仲裁器产生了更多的进程。

我的基本代码如下所示:

process_list = []
for command in command_list:
  start_tm = time.time()
  process = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
  end_tm = time.time()
  print end_tm-start_tm
  process_list.append(process)

我看到end_tm-start_tm 的打印量随着我产生越来越多的进程而增加。每个command 运行的服务可以是任何顺序,我看到相同的行为。时间增加不是完全线性的,但我一直看到一个模式:第一个进程需要约 0.005 秒才能产生,第 10 个需要约 0.125 秒,第 20 个需要约 0.35 秒,依此类推。

我的仲裁进程运行超过 100 个子进程。我可以将其拆分,以便多个仲裁器分别运行较少数量的子进程,但我想首先了解问题所在。一个进程拥有许多子进程的开销是否如此之大,以至于每个额外的子进程都会增加subprocess.Popen() 的返回时间?有什么办法可以缓解这种情况吗?

编辑:我将我的单个仲裁进程一分为二。在我之前的测试中,我的仲裁器运行了 64 个进程。我为我的仲裁器创建了两个独立的配置,每个配置运行 32 个进程。我运行第一个仲裁器,让它完全启动所有 32 个进程,然后启动第二个仲裁器。

在这两种情况下,第一个进程再次启动大约需要 0.005 秒,而第 32 个也是最后一个进程大约需要大约 0.45 秒才能启动。在我之前对具有 64 个进程的单个仲裁器的测试中,第一个进程大约需要 0.005 秒才能启动,而第 64 个进程大约需要 0.85 秒。

【问题讨论】:

  • 您正在并行运行 100 个进程。因此,每个进程都必须与所有其他进程共享您的 CPU。
  • 流程本身的性能不是问题。每个过程都表现良好。这些进程的产生似乎需要越来越多的时间。
  • 我不相信这不是操作系统级别的开销,与 Python 正在做的任何事情无关。使用诸如Sysdig 之类的工具来测量完成fork()execve() 操作的实际时间可以大大消除这种怀疑。
  • (实际上尝试您提出的解决方案——拥有多个主管并查看是否发生相同的效果——同样会提供有用的数据点)。
  • ...确定这是 fork 还是 execv 开销也很有趣——如果是前者,也许您的用例需要 prefork 池或类似的,在这种情况下您可能会考虑使用 Celery 进行第 3 方维护的预优化作业池实施。

标签: python python-2.7 subprocess


【解决方案1】:

不是直接回答您所注意到的“为什么”发生的问题,但我强烈建议您通过使用 ThreadPoolExecutor 管理系统资源来更改处理多处理的策略。

由于您的系统无法有效管理多于系统线程的进程,我会尝试:

>>> from concurrent.futures import ThreadPoolExecutor
>>> from multiprocessing import cpu_count
>>> with ThreadPoolExecutor(workers=cpu_count()) as pool:
    results = pool.map(lambda cmd:  subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE), command_list)

我发现 API 很简单,资源管理也很有效,而且“陷阱”较少。

https://docs.python.org/3.6/library/concurrent.futures.html#concurrent.futures.ThreadPoolExecutor.shutdown

虽然这是针对 3.6 的,但 API 在您 pip install 2.7 的 concurrent.futures 模块后基本相同。

【讨论】:

  • 可能需要稍微修改代码以适应您的观察/管理目的
  • 虽然按照您的建议,这样做可能会更快一些,但由于每个线程都没有启动尽可能多的子进程,根据 OP 的观察,您的代码没有做你认为它是什么。对subprocess.Popen() 的每次调用都在开始一个新进程,无论是否在线程中完成。启动时,调用返回,允许线程继续启动另一个子进程。因此,最终所有子流程都在并行运行。
  • 此外,没有什么说子进程受 CPU 限制,如果不是,那么将进程数量限制为 CPU 数量的整个想法都是徒劳的。也许他们正在等待外部事件完成(例如加载网页)。然后,您可以从并行等待它们中获得显着的加速。
  • 啊,但这正是重点 :) 使用 ThreadPool 来启动使用 Popen 的进程是一种很棒的技术,它可以让您充分利用 Python 线程和操作系统进程。它正在做我认为它做的事情。开发人员必须有足够的纪律来管理 Popen 启动的独立子流程
  • 你为什么要在线程中运行Popen?无论如何,这些进程都会被分叉,您的主程序将继续运行。你不需要线程。
猜你喜欢
  • 2018-06-08
  • 2020-02-19
  • 2019-04-14
  • 1970-01-01
  • 1970-01-01
  • 2012-09-12
  • 2017-08-21
  • 2021-05-06
  • 1970-01-01
相关资源
最近更新 更多