InnoDB存储引擎-MySQL体系结构/InnoDB存储引擎

539 阅读14分钟

在正式开始讨论之前,首先明确两个概念:

数据库,是文件的集合,是一个个数据文件组成的;数据库实例,是一个程序,用户程序通过数据库程序(也就是数据库实例)与数据库文件打交道。

来看一下MySQL架构图: mysql-architecture.png

2.1. InnoDB体系架构

1902BDE9-6218-4D87-A1DA-11CD53E78486_1_105_c.jpeg

首先来看看InnoDB的体系结构,InnoDB使用后台线程进行实际的数据操作,同时,InnoDB有多个内存块,负责和后台线程搭配,它们组成了一个大的内存池,用来负责如下工作:

  • 维护所有进程/线程所需要的数据结构。
  • 缓存磁盘页,同时在更新磁盘之前先在这里更新。
  • 重做日志的缓存

后台线程的主要作用是把磁盘中的页读到缓冲区,同时保证缓冲区中的页是最近使用的页(比如使用LRU算法);把缓冲区中的更新的页刷回到磁盘,或者使用重做日志处理宕机时的数据恢复。

现在来一个个讲解。

2.1.1. 后台线程

InnoDB使用多个线程进行实际的数据处理,这些线程包括:

  1. Master Thread。是最核心的一个线程,主要负责同步缓冲区到磁盘。把缓冲区中的数据,异步刷新到磁盘以此来保证数据一致性,包括脏页(在缓冲区更新了的页,此时它们已经不同于硬盘上对应位置的页)的刷新,合并插入缓冲(插入缓冲见后),undo页的回收。

  2. IO Thread。在InnoDB中大量使用了AIO操作来处理写IO,而IO Thread主要用于处理这些写IO的回调操作。

  3. Purge Thread(清除Thread)。事务被提交后,其所使用的undo log可能不再使用,因此需要使用Purge Thread来回收已经分配并使用的undo页

  4. Page Cleaner Thread。用来把脏页的刷新操作放在单独的线程中进行。

2.1.2. 内存

说完了四个后台线程,现在来说说内存池。

  1. 缓冲池。

用来把磁盘中的页读取到缓冲区,实现下次同区域的快速查询。但是对于数据的修改,是首先修改缓冲区(其实先写重做日志,防止宕机数据丢失,再写缓冲区),再以一种称为checkpoint的技术间隔性地刷新到磁盘。

来看看缓冲区的组成:

image.png

其中,数据页和索引页占了大部分。

从1.0.X版本开始,允许有多个缓冲池实例,每个页根据哈希值平均分配到不同的缓冲池实例中,这样可以减少数据库内部的资源竞争,增加数据库的并发能力。

  1. LRU List,Free List,Flush List。

InnoDB对于缓冲池的管理是在LRU算法下进行的,InnoDB的默认页大小是16KB。如果想要添加页,但是页不够了,可以从列表尾删除一个,添加到列表头。然后如果每次访问了某个页,就把它移动到队头,这也是最常见的LRU,但是InnoDB为之做了优化:

首先引入一个midpoint,然后再引入一个old_block_time。midpoint指出新加入的页应该放在整个LRU中的哪个位置,而不是直接放在队首,这样可以避免在频繁的查询操作中把热点数据刷出。但是这并不能保证在大量操作中的热点数据不被刷出(数量大了还是会刷出,不管新数据放在哪里),所以引入了old_block_time,指出加入到midpoint的页需要等多久才会被加入到热点数据区

一开始,整个LRU都是空的,所以使用一个Free List来维护,每次添加页时,会先从Free List加载,如果有空闲页,则从Free List移除,并使用;否则使用LRU淘汰一个旧的页。

InnoDB在1.0.X开始支持了页压缩功能,原本16KB的页,可以划分成1KB,2KB,4KB,8KB大小,而压缩之后的页不再使用LRU,而是使用unzip_LRU进行管理。LRU中的页包含unzip_LRU中的页,因为压缩页是使用LRU中的页压缩得来的。

对于unzip_LRU的管理方式:如果此时我们申请一个4KB的页,那么具体流程是:

A. 首先检查4KB的页是否可用,如果可用,直接使用,否则。

B. 否则检查8KB的页,如果有8KB的页可用,分成两个4KB,加入到unzip_LRU。

C. 如果没有8KB可用,则向LRU申请一个16KB的页,并分成8KB + 2 * 4KB。

这个算法称为伙伴算法

在缓冲区中的数据页被修改后,需要把它们写回到磁盘,此时这些页称为"脏页"。InnoDB通过CHECKPOINT技术定期刷新脏页,为了记录它们,使用称为Flush List的列表进行记录,这样只要把Flush List里面的页刷新到磁盘即可,注意,一个页可能即存在于LRU中,也可以存在于Flush List中,这很好理解,因为LRU中可能存在"脏页"。

一般来说,这些算法用于数据页和索引页这种大的数据区管理。

  1. 重做日志缓冲

重做日志记录的是一个事务所作出的修改,以防突然的宕机之类的意外导致修改消失,用来保证事务的持久性。

