innodb 存储引擎 -- 读书笔记

InnoDB体系架构

后台线程

Master Thread

  • Master Thread是一个非常核心的后台线程,主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新、合并插入缓冲(INSERT BUFFER)、UNDO页的回收等。

IO Thread

  • 在InnoDB存储引擎中大量使用了AIO(Async IO)来处理写IO请求,这样可以极大提高数据库的性能。
  • IO Thread的工作主要是负责这些IO请求的回调(call back)处理。
  • 通过命令SHOW ENGINE INNODB STATUS来观察InnoDB中的IO Thread

Purge Thread

  • 事务被提交后,其所使用的undolog可能不再需要,因此需要PurgeThread来回收已经使用并分配的undo页。
  • InnoDB支持多个Purge Thread,这样做的目的是为了进一步加快undo页的回收。同时由于Purge Thread需要离散地读取undo页,这样也能更进一步利用磁盘的随机读取性能。
  • SHOW VARIABLES LIKE 'innodb_purge_threads'\G

Page Cleaner Thread

  • 作用是将之前版本中脏页的刷新操作都放入到单独的线程中来完成。而其目的是为了减轻原Master Thread的工作及对于用户查询线程的阻塞,进一步提高InnoDB存储引擎的性能。

内存

缓冲池

  • InnoDB存储引擎是基于磁盘存储的,并将其中的记录按照页的方式。
  • 缓冲池简单来说就是一块内存区域,通过内存的速度来弥补磁盘速度较慢对数据库性能的影响。
  • 数据库中进行读取页的操作,首先将从磁盘读到的页存放在缓冲池中,这个过程称为将页“FIX”在缓冲池中。下一次再读相同的页时,首先判断该页是否在缓冲池中。若在缓冲池中,称该页在缓冲池中被命中,直接读取该页。否则,读取磁盘上的页。
  • 对于InnoDB存储引擎而言,其缓冲池的配置通过参数innodb_buffer_pool_size来设置。
  • SHOW VARIABLES LIKE'innodb_buffer_pool_size'\G;
#
  • 允许有多个缓冲池实例。每个页根据哈希值平均分配到不同缓冲池实例中。这样做的好处是减少数据库内部的资源竞争,增加数据库的并发处理能力。
  • 可以通过参数innodb_buffer_pool_instances来进行配置,该值默认为1
  • SHOW VARIABLES LIKE'innodb_buffer_pool_instances'\G;
  • show engine innodb status\G;
image.png

可以通过information_schema架构下的表INNODB_BUFFER_POOL_STATS来观察缓冲的状态,如运行下列命令可以看到各个缓冲池的使用状态

LRU List、Free List和Flush List

  • 数据库中的缓冲池是通过LRU(Latest Recent Used,最近最少使用)算法来进行管理的。即最频繁使用的页在LRU列表的前端,而最少使用的页在LRU列表的尾端。当缓冲池不能存放新读取到的页时,将首先释放LRU列表中尾端的页。
  • InnoDB存储引擎对传统的LRU算法做了一些优化。在InnoDB的存储引擎中,LRU列表中还加入了midpoint位置。
  • 新读取到的页,虽然是最新访问的页,但并不是直接放入到LRU列表的首部,而是放入到LRU列表的midpoint位置。
  • midpoint位置可由参数innodb_old_blocks_pct控制
  • SHOW VARIABLES LIKE'innodb_old_blocks_pct'\G;
  • 在InnoDB存储引擎中,把midpoint之后的列表称为old列表,之前的列表称为new列表。可以简单地理解为new列表中的页都是最为活跃的热点数据。

那为什么不采用朴素的LRU算法,直接将读取的页放入到LRU列表的首部呢?

innodb_old_blocks_pct

  • 这是因为若直接将读取到的页放入到LRU的首部,那么某些SQL操作可能会使缓冲池中的页被刷新出,从而影响缓冲池的效率。常见的这类操作为索引或数据的扫描操作。这类操作需要访问表中的许多页,甚至是全部的页,而这些页通常来说又仅在这次查询操作中需要,并不是活跃的热点数据。
  • 如果页被放入LRU列表的首部,那么非常可能将所需要的热点数据页从LRU列表中移除,而在下一次需要读取该页时,InnoDB存储引擎需要再次访问磁盘。
  • midpoint位置可由参数innodb_old_blocks_pct控制
  • SHOW VARIABLES LIKE'innodb_old_blocks_pct'\G;
  • SET GLOBAL innodb_old_blocks_pct=20;

