python多进程通信manager_Python2.7:多进程通信

写在前面

上文(从来也不说点正事儿 | Python2.7进程池)最后说到为什么multiprocessing.Pool 启动程序会“多出来“两个进程,其中一个是Pool,另一个就是负责多进程通信的组件进程。

本文将简单介绍项目中使用的工具,以及在多进程通信组件上踩过的坑,回顾来看,都是血泪史。

I. 项目中使用过的python多进程通信工具multiprocessing.Manager()

项目接手时,进程间通信用的是multiprocessing.Manager()。

关于multiprocessing.Manager(),网上对该套组件的定义是(翻译自官网):Manager()返回的manager对象控制了一个server进程,此进程包含的python对象可以被其他的进程通过proxies来访问。从而达到多进程间数据通信且安全。

Manager支持的类型有list, dict, Namespace, Lock, RLock, Semaphore, BoundedSemaphore, Condition, Event, Queue, Value和Array。

其次,其使用方法,和python原生的dict()类似。提供get(${key}, ${defualt_value}),items()等,网上有很多教程。

最后需要注意点的是,项目使用的是multiprocessing.Manager().dict(),配合multiprocessing.Manager().Lock() ,因为multiprocessing.Manager().dict()组件是不带锁的,见stackoverflow。

补充一点,项目过渡版本还使用过multiprocessing.Manager().list(),同multiprocessing.Manager().dict()类似,只不过是列表数据结构。

2.multiprocessing.Queue()

使用multiprocessing.Queue()是因为Pool的坑导致的,上文有说到“进程池Pool会在发现有退出的就清除,然后再调用这个方法,装一个不实际干活的worker进来”。实际项目中有一次,因为OOM把Pool的一个进程杀死了,结果Pool很快就起了一个无用进程,这个问题让我们排查了好一会儿。最后,取缔了Pool,直接用Process。

项目后期重构,因代码逻辑修改,需要队列而非multiprocessing.Manager().dict()这种key,value的字典数据结构,最终选择了multiprocessing.Queue() 。首先,multiprocessing.Queue() 不同于python原生的Queue(),专门为多进程通信而生。但是,multiprocessing.Queue()不支持Pool!

使用方法很简单,调用put()和get()就可以取、存数据。取用前记得判空。

multiprocessing.Queue()自带锁机制。

II. 坑

工具的使用不再赘述,这里说几个项目使用时的坑点:multiprocessing.Manager()上踩过的坑 - 内存不释放

出现这个问题是之前忘记限制队列容量,导致队列里一时间塞了几百个数据包,py进程的内存占有量达到将近10%。但是,之后当数据包被“消费”之后,内存仍旧保持在10%不下降,状态维持了几天,最后被我们手工kill。

1.1 排查问题起源于猜测

刚开始出现这个问题的时候,本能首先想到的是“引用计数未清零”导致变量没有被GC回收,经典的case是循环引用。为此,排查了好长一段时间的代码,甚至求助了memory_profiler等工具查看内存,打印出变量的引用计数等等,折腾了很久,发现了multiprocessing.Manager()的一个特性:Manager()是proxy,他存放的不是数据的引用,而是应该直接把数据pickle到自己的内存中。也就是说,放入和取出的ID是会变化的,见StackOverflow。

1.2 实验以佐证

这也就意味着,定位引用计数需要分成“生产者”、“队列”和“消费者”三块分别排查。第一和第三个是自己的源码,在程序退出前显式增加如下代码片段:

del ${val}

gc.collection()

问题没有解决。此外,又借鉴了不少博客,比如使用gc、objgraph干掉python内存泄露与循环引用。然而,把引用计数打印出来,加上再三review代码之后,代码中确实没有循环,没有闭包。只能排除这个原因了。

1.3 第一次猜测失败,继续猜测+实验

脑洞风波开始。

猜测1. “生产者”代码是一个tornado的POST服务,会不会是tornado的打开方式有误呢?

