hyperf 读取 mysql blob 字段易触发 oom,因 pdo 默认启用缓冲模式(pdo::mysql_attr_use_buffered_query=true),需在 prepare 前设为 false,并手动 unset 大字段释放内存。

Hyperf 读取 MySQL 二进制大字段(如 BLOB、MEDIUMBLOB、LONGBLOB)时,不加控制会直接加载进内存,极易触发 OOM —— 尤其在连表 + 大字段 + 高并发场景下,不是数据有问题,是 PDO 缓冲模式没关。
为什么 BLOB 字段一查就内存爆炸
Hyperf 默认用 PDO 执行查询,而 PDO::MYSQL_ATTR_USE_BUFFERED_QUERY 默认为 true。这意味着:哪怕你只 fetch() 一行,PDO 也会把整张结果集(含所有 BLOB 内容)一次性从 MySQL 拉到 PHP 进程内存里缓存起来。
协程再轻量,也扛不住单次加载几十 MB 的二进制块;Worker 进程常驻,内存不释放,泄漏会累积;错误日志只报 Fatal error: Allowed memory size of ... exhausted,根本看不出是 BLOB 搞的鬼。
- 连表后结果行数翻倍,
BLOB字段被重复拷贝多次(比如users JOIN avatars) -
DB::select()或 ORM 的get()方法底层都走缓冲模式,表面“逐行”,实际早全载入 - Hyperf 3.1.66 及之前版本,
cursor()方法不真正穿透到底层游标流式读取,仍可能触发缓冲
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY = false 必须在 prepare() 前设置
这是最关键的一步,设晚了完全无效。必须在获取 PDO 实例后、调用 prepare() 前完成属性设置:
$pdo = Db::getPdo();
$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); // ✅ 正确位置
$stmt = $pdo->prepare('SELECT id, name, avatar FROM users WHERE status = ?');
$stmt->execute([1]);
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
// $row['avatar'] 是原始二进制字符串,可按需处理(如写文件、计算 hash、跳过)
if (isset($row['avatar']) && strlen($row['avatar']) > 1024 * 1024) {
// 超过 1MB 的 BLOB,不参与后续业务逻辑,及时 unset
unset($row['avatar']);
}
processUserRow($row);
}
- 不能用
fetchAll()、get()、cursor()等封装方法,它们绕不开缓冲层 - 如果要用 ORM,必须手动禁用缓冲后,再通过
$model->newQuery()->getConnection()->getPdo()拿到底层PDO实例 - 设完属性后,
prepare()必须用原生 SQL,不要混用 Query Builder 的参数绑定(部分版本存在绑定失效风险)
大字段读取后的内存管理要点
即使关了缓冲,fetch() 出来的 $row 仍包含完整二进制内容,PHP 不会自动释放。必须主动干预:
- 对确定不用的大字段(如
raw_log_data、thumbnail),在循环体内立即unset($row['field_name']) - 避免在循环外保存整个
$row数组(比如塞进一个全局$allRows),否则等于又建了一次内存副本 - 若需传输或存储
BLOB,优先用流式写入(如file_put_contents($path, $row['blob'], FILE_APPEND | LOCK_EX)),而非先拼接再写 - 注意
memory_limit设置:Hyperf Worker 进程生命周期长,单次请求占用的内存不会随请求结束自动归零,残留引用会导致持续增长
连接池配置要配合大字段场景调低 max_connections
每个连接在 fetch 大字段时都会独占一份内存副本,连接数越多,并发加载压力越大。此时盲目提高 max_connections 只会让 OOM 更快到来:
- 假设单个
BLOB平均 2MB,每连接并发处理 5 行 → 单连接峰值内存 ≈ 10MB - 8 个 Worker ×
max_connections=10→ 最多 80 连接 → 理论峰值内存 ≈ 800MB,远超常见容器限制 - 建议将
max_connections压到Worker 数 × 2~3(如 8 Worker 设为 24),同时确保min_connections ≥ 1避免首请求建连延迟 - MySQL 侧也要检查
max_allowed_packet是否足够(默认 4MB),否则大字段会被截断,且错误不明显
真正麻烦的从来不是“怎么读出来”,而是“读出来之后,那一坨二进制还赖在内存里不肯走”——unset 不是可选项,是必做动作;buffered_query = false 不是高级技巧,是保命开关。











