更新时间:2026-07-13 GMT+08:00
分享

预期外的OOM场景

场景描述

OOM(Out of Memory)是指Linux系统物理内存被完全占用且无法通过内存回收机制释放足够空间,触发操作系统OOM killer机制终止占用内存最多的进程。当数据库实例因内存泄漏、大查询或批量操作导致内存陡升至100%时,OOM killer可能直接终止数据库主节点或只读节点进程,引发主备倒换(主节点被kill)或只读节点重启,导致业务短时中断或读请求失败。

典型表现包括内存使用率在数秒内从正常值(如60%)飙升至100%后骤降,造成服务中断或性能波动。

数据采集原因可能并未采集到内存100%的数据,所以内存监控显示内存峰值有可能未到100%,通常是接近100%。

本章节主要针对超大SQL、大量重复执行SQL这两类最常见的导致OOM场景进行分析。

表1 OOM问题场景分析

原因类别

核心问题描述

典型特征

超大SQL

单个或少数几个超大SQL突然占用大量内存,导致内存使用率超过100%,触发OOM。

  • SQL涉及大表(如千万级数据量)。
  • 多表JOIN、ORDER BY、GROUP BY操作。
  • IN子句字段数量过多(如IN (1,2,3,...,10000))。
  • 临时表或排序操作占用内存激增。

大量重复执行SQL

某类或少数几类SQL大量重复执行,单个SQL内存占用不高,但数量庞大导致内存使用率超过100%,触发OOM。

  • 同类SQL并发执行次数极高(如几十次、成百上千次)。
  • SQL本身简单但频繁调用(如高频查询或更新)。
  • 缺乏缓存或连接池复用,导致重复解析和执行。

原因分析

表2 内存高负载原因分析

原因类别

核心问题描述

影响

数据量增长

表数据和索引量增加,数据库需更多内存缓存数据页和索引页(如InnoDB Buffer Pool)。

  • 数据量和索引量持续增长。
  • 查询中间结果集、排序、哈希操作等临时内存占用增加。

连接数增加

每个连接消耗少量内存(如连接缓冲区、会话变量、临时表空间),长连接场景下资源累积。

  • 连接数增加导致内存消耗累积。
  • 长连接场景下,会话级缓存或未释放的临时资源(如临时表、排序缓冲区)持续占用内存。

缓存策略与命中率追求

数据库主动扩展缓存(如Buffer Pool)以提升缓存命中率,减少磁盘I/O。

  • Buffer Pool自动扩展,占用更多内存。
  • 查询计划缓存、存储过程缓存随使用次数增长而扩大。
  • 缓存命中率高但内存占用持续上升。

写入负载变化

大量写入导致日志缓冲区(如redo log、binlog cache)内存使用上升,批量操作临时占用内存。

  • 写入量增加,日志缓冲区(如redo log、binlog cache)内存占用上升。
  • 批量操作或大事务临时占用额外内存(如排序、哈希操作)。

超大SQL

超大SQL可能导致内存异常上升并引发OOM(内存溢出),常见于小规格实例。这类SQL通常包含复杂操作,如大结果集、大表排序、多表JOIN等,导致临时内存占用激增。

定位方法:在慢日志/审计日志/全量SQL中,定位内存上涨至OOM期间是否有异常SQL执行。如果未查询到异常SQL,请提交工单

  1. 检查日志:
    • 慢日志:可能未记录未完成的SQL,因为发生OOM时,该SQL还未执行完成(未执行完的SQL不会记录到慢日志),仅记录执行完成的SQL。
    • 审计日志/全量SQL:默认关闭,需手动开启(全量SQL审计日志)。
  2. 时间范围分析:定位内存上涨至OOM期间是否有异常SQL执行。

这类SQL主要包括以下几种情况:

  • 大结果集:查询返回大量数据,导致内存消耗急剧增加,尤其是在未使用LIMIT限制行数时。
  • 对大表进行 ORDER BY:在未建立合适索引的情况下,对大表进行排序操作会消耗大量内存,用于排序缓冲区。
  • 大表/多表JOIN:涉及多个大表的JOIN操作,尤其是全连接或复杂连接,会生成庞大的中间结果集,占用大量内存。
  • 大量 IN 字段:使用包含大量值的IN子句,会导致查询生成复杂的执行计划和临时表,增加内存开销。
  • 大批量插入、更新:执行大批量数据的插入或更新操作时,会临时占用大量内存,用于事务日志和缓冲区。
  • BLOB、TEXT 字段:当查询或更新操作涉及大量大字段(如 TEXT、BLOB)时,数据库需在内存中缓存更大的结果集,导致内存占用飙升。

大结果集

  • 问题描述

    执行SELECT * FROM t1 时,若表t1 数据量较大(如百万级或千万级),且未使用 LIMIT 限制返回行数,结果集会占用服务器会话缓冲区客户端结果缓存的大量内存。若网络传输或客户端处理速度较慢,内存无法及时释放,可能导致内存利用率持续上升,甚至触发 OOM(内存溢出) 风险。

  • 优化建议
    • 只查询必要字段,而不使用select *。
    • 加上limit使用分页查询,而不是一次性查询全量数据。

