Mysql

【Mysql技术内幕笔记--2】--InnoDB存储引擎

2019-02-12  本文已影响0人  都是浮云啊

[TOC]

开篇

InnoDB是 事物安全 的Mysql存储引擎,从 Mysql5.5之后的版本创建表的时候默认是 Innodb 存储引擎,是第一个完整支持ACID事物的Mysql存储引擎,它的特点是行锁设计、支持MVCC(多版本并发控制,后面有重点介绍的)、支持外键、提供一致性非锁定读同时被设计用来最有效的利用内存和CPU。

1. InnoDB

InnoDB架构

InnoDB存储引擎的结构如图,它有多个内存块,可以理解为组成了一个大的内存池,在内存池中干了几件事

1.1 InnoDB的后台线程

InnoDB 引擎是多线程的模型,因此其后台有多个不同的后台线程,负责处理不同的任务。包括负责刷新内存池中的数据,保证缓冲池中的内存缓存的是最近的数据。并且将已修改的数据文件刷新到磁盘文件,同时保证在数据库发生异常的情况下 InnoDB 能恢复到正常状态。来看一下这些线程。

1.2 Innodb 内存
1.2.1 Innodb 缓冲池

InnoDB 存储引擎是基于磁盘存储的,并将其中的数据记录按照页的方式进行管理。因此可将其视为基于磁盘的数据库系统。在数据库系统中,CPU速度和磁盘IO速度的不匹配会导致数据库性能的瓶颈。所以基于磁盘的数据库系统通常使用缓冲池技术来提高数据库的整体性能。缓冲区简单来说就是一块内存区,通过内存的速度来弥补磁盘速度与CPU速度不匹配带来的影响。在数据库中进行读取页的时候,首先将从磁盘读到的页存放在缓冲池中,这个过程称为将页 FIX 到缓存池中。下一次再读相同的页时,首先判断该页是否存在,如果在缓冲池中直接命中读取否则读取磁盘上的页。而对于修改则是先修改缓冲池中的页然后定期刷新到磁盘上,这个刷新到磁盘是 CheckPoint 的机制(后面会说checkpoint),这也是为了提高数据库的整体性能。InnoDB的缓冲池大小可以直接影响数据库的整体性能,也是可以配置的。缓冲池中缓存的数据页类型包括:索引页、数据页、自适应哈希索引、InnoDB存储的锁信息、数据字典信息等等。如下图是 InnoDB 内存数据对象。在后来的版本中,可以配置多个缓冲池实例,每个页根据哈希值平均分配到不同的缓冲池实例中,这样做的好处是减少数据库内部的资源竞争,增加并发处理能力,也可以在数据库实例运行的时候通过命令查看缓冲池的使用状态。

缓冲池设计
1.2.2 LRU 、 List Free List和Flush List

缓冲池是一个很大的内存区域,其中存放各种类型的页(数据页、索引页等)。Mysql需要对这么大的内存区域进行管理,通常来说,缓冲池是通过 LRU最近最少使用 算法管理这些页的。最频繁使用的页在 LRU 列表的前端,最少使用的在尾部。当满了的时候首先释放LRU列表末尾的页。不过InnoDB在传统的LRU算法上做了一些优化,LRU列表加入了midpoint这个位置。新读取到的页,虽然是最新访问的页但是病不是直接放到LRU首部而是放到LRU列表的midpoint位置,大概是5/8处(可配置)。这么做的目的是因为如果直接读取到的页放到LRU的首部,那么某些SQL操作可能会使缓冲池中的页被刷出,从而影响缓冲区的效率。比如,索引或者扫描操作时会访问很多页,甚至全部页,这些通常来说只是本次的查询需要的,并不是真正活跃的热点数据,如果页被放入LRU列表的首部,那么非常可能将所需要的热点数据页从LRU列表中移除,下次就会直接访问到磁盘了。这样最起码保证那些真正热点数据,比如前3/8的热点数据不被本次访问影响到,mid之后的列表为old列表,之前的为new列表也就是活跃数据列表。然后就引出一个新的问题了,那前面的热点数据是如何放上去的,InnoDB还引入了另一个参数进一步管理LRU列表,这个参数是innodb_old_blocks_time,用于表示页读取到mid位置后需要等到多久才会被加入的LRU列表的热端。整个过程简单来说就是:把新访问页放到mid位置上,如果过去配置时间后还在mid位置上就把这个页放到LRU的首部,代表它是热点数据。
从上面的过程知道了 LRU列表中的页都是已经读取的页,在数据库刚刚启动时,LRU列表自然是空的。此时这些页都在 Free 列表中,当需要从缓冲池中分配页时首先到这里查找是否有可用的空闲页,如果有 这个页从Free 列表中删除,然后放到LRU列表页中,满了淘汰尾部的页。在最早的InndoDB 版本中,页大小为16k,后来支持了压缩页的功能,可以被压缩成1k,2k,4k,8k。对于非16k的页,是通过 unzip_LRU 列表进行管理的,有4k的unzip_LRU列表和8k的unzip_LRU列表。

