Skip to content

数据同步过程中出现 OOM 内存溢出,应该如何排查和解决? ​

  • 标签:后端、场景题

🏛 业务架构师 ​

数据同步任务 OOM,我按应急止血 → 根因定位 → 结构性修复三步答,重点是**"加内存"永远只是拖延**——OOM 的根因通常是设计把无界的数据装进了有限的内存。

第一步:止血

  • 任务重启 + 限速(降低批大小/并发),先把同步跑起来减少积压
  • 确认影响:同步延迟多少、下游业务是否被卡(该告警的告警,别闷头修)

第二步:定位根因(OOM 的三种典型成因)

  1. 全量加载:SELECT 百万行一次性进 List,或 OFFSET 深翻页全量 hold 在内存——最常见
  2. 无界队列/缓冲:生产快消费慢,内存队列无限堆积(MQ 消费积压转本地缓冲的场景)
  3. 内存泄漏:连接/游标未关、缓存只增不减、大对象引用被持有

定位工具: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 前的监控曲线:缓慢上涨(泄漏)还是瞬时尖峰(单批过大)

第二步:高频根因(按我遇到的概率):

  1. 一次性加载过多数据:SELECT * 无分页把百万行装进 List;或分页了但 LIMIT 100000 太大。解决:游标分页 + 小批次(1000~5000 行/批)+ 流式读(MyBatis 流式游标 Cursor、JDBC fetchSize=Integer.MIN_VALUE 或 useCursorFetch),边读边处理边丢弃
  2. 消息批量聚合失控:为了批量写数仓,攒批逻辑按"条数或大小"触发,但只写了条数没写大小;或某个 key 的数据倾斜,一个分组攒百万条。解决:批次上限双阈值(条数 + 字节数),倾斜 key 二次打散
  3. Flink 状态爆炸:大 keyby 的 keyed state 无 TTL 无限增长。解决:状态 TTL、 RocksDB state backend(堆外)、大 key 预分裂
  4. 队列无界:生产快消费慢,中间 LinkedBlockingQueue 无界堆积。解决:有界队列 + 反压(满了阻塞生产者)
  5. 引用未释放:同步任务的缓存 Map 只 put 不 evict。解决:Caffeine 带 maximumWeight + expire

第三步:解决原则

  • 内存预算制:同步任务给 JVM 划定堆上限,任何攒批逻辑按"最坏批次大小 × 并发"倒推上限
  • 压测验证:目标峰值流量 × 3 跑 24 小时内存不涨才算过
  • 兜底:容器内存 limit + 合理的 OOM 自动重启与告警(K8s liveness),但要配"OOM 前先告警"(80% 水位)争取处理时间

一句话:先 dump 定位大对象,九成是"一次捞太多/攒太大/无界队列",治法是流式化 + 双阈值分批 + 反压。

💻 全栈开发专家 ​

同步任务 OOM,我按先定位、再结构性解决的思路处理,经验里 90% 是"想一次性把数据装进内存"。

排查三步

  1. 区分两种 OOM:K8s 里先看是 JVM 堆 OOM(日志 java.lang.OutOfMemoryError)还是容器 OOMKilled(kubectl describe pod 看 exit code 137)——后者是堆外/原生内存超了容器 limit,调小 -Xmx 留出堆外余量反而能解。
  2. 拿证据:启动参数加 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/,dump 用 MAT 打开看 Dominator Tree,通常一眼看到某个 ArrayList 装了几千万行记录,或某个 Kafka Consumer 缓冲积压。
  3. 看趋势: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 复现路径确认修复有效。