InnoDB存储引擎(1)

Posted by BY KiloMeter on February 19, 2020

InnoDB体系架构

从上图可以看到,Innodb内部由多个内存块构成了内存池,主要负责以下工作:

  • 维护所有进程/线程需要访问的多个内部数据结构。
  • 缓存磁盘中的数据,并且修改后的数据先缓存在缓存池中。
  • redo log缓冲。
  • ….

后台线程

后台线程的主要作用是负责刷新内存池中的数据,保证缓存池中的内存缓存的是最近的数据。同时将已修改的数据文件刷新到磁盘。

Innodb后台线程主要有7个(除Windows系统),包含了4个IO thread,1个master thread(大部分工作都由该线程完成),1个锁(lock)监控线程,还有1个错误监控线程。4个IO线程分别是insert buffer thread、log thread、read thread、write thread。

在windows系统下,read thread和write thread都增大到了4个。

内存

InnoDB存储引擎内存由以下几个部分组成:缓冲池、重做日志缓冲池(redo log buffer)以及额外的内存池。

缓冲池是占据最大内存的部分,用来存放各种数据的缓存。Innodb存储引擎的工作方式是将数据库文件按页(每页16K)读取到缓冲池,然后按照最近最少使用(LRU)算法保留缓冲池中的数据,如果发生了修改,则首先修改缓冲池中的数据(发生修改后,该页为脏页),然后按照一定的频率将缓冲池中的脏页数据刷新到文件。

通过show engine innodb status\G;可以查看innodb引擎的各种状况

上图是语句运行后的部分截图,buffer pool size表明了一共有多少个缓冲帧(buffer frame),每个buffer frame大小为16K,Free buffers表示当前空闲的缓冲帧,Database pages表示已经使用的缓冲帧,Modified db pages表示脏页的数量。

具体来看,缓冲池中缓存的数据页类型有:索引页、数据页、undo页、插入缓冲(insert buffer),自适应哈希索引,InnoDB存储的锁信息、数据字典信息等。

日志缓冲将redo log信息先放入这个区域,然后按一定频率刷新到日志文件。

额外内存池的作用主要是用于存储一些特殊数据结构的信息,上面讲到,缓冲池中的基本内存单元是缓冲帧(buffer frame),每个缓冲帧有其对应的缓冲控制对象(buffer control block),这些对象记录了诸如LRU、锁、等待等方面的信息,缓冲控制对象的内存就是从额外内存池中进行申请的。因此,如果Innodb缓冲池的空间申请得比较大,额外内存池得空间也应该相应地增加。

master thread

前面在讲后台线程的时候,其中有一个线程是master thread,Innodb引擎的主要工作都是由该线程完成的。

下面来剖析master thread源码

master thread优先级最高,其内部由几个循环组成,分别是:主循环(loop)、后台循环(background loop)、刷新循环(flush loop)、暂停循环(suspend loop)。master thread会根据数据库运行的状况在这几个循环中进行切换。

大多数操作都在loop主循环中,其中有两大部分操作:每秒操作和每10秒操作。伪代码如下:

void master_thread(){
    loop:
    for(int i = 0;i<10;i++){
        do thing once per second
        sleep 1 second if necessary
    }
    do things once per ten seconds
    goto loop;
}

在每一秒中,主要做的事情有:

  • 日志缓冲刷新到磁盘,即使事务还未提交
  • 合并插入缓冲(Innodb会判断这一秒内发生的次数是否小于5次,如果小于5次,则认为当前IO压力很小,可以合并插入缓冲)
  • 最多刷新100个缓冲池的脏页到磁盘(Innodb会判断当前脏页数量是否达到配置文件中的innodb_max_dirty_pages_pct这个参数的值(默认为90,代表90%),如果超出这个阈值,才会将最多100个脏页写入磁盘)
  • 如果当前没有用户活动,切换到backgroud loop

每十秒主要做的事情有:

  • 刷新100个脏页到磁盘(Innodb会判断如果在过去10秒中,IO操作是否小于200次,如果是,则认为当前有足够的IO操作能力,因此会将100个脏页刷新到磁盘)
  • 合并至多5个插入缓冲
  • 将日志缓冲刷新到磁盘
  • 删除无用的undo页
  • 刷新100个或10个脏页到磁盘(Innodb会判断脏页的比例(buf_get_modified_ratio_pct)是否超出70%,如果超出了,则刷新100个脏页到磁盘,否则刷新10%的脏页到磁盘)
  • 产生一个检查点(将最老日志序列号的页写入磁盘)

