从生产数据库内存打满学习InnoDB(使用deepSeek学习)

163 阅读7分钟

1.背景

数据库配置:8C32G(阿里云polarDB)

系统配置:4C8G12个实例

使用场景:调度任务数据表存在大量更新删除操作

2.问题

2月5号晚上数据库的内存达到96%出现告警,DBA拉齐开发做扩容。

联系阿里云给出原因如下: 整体的buffer pool占用如下:

Buffer Pool Size: 1703936 页(每页大小通常为 16KB,因此总大小约为 26.6GB)。

Free Buffers: 16181 页(约 252MB),表示当前空闲的缓冲区页面数。

Database Pages: 1685791 页(约 26.3GB),表示当前被使用的数据库页面数。

Old Database Pages: 622128 页(约 9.7GB),表示旧页(Old Sublist)的数量。

Modified DB Pages: 56066 页(约 876MB),表示已被修改但尚未写入磁盘的页面数。

分析:

Buffer Pool 使用率:Database Pages / Buffer Pool Size = 1685791 / 1703936 ≈ 99%,表明 Buffer Pool 几乎被完全占用,空闲缓冲区非常少(仅 16181 页)。

Old Pages 占比:Old Database Pages / Database Pages = 622128 / 1685791 ≈ 36.9%,表明旧页占据了较大比例。

Modified Pages 占比:Modified DB Pages / Database Pages = 56066 / 1685791 ≈ 3.3%,表明脏页比例较低。

3.原因

当前 Buffer Pool 的空闲空间freeList较少,而旧页占据了较大的比例。

性能瓶颈:当新数据需要加载到 Buffer Pool 时,可能需要频繁淘汰旧页,增加 I/O 开销。

资源浪费:旧页占据较多空间,但可能并未被频繁访问,导致缓存效率降低。

解决方案: 为了减少旧页空间并增大空闲页空间,可以从以下几个方面进行调整:

  1. 调整 innodb_old_blocks_pct 参数 作用:该参数控制旧页子列表(Old Sublist)占 Buffer Pool 的比例,默认值为 37%。 建议调整:将 innodb_old_blocks_pct 降低到 20%-30%,以减少旧页占用的空间,从而释放更多空间给新页和空闲页。 SET GLOBAL innodb_old_blocks_pct = 25; 注意事项:调整后需观察性能变化,确保不会因旧页过少而导致频繁的页淘汰。

  2. 增加 Buffer Pool 大小 作用:如果服务器内存充足,可以增加 Buffer Pool 的大小,从而提供更多空闲空间。 建议调整:通过修改 innodb_buffer_pool_size 参数来增加 Buffer Pool 的容量。例如,将其从 26.6GB 增加到 32GB。 SET GLOBAL innodb_buffer_pool_size = 34359738368; -- 32GB 注意事项:增加 Buffer Pool 大小时需确保服务器有足够的物理内存,避免因内存不足导致系统性能下降。

  3. 监控和优化查询 作用:某些低效查询可能导致大量数据加载到 Buffer Pool 中,进而占用过多空间。 建议调整: 使用慢查询日志或性能监控工具(如 EXPLAIN、Performance Schema)分析查询性能。 优化索引设计,减少不必要的全表扫描。 调整查询逻辑,避免一次性加载过多数据。 (以上解决方案来自DeepSeek)

最终决策:扩容buffer pool:8C64G

4.理解InnoDB

image.png

4.1.内存缓冲池Buffer pool

Buffer Pool 是数据库管理系统的核心组件,用于缓存磁盘数据页以减少 I/O 操作

内存划分结构

image.png

内存管理流程

image.png

1. 初始状态:页加载
  • 从 Free List 分配空闲页
    当需要加载磁盘数据页到 Buffer Pool 时:

    1. 检查 Free List 是否有空闲页。

    2. 若有空闲页,直接取出分配给新页,并插入 LRU List 的 Old 区(冷数据区)。

    3. 若 Free List 为空,触发 页面替换 机制(见下文)。

