InnoDB的“无用”知识

Posted hello_读书就是赚钱

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了InnoDB的“无用”知识相关的知识,希望对你有一定的参考价值。

一、本文目的

一个新的OLTP项目不使用InnoDB将是多么愚蠢.

这篇文章是介绍InnoDB的体系架构与工作机制,在平时我们工作中也许不会用到这些知识的内容,但对我们去认识InnoDB却是一份好的材料.

在这篇文章里,我没有关注版本间的不同信息,而是关注其内在的工作机制,总体来说,包含了内存+工作线程两部分知识,来阐述InnoDB的工作机制.

二、体系架构


如图所示,InnoDB的体系架构如上图所示,是由多条后台线程+内存池+文件系统组合而成.

1、线程概述

我们先看看后台线程的分类.它包括:

主线程(master 线程)

它大概有下述几种作用,后面会利用伪代码更细致说明

  • 同步内存数据到盘
  • 清理合并无用数据
  • 负责InnoDB里面其他的同步刷新工作

在最新的InnoDB的版本又将master线程的工作拆分到以下几种线程种去.

IO线程

即协调内部异步IO调用的回调线程,包括:

  • insert buffer的回调,后面会说明insert buffer的作用
  • log thread 处理日志
  • read thread 处理读事件
  • write thread 处理写事件

show variables like 'innodb_%io_threads'\\G命令我们可以看到线程数量

也可以从show engine innodb status看到线程的情况

undo 日志清理线程

即清理undo标识为删除的数据的线程,该部分功能也是从master线程种移植出来的

脏页清理线程

即脏页刷新动作,把脏页刷新回磁盘,该部分的功能同样也是从master线程中移植出来

深究master线程

看完上面的线程总览可能会有点迷糊,接下来,我们来看下master线程里面的机制是怎样的,下面用伪代码进行演示:
先总结一下master线程里面的操作:

  • 刷日志缓存
  • 合并insert buffer
  • 刷数据脏页
  • undo日志清理
  • 可以通过核心参数innodb_io_capacity控制刷新脏页数量多少,如果高性能的服务器可以提高这个值,提高刷页的数量

把缓存刷新与undo清理的动作都放在线程里面,尽量利用空闲的时间去做刷脏的动作,按书籍指示,我们可以理解master线程函数里面存在下面几个loop循环,做了上面的事情,尽量保证是在空闲的时间去做.下面代码无关版本,我们只在乎他做了什么事.在1.2.x的版本,刷页的动作已经被移出到page cleaner thread中

void master_thread()
    //下面是每秒动作
    loop:
    for(int i=0;i<10;i++)
        thread_sleep(1)
        do log buffer flush to disk //刷新日志缓存到磁盘
        if (last_one_sceond_ios< innodb_io_capacity*5%) //当过去一秒的io数量小于innodb_io_capacity的%5时
            do merge innodb_io_capacity*5% insert buffer //合并insert buffer
        if (buf_get_modified_ratio_pct>innodb_max_dirty_pages_pct) //脏页数大于配置innodb_max_dirty_pages_pct时,也就是上面说的too much dirty pages
            do buffer pool flush innodb_io_capacity dirty page //刷innodb_io_capacity个脏页
        else if enable adaptive flush //如果开启自适应刷新,那么将会根据redo log的数量判断是否刷盘
            do buffer pool flush adaptive dirty page
        if (no user activity)
            goto backgroud loop
    
    //下面开始是10秒的动作
   if (last_ten_second_ios<innodb_io_capacity) //当过去10秒的io数量小于innodb_io_capacity时
        do buffer pool flush innodb_io_capacity dirty page //刷innodb_io_capacity个脏页
    do merge innodb_io_capacity*5% insert buffer //合并insert buffer
    do log buffer flush to disk //刷新日志缓存到磁盘
    do full purge //做undo日志的清理动作
    if (buf_get_modified_ratio_pct>70%)//脏页数量大于70%时
        do buffer pool flush innodb_io_capacity dirty page //刷innodb_io_capacity个脏页
    else
        buffer pool flush innodb_io_capacity*10% dirty page //刷innodb_io_capacity*5%个脏页
    goto loop
    
    //下面是backgroup loop动作,只有在没有活跃用户时才会进入
    backgroud loop:
        do full purge //做undo日志的清理动作
        do merge innodb_io_capacity insert buffer
        if not idle://非io空闲的情况,回到主循环
            goto loop
        else
            goto flush loop

    //下面是flush loop,主要是做脏页刷新动作
    flush loop:
        do buffer pool flush innodb_io_capacity dirty page //刷innodb_io_capacity个脏页
        if (buf_get_modified_ratio_pct>innodb_max_dirty_pages_pct) //脏页数大于配置innodb_max_dirty_pages_pct时,也就是上面说的too much dirty pages
            goto flush loop
        else
            goto suspend loop

    //下面是suspend loop,是用来挂起线程,没事做减少资源消耗
    suspend loop:
        suspend_thread()
        waiting event
        goto loop