innodb_old_blocks_time

  • InnoDB存储引擎引入了另一个参数来进一步管理LRU列表,这个参数是innodb_old_blocks_time,用于表示页读取到mid位置后需要等待多久才会被加入到LRU列表的热端。
  • 因此当需要执行上述所说的SQL操作时,可以通过下面的方法尽可能使LRU列表中热点数据不被刷出。
  • SET GLOBAL innodb_old_blocks_time=1000;

Free List LRU List

  • LRU列表用来管理已经读取的页,但当数据库刚启动时,LRU列表是空的,即没有任何的页。这时页都存放在Free列表中。
  • 当需要从缓冲池中分页时,首先从Free列表中查找是否有可用的空闲页,若有则将该页从Free列表中删除,放入到LRU列表中。否则,根据LRU算法,淘汰LRU列表末尾的页,将该内存空间分配给新的页。
  • 当页从LRU列表的old部分加入到new部分时,称此时发生的操作为page madeyoung,
  • 而因为innodb_old_blocks_time的设置而导致页没有从old部分移动到new部分的操作称为page not made young。
  • 可以通过命令SHOW ENGINE INNODB STATUS来观察LRU列表及Free列表的使用情况和运行状态。
image.png
  • 当前Buffer pool size共有8191个页,8191*16kb 128MB
  • Free buffers表示当前Free列表中页的数量
  • Database pages表示LRU列表中页的数量
  • 可能的情况是Free buffers与Database pages的数量之和不等于Buffer pool size。
  • 因为缓冲池中的页还可能会被分配给自适应哈希索引、Lock信息、Insert Buffer等页,而这部分页不需要LRU算法进行维护,因此不存在于LRU列表中。
  • pages made young显示了LRU列表中页移动到前端的次数,
  • not young为因为innodb_old_blocks_time的设置而导致页没有从old部分移动到new部分的操作次数
  • youngs/s、non-youngs/s表示每秒这两类操作的次数

Buffer pool hit rate,表示缓冲池的命中率,这个例子中为100%,说明缓冲池运行状态非常良好。
通常该值不应该小于95%。若发生Buffer pool hit rate的值小于95%这种情况,用户需要观察是否是由于全表扫描引起的LRU列表被污染的问题。

image.png

LRU中的页包含了unzip_LRU列表中的页。
unzip_LRU是怎样从缓冲池中分配内存的

  • 首先,在unzip_LRU列表中对不同压缩页大小的页进行分别管理。
  • 其次,通过伙伴算法进行内存的分配。
    例如对需要从缓冲池中申请页为4KB的大小,其过程如下:
  • 1)检查4KB的unzip_LRU列表,检查是否有可用的空闲页;
  • 2)若有,则直接使用;
  • 3)否则,检查8KB的unzip_LRU列表;
  • 4)若能够得到空闲页,将页分成2个4KB页,存放到4KB的unzip_LRU列表;
  • 5)若不能得到空闲页,从LRU列表中申请一个16KB的页,将页分为1个8KB的页、2个4KB的页,分别存放到对应的unzip_LRU列表中。
image.png

在LRU列表中的页被修改后,称该页为脏页(dirty page),即缓冲池中的页和磁盘上的页的数据产生了不一致

  • 这时数据库会通过CHECKPOINT机制将脏页刷新回磁盘,而Flush列表中的页即为脏页列表。
  • 脏页既存在于LRU列表中,也存在于Flush列表中。LRU列表用来管理缓冲池中页的可用性,Flush列表用来管理将页刷新回磁盘,二者互不影响
  • Modified db pages 0就显示了脏页的数量。

重做日志缓冲

InnoDB存储引擎的内存区域除了有缓冲池外,还有重做日志缓冲(redo log buffer)

  • InnoDB存储引擎首先将重做日志信息先放入到这个缓冲区,然后按一定频率将其刷新到重做日志文件。
  • 一般情况下每一秒钟会将重做日志缓冲刷新到日志文件,因此用户只需要保证每秒产生的事务量在这个缓冲大小之内即可。该值可由配置参数innodb_log_buffer_size控制,默认为8MB
  • SHOW VARIABLES LIKE'innodb_log_buffer_size'\G

重做日志在下列三种情况下会将重做日志缓冲中的内容刷新到外部磁盘的重做日志文件中

  • Master Thread每一秒将重做日志缓冲刷新到重做日志文件;
  • 每个事务提交时会将重做日志缓冲刷新到重做日志文件;
  • 当重做日志缓冲池剩余空间小于1/2时,重做日志缓冲刷新到重做日志文件。

额外的内存池