对大表进行 ORDER BY

  • 问题描述

    当查询无法通过索引直接返回有序结果时(如排序字段未建立索引),数据库会执行排序操作。排序过程会优先使用内存缓冲区(sort_buffer),其大小由参数 sort_buffer_size 控制。

    • 内存排序:若待排序数据量小于 sort_buffer_size,排序在内存中完成,效率较高。
    • 磁盘排序:若数据量超过 sort_buffer_size,数据库会将部分数据写入磁盘临时文件,排序效率显著下降。

    每个连接独立分配 sort_buffer:每个执行排序的连接都会分配独立的 sort_buffer,即使单个连接的排序内存需求不高,高并发场景下多个连接同时排序,仍可能导致内存占用激增,甚至触发OOM(内存溢出)。

  • 典型查询示例
    • 对大表(如上亿行)进行排序,且排序字段无索引,需全表扫描并排序。
      SELECT * FROM massive_table ORDER BY non_indexed_column DESC;
    • 复杂的分组聚合,需要先排序再分组。
      SELECT user_id, COUNT(*) FROM huge_log_table GROUP BY user_id;
    • 多表 JOIN 后对结果集进行排序,JOIN结果集大,且排序字段无索引,需对海量数据排序。
      SELECT a.*, b.name FROM big_table a
      JOIN another_table b ON a.id = b.a_id
      ORDER BY a.created_at
      LIMIT 1000 OFFSET 1000000;
  • 优化建议
    • 尽可能利用索引避免排序,例如为排序字段建立联合索引

      若查询为WHERE a = ? ORDER BY b,建立 (a, b) 联合索引,让 ORDER BY 直接利用索引顺序,避免排序。

    • 适度调小sort_buffer_size值,确保sort_buffer_size不超过1MB;对于大规格实例可适当增大,但需注意高并发下的内存叠加效应。

大表/多表JOIN

  • 问题描述

    当执行大表JOIN且连接条件无索引时,数据库会通过 join_buffer 缓存扫描的行数据以加速JOIN操作。

    若连接字段(如 non_indexed_key)无索引,MySQL需对表进行全表扫描,并将所有匹配行加载到 join_buffer 中。

    数据量越大,join_buffer占用内存越高,可能导致内存资源快速耗尽。

    join_buffer_size 是每个JOIN操作独立分配的。高并发场景下,多个连接同时执行JOIN操作,每个连接都会分配独立的 join_buffer,导致内存占用呈线性增长,可能触发 OOM(内存溢出)。

  • 典型查询示例
    • 场景1:连接条件无索引,需全表扫描并加载大量数据到 join_buffer。
      SELECT *
      FROM large_table_a A
      JOIN large_table_b B ON A.non_indexed_key = B.non_indexed_key;
    • 场景2:笛卡尔积(无ON条件),导致两表所有行组合,生成 M*N 的巨大结果集,内存占用极高。
      SELECT * FROM table1, table2;
  • 优化建议
    • 确保连接列有索引
      • 被驱动表的连接列必须有索引(如 t2.t1_id),这是JOIN操作性能优化的关键。
      • 为 ON 子句中使用的列创建索引

      SQL示例

      假设查询:SELECT * FROM t1 JOIN t2 ON t1.id = t2.t1_id

      需要为 t2.t1_id 创建索引
      CREATE INDEX idx_t1_id ON t2(t1_id);
    • 选择驱动表与被驱动表

      MySQL通常选择小表作为驱动表(可通过STRAIGHT_JOIN 强制指定),减少循环次数。大表作为被驱动表,通过索引加速匹配。

      如下SQL表示强制指定驱动顺序,指定 small_table 为驱动表,large_table 为被驱动表。

      SELECT* FROM small_table t1 STRAIGHT_JOIN large_table t2 ON t1.id = t2.ref_id;
    • 通过 EXPLAIN 验证优化效果
      • type字段:确认被驱动表是否使用索引(如 ref 或 range)。
      • rows字段:评估扫描行数是否因索引优化而减少。
      • Extra字段:检查是否出现 Using filesort 或 Using temporary(需进一步优化)。

大量 IN 字段

  • 问题描述
    • 小规模IN字段:当IN列表中的字段数量较少(如几十个以内),MySQL会使用临时数组存储匹配值,内存占用较低。
    • 大规模IN字段:当IN列表值数量较大(如数千个以上),可能超出range_optimizer_max_mem_size限制,导致优化器放弃 range 扫描优化,退化为全表扫描,间接引发内存飙升。此外,超大 IN 列表还会增加 SQL 解析和优化阶段的耗时。建议将 IN 列表值数量控制在 200 个以内,超过时使用分批查询或临时表替代。
  • 典型查询示例

    IN列表包含1000个ID。

    SELECT * FROM users WHERE id IN (1, 2, 3, ..., 1000);

    优化策略:分批查询 + 业务层合并

    将大规模IN列表拆分为多个小批次,降低单次查询的内存占用,避免哈希表过大。

    1. 分批查询:
      • 将1000个ID拆分为10批,每批100个。
      • 每次查询仅处理100个ID,减少单次哈希表内存占用。
      -- 第1批:100个ID
      SELECT * FROM users WHERE id IN (1, 2, ..., 100);
      -- 第2批:100个ID
      SELECT * FROM users WHERE id IN (1001, 1002, ..., 200);
      -- 第10批:100个ID
      SELECT * FROM users WHERE id IN (9001, 9002, ..., 1000);
    2. 业务层合并:

      在应用层(如Java、Python)合并10批查询结果,最终返回完整数据。