2、内存概述

2.1、内存池

从上这张架构图可以看出,InnoDB就像Java一样,在启动的时候会预先分配一大块内存作为它的内存池,这个内存池里面,数据页合索引页占用了大部分的比例,还有一些内存会划给insert buffer、double writer buffer undo log等的缓冲,它揽盖了InnoDB中要用到内存的地方.还有一些类似“堆外”内存的机制,比如redo log buffer就是被放置在InnoDB的内存池之外,还有InnoDB中对象自己的内存,也是被放置到额外的内存池.

除此之外,我们可以看到内存池不止一个,可以存在多个.

我们通过下面这些语句来探索内存池的信息

查看内存池大小

show variables like 'innodb_buffer_pool_size'

查看内存池实例数量

show variables like 'innodb_buffer_pool_instance'

查看内存池的状态

select * from innodb_buffer_pool_stats

2.2、淘汰策略

有了缓冲,那么就一定会有脏数据的问题.有脏数据的问题,就一定要了解内存页淘汰策略.在InnoDB中维护着下面这几条list来做内存的淘汰刷新支持.

LRU列表

基于LRU算法 的改进版,在原来的算法基础上,加入了midpoint的概念,这个点默认是列表的37%长度,在这个point之前的数据叫热点数据,后面的数据叫冷数据.当读取到数据页的时候,并不会直接当如LRU list的头部,而是放置在midpoint之后,当冷数据经过innodb_old_blocks_time之后,就会被升级为热点数据.

它这么说的理由是因为mysql可能做很多扫表操作,扫描会获取很多页,导致真正的热点页被淘汰.

unzip_LRU列表

LRU列表上面的维护的页都是有固定尺寸的,所以增加了unzip_LRU列表来支持页的压缩功能.

根据不同的页面尺寸拥有多条unzip_LRU列表.

这个列表是根据伙伴算法进行工作的,作用的过程如下所示:

  • 检查4KB的unzip_LRU列表,检查是否可用的空闲页,若有,则直接使用
  • 否则,检查8KB的unzip_LRU列表
  • 若能够得到空闲页,将页分成2个4KB的页,存放到4KB的unzip_LRU列表
  • 若不能得到空闲页,从LRU列表中申请一个16KB的页,将页分为1个8KB的页、2个4KB的页,分别存放到对应的unzip_LRU列表中

Free列表

即是存放空的页的列表,如果还有空页,那么从这个列表中取数据加入LRU,否则直接从LRU中淘汰最久没使用的页.

Flush列表

即是脏页列表,所有数据脏页都会丢在这个列表里面.page clear线程会从这个列表中取脏页进行刷新

说完了这么多,我们来看看具体的观察方法

show engine innodb status里面的buffer pool and memory项

除此之外,我们也可以通过查阅下面的表来获取相关信息:

select * from innodb_buffer_page_lru;

2.3、其他的内存使用

上面我们也说其他内存的使用,主要有以下两个点

额外的内存池

在对一些数据结构本身的内存进行分配时,需要从额外的内存池中进行申请,当该区域内存不够时,会从缓冲池中进行申请.比如内部对象的内存,我的理解是这一块是运行时的内存,跟存放数据无关.

重做日志缓冲(redo log buffer)

写重做日志前,会先把日志刷到这个缓冲内,不需要太大,满足1s内(默认1s刷一次盘)的所有事务大小即可.
我们可以通过innodb_log_buffer_size来对buffer的大小进行调整.注意,如果buffer太小的话不会导致异常,但是会慢,这是刷盘机制决定的,看下刷盘机制

  • 主线程1s刷一次
  • 每个事物提交时会触发刷盘
  • 当buffer剩余空间小于1/2时,会触发刷盘

2.4、checkpoint技术

checkpoint技术与刷盘技术息息相关,所以我们把这个知识点放在内存这里来讲.
不难理解,我们

  • 缓冲池不能无限,即内存不可能是无限的
  • 重做日志不能无限,会增加运维成本,恢复数据的时候时间需要很久

所以,需要checkpoint技术.

checkpoint是检查站的意思,是InnoDB中脏页的检查站,当某些触发条件满足的时候就启用checkpoint把脏页刷回磁盘,checkpoint之前的数据表示已经刷回磁盘,之后的数据才需要重做.总体来说,他是为了解决这个问题.它的体现形式为LSN,即日志版号.

通过LSN来标记当前缓存与日志的版本,通过LSN来比较到底刷了多少数据到磁盘,可以通过show engine innodb status观察.
checkoutpoint大致分为下面的两个时机

sharp(锋利的) checkpoint

是工作在关闭数据库的时候,将所有的脏页都回刷磁盘,对性能影响较大.

