Hyperf导出大数据时RSS飙升而memory_get_usage()不涨,因后者仅统计PHP堆内存,忽略Swoole协程栈、C扩展malloc及Monolog缓冲区;泄漏主因是协程强引用或C层未释放,Generator若配合ORM仍全量加载,反加剧泄漏;需直连PDO禁用缓冲、逐行fetch、避免闭包捕获$this、定期gc回收,并考虑进程隔离。

Hyperf导出大数据时memory_get_usage()不涨但RSS飙升
这是因为memory_get_usage()只统计PHP Zend堆内存,完全看不见Swoole协程栈、PDO/Redis扩展的C层malloc内存、甚至Monolog缓冲区。你看到memory_get_usage(true)是12MB,ps -o rss= -p $(pgrep -f "hyperf")查到的RSS可能已是480MB——泄漏发生在协程上下文强引用或C扩展未释放,不是PHP变量没unset。
Generator本身不解决协程内存泄漏,反而可能加重
在Hyperf里直接用yield返回查询结果,看似流式,实则陷阱重重:
- 若底层用
PDO::fetchAll()或Hyperf\Database\Connection::select(),Generator只是把已加载的全量数组“包装”成迭代器,内存早已吃满 - 协程未退出前,Generator对象+闭包+上下文里的
$this被强持有,GC无法回收 - Hyperf默认开启AOP和DI,
Cacheable注解生成的闭包会隐式捕获整个容器实例,哪怕Generator执行完,对象仍驻留
真正有效的Generator写法:绕过ORM,直连PDO并禁用缓冲
必须让数据库驱动不缓存结果集,再配合协程安全的逐行消费:
- 使用
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false(MySQL)或PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL(PostgreSQL) - 不用
fetchAll(),改用fetch()或fetch(PDO::FETCH_ASSOC)单行读取 - Generator函数内避免
use ($this),所有依赖通过参数传入,防止DI容器被钉住 - 每处理N行(如500行)后显式调用
gc_collect_cycles()和gc_mem_caches()
示例关键片段:
public function exportRows(): \Generator
{
$pdo = $this->pdo; // 避免从容器get,防止$this被闭包捕获
$stmt = $pdo->prepare('SELECT id,name,amount FROM orders WHERE status = ?');
$stmt->execute(['paid']);
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
yield $row;
if (++$i % 500 === 0) {
gc_collect_cycles();
gc_mem_caches();
}
}
}
比Generator更关键的是调度与资源清理时机
Hyperf里Generator常跑在Worker协程中,而协程生命周期不由Generator控制。容易忽略的硬伤:
- 没配
Context::set()的defer清理,中间存的大数组(如临时统计map)永远不释放 - 导出过程中启了子协程(如异步写日志),但没等它结束就return,导致子协程残留
- 用了
Hyperf\Cache\Driver\RedisDriver做进度上报,但setex命令在协程中断时可能卡住连接池 - 导出接口没加
@Middleware(TimeoutMiddleware::class),超时后协程强行终止,C层内存(如PDO预处理句柄)根本来不及释放
最稳妥的做法:导出逻辑单独起Hyperf\Process\Process,用pcntl_fork隔离内存空间,完成即exit,彻底规避协程上下文污染。


















