hyperf 服务 rss 持续上涨被 oom kill 是因协程上下文强引用未断导致内存泄漏;memory_get_usage() 无法反映 swoole 协程栈、redis/pdo c 层等真实内存占用,需用 ps 查 rss 并结合 gc_collect_cycles 观察回落情况。

Hyperf 服务跑着跑着 RSS 持续上涨,最后被系统 OOM Kill —— 这不是 PHP 内存配置太小的问题,而是协程上下文里有强引用没断,GC 根本收不走。
为什么 memory_get_usage() 看不出泄漏?
它只统计 Zend 堆内存,完全看不见 Swoole 协程栈、Redis 扩展 malloc 的 C 层内存、PDO 预处理语句缓存、甚至 Monolog 的日志缓冲区。你看到 memory_get_usage(true) 是 8MB,ps aux --sort=-rss 查到的 RSS 可能已是 320MB。
- 必须用
ps -o rss= -p $(pgrep -f "hyperf")每 30 秒采一次,看是否单向爬升 - 每次请求结束加
gc_collect_cycles(); gc_mem_caches();,再等 5 秒观察 RSS 是否回落 - 回落不了 → 说明有对象被静态变量、闭包、事件监听器或 Table 行强持有
Hyperf 里最常踩的 4 类强引用坑
Hyperf 默认启用 DI 容器和 AOP,但很多内置行为在协程复用下会悄悄累积引用:
-
Hyperf\HttpServer\Router\Dispatched实例被中间件链反复注入,$request->getAttribute('dispatched')返回的仍是同一个对象,上传文件路径、解析参数等都堆在里面 - 全局
Hyperf\Utils\Coroutine::create()启动的协程,若回调里用了use ($this),整个容器实例就被钉住 -
@OnWorkerStart里注册的Event::on()监听器,没配once=true或没手动Event::off(),每次 reload 都叠加一份 -
Hyperf\Cache\Annotation\Cacheable注解生成的闭包,隐式捕获了$this和完整上下文,缓存键失效后对象仍不释放
用 swoole_tracker 抓 C 层泄漏(尤其 Redis/PDO)
Hyperf 大量依赖 Redis、MySQL、JSON 扩展,这些扩展的 C 层 malloc 不受 PHP GC 管理,swoole_tracker 是唯一能定位到具体行号的工具:
- 启动前设环境变量:
export SWOOLE_TRACKER_ENABLE_MALLOC_HOOK=1,并确保tracker.enable_memcheck=1关闭(二者互斥) - 调试时采样率设为
tracker.sampling_rate=5,上线绝对不能开满 - 泄漏日志在
/tmp/swoole_tracker.log,关键行类似:[leak] addr=0x7f8b4c0a1234 size=32768 at ext/redis/redis.c:1234 - 常见泄漏点:
redisCommand返回的redisReply*未 free、PDOStatement::execute()后未调用closeCursor()
协程退出前必须做的三件事
Hyperf 的 go() 或 defer 并不自动清理上下文,得手动干预:
- 所有大数组、资源句柄、临时文件路径变量,退出前显式
unset($var) - 用
WeakReference::create($obj)替代直接use ($obj),避免闭包强持对象 - 检查
Hyperf\Contract\ConnectionInterface实现类(如Hyperf\Redis\Redis),确认__destruct()里调了close(),且没被协程生命周期绕过
真正难的不是发现泄漏,而是识别“谁还在 hold 它”——静态属性、全局事件总线、ORM 的 UnitOfWork、甚至日志处理器的缓冲队列,都可能在你看不见的地方默默攒着引用。RSS 不降,就别信“GC 已运行”。











