OOM 首先区分堆内存或直接内存:堆爆常见于 runtime.sysMap + mallocgc + 大量对象分配;堆外问题多见于 goroutine 泄漏、pgx 简单协议未释放 buffer 或 io.Copy 切片膨胀。

查日志先看 OOM 类型是堆还是直接内存
GORM 本身不直接分配堆外内存,但底层驱动(如 pgx、mysql)和连接池行为会影响内存走向。遇到 fatal error: runtime: out of memory,第一反应不是改 GORM 配置,而是确认 Go 运行时报的是哪类 OOM:
– 如果 panic 堆栈里出现 runtime.sysMap + 大量 mallocgc 调用,且申请 size 是几百 MB 级(如 0x12fffff10),基本是堆内存爆了;
– 如果日志里有 runtime: cannot allocate memory for stack 或伴随大量 net.Conn、bufio.Reader 实例,要怀疑 goroutine 泄漏 + 堆累积;
– 若用 pgx 且启用了 pgconn.Config.PreferSimpleProtocol = true,还要留意是否触发了未释放的 bytes.Buffer 或 io.Copy 中的临时切片膨胀。
检查 GORM 查询是否一次性加载全量结果
GORM 的 Find、First、Rows 等方法默认把结果集全拉进内存。高并发下多个请求同时执行 db.Find(&users) 加载几万条记录,会瞬间撑爆堆。
- 用
db.Session(&gorm.Session{PrepareStmt: true}).Limit(1000).Find()强制分页,避免无限制拉取 - 对大数据导出场景,改用
db.Rows()+ 手动Scan,配合rows.Next()流式读取,每轮只 hold 一行数据 - 禁用 GORM 的预加载(
Preload)嵌套过深,比如Preload("Orders.Items.Tags")容易产生笛卡尔积级对象树 - 检查是否误用
db.Model(&User{}).Select("id").Where(...).Find()却把结果存进一个大 slice,实际只需要 ID 列 —— 改用pluck:db.Model(&User{}).Where(...).Pluck("id", &ids)
排查连接泄漏与 context 取消失效
GORM v2 默认使用 context.Background() 执行查询,如果上层没传带超时的 ctx,且数据库响应慢或卡住,goroutine 就会一直挂着,附带的 *sql.Rows、bytes.Buffer、甚至整个调用栈帧都留在堆上。
- 所有 GORM 调用必须显式传入带 deadline 的 context:
db.WithContext(ctx).Find(&u),且ctx应来自 HTTP 请求或定时任务的顶层生命周期 - 确保
http.Server.ReadTimeout/WriteTimeout与数据库context.WithTimeout匹配,避免 goroutine 在 DB 响应前就被 cancel,但 GORM 没监听到(尤其用pgx时需确认其支持 cancel) - 不要在 goroutine 里直接调用
db.Create()而不传 ctx;若必须异步,用context.WithCancel并在 defer 里调用 cancel,同时在 GORM 调用前后加select { case 主动退出 - 检查中间件是否重复 wrap ctx(如多次
WithTimeout),导致子 ctx 生命周期混乱
验证 GORM 日志和连接池配置是否放大问题
GORM 开启 logger.Default.LogMode(logger.Info) 后,每条 SQL 都会生成字符串拼接 + 格式化,高 QPS 下日志本身就能吃掉数百 MB 内存。连接池配置不当也会让空闲连接长期 hold 住资源。
- 生产环境关闭 GORM 日志,或至少设为
logger.Warn;自定义 logger 时避免在Trace回调里做fmt.Sprintf或json.Marshal - 检查
db.DB().SetMaxOpenConns(20)和SetMaxIdleConns(10)是否合理:过大导致空闲连接堆积(每个连接含 buffer、tls conn、parser 状态),过小则频繁建连耗 CPU+内存 - 确认
SetConnMaxLifetime和SetConnMaxIdleTime已设置(如 30m / 5m),防止老化连接残留引用 - 用
pprof抓取/debug/pprof/heap?debug=1,重点关注*gorm.db、*sql.Rows、bytes.Buffer、reflect.Value的 count 和 inuse_space
最常被忽略的一点:GORM 的 Scan 方法对结构体字段做反射赋值,如果结构体含大数组、嵌套 map 或未导出字段,反射开销和临时对象分配会指数级上升。上线前务必用真实数据压测 + pprof 对照,而不是只信本地小数据跑通。


















