多处理:分叉的缺点?

问题描述 投票:0回答:1

我们在使用 Python Celery(使用多处理)时遇到了一个问题,即大型周期性(计划)任务会在短时间内消耗大量内存,但由于工作进程在池的整个生命周期中都存在 (

MAX_TASKS_PER_CHILD=None
),内存没有被垃圾收集(即它被“高水位”保留)。

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

我们发现,通过设置

MAX_TASKS_PER_CHILD=1
,我们可以在每个任务之后分叉一个新进程(Celery 工作实例),并且内存会被正确地垃圾收集。甜甜的!

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

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

python multithreading multiprocessing celery
1个回答
3
投票

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

在 Python 3.4 及更高版本上,您可以通过在程序启动时显式将 multiprocessing

 启动方法设置为 
forkserver
(在执行工作线程不依赖的任何工作之前)来避免此问题,这会将工作线程从单独的服务器进程不应大幅增加大小。


注意:上面我说过“大概会写入很少的内存,因此需要很少的复制,实际的内存使用不会是一个主要问题”,但这对 CPython 来说是一个谎言。一旦循环垃圾收集器运行,所有可能参与引用循环的对象的引用计数(例如所有异构容器类型,但不是像

int

float
这样的简单“叶”类型)都会被触及。这样做会导致包含它们的页面被复制,因此您
实际上消耗了父级和子级中的内存。

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

    在启动循环垃圾收集器之前完全禁用它们(内存泄漏的巨大潜力;循环很容易由各种事物形成,并且循环引用的任何内容都永远不会被清除)。
  1. 做你正在做的事情并设置
  2. MAX_TASKS_PER_CHILD=1
    ,这样即使进程确实执行 COW 副本,它们也会快速退出并被重新绑定到父进程的新进程替换,并且不会自行消耗内存。
也就是说,从 3.7 开始,当您自己手动启动进程(或负责创建池)时,还有第三种选择:

  1. import gc

    位于文件顶部,在尽可能初始化之后,但在创建第一个 
    Process
    Pool
     对象之前,运行:

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

    gc.freeze

    文档
    进一步建议尽快在父级中禁用GC,在freeze
    之前禁用
    fork
    ,并在子级中重新启用
    gc
    ,以避免由其他预
    fork
    垃圾回收触发的COW可以通过触发 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

(和相关函数)并解释了预期用例。

© www.soinside.com 2019 - 2024. All rights reserved.