python - 多处理: fork 的缺点?

标签 python multithreading multiprocessing celery

我们在使用 Python Celery(使用多处理)时遇到了一个问题,其中大型定期(计划)任务会在短时间内消耗大量内存,但因为工作进程在池的整个生命周期中都存在 ( MAX_TASKS_PER_CHILD=None ),内存不会被垃圾回收(即它被“高水位”保留)。

(Heroku 使这个问题进一步恶化,它会分配大量且恒定的内存,并将其转换为交换,从而降低性能。)

我们发现通过设置MAX_TASKS_PER_CHILD=1 ,我们在每个任务之后 fork 一个新进程(Celery 工作实例),并且内存被正确地垃圾收集。甜甜的!

但是,有很多文章提出了相同的解决方案,但我没有发现任何缺点。 在每项任务之后 fork 一个新流程有哪些潜在的缺点?

我的猜测是:
1. CPU 开销(但可能很小)
2. fork 时可能出现的错误(但我找不到任何相关文档)

最佳答案

除了重复 fork 导致 CPU 开销明显增加(如果工作线程为每个任务完成足够的工作,这没什么大不了的)之外,一个可能的缺点是父进程的大小继续增长。如果是这样,它会增加所有子进程的大小(这些子进程正在 fork 一个越来越大的父进程)。这并不重要(大概会写入很少的内存,因此需要很少的复制,实际内存使用不会成为主要问题),但是 IIRC,Linux 过度使用启发法假设 COW 内存最终会被复制,即使您实际上远没有超出私有(private)页面的启发式限制,您也可以调用 OOM killer 。

在 Python 3.4 及更高版本上,您可以通过显式 setting your multiprocessing start method to forkserver 来避免此问题在程序启动时(在执行工作程序不依赖的任何工作之前),这将从一个单独的服务器进程中 fork 工作程序,该进程的大小不应显着增加。

<小时/>

注意:上面我说过“大概会写入很少的内存,因此需要很少的复制,实际的内存使用不会是一个主要问题”,但这对 CPython 来说是一个谎言。一旦循环垃圾收集器运行,所有可能参与引用循环的对象的引用计数(例如所有容器类型,但不是像 intfloat 这样的简单基元) >) 被感动了。这样做会导致包含它们的页面被复制,因此您实际上消耗了父级和子级中的内存。

在 3.4 中,对于长时间运行的子进程没有好的解决方案,唯一的选择是:

  1. 在启动循环垃圾收集器之前完全禁用它们(存在内存泄漏的巨大潜力;循环很容易由各种事物形成,并且循环引用的任何内容都永远不会被清除)。
  2. 按照您正在做的事情进行操作并设置 MAX_TASKS_PER_CHILD=1,这样即使进程确实执行 COW 副本,它们也会快速退出并被新的进程所取代,这些新进程与父进程相关联,并且不会自行消耗内存。

也就是说,从 3.7 开始,当您自己手动启动进程(或负责创建池)时,还有第三种选择:

  • import gc 在文件顶部,并在尽可能初始化之后,但在创建第一个 ProcessPool 对象,运行:

    gc.freeze()   # Moves all existing tracked objects to permanent generation,
                  # so they're never looked at again, in parent or child
    

    The gc.freeze docs进一步建议尽快在父级中禁用 GC,在 fork 之前卡住,并在子级中重新启用 gc,以避免其他 pre 触发 COW。 -fork 垃圾回收留下的内存间隙可以通过触发 COW 的新分配来填充(您在父级中泄漏了一些内存,以换取最大限度地减少子级中的取消共享),因此更完整的解决方案可能看起来像:

    # Done as early as possible in the parent process to minimize freed gaps
    # in shared pages that might get reused and trigger COW
    gc.disable()  # Disables automatic garbage collection
    
    # Done immediately before forking
    gc.freeze()   # Moves all existing tracked objects to permanent generation so GC
                  # never touches them
    with multiprocessing.Pool(initializer=gc.enable) as pool:  # Reenables gc in each
                                                               # worker process on launch
        # Do stuff with pool
    # Outside with block, done with pool
    gc.enable()  # Optionally, if you never launch new workers,
                 # reenable GC in parent process
    
  • 您可以在CPython bug #31558上阅读有关此功能的基本原理和预期用例的更多信息。 ,它描述了问题,创建了 gc.freeze (和相关函数)并解释了预期的用例。

    关于python - 多处理: fork 的缺点?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41745319/

    相关文章:

    python - Tensorflow 2 字符串标签到 one_hot

    python - 索引错误: list index out of range for the caesar cipher

    python-3.x - 执行 popen 并超时

    Python 类和方法

    python - Tensorflow LSTM 中的 c_state 和 m_state 是什么?

    c# - 从多个线程无锁写入文件

    c# - 当其他线程正在使用字典时,在字典中合并修改的好方法是什么?

    multithreading - 如何在多核处理器上完成线程的上下文切换?

    python - 多线程,无法运行进程命令

    python - 如何在蒙特卡罗积分中实现多处理