大批量插入、更新

  • 问题描述

    在大批量并发插入或更新操作中,尤其是在大事务场景下,InnoDB 引擎需要维护事务相关的锁信息、Buffer Pool 中的脏页,同时 redo log buffer 和 undo log 相关的内存资源在事务提交前不会完全释放。若操作涉及大量二级索引更新,Buffer Pool 的占用将显著增加,可能导致内存飙升,甚至触发 OOM。

  • 内存消耗原因
    • 事务资源占用:
      • undo log:用于事务回滚和MVCC(多版本并发控制),事务未提交前占用内存。
      • redo log:记录事务对数据页的修改,确保崩溃恢复。
    • 二级索引更新:

      每个索引字段的更新(如 UPDATE table SET col1 = value1, col2 = value2)均需维护对应索引树,内存消耗呈线性增长。

  • 优化建议
    • 将单次插入的 VALUES 数量控制在100 条以内,避免单次事务过大。
      分批操作后,及时提交事务(COMMIT),释放占用的undo/redo资源。
      -- 分批插入(每批100条)
      INSERT INTO users (id, name) VALUES  (1, 'Alice'), (2, 'Bob'), ..., (100, 'Zoe');
    • 使用 LIMIT N 控制单次更新的行数(建议 N ≤ 100),避免大事务。

      避免在频繁更新的字段上创建非必要索引,减少二级索引更新开销。

      -- 分批更新(每次更新100行)
      UPDATE large_table 
      SET status = 'active' 
      WHERE condition = true 
      LIMIT 100;

BLOB、TEXT 字段

  • 问题描述

    当查询或更新操作涉及大量大字段(如 TEXT、BLOB)时,数据库需在内存中缓存更大的结果集,导致内存占用飙升。具体场景包括:

    • 结果集缓存:查询大字段时(如 SELECT big_text_col FROM logs),需将整个字段加载到内存中。
    • 排序操作:对大字段排序时,sort_buffer 中仅存储前 max_sort_length 字节 + rowid,不会加载完整字段内容;但结果集传输阶段仍需将完整字段读入内存。
    • 去重操作:SELECT DISTINCT big_text_col 需在内存中维护临时哈希表或排序结构。
    • 更新操作:批量更新大字段(如 UPDATE logs SET big_text_col = REPEAT('a', 10*1024*1024))会生成大量临时数据,占用内存。
  • 典型示例
    • 场景1:查询大字段,结果集占用大量内存
      SELECT big_text_col FROM logs;  
    • 场景2:排序大字段,内存消耗激增
      SELECT * FROM logs ORDER BY big_text_col;  
    • 场景3:去重大字段,需维护临时结构
      SELECT DISTINCT big_text_col FROM logs;  
    • 场景4:批量更新大字段,生成临时数据
      UPDATE logs SET big_text_col = REPEAT('a', 10*1024*1024);
  • 优化建议
    • 限制查询数据量
      • 分页查询:通过 LIMIT 控制单次返回的行数,避免一次性加载过多数据。
      • 避免全表扫描:若仅需部分字段,避免使用 SELECT *,仅查询必要字段。
    • 优化排序与去重
      • 减少排序字段大小:若排序字段为大字段,可提取关键部分(如前缀)进行排序。
      • 使用索引:为排序字段创建索引,避免内存排序。
    • 控制更新操作
      • 分批更新:将大字段更新拆分为小批次,避免单次事务过大。
      • 避免冗余更新:仅更新实际需要修改的行,减少不必要的数据处理

大量重复执行SQL

  • 典型场景

    业务变更(如新增SQL上线)后,某类SQL被高频重复执行,单个SQL内存占用不高,但并发量或执行次数激增,导致内存持续累积,最终触发OOM。

  • 定位问题SQL
    1. 时间范围定位:通过分析慢日志/审计日志/全量SQL,确定内存开始上涨至OOM发生的时间段。
    2. 日志分析:
      • 慢日志:检查slow_query_log中是否有高频重复SQL(如SELECT * FROM table WHERE id IN (...))。
      • 审计日志/全量SQL日志:通过SHOW ENGINE INNODB STATUS或performance_schema分析SQL执行频率。
    3. 工具辅助:通过EXPLAIN分析SQL执行计划,确认是否涉及全表扫描、临时表或排序操作。
  • 解决方案
    • 优化SQL逻辑:
      • 避免重复查询(如引入缓存机制)。
      • 拆分复杂SQL(如分页查询、减少IN子句字段数)。
      • 增加索引(如对WHERE、JOIN字段建立索引)。
    • 紧急限流:

      通过SQL限流限制高频SQL的并发数。

相关文档