最近在公司使用swoft写了一个定时爬取数据的项目,上线后跟踪线上日志发现swoft的worker进程会出现周期性的内存溢出:
Fatal error: Uncaught ErrorException: Allowed memory size of 268435456 bytes exhausted (tried to allocate 262144 bytes)
一、分析
观察到线上worker进程的内存占用是随时间增长而逐渐上升的,所以排除单次执行导致内存溢出的情况,基本可以确定是项目中的某处代码存在内存泄漏。
因此,解决问题的关键就在于如何定位到项目中产生内存泄漏的位置。
二、定位内存泄漏
1、Swoole Tracker
Swoole Tracker是Swoole官方提供的分析工具,可以用来检测项目中的内存泄漏问题(使用方法)。
安装
Swoole Tracker的安装比较简单,因为它本身就是一个php的扩展,而且是已经编译好的,我们只需要将编译好的动态链接库文件(.so)放到php的扩展目录下,然后在php.ini中加上配置即可。
使用
安装完Swoole Tracker扩展后,在相应的代码中加入Swoole Tracker提供的Hook函数,例如:
public function onPipeMessage(Server $server, int $srcWorkerId, $message): void
{
trackerHookMalloc();
// ...
}
接着启动服务,运行一段时间后,在docker容器内执行php -r "trackerAnalyzeLeak();"
即可获取到服务运行时的内存申请信息、内存释放信息以及可能存在内存泄漏的位置。
检测结果
文件中含命令行颜色代码,可以在命令行使用
cat memory_leak.txt
查看
以下是检测结果中可能存在内存泄漏的代码位置:
The Possible Leak As Malloc Size Keep Growth:
/var/www/swoft_marketing_engine/vendor/swoft/server/src/Server.php:544 => Growth Times : [29]; Growth Size : [181440]
/var/www/swoft_marketing_engine/vendor/swoft/stdlib/src/Helper/ArrayHelper.php:959 => Growth Times : [8]; Growth Size : [2784]
/var/www/swoft_marketing_engine/vendor/swoft/stdlib/src/Helper/ArrayHelper.php:997 => Growth Times : [12]; Growth Size : [17920]
/var/www/swoft_marketing_engine/vendor/swoft/bean/src/Container.php:413 => Growth Times : [24]; Growth Size : [10752]
/var/www/swoft_marketing_engine/vendor/swoft/db/src/Database.php:252 => Growth Times : [24]; Growth Size : [1536]
/var/www/swoft_marketing_engine/vendor/swoft/db/src/Connection/Connection.php:370 => Growth Times : [24]; Growth Size : [4608]
根据结果,发现是Swoft框架本身某个位置存在内存泄漏。
但是,检测结果中只有产生内存申请时的代码位置,还不能确定具体是什么地方未释放内存。
因此,只能换一种思路。
2、Valgrind
尝试另外一款内存泄漏分析神器——valgrind。
使用后发现分析报告中只会出现相关的C/C++代码,无法定位到具体的PHP代码位置,放弃。
3、memory_get_usage()
使用几种内存泄漏分析工具都无果后,尝试使用最原始的方式——PHP的内置函数memory_get_usage()
。
大致思路就是在需要调用的函数上下,加上memory_get_usage()
函数,例如:
var_dump(memory_get_usage());
test();
var_dump(memory_get_usage());
因为,正常情况下,在函数调用完成后,Zend引擎会将对应的函数栈销毁,相应的内存会被释放。
在使用时,有一些需要注意的点:
memory_get_usage()
支持一个布尔类型的参数real_usage
,表示是否获取真实内存大小,其中包括未被使用的内存空间。因为PHP引擎在申请内存时,会一次申请一大块内存,用于后续使用,以减少系统调用的次数。所以,在这里不要将它设置为true。- 如果函数中存在循环引用(如:Swoft中ORM的join方法),在函数执行完成后,变量并不会被及时释放,输出内存占用会增加,但是这是正常的。因为当待释放变量达到一定数量或内存达到一定阈值时,PHP的GC才会统一释放掉这些的内存。
检查结果
这种方法虽然费时费力,但效果还算不错。经过一段时间排查,成功定位到了两处内存泄漏:
- 在协程中使用Redis时,Swoft未在协程结束后释放相应的Redis连接信息;
- Swoft在处理pipeMessage事件结束后未触发协程结束事件,导致协程数据(MySQL、Redis连接信息等)未被释放。
三、解决方案
既然已经定位到内存泄漏的原因了,解决办法就不难了:
- 协程结束后,手动unset Redis的连接信息;
- 在pipeMessgae事件结束后,手动触发协程结束事件,销毁对应协程产生的数据。
四、后续
经过以上处理后,截至目前,线上swoft项目的内存占用稳定在50M左右,未出现内存溢出错误。
此外,针对这两个问题,我在GitHub上向Swoft团队提交了两个相应的issue及修复PR,目前代码已被合并到master。


五、总结
其实,内存溢出、内存泄漏的情况并不容易发生(单次运行直接爆内存的除外emmm...),只要你:
- 不轻易使用全局变量、静态变量;
- 如果不可避免的需要使用到全局变量、静态变量,一定要在用完后unset掉相应的数据。
另外,在排查过程中,用到的一个挺好用的查看内存占用的工具——smem,可以查看程序的USS、PSS、RSS👍。