InnoDB存储引擎中,对内存的管理是通过一种称为内存堆(heap)的方式进行的。

  • 在对一些数据结构本身的内存进行分配时,需要从额外的内存池中进行申请,当该区域的内存不够时,会从缓冲池中进行申请
  • 例如,分配了缓冲池(innodb_buffer_pool),但是每个缓冲池中的帧缓冲(frame buffer)还有对应的缓冲控制对象(buffer control block),这些对象记录了一些诸如LRU、锁、等待等信息,而这个对象的内存需要从额外内存池中申请。

Checkpoint技术

为了避免发生数据丢失的问题,当前事务数据库系统普遍都采用了Write Ahead Log策略,即当事务提交时,先写重做日志,再修改页。当由于发生宕机而导致数据丢失时,通过重做日志来完成数据的恢复。这也是事务ACID中D(Durability持久性)的要求。
Checkpoint(检查点)技术的目的是解决以下几个问题:

  • 缩短数据库的恢复时间;
  • 缓冲池不够用时,将脏页刷新到磁盘;
  • 重做日志不可用时,刷新脏页。

当数据库发生宕机时,数据库不需要重做所有的日志,因为Checkpoint之前的页都已经刷新回磁盘。故数据库只需对Checkpoint后的重做日志进行恢复。这样就大大缩短了恢复的时间。

当缓冲池不够用时,根据LRU算法会溢出最近最少使用的页,若此页为脏页,那么需要强制执行Checkpoint,将脏页也就是页的新版本刷回磁盘。

前事务数据库系统对重做日志的设计都是循环使用的

LSN

  • 对于InnoDB存储引擎而言,其是通过LSN(Log Sequence Number)来标记版本的。而LSN是8字节的数字,其单位是字节。
  • 每个页有LSN,重做日志中也有LSN,Checkpoint也有LSN。
  • SHOW ENGINE INNODB STATUS 来观察 LSN
image.png

在InnoDB存储引擎内部,有两种Checkpoint,分别为:

  • Sharp Checkpoint
  • Fuzzy Checkpoint

Sharp Checkpoint发生在数据库关闭时将所有的脏页都刷新回磁盘,这是默认的工作方式,即参数innodb_fast_shutdown=1。

但是若数据库在运行时也使用Sharp Checkpoint,那么数据库的可用性就会受到很大的影响。故在InnoDB存储引擎内部使用Fuzzy Checkpoint进行页的刷新,即只刷新一部分脏页,而不是刷新所有的脏页回磁盘

在InnoDB存储引擎中可能发生如下几种情况的Fuzzy Checkpoint:

  • Master Thread Checkpoint
  • FLUSH_LRU_LIST Checkpoint
  • Async/Sync Flush Checkpoint
  • Dirty Page too much Checkpoint

Master Thread Checkpoint

对于Master Thread中发生的Checkpoint,差不多以每秒或每十秒的速度从缓冲池的脏页列表中刷新一定比例的页回磁盘。这个过程是异步的,即此时InnoDB存储引擎可以进行其他的操作,用户查询线程不会阻塞。

FLUSH_LRU_LIST Checkpoint

  • FLUSH_LRU_LIST Checkpoint是因为InnoDB存储引擎需要保证LRU列表中需要有差不多100个空闲页可供使用
  • 在InnoDB1.1.x版本之前,需要检查LRU列表中是否有足够的可用空间操作发生在用户查询线程中,显然这会阻塞用户的查询操作。倘若没有100个可用空闲页,那么InnoDB存储引擎会将LRU列表尾端的页移除。如果这些页中有脏页,那么需要进行Checkpoint,而这些页是来自LRU列表的,因此称为FLUSH_LRU_LIST Checkpoint。
  • 是InnoDB1.2.x版本开始,这个检查被放在了一个单独的Page Cleaner线程中进行,并且用户可以通过参数innodb_lru_scan_depth控制LRU列表中可用页的数量,该值默认为1024,