2. 页面访问与热度管理(LRU List)
  • 访问页时的状态变化

    • 当页首次被访问时,可能停留在 LRU Old 区。

    • 若短时间内被多次访问(例如,通过 innodb_old_blocks_time 阈值控制),则将其移动到 LRU Young 区(热数据区)

    • 每次访问会调整页在 LRU List 中的位置,避免冷数据占据内存。

  • LRU 链表的冷热分区

    • Young 区:存放高频访问的热数据页,替换优先级最低。

    • Old 区:存放低频访问的冷数据页,替换时优先淘汰。

3. 页面替换(Free List 耗尽时)

当 Free List 无空闲页,需通过 LRU List 淘汰冷页以腾出空间:

步骤 1:选择淘汰页
  • 从 LRU Old 区尾部 选择最近最少使用的页(冷页)作为候选。

  • 若页的 pin count > 0(被事务占用),跳过并选择下一个页。

步骤 2:处理脏页
  • 若淘汰页是脏页(修改未刷盘):

    1. 将其从 LRU List 移除,并加入 Flush List(若尚未加入)。

    2. 触发 异步刷盘(Async Flush):后台线程将脏页写入磁盘。

    3. 刷盘完成后,该页变为干净页,移出 Flush List,并释放到 Free List 中。

步骤 3:复用空闲页
  • 空闲页(来自 Free List)被分配给新加载的磁盘页,插入 LRU Old 区。
4. 脏页刷盘(Flush List 管理)
  • Flush List 的作用
    记录所有待刷盘的脏页,按修改时间(LSN)排序,确保崩溃恢复时数据一致性。

  • 刷盘触发条件

    • Checkpoint 机制:定期(如每秒)或日志空间不足时触发批量刷盘。

    • LRU 淘汰脏页:淘汰脏页时强制刷盘(同步或异步,取决于策略)。

    • 用户主动刷盘:如执行 FLUSH TABLES 命令。

(以上流程来自DeepSeek)

4.2.线程模型

InnoDB是多线程的模型,整个引擎后台有多个不同的后台线程负责处理不同的任务,后台线程梳理如下:

Master Thread

  • 核心后台线程

    • 负责脏页刷新
    • checkPoint刷新
    • Undo Log 回收
    • Redo Log刷新
    • 合并 Change Buffer(非主键索引会使用changeBuffer,该区域的作用是对普通索引的插入和更新起到加速的作用,将更新或者插入非主键索引的操作在先写入change Buffer缓冲区,后续查询可以直接操作该缓冲区的数据。该作用后期由Page Cleaner Thread接管,但Master Thread 仍然可能会在某些情况下参与插入缓冲的管理,例如在系统启动初期或者负载较低时,Master Thread 可能会直接处理一些插入缓冲的合并操作。)
  • 工作周期:每 1 秒或 10 秒执行特定任务(如刷盘) InnoDB会判断缓冲池中脏页的比例(buf_get_modified_ratio_pct)如果超过70%则刷新100个脏页到磁盘,如果脏页比例小于70%,则只需要刷新10%的脏页到磁盘。

IO Thread

InnoDB中大量使用AIO(Async IO)来处理IO请求,这样可以加大提高数据库的性能。IO Thread的工作主要负责这些IO请求的回调处理。IO线程又分为多种线程,如下:

  • 读线程(Read Thread):处理数据页的异步读请求。

  • 写线程(Write Thread):处理脏页的异步写回。

  • 插入缓冲线程(Insert Buffer Thread):处理插入缓冲的写入

  • 日志线程(Log Thread):处理Redo Log的异步写回Redo Log File

Purge Thread

  • 职责:清理已提交事务不再需要的 Undo Log。

Page Cleaner Thread

Page Cleaner Thread是在InnoDB1.2.X版本引入的。

  • 职责
    • 脏页刷新:将之前版本中脏页的刷新操作都放到单独的线程完成,减轻Master Thread的工作,提高InnoDB的性能。
    • 合并插入缓冲:处理插入缓冲区中的数据merge到非聚集索引中