Hyperf 内存泄漏典型现象是 PHP 堆内存耗尽或容器 OOMKilled,需先区分二者:日志含 PHP 内存错误无 OOMKilled 为堆内泄漏;Pod 事件出现 OOMKilled 则为堆外泄漏,如协程栈、连接池等。

Hyperf 内存泄漏的典型现象和第一判断点
看到 Fatal error: Allowed memory size of ... exhausted 或容器被 OOMKilled(exit code 13),先别急着改代码——先确认是 PHP 堆内存溢出,还是系统级容器 OOM。两者排查路径完全相反:
日志里有 java.lang.OutOfMemoryError: Java heap space?那是 Java 服务,和 Hyperf 无关;
日志里只有 PHP 内存报错、没 OOMKilled 事件?说明是 PHP 进程自己吃爆了内存;
日志安静无声、Pod 事件里明确出现 OOMKilled?那问题在堆外:协程栈、Redis 连接池、PDO 非缓冲结果集、Swoole 扩展原生内存等。
Hyperf 场景下,绝大多数“内存涨得慢、重启后回落”的情况,都指向协程常驻进程里的累积型泄漏,不是单次请求的问题。
连表查询导致 OOM:关缓冲 + 游标读取是硬性要求
Hyperf 的 Db::select() 或 ORM get() 看似安全,实则默认走 PDO 缓冲查询——哪怕你只 fetch 一行,整张关联结果(比如 50 万行订单+明细)已全量加载进内存。
这不是 SQL 写得不好,是 PDO 默认行为:PDO::MYSQL_ATTR_USE_BUFFERED_QUERY 为 true。
必须手动禁用缓冲并逐行处理:
- 在数据库配置中显式关闭缓冲:
'options' => [PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false] - 避免使用
get()或all(),改用原生query()->cursor()(Hyperf 3.x+)或底层fetch()循环 - 注意:ORM 的
cursor()在 Hyperf 3.0 之前不真正穿透到底层游标,仍会缓冲,务必确认版本 - 连表后行数爆炸(如 1:N 关联),内存压力直接翻倍,尤其配合
toArray()或 JSON 序列化时更危险
定时任务和长周期协程是泄漏高发区
Hyperf 定时任务(@Cron)跑在常驻 Worker 进程里,每次执行若积累变量、缓存对象、未释放资源,内存就只增不减。
常见泄漏点:
- 全局数组反复
[]=追加数据,未unset或清空 - 闭包
use捕获大对象(如整个$container或 DB 连接),生命周期被延长 - Redis
pipeline()或multi()调用后异常跳出,未执行exec(),连接卡在协程上下文里不归还 - 忘记调用
gc_collect_cycles()—— 尤其在循环中创建大量小对象时,PHP GC 不会自动触发 - Worker 进程 ID 持续增长(如到 78 万)、且泄漏时间点与定时任务执行节奏吻合,基本可锁定
协程上下文污染和连接池泄漏难察觉但影响致命
Hyperf 的协程模型让资源复用变得隐蔽:一个协程出错,可能污染整个上下文,导致后续 Redis 或 MySQL 操作异常,间接引发连接泄漏。
典型连锁反应:
- MySQL 因
wait_timeout断连 → 事务回滚失败 → 异常未捕获 → 同一协程继续执行 Redis 操作 →Co\Redis报Connection reset by peer或静默卡死 - Redis 连接池耗尽表现为
WaitTimeoutException,但根因可能是前面的 MySQL 异常阻塞了连接归还 -
Context::has($redis->getContextKey())返回true却没调exec(),说明 pipeline 上下文已泄漏 - 用
swoole-tracker开启tracker.enable_malloc_hook=1可定位原生内存分配热点,但需注意它本身有性能开销,仅用于排查期
真正难处理的不是单个泄漏点,而是多个组件在同一个协程生命周期里相互污染——查到某处释放了连接,但上游早把上下文搞坏了。所以排查必须从请求/任务入口开始,顺藤摸瓜看完整协程链路,而不是孤立看某一行代码。


