InnoDB一般会把重做日志缓冲到这个缓冲区,然后就像脏页刷新一样,以一定频率刷新到磁盘。关于重做日志的刷新触发机制,有三个:

A. MasterThread每秒都会刷新重做日志缓冲到磁盘。

B. 事务提交会触发磁盘写入(事务都提交了,自然就不需要再记录事务的修改了)。

C. 重做日志缓冲区剩余空间小于一半时。

  1. 额外的内存池

对于一些数据结构所需要的内存,或者缓冲区中的帧缓冲所对应的控制对象,是需要在额外的内存池申请空间的,所以当缓冲区很大时,可以考虑同步扩大这部分空间。

2.2. Checkpoint技术

在InnoDB中,使用了Write Ahead Log技术,就是事务所做的修改,先修改在重做日志中,再把重做日志缓冲写到磁盘,然后再修改页。这样做是为了保证ACID中的持久化特性

即使有这种能力,但是也不能一直就让重做日志来保证持久化,因为从持久化恢复,需要时间。而且数据页脏页的刷新也需要时间,不能积攒太多。

所以我们引入了Checkpoint技术,保证脏页和重做日志可以隔一段时间回写到数据库或磁盘

在InnoDB中,重做日志不是无限大的,而是一个循环文件,重复利用,所以可能出现用尽的可能,此时大多可以通过刷新脏页,释放他们对应的重做日志缓解。所以checkpoint大多适用于脏页处理。

InnoDB中,有两种Checkpoint,它们分别是:Sharp Checkpoint/Fuzzy Checkpoint

Sharp Checkpoint在数据库关闭时把所有脏页全部回写到磁盘。

如果在运行时使用Sharp技术,那么会影响数据库性能,于是使用Fuzzy技术。

现在可用的Fuzzy Checkpoint主要有这几种:

  • MasterThread的间隔性触发。
  • FlushLRUList。因为需要保证LRU有足够的页可用,所以会把队尾的页移出,如果其中包含脏页,则触发(检查脏页的操作在后来的版本中放到了Page Cleaner线程中单独进行)。
  • Async/Sync Flush Checkpoint用于在重做日志不可用时,触发机制,强制刷新脏页以此来释放日志文件。
  • Dirty Page Too Much Checkpoint用于在脏页过多时触发,保证缓冲区可用空间不至于太小。

2.3. MasterThread

旧版中的MatserThread实现

MasterThread有最高的优先级,其内部有四个循环:主循环,后台循环,刷新循环,和挂起循环。

主循环有两个操作:每秒一次和每十秒一次。

每秒一次包括:

  • 总是把日志缓冲回写到磁盘。
  • 可能会合并insert buffer(如果当前系统I/O压力小就合并)。
  • 可能会刷新最多100个脏页到磁盘。
  • 在没有用户活动时切换到后台循环。

即使某个事务并未commit,也把日志刷新到日志文件,这就可以解释即使一个事务很大,为什么提交依旧很快。

每十秒一次包括:

  • 可能刷新100个脏页(如果系统I/O压力小就执行)。
  • 总是合并最多5个insert buffer。
  • 总是回写日志缓冲。
  • 总是删除无用的undo页。
  • 总是刷新100/10个脏页。

后台循环会在当前没有用户活动或者数据库关闭时切换至此,它有如下几个操作:

  • 总是删除无用的undo页。
  • 总是合并20个insert buffer。
  • 闲置的话跳转到刷新循环,否则跳转到主循环。

刷新循环会刷新100个脏页直到无页可刷,此时会跳转到挂起循环。

挂起循环会一直挂起,直到发生事件,跳转到主循环。

IMG_3218.jpeg

改进版的MasterThread

通过观察可以发现,旧版的MasterThread对于IO其实是有限制的,因为它是否进行刷新脏页的标准取决于前一阶段内IO次数,同时每次刷新的脏页数量固定,在这SSD中明显太慢了,因为SSD写入和读取速度远比HDD快多了。

可以使用参数调整,比如:innodb_io_capacity值,还有innodb_max_dirty_pages_pct指出了脏页占缓冲区的比例到达这个值时触发,可以调小一点。

另外可以使用自适应刷新技术:innodb_adaptive_flush,这个参数会根据产生重做日志的速度来动态调整。

1.2.X版本中的MasterThread

此版本中,刷新脏页的操作分离到了独立的PageCleanerThread中进行。

2.4. InnoDB关键特性

InnoDB的关键特性正是决定它与众不同的原因,它们有:

  • 插入缓冲(Insert Buffer)
  • 两次写
  • 自适应哈希索引
  • 异步IO
  • 刷新邻接页

2.4.1. InsertBuffer插入缓冲

2.4.1.1. 描述

InsertBuffer用于解决非聚族索引(辅助索引)的插入问题。对聚族索引(主键索引)的插入,可以通过提前保存最后一个主键位置来实现复杂度为O(1)的操作(在这里补充一下,聚族索引的插入,会把主键相邻的数据存放在相邻的磁盘上,因为主键索引负责实际的数据保存嘛~);但是对辅助索引的插入,不可能通过记录上一次插入的位置来实现(主键直接递增插入就行了,插入的主键肯定是上一个+1,但是辅助索引可不是,新添加的辅助索引的值和上一个没有任何关系,时间等特殊类型除外)。所以对于辅助索引的插入操作,往往需要一个IO随机读来实现,这往往需要数次IO