在LRU列表中的数据页被修改之后,该页被称之为脏页,即缓冲池中的页和磁盘上的页数据产生了不一致。这时数据库会通过 CheckPoint 机制将脏页刷新回磁盘,而Flush List列表就是脏页列表,这里的页都是要刷新的磁盘的,脏页既存在于LRU列表中也存在Flush列表中。LRU列表用来管理缓冲池中页的可用性,Flush列表用来管理将页刷新回磁盘,二者互不影响。

1.2.3重做日志缓冲

InnoDB 存储引擎的内存区域除了有缓冲池之外还有重做日志缓冲 redo log buffer。InnoDB 存储引擎首先将重做日志信息放入到这个缓冲区中,然后按一定频率将其刷新到重做日志文件。一般情况下每1s会把重做日志缓冲刷新到日志文件,因此用户只需要保证每秒产生的事物量在这个缓冲区大小之内即可。有三种情况会将重做日志缓冲中的内容刷新到外部磁盘的重做文件中,

2.CheckPoint技术

缓冲池的设计目的是为了解决CPU速度和磁盘速度不匹配的问题。因此页的操作通常都是先在缓冲池中完成的,比如 UPDATE DELETE 改变了页的记录,那么此时页是脏页,就需要将缓冲池中最新的数据也就是脏页里的记录刷新到磁盘上。
假设每次有脏页了就把新版本从内存池中刷新到磁盘上,那么这个开销是非常大的。若热点数据集中在某几页中,那么数据库的开销就会很大,性能就被影响了。同时,如果在缓冲池中将新版本刷新到磁盘的过程宕机了,那么数据就不能恢复了。为了避免发生数据丢失的问题,当前事物数据库都采用了 Write Ahead Log 策略,即当事物提交时先写重做日志再修改页。当由于发生宕机导致数据丢失时,通过重做日志来完成数据的恢复。这也是ACID中持久性的要求。
但是要注意的是,缓冲池一般并不缓冲全部的数据,也就是之前说的当未命中的时候还是会去磁盘中查询。CheckPoint技术的目的主要解决几种问题:

  1. 当数据库发生宕机时,数据库不需要重做所有日志,因为 CheckPoint 之前的页都已经刷新回磁盘。故而数据库只需要对 CheckPoint 后的重做日志进行恢复,这样就可以大大缩短了恢复的时间。
  2. 还有就是当缓冲区不够用的时候会根据LRU算法去掉LRU尾部的页也就是最近最少使用的列,若这个页是脏页就需要强制执行CheckPoint,将脏页也即是页的最新版本刷新到磁盘上。
  3. 重做日志并不是无限增大的,重做日志中可以被重用的部分说明这段已经没用了,准确的理解就是这部分的操作已经持久到磁盘上了,那么在数据库恢复的时候不需要这部分的日志的。如果这个时候重做日志还需要使用,那么强制产生CheckPoint将缓冲池中的页至少刷新到当前重做日志的位置。这个 CheckPoint 可以理解为那些还没来得及同步持久化到磁盘的或者持久化到磁盘的过程中发生了宕机导致这个操作失败的一个记录的一个点,一种记录的机制。

对于InnoDB存储引擎而言,checkpoint 是通过LSN(log sequeuece number)来标记版本的,而LSN是8字节的数字。每个页都有LSN,重做日志中也有LSN,checkPoint中也有LSN
在innodb存储引擎中,checkpoint发生的时间、条件及脏页的选择都非常复杂。而checkpoint所做的事情就是将缓冲池中的脏页刷新到磁盘上,不同之处在于每次刷新多少页到磁盘,每次从哪里取脏页以及什么时间出发CheckPoint,。在innodb存储引擎内部,有2种Checkpoint

