linhtV / DB

0 stars 0 forks source link

InnoDB存储引擎 #2

Open linhtV opened 6 years ago

linhtV commented 6 years ago

概述

InnoDB设计主要目标是面向在线事务处理(OLTP)的应用。 InnoDB存储引擎将数据放在一个逻辑的表空间中,这个表空间就像一个黑盒一样由InnoDB存储引擎自身管理。它可以将每个InnoDB存储引擎的表单独存放在一个独立的ibd文件中。此外支持裸设备用来建立表空间。 InnoDB通过使用多版本并发控制(MVCC)来获取高并发性,并且实现了SQL标准的四种隔离级别,默认为REPEATABLE级别。同时使用next-keylocking的策略来避免幻读现象的产生。此外,还提供了插入缓冲,二次写,自适应哈希索引,预读等高性能和高可用功能。 对于表中数据的存储,InnoDB存储引擎采用了聚集的方式,因此每张表的存储都是按主键的顺序进程存放的。如果没有显示地为表定义一个主键,InnoDB存储引擎会为每一行生成一个6字节的ROWID,并以此为主键。

架构

innodb1

后台线程

InnoDB存储引擎是多线程模型,因此后台有多个不同的后台线程,负责处理不同的任务。

  1. Master Thread 主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新,合并插入缓冲,UNDO页的回收等等。
  2. IO Thread InnoDB中大量使用AIO来处理写IO请求,而IO Thread的工作主要是负责这些IO请求的回调处理。4个IO Thread分别是log thread,insert buffer thread,innodb_read_io_threads(4个),innodb_write_io_threads(4个)。
  3. Purge Thread 事务被提交后,其所使用的undolog可能不再需要,因此PurgeThread回收已经使用并分配的undo页。支持设置多个Purge线程。
  4. Page Cleaner Thread 将之前版本中的脏页刷新操作都放入单独的线程中来完成。进一步减轻Master线程的压力以及对用户查询线程的阻塞。

    内存

    缓冲池

    InnoDB是基于磁盘存储的,并将其中的记录按照页的方式进行管理。通过缓冲池来缓解磁盘和CPU速度的问题。 缓冲池简单说是一个内存区域,通过内存的速度来弥补磁盘速度较慢对数据库的影响。

    • 页读操作 首先将从磁盘读到的页存放在缓冲池中,这个过程将页“FLX"在缓冲池中,下一次再读相同的页时,首先判断该页是否存在在缓冲池中。若存在,称为页在缓冲池中命中,直接读取该页。否则从磁盘中读取。
    • 页写操作 首先先修改在缓冲池中的页,然后在以一定的频率刷新到磁盘上。页从缓冲池刷新回磁盘的操作并不是每次页变更时触发,而是通过Checkpoint机制刷新回磁盘。

InnoDB允许有多个缓冲池实例,每个页根据哈希值不同平均分配到不同缓冲池实例中。

LRU List, Free List和Flush List

在InnoDB存储引擎中,缓冲池中页的大小为16KB,同样使用LRU算法对缓冲进行管理。但进行了一些优化,在InnoDB的存储引擎中,LRU列表中还加入了midpoint位置(LRU队列5/8处)。新读取到的页,虽然是最新访问的页,但是不直接放在LRU列表的首部,而是放在midpoint位置。这个算法在InnoDB存储引擎下称为midpoint insertion strate。把midpoint之后的页称为old列表,之前的称为new列表。 InnoDB存储引擎中,引入innodb_old_blocks_time来设置页读取到mid位置之后需要等待多久才会被加入到LRU列表的热端。 当数据库刚启动时,LRU列表为空。这时页都存放在Free列表中,当需要从缓冲池中分页时,首先从Free列表中查找是否有可用的空闲页,若有则将该页从Free列表中删除,放在LRU列表中。否则,根据LRU列表,淘汰LRU末尾的页,并将该页分配给新的页。 当页从LRU列表old区进入new区时,称为page made young。 由于innodb_old_blocks_time存在而导致页没有从old部分移动到new部分,称为page not made young。 当LRU列表中页被修改后,称该页为脏页,即缓冲池中的页和磁盘上的页的数据不一致。这时将通过CHECKPOINT机制将脏页刷新回磁盘。而FLUSH列表中的页即为脏页列表。

重做日志缓冲

InnoDB首先将重做日志信息放入这个缓冲区,然后按一定频率将其刷新到重做日志文件。 重做日志在下列三种情况下会讲重做缓冲中的内容刷新到外部磁盘的重做日志文件中。

当数据库宕机时,数据库不需要重做所有的日志,因为checkpoint之前的页都已经被刷新回磁盘。数据库只需对checkpoint之后的重做日志进行恢复。 当缓冲池不够用时,根据LRU算法会将溢出的脏页强制执行Checkpoint ,将脏页刷新回磁盘。 重做日志不可用是因为数据库系统对重做日志的设计是循环使用的,并不让其无限增大。重做日志可以被重用的部分是指该部分重做日志不再被需要,即当数据库宕机时,数据库恢复操作不需要这部分重做日志。若此时仍需要使用,那么必须强制使用Checkpoint,将缓冲池中的页刷新到重做日志的位置。 InnoDB通过LSN(Log Sequence Number)来标记版本的。LSN是8字节数字。每个页,重做日志和Checkpoint都要LSN。 根据每次刷新页数,页的选取及触发时间,将Checkpoint分为以下两种:

发生Fuzzy Checkpoint情形:

