废话不多说,总共分三步

1、初步定为泄漏:

  • 迫于 996ICU 的压力,广大的 PHPer 一般不会关注泄漏问题,都是在看到报错
Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 12288 bytes)
Bash

才发现泄漏问题,此时我们一般会通过查看进程的RSS占用来确定内存占用,例如这样

cat /proc/28806/status |grep RSS
Bash

一定要注意的是,此处查看的RSS是包含共享内存的(共享的内存会重复计算多次),并不是进程真正占用的内存,USS才是我们 PHP 代码申请的内存,我们更应该关注的是USS指标。(感兴趣的小伙伴可以看这个视频)。

  • 怎么看USS,这里推荐smem这个命令,用法和结果如下:

RSS 占用特别多,USS 特别少,证明大部分 RSS 都是共享内存占用,此时大概率是你的Swoole Table或者Apcu用的有问题,因为这两个底层是基于共享内存的。

2、定位泄漏的代码:

可以用Swoole Tracker提供的工具来定位,具体参考这篇文章

3、清理内存碎片:

如果 Tracker 发现不了泄漏,内存还一直涨,八成是遇到了 PHP 的内存碎片问题,内存碎片问题也是我想写这篇文章的原因,社区里面有个小伙伴用了Swoole Tracker没有发现泄漏,但是通过 smem 命令查看内存确实在涨,即使unset了所有变量,内存仍然无法降下去,代码如下:

function main()
{
    for ($i = 1; $i < 2000000; $i++) {
        $GLOBALS[$i] = str_repeat("str_repeat这个函数会申请内存,但我马上就unset掉", 10);
    }

    for ($i = 1; $i < 2000000; $i++) {
        unset($GLOBALS[$i]);
    }
}

main();
PHP

咋回事呢?根本原因是产生了内存碎片,和 PHP 的内存分配算法有关,这里不展开讲,大概原理是:小于 3072 字节的内存申请 PHP 会认为是小内存,PHP 会把所有申请的小内存块缓存起来,即使释放了也不归还给操作系统,以保证内存管理的效率

在 FPM 下请求结束后会释放所有内存,大部分归还给系统,只保留一小部分,但是在 Cli 下没有这样的机制,我们该怎么办?

我研究了一天,貌似只能给 php-src 提 pr 了,写了一天发现”咦?”怎么有段代码怎么和我的思路这么相似,仔细一看”日哦”,在 php 高版本提供了一个gc_mem_caches()函数(网上没有任何文章介绍这个函数),可以手动清理这种小内存的碎片问题 - -!

我们可以设置一个 Swoole 的定时器,定期调用gc_mem_caches()即可。

注意gc_mem_caches()能极大的缓解PHP下面的内存碎片导致的内存增长问题,但是由于zend mm的设计机制问题,长时间运行还是会缓慢增长,这里我们也可以通过替换 PHP 的内存管理模块(比如采用 jemalloc)来彻底避免这种问题,具体方法是自己编译一个jemalloc的so然后启动PHP的时候这样export USE_ZEND_ALLOC=0 && LD_PRELOAD='/usr/local/lib/libjemalloc.so' php /home/guoxinhua/swoole_server.php即可。

总结

第一步正确的发现泄漏,第二步使用 tracker 定位泄漏代码并 fix 它,第三步如果还是泄漏,尝试调用gc_mem_caches()清理碎片。

希望此文章能帮到你^_^

PHP常驻进程,内存泄漏排查指南