PHP常驻进程内存泄漏排查

最近写了某个网站的爬虫(具体爬取什么内容不是此篇的重点哈。。),使用的是PHP的 hyperf 框架。进程运行起来发现内存一直在涨,如图:

image-20211008195129758

如图,我使用 hyperf 的 process 模块,开启了四个常驻进程,用来持续地遍历爬取页面。但运行起来以后发现 process 进程内存持续增长,这台 2C4G 的机器大约持续爬取 半个小时,整台机器就直接 ssh 不上了,查看阿里云后台内存占用和 CPU 均占用 100%,top 负载高达 60+.

解决思路:

1.首先详细过了一遍逻辑部分,发现没有持续追加赋值的变量,那这部分累积的内存是什么地方占用了呢?
2.尝试使用 top 观看占用,process 进程的 cpu 占用并不高,变化在 8-50% 之间,主要是网络请求和解析 json 部分。
3.尝试使用 swoole 官方提供的调试工具 swoole_tracker,看能不能找到疑似内存的代码,无果。
4.strace -p [Process进程ID] ,截图如下:

image-20211008195809879

基本都是 socket 请求收发,看不出什么异常。

5.此时心里大致有个数了,一定是代码里有哪块的静态变量在持续赋值申请内存。由于我使用的是 GuzzleHttp 的协程网络库,尝试在 $httpClient = new Client() 使用完之后主动释放内存 unset($client)。发现并不解决问题。
6.忙活一上午,突然想到最近用了一个 php 的 html DOM 解析包 phpQuery(https://github.com/phpquery/phpquery),这个库开发时针对的是 php5.3+ 版本,已经两年没维护了。这个包在传统 php-fpm 模式下是没啥问题的,但在 swoole 常驻进程下就不行了,里面创建的解析对象用到了一些静态变量:

image-20211008200653782

由于我采用的某种循环遍历的方式不断扩散爬取的页面入口 url,这个 process 进程会持续请求新的页面,对于静态变量,只要我的进程没主动退出,将永远不会被销毁。基本就是这里的内存在占用了。

7.验证一下:

我们可以在代码处打上log,使用内存统计 memory_get_usage(),在 new 这个静态对象的前后几处埋上 log。

发现前后对比,内存稳定增加的。

8.解决一下:

静态变量在 php 的 GC 下一般不会被主动回收,我这里在使用完之后直接 unset() 。重启服务,发现进程内存不再增长了。运行半天没变化,问题解决。

总结:如果是真正的内存泄漏,排查方式可能得借助断点调试。这类问题出现在静态变量上的也很频繁。有一些老的 composer package 使用静态变量的问题,在 php-fpm 下并不明显,因为请求完了所有的变量也就销毁了,不会持续累积。但在 swoole 的常驻进程下就不会回收,需要主动回收了。在引入新包的时候需要做好详细的测试,风险是存在的。