为了解决这个问题,InnoDB设置了InsertBuffer,插入缓冲不仅仅存在于内存区域,还存在于磁盘上,其中,磁盘上的用作持久化保存,缓冲区中的用于快速操作。引入插入缓冲后,每一次对于辅助索引的插入操作,都会先判断需要插入的辅助索引页在不在缓冲区(辅助索引页是一个数据结构,里面包含了好多个辅助索引,它是辅助索引树这颗B+树(这棵树在磁盘上)的叶子结点,每次插入索引都是插入到这个索引应该在的辅助索引页中,通过B+树算法计算出这个辅助索引应该放在哪个页中);如果在,那好办,直接插入,然后等待刷新到磁盘;如果不在,那么需要插入到InsertBuffer这个数据结构中,再通过一定的频率和情况,把InsertBuffer中的辅助索引刷新到它们对应的辅助索引页中。此时多个插入操作变成了一个刷新磁盘的操作。

想要使用InsertBuffer,需要保证两个前提

  • 索引必须是辅助索引。
  • 索引没有被设置为unique。

关于第二点稍微提一下,如果索引被设置为unique,就会导致每次插入时先检查索引是否是唯一的,这又会触发随机读IO来遍历这个索引的B+树(它在磁盘中)进行判断,而这正是InsertBuffer想要避免的,所以才有此要求。

2.4.1.2. ChangeBuffer

InnoDB在后面提供了对于DML(增删改)操作的支持,提供了三个缓冲区:InsertBuffer, DeleteBuffer, PurgeBuffer。

2.4.1.3. InsertBuffer内部实现

InnoDB使用一颗全局B+树维护所有表的辅助索引,用来实现InsertBuffer操作,它的非叶子结点结构如下:

space(4)marker(1)offset(4)

其中,space指出这是哪张表的,offset指出页的偏移量,用作遍历查找使用,这棵树的key是(space, offset)决定的,也就是说,同一张表,搜索键的值就是offset的值。

再来看看它的叶子结点:

space(4)marker(1)offset(4)metadata(4)real-index-page(x)

其中,metadata保存每个索引进入InsertBuffer的顺序等信息,而后面的就是实际数据(结构和辅助索引页一样,记录插入到这个页中来)。为了保证每次插入的必须成功,需要引入bitmap记录每个辅助索引页的可用空间,如果空间不够则会触发合并操作。

2.4.1.4. 合并InsertBuffer

如果需要插入的记录应该插入的辅助索引页不在缓冲区中,那么需要插入到InsertBuffer这颗B+树中,那么何时进行合并操作呢?也就是何时把InsertBuffer中的数据插入到磁盘上的辅助索引B+树中呢?一般来说,有以下三种触发:

  • 辅助索引页因为select语句被读取到缓冲区。
  • bitmap发现InsertBuffer中的某个页可用空间不足。
  • MasterThread的定期操作。

image.png

进行select时,会把磁盘中的某个辅助索引页加载到缓冲区,如果此时通过bitmap发现这个辅助索引有记录存放在InsertBuffer中,就会把InserBuffer中的记录添加到这个辅助索引中,等待它被写回。可以看到,对该页的多次插入记录由于InsertBuffer的存在而变成了一次,因此性能大大提高(至于B+树的维护,那个另说)。

而如果bitmap检测到某个InsertBuffer中的页马上满了,就会强制从磁盘读取对应的辅助索引页,然后把InsertBuffer中应该插入该页的记录和待插入的记录插入到辅助索引页中,等待它被写回

MasterThread为了公平性会随机选择页进行合并插入操作。

2.4.2. 两次写

两次写本质上是为了保证在发生宕机时,保证数据一致性。重做日志记录的是对页的物理操作,而如果页本身已经发生了损坏,那么进行重做就没意义了。

doublewrite由两个部分组成,一个是缓冲区中的2MB的buffer,一个是磁盘上的2MB空间(连续分配的)。每次对脏页回写时,会先把脏页复制到doublewrite的buffer中,然后再每次1MB地把buffer写入到磁盘上,最后刷新buffer里的脏页。

4EF57A91-B2E7-418A-8AD6-31DADA5F5B56_1_105_c.jpeg

2.4.3. 自适应哈希索引

对于一些热点索引的查询,InnoDB会自动建立哈希索引,方便快速的索引,而不是遍历整个B+树,哈希索引的值是索引列或查询键的值,哈希索引的值是B+树的叶子结点,这通常是一个索引页结构。

2.4.4. 异步IO

为了提高磁盘操作性能,一般使用异步IO操作,这一般是OS提供的。

2.4.5. 刷新邻接页

当InnoDB刷新某个页时,会顺便检查这个页所在的区(磁盘划分格式,比页大一些,一个区包含很多页)内的其他页,如果有脏页,则一块刷新,这样可以利用顺序IO将多个IO操作合并到一个。