Async/Sync Flush Checkpoint

  • Async/Sync Flush Checkpoint指的是重做日志文件不可用的情况,这时需要强制将一些页刷新回磁盘,而此时脏页是从脏页列表中选取的。
  • 若将已经写入到重做日志的LSN记为redo_lsn,将已经刷新回磁盘最新页的LSN记为checkpoint_lsn,
  • checkpoint_age=redo_lsn-checkpoint_lsn
    • async_water_mark=75%*total_redo_log_file_size
    • sync_water_mark=90%*total_redo_log_file_size
    • 若每个重做日志文件的大小为1GB,并且定义了两个重做日志文件,则重做日志文件的总大小为2GB。那么async_water_mark=1.5GB,sync_water_mark=1.8GB。
    • 当checkpoint_age<async_water_mark时,不需要刷新任何脏页到磁盘
    • 当async_water_mark<checkpoint_age<sync_water_mark时触发Async Flush,从Flush列表中刷新足够的脏页回磁盘,使得刷新后满足checkpoint_age<async_water_mark;
    • checkpoint_age>sync_water_mark这种情况一般很少发生,除非设置的重做日志文件太小,并且在进行类似LOADDATA的BULK INSERT操作。此时触发Sync Flush操作,从Flush列表中刷新足够的脏页回磁盘,使得刷新后满足checkpoint_age<async_water_mark。
  • 在InnoDB 1.2.x版本之前,Async FlushCheckpoint会阻塞发现问题的用户查询线程,而Sync Flush Checkpoint会阻塞所有的用户查询线程,并且等待脏页刷新完成。
  • 从InnoDB 1.2.x版本开始——也就是MySQL 5.6版本,这部分的刷新操作同样放入到了单独的Page Cleaner Thread中,故不会阻塞用户查询线程。

重做日志而产生的Async/Sync Flush的次数
image.png

Master Thread工作方式

InnoDB 1.0.x版本之前的Master Thread

Master Thread具有最高的线程优先级别。其内部由多个循环(loop)组成:主循环(loop)、后台循环(backgrouploop)、刷新循环(flush loop)、暂停循环(suspend loop)。Master Thread会根据数据库运行的状态在loop、background loop、flush loop和suspendloop中进行切换

Loop

Loop被称为主循环,因为大多数的操作是在这个循环中,其中有两大部分的操作——每秒钟的操作和每10秒的操作
每秒一次的操作包括:

  • 日志缓冲刷新到磁盘,即使这个事务还没有提交(总是)
  • 合并插入缓冲(可能)
  • 至多刷新100个InnoDB的缓冲池中的脏页到磁盘(可能)
  • 如果当前没有用户活动,则切换到background loop(可能)

即使某个事务还没有提交,InnoDB存储引擎仍然每秒会将重做日志缓冲中的内容刷新到重做日志文件。这一点是必须要知道的,因为这可以很好地解释为什么再大的事务提交(commit)的时间也是很短的。

合并插入缓冲(Insert Buffer)并不是每秒都会发生的。InnoDB存储引擎会判断当前一秒内发生的IO次数是否小于5次,如果小于5次,InnoDB认为当前的IO压力很小,可以执行合并插入缓冲的操作。

刷新100个脏页也不是每秒都会发生的。InnoDB存储引擎通过判断当前缓冲池中脏页的比例(buf_get_modified_ratio_pct)是否超过了配置文件中innodb_max_dirty_pages_pct这个参数(默认为90,代表90%),如果超过了这个阈值,InnoDB存储引擎认为需要做磁盘同步的操作,将100个脏页写入磁盘中。

接着来看每10秒的操作,包括如下内容:

  • 刷新100个脏页到磁盘(可能的情况下)
  • 合并至多5个插入缓冲(总是)
  • 将日志缓冲刷新到磁盘(总是)
  • 删除无用的Undo页(总是)
  • 刷新100个或者10个脏页到磁盘(总是)

InnoDB存储引擎会先判断过去10秒之内磁盘的IO操作是否小于200次,如果是,InnoDB存储引擎认为当前有足够的磁盘IO操作能力,因此将100个脏页刷新到磁盘

InnoDB存储引擎会合并插入缓冲。不同于每秒一次操作时可能发生的合并插入缓冲操作,这次的合并插入缓冲操作总会在这个阶段进行。

InnoDB存储引擎会再进行一次将日志缓冲刷新到磁盘的操作。这和每秒一次时发生的操作是一样的。

full purge操作,即删除无用的Undo页。

  • 对表进行update、delete这类操作时,原先的行被标记为删除,但是因为一致性读(consistent read)的关系,需要保留这些行版本的信息。
  • 但是在full purge过程中,InnoDB存储引擎会判断当前事务系统中已被删除的行是否可以删除,比如有时候可能还有查询操作需要读取之前版本的undo信息,如果可以删除,InnoDB会立即将其删除。
  • 从源代码中可以发现,InnoDB存储引擎在执行full purge操作时,每次最多尝试回收20个undo页。

InnoDB存储引擎会判断缓冲池中脏页的比例(buf_get_modified_ratio_pct),如果有超过70%的脏页,则刷新100个脏页到磁盘,如果脏页的比例小于70%,则只需刷新10%的脏页到磁盘

background loop

