以下图表展示了组成 InnoDB 存储引擎架构的内存结构和磁盘结构。
内存结构
1. 缓冲池 (Buffer Pool)
缓冲池是主内存中的一块区域,用于缓存 InnoDB 在访问过程中使用的表和索引数据。缓冲池允许频繁使用的数据直接从内存中访问,从而加快处理速度。在专用服务器上,通常将多达 80% 的物理内存分配给缓冲池。
高效的大量读取操作
为提高大量读取操作的效率,缓冲池被划分为页面(pages),每个页面可以容纳多行数据。为高效地管理缓存,缓冲池通过一个页面的链表实现;不常使用的数据会使用一种改进的最近最少使用(LRU)算法被逐步移出缓存。
合理利用缓冲池,将频繁访问的数据保存在内存中,是 MySQL 性能调优的重要环节。
1.1 缓冲池 LRU 算法
缓冲池使用一种变体的 LRU 算法作为链表管理策略。当需要为新的页面腾出空间时,最少最近使用的页面将被移出,并且新页面会插入到链表的中间位置。此“中点插入”策略将链表分为两个子列表:
-
新页面子列表,(年轻页面)位于链表的头部,包含最近访问的页面;
-
旧页面子列表,位于链表的尾部,包含最近较少访问的页面。
算法特点
• 算法保持频繁使用的页面在新子列表中。
• 旧子列表中的页面较少使用,因此更可能被逐步移出。
默认配置:
-
缓冲池的 3/8 被分配给旧子列表;
-
链表的中点是新子列表的尾部与旧子列表的头部之间的边界;
-
当 InnoDB 将一个页面读取到缓冲池时,该页面最初被插入到中点(即旧子列表的头部)。页面的读取可能由用户操作(如 SQL 查询)触发,也可能由 InnoDB 的预读操作自动完成。
页面访问策略
-
访问旧子列表中的页面会将该页面变为“年轻页面”,并移动到新子列表的头部。如果页面因用户操作被访问,则其首次访问会立即发生,页面会变年轻。如果页面因预读操作而被访问,则可能不会立即被访问,甚至可能在被移出前未被访问过。
-
随着数据库的运行,缓冲池中未被访问的页面会“变老”,逐渐向列表的尾部移动。新子列表和旧子列表中的页面都会因其他页面变年轻而变老。旧子列表中的页面还会因为新的页面插入到中间位置而变老。最终,未被使用的页面会到达旧子列表的尾部,并被驱逐出缓冲池。
默认情况下,通过查询读取的页面会立即移动到新子列表中,这意味着它们在缓冲池中停留的时间更长。例如,执行表扫描(如 mysqldump 操作或没有 WHERE 子句的 SELECT 语句)可能会将大量数据引入缓冲池,并驱逐出相应数量的旧数据,即使这些新数据可能再也不会被使用。类似地,由预读后台线程加载的页面,即使仅被访问一次,也会被移动到新列表的头部。这些情况可能会将频繁使用的页面推到旧子列表,从而使它们更容易被驱逐。
InnoDB 标准监控输出在 BUFFER POOL AND MEMORY 部分包含与缓冲池 LRU 算法运行相关的多个字段。有关详细信息,请参阅“使用 InnoDB 标准监控监视缓冲池”。
1.2 缓冲池配置
您可以配置缓冲池的各个方面,以提高性能。
理想情况下,您应该将缓冲池的大小设置为尽可能大的值,同时为服务器上的其他进程保留足够的内存,以避免过度的页面交换。缓冲池越大,InnoDB 就越像一个内存数据库,它会一次从磁盘读取数据,并在后续读取时直接从内存访问数据。
在具有足够内存的 64 位系统上,您可以将缓冲池拆分为多个部分,以最小化并发操作之间对内存结构的争用。
您可以确保频繁访问的数据始终保存在内存中,无论是由于操作的突然活动导致大量不常访问的数据被引入缓冲池。
您可以控制如何以及何时执行预读请求,将页面异步地预取到缓冲池中,以预期它们即将被需要。
您可以控制后台刷新何时发生,以及是否根据工作负载动态调整刷新速率。
您可以配置 InnoDB 如何保存当前的缓冲池状态,以避免服务器重启后的长时间预热期。
2. 写缓冲 (Change Buffer)
写缓冲是一种特殊的数据结构,用于缓存对二级索引页的变更操作(如 INSERT、UPDATE 或 DELETE 操作产生的 DML 变更),当这些页面不在缓冲池中时会存储到变更缓冲区中。
这些缓冲的变更会在页面因其他读操作被加载到缓冲池时,进行后续的合并处理。
与聚簇索引不同,二级索引通常是非唯一的,因此对二级索引的插入操作通常以相对随机的顺序进行。同样,删除和更新操作可能会影响二级索引树中位置不相邻的页面。通过延迟合并缓存的变更(即在受影响的页面因其他操作被加载到缓冲池时再进行合并),可以避免频繁的随机 I/O 操作,这些操作本来需要将二级索引页面从磁盘读入缓冲池。
系统在大部分时间空闲时运行的清理操作,或在慢速关闭期间运行的清理操作,会定期将更新的索引页面写入磁盘。相比每次索引值更新后立即写入磁盘,清理操作可以更高效地将一系列索引值对应的磁盘块写入磁盘。
当涉及大量受影响的行以及多个需要更新的二级索引时,写缓冲的合并可能需要数小时。在此期间,磁盘 I/O 增加,可能会显著减慢以磁盘为瓶颈的查询。写缓冲的合并可能在事务提交后仍继续进行,甚至在服务器关闭并重启后仍会继续。
在内存中,写缓冲占用了缓冲池的一部分;在磁盘上,写缓冲是系统表空间的一部分,在数据库服务器关闭时用于缓冲索引变更。
如果二级索引包含降序索引列,或者主键包含降序索引列,则该二级索引不支持变更缓冲。
2.1 配置写缓存(Configuring Write Buffering)
当对表执行 INSERT、UPDATE 和 DELETE 操作时,索引列(尤其是二级索引键的值)通常是无序的,因此需要大量 I/O 操作来更新二级索引。写缓存在相关页面不在缓冲池中时缓存对二级索引条目的更改,从而避免通过磁盘立即读取页面所需的高成本 I/O 操作。这些缓存的更改会在页面加载到缓冲池时被合并,而更新后的页面稍后会被刷新到磁盘。
InnoDB 主线程会在服务器几乎空闲时或在慢速关闭期间合并缓存的更改。
由于写缓存可以减少磁盘读写操作,它对于 I/O 受限的工作负载最为有用。例如,处理大量 DML 操作(如批量插入)的应用程序可以显著受益于写缓存。
然而,写缓存会占用缓冲池的一部分,从而减少可用于缓存数据页的内存。如果工作集几乎能够完全放入缓冲池,或者表中二级索引较少,那么禁用写缓存可能更为有利。如果工作数据集完全适合缓冲池,写缓存不会带来额外的开销,因为它仅适用于不在缓冲池中的页面。
innodb_change_buffering 变量
innodb_change_buffering 变量控制 InnoDB 执行写缓存的程度。你可以启用或禁用以下缓冲操作:
• 插入操作(INSERT)
• 删除操作(在索引记录初次标记为删除时)
• 清理操作(在后台物理删除索引记录时)
更新操作是插入和删除操作的组合。默认情况下,innodb_change_buffering 的值为 all。
允许的 innodb_change_buffering 值
3. 自适应哈希索引(Adaptive Hash Index)
自适应哈希索引使 InnoDB 在具备合适工作负载和足够缓冲池内存的系统上,能够像内存数据库一样高效运行,同时保持事务特性和可靠性。自适应哈希索引由变量 innodb_adaptive_hash_index 启用,或者在服务器启动时通过 --skip-innodb-adaptive-hash-index 参数禁用。
4. 日志缓冲 (Log Buffer)
日志缓冲区是一个内存区域,用于保存即将写入磁盘日志文件的数据。日志缓冲区的大小由变量 innodb_log_buffer_size 定义,默认大小为 16MB。日志缓冲区的内容会定期刷新到磁盘。较大的日志缓冲区允许较大的事务运行,而无需在事务提交前将重做日志数据写入磁盘。因此,如果您的事务涉及大量的更新、插入或删除操作,增大日志缓冲区的大小可以减少磁盘 I/O。
变量 innodb_flush_log_at_trx_commit 控制日志缓冲区内容的写入和刷新到磁盘的方式,而变量 innodb_flush_log_at_timeout 则控制日志刷新的频率。