我们在Python Celery(使用多处理)中遇到了一个问题,其中大型周期性(计划的)任务在短时间内消耗大量内存,但由于工作进程在池(MAX_TASKS_PER_CHILD=None
)的整个生命周期中都存在,因此内存不会被垃圾收集(即,它被"高水位"保留)。
(Heroku进一步恶化了这个问题,它看到分配了大量恒定的内存,并将其转换为交换,从而降低了性能。)
我们发现,通过设置MAX_TASKS_PER_CHILD=1
,我们在每个任务之后派生一个新的进程(Celery worker实例),并且内存被正确地垃圾收集。含糖的
然而,有很多文章提出了同样的解决方案,但我没有发现任何缺点。在每项任务后分叉一个新流程的潜在缺点是什么
我的猜测是:
1。CPU开销(但可能是少量)
2。分叉时的潜在错误(但我找不到任何关于的文档)
除了重复分叉会明显增加CPU开销(如果工人每个任务做足够的工作,那就没什么大不了的了)之外,一个可能的缺点是父进程的规模继续增长。如果是这样,它会增加所有子进程的大小(这些子进程正在分叉一个越来越大的父进程)。这并不重要(大概只会写入很少的内存,因此只需要很少的复制,实际的内存使用也不会是一个主要问题),但IIRC,Linux的过度使用启发式假设COW内存最终会被复制,并且您可以调用OOM杀手,即使您远未达到实际在私有页面方面超过启发式限制。
在Python3.4及更高版本上,您可以通过在程序启动时(在执行工作人员不依赖的任何工作之前)将multiprocessing
启动方法显式设置为forkserver
来避免此问题,这将从一个单独的服务器进程派生工作人员,该进程的大小不应急剧增加。
注意:在上面,我说";大概很少的存储器将被写入并且因此需要很少的复制并且实际的存储器使用将不是主要问题";,但这在某种程度上是对CPython的谎言。循环垃圾收集器一运行,所有可能参与引用循环的对象(例如,所有容器类型,但不是像int
和float
这样的简单基元)的引用计数就会被触发。这样做会导致包含它们的页面被复制,因此实际上会消耗父级和子级的内存。
在3.4中,对于长时间运行的子进程没有好的解决方案,唯一的选择是:
- 在启动循环垃圾收集器之前完全禁用它们(这会带来巨大的内存泄漏潜力;各种东西很容易形成循环,从循环中引用的任何东西都永远不会被清理)
- 执行您正在执行的操作并设置
MAX_TASKS_PER_CHILD=1
,这样即使进程确实执行COW复制,它们也会快速退出,并被重新定时到父进程的新进程所取代,并且不会自行消耗内存
也就是说,从3.7开始,当您自己手动启动流程(或负责创建池)时,还有第三种选择:
-
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
文档进一步建议在父级ASAP中禁用GC,在fork
之前禁用freeze
,并在子级重新启用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错误#31558上阅读更多关于此功能的基本原理和预期用例的信息,该错误描述了问题、创建了gc.freeze
(和相关函数)并解释了预期用例。