前没有用户活动(数据库空闲时)或者数据库关闭(shutdown),就会切换到这个循环。background loop会执行以下操作:

  • 删除无用的Undo页(总是)
  • 合并20个插入缓冲(总是)
  • 跳回到主循环(总是)
  • 不断刷新100个页直到符合条件(可能,跳转到flush loop中完成)

若flush loop中也没有什么事情可以做了,InnoDB存储引擎会切换到suspend__loop,将Master Thread挂起,等待事件的发生。若用户启用(enable)了InnoDB存储引擎,却没有使用任何InnoDB存储引擎的表,那么Master Thread总是处于挂起的状态.

InnoDB1.2.x版本之前的Master Thread

问题:InnoDB存储引擎最大只会刷新100个脏页到磁盘,合并20个插入缓冲。如果是在写入密集的应用程序中,每秒可能会产生大于100个的脏页,如果是产生大于20个插入缓冲的情况,Master Thread似乎会“忙不过来”,或者说它总是做得很慢
解决办法:提供参数innodb_io_capacity,用来表示磁盘IO的吞吐量,默认值为200。对于刷新到磁盘页的数量,会按照innodb_io_capacity的百分比来进行控制.规则如下:

  • 在合并插入缓冲时,合并插入缓冲的数量为innodb_io_capacity值的5%;
  • 在从缓冲区刷新脏页时,刷新脏页的数量为innodb_io_capacity。

问题:参数innodb_max_dirty_pages_pct默认值的问题,在InnoDB 1.0.x版本之前,该值的默认为90,意味着脏页占缓冲池的90%。但是该值“太大”了,因为InnoDB存储引擎在每秒刷新缓冲池和flush loop时会判断这个值,如果该值大于innodb_max_dirty_pages_pct,才刷新100个脏页,如果有很大的内存,或者数据库服务器的压力很大,这时刷新脏页的速度反而会降低。同样,在数据库的恢复阶段可能需要更多的时间。
解决办法:从InnoDB 1.0.x版本开始,innodb_max_dirty_pages_pct默认值变为了75,和Google测试的80比较接近。这样既可以加快刷新脏页的频率,又能保证了磁盘IO的负载。

InnoDB 1.0.x版本带来的另一个参数是innodb_adaptive_flushing(自适应地刷新),该值影响每秒刷新脏页的数量

  • 刷新规则是:脏页在缓冲池所占的比例小于innodb_max_dirty_pages_pct时,不刷新脏页;大于innodb_max_dirty_pages_pct时,刷新100个脏页。
  • 随着innodb_adaptive_flushing参数的引入,InnoDB存储引擎会通过一个名为buf_flush_get_desired_flush_rate的函数来判断需要刷新脏页最合适的数量。粗略地翻阅源代码后发现buf_flush_get_desired_flush_rate通过判断产生重做日志(redo log)的速度来决定最合适的刷新脏页数量。因此,当脏页的比例小于innodb_max_dirty_pages_pct时,也会刷新一定量的脏页。

之前每次进行full purge操作时,最多回收20个Undo页,从InnoDB 1.0.x版本开始引入了参数innodb_purge_batch_size,该参数可以控制每次full purge回收的Undo页的数量。该参数的默认值为20,并可以动态地对其进行修改

从InnoDB 1.0.x开始,命令SHOW ENGINE INNODB STATUS可以查看当前Master Thread的状态信息

InnoDB 1.2.x版本的Master Thread

对于刷新脏页的操作,从Master Thread线程分离到一个单独的Page Cleaner Thread,从而减轻了Master Thread的工作,同时进一步提高了系统的并发性。
InnoDB存储引擎的关键特性包括:

  • 插入缓冲(Insert Buffer)
  • 两次写(Double Write)
  • 自适应哈希索引(Adaptive Hash Index)
  • 异步IO(Async IO)
  • 刷新邻接页(Flush Neighbor Page)

插入缓冲

Insert Buffer
  • InnoDB存储引擎开创性地设计了Insert Buffer,对于非聚集索引的插入或更新操作,不是每一次直接插入到索引页中,而是先判断插入的非聚集索引页是否在缓冲池中,若在,则直接插入;若不在,则先放入到一个Insert Buffer对象中,好似欺骗。
  • 数据库这个非聚集的索引已经插到叶子节点,而实际并没有,只是存放在另一个位置。
  • 然后再以一定的频率和情况进行Insert Buffer和辅助索引页子节点的merge(合并)操作,这时通常能将多个插入合并到一个操作中(因为在一个索引页中),这就大大提高了对于非聚集索引插入的性能。

