Hyperf内存泄漏最典型信号是Worker进程内存持续上涨、重启回落、再飙升;应使用memory_get_usage(true)在协程入口出口打点测差值,差值超1MB且复现即为泄漏点。

直接看内存增长规律,别等服务崩了才动手。Hyperf内存泄漏最典型的信号是某个Worker进程内存随时间持续上涨、重启后回落、过段时间又飙升——这说明不是偶发抖动,而是资源没被释放。用memory_get_usage(true)打点是最轻量、最直接的定位手段,不需要改架构、不依赖外部工具。
在关键协程入口和出口打点测真实堆内存
协程退出不等于内存释放,memory_get_usage(true)能反映当前脚本分配的**真实堆内存总量**(单位字节),比memory_get_usage()更准。重点不是“用了多少”,而是“退出前后差多少”。
- 在
Coroutine::create()回调开头、结尾各调一次,记录差值。差值 > 1MB 且稳定复现,基本就是泄漏点 - 不要只测单次请求:启动一个每秒触发的定时任务,连续跑5分钟,用
posix_getpid()标出Worker ID,观察同一PID下内存是否线性增长 - 避免干扰:关闭Profiler、Xdebug等可能影响堆分配的组件,确保测的是业务逻辑本身
Context::set()后必须配defer清理
协程上下文是高频泄漏源。Context::set()存的数据不会随协程结束自动销毁,尤其当值是大数组、对象或资源句柄时。
- 错误写法:
Context::set('cache', $bigArray);—— 协程结束,$bigArray仍驻留内存 - 正确写法:在协程内立刻加
defer,例如defer(fn() => Context::del('cache')); - 若用
Coroutine::fork()并复制了上下文,要确认复制的键是否包含大对象;否则复制行为本身就会触发一次深拷贝内存分配
检查静态变量和自定义进程的生命周期
静态变量在Swoole常驻进程中永不销毁,自定义进程若未显式退出,其内部协程和变量也会长期持有内存。
- 全局静态缓存如
public static $data = [];,每次push都累加,必须配合unset或定期clear - 自定义进程里用
Coroutine::create()启协程,必须确保有明确退出路径,并在退出前调用Coroutine::close($cid)(注意:不是所有版本Swoole都支持该函数,PHP 8.0+ + Swoole 4.8+ 才稳定) - 数据库连接、文件句柄、Redis连接池实例,若存在“创建但不close/destroy”的情况,
memory_get_usage(true)会看到它们长期占位
日志轮转配置不当也会导致句柄堆积
看起来和内存无关,但lsof | grep hyperf.log若显示上百个打开的rotating日志文件句柄,每个句柄背后都有缓冲区和结构体,会实实在在吃掉几十MB堆内存。
- 确认
RotatingFileHandler的useLocking设为false(Swoole协程下必须关) - 检查
maxFiles是否过大(比如设成100),旧日志没及时删除,句柄就一直挂着 - 用
strace -p $(pgrep -f 'hyperf start') -e trace=openat,close抓系统调用,看是否有openat频繁但close缺失
真正难排查的不是“哪行代码漏了”,而是“哪个上下文、哪个静态引用、哪个自定义进程,在你没盯住的时候悄悄涨内存”。memory_get_usage(true)只是探针,关键得带着问题意识去打点——比如每次加缓存、每次启协程、每次轮转日志,都默认它可能不收尾。


















