[toc]

存储引擎

体系架构

image
上图为InnoDB存储引擎的架构,下面会对各个模块进行拆解

1.1 后台线程

  • master Thread:负责将缓冲池中的数据异步的刷新到磁盘上,维护数据一致性
  • IO Thread:进行异步IO管理线程
  • Purge Thread:负责对undo页进行回收,在InnoDB1.0版本之前这个工作是放到master线程上进行处理的
  • Page Cleaner Thread:负责处理脏页的刷新工作

1.2 内存

image

1.2.1 缓冲池

  • 本质就是一个缓存机制,帮助解决磁盘和CPU之间读写速度差距过大的问题,我们来具体看看InnoDB的缓存机制是怎么样的
  • 大体的流程是通用的,CPU只面向内存进行读写,数据需要先加载到缓冲池中,如果数据页已经在缓冲池中那就可以直接使用这个叫做缓冲池命中,而缓冲池和磁盘的同步并不是每次页数据发生更改就会进行同步,而是依赖一种叫Checkpoint的机制
  • 所以我们可以看出缓冲池的大小是很大程度上决定了数据库处理速度的,我测试了一下我个人的服务器上这个缓冲池的大小是一个G
  • 从InnoDB1.0开始就允许存在多个缓冲池,

1.2.2 缓冲池管理

上面说了缓冲池是一片内存区域,现在我们来了解一下这片内存区域是如何进行管理的

LRU(Latest Recent Used)

最近最少使用算法,思想是使用频繁的页被放在LRU列表的前端,使用少的页放在尾端,当缓冲池中放不下新读取到的页时,首先释放LRU列表中尾端的页

  • InnoDB中对LRU进行了一些优化,加入了一个midpoint位置,新读取到的页不是直接放到LRU的首部,而是放在这个midpoint位置,这个策略就是为了避免大量的读取页的时候将之前的热度页全给冲掉了(说的就是那种性能糟糕的查询或者全表查询读取了大量的页),默认情况下这个midpoint是LRU列表长度的3/8处(可以进行设置),InnoDB中将midpoint之后的列表成为old列表,之前的叫new列表,这里就和JVM命名思路相反了,这里的new才是热度数据
    1
    2
    3
    4
    # 查看midpoint参数,默认值是37,即37%差不多3/8
    show variables like 'innodb_old_blocks_pct'
    # 设置
    set global innodb_old_blocks_pct=38
  • 这里还有一个参数叫做innodb_old_blocks_time,表示页被放到mid后多久也就是old列表页中的数据存活多久之后会被放到new中,很有JVM分代的感觉了,具体的设置方法
    1
    2
    # 这里书上没有说这个1000的时间单位是什么
    set global innodb_old_blocks_time=1000;
  • 页从old部分加入到new的操作叫page made young,设置了innodb_old_blocks_time导致页没有从old部分转移到new的操作叫page not made young
    1
    2
    # 通过这个命令可以看到数据库中页的信息以及一段时间内made young的操作次数(得往下拉)
    show engine innodb status
    image
    这里也简单的介绍一下:
    • Buffer pool size表示共有多少个页x16k(默认每个页是16k)即缓冲池的总大小1.5G
    • 这里的database pages就是上面图里面的data page,以及下面的old列表,之后还有pages made young的操作次数
    • Buffer pool hit rate就是指命中率,低于95%就可能是出现了LRU列表污染问题了
    • 之后还有一个LRU len和unzip_LRU len这里的unzip是指压缩,上面不是说了默认一个页的大小是16k嘛,但是从InnoDB 1.0.x开始支持压缩页的功能,可以将16k的页压缩为1、2、4、8k,这里不详细介绍压缩的机制,大概是压缩率是不同的,所以刚才说的几个大小的空间是分开进行管理的,压缩完成后找对应大小的空间进行存储,如果对应大小的空间没有空闲页了就去找一个更大的空间的空闲页进行拆分,比如一个页压缩后是4k结果4k的空间没有空闲了就去将一个8k拆为两个4k一个用于存储,一个成为4k空闲页
    • 在LRU列表中的页被修改之后就成了脏页,也就是缓冲数据与磁盘中的数据不一致,通过Checkpoint技术将脏页刷新回磁盘,所以Flush列表中的页即为脏页,脏页同时存在于LRU和Flush列表中,Modified db pages就是展示了脏页的数据

1.2.3 重做日志缓冲

就是重做日志的缓存,默认的值是8M,在一下三种情况下会将缓存刷新到磁盘中

  • Master Thread每秒刷新一次
  • 每个事务提交时刷新
  • 重做日志缓冲剩余空间小于一半时

1.2.4 额外的内存池

上面说了一些数据结构,例如缓冲池中帧缓冲和对应的缓冲控制对象(LRU、锁)等,这些对象本身占用的内存是从额外内存池中申请的

二、CheckPoint技术

2.1 简介

上面说了数据库通过缓冲的机制来协调CPU和磁盘之间的代沟,但是如果每次缓存中的数据页内容发生变化的时候就将数据刷新到磁盘的话IO开销会特别大(等于没有缓存),于是肯定是需要累积一部分的数据后再做刷新;