然而Insert Buffer的使用需要同时满足以下两个条件:

  • 索引是辅助索引(secondary index);
  • 索引不是唯一(unique)的

用户可以通过命令SHOW ENGINE INNODB STATUS来查看插入缓冲的信息:
image.png
  • seg size显示了当前Insert Buffer的大小
  • free list len代表了空闲列表的长度
  • size代表了已经合并记录页的数量。
  • Inserts代表了插入的记录数
  • merges代表合并的次数,也就是实际读取页的次数

目前Insert Buffer存在一个问题是:在写密集的情况下,插入缓冲会占用过多的缓冲池内存(innodb_buffer_pool),默认最大可以占用到1/2的缓冲池内存。

Change Buffer

InnoDB存储引擎可以对DML操作——INSERT、DELETE、UPDATE都进行缓冲,他们分别是:Insert Buffer、Delete Buffer、Purge buffer。
当然和之前Insert Buffer一样,Change Buffer适用的对象依然是非唯一的辅助索引。

对一条记录进行UPDATE操作可能分为两个过程:

  • 将记录标记为已删除;
  • 真正将记录删除。

因此Delete Buffer对应UPDATE操作的第一个过程,即将记录标记为删除。Purge Buffer对应UPDATE操作的第二个过程,即将记录真正的删除

  • InnoDB存储引擎提供了参数innodb_change_buffering,用来开启各种Buffer的选项。
  • 该参数可选的值为:inserts、deletes、purges、changes、all、none。inserts、deletes、purges就是前面讨论过的三种情况
  • changes表示启用inserts和deletes,all表示启用所有,none表示都不启用。该参数默认值为all。

可以通过参数innodb_change_buffer_max_size来控制Change Buffer最大使用内存的数量

通过命令SHOW ENGINE INNODB STATUS来查看缓冲的信息:
image.png

显示了merged operations和discarded operation,并且下面具体显示Change Buffer中每个操作的次数。

  • insert表示Insert Buffer;
  • delete mark表示Delete Buffer;
  • delete表示Purge Buffer;
  • discarded operations表示当Change Buffer发生merge时,表已经被删除,此时就无需再将记录合并(merge)到辅助索引中了。
Insert Buffer的内部实现
  • Insert Buffer的数据结构是一棵B+树
  • 现在版本本中,全局只有一棵Insert BufferB+树,负责对所有的表的辅助索引进行Insert Buffer。而这棵B+树存放在共享表空间中,默认也就是ibdata1中
  • 试图通过独立表空间ibd文件恢复表中数据时,往往会导致CHECK TABLE失败。这是因为表的辅助索引中的数据可能还在Insert Buffer中,也就是共享表空间中,所以通过ibd文件进行恢复后,还需要进行REPAIR TABLE操作来重建表上所有的辅助索引。
  • 非叶节点存放的是查询的search key(键值)
  • image.png
    • search key一共占用9个字节,
    • 其中space表示待插入记录所在表的表空间id,在InnoDB存储引擎中,每个表有一个唯一的space id,可以通过space id查询得知是哪张表,space占用4字节
    • marker占用1字节,它是用来兼容老版本的Insert Buffer
    • offset表示页所在的偏移量,占用4字节
  • 当一个辅助索引要插入到页(space,offset)时,如果这个页不在缓冲池中,那么InnoDB存储引擎首先根据上述规则构造一个search key,接下来查询Insert Buffer这棵B+树,然后再将这条记录插入到Insert Buffer B+树的叶子节点中
  • 对于插入到Insert Buffer B+树叶子节点的记录(如图2-4所示),并不是直接将待插入的记录插入,而是需要根据如下的规则进行构造:
  • image.png
    • space、marker、page_no字段和之前非叶节点中的含义相同,一共占用9字节。第4个字段metadata占用4字节,其存储的内容如表
    • image.png
    • IBUF_REC_OFFSET_COUNT是保存两个字节的整数,用来排序每个记录进入Insert Buffer的顺序
    • 从Insert Buffer叶子节点的第5列开始,就是实际插入记录的各个字段了。因此较之原插入记录,Insert Buffer B+树的叶子节点记录需要额外13字节的开销。
    • 因为启用Insert Buffer索引后,辅助索引页(space,page_no)中的记录可能被插入到Insert Buffer B+树中,所以为了保证每次Merge Insert Buffer页必须成功,还需要有一个特殊的页用来标记每个辅助索引页(space,page_no)的可用空间。这个页的类型为Insert Buffer Bitmap。
    • 每个Insert Buffer Bitmap页用来追踪16384个辅助索引页,也就是256个区(Extent)。每个Insert Buffer Bitmap页都在16384个页的第二个页中。
  • image.png

