索引目录
INNODB 的体系结构
缓冲池
缓存中页的定位:
checkpoint 技术
INNODB 的关键特性
插入缓冲
change buffer
两次写
以下的资料总结自: 官方文档和《MySQL 技术内幕 - INNODB 存储引擎》一书.
对 INNODB 存储引擎缓冲池的那一段描述来自博文: http://www.ywnds.com/?p=9886 http://www.ywnds.com/?p=9886 说句实话这片博文写的很清楚, 通过问答形式加紧逻辑性!
这篇文字会详细的说明 INNODB 存储引擎的体系结构及特性.
INNODB 存储引擎的内存管理
Checkpoint 技术
INNODB 存储引擎的关键特性
插入缓冲
- DOUBLEWRITE
- AHI(自适应哈希索引)
异步 IO
刷新临近页
INNODB 的体系结构
首先通过一张图来说明 INNODB 存储引擎的体系结构.
INNODB 存储引擎有多个内存块, 这些内存块组成了一个大的内存池.(innodb_buffer_size)
INNODB 的后台线程主要作用: 刷新内存池中的数据, 保证缓冲池中的内存缓存的是最近的数据. 其二: 将已修改的数据文件刷新到磁盘文件, 同时保证数据库发生异常的情况下 INNODB 能恢复到正常状态.
在上一篇博文中说明了 INNODB 存储引擎后台线程的具体作用: CLICK HERE! https://www.cnblogs.com/wxzhe/p/8876573.html
上面的图片只是简单地说明了 INNODB 存储引擎的作用, 下面是一张详细的图, 说明了 INNODB 在具体是怎么样做这些事的!(图片来自官方网站的 MySQL5.7 的文档)
下面我们会详细的说明这张图的具体是怎么工作的!
缓冲池
INNODB 存储引擎是基于磁盘存储的, 并将其记录按照页的方式进行管理. 在数据库系统中由于 CPU 速度与磁盘速度之间的鸿沟, 基于磁盘的数据库系统通常使用缓冲技术来提高数据的整体性能.
缓冲池简单来说就是一块内存区域. 通过内存的速度来弥补磁盘速度较慢对数据库性能的影响. 在数据库中进行读取页的操作, 首先将从磁盘读到的页存放在缓冲池中(这个过程称作 FIX), 下一次读取相同的页时, 首先判断该页是不是在缓冲池中, 若在, 称该页在缓冲池中被命中, 直接读取该页. 否则, 读取磁盘上的页.
对于数据库中页的修改操作, 首先修改在缓冲池中页, 然后再以一定的频率刷新到磁盘, 并不是每次页发生改变就刷新回磁盘, 而是通过一种叫做 checkpoint 的机制把页刷新会磁盘.
因此数据的操作都是对缓冲池进行的操作, 而不是磁盘.
缓冲池的大小设置:
缓冲池配置可以通过 INNODB_BUFFER_POOL_SZIE 来设置, 官方文档建议, 缓冲池的大小最多应设置为物理内存的 80%, 正常使用可以设置为 (50%~80%) 之间.
缓冲池是一块内存, INNODB 存储引擎是通过页的方式对这块内存进行管理的. 缓冲池中存储的页有: 索引页, 数据页, 插入缓冲, 自适应哈希索引(AHI),INNODB 存储的锁信息, 数据字典信息等. 缓冲池中的索引页和数据页只是占据了缓冲池的很大一部分而已. 如图(图片地址 https://zhuanlan.zhihu.com/p/47581960 )
从 INNODB1.0.x 开始, 允许有多个缓冲池实例. 每个页根据哈希值平均分配到不同的缓冲池实例中. 这样做的好处是减少数据库内部的资源竞争, 增加数据库并发处理能力.
innodb_buffer_pool_instances: 设置有多少个缓冲池. 通常建议把缓冲池个数设置为 CPU 的个数.
在使用 show engine innodb status\G 的时候会以 ---BUFFER POOL 5 的形式分别标识每一个 bp, 所有的 bp 会均分 INNODB_BUFFER_POOL_SZIE 的大小.
缓冲池的管理
缓冲池的结构描述(或者组织形式, 不太准确):
我们已经知道这个 Buffer Pool 其实是一片连续的内存空间, 那现在就面临这个问题了: 怎么将磁盘上的页缓存到内存中的 Buffer Pool 中呢? 直接把需要缓存的页向 Buffer Pool 里一个一个往里怼么? 不不不, 为了更好的管理这些被缓存的页, InnoDB 为每一个缓存页都创建了一些所谓的控制信息, 这些控制信息包括该页所属的表空间编号(space id), 页号(page number), 页在 Buffer Pool 中的地址, 一些锁信息以及 LSN 信息(锁和 LSN 这里可以先忽略), 当然还有一些别的控制信息.
每个缓存页对应的控制信息占用的内存大小是相同的, 我们就把每个页对应的控制信息占用的一块内存称为一个控制块吧, 控制块和缓存页是一一对应的, 它们都被存放到 Buffer Pool 中, 其中控制块被存放到 Buffer Pool 的前边, 缓存页被存放到 Buffer Pool 后边, 所以整个 Buffer Pool 对应的内存空间看起来就是这样的:
控制块和缓存页之间的那个碎片是个什么呢? 你想想啊, 每一个控制块都对应一个缓存页, 那在分配足够多的控制块和缓存页后, 可能剩余的那点儿空间不够一对控制块和缓存页的大小, 自然就用不到喽, 这个用不到的那点儿内存空间就被称为碎片了. 当然, 如果你把 Buffer Pool 的大小设置的刚刚好的话, 也可能不会产生碎片~
前面我们知道了缓冲池的结构. 接下来说 InnoDB 存储引擎是怎么对缓冲池进行管理的?
当我们最初启动 MySQL 服务器的时候, 需要完成对 Buffer Pool 的初始化过程, 就是分配 Buffer Pool 的内存空间, 把它划分成若干对控制块和缓存页. 但是此时并没有真实的磁盘页被缓存到 Buffer Pool 中(因为还没有用到), 之后随着程序的运行, 会不断的有磁盘上的页被缓存到 Buffer Pool 中, 那么问题来了, 从磁盘上读取一个页到 Buffer Pool 中的时候该放到哪个缓存页的位置呢? 或者说怎么区分 Buffer Pool 中哪些缓存页是空闲的, 哪些已经被使用了呢? 我们最好在某个地方记录一下哪些页是可用的, 我们可以把所有空闲的页包装成一个节点组成一个链表, 这个链表也可以被称作 Free 链表(或者说空闲链表). 因为刚刚完成初始化的 Buffer Pool 中所有的缓存页都是空闲的, 所以每一个缓存页都会被加入到 Free 链表中, 假设该 Buffer Pool 中可容纳的缓存页数量为 n, 那增加了 Free 链表的效果图就是这样的:
从图中可以看出, 我们为了管理好这个 Free 链表, 特意为这个链表定义了一个控制信息, 里边儿包含着链表的头节点地址, 尾节点地址, 以及当前链表中节点的数量等信息. 我们在每个 Free 链表的节点中都记录了某个缓存页控制块的地址, 而每个缓存页控制块都记录着对应的缓存页地址, 所以相当于每个 Free 链表节点都对应一个空闲的缓存页.
有了这个 Free 链表事儿就好办了, 每当需要从磁盘中加载一个页到 Buffer Pool 中时, 就从 Free 链表中取一个空闲的缓存页, 并且把该缓存页对应的控制块的信息填上, 然后把该缓存页对应的 Free 链表节点从链表中移除, 表示该缓存页已经被使用了, 并且把改页写入 LRU 链表!
不要因为走的太远而忘记为什么出发.
简单回顾一下, 为什么讲 free list? 是为了讲怎么管理 buffer pool 对吧. 那 free list 就相当于是数据库服务刚刚启动没有数据页时, 维护 buffer pool 的空闲缓存页的数据结构.
下面再来简单地回顾 Buffer Pool 的工作机制. Buffer Pool 两个最主要的功能: 一个是加速读, 一个是加速写. 加速读呢? 就是当需要访问一个数据页面的时候, 如果这个页面已经在缓存池中, 那么就不再需要访问磁盘, 直接从缓冲池中就能获取这个页面的内容. 加速写呢? 就是当需要修改一个页面的时候, 先将这个页面在缓冲池中进行修改, 记下相关的重做日志, 这个页面的修改就算已经完成了. 至于这个被修改的页面什么时候真正刷新到磁盘, 这个是后台刷新线程来完成的.
在初始化的时候, bp 中所有的页都是空闲页(也就是 free list 的页), 需要读数据时, 就会从 free 链表中申请页, 因为物理内存不可能无限增大, 但是数据库的数据却是在不停增大的, 所以 free 链表的页是会用完的, 这时候应该怎么办? 这时候我们可以考虑把已经缓存的页从 bp 中删除一部分, 那么究竟采用什么样的方式来删除, 究竟该删除哪些已经缓存的页?
为了回答这个问题, 我们还需要回到我们设立 Buffer Pool 的初衷, 我们就是想减少和磁盘的 I/O 交互, 最好每次在访问某个页的时候它都已经被缓存到 Buffer Pool 中了. 假设我们一共访问了 n 次页, 那么被访问的页已经在缓存中的次数除以 n 就是所谓的缓存命中率, 我们的期望就是让缓存命中率越高越好
怎么提高缓存命中率呢? InnoDB Buffer Pool 采用经典的 LRU 算法来进行页面淘汰, 以提高缓存命中率. 当 Buffer Pool 中不再有空闲的缓存页时, 就需要淘汰掉部分最近很少使用的缓存页. 不过, 我们怎么知道哪些缓存页最近频繁使用, 哪些最近很少使用呢? 呵呵, 神奇的链表再一次派上了用场, 我们可以再创建一个链表, 由于这个链表是为了按照最近最少使用的原则去淘汰缓存页的, 所以这个链表可以被称为 LRU 链表(Least Recently Used). 当我们需要访问某个页时, 可以这样处理 LRU 链表
如果该页不在 Buffer Pool 中, 在把该页从磁盘加载到 Buffer Pool 中的缓存页时, 就把该缓存页包装成节点塞到链表的头部.
如果该页在 Buffer Pool 中, 则直接把该页对应的 LRU 链表节点移动到链表的头部.
但是这样做会有一些性能上的问题, 比如你的一次全表扫描或一次逻辑备份就把热数据给冲完了, 就会导致导致缓冲池污染问题! Buffer Pool 中的所有数据页都被换了一次血, 其他查询语句在执行时又得执行一次从磁盘加载到 Buffer Pool 的操作, 而这种全表扫描的语句执行的频率也不高, 每次执行都要把 Buffer Pool 中的缓存页换一次血, 这严重的影响到其他查询对 Buffer Pool 的使用, 严重的降低了缓存命中率 !
所以 InnoDB 存储引擎对传统的 LRU 算法做了一些优化, 在 InnoDB 中加入了 midpoint. 新读到的页, 虽然是最新访问的页, 但并不是直接插入到 LRU 列表的首部, 而是插入 LRU 列表的 midpoint 位置. 这个算法称之为 midpoint insertion stategy. 默认配置插入到列表长度的 5/8 处. midpoint 由参数 innodb_old_blocks_pct 控制.
midpoint 之前的列表称之为 new 列表, 之后的列表称之为 old 列表. 可以简单的将 new 列表中的页理解为最为活跃的热点数据.
同时 InnoDB 存储引擎还引入了 innodb_old_blocks_time 来表示页读取到 mid 位置之后需要等待多久才会被加入到 LRU 列表的热端. 可以通过设置该参数保证热点数据不轻易被刷出.
[free 链表是空的, 数据库刚初始化的时候产生的, 当需要读取数据时, 会从 free list 中申请一个页, 把从放入磁盘读取的数据放入这个申请的页中, 这个页的集合叫 LRU 链表]
上面说到了读数据, 下面说明写数据:
前面我们讲到页面更新是在缓存池中先进行的, 那它就和磁盘上的页不一致了, 这样的缓存页也被称为脏页(英文名: dirty page). 所以需要考虑这些被修改的页面什么时候刷新到磁盘? 以什么样的顺序刷新到磁盘? 当然, 最简单的做法就是每发生一次修改就立即同步到磁盘上对应的页上, 但是频繁的往磁盘中写数据会严重的影响程序的性能(毕竟磁盘慢的像乌龟一样). 所以每次修改缓存页后, 我们并不着急立即把修改同步到磁盘上, 而是在未来的某个时间点进行同步, 由后台刷新线程依次刷新到磁盘, 实现修改落地到磁盘.
但是如果不立即同步到磁盘的话, 那之后再同步的时候我们怎么知道 Buffer Pool 中哪些页是脏页, 哪些页从来没被修改过呢? 总不能把所有的缓存页都同步到磁盘上吧, 假如 Buffer Pool 被设置的很大, 比方说 300G, 那一次性同步这么多数据岂不是要慢死! 所以, 我们不得不再创建一个存储脏页的链表, 凡是在 LRU 链表中被修改过的页都需要加入这个链表中, 因为这个链表中的页都是需要被刷新到磁盘上的, 所以也叫 FLUSH 链表, 有时候也会被简写为 FLU 链表. 链表的构造和 Free 链表差不多, 这就不赘述了. 这里的脏页修改指的此页被加载进 Buffer Pool 后第一次被修改, 只有第一次被修改时才需要加入 FLUSH 链表 (代码中是根据 Page 头部的 oldest_modification == 0 来判断是否是第一次修改), 如果这个页被再次修改就不会再放到 FLUSH 链表了, 因为已经存在. 需要注意的是, 脏页数据实际还在 LRU 链表中, 而 FLUSH 链表中的脏页记录只是通过指针指向 LRU 链表中的脏页. 并且在 FLUSH 链表中的脏页是根据 oldest_lsn(这个值表示这个页第一次被更改时的 lsn 号, 对应值 oldest_modification, 每个页头部记录) 进行排序刷新到磁盘的, 值越小表示要最先被刷新, 避免数据不一致.
[理解脏页的概念? 脏页是 bp 中被修改的页, 脏页寄存在与 lru 链表中, 也存在与 flush 链表中, flush 链表中存在的是一个指向 lru 链表中具体数据的指针. 因此只有 lru 链表中的页第一次别修改时, 对应的指针才会存入到 flush 中, 若以后再修改这个页, 则是直接更新对应的数据.]
这三个重要列表 (LRU list, free list,flush list) 的关系可以用下图表示:
Free 链表跟 LRU 链表的关系是相互流通的, 页在这两个链表间来回置换. 而 FLUSH 链表记录了脏页数据, 也是通过指针指向了 LRU 链表, 所以图中 FLUSH 链表被 LRU 链表包裹.
缓存中页的定位:
我们前边说过, 当我们需要访问某个页中的数据时, 就会把该页加载到 Buffer Pool 中, 如果该页已经在 Buffer Pool 中的话直接使用就可以了. 那么问题也就来了, 我们怎么知道该页在不在 Buffer Pool 中呢? 难不成需要依次遍历 Buffer Pool 中各个缓存页么? 一个 Buffer Pool 中的缓存页这么多都遍历完岂不是要累死?
再回头想想, 我们其实是根据表空间号 + 页号来定位一个页的, 也就相当于表空间号 + 页号是一个 key, 缓存页就是对应的 value, 怎么通过一个 key 来快速找着一个 value 呢? 那肯定是哈希表了.
所以我们可以用表空间号 + 页号作为 key, 缓存页作为 value 创建一个哈希表, 在需要访问某个页的数据时, 先从哈希表中根据表空间号 + 页号看看有没有对应的缓存页, 如果有, 直接使用该缓存页就好, 如果没有, 那就从 Free 链表中选一个空闲的缓存页, 然后把磁盘中对应的页加载到该缓存页的位置.
上面基本说明了 bp 是怎么工作的, 接下来我们看一个实例的 bp 信息.
- MySQL> show engine innodb ststus;
- ....
- BUFFER POOL AND MEMORY
- ----------------------
- Total large memory allocated 5502402560 #总的内存是多少, 字节为单位
- Dictionary memory allocated 991733 #为 InnoDB 数据字典分配的总内存
- Buffer pool size 327680 #总的 bp 有多少个页, 每个页默认大小为 16K(innodb_page_size 的数值)
- Free buffers 8192 #当数据库刚启动时, bp 中没有数据, 会含有许多 16KB 的块, 这些块就是 free buffer. 当读取数据时, 就从 free list 中申请一个块, 然后把这个块放入 lru 列表中. Free buffers 表示当前 free 列表页中的数量.
- Database pages 490679 #表示的就是 lru 列表中的页, 也就是数据页.(可能情况是 free buffer+database pages 的数量之和等于 bp, 因为缓冲池中还可能会被分配自适应哈希索引, lock 信息, insert buffer 等页, 这部分页不需要 lru 算法维护, 因此不存在 lru 列表中)
- Old database pages 180966 # lru 列表中 old 部分的页数量
- Modified db pages 0 # 脏页的数量. flush 列表
- Percent of dirty pages(LRU & free pages): 0.000 #
- Max dirty pages percent: 75.000 #
- Pending reads 0 # 等待读入缓冲池的缓冲池页数.
- Pending writes: LRU 0, flush list 0, single page 0
- #
- Pages made young 452994, not young 1694417
- 0.00 youngs/s, 0.00 non-youngs/s
- (将页从 lru 列表的 old 部分加入到 new 部分时, 称此时的操作为 page made young. 而因为 innodb_old_blocks_time 的设置导致页没有从 old 部分移动到 new 部分的操作称为 page not made young.--pages made young: 显示了 lru 列表中页移动到前端的次数. young/s, non-young/s 表示每秒这两类操作的次数.)
- Pages read 1436912, created 4603153, written 3896513
- 0.00 reads/s, 0.00 creates/s, 0.00 writes/s
- No buffer pool page gets since the last printout
- Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
- LRU len: 490679, unzip_LRU len: 49074
- I/O sum[0]:cur[0], unzip sum[0]:cur[0]
- ....
- # 这个页数据的整体介绍可以查看官方文档: https://dev.mysql.com/doc/refman/5.7/en/innodb-buffer-pool.html
我们还可以使用统计表查看如下:
- MySQL> use information_schema;
- Reading table information for completion of table and column names
- You can turn off this feature to get a quicker startup with -A
- Database changed
- MySQL> select * from INNODB_BUFFER_POOL_STATS \G
- *************************** 1. row ***************************
- POOL_ID: 0
- POOL_SIZE: 8191
- FREE_BUFFERS: 7005
- DATABASE_PAGES: 1186
- OLD_DATABASE_PAGES: 448
- MODIFIED_DATABASE_PAGES: 0
- PENDING_DECOMPRESS: 0
- PENDING_READS: 0
- PENDING_FLUSH_LRU: 0
- PENDING_FLUSH_LIST: 0
- PAGES_MADE_YOUNG: 0
- PAGES_NOT_MADE_YOUNG: 0
- PAGES_MADE_YOUNG_RATE: 0
- PAGES_MADE_NOT_YOUNG_RATE: 0
- NUMBER_PAGES_READ: 1126
- NUMBER_PAGES_CREATED: 60
- NUMBER_PAGES_WRITTEN: 70
- PAGES_READ_RATE: 0
- PAGES_CREATE_RATE: 0
- PAGES_WRITTEN_RATE: 0
- NUMBER_PAGES_GET: 22583
- HIT_RATE: 0
- YOUNG_MAKE_PER_THOUSAND_GETS: 0
- NOT_YOUNG_MAKE_PER_THOUSAND_GETS: 0
- NUMBER_PAGES_READ_AHEAD: 384
- NUMBER_READ_AHEAD_EVICTED: 0
- READ_AHEAD_RATE: 0
- READ_AHEAD_EVICTED_RATE: 0
- LRU_IO_TOTAL: 0
- LRU_IO_CURRENT: 0
- UNCOMPRESS_TOTAL: 0
- UNCOMPRESS_CURRENT: 0
- 1 row in set (0.00 sec)
- # 这个表的字段信息和上面命令输出的信息都可以用来查看当前 bp 的统计信息
数据访问机制
1. 当访问的页面在缓存池中命中, 则直接从缓冲池中访问该页面. 另外为了避免查询数据页时扫描 LRU, 还为每个 buffer pool instance 维护了一个 page hash, 通过 space id 和 page id 可以直接找到对应的 page. 一般情况下, 当我们需要读入一个 Page 时, 首先根据 space id(space id 对应的是表)和 page id 找到对应的 buffer pool instance. 然后查询 page hash, 如果 page hash 中没有, 则表示需要从磁盘读取.
2. 如果没有命中, 则需要将这个页面从磁盘上加载到缓存池中, 因此需要在缓存池中的空闲列表中找一个空闲的内存块来缓存这个从磁盘读入的页面.
3. 但存在空闲内存块被使用完的情况, 不保证一定有空闲的内存块. 假如空闲列表为空, 没有空闲的内存块, 则需要想办法去产生空闲的内存块.
4. 首先去 LRU 列表中找可以替换的内存页面, 查找方向是从列表的尾部开始找, 如果找到可以替换的页面, 将其从 LRU 列表中摘除, 加入空闲列表, 然后再去空闲列表中找空闲的内存块. 第一次查找最多只扫描 100 个页面, 循环进行到第二次时, 会查找深度就是整个 LRU 列表. 这就是 LRU 列表中的页面淘汰机制.
5. 如果在 LRU 列表中没有找到可以替换的页, 则进行单页刷新, 将脏页刷新到磁盘之后, 然后将释放的内存块加入到空闲列表. 然后再去空闲列表中取. 为什么只做单页刷新呢? 因为这个函数的目的是获取空闲内存页, 进行脏页刷新是不得已而为之, 所以只会进行一个页面的刷新, 目的是为了尽快的获取空闲内存块.
因为空闲列表是一个公共的列表, 所有的用户线程都可以使用, 存在争用的情况. 因此, 自己产生的空闲内存块有可能会刚好被其他线程所使用, 所以用户线程可能会重复执行上面的查找流程, 直到找到空闲的内存块为止.
通过数据页访问机制, 可以知道其中当无空闲页时产生空闲页就成为一个必须要做的事情了. 如果需要刷新脏页来产生空闲页面或者需要扫描整个 LRU 列表来产生空闲页面的时候, 查找空闲内存块的时间就会延长, 这个是一个 bad case, 是我们希望尽量避免的. 因此, innodb buffer pool 中存在大量可以替换的页面, 或者 free 列表中一直存在着空闲内存块, 对快速获取到空闲内存块起决定性的作用. 在 innodb buffer pool 的机制中, 是采用何种方式来产生的空闲内存块, 以及可以替换的内存页的呢? 这就是我们下面要讲的内容 -- 通过后台刷新机制来产生空闲的内存块以及可以替换的页面.
checkpoint 技术
当前事务数据库系统普遍采用了 write ahead log 策略, 即当事务提交时, 先写重做日志, 再修改页. 当由于数据库宕机而导致数据丢失时, 通过重做日志来完成数据的恢复.
想一种情况, 当重做日志变得很大时, 数据库的恢复时间就会变得很长, 恢复代价变得很大?
checkpoint 技术主要目的是:
缩短数据库恢复时间
缓冲池不够用时, 将脏页刷新到磁盘.
重做日志不可用时, 刷新脏页.
当数据库发生宕机时, 数据库不需要所有的重做日志, 因为 checkpoint 之前的页都已经刷新回磁盘, 故数据库只需要对 checkpoint 之后的重做日志进行恢复即可.
此外当缓冲池不够用时, 根据 lru 算法会释放最近最少使用的页, 若此页为脏页, 那么就需要强制执行 checkpoint, 将脏页刷新回磁盘.
因为当前事务数据库系统对重做日志的设计都是循环使用的, 在写入重做日志时, 若这部分重做日志不可用[是因为数据库在宕机恢复时若需要这使用这部分日志, 若此时想要使用这部分重做日志(前面的不可用状态的重做日志)] 则必须强制 checkpoint, 将缓冲池中的页刷新到对应当前重做日志的位置.
若重做日志可以被重用的部分是指这些重做日志已经不再需要(缓冲池中的页和重做日志的位置吻合), 那就可以直接覆盖.
在这里刷新缓冲池中页的时候, 我们提到过, 要把页刷新道道重做日志的位置, 那么 INNODB 是怎么确定这些位置的?
INNODB 使用 LSN(log sequenct number)来标识页刷新的位置.[在前面的字节数上加上写入的字节数]
每个页都有对应 LSN 数值, 重做日志有 LSN,checkpoint 也有 LSN.
- MySQL> show engine innodb status\G
- ......
- LOG
- ---
- Log sequence number 293633237 #当前缓冲池中的 lsn 的值, 也就是 redo log 的 lsn
- Log flushed up to 293633237 #当前磁盘中的 lsn 的值, 是刷 redo log file flush to disk 中的 lsn;
- Pages flushed up to 293633237 #是已经刷到磁盘数据页上的 LSN;
- Last checkpoint at 293633228 #上一次刷新的 LSN
- .....
在 INNODB 存储引擎中, checkpint 发生的时间, 条件及脏页选择都很复杂, 而 checkpoint 所做的事情就是把缓冲池中的脏页刷新到磁盘. 不同之处在于每次刷新多少脏页以及什么时候出发 checkpoint?[这是只是简单说明下]
在 INNODB 存储引擎内部有两种 checkpoint 方式:
- Sharp Checkpoint
- Fuzzy Checkpoint
Sharp Checkpoint 发生在将数据库关机时将所有的脏页刷新回磁盘, 这是默认的工作方式, 即参数 innodb_fast_shutdown=1.
但是若在数据库运行时也使用 Sharp Checkpoint, 那么数据库的性能就会受到影响. 故在 INNODB 内部使用 Fuzzy Checkpoint 的刷新方式, 即每次只刷新一部分脏页, 而不是刷新所有的脏页.
在 INNODB 内部在发生如下情况时, 会进行 fuzzy checkpoint 刷新.
Master Thread Checkpoint: [异步刷新, 每秒或每 10 秒从缓冲池脏页列表刷新一定比例的页回磁盘. 异步刷新, 即此时 InnoDB 存储引擎可以进行其他操作, 用户查询线程不会受阻]
FLUSH_LRU_LIST Checkpoint:InnoDB 存储引擎需要保证 LRU 列表中差不多有 100 个空闲页可供使用. 在 InnoDB 1.1.x 版本之前, 用户查询线程会检查 LRU 列表是否有足够的空间操作. 如果没有, 根据 LRU 算法, 溢出 LRU 列表尾端的页, 如果这些页有脏页, 需要进行 checkpoint. 因此叫: flush_lru_list checkpoint.
InnoDB 1.2.x 开始, 这个检查放在了单独的进程 (Page Cleaner) 中进行, 并且可以使用 innodb_lru_scan_depth 参数控制 LRU 列表中可用页的数量, 默认是 1024! 好处: 1. 减少 master Thread 的压力 2. 减轻用户线程阻塞.
异步 / 同步 Checkpoint: 重做日志不可用时, 需要强制将一些页刷新回磁盘, 而此时脏页是从脏页列表中选择的.
脏页太多时强制 checkpoint: 脏页数量太多时, 强制进行 checkpoint, 当缓冲池中脏页的数量占据超过 innodb_max_dirty_pages_pct 设定的值时, 就进行强制刷新. 默认数值是 75%.
INNODB 的关键特性
插入缓冲
INNODB 在插入非聚集的非唯一性索引时, 会随机插入的数据, 这就会导致性能下降. 因此 INNODB 采用了 insert buffer 来完成非聚集非唯一性索引的插入. 当插入这些索引时, 不是每一次直接插入到索引页中, 而是先判断插入的非聚集索引是否在缓冲池中, 若在, 则直接插入; 若不在, 则先放入到一个 insert buffer 对象中, 好似欺骗. 然后再以一定的频率进行 insert buffer 和辅助所引页子节点的合并操作, 这时通常能将多个插入合并到一个操作中, 这就大大提高了对于非聚集索引的插入性能.
insert buffer 需要同时满足以下两个条件:
索引时辅助索引
索引不是唯一索引
辅助索引不能是唯一的, 因为在插入缓冲时, 数据库并不去查找所引页来判断记录的唯一性. 如果去查找肯定导致又忽悠离散型读情况发生, 从而导致 insert buffer 失去了意义.
- INSERT BUFFER AND ADAPTIVE HASH INDEX
- -------------------------------------
- Ibuf: size 1, free list len 0, seg size 2, 0 merges
- #size: 表示已经合并记录页的数量, free list: 表示空闲列表的长度, seg size 显示当前 insert buffer 的大小, mergers: 表示合并页的数量
- merged operations:
- insert 0, delete mark 0, delete 0
- discarded operations: #表示 change buffer 发生 merge, 表已经被删除, 此时就无需再将记录合并到辅助索引中.
- insert 0, delete mark 0, delete 0
- Hash table size 34673, node heap has 0 buffer(s)
- Hash table size 34673, node heap has 0 buffer(s)
- Hash table size 34673, node heap has 0 buffer(s)
- Hash table size 34673, node heap has 0 buffer(s)
- Hash table size 34673, node heap has 0 buffer(s)
- Hash table size 34673, node heap has 0 buffer(s)
- Hash table size 34673, node heap has 0 buffer(s)
- Hash table size 34673, node heap has 0 buffer(s)
- 0.00 hash searches/s, 0.00 non-hash searches/s
- change buffer
INNODB 从 1.0.x 开始引入了 Change Buffer, 从这个版本开始 INNODB 存储引擎可以对 DNL 操作 ---insert(insert buffer), delete(dlete buffer),update(purge buffer)都进行缓冲.
和之前一样 change buffer 的对象依然是辅助的非唯一索引.
对一条记录进行 update 操作包含两个过程: 1 将记录标记为删除, 2: 将记录删除. delete buffer 对应 update 的第一个阶段, purge buffer 对应 update 的第二个阶段.
同时 INNODB 存储引擎还提供了参数 innodb_change_buffering , 用来开启各种 buffer 的选项. 可选择的值如下:
- inserts, deletes, purges, changes, all, none.
- #changes: 表示 inserts 和 deletes.
- #all: 表示启用所用
- #none: 表示全部关闭. 默认是 all
在写密集的情况下, change buffer 会占用过多的缓冲池资源, 在 INNODB1.2 版本中可以使用 innodb_change_buffer_max_size 参数进行控制, 默认数值是 25, 表示 1/4.
inert buffer 的内部实现:
[站位]
两次写
在数据库发生宕机时, 可能 INNODB 存储引擎正在写入某个页到表中, 而这个页只写了一部分, 比如 16KB 的页, 只写了前 4KB, 之后就发生了宕机, 这种情况被称为部分写失效.
doublewrite 由两部分组成, 一部分是内存中的 double buffer, 大小为 2M, 另一部分是物理磁盘上共享表空间的连续的 128 个页, 即 2 个区, 大小为 2M. 在对缓冲池进行脏页刷新时, 并不直接写磁盘, 而是会通过 memcpy 函数将脏页首先复制到内存中的 doublewrite buffer. 之后通过 doublewrite buffer 再分两次, 每次 1M 顺利地写入共享表空间的物理磁盘上, 然后马上调用 fsync 函数, 同步磁盘, 避免写缓冲带来的问题. 在这个过程中 doublewriter 是连续的, 因此开销不大.
来源: http://www.bubuko.com/infodetail-3108746.html