3. Master Thread

前面我们说了有一个非常重要的具有高优先级的线程 Master Thread,InnoDB存储引擎的主要工作都是在一个单独的后台线程 Master Thread 中完成的,在 InnoDB 的历史版本中对它进行了各种优化迭代.

3.1 InnoDB 1.0.X Master Thread

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

3.1.1 loop 主循环

大多数操作都是在这个循环中的,其中有2个周期任务在这个分别是1s执行一次和10s执行一次的(时钟不可靠的,也就是说有细微偏差),1s的操作有:

10s的操作有下面几种,这些过程中,InnoDB会先判断过去10s内磁盘的IO操作是否小于200次,如果是,InnoDB认为有足够的磁盘IO能力,将100个脏页刷盘。接着innodb存储引擎会合并插入缓冲,不同于1s的操作,这个操作总是会执行的。之后就是日志刷盘。然后就是full purge操作,把无用的 unod 页删除,还有就是当一个行被 delete这种操作标记为删除的时候,在full purge 的过程中会判断当前事物系统已被删除的行是否可以删除,可以删立刻删除。最后就是判断缓冲区脏页比例,超过比例就刷100个不超过比例就刷10个到磁盘上。

3.1.2 background loop 背景循环

如果当前没有用户活动(数据库空闲、关闭)就会切换到这个循环,执行下面的操作

如果flush loop也没事做了就切换到suspend loop把Master Thread 挂起,等待事件的发生。

3.2 InnoDB 1.2.X之前的 Master Thread

前面的版本中,磁盘IO可以说是大大影响着性能,无论何时,innodb引擎最大只会刷新100个脏页到磁盘,合并20个插入缓冲,如果是写入密集情况下,Master Thread就忙不过来了,当积累很多没刷盘的数据时如果宕机了恢复就会很慢很慢。后来进行了一版改进,提供了一个参数 innodb_io_capacity 表示磁盘IO的吞吐量,默认值为200,对于刷新到磁盘的数量,会按照这个参数的百分比来控制,规则:

也就是说如果使用了性能好的磁盘比如SSD,就可以把这个参数调高一些。
还有一个就是脏页的比例设置问题,默认值是90,意味着脏页最多可以占缓冲池的90%,之前是考虑IO慢,现在有SSD这种性能好的磁盘了,这个是有点大了。谷歌团队通过大量的测试最后官网参照了数据取值75%,这样既可以加快刷新脏页的效率,又保证了IO的负载。除了脏页达到比例刷新,还引入了一个脏页的合适数量设置的值,大于这个值也会刷新。

3.3 InnoDB 1.2.X的 Master Thread

最新的版本中,10s的操作抽出来了,减轻了Master Thread的工作提供系统并发性。当然还有一些其它的抽离。

4. InnoDB的关键特性

InnoDB具备一些关键特性,这些特性能给数据库带来高性能和高可靠,比如:

4.1 插入缓冲

Insert Buffer 并不是缓冲池中的一个组成部分,它和数据页一样,也是物理页,它是一种B+树。在 InnoDB 引擎中,主键是唯一的标识符,通常应用程序中记录的插入顺序是按照主键递增的顺序进行插入的,因此插入聚集索引(Primary key)一般是顺序的。同时页中的行记录按照这个主键的值进行顺序存放,一般情况下不需要随机读取另一个页中的记录。因此对于这种情况下的插入操作速度非常快。但是如果主键类是UUID这种的那么插入和辅助索引一样的是随机的,不可能每张表都有一个聚集索引,更多情况下,一张表上有多个非聚集的辅助索引,比如我们建立的各种普通索引 index 。这样的情况下产生了一个非聚集且不唯一的索引,在进行插入操作的时候,数据页的存放还是按照主键进行顺序存放。但是对于非聚集索引叶子节点的插入不再是顺序的了,这个时候就需要离散地访问非聚集索引页,由于随机读取的存在而导致了插入操作的性能下降。
后来 InnoDB 存擎设计了 Insert Buffer,对于非聚集索引的插入或者更新操作不是每一次直接插入到索引中,而是先判断插入的非聚集索引是否在缓冲池中,若在就直接插入。不在的话先放到一个 insert buffer 对象中,数据库这个非聚集的索引已经插到叶子节点,而实际并没有,只是存在 Insert Buffer 中,然后以一定的频率和情况进行Insert Buffer和辅助索引页子节点的merge合并操作,这是通常能将多个插入合并到一个操作中,这就大大提高了对于非聚集索引插入的性能。然而 Insert Buffer 使用需要同时满足2个条件:

首先主键是聚集索引,插入按照主键的顺序插入的,所以不需要随机IO所以它针对的是辅助不唯一索引。辅助索引不能是唯一的,在插入缓冲时,数据库并不去查找索引页来判断插入的记录的唯一性,如果去查找肯定又会有离散读取的情况发生从而导致 Insert Buffer 失去意义。它存在一个问题是:在写密集的情况下,插入缓冲会占用过多的缓冲池内存,这会给其它操作带来影响。Insert Buffer 的数据结构是一棵B+树,这颗B+树放在共享表空间中,是一个全局的树负责对所有的表的辅助所有进行Inser Buffer。Insert Buffer是一棵B+树,叶子节点和非叶子节点组成的。非叶子节点存放的是查询的键值 search key
举个现实中的例子,我们要把几本从图书馆借的书还给图书馆,管理员在拿到书的时候有2种处理方式,第一种是一本一本的把书放到书架上,此时如果有10本的话要放10次。第二种是把书放到旁边的一个回收柜架上,然后等有空的时候再去把书放到图书馆上。这个时候10本书一次就放好了。如果把每次的IO持久化到磁盘看做是一次把书放到柜子上的过程,那么insert buffer就相当于先把书放到书架上,等到空闲的时候一次还完。后来引申出5.5版的 change buffer 不再单单针对 insert 的操作了,对于delete update 这类操作也同样适用insert buffer。

简而言之,innodb的Insert buffer是一个用来对那些离散的操作缓冲的,把若干对同一页面的更新缓存起来合并为一次IO,也就是把随机IO转换成顺序IO,避免性能损耗。它的原理就是先判断要更新的页在不在内存,如果不在就读取 index page存储 insert buffer,按照 Master Thread 的调度来合并非唯一所以和索引页的叶子节点。同时只限制于更新和插入时有效,因为这些操作都是IO操作,尽量把多个随机IO转变为顺序IO,然后就是根据IO的频率让Master Thread去刷新。同时这个buffer需要merge到索引页上,发生的时机有:辅助索引页被读到缓冲池时、Insert Buffer无可用空间时、Master Thread的10s任务

4.2 双写 double write

Insert Buffer 带给InnoDB存储引擎的是性能上的提升,doublewrite带给InnoDB存储引擎的是数据页的可靠性。InndoDB 使用了 doublewrite 的特殊文件刷新技术,如当数据库宕机时可能正在写入某个页到表中,而这个页只被写了一部分,之后就宕机了,这种情况被称为部分写失效。这个时候前面说过可以使用重做日志恢复,但是重做日志是对页的物理操作,如果页本身损坏的话就没意义了。好的办法是重做之前需要一个副本,如果写入失效发生时,先通过页的副本来还原页再重做,这就是doubleWrite。在InnoDB存储引擎中它的结构如下图:

双写

doublewrite由2部分组成的,一部分是内存中的doublewrite buffer,大小为2MB,另一部分是物理磁盘上共享表空间中的连续的128个分页,2个区,大小同样为2MB。在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是会通过memcpy函数将脏页先复制到内存中的doublewrite buffer,之后通过doublewrite buffer再分两次,每次1MB顺序地写入共享表空间到物理磁盘上,然后马上调用fsync函数同步磁盘。避免缓冲写带来的问题。在这个过程中,doublewrite页是连续的,因此这个过程是顺序写的开销不大。在完成doublewrite页的写入后,再将doublewrite buffer中的页写入各个表空间文件中,此时的写入是离散的。如果操作系统在将页写入到磁盘的过程中发生了崩溃,在恢复的过程中,InnoDB存储引擎可以从共享表空间中的doublewrite中找到该页的一个副本,将其复制到表空间文件,再应用重做日志。
默认情况下,所有页的刷新首先需要放入doublewrite中
doublewrite 的缺点
共享表空间的 doublewrite buffer 实际上也是一个文件,写共享表空间会导致更多的 fsync 操作,而硬盘的 fsync 性能因素会降低 mysql的性能但是不是太降低因为是顺序写不是随机写并且 doublewrite buffer是连续的,每次可以刷新多个pages(slave上一般都关闭这个功能,因为它及时写文件失败了还可以从中继日志中恢复),这个了解下就好。

4.3 自适应哈希索引

哈希是一种非常快的查找方法,在一般情况下,这种查找的时间复杂度为O(1),一次查找就能定位到记录。而B+树查找取决于树的高度,一般也就3-4层的样子,需要3-4次查询。InnoDB存储引擎会监控对表上各索引页的查询,如果观察到建立合适的哈希索引会带来速度上的提升就建立哈希索引成为自适应哈希索引AHI。
AHI 通过缓冲池的B+树页构造来的,因此建立的速度很快,而且不需要对整张表结构建立哈希索引。
AHI有一个要求是这个页的连续访问模式必须是一样的,也就是,每次都是where a = ? 这种。启动了AHI后,读取和写入速度提升了2倍,辅助索引的连接操作性能可以提高5倍。需要注意的是针对的是等值查询,不是模糊查询或者范围查询。它是存储引擎控制的,不需要人为介入。我们可以选择开启或者关闭。

4.4 异步IO

为了提高磁盘操作性能,一般都是使用异步IO(AIO)的方式处理磁盘操作,与AIO对应的是同步IO(Sync IO),也就是每进行一次IO操作,需要等待此次操作结束才能继续下面的操作。如果用户发出的是一条索引扫描的查询,那么这条SQL查询语句可能需要扫描多个索引页,也就是需要进行多次IO操作,每扫描一个页并等待完成后再进行下一次的扫描,如果要等完全结束可能会耗费很多等待时间。使用AIO,发出所有的IO指令,等所有IO操作完成一起回调。还有一个优势是IO Merge操作,将多个IO合并为1个IO。

4.5 刷新邻接页

InnoDB存储引擎还提供了Flush Neighbor page刷新邻接页的特性,工作原理为:当刷新一个脏页时,InnoDB存储引擎会检测该页所在区的所有页,如果是脏页就一起刷新。这样做可以通过AIO将多次IO变为1次IO完成。这个可以关闭

5. 启动、关闭和恢复

这部分了解下mysql启动、关闭时InnoDB引擎发生了什么。

  1. 在关闭时,参数 innodb_fast_shutdown 影响着表的 InnoDB存储引擎的行为,参数值可以为0 1 2,默认为1
  1. 在启动时,b_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:不进行前滚的操作。

总结

  1. InnoDB 引擎是多线程的模型,因此其后台有多个不同的后台线程,负责处理不同的任务。包括负责刷新内存池中的数据,保证缓冲池中的内存缓存的是最近的数据。并且将已修改的数据文件刷新到磁盘文件,同时保证在数据库发生异常的情况下 InnoDB 能恢复到正常状态
  2. InnoDB 存储引擎是基于磁盘存储的,并将其中的数据记录按照页的方式进行管理。而内存和IO的速度不匹配衍生出了它的缓冲池的设计,缓冲池是一个很大的内存区域,其中存放各种类型的页(数据页、索引页等)。缓冲池是通过 LRU最近最少使用 算法管理这些页的,不过InnoDB在传统的LRU算法上做了一些优化,LRU列表加入了midpoint这个位置,过了指定的时间如果还在这个位置就放到前面那部分热点数据部分。缓冲池的设计是提供高性能的。
  3. 当数据库发生宕机时,数据库不需要重做所有日志,因为 CheckPoint 之前的页都已经刷新回磁盘。故而数据库只需要对 CheckPoint 后的重做日志进行恢复,这样就可以大大缩短了恢复的时间。这个 CheckPoint 可以理解为那些还没来得及同步持久化到磁盘的或者持久化到磁盘的过程中发生了宕机导致这个操作失败的一个记录的一个点,一种记录的机制。
  4. 早期的Master Thread 具有最高的线程优先级别,内部由多个循环(loop)组成的:主循环loop、后台循环background loop、刷新循环flush loop、暂停循环等suspend loop,Master Thread 会根据数据库运行的状态在这几个循环中进行切换。后来的版本中逐渐拆分和参数配置提高了性能和并发性
  5. InnoDB的一些关键特性
    • 插入缓冲(Insert Buffer)
    • 两次写(Double Write)
    • 自适应哈希索引
    • 异步IO
    • 刷新邻接页
上一篇下一篇

猜你喜欢

热点阅读