本章是mysql技术内幕这本书的读书笔记。可以点击超链接查看所有读书笔记。
1. InnoDB体系结构
1. 基本介绍
基本结构如下图:
书上的图实在太简单了,因此我找了一副网上的图。其实绝大部分系统的体系结构归根结底都是对内存、线程的设计。
2. 后台线程
2.1 master thread(核心线程)
核心后台线程,负责将缓冲池中的数据异步刷新到磁盘,保证数据一致性。包括脏页刷新、合并插入缓存、UNDO页的回收等。
2.2 asynchronous IO Thread
可以看到图上也有标示出来。异步IO线程总共分为四类:
- insert buffer thread
- read thread
- write thread
- redo log thread
读写线程数的值可以通过命令来查看
show variables like '%io_thread%';
查看完整的AIO线程可以使用如下命令:
show engine innodb status
部分结果截图:
可以看到这里是4个读线程,4个写线程,1个插入缓存线程和1个日志线程
2.3 Purge Thread
从图上可以看到,该线程是配合master thread一起来工作的。
事务被提交后,undolog可能不再需要,因此需要PurgeThread来回收已经使用并分配的undo页。
该线程的数量可以在配置文件中配置:
[mysqld]
innodo_purge_threads=1
2.4 其他线程
书上没列举其他线程的功能,这个我会在后续再整理进来。
3. 内存
3.1 缓冲池
把内存充当缓存的含义应该是很清楚的。就是平衡CPU速度与磁盘速度之间的鸿沟,提高性能。
页从缓冲池刷新回磁盘的操作通过checkpoint的机制来刷到磁盘。提升数据库整体性能。
查看缓冲池大小
show variables like '%buffer_pool_size%';
缓存池的基本结构如下。
也可以查看最前面的体系结构图。最前面的体系结构图看样子是漏了数据页(data page),这里注意下。
innodb允许多个缓冲池实例来减少数据库内部竞争。缓冲池实例个数可以使用:
show variables like '%buffer_pool_instances%';
同理要修改的话,也可以在配置文件中修改该变量值。多例化后通过show engine innodb status可以看到2个buffer pool的信息。
3.2 LRU List、Free List和Flush List(重要)
3.2.1 基本过程
- 数据库刚启动,空闲页都在Free List当中
- 需要缓冲页时,从Free List删除页,然后添加到LRU List;如果没有空闲页则将LRU尾端页淘汰,并分配给新页
- 如果LRU List当中的页被修改了,则会产生脏页。此时缓冲池数据与硬盘不一致,该页会存在于Flush List当中。
3.2.2 改进的LRU算法——midpoint insertion strategy
InnoDB的LRU算法是做过改进的。新读取到的页并不是放到LRU列表的首部(首部是使用最频繁的,尾部的是最近最久未使用的),而是放到LRU列表的midpoint位置,默认在5/8的位置。该参数可以由于innodb_old_blocks_pct控制(例如37表示37%的位置)。midpoint之后的为old列表,midpoint之前的为new列表,表示最活跃的热点数据。示例如下图:
PS:我认为使用这种midpoint insertion主要是为了更好的度量热点数据。而不是做一次访问马上就认为其实最热的数据放到顶端。
减小单次的大批量数据查询(类似于mysqldump的行为,还有全表扫描,索引操作等)对于BufferPool(简称BP)的污染。
BP可以被认为是一条长链表。被分成young 和 old两个部分,其中old默认占37%的大小(由innodb_old_blocks_pct 配置)。靠近顶端的Page表示最近被放问。靠近尾端的Page表示长时间未被访问。而这两个部分的交汇处成为midpoint。每当有新的Page需要加载到BP时,该page都会被插入到midpoint的位置,并声明为old-page。当old部分的page,被访问到时,该page会被提升到链表的顶端,标识为young。
由于table scan的操作是先load page,然后立即触发一次访问。所以当innodb_old_blocks_time =0 时,会导致table scan所需要的page不断的作为young page被添加到链表顶端。而一些使用较为不频繁的page就会被挤出BP,使得之后的SQL会产生磁盘IO,从而导致响应速度变慢。这也就是标题中所提到的BP污染。
为了避免BP污染可以使用以下参数控制加入到LRU new部分的时机,从而避免BP污染(即放置频繁地把时机上不是热点的数据放到了LRU new部分):
(1)当InnoDB_old_blocks_time的参数值设置为0时。当old部分的数据页被访问到时,该数据页会被提升到链表的头部,并被标记为new数据页。
(2)当InnoDB_old_blocks_time的参数值大于0时(以1000毫秒或者1秒为例)。old部分数据页插入缓冲池后,1秒之后被访问,该数据页会被提升到链表的头部,并被标记为new数据页。在刚插入到一秒内,即便old部分的数据页被访问,该数据页也不会移动到new链表的头部。
PS:数据从 old->new的过程称为made young ;由于innodb_old_blocks_time的设置而导致没有从old移动到new的操作称为page not made young
使用MySQL命令“show variables like '%innodb_old%';可以查看InnoDB缓冲池结构的参数信息。
通过show engine innodb status\G;命令还能查看buffer pool相关的详细信息。包括made yound,not young的每秒操作数,缓冲的命中率(小于95%就可以考虑LRU污染的情况了)
3.2.3 页压缩
unzip_LRU用于对不同压缩页大小的页进行管理
3.2.4 关于Flish List
LRU列表中的页被修改后产生脏页,与磁盘上数据不一致。这时候数据库会通过CHECKPOINT机制将脏页刷新回磁盘,而FLUSH列表中的页为脏页列表。脏页既存在于LRU列表页存在于FLUSH列表,两者互不影响。LRU列表用来管理缓冲池中页的可用性,FLUSH列表用来管理将页刷新回磁盘,两者互不影响。
3.3 重做日志缓冲
重做日志一般不会太大,一般每一秒钟都会将重做日志缓冲刷新到日志文件。
查看重做日志缓冲大小:show variables like 'innodb_log_buffer_size'\G
例如这台机器的重做日志缓存为16M
触发重做日志缓存写磁盘的时机:
- master thread每一秒将重做日志缓冲刷新到重做日志文件
- 每个事物提交时会将重做日志缓冲刷新到重做日志文件
- 当重做日志缓冲池剩余空间小于50%时,重做日志缓冲刷新到重做日志文件
3.4 额外内存池
缓冲池有一些缓冲控制对象。数据结构本身的内存申请需要从额外内存池申请。额外内存池可以理解为内存堆,类似JVM的堆,用于对象内存分配。
4. Checkpoint技术
ACID的D要求持久性。为了避免提交事务时发生数据丢失,都是采用先写重做日志再修改页。
LSN(Log Sequence Number)是来标记半本的,同理重做日志也有LSN,检查点也有LSN。可以通过命令show engine innodb status来观察:
4.1 为什么使用检查点机制
每次有脏页都马上刷入磁盘代价很高。引入checkpoint机制可以解决以下问题:
- 缩短数据库的恢复时间: 数据库发生宕机时,不需要重做所有日志,因为检查点之前的页都是干净的,只需要重做检查点之后的页即可。
- 缓冲池不够用时,刷新脏页:缓冲池不够用时,LRU算法会溢出最近最少使用的页,若此页为脏页,需要强制执行检查点。
- 重做日志不可用时,刷新脏页:重做日志时可以循环使用的,当恢复时不需要这部分重做日志的时候可以覆盖。覆盖前,需要强制产生检查点,将脏页刷入新的重做日志。
4.2 Sharp Checkpoint和Fuzzy Checkpoint
- Sharp Checkpoint: 发生在数据库关闭时将所有的脏页都刷新回磁盘(默认工作),通过参数innodb_fast_shutdown=1来配置。缺点是数据库可用性会受到影响(刷新时都不能用)
- Fuzzy Checkpoint: 只刷新一部分脏页,而不是刷新所有的脏页回磁盘
4.3 Fuzzy Checkpoint的几种类型
- master thread checkpoint: 异步形式每秒或者每十秒从缓冲池的脏页列表刷新一定比例的页回磁盘。Innodb存储引擎可以进行其他操作,用户查询线程不会阻塞。
- flush_lru_list checkpoint: innodb需要保证LRU列表尾部有一定数量的空闲页。该判断交给一个单独的page cleaner线程中进行。LRU列表中空闲可用页的数量由参数innodb_lru_scan_depth来控制(show variables可以查看)。清除LRU的页来满足空闲可用页数量时,如果发现脏页需要写入磁盘。
- Async/Sync Flush Checkpoint: 重做日志不可用时,强制将一些页写回磁盘。此时的脏页是从脏页列表中选取的。若将已经写入到重做日志的LSN记为redo_lsn,将已经刷新回磁盘最新页的lsn记为checkpiont_lsn。这样可以定义checkpoint_age=redo_lsn-checkpoint_lsn;
再定义如下两个变量:
async_water_mark=75%*total_redolog_file_size
sync_water_mark=90%*total_redo_log_file_size
则有:
- checkpoint_age<async_water_mark时,不需要刷新脏页到磁盘
- async_water_mark<checkpoint_age<sync_water_mark时,触发Async Flush,使得checkpoint_age<async_water_mark
- checkpoint_age>sync_water时,触发Sync Flush,使得刷新后满足checkpoint_age<async_water_mark(这种情况很少发生,除非重做日志文件设定的太小了)
该checkpoint是为了满足重做日志循环使用而产生的。可以在show engine innodb status中看到相关信息:
- dirty page too much checkpoint: 当脏页太多的时候强制进行检查点。使用参数innodb_max_pages_pct来控制。
5. Master Thread工作方式
master thread具有最高的线程优先级别。其内部由4个循环(主、后、刷、暂)组成:
- 主循环(loop)
- 后台循环(background loop)
- 刷新循环(flush loop)
- 暂停循环(suspend loop)
master thread会根据数据库运行状态在loop,backgroup,flush loop和suspend loop
PS: 这里以innodb 1.0为例解释原理,在innodb 1.2以上的版本已经发生一些改变
5.1 主循环(loop)
主循环主要是2件事:
- 每秒一次的操作:
- 每10秒一次的操作:
PS:这里的每秒和每十秒只是个近似的值,并不是绝对按照该操作间隔时间的。
5.1.1 每秒一次的操作
每秒一次的操作主要包括:
- 日志缓冲刷新到磁盘,即使这个事务还没有被提交(总是):说明了为什么大事务提交时间还是很短(因为提前刷磁盘了而不是一次性刷入所有)
- 合并插入缓冲(可能):IO次数较少即IO压力小的时候执行
- 至多刷新100个innodb的缓冲池中的脏页到磁盘(可能):根据脏页比例是否超过innodb_max_dirty_pages_pct参数来决定是否刷入磁盘
- 如果当前没有用户活动,则切换到backgroud loop
5.1.2 每10秒一次的操作
- 刷新100个脏页到磁盘(可能)
- 合并至多5个插入缓冲(总是)
- 将日志缓冲刷新到磁盘(总是)
- 删除无用的UNDO页(总是):比如已经标记删除的行信息可以在这时删除UNDO信息
- 刷新100个或者10个脏页到磁盘(总是)
5.2 后台循环(background loop)
后台循环执行以下操作:
- 删除无用的UNDO页(总是):full purge操作
- 合并20个插入缓冲(总是)
- 跳回到主循环(总是)
- 不断刷新100个页直到符合条件(可能,跳转到flush loop中完成):只有很空的时候才会跳转到刷新循环
5.3 刷新循环和暂停循环
刷新循环: 不断刷新100个页直到符合条件
暂停循环: 如果master thread无事可做了,就会进入该循环,直接挂起master thread线程
5.4 关于刷新缓存方面的改进
主要是由于SSD、RAID等存储技术导致原来innodb固定刷新一定页数的方式被淘汰。引入了以下参数来更好的提升IO性能:
- innodb_io_capacity: 默认为200,根据吞吐能力多少来设置。例如SSD可以设置的高点。合并插入缓存的数量也受该值影响。
- innodb_adaptive_flushing: 设定该参数为on,则MYSQL会自适应的根据产生重做日志的速度来决定最合适的刷新脏页的数量。
5.5 innodb 1.2.x以上版本的Master Thread
我们仍然使用 show engine innodb status\G;来查看相关信息
我们采用的是MYSQL5.7(innodb 版本在1.2以上),所以有一些改动:
- srv_idle表示每10秒的操作
- active是每秒的操作
- 刷新脏页的操作单独交给了Page Cleaner Thread
6. InnoDB关键特性
6.1 插入缓存
Innodb开创的特性,十分重要。
非聚集索引插入具有离散型。对索引不是很清楚可以查看聚集索引、非聚集索引和复合索引
6.1.1 为什么要使用插入缓存
利用插入缓存,可以使得对于非聚集索引的插入或更新操作不直接插入到索引页,而是先判断插入的非聚集索引页是否在缓存池中。如果在则直接插入,如果不在就先放入缓存池。然后再以一定的频率和情况进行insert buffer和辅助索引(非聚集索引)叶子节点的合并操作。
可见,这通常能将多个插入合并到一个操作中(因为在一个索引页中),这就大大提高了对于非聚集索引插入的性能。
总结:可以简单的理解为插入缓存将对非聚集索引的插入更新操作进行了合并按照批处理写入一个索引页。改善了非聚集索引的插入效率。
6.1.2 插入缓存的使用时机
使用insert buffer的时机
- 索引是非聚集索引
- 索引不是唯一的:因为在插入缓存 时,数据库并不去查找索引页来判断插入的记录的唯一性。如果去查找肯定又会有离散读取的情况发生
6.1.3 注意点
insert buffer可能占用过多的内存资源。通过ibuf_pool_size_per_max_size来对插入缓存占总缓存的比例进行控制。设定为3即表示占用总缓存三分之一
6.2 change buffer(insert buffer属于change buffer)
innodb现在可以对增删改的DML操作都进行缓冲。将insert buffer,delete buffer,purge buffer统称为 change buffer。注意其中purge buffer对应update操作。
change buffer采用B+树实现。
插入缓存相关的信息也通过show engine innodb status\G;来查看
seg size:插入缓存的大小
size:已经合并的记录页数量
free list: 空闲列表的长度
merges: 合并次数
merged operations: 已经合并的操作数
merged operations(3中操作的个数)与merges的比值越大越好:说明有较多的离散IO逻辑被降低
insert 表示insert buffer;delete mark表示delete buffer;delete表示purge buffer
discarded operations表示当change buffer发生merge时,表已经被删除。此时就无需将记录合并到辅助索引中
6.3 两次写
两次写是为了提升数据页的可靠性。
例如写16K的页,写了4KB发生宕机,会导致数据丢失。单纯依靠重做日志时没法恢复已经损坏的某个页的。页是重做日志恢复的基本单位。这时候我们就需要有一个页的副本来避免这种数据丢失的情况。
缓存池的脏页要先复制到内存中的两次写缓存然后再写入共享表空间和数据文件。
双写缓存的页是连续的,顺序IO开销不是很大。
innodb两次写的结构如下:
需要重做日志恢复的时候会用到共享表空间里面的双写缓存数据来做页恢复。
查看双写缓存情况:
show global status like 'innodb_dblwr%'\G;
6.4 自适应哈希索引(AHI)
AHI通过缓冲池的B+树页构造而来,因此构建速度很快。Innodb存储会自动根据访问频率和模式来为热点页建立哈希索引。AHI查找时间复杂度为O(1)
6.5 异步IO
效率比同步IO高,还支持一些IO merge
还可以开启native AIO可以大大提升性能。通过命令 show variables like 'innodb_use_native_aio'\G来查看是否开启。LINUX默认开启。
注意native aio需要libaio支持,否则报错:
6.6 刷新邻接页
SSD不需要通过该特性提升IOPS。可以通过innodb_flush_neighbors关闭。
机械硬盘使用该特性可以在刷新脏页的时候检测该页所在区的所有页,如果有脏页则一起刷新。这样通过AIO可以将多个IO合并为一个IO操作。
7启动关闭与恢复
关闭时参数innodb_fast_shutdown影响innodb的行为。该参数有以下三个值可以设置:
- 0: 最可靠,需要完成所有的full purge 和merge insert buffer并且将所有脏页刷新回磁盘。最耗时。
- 1: 只刷会脏页
- 2:只将日志写入日志文件。不会有任何事务丢失,但是重启需要进行恢复操作。
innodb_force_recovery有6个值可以设定。该参数用来管理INNODB如何做恢复。该值设定为0代表当发生需要恢复时,进行有所的恢复操作。具体可以看书本P59对于这6个取值的介绍
8 小结
主要介绍innodb的体系结构、线程和内存管理以及关键特性。
参考资料:
http://blog.itpub.net/29272216/viewspace-1244637/
http://www.jb51.net/article/31542.htm