Merge Insert Buffer

Merge Insert Buffer的操作可能发生在以下几种情况下:

  • 辅助索引页被读取到缓冲池时;
  • Insert Buffer Bitmap页追踪到该辅助索引页已无可用空间时;
  • Master Thread

第一种情况为当辅助索引页被读取到缓冲池中时,例如这在执行正常的SELECT查询操作,

  • 这时需要检查Insert Buffer Bitmap页,然后确认该辅助索引页是否有记录存放于Insert Buffer B+树中。
  • 若有,则将Insert Buffer B+树中该页的记录插入到该辅助索引页中。
  • 可以看到对该页多次的记录操作通过一次操作合并到了原有的辅助索引页中,因此性能会有大幅提高

第二种情况Insert Buffer Bitmap页用来追踪每个辅助索引页的可用空间,并至少有1/32页的空间。

  • 若插入辅助索引记录时检测到插入记录后可用空间会小于1/32页,则会强制进行一个合并操作,即强制读取辅助索引页,将Insert Buffer B+树中该页的记录及待插入的记录插入到辅助索引页中。

在Master Thread线程中每秒或每10秒会进行一次Merge Insert Buffer的操作,不同之处在于每次进行merge操作的页的数量不同。

对于Insert Buffer页的选择,InnoDB存储引擎并非采用这个方式,它随机地选择Insert Buffer B+树的一个页,读取该页中的space及之后所需要数量的页。该算法在复杂情况下应有更好的公平性。

若进行merge时,要进行merge的表已经被删除,此时可以直接丢弃已经被Insert/Change Buffer的数据记录。

两次写

doublewrite(两次写)带给InnoDB存储引擎的是数据页的可靠性。

image.png

doublewrite由两部分组成,

  • 一部分是内存中的doublewrite buffer,大小为2MB,
  • 另一部分是物理磁盘上共享表空间中连续的128个页,即2个区(extent),大小同样为2MB

doublewrite 写入过程

  • 在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是会通过memcpy函数将脏页先复制到内存中的doublewrite buffer,之后通过doublewrite buffer再分两次,每次1MB顺序地写入共享表空间的物理磁盘上,然后马上调用fsync函数,同步磁盘,避免缓冲写带来的问题。
  • 在这个过程中,因为doublewrite页是连续的,因此这个过程是顺序写的,开销并不是很大。
  • 在完成doublewrite页的写入后,再将doublewrite buffer中的页写入各个表空间文件中,此时的写入则是离散的

查看doublewrite 运行情况

SHOW GLOBAL STATUS LIKE'innodb_dblwr%'\G;
image.png

doublewrite一共写了 1976156526个页,但实际的写入次数为 480823641,

如果发现系统在高峰时的Innodb_dblwr_pages_written:Innodb_dblwr_writes远小于64∶1,那么可以说明系统写入压力并不是很高。

如果操作系统在将页写入磁盘的过程中发生了崩溃,在恢复过程中,InnoDB存储引擎可以从共享表空间中的doublewrite中找到该页的一个副本,将其复制到表空间文件,再应用重做日志

参数skip_innodb_doublewrite可以禁止使用doublewrite功能,这时可能会发生前面提及的写失效问题。不过如果用户有多个从服务器(slave server),需要提供较快的性能(如在slaves erver上做的是RAID0),也许启用这个参数是一个办法。不过对于需要提供数据高可靠性的主服务器(master server),任何时候用户都应确保开启doublewrite

自适应哈希索引

InnoDB存储引擎会监控对表上各索引页的查询。如果观察到建立哈希索引可以带来速度提升,则建立哈希索引,称之为自适应哈希索引(Adaptive Hash Index,AHI)
AHI是通过缓冲池的B+树页构造而来,因此建立的速度很快,而且不需要对整张表构建哈希索引。

AHI构建要求

  • 对这个页的连续访问模式必须是一样的。
  • 以该模式访问了100次
  • 页通过该模式访问了N次,其中N=页中记录*1/1

根据InnoDB存储引擎官方的文档显示,启用AHI后,读取和写入速度可以提高2倍,辅助索引的连接操作性能可以提高5倍。毫无疑问,AHI是非常好的优化模式,其设计思想是数据库自优化的(self-tuning),即无需DBA对数据库进行人为调整。

通过命令SHOW ENGINE INNODB STATUS可以看到当前AHI的使用状况
image.png

