数据同步过程中出现 OOM 内存溢出,应该如何排查和解决?
- 标签:后端、场景题
🏛 业务架构师
数据同步任务 OOM,我按应急止血 → 根因定位 → 结构性修复三步答,重点是**"加内存"永远只是拖延**——OOM 的根因通常是设计把无界的数据装进了有限的内存。
第一步:止血
- 任务重启 + 限速(降低批大小/并发),先把同步跑起来减少积压
- 确认影响:同步延迟多少、下游业务是否被卡(该告警的告警,别闷头修)
第二步:定位根因(OOM 的三种典型成因)
- 全量加载:
SELECT百万行一次性进 List,或 OFFSET 深翻页全量 hold 在内存——最常见 - 无界队列/缓冲:生产快消费慢,内存队列无限堆积(MQ 消费积压转本地缓冲的场景)
- 内存泄漏:连接/游标未关、缓存只增不减、大对象引用被持有
定位工具:heap dump(-XX:+HeapDumpOnOutOfMemoryError)+ MAT 分析 dominator tree,一眼看到谁占了内存;配合 GC 日志看是"逐步涨"(泄漏/堆积)还是"瞬间涨"(单批太大)。
第三步:结构性修复
- 流式化:游标分批(
WHERE id > #{lastId} LIMIT 5000)、JDBC 流式读(fetchSize=Integer.MIN_VALUE或游标)、MyBatis 流式查询——永不一次装全量 - 背压:队列设上限,满了阻塞生产者或落盘,宁可慢不可炸
- 批大小与并发参数化:批 5000 条、并发 4 起步,压测调优——参数必须可配置,数据量涨了能调
- 超大表走分片并行(按主键区间切片,每片独立跑完释放),而不是单进程硬扛
- 监控:任务级内存水位 + 批次耗时告警,OOM 前发现异常
收尾:我 review 同步类代码的第一原则是**"任何时刻,这个进程内存里最多只有 N 批数据"**——写代码时能回答这个问题,OOM 就不会发生;回答不了,OOM 只是时间问题。
🏗 系统架构师
同步链路 OOM,我按定位(内存去哪了)→ 根因分类 → 解决的流程讲。
第一步:定位证据链
- 拿到 OOM 时的 heap dump(启动参数配
-XX:+HeapDumpOnOutOfMemoryError),MAT/JProfiler 看支配树:通常是某一个集合(List/Map)持有几 GB 对象,指向具体代码 - 看堆 dump 前的监控曲线:缓慢上涨(泄漏)还是瞬时尖峰(单批过大)
第二步:高频根因(按我遇到的概率):
- 一次性加载过多数据:
SELECT *无分页把百万行装进 List;或分页了但LIMIT 100000太大。解决:游标分页 + 小批次(1000~5000 行/批)+ 流式读(MyBatis 流式游标Cursor、JDBCfetchSize=Integer.MIN_VALUE或 useCursorFetch),边读边处理边丢弃 - 消息批量聚合失控:为了批量写数仓,攒批逻辑按"条数或大小"触发,但只写了条数没写大小;或某个 key 的数据倾斜,一个分组攒百万条。解决:批次上限双阈值(条数 + 字节数),倾斜 key 二次打散
- Flink 状态爆炸:大 keyby 的 keyed state 无 TTL 无限增长。解决:状态 TTL、 RocksDB state backend(堆外)、大 key 预分裂
- 队列无界:生产快消费慢,中间
LinkedBlockingQueue无界堆积。解决:有界队列 + 反压(满了阻塞生产者) - 引用未释放:同步任务的缓存 Map 只 put 不 evict。解决:Caffeine 带 maximumWeight + expire
第三步:解决原则
- 内存预算制:同步任务给 JVM 划定堆上限,任何攒批逻辑按"最坏批次大小 × 并发"倒推上限
- 压测验证:目标峰值流量 × 3 跑 24 小时内存不涨才算过
- 兜底:容器内存 limit + 合理的 OOM 自动重启与告警(K8s liveness),但要配"OOM 前先告警"(80% 水位)争取处理时间
一句话:先 dump 定位大对象,九成是"一次捞太多/攒太大/无界队列",治法是流式化 + 双阈值分批 + 反压。
💻 全栈开发专家
同步任务 OOM,我按先定位、再结构性解决的思路处理,经验里 90% 是"想一次性把数据装进内存"。
排查三步
- 区分两种 OOM:K8s 里先看是 JVM 堆 OOM(日志
java.lang.OutOfMemoryError)还是容器 OOMKilled(kubectl describe pod看 exit code 137)——后者是堆外/原生内存超了容器 limit,调小-Xmx留出堆外余量反而能解。 - 拿证据:启动参数加
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/,dump 用 MAT 打开看 Dominator Tree,通常一眼看到某个ArrayList装了几千万行记录,或某个 Kafka Consumer 缓冲积压。 - 看趋势:SkyWalking/JVM 监控看堆是缓慢爬升(泄漏)还是瞬时打满(大批量加载),处理方向不同。
结构性解决(治本)
- 读改流式:JDBC 流式读 MySQL,避免全量 load:
java
stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
stmt.setFetchSize(Integer.MIN_VALUE); // MySQL 流式游标
// MyBatis 用 Cursor<Order> cursor = mapper.streamAll(); 逐条处理- 写改分批:每 500~1000 条一批提交事务并更新位点,内存里永远只有一批。
- 消息侧:消费端限制
max.poll.records,处理不过来让消费者暂停拉取,而不是让 MQ 客户端缓冲区无限堆积。 - 攒批别攒对象:聚合用计数/字节数(如攒 64KB 就 flush),不要按"攒够 100 万条"这种行数维度。
验证
改完用大表压测(如 5000 万行全量同步)观察堆曲线平稳、GC 频率正常,并在预发模拟一次 OOM 复现路径确认修复有效。