InnoDB存储引擎 (第2章 InnoDB存储引擎)
2.1 InnoDB存储引擎概述
从MySQL5.5开始是默认的存储引擎.
第一个完整支持ACID事务的MySQL存储引擎,特点是行锁设计,支持MVCC,支持外键,提供一致性非锁定读
2.2 InnoDB存储引擎的版本
期版本随着MySQL的版本更新而更新.MySQL5.1开始允许存储引擎开发商以动态方式加载引擎.
MySQL5.1中支持两个版本的InnoDB,一个是静态编译的InnoDB,一个是以动态方式加载的InnoDB ,称之为InnoDB Plugin(1.0.x) ;
2.3 InnoDB体系架构
从图中可以看出InnoDB存储引擎有多个内存块,可以认为这些内存块组成了内存池,负责如下工作:
1.维护所有进程/线程需要访问的多个内部数据结构
2.维护磁盘上的数据,方便快速读取,同时在对磁盘文件修改之前在这里缓存
3.重做日志(redo log)缓冲 ...
后台线程的主要作用是 :
刷新内存池中的数据,保证缓冲池中的数据是最新的 ;
已修改的数据文件刷新到磁盘文件;
2.3.1 后台线程
InnoDB是多线程的存储模型.
1. Master Thread
非常核心的一个线程,负责将缓冲池中的数据异步刷新到磁盘,保证数据一致性, 包括脏页的刷新,合并插入缓冲, UNDO页的回收等.
2. IO Thread
InnoDB中大量使用了AIO(Async)来处理写IO请求. 而IO Thread线程主要负责这些IO请求的回调处理.
InnoDB1.0版本之前有四个IO Thread,分别为write, read, insert buffer,log IO Thread.
Linux平台不能调整,但是Windows可以通过innodb_file_io_threads来增大IO Thread.
从1.0版本开始,write和read线程增加到了四个,并且去掉了innodb_file_io_threads参数,而使用
innodb_read_io_threads和innodb_write_io_threads参数进行设置.
show variables like 'innodb_version'; show variables like 'innodb%threads'; (mysql8)
观察InnoDB中的IO Threads:
show engine innodb status;
如下:
ILE I/O
--------
I/O thread 0 state: wait Windows aio (insert buffer thread)
I/O thread 1 state: wait Windows aio (log thread)
I/O thread 2 state: wait Windows aio (read thread)
I/O thread 3 state: wait Windows aio (read thread)
I/O thread 4 state: wait Windows aio (read thread)
I/O thread 5 state: wait Windows aio (read thread)
I/O thread 6 state: wait Windows aio (write thread)
I/O thread 7 state: wait Windows aio (write thread)
I/O thread 8 state: wait Windows aio (write thread)
I/O thread 9 state: wait Windows aio (write thread)
.....
可以看到:
I/O thread 0 是insert buffer thread,
I/O thread 1 是 log thread,
并且读线程的ID总是小于写线程的ID ;
3. Purge Thread
show variables like 'innodb_purge_tshreads';
- - 4
清洗线程;
事务被提交之后,undolog可能已经不需要了,需要回收已经分配使用的undo页.
可以通过在配置文件增加innodb_purge_threads=x来启用独立的purge thread
4. Page Cleaner Thread
是InnoDB1.2.x版本引入的,
作用是将之前版本中 脏页的刷新操作都放入到单独的线程中来完成;
目的是为了减轻MasterThread的工作及对于用户查询线程的阻塞,进一步提高性能;
2.3.2 内存
1. 缓冲池
CPU速度和磁盘速度之间通过缓冲池技术来提高数据库的整体性能;
缓冲池简单来说是一块内存区域;
页从缓冲池刷新回磁盘是通过checkpoint机制来实现的
大小可以通过innodb_buffer_pool_size来设置
show variables like 'innodb_buffer_pool_size';
缓冲池缓存的数据类型:
索引页, 数据页, undo页, 插入缓冲, 自适应哈希索引, InnoDB存储的锁信息, 数据字典信息等.
从InnoDB1.0开始允许多个缓冲池实例.可以通过innodb_buffer_pool_instances设置.
show variables like 'innodb_buffer_pool_instances';
2. LRU List,Free List和Flush List
数据库中的缓冲池是通过LRU算法来进行的. 缓冲池中的页默认大小为16kb.
跟其他的LRU算法相比,InnoDB做了一些优化:
加入了midpoint位置, 新读取的页,虽然是最新访问的页,但是不会直接放入LRU的首部,而是放入midpoint位置.这个算法在InnoDB中称为midpoint insertion strategy(中点插入策略)
默认在5/8处,大概37%.这个位置可以通过innodb_old_blocks_pct控制.
show variables like 'innodb_old_blocks_pct';
midpoint之后的称为old列表,之前的称为new列表,
那为什么不直接用朴素的LRU算法呢?
如果直接放入首部,那么可能某些sql操作会使得缓冲池中的数据被刷出.常见的这类操作为索引或数据的扫描操作.这些操作需要使用内存中的所有页,会使得数据被从缓冲池中刷出.热点数据等到需要访问时需要再次读取.
为了解决这个问题引入了另外一个参数innodb_old_blocks_time,即读到mid位置之后多久才会被放入LRU列表的热端,尽可能使LRU列表中的热点数据不被刷出.
LRU列表用来管理已经读取的页,但是在启动时是空的,需要从Free List中查询是否有可用空闲的页,
如果有就从Free List中取,取出该页后从Free List中删除,
如果没有没有则淘汰LRU列表中的末尾的页,内存空间分配给新的页;
当页从old列表进入new列表时的操作称为page made young,从old列表进入new列表失败时称为page not made young.
通过 show engine innodb status; 可以查看LRU List和Free List的使用情况和运行情况.
Buffer pool size 512 页数
Free buffers 252 当前Free列表中页的数量
Database pages 256 LRU列表中页的数量
Pages made young 0, not young 0 LRU列表中页移动到前端的次数,
....
show engine innodb status; 表示的是过去一段时间内innodb的状态
Per second averages calculated from the last 51 seconds , 表示过去51秒中innodb数据状态;
这里有一个非常重要的观察变量: buffer pool hit rate,表示缓冲池的命中率,这个值不应该小于95%,如果小于应该考虑是全表扫描引起的LRU列表被污染的情况.
InnoDB从1.0.x版本开始支持压缩页的功能,即原来的16KB压缩为1KB,2KB,4KB,8KB.对于非16KB的页是通过unzip_LRU列表进行管理的.
在页上的数据被修改之后,该页被称为脏页,
Flush列表中的页都是脏页.
需要注意的是LRU List和Flush List中都存在脏页 .
LRU列表用来管理缓冲池中页的可用性,
Flush列表用来管理将页刷新回磁盘, 二者互不影响.
Modified db pages 0 表示脏页数量;
3.重做日志缓冲 (redo log buffer)
先将重做日志放入缓冲区中,然后再刷新回 redo log日志中.不需要设置的太大.
通过 innodb_log_buffer_size 来控制,默认大小为 8MB, (mysql8 里面默认16MB).
show variables like '%innodb_log_buffer_size%';
三种情况会刷新重做日志缓冲:
a.Master Thread每秒刷新一次
b.每个事务提交时会刷新
c.重做日志缓冲池空闲空间小于1/2时
4.额外的内存池
在InnoDB存储引擎中,对内存的管理是通过一种称为内存堆的方式进行的.
在对一些数据结构本身的内存进行分配时,需要从额外的内存池中进行申请;
该区域的内存不够时,会从缓冲池中进行申请;
2.4 Checkpoint技术
页的操作首先是在缓冲池中完成的, 缓冲池的页版本比磁盘新,数据库需要将新的版本的页刷新到磁盘;
事务数据库普遍采用Write Ahead Log策略, 即当事务提交,先写重做日志,再修改页; 如果发生宕机而导致数据丢失,通过重做日志来完成数据的恢复,(即持久性Durability);
当通过重做日志来恢复数据时会有两个问题:
1.缓冲池无法缓存数据库中的所有数据
2.重做日志无法做到无限大
checkpoint作用:
-
缩短数据库的恢复时间
-
缓冲池不够用时,将脏页刷新到磁盘
-
重做日志不可用时,刷新脏页
当发生故障时不需要应用所有的重做日志,因为checkpoint之前的已经刷新回磁盘了,只需要刷新checkpoint之后的重做日志.
此外缓冲池不够用时, LRU算法会溢出最近最少使用的页,如果这个页是脏页,那么需要强制执行checkpoint将脏页刷新回磁盘.
对于InnoDB来说,通过LSN(Log sequence number)来标记版本的,而LSN是8字节的数字,单位为字节.每个页有LSN, 重做日志也有LSN,checkpoint也有LSN.
可以通过show engine innodb status;
checkpoint所做的事情就是将脏页刷新回磁盘.
有两种checkpoint:
1.Sharp Checkpoint
发生在数据库关闭时将所有的脏页刷新回磁盘,这是默认的工作方式,
即innodb_fast_shutdown=1
2.Fuzzy Checkpoint (模糊)
但是如果运行时每次都把脏页刷新到磁盘,那么性能开销非常的大,所以会使用Fuzzy Checkpoint (模糊检查点) 来处理,而不是刷新所有的脏页 ;
Fuzzy Checkpoint情况:
-
Master Thread Checkpoint
差不多以每秒或每十秒的速度从缓冲池的脏页列表中刷新一定比例的页回磁盘, 异步;
-
FLUSH_LRU_LIST Checkpoint
Innodb需要保证LRU列表中需要保证LRU列表中差不多100个空闲页可以使用;
如果没有100个空闲页可用.则会将LRU列表尾端的页移除,如果有脏页则需要checkpoint;
-
Async/Sync Flush Checkpoint
指的是redo日志不可用的情况,这时需要强制将一些页刷新会磁盘,
-
Dirty Page too much Checkpoint
脏页数量太多,innodb强制进行checkpoint, 目的为了保证缓冲池有足够可用的页;
参数控制:
show variables like 'innodb_max_dirty_pages_pct';
例如 value= 90.000000 表示,当脏页数量90%时,强制执行 ;
2.5 Master Thread工作方式
InnoDB存储引擎的主要工作都是在一个单独的后台线程master thread中完成的。
2.5.1 InnoDB 1.0.x版本之前的Master Thread
master thread的线程优先级别最高,其内部由几个循环组成:主循环、后台循环、刷新循环、暂停循环。 master thread会根据数据库的运行状态在 loop、background loop、flush loop和suspend loop中进行切换。
loop:主循环
大多数的操作都在这个循环中。其中有两大部分操作:每秒的操作和每10秒的操作。
主要通过thread sleep来实现,负载很大情况会有延迟;
每秒操作包括:
-
日志缓冲刷新到磁盘,即使这个事务还没有提交(总是)。
即使某个事务还没有提交,Innodb存储引擎仍然会每秒将重做日志缓冲中的内容刷新到重做日志文件,因此再大的事务commit的时间也很快。
-
合并插入缓冲(可能)。
InnoDB存储引擎会判断当前1秒内发生的IO次数是否小于5次,如果小于5次,InnoDB认为当前的IO压力很小,可以执行合并插入缓冲的操作。
-
至多刷新100个InnoDB的缓冲池中的脏页到磁盘(可能)。
当前缓冲池中脏页的比例如果超过配置的阈值,则将100个脏页写入磁盘。
-
如果当期没有用户活动,切换到background loop(可能)。
每10秒的操作:
-
刷新100个脏页到磁盘(可能)。
InnoDB存储引擎先判断过去10秒内磁盘的IO操作是否小于200次,如果是,才将100个脏页刷新到磁盘。
-
合并至多5个插入缓冲(总是)。
-
将日志缓冲刷新到磁盘(总是)。
-
删除无用的undo页(总是)。
-
刷新100个或10个脏页到磁盘(总是)。
判断缓冲池中脏页的比例;
-
产生一个检查点(总是)。
background loop:后台循环。
若当前没有用户活动(数据库空闲时)或者数据库关闭时,就会切换到这个循环。
包含操作:
-
删除无用的undo页(总是)
-
合并20个插入缓冲(总是)
-
跳回到主循环(总是)
-
不断刷新100个页,直到符合条件(可能,跳到flush loop中完成)
如果flush loop中也没有事情可做了,InnoDB存储引擎会切换到suspend loop,将master thread挂起,等待事件的发生。
2.5.2 InnoDB1.2.x版本之前的Master Thread
master thread潜在问题:
随着固态硬盘的普及,IO速度提高,硬编码中InnoDB最多刷新100个脏页、合并20个缓冲会限制性能。
-
新增了innodb_io_capacit,,按其值百分比刷新对应数量的页。
-
innodb_max_dirty_pages_pct:脏页占缓冲池的比例,75到80较好。
2.5.3 InnoDB1.2.x版本的Master Thread
进行了优化,分离出一个单独的Page Cleaner Thread来进行刷新脏页的操作;
2.6 InnoDB关键特性
插入缓冲(insert buffer)、两次写(double write)、自适应哈希索引(apaptive Hash Index),异步IO (Async IO) ,刷新邻接页(Flush Neighbor page)
2.6.1 插入缓冲
1. Insert buffer
(插入缓冲)并不是缓冲池的一个部分,它和数据页一样,是物理页的一个组成部分。
为什么产生(解决的问题)?
主键是行唯一的标识符,在应用程序中记录的插入顺序是按照主键递增的顺序进行插入的。因此,插入聚集索引一般是顺序的,不需要磁盘的随机读取。因此,这样的情况下,插入操作一般很快就能完成。
注意: 不是所有的主键插入都是顺序的,如果是UUID这样的,那么插入和辅助索引一样,是随机的;
但是,不可能每张表上只有一个聚集索引,在更多情况下,一张表上有多个非聚集的辅助索引。
比如:
create table t_insert_buffer (
id int auto_increment,
name varchar(30),
primary key (id),
key(name)
)
对于主键id数据页是顺序存放的,但是对于非聚集索引叶子节点的插入不再是顺序的,需要离散访问非聚集索引页,随机读取的存在导致插入操作性能下降; 插入缓冲就是用来解决这个问题的。
某些情况下,辅助索引的插入依然是顺序的或者说比较顺序的,比如时间字段,插入时根据时间的递增而插入的;
原理过程 :
对于非聚集索引的插入和更新操作,不是每一次都直接插入索引页,而是先判断插入的非聚集索引页是否在缓冲池中,如果在就直接插入,如果不在就放入到一个insert buffer对象中,好似欺骗数据库这个非聚集索引已经插入到叶子节点了。然后再以一定的频率和情况进行 insert buffer和非聚集索引页子节点的merge操作。
适用场景:
插入缓存的使用需要满足以下两个条件(也就是非唯一的辅助索引):
索引是辅助索引;
索引不是唯一的。(因为插入缓冲时,数据库并不会查找索引页来判断插入的记录的唯一性)
show engine innodb status ; 查看Insert buffer;
例如(个人本机):
Ibuf: size 1, free list len 0, seg size 2, 0 merges
size 代表了已经合并记录页的数量;
seg size 显示当前 insert buffer大小 2*16KB,
free list len 代表空闲列表的长度
merges 代表合并的次数,也就是实际读取页的次数
存在的问题:
在写密集的情况下,插入缓冲会过多的占用缓冲池内存,默认情况下最大可以占用1/2的缓冲池内存。
2. change buffer
innodb从1.0.x版本开始引入了change buffer,可以将其视为insert buffer升级;
innodb可以对DML操作-INSERT,DELETE,UPDATE都进行缓冲,分别是Insert buffer,Delete buffer,purge buffer;
change buffer适用对象依然是非唯一的辅助索引;
对一条记录进行UPDATE操作可能分为两个过程:
-
将记录标记为已删除 (delete buffer)
-
真正删除 (purge buffer)
查看开启各种buffer参数:
show variables like 'innodb_change_buffering';
默认all;
查看change buffer最大使用内存的数量:
show variables like 'innodb_change_buffer_max_size';
默认25,即最多使用1/4的缓冲池空间,上限最大有效值是50;
例如下面参数:
-------------
merged operations:
insert 0, delete mark 0, delete 0
discarded operations:
insert 0, delete mark 0, delete 0
....
上面显示合并操作和被丢弃的操作,
insert表示Insert buffer,
delete mark表示delete buffer,
delete表示Purge buffer,
discarded operations: 表示当Change buffer发生merges时,表已经被删除,此时无需再将记录合并到辅助索引中了;
3. insert buffer的内部实现
数据结构是一棵B+树;
现在的版本中,全局只有一个Insert buffer b+树,负责所有的表的辅助索引进行Insert Buffer,存放在共享表空间中,默认也就是ibdata1中;
Insert buffer b+树,由叶节点和非叶节点组成,非叶节点存放的是查询的search key(键值),
search key占用9个字节,
space表示待插入记录所在的表的表空间id,占4个字节;
marker占用1个字节,用来兼容老版本insert buffer;
offset表示页所在的偏移量,占用4个字节;
辅助索引插入到页(space,offset):
如果页不在缓冲池中,innodb根据规则构造一个search key,然后查询insert buffer b+树, 然后将这条记录插入到b+树的叶子节点中;
IBUF_REC_OFFSET_COUNT是保存两个字节的整数,用来排序每个记录进入insert buffer的顺序;
上面叶子节点中从第5列开始就是实际插入记录的各个字段,所以叶子节点记录需要额外13个字节的开销(9+4);
特殊页 Insert buffer bitmap:
用来标记每个辅助索引页(space,page_no)的可用空间;
每个 Insert buffer bitmap 用来追踪16384个辅助索引页,也就是256个区(Extent);
每个 Insert buffer bitmap 页 都在16384个页的第二个页中,
每个辅助索引页在 Insert buffer bitmap 页中占用4位(bit),由表2-3中的三个部分组成;
4. merge insert buffer(重点)
合并操作的发生情况:
-
辅助索引页被读取到缓冲池时;
例如执行select 查询操作需要检查Insert buffer bitmap页,是否存放在Insert buffer B+树中;
若有,则将Insert buffer B+树中该页的记录插入到该辅助索引页中;
对该页的多次的记录操作通过一次操作合并到了原有的辅助索引页中,性能大幅度提高;
-
Insert buffer Bitmap页追踪到该辅助索引页已无可用空间时;
辅助索引页至少有1/32页的空间,如果插入辅助索引记录时,检测到小于1/32页,则会强制进行一次合并操作;即强制读取辅助索引页,将插入缓冲中该页的记录及待插入的记录插入到辅助索引页中;
-
master Thread
主线程每秒或每10秒会进行一次Merge insert buffer的操作;
innodb会随机选择insert buffer b+树的一个页,读取该页中的space和之后所需要的数量的页,进行merge;
2.6.2 两次写
插入缓冲带给InnoDB存储引擎性能,两次写带给InnoDB数据的可靠性。
当数据库宕机时,可能发生数据库正在写一个页面,而这个页只写了一部分的情况,我们称之为部分写失效。
可能会想,如果发生写失效,可以通过重做日志进行恢复。但是需要知道,重做日志中记录的是对页的物理操作,如果这个页本身已经损坏,在对其进行重做是没有意义的。这就是说,在应用重做日志前,我们需要一个页的副本,当写入失效发生时,先通过页的副本来还原该页,再重做日志,这就是两次写。
doublewrite由两部分组成:
一部分是内存中的doublewrite buffer,大小为2MB;
另一部分是物理磁盘上共享表空间中连续的128个页,即两个区,大小同样为2MB。
原理过程:
当缓冲区的脏页刷新时,并不直接写磁盘,而是通过memcpy函数将脏页先拷贝到内存中的doublewrite buffer。之后通过doublewrite buffer再分两次,每次写入1MB到共享表空间的物理磁盘上,然后马上调用fsync函数,同步磁盘,避免缓冲写带来的问题。在这个过程中,因为doublewrite页是连续的,因此这个过程是顺序写的,开销并不是很大。在完成doublewrite页的写入后,再将doublewrite buffer中的页写入到各个表空间文件中,此时的写入则是离散的。
如果操作系统在将页写入磁盘的过程中崩溃了,在恢复过程中,InnoDB存储引擎可以从共享表空间中的doublewrite中找到该页的一个副本,将其拷贝到表空间文件,再应用重做日志。
观察两次写运行情况命令:
show global status like 'innodb_dblwr%' ;
上面是写了多少页数,下面是实际写入次数;
2.6.3 自适应哈希索引
哈希是一种非常快的查询方式.复杂度为o(1).而B+树的查找次数,取决于B+树的高度,生产环境一般为3-4层
InnoDB存储引擎会监控对表上索引的查找,如果观察到建立哈希索引可以带来速度的提升,则建立哈希索引,所以称为自适应的。
自适应哈希索引通过缓冲池的B+树构造而来,因此建立的速度很快,而且不需要将整个表都建哈希索引,InnoDB存储引擎会自动根据访问的频率和模式来为某些页建立哈希索引。
需要注意的是,哈希索引只能用来搜索等值的查询,而对于其他类型,如范围查找,是不能使用的。
AHI 相关的参数
innodb_adaptive_hash_index,
innodb_adaptive_hash_index_parts,
其中 innodb_adaptive_hash_index 为动态调整的参数,用以控制是否打开 AHI 功能,关闭它以节省不必要的性能开销;
innodb_adaptive_hash_index_parts 是只读参数,在实例运行期间是不能修改,用于调整 AHI 分区的个数(5.7.8 引入),减少锁冲突,
查看命令: show variables like '%innodb_adaptive_hash_index%'
从MySQL 5.7开始,自适应哈希索引搜索系统是分区的。每个索引都会绑定到一个特殊的分区上,并且每个分区都由各自独立的锁存器来保护。分区受到innodb_adaptive_hash_index_parts配置项的控制。在MySQL5.7之前,自适应哈希索引搜索系统是通过一个单独的锁存器来保护,在高负载的情况下它会变成竞争点。innodb_adaptive_hash_index_parts选项默认值为8,最大值为512。
2.6.4 异步IO
异步 IO(Asynchronous IO-AIO)方式处理磁盘操作能提高磁盘操作性能。
查看系统是否支持Native AIO:
show variables like '%innodb_use_native_aio%' ;
Native AIO 跟操作系统有关,wins 和 linux支持,mac osx不支持;
Innodb中,read ahead方式的读取,脏页的刷新,即磁盘的写入操作全部是AIO完成;
2.6.5 刷新邻接页
原理:当刷新一个脏页时,会自动检测邻近的页(所在区extent所有的页),如果是脏页就进行刷新 ;
show variables like '%innodb_flush_neighbors%';
可以对参数innodb_flush_neighbors进行设置,设置为0,即关闭此特性;
传统机械硬盘建议启用该特性,对于固态硬盘有着超高的IOPS性能的磁盘,建议关闭此特性;
2.7 启动、关闭与恢复
innodb_fast_shutdown 影响InnoDB表关闭。该参数有0、1、2三个参数。
0 : MySQL关闭时 完成所有的full purge和merge insert buffer操作,耗时长,innodb升级时,必须将这个参数调为0,然后再关闭数据库;
1: 默认值 只将缓冲池内的一些脏页刷新至磁盘,不需要上面的full purge和merge insert buffer;
2: 将日志都写入日志文件不会有任何事务丢失,但下次启动时会进行recovery
innodb_force_recovery 影响整个innodb存储引擎的恢复状况,
该值默认为0,表示当需要恢复时,需要执行所有的恢复操作,当不能进行有效恢复时,如数据页发生了corruption,mysql数据库可能宕机,并把错误写入错误日志中。
用户也可以手动恢复对应的错误数据;
可以设置1-6,大的数字包含前面所有小的数字表示的影响;
1(SRV_FORCE_IGNORE_CORRUPT):忽略检查corrupt页 ;
2(SRV_FORCE_NO_BACKGROUND):阻止Master Thread 线程的运行,如master thread线程需要进行full purge操作,而这会导致crash ;
3(SRV_FORCE_NO_TRX_UNDO):不进行事务的回滚操作;
4(SRV_FORCE_NO_IBUF_MERGE):不进行插入缓冲的合并操作;
5(SRV_FORCE_NO_UNDO_LOG_SACN):不查看撤销日志,Innodb存储引擎会将未提交的事务视为已提交;
6(SRV_FORCE_NO_LOG_REDO):不进行前滚的操作;
需要注意的是:在设置了innodb_force_recovery>0后,用户可以对表进行select, create和drop操作,但是insert,update和delete这类DML操作是不允许的;
2.8 小结