如果当前没有用户活动或者数据库关闭时,会切换到background这个循环,该循环会执行以下操作:

  • 删除无用的undo页
  • 合并20个插入缓冲
  • 跳回主循环(如果上面两步都没有事情的话,则不会跳回主循环,而是跳转到flush loop这个循环)

在flush loop循环中,会不断进行刷新,看看是否有脏页,有的话就刷写回磁盘(最多刷写100个脏页)。如果flush loop循环也没事的话,则会将master thread线程挂起,等待事件的发生。

在后续的使用过程中,发现master thread存在一些问题,如上面在刷新脏页数据的时候,最多只刷写100个脏页,合并20个插入缓冲,在一些写入比较密集的应用中,如果每秒产生的脏页大于100或者产生的插入缓冲大于20,则会导致master thread总是处于“繁忙”的状态。同时,如果发生宕机的话,由于很多脏页没有刷写回磁盘,可能会导致花费了较长的时间用于系统恢复。后续google的工程师对其进行了一些优化,可以通过调整参数来修改要刷写的脏页数等等。

关键特性

插入缓冲

在上面讲解各个循环的时候,都有一个合并插入缓冲的步骤,这个步骤究竟是干什么的呢?这里就牵扯到了Innodb的插入缓冲。首先我们都知道的是,主键(即聚集索引)是一行数据的唯一标识,在使用自增主键的时候,在一般情况下,数据的插入都是顺序的,在插入数据的时候,不需要随机读取另一页数据页,这样插入数据就能很快完成,但在更多的情况下,一张表会有其他辅助索引,在这种情况下,在插入一条数据的时候,数据的位置仍然是根据主键进行顺序存放,但是对于辅助索引来说,则需要离散地访问所有非聚集索引页,会导致插入数据的效率降低。因此,插入缓冲就是为了解决这个问题而存在的,在Innodb中,非聚集索引的插入或者更新操作,首先会查找下索引页是否在缓冲池中,如果在,则直接插入,如果不在,则先写入到插入缓冲区中,后续再进行插入缓冲以及非聚集索引页节点的合并,这样就大大提高了插入数据的效率。其实这个和HBase的LSM思路基本一样,通过再内存中进行插入更新,然后定期刷写会磁盘进行合并。

在这里需要注意,如果非聚集索引的值是唯一的话,就不能使用插入缓冲,因为在使用插入缓冲时,并没有查找索引页的情况,如果取查找索引页的话,则又会出现离散读的情况,插入缓冲也就失去了意义。

两次写

两次写主要是为了数据的可靠而设计的。现在假设一种情况,在数据库发生宕机的时候,如果正在写入一个页面,而这个页面只写了一部分,这种情况称之为写失效。这种情况该如何解决呢?有人可能会说了,redo日志不就是用来恢复的吗?很可惜,redo对写失效这种情况无能为力,为什么呢?因为redo日志记录的是修改的位置,redo日志是按照块进行数据记录的,比如,它记录了某个数据文件的第1023个数据块的100字节位置,数据修改为了‘new data’,redo日志只记录了这么一条数据,而对于其他没有修改的地方,redo日志是不会进行记录的,因此,在写失效发生的时候,我们无法得知从哪里为止的数据是正确的,因此也就无法利用redo日志进行数据恢复了。

double write解决这个问题的思路很简单,就是在脏页写入磁盘前,先把脏页中的数据保存一份到磁盘就行了,这样即使发生了写失效,也能够通过磁盘中的脏页副本加上redo日志进行数据恢复。doublewrite分为两个部分,一部分是内存中的double write buffer,大小为2MB,另一部分是磁盘上共享表空间中连续的128个页,大小也为2MB,当要刷写脏页数据时,并不直接写磁盘,而是先通过memcpy函数将脏页数据拷贝到double wirte buffer,然后double write buffer分两次,每次写入1MB到共享表空间上,然后马上调用fsync函数,同步磁盘。这个过程中,由于doublewrite页是连续的,因此是顺序写入,开销并不是很大。

自适应哈希索引

Innodb存储引擎会监控对表上非聚簇索引的查找,如果发现某非聚簇索引被频繁访问,非聚簇索引成为热数据,建立哈希索引可以带来速度的提升。经常访问的非聚簇索引数据会自动被生成到hash索引里面去(最近连续被访问三次的数据),自适应哈希索引通过缓冲池的B+树构造而来,因此建立的速度很快。好处在于可以降低对非聚簇索引树的频繁访问,缺点是会占据内存(上面的内存分布可以看到,有部分内存用于自适应哈希索引),而且自适应哈希索引只适用于select * from table where index_col=’xxx’,而对于其他查找类型,如范围查找,是不能使用的。