OOM主因是协程上下文持续持有模型实例、查询器和PDO连接引用,导致GC不及时;应改用原生数组+Db::insert()、控制并发度、避免链式处理。

Hyperf里用Collection::chunk做批处理,但为什么还是OOM?
直接用 Collection::chunk(1000) 分块后循环 insert(),看似合理,实际容易触发内存溢出——因为Hyperf的协程上下文会持续持有未释放的模型实例、查询构造器和PDO连接引用。尤其当每条数据都 new 一个 Model 或调用 newQuery()->insert(),对象不会自动销毁,GC 又不及时。
实操建议:
- 避免在 chunk 内部 new 模型实例;改用原生数组 +
Db::insert()或Query::insert() - 每批执行完主动调用
gc_collect_cycles()(仅限 CLI 场景,协程中慎用) - 确保数据库连接配置启用了
pool.max_connections足够支撑并发批数,否则会卡在连接等待 - 不要在循环中调用
Model::create(),它会触发事件、访问器、强制转模型,开销远超直插
Db::insert() 和 Query::insert() 的参数差异与性能陷阱
Db::insert() 是 Hyperf\Database\Connection 的快捷方法,只接受二维关联数组,键名必须严格匹配字段名;Query::insert()(即 Db::table('xxx')->insert())支持更灵活的字段映射,但底层仍走 PDO::prepare,批量插入时若字段顺序不一致,会导致 SQL 编译失败或静默丢数据。
常见错误现象:SQLSTATE[HY093]: Invalid parameter number,本质是占位符数量和绑定值数量对不上。
实操建议:
- 统一用
Db::table('users')->insert($batch),$batch 必须是索引数组,每项为关联数组且键名完全一致 - 字段顺序不重要,但所有行的键名集合必须相同;缺失字段会报错,多出字段会被忽略(取决于 strict 模式)
- 单次
insert()最好控制在 500–2000 行之间:太小增加网络往返,太大触发 MySQL max_allowed_packet 限制(默认 4MB) - 如果字段含 JSON 类型,确保已通过
JSON_ENCODE预处理,不要依赖模型自动转换
如何让协程真正并发跑批,而不是串行等上一批结束?
很多人写 foreach ($collection->chunk(1000) as $chunk) { co::run(fn() => Db::table(...)->insert($chunk)); },结果发现耗时没降反升——因为 co::run 在协程外调用会退化为同步执行,且没做并发控制,瞬间打爆连接池。
正确做法是用 Co\Channel 控制并发度,配合 go 启动协程:
$channel = new Co\Channel(10); // 并发上限 10
foreach ($collection->chunk(500) as $chunk) {
go(function () use ($chunk, $channel) {
$channel->push(true); // 占位
try {
Db::table('logs')->insert($chunk);
} finally {
$channel->pop(); // 释放
}
});
}
// 等待全部完成
for ($i = 0; $i < $collection->count() / 500; $i++) {
$channel->pop();
}
注意点:
- 别用
Parallel组件,它默认共享连接,高并发下易报MySQL server has gone away - 每批 insert 前检查
Db::getPdo()->inTransaction(),避免嵌套事务冲突 - 如果表有唯一索引且数据可能重复,用
insertIgnore()或onDuplicateKeyUpdate(),但注意 Hyperf 3.1+ 才原生支持后者
为什么用 Collection::pipe() 链式处理反而更慢?
有人喜欢写 $collection->chunk(1000)->pipe(fn($chunks) => collect($chunks)->each(...)),看起来函数式很优雅,但 pipe() 会额外创建闭包作用域,每个 each() 还要遍历 chunk 数组,中间产生大量临时 collection 实例,在万级数据下 GC 压力明显。
实操建议:
- 放弃链式,用传统 foreach;Hyperf 的 Collection 不是 Laravel 那个,底层没做深度优化
- 如果要做字段清洗(比如时间格式化、空值转 null),提前在 chunk 外用
map()一次性处理,别在 insert 前实时计算 - 避免在循环中调用
now()、Str::uuid()等协程不安全函数;改用date('Y-m-d H:i:s')或uniqid('', true) - 日志记录别放 insert 循环里,用
Logger::info('batch done', ['size' => count($chunk)])即可,避免 IO 阻塞
真正的瓶颈往往不在 SQL 本身,而在每次 insert 前的 PHP 层对象构建、类型转换和协程调度开销。把数据当成纯数组来处理,比当成模型来操作快 3–5 倍,这点很容易被忽略。


















