专栏地址:
文章目录
1. 整体架构
InnoDB存储引擎的结构主要分为两部分:内存结构和磁盘结构,前者位于MySQL的进程内,后者位于文件系统中。此外,还有一组后台线程负责刷新内存中的数据。
内存结构主要包含:
- 缓存池 Buffer Pool
- 写缓存 Change Buffer
- 自适应哈希索引 Adaptive Hash Index
- 日志缓存 Log Buffer
1.1 内存结构
1.1.1 缓存池 buffer pool
InnoDB利用内存来弥补硬盘速度对数据库整体性能的影响。缓存池中的数据页类型有:索引页、数据页、undo页、change buffer写缓存等信息。InnoDB在读取和修改页的时候,都会首先尝试修改缓存中的页,同时利用重做日志redo log和checkpoint机制,异步地将脏页刷新落盘。
在MySQL服务器上,一般将高达70%-80%的内存用于InnoDB缓存。
缓存池的LRU算法
缓存池中的页主要来源于:查询读取的页面和后台线程预读的页面,使用优化的LRU(Least Recently Used,最近最少使用)算法对缓存页进行管理。
传统的LRU算法,最新加入的数据被放置在链表的头部,同时将末尾超出容量的数据淘汰掉。当链表中的数据被命中时,将其移动到首部。这样,保证了最近被访问和访问最频繁的数据位于链表的首部,使用最少的数据会先被淘汰掉。但其缺点主要在于无法解决预读失效和缓存池污染问题:
- 预读失效
预读的页并没有被访问过,需要尽快地从LRU列表中被驱逐。 - 缓存池污染
某些SQL会扫描大量的行,可能会将缓存池中大部分甚至所有的页替换出去。而这些页可能仅仅是这次查询需要的,而被替换出去的是热数据。会造成缓存池性能的急剧下降。
InnoDB缓存池引入了Midpoint,将LRU列表分为 new sublist(young区)和 old sublist(old 区),默认分别占据5/8和3/8的空间。新加入的页被放置在Midpoint处,当其被访问且在old sublist列表中的停留时间超过阈值(默认1秒)时,晋升到new sublist的头部,称之为page made young。同时,new sublist尾部的数据页会被挤到old sublist,称之为page become old。最终,old sublist尾部超出容量的数据页会被最终淘汰。这样,预读失效的页和会造成缓存池污染的页,只会在old sublist中停留,new sublist中真正的热数据并不会被替换出去。
预读
InnoDB会异步将可能被访问的页面预先读取到缓存池中。预读分为:线性预读和随机预读。线性预读将下一个区的页读取到缓存池中,随机预读将当前区剩余的页读取到缓存池中。
脏页刷新
InnoDB会在以下情况将缓存池中的脏页刷新到磁盘:
- 系统空闲时
- 缓存池空间不足时
- redo log 空间不足时
脏页刷新由Page Cleaner Thread线程负责。
1.1.2 change buffer 写缓存
InnoDB利用缓存池buffer pool来加速数据页的访问速度,对数据页的修改也会先在缓存池中进行更新,并在随后适当的时候对脏页进行刷新。当待修改的页不在缓存,若将页先从磁盘中读取到缓存池中再进行修改,会产生较高的随机IO开销。为此,InnoDB引入了change buffer,将对辅助索引(二级索引)的操作缓存下来,以此减少随机读IO,并达到操作合并的效果。
例如,当需要更新一个辅助索引的叶子节点时,如果
- 该数据页在缓存池中,则直接更新缓存中的页,并在随后进行脏页刷新。
- 该数据页不在缓存池中,InnoDB会将这些操作缓存在change buffer中,在下次读入该数据页时,在进行merge操作。
Change Buffer支持insert、update、delete操作,不仅存在于缓存池中,还存储在系统表空间中(以普通B+树的方式),缓存的对象是:
- 普通的辅助索引的叶子节点,聚集索引不可(PS:聚集索引的插入一般为顺序IO);
- 唯一索引的删除操作,由于需要查询数据页以判断唯一性,所以不能缓存插入操作。
merge
将Change Buffer中的操作应用到相应的数据页的操作称之为merge,触发的时机有:
- 查询访问了相应的数据页
- Master Thread会定期执行merge操作
merge的执行流程:
- 从磁盘读取原始的数据页到缓存池中
- 从change buffer中找到该数据页的change buffer记录,可能有多条,依次应用
- 写redo log,包括数据页的变更和change buffer的变更
- 后续的脏页刷新
适应场景
对同一个数据页积累的操作越多,change buffer进行操作合并的收益越大。其主要适用于:写多读少场景,譬如账单、日志类应用,数据页在写完后被立即访问的概率比较小。相反的,如果一个业务的更新模式是写入后立刻进行查询,那么随机IO次数不仅不会减少,反而增加了change buffer的维护代价。
Change Buffer的崩溃恢复
在事务提交的时候,Change Buffer中的操作会记录到redo log中,在发生意外故障后,可以利用redo log进行崩溃恢复。
Change Buffer 结构图
Change Buffer思维导图
1.1.3 自适应哈希索引 AHI
一般情况下,B+树的高度为2-4层,故需要2-4次IO才可以定位到目标数据页。譬如,使用默认自增主键,每条记录在1KB的情况下,一颗3层高的聚集索引B+树大概可以存储2000万条左右的数据。而哈希索引的时间复杂度在O(1),一般仅需一次IO即可定位到数据。
为了减少重复的寻路开销,InnoDB会自动根据访问模式(查询条件)和频率来对热点页建立哈希索引,以便快速定位到叶子节点。其key由查询条件中索引的前N列构成,value是叶子节点的位置。AHI是内存结构,可以看作是索引的索引。
建立AHI的前提是:
- 对一个页的连续访问模式相同
- 页以该模式访问的次数达到以一定的阈值
AHI的适应场景:
- 等值查询
- 查询条件中前缀索引的列数大于AHI中的列数
在高并发下,AHI可能会成为竞争资源。若查询模式无法从AHI中获益,可考虑关闭AHI以减少不必要的性能开销。
1.2.4 log buffer 日志缓存
log buffer主要对重做日志redo log进行缓存,一般不需要设置太大,Master Thread 每秒会对redo log进行落盘,在事务提交或者log buffer空间过小时也会进行刷新。
详见日志系统章节
1.2 硬盘结构
存储在硬盘上的结构主要有:
- 表空间
- 系统表空间(共享表空间):数据字典、double write buffer、change buffer、undo log
- 独占表空间:存储每张表的数据和索引
- undo表空间:undo表空间包含undo log撤销记录的集合
- 临时表空间:用户创建的临时表
- redo log
当数据库意外宕机时,InnoDB存储引擎会利用重做日志进行故障恢复.InnoDB存储引擎至少有1个重做日志文件组,每个组下至少有2个重做日志.日志组中的每个重做日志文件大小一致,并以循环追加写的方式运行.
详见事务章节
1.3 后台线程
InnoDB存储引擎是一个多线程架构,其后台线程主要有:Master Thread、IO Thread、Page Cleaner Thread和Purge Thread。
1.3.1 Master Thread
InnoDB的主要工作都是在Master Thread中完成的,其线程优先级最高。Master Thread内部有多个循环组成:主循环、后台循环、刷新循环、暂停循环。其主要工作有:
- redo log的刷盘
- merge change buffer (合并写缓存)
脏页刷新和undo 页的回收现由Page Cleaner Thread和Purge Thread负责。
1.3.2 IO Thread
InnoDB使用了大量的AIO,IO Thread的主要工作是处理这个IO请求的回调,主要有:
- write thread
- read thread
- insert buffer thread
- log thread
每个类型的IO Thread都有多个。
1.3.3 Page Cleaner Thread
Page Cleaner Thread主要负责将缓存池buffer pool中的脏页刷新到磁盘中。
1.3.4 Purge Thread
负责收回undo log页以及数据页空间。
由于InnoDB支持MVCC,所以对记录的删除操作、undo log的回收操作等不能立刻进行,而是需要在这些旧版本没有被事务引用时再进行清理操作。
update undo log会按照顺序放到history list中,后台Purge Thread会在定期扫描,清理无用的undo log。另外,还会对标记为删除行记录(delete flag)进行彻底的删除,即该记录占用的空间放入PAGE_FREE链表中,以便复用。
insert undo log,由于只对当前事务生效,所以在事务提交后可以直接删除,不需要purge操作。
2. InnoDB的关键技术
InnoDB的关键技术主要有以下5个:
- 写缓存 Change Buffer
- 两次写 Double Write
- 自适应哈希索引 Adaptive Hash Index
- 异步IO Async IO
- 刷新邻接页 Flush Neighbor Page
2.1 Double Write
Double Write主要是为了提升数据页可靠性,防止部分写失效导致的数据丢失。因为InnoDB数据页一般为16K,而文件系统的页大小为4K,所以操作系统可能无法保证InnoDB数据页的原子写入。倘若在刷新页的过程中,服务器宕机了,则会导致原始数据页的损毁。重放redo log也无法解决部分写失效问题,因为重做日志记录的是对数据页的修改操作,如果这个数据页本身就是损坏的,对其应用redo log中的修改也是没有意义的。
InnoDB的解决方案是:拷贝一份数据页的副本,即Double Write。当脏页要刷新时,Doube Write的主要流程是:
- 先将Buffer Pool中的脏页拷贝到Double Write Buffer中。
- 将Double Write Buffer以顺序追加写的方式实时刷盘(系统表空间),同步IO。
- 再将Double Write Buffer中的页写到相应的表空间,此时为随机IO,异步IO。异步IO回调函数中会进行完整性校验。
Double Write Buffer不仅仅存在于内存中,是一个内存/磁盘的两层结构
当发生了部分写失效时,可以通过系统表空间的Double Write Buffer进行恢复,随后再应用redo log重做日志,完成完整的故障恢复流程。
如果文件系统能够提供页大小的原子写入,提供防范部分写失效的解决方案,那么可以关闭Double Write。
2.2 异步IO
InnoDB利用AIO来提高磁盘操作性能,同时还会进行IO Merge操作,将多个IO操作合并为1个IO操作。
2.3 刷新邻接页 Flush Neighbor Page
InnoDB在进行脏页刷新时,会检测该页所在区下面的所有页,如果也是脏页,那么一并刷新。这样可以减少IO次数。但是可能存在:
- 将不怎么脏的页刷新了,该页立马又变脏了
- SSD是否可以考虑关闭
参考
InnoDB 存储引擎—MySQL官网
《MySQL技术内幕(InnoDB存储引擎)》
《MySQL实战45讲》 极客时间
《InnoDB架构,一幅图秒懂!》 微信公众号-架构师之路
InnoDB缓存池