实验:关键字搜索“python tornado 内存不释放”,真的发现了tornado的一个特点:tornado首先会把post过来的数据全部放入内存,如果程序员一直不做处理,最后内存会被吃完。

写了一个简单的demo:

class Service(RequestHandler):

def post(self):

self.write("hello world")

然后就往这个地址不断post15M的数据,不一会儿,内存就达到80%,并且一直没有下降。

但问题是,业务代码确确实实处理了客户端post过来的数据。因此,这个原因暂时排除了。

... ... (挣扎)

猜测2:python的GC锅。

解释:恰巧中间和另一个朋友交流了会儿,他正好也遇到内存不释放的问题,代码和使用的数据结构都不一样,但是表现很像。我们的结论是,可能是python的GC锅,正巧也遇到一篇不错的排查python内存不释放的问题的博客 - python 内存问题(glibc库的malloc相关)。这里姑且留一个坑给自己未来排查吧。

1.4 结论

这个问题至今没有定位到具体原因,如前文所述,可能是GC的锅,但是也可能是multiprocessing.Manager()的bug。

对于项目而言,最后重构了一个版本,替换为multiprocessing.Queue()并且设置了队列上限,至今没有出现内存暴涨不释放问题。

2.multiprocessing.Queue()上踩过的坑 - Broken pipe

Broken pipe顾名思义就是“管道坏了”,这个错误最常见于网络通信中,建立网络连接的接收端和发送端的其中一方回收/向管道发送关闭信号,抑或是其他原因。

第一次出现这个问题是kill了多进程通信组件进程之后,“生产者”和“消费者”的日志就开始打印Broken pipe,这个合情合理,可以理解。

但是,在一次线上运行时,生产者把若干个几十兆的大包数据塞进队列之后,日志也出现了Broken pipe!冷静下来,持续跟踪multiprocessing.Queue()了一段时间的性能表现,发现:处理1M不到的小数据时,长时间(超过一个礼拜)运行倒是从来没有出现这个问题,非常稳定。因此,首先对单个数据包的大小进行了限制,确保不会有几十兆的异常数据产生;此外,设置了队列的容量以减轻工具的压力。目前线上运行稳定。

这个问题在StackOverflow上看到了一个非正式官方说法:这个就是multiprocessing.Queue()自带的bug!

3.multiprocessing.Manager()和multiprocessing.Queue()共同的坑 - 数据存放耗时

这个发现是在multiprocessing.Manager().dict()内存不释放时发现的。排查问题时发现,向python的tornado服务发送一个15M的数据包的响应时间居然要十几秒!

tornado服务只完成简单的几个步骤:

a). 接收post过来的数据;

b). dump成json数据包;

c). 放入multiprocessing.Manager().dict()

在把每个步骤耗时打印出来之后,a). 基本不耗时;b). 大约需要2s的json dump时间;c). 居然要接近10s!

c)步骤的代码demo如下:

content = self._msg_dict.get(msg_type, [])

content.append(msgs)

self._msg_dict[msg_type] = content

解释下代码:

- 第一步,从dict里面取出key为msg_type的value数据content,value是是一个list套dict的数据结构。

- 第二步,把新到来的msgs数据放入content。

- 第三步,重新把content放入dict。

万万没有想到,最耗时的一步居然是“=”赋值,说好的“python一切皆引用”呢?当然,最后的原因在前文已经交代了Manager()是proxy,他不是简单地存入数据对象的引用。

注意这里不能直接用self._msg_dict[msg_type].append(msgs),取出数据后会发现msgs为空。这个是Manager()使用的一个注意点。这也是踩过的一个坑点。

一点心得

之前看到一篇博文说,使用python多进程的时候尽量不要共享数据!实实在在踩了两三个月坑之后,对这句话体会太深。诚挚期待之后官方能够推出更鲁棒的多进程通信组件。

最后,文章写的一些地方有不对之处,望指摘。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值