如果辅助索引是唯一的,就不能使用该技术,原因很简单,因为如果这样做,整个索引数据被切分为2部分,无法保证唯一性。 插入缓冲主要带来如下两个坏处:

  1. 可能导致数据库宕机后实例恢复时间变长。如果应用程序执行大量的插入和更新操作,且涉及非唯一的聚集索引,一旦出现宕机,这时就有大量内存中的插入缓冲区数据没有合并至索引页中,导致实例恢复时间会很长。
  2. 在写密集的情况下,插入缓冲会占用过多的缓冲池内存,默认情况下最大可以占用1/2,这在实际应用中会带来一定的问题。

    Change Buffer

    Insert Buffer实现

    Insert Buffer是一颗B+树。全局只有一个Insert Buffer负责对所有表的辅助索引进行Insert Buffer。这颗B+树存放在共享表空间中(ibdata1)中。 (未完待续)

    两次写

    保证数据页的可靠性。 部分写失效:当InnoDB在写入数据页到表时,在写入过程中,发生宕机的情况。 重做日志记录的是对页的物理表操作,如果数据页本身发生损坏,在对其进行重做是无意义的。 因此,在应用重做日志之前,需要一个页的副本,当写入失效发生时,先通过页的副本还原该页,再进行重做。这就是两次写。 两次写需要额外添加两个部分:

    • 内存中的两次写缓冲(doublewrite buffer),大小为2MB
    • 磁盘上共享表空间中连续的128页,大小也为2MB 其原理是这样的:
  3. 当刷新缓冲池脏页时,并不直接写到数据文件中,而是先拷贝至内存中的两次写缓冲区。
  4. 接着从两次写缓冲区分两次写入磁盘共享表空间中,每次写入1MB
  5. 待第2步完成后,再将两次写缓冲区写入数据文件

这样就可以解决上文提到的部分写失效的问题,因为在磁盘共享表空间中已有数据页副本拷贝,如果数据库在页写入数据文件的过程中宕机,在实例恢复时,可以从共享表空间中找到该页副本,将其拷贝覆盖原有的数据页,再应用重做日志即可。 其中第2步是额外的性能开销,但由于磁盘共享表空间是连续的顺序写,因此开销不是很大。

自适应哈希索引

哈希索引是一种非常快的等值查找方法(注意:必须是等值,哈希索引对非等值查找方法无能为力),它查找的时间复杂度为常量,InnoDB采用自适用哈希索引技术,它会实时监控表上索引的使用情况,如果认为建立哈希索引可以提高查询效率,则自动在内存中的“自适应哈希索引缓冲区”建立哈希索引。

异步IO

为了提高磁盘操作性能,InnoDB存储引擎采用异步IO(Asynchronous IO,AIO)的方式来处理磁盘操作。

IO Merge

AIO另一个优势是进行IO Merge操作,也就是将多个IO合并为1个IO,这样可以提高IOPS的性能。例如用户需要访问页的(space, offset)为: (8,6),(8,7),(8,8) 每个页的大小为16KB,那么同步IO需要进行3次IO操作。而AIO会判断到这三个页是连续的(可以通过(space,offset)知道)。因此AIO底层会发送一个IO请求,从(8,6)开始,读取48KB的页。

刷新邻接页

InnoDB存储引擎还提供了Flush Neighbor Page(刷新邻接页)的特性。其工作原理为:当刷新一个脏页时,InnoDB存储引擎会检测该页所在区(extent)的所有页,如果是脏页,那么一起进行刷新。 有两个问题:

  1. 是不是可能将不怎么脏的页进行了写入,而该页之后又会 很快变成脏页?
  2. 固态硬盘有着较高的IOPS,是否还需要这个特性?

    启动关闭与恢复

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

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

当正常关闭MySql数据库时,下次的启动应该会非常正常。但是如果没有正常地关闭数据库,如用kill命令关闭数据库,在MySql数据库运行中重启了服务器,或者在关闭数据库时,将参数innodb_fast_shutdown设置为了2,下次MySql数据库启动时都会对InnoDB存储引擎的表进行恢复操作。 参数innodb_force_recovery影响了整个InnoDB存储引擎恢复的状态。该参数默认值为0,表示当发生需要恢复时,进行所有的恢复操作,当不能进行有效恢复时,如数据页发生了corruption,MySql数据库可能发生宕机(crash),并把错误写入错误日志中去。 某些情况下,可能并不需要进行完整的恢复操作,因为用户自己知道怎么进行恢复。比如在对一个表进行alter table操作时发生了意外,数据库重启时会对InnoDB表进行回滚操作,对于一个大表来说这需要很长时间,可能是几个小时。这时用户可以自行进行恢复,如可以把表删除,从备份中重新导入数据到表,可能这些操作的速度要远远快于回滚操作。

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

  1. 忽略检查到的corrupt页。
  2. 阻止Master Thread线程的运行,如Master Thread线程需要进行full purge,而这会导致crash。
  3. 不进行事务的回滚操作。
  4. 不进行插入缓冲的合并操作。
  5. 不查看撤销日志(Undo Log),InnoDB存储引擎会将未提交的事务视为已提交。
  6. 不进行前滚的操作。 参数innodb_force_recovery的值大于0时,可以对表进行select,creaete和drop操作,但是insert,update和delete这类DML操作是不允许的。
linhtV commented 6 years ago

LRU

最频繁使用的也在LRU列表的前端,而最少使用的页在列表的尾端。

压缩页

将原本16KB的页压缩为1KB,2KB,4KB,8KB。 对于非16KB的页是通过unzip_LRU列表管理的。 当申请4KB页时:

  1. 检查4KB的unzip_LRU列表。
  2. 有空闲页,直接使用。
  3. 否则,检查8KB的unzip_LRU列表。
  4. 若能得到,则分页为2个4KB的页,放在unzip_LRU列表。
  5. 如不能得空空闲页,则从LRU列表申请一个16KB的页,分为1个8KB和2个4KB的页。