千金良方:MySQL性能优化金字塔法则
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

3.5 InnoDB存储引擎体系结构

InnoDB存储引擎体系结构如图3-3所示(该图来自Percona Database Performance Blog,参考链接:https://www.percona.com/blog/2010/04/26/xtradb-innodb-internals-in-drawing/)。

图3-3

结合图3-3,我们可以看到,InnoDB存储引擎体系结构主要包含如下组件,分为两大部分。

1.内存结构

● Buffer Pool:缓冲池是InnoDB在启动时分配的一个内存区域,用于InnoDB在访问数据时缓存表和索引数据。利用缓冲池,可以合并一些对经常访问的数据的操作,直接从内存中处理,加快了处理速度。通常,在专用数据库服务器上,可以将80%的物理内存分配给InnoDB缓冲池。为了提高缓存管理的效率,使用页面链表的方式+LRU(最近最少使用)算法进行管理。

● Change Buffer(Insert buffer part of buffer pool):这是一种特殊的数据结构(早期只支持INSERT操作的缓冲,所以也叫作Insert Buffer),当受影响的页面不在缓冲池中时,将会缓存对辅助索引页的更改。这些更改可能是由INSERT、UPDATE、DELETE(DML)语句执行时所导致的,当其他读取操作从磁盘中加载数据页时,如果这些数据页包含Change Buffer中缓存的更改操作页,那么将进行合并操作。

● Adaptive Hash Index:自适应哈希索引(AHI),用于管理缓冲池中的内部数据结构,并对缓冲池中的相关工作负载和内存操作组合进行自动调节,且不会牺牲任何事务功能、性能和可靠性。

● Log Buffer(Redo Log Buffer):重做日志缓冲区是用于保存将要写入重做日志磁盘文件中的数据的内存缓冲区域。重做日志缓冲区的大小由innodb_log_buffer_size配置参数定义。重做日志缓冲区中的内容会定期刷新到磁盘上的日志文件中。更大的重做日志缓冲区允许运行更大的事务,这在一定程度上避免提交大事务之前需要将重做日志写入磁盘中。因此,如果在应用场景中经常有大事务,则可以考虑增大重做日志缓冲区以减少磁盘I/O操作。innodb_flush_log_at_trx_commit参数控制如何将重做日志缓冲区的内容写入日志文件中(例如,设置为1时,每个事务提交时都需要执行一次将重做日志缓冲区的内容写入日志文件中)。innodb_flush_log_at_timeout参数控制重做日志的刷新频率。

2.磁盘结构

● System Tablespace:InnoDB系统表空间包含InnoDB数据字典(InnoDB相关对象的元数据)、Doublewrite Buffer、Change Buffer磁盘部分和Undo Logs,还包含在系统表空间中创建的任何表和索引数据。它之所以被称为系统表空间,是因为它可以被多个用户表共享。系统表空间可以由一个或多个数据文件构成。在默认情况下,只会创建一个名为ibdata1的共享表空间文件。但可以使用innodb_data_file_path启动选项控制共享表空间的数量和大小。

● Data Dictionary(InnoDB Data Dictionary):InnoDB数据字典由内部系统表组成,这些系统表包含用于跟踪对象(如表、索引和表列)的元数据。元数据存放在InnoDB系统表空间中。由于历史原因,数据字典元数据在一定程度上与存储在InnoDB表的.frm文件中的信息重叠。

● Doublewrite Buffer:双写缓冲区是一个位于系统表空间的存储区域,InnoDB在进行刷脏操作时,在将脏数据写入数据文件中的正确位置之前先把脏页从InnoDB缓冲池写入双写缓冲区中。只有将脏页成功写入并落盘到ibdata1共享表空间中的双写缓冲区之后,InnoDB才能将脏页从缓冲池中写入数据文件中的正确位置。如果操作系统、存储子系统或mysqld进程在刷新脏页过程中发生崩溃,那么可能发生部分写(InnoDB默认的页大小为16KB,而文件系统默认的块大小为4KB,如果InnoDB的一个页在写入磁盘过程中发生异常,则可能导致数据页只写入了一部分到磁盘中),InnoDB在重新启动时的崩溃恢复期间从双写缓冲区中找到正确的页面副本进行覆盖恢复。虽然双写会导致脏页被两次写入磁盘中,但双写缓冲区不需要两倍的I/O开销或两倍的I/O操作。因为脏页在双写时是以一次1MB,作为一个大的顺序块被写入双写缓冲区中,并执行一次fsync()调用的。另外,如果系统表空间文件(ibdata文件)被存放在支持原子写入的Fusion-io设备上,则会自动禁用双写缓冲区功能,并将Fusion-io原子写入功能用于所有的数据文件。注意:由于双写缓冲区设置参数(innodb_doublewrite)是全局的,因此对于存放在非Fusion-io设备上的数据文件,也会禁用双写缓冲区功能(对于这部分数据文件可能造成部分写)。所以,原子写功能仅在Fusion-io设备上且在Linux中启用了Fusion-io NVMFS时生效。要充分使用该功能,建议将innodb_flush_method参数设置为O_DIRECT。

● Undo Logs:用于存放事务修改之前的旧数据(undo log记录了有关如何撤销事务对聚集索引记录的最新更改的信息),基于undo实现了MVCC和一致性非锁定读。InnoDB总共支持128个回滚段,每个回滚段有1023个事务槽位,在并行事务场景中一个事务槽位对应一个事务。其中32个回滚段位于临时表空间(Temporary Tablespace),也就是说,对临时表操作的最大并行事务数大约为32×1023个;96个回滚段位于非临时表空间(系统表空间至少一个,因为MySQL 5.7新增的在线undo truncate功能需要,Undo Tablespace最多95个),也就是说,对非临时表操作的最大并行事务数大约为96×1023个。

● File-Per-Table Tablespaces:设置参数innodb_file_per_table=1启用独立表空间时,每个表都会对应生成一个.ibd文件用于存放自己的索引和数据等;否则,在创建表时数据和索引将被存放在ibdata系统表空间文件中(系统表空间)。

● General Tablespaces:常规表空间,在datadir路径下使用CREATE TABLESPACE语法创建的InnoDB共享表空间(tablespace_name.ibd)。可以在MySQL datadir之外创建,能够保存多个表,并支持所有行格式的表。使用CREATE TABLE tbl_name ... TABLESPACE [=] tablespace_name或ALTER TABLE tbl_name TABLESPACE [=] tablespace_name语法将表添加到常规表空间中,该功能是MySQL 5.7新增的。

● Undo Tablespace:undo表空间,包含一个或多个undo log文件,文件个数由配置参数innodb_undo_tablespaces控制。关于回滚段的分配详见“Undo Logs”解释。

● Temporary Tablespace:临时表空间用于存放非压缩的InnoDB临时表和相关对象。配置参数innodb_temp_data_file_path为临时表空间的数据文件定义了相对路径和初始大小等。如果未设置innodb_temp_data_file_path参数,则会在数据目录中创建一个名为ibtmp1的自动扩展的12MB初始大小的文件。临时表空间文件在服务器每次重启时都会重新创建(正常停止或终止初始化时会自动删除,但发生崩溃时不会自动删除),并使用动态生成的空间标识ID来避免与现有空间标识ID冲突。另外,在INFORMATION_SCHEMA.INNODB_TEMP_TABLE_INFO表中可以查看有关InnoDB临时表的元数据(可以看到InnoDB实例中处于活动状态的所有用户和系统创建的临时表)。注意:如果无法创建临时表空间,则Server将启动失败。该功能是MySQL 5.7新增的。

● Redo Logs:重做日志是在崩溃恢复期间使用的基于磁盘的数据结构文件,用于恢复不完整提交事务写入的数据。在MySQL实例正常运行期间,重做日志对事务产生的数据变更部分进行编码并持久化到磁盘中(重做日志中的数据就是对受影响的行记录进行编码,利用这些编码数据把事务进行前滚的操作就叫作重做)。在默认情况下,重做日志在磁盘中创建一组名为ib_logfile0和ib_logfile1的文件。MySQL以循环滚动方式写入重做日志文件,并使用一个不断增加的LSN值表示重做日志的写入量,以及标记写入重做日志文件中的位置。根据WAL(Write-Ahead Logging,日志先行)原则,在提交事务时会先使用redo log持久化事务发生修改的部分数据(只要redo log落盘并打上commit标记就表示事务已经持久化)。

那么,InnoDB存储引擎体系结构中的各个组件是如何协同工作的?我们列举一个UPDATE场景加以说明。

假设有一个UPDATE语句正在执行:UPDATE test SET idx = 2 WHERE id=10,执行流程如下(这里主要以InnoDB存储引擎体系结构中的组件为主):

(1)在Server层进行词法解析,解析成MySQL认识的语法,查询什么表、什么字段,并生成查询路径树,选择最优查询路径。

(2)到了InnoDB存储引擎这里,先判断id=10这行数据对应的页是否在缓冲池中,如果不在,则将id=10记录对应的页从datafile中读入InnoDB缓冲池中(如果该页已经在缓冲池中,就省去了读入这一步),并对相关记录加独占锁。

(3)将idx修改之前的值和对应的主键、事务ID原来的信息写入Undo Tablespace的回滚段中。

(4)更改缓存页中的数据,并将更新记录和新生成的LSN值(日志序列号)写入Log Buffer中,更新完之后在缓冲池中这个页就是脏页了。

(5)在提交事务时,根据innodb_flush_log_at_trx_commit的设置,用不同的方式将Log Buffer中的更新记录刷新到redo log中,然后写binlog(二进制日志文件),写完binlog就开始commit(这里的commit是指binlog的commit,就是同步到磁盘),binlog同步之后就把binlog文件名和position(binlog文件内的位置)也写到redo log中。然后在redo log中写入一个commit标记,那么此时就完成了这个事务的提交。接下来释放独占锁。

(6)后台I/O线程根据需要择机将缓存中合适的脏页刷新到磁盘数据文件中。当然,在刷新脏页时要先拷贝一份到双写缓冲区中(如果开启了双写缓冲区功能的话),当双写缓冲区中的数据落盘之后,再从缓冲池中把脏页刷新到各个数据文件中。