fuzzy(迟钝的) checkpoint

可以说是online的checkpoint,主要有:

  • 主线程:会以每秒或者每秒的时间触发刷屏操作
  • flush lru list:通过page cleaner线程,观察lru list里面的脏页是否达到一定的数量,如果是,就进行刷盘,show variables like 'innodb_lru_scon_depth'
  • async/sync flush:指的是重做日志文件不可用的情况,这时需要强制将一些脏页列表的页刷新回磁盘.在page cleaner thread中进行,保证了重做日志的循环使用
  • diry page too mush.当脏页数量达到一定的比例,将直接搞,show variables like 'innodb_max_dirty_pages_pct'是它的配置,默认为75%.目的是为了保障缓冲池中有足够可用的页

3、其他的关键特性

上面说了挺多名词,下面我们一次性来给出答案,说明一下他们的作用与工作机制.

insert buffer

介绍这个知识点,我们通过问答式来进行探索

它存在的必要性是什么?

一句话总结:它是为了解决数据插入时,需要更新辅助索引,慢的问题.

注意:跟数据缓存,数据页是没关系的,而是跟插入索引有关系.,不要被它的名字误导.

对于非聚集索引的插入或更新操作

  • 先判断插入的非聚集索引页是否在缓冲池中
  • 若在,则直接插入.
  • 若不在,则先放入到一个insert buffer 对象中,好似欺骗.

这样就少了一次IO操作,insert buffer都是在内存中的,会定时刷insert buffer的缓冲到磁盘.

它的大小?

内存占用最大能占用到内存池的1/2

什么时候会促发insert buffer?

记录的索引是辅助索引,并不是聚集索引.

索引并不是唯一的,因为如果是唯一的就一定要重新拉取一次内存页,加gap锁,判断是否唯一.

所以这就是为什么加唯一索引会比较慢的原因.

它的数据结构是怎样的?

想不到的是,它是一棵二叉树,物理存储在共享表空间中(ibdata1)

  • 叶子节点:记录数据所在的表的表空间与页的偏移量
  • 叶子节点:除了上述索引信息外,还记录了辅助索引的记录信息

说白了就是记录索引页的一棵聚集索引树.

如何解决查询时,脏数据的问题?

上面说过在master线程中,以一定的速度去merge buffer到辅助索引中,此时采用公平算法,保证merge的数据是公平随机的.
除此之外,如果没在在线及时的合并了buffer,那么buffer会被刷入磁盘,在重启的时候进行恢复动作.

辅助索引被读取到内存时,即要用某个数据页来做查询的时候.即查询会做数据合并.

发现辅助索引页无可用空间时,这是是通过一个叫insert buffer bitmap记录辅助索引页的剩余可用空间的,如果发现这个辅助索引页快没空间的时候就触发合并.

与此类似的机制还有change buffer,因为功能类似,我们就不再累述.

double write

背景介绍,如果在写磁盘的时候,一个数据页写一半的时候宕机了,那就出现了脏数据,此时没法通过redo log进行回滚.所以提供了double write解决方案.所以可以double write机制是为了可靠性而生.

准对这种写数据问题,就是先副本、造一份副本防止数据出错.我们来看一下它的执行步骤:

  • 刷脏页时,不直接刷磁盘,而是先刷到double buffer缓冲区
  • 然后刷double buffer到磁盘中
  • 先写完double buffer的磁盘数据后,再真正的去刷盘.

可见,如果物理数据页坏了的话,可以用double buffer的copy数据进行回滚.如果还没写入物理数据的话,也可以直接通过redo log重做

adaptive hash index

是自适应hash,加快数据搜索的速度.我们叫他AHI.

AHI是通过缓冲池中的B+树页构造而来的,会自动根据访问的频率和模式来自动地为某些热点数据页建立哈希索引.
所以可见为什么我们通过覆盖索引和id来查找的时候会这么快,就是因为有自适应hash的存在,可能击中到hash中的数据,算法复杂度降低为O(1)

async IO

即利用操作系统的能力来进行异步IO操作,与AIO知识点相同,我们不再提及,但InnoDB就是用了这个特性.

flush neighbor page

当刷盘的时候,会根据脏页判断是否在物理位置上相邻的脏页,如果有,那就一并刷入磁盘.
注意,书中提到,如果使用的是固态硬盘的话可以关闭这个特性,因为固态硬盘本身操作已经很快了,也是走随机存储,并不需要这种顺序刷盘的过程.

以上是关于InnoDB的“无用”知识的主要内容,如果未能解决你的问题,请参考以下文章

InnoDB的“无用”知识

InnoDB的“无用”知识

无用的知识(日历矩阵)

杂文 | 一些无用但有趣的冷知识

Python所有的学习路线,你要的知识体系在这,千万别做了无用功

Python所有方向的学习路线,你们要的知识体系在这,千万别做了无用功!