可以看到AHI的使用信息了,包括AHI的大小、使用情况、每秒使用AHI搜索的情况。

  • 值得注意的是,哈希索引只能用来搜索等值的查询,如SELECT*FROM table WHERE index_col='xxx’。
  • 而对于其他查找类型,如范围查找,是不能使用哈希索引的,因此这里出现了non-hash searches/s的情况。
  • 通过hash searches:non-hash searches可以大概了解使用哈希索引后的效率。

由于AHI是由InnoDB存储引擎控制的,因此这里的信息只供用户参考。不过用户可以通过观察SHOW ENGINE INNODB STATUS的结果及参数innodb_adaptive_hash_index来考虑是禁用或启动此特性,默认AHI为开启状态

异步IO

为了提高磁盘操作性能,当前的数据库系统都采用异步IO(Asynchronous IO,AIO)的方式来处理磁盘操作

  • 用户可以在发出一个IO请求后立即再发出另一个IO请求,当全部IO请求发送完毕后,等待所有IO操作的完成,这就是AIO。
  • AIO的另一个优势是可以进行IO Merge操作,也就是将多个IO合并为1个IO,这样可以提高IOPS的性能。

InnoDB 1.1.x开始(InnoDB Plugin不支持),提供了内核级别AIO的支持,称为Native AIO。

参数innodb_use_native_aio用来控制是否启用Native AIO,在Linux操作系统下,默认值为ON:

用户可以通过开启和关闭Native AIO功能来比较InnoDB性能的提升。官方的测试显示,启用Native AIO,恢复速度可以提高75%。

刷新邻接页

刷新一个脏页时,InnoDB存储引擎会检测该页所在区(extent)的所有页,如果是脏页,那么一起进行刷新。这样做的好处显而易见,通过AIO可以将多个IO写入操作合并为一个IO操作,故该工作机制在传统机械磁盘下有着显著的优势

InnoDB存储引擎从1.2.x版本开始提供了参数innodb_flush_neighbors,用来控制是否启用该特性。对于传统机械硬盘建议启用该特性,而对于固态硬盘有着超高IOPS性能的磁盘,则建议将该参数设置为0,即关闭此特性。

启动、关闭与恢复

在关闭时,参数innodb_fast_shutdown影响着表的存储引擎为InnoDB的行为。该参数可取值为0、1、2,默认值为1。

  • 0表示在MySQL数据库关闭时,InnoDB需要完成所有的full purge和merge insert buffer,并且将所有的脏页刷新回磁盘。这需要一些时间,有时甚至需要几个小时来完成。如果在进行InnoDB升级时,必须将这个参数调为0,然后再关闭数据库。
  • 1是参数innodb_fast_shutdown的默认值,表示不需要完成上述的full purge和merge insert buffer操作,但是在缓冲池中的一些数据脏页还是会刷新回磁盘。
  • 2表示不完成full purge和merge insert buffer操作,也不将缓冲池中的数据脏页写回磁盘,而是将日志都写入日志文件。这样不会有任何事务的丢失,但是下次MySQL数据库启动时,会进行恢复操作(recovery)。

参数innodb_force_recovery影响了整个InnoDB存储引擎恢复的状况。该参数值默认为0,代表当发生需要恢复时,进行所有的恢复操作,当不能进行有效恢复时,如数据页发生了corruption,MySQL数据库可能发生宕机(crash),并把错误写入错误日志中去

参数innodb_force_recovery还可以设置为6个非零值:1~6。大的数字表示包含了前面所有小数字表示的影响。具体情况如下:

  • 1(SRV_FORCE_IGNORE_CORRUPT):忽略检查到的corrupt页。
  • 2(SRV_FORCE_NO_BACKGROUND):阻止Master Thread线程的运行,如Master Thread线程需要进行full purge操作,而这会导致crash。
  • 3(SRV_FORCE_NO_TRX_UNDO):不进行事务的回滚操作。
  • 4(SRV_FORCE_NO_IBUF_MERGE):不进行插入缓冲的合并操作。
  • 5(SRV_FORCE_NO_UNDO_LOG_SCAN):不查看撤销日志(Undo Log),InnoDB存储引擎会将未提交的事务视为已提交。
  • 6(SRV_FORCE_NO_LOG_REDO):不进行前滚的操作

需要注意的是,在设置了参数innodb_force_recovery大于0后,用户可以对表进行select、create和drop操作,但insert、update和delete这类DML操作是不允许的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,009评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,808评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,891评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,283评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,285评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,409评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,809评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,487评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,680评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,499评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,548评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,268评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,815评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,872评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,102评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,683评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,253评论 2 341

推荐阅读更多精彩内容