这样会产生一个新的问题,屯了一批脏页,如果在缓冲池将页刷新到磁盘的过程中发生宕机,那这部分的数据就不能恢复了,目前对于这个问题通用的解决方案是“Write Ahead Log”,即当事务提交时先写重做日志,再修改页(这里应该是写日志只需要一次寻址写入即可,而写页的话需要很多次的寻址?),出现宕机时还可以通过重做日志来完成数据的恢复(重做日志上面有说写入的机制,反正就是很及时,最多损失一秒的数据)

2.2 机制探索

现在有了缓存,也有了重做日志,进行设想,如果缓存和重做日志的容积都足够大的话,完全就不需要将数据刷新到磁盘了,正常运行的时候直接访问缓存,宕机了通过重做日志进行恢复;

问题在于内存比磁盘贵,重做日志也不好进行磁盘空间分配,整体来说是不合理的,及时都满足了,那发生宕机后需要将所有的重做日志都执行一遍可能要好几个月

于是CheckPoint技术出现了,解决以下几个问题

  • 缩短数据恢复时间:很好理解,CheckPoint之前的数据刷新过了不用再执行了(CheckPoint过程中发生宕机怎么办)
  • 缓冲池不够用是,将脏页刷新到磁盘:LRU算法中的会溢出最近最少使用的页,如果溢出的是脏页,需要强制执行CheckPoint将脏页刷新到磁盘(这里是溢出的脏页还是所有脏页)
  • 重做日志不可用时,刷新脏页:数据库对重做日志是循环的去写入的,当新的日志要去做覆盖的时候,如果被覆盖的日志内容还没有被刷新到磁盘,那必须强制CheckPoint

2.3 LSN

image

通过Log Sequence Number来标记版本,仍然是在show engine innodb status命令中可以看到版本信息

2.4 CheckPoint的详细机制

在上面的笔记中就提出了一些问题,CheckPoint到底是全量的将脏页刷新到磁盘还是局部的,这里来详细的说明,CheckPoint在InnoDB中有两种类型:

  • Sharp CheckPoint:在数据库关闭的时候,全量刷新脏页
  • Fuzzy CheckPoint:局部刷新
    • Master Thread CheckPoint:每一秒或十秒刷新一定比例的脏页,异步,请求操作不会被阻塞
    • Flush_LRU_List CheckPoint:InnoDB会保证LRU中保持有100个页可用,老版本中这个查询工作是在用户查询线程中进行的,会阻塞用户查询,新版本这个检查的工作被放到了独立的Page Cleaner线程中且默认数值变为了1024;检查出现空闲页不足时要将LRU尾部的页移除,如果这里面存在脏页则需要进行CheckPoint
    • Async/Sync Flush CheckPoint:都是为了保证重做日志的可用性,我们将没有进行过CheckPoint的日志内容定义为未处理(这里之所以会是两种处理方式,是由于老版本中Async会阻塞发现问题的用户查询线程,而Sync会阻塞所有用户线程;新版中都已经放到了Page Cleaner线程中,估计区别已经消失了)
      • 当未处理内容小于总量的75%时不需要刷新
      • 当未处理内容大于75%小于90%时进行Async刷新,使得刷新后的未处理内容小于总量的75%
      • 当未处理内容大于90%时进行sync刷新,使得刷新后的未处理内容小于总量的75%
    • Dirty Page too much CheckPoint:当缓冲池中脏页占比大于75%时强制进行CheckPoint,刷新部分脏页

三、Master Thread的工作方式

3.1 1.0.x之前的工作方式

master thread具有最高的线程优先级,内部由主循环、后台循环、刷新循环、暂停循环组成.

3.1.1 主循环(Loop)

主循环内主要有两部分的操作:每秒钟执行的逻辑和每十秒执行的逻辑;

  • 每秒执行的逻辑:
    日志缓冲刷新到磁盘,即使事务还没有提交
  • 合并插入缓冲
  • 至多刷新100个脏页到磁盘
  • 如果当前没有用户活动,切换到后台循环
    每十秒执行的逻辑:
  • 刷新100个脏页到磁盘
  • 合并至多5个插入缓冲
  • 将日志缓冲刷新到磁盘
  • 删除无用的Undo页
  • 刷新100或10个脏页到磁盘

    3.1.2 后台循环(background Loop)

    当前没有用户活动或者数据库关闭时切换到这个循环,执行的操作如下
  • 删除无用的Undo页
  • 合并20个插入缓冲
  • 跳回到主循环
  • 不断刷新100个页直到符合条件(可能跳转到刷新循环中)

3.2 1.2.x之前的工作方式

由于磁盘读写速度的加快,固态硬盘的出现,应对更高的写入场景时mater thread中对缓冲刷新到磁盘的限制显得太保守了,不能很好的发挥性能,所以做了一下的调整

  • 合并插入缓冲是,动态的决定合并的数量
  • 刷新脏页时动态的决定刷新的数量

    3.3 1.2.x的工作方式

    再次优化了master thread的逻辑,将脏页刷新的工作分离到了单独的Page Cleaner Thread线程中,提高了系统并发性

四、InnoDB关键特性

  • 插入缓冲
  • 两次写
  • 自适应hash索引
  • 异步IO
  • 刷新领接页

4.1 插入缓冲

  • Insert Buffer

    对于主键索引由于数据存储是连续的(不连续属于设计不合理),所以插入操作速度很快,但是对于普通索引来说一般都是离散分布的,所以插入的效率不高(涉及数据的移动),于是为普通索引设计了Insert Buffer

    原理就是将数据插入的操作放到缓存里面,如果有多次对同一个索引的修改就可以进行合并插入,避免索引上数据多次的移动

    必须要满足两个条件:是辅助索引、索引不是唯一的

    从原理可以看出在一个写入密集型的系统中Insert Buffer对缓冲池的占用会很大,默认最大值是二分之一

  • Change Buffer

    本质是Insert Buffer的升级版,在Innodb 1.0.x中给update、delete操作也加入了缓存,分为了Insert、delete、purge,delete是将数据标记为删除,而purge是真正将记录删除

    image
    这里merged operations里面展示了insert、delete、purge的次数;而下面的discarded operations表示数据

    4.2 两次写

    如果服务器在脏页刷新到磁盘的过程中出现了宕机,对应页的数据会被损坏(),这个时候通过重做日志无法修复(没有完整的页信息,只有操作信息),两次写即是为了解决这个问题
    image

具体实现是:中内存和磁盘中开辟两个2M的空间作为缓存,先将脏页放到doublewrite buffer中,再刷新到磁盘(1M),注意这个时候由于着1M的空间是连续的顺序写入效率很高,如果发生宕机还可以用doublewrite中的数据进行恢复(内存中别的数据可以通过重做日志进行恢复)

部分文件系统本身提供了写失效防范机制,如ZFS文件系统,这个时候就可以关闭两次写,另外如果是主从结构中的从服务器可以进行关闭来提高速度(数据本来就备份)

4.3 自适应hash索引(adaptive hash index AHI)

InnoDB存储引擎会自动为一些频繁查询的索引创建AHI(hash的时间复杂度为O(1)),由于这些hash索引都是通过缓冲池中的B+树页创建,所以速度很快,且这个策略是存储引擎自己的机制不用人为的干涉

  • 创建的条件

    • 同一个sql结构访问了100次
    • 页通过该模式访问了N次,N=页中记录*1/16
  • 启用后读取和写入速度提升2倍,负责索引的连接操作性能提高5倍
    image

  • 同样可以通过show engine innidb status来查看AHI的工作情况,这里可以看到目前数据库中AHI的使用不是很好

    4.4 异步IO

    用户需进行数据查询时可能需要扫描磁盘上多个页的数据,这个时候只要发出一个页的IO请求后不必等待响应直接发出下一个IO请求,这就叫做异步IO,一看原理就知道很省时间

  • IO merge:单要访问的几个页是连续的(每个页大小是16k)时候就不用发起多个请求,直接从起点读取48k数据即可

  • Native AIO:1.1.x版本之前AIO是通过存储引擎的代码模拟实现的,之后提供了内核级别的AIO支持成为Native AIO,需要libaio库的支持,Windows和Linux都提供了支持,但是Mac OSX没有

  • AIO同样可以进行手动关闭

  • read ahead方式的读取都是通过AIO完成的,脏页的刷新,即磁盘的写入则全部由AIO完成

4.5 刷新领接页

InnoDB提供了Flush Neighbor Page的特性,即是一个脏页刷新时检查所在区的所有页,如果是脏页则一起刷新,参考AIO可以进行IO merge操作,在机械硬盘上有显著的优势,需要考虑一下两个问题

  • 如果临近的脏页其实不怎么脏,之后又很快的变成了脏页?
  • ==固态硬盘有着较高的IOPS==,是否还需要这个特性?

结论是固态硬盘就不建议开这个特征了

五、启动、关闭与恢复

这里就简单说一下,关闭时候大概分为三个级别innodb_fast_shutdown取值0、1、2,每个级别对不同的启动方式

  • 0 完成所有的full purge和merge insert buffer,并将所有的脏页刷回磁盘,这个级别的问题在于 慢!
  • 1 只需要将缓冲池中的一些脏页刷新到磁盘(估计就是一些已经进行刷新流程的数据),这是默认的级别
  • 2 只将日志都写入日志文件(日志缓存很小好像是2M),操作极快,坏处是下次启动时需要进行事务恢复操作

对于服务器意外宕机、手动kill mysql或者设置为2时启动的时候都需要进行恢复操作,可以通过innodb_force_recovery来配置恢复情况,默认为0,会对数据进行恢复,还有一些特殊情况,比如是alter表结构的时候发生了意外,数据库重启,这个时候如果进行自动恢复需要很长的时间,可以选择人为的干涉