不知不觉踩到PHP内存泄漏的雷

最近工作上需要排查php频繁达到内存限制进程被杀掉的原因。项目中使用php写一个死循环,把mysql的数据同步到mq或者mongodb当中。内存问题主要出现在mq消息的发布上。项目中有使用到php-amqplib。

跟踪代码发现,循环内部,获取mq单例对象有问题导致每次循环都是new的一个mq对象。刚开始以为是这个原因导致内存不断增长。三下五除二就改完了,结果一试,没什么效果,还是不断飙升啊。

既然不是新对象引起的,那估计就是就对象的问题。因为新建对象都没有对已有的mq对象进行处理,例如端口连接,释放资源等。因此在新建对象之前,执行php-amqplib 中connection的close操作,关闭连接以及释放资源。关闭之后再操作,确实有些改变,飚的慢点,但是还是会飚。然后又在循环结束的时候unset对象,结果依然没什么变化。

只能接着看代码。php-amqplib中connection的属性中有一个channels属性,用于保存channel对象数组。然而这个channel对象本身又有一个connection属性,这样这两个对象之间就构成一个循环引用,当我们删除connection以及channel的时候,内部引用计数器不会到0,所以内存不会被释放。

用一下简化版说明一下其中的问题:

按正常的逻辑,对象赋值null,那对象所占用的内存应该要被释放。上面的代码输出内容如下:

可以看出,跑了4500次之后内存就已经操作128M了。如果Channel中没有connection的属性,则会有不一样的结果。我们把Channel的构造方法注释掉,再重新跑

只是一个简单的修改,循环就没有内存的问题了。

问题的根本就是对象之间循环引用。有个很有趣的现象,如果对象之间构成循环引用,在xdebug中就可以看到一个无限的树状对象。Connection->Channel->Connection->Channel….

对于普通的web应用而已,一般不会有什么问题,每次请求结束之后fpm会释放掉。但是对于cli应用,这就是致命的。基本上跑个一天就挂了。

但是,现实就是这样。对象之间相互引用很容易出现。这个model需要那个model,几个model之间也很容易构成一个回环。同时,很多东西需要引用第三方类,没办保证第三方类没有相互引用。那有没有不改类之间引用可以解决的呢?

在这次排查,我使用的是 gc_collect_cycles() 强制执行gc操作,释放内存。还是第一段程序代码,循环内容改为一下内容:

输出内容如下:

内存飙升的问题解决了。

网上很多描述都是php5.3之后的gc会自动回收类似这类的垃圾,但是前提是zend节点满了。但实际上,说的只是数组类型。下面的代码在循环结束之后,局部变量data的资源会得到释放。

总的而言,PHP在一些长时间的循环运行当中,一定要小心对象之间相互引用造成内存上升的问题。如果遇到内存上升问题,可以先看看代码当中有没有什么类之间存在循环引用。平时写代码的时候也需要尽量避免对象之间构成循环引用,避免在不经意之间给自己或团队挖个坑。