2.1 InnoDB存储引擎概述

  • 支持ACID事务,特点是行锁设计
  • 支持MVCC
  • 支持外键
  • 提供一次性非锁定读

2.2 InnoDB存储引擎的版本

各版本对比

2.3 InnoDB体系结构

InnoDB体系结构

后台线程总览

  • 主要作用是负责刷新内存池中的数据,保证缓冲池中的数据是最近的数据
  • 将已修改的数据文件刷新到磁盘文件
  • 保证异常发生时InnoDB能恢复到正常状态

内存池总览

  • 维护所有进程/线程需要访问的多个内部数据结构
  • 缓存磁盘上的数据,方便快速的读取,同时在对磁盘文件的数据修改之前在这里缓存
  • 重做日志(redo log)缓冲

2.3.1 后台线程(建议先看2.3.2)

后台线程由Master ThreadI/O ThreadPurge Thread, Page Cleaner Thread 组成,详细如下

2.3.1.1 Master Thread

主要负责将缓冲池中的数据异步刷新到磁盘,保持数据的一致性,包括脏页的刷新、合并插入缓冲、UNDO页的回收等。

Master Thread总体上分两类操作,分别是每秒进行的,以及每10秒进行的。
每秒进行的操作:

  • 日志缓冲刷新到磁盘,即使这个事务还没有提交
  • 合并插入缓冲(可能发生)当前1秒内发生IO次数小于5则合并
  • buf_get_modified_ratio_pct > innodb_max_dirty_pages时,刷新脏页到磁盘(可能发生,至多100个脏页)
  • 如果当前没有用户活动,则切换到background loop

每10秒进行的操作:

  • 刷新100个脏页到磁盘(可能)
  • 合并之多5个插入缓冲(总是)
  • 将日志缓冲刷新到磁盘
  • 删除无用的undo页
  • 刷新100个或者10个脏页到磁盘

Master Thread具体分为loop, background loop,flush loop, suspend loop四个循环

2.3.1.1.1 InnoDB 1.0.x版本之前 Master Thread的伪代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
void master_thread(){
    goto loop;
    loop:
        for(int i=0; i<10; ++i){
            thread_sleep(1);
            
            flush_log_buffer_to_disk();

            if(last_one_second_io < 5){
                merge_at_most_5_insert_buffer();
            }
            if(buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct){
                flush_100_dirty_page();
            }
            if(no_user_activity){
                 goto background_loop;
            }
        }
        
        if(last_ten_second_io < 200){
            flush_100_dirty_page();
        }
            
        merge_at_most_5_insert_buffer();

        flush_log_buffer_to_disk();

        full_purge();

        if(buf_get_modified_ratio_pct > 0.7){
            flush_100_dirty_page();
        }
        else{
            flush_10_dirty_page();
        }
        goto loop;
    
    background_loop:
        full_purge();          # 重点解释
        merge_20_insert_buffer();
        if(not idle){
            goto loop;
        }
        else{
            goto flush_loop;
        }    
            
    flush_loop:
        flush_100_dirty_page();
        if(buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct){
            goto flush_loop;
        }
        goto suspend_loop;

    suspend_loop:
        suspend_thread();
        waiting_event();    
        goto loop;
}

2.3.1.1.2 InnoDB 1.2.x版本之前

  • 解决硬编码问题
  • 改用百分比,引进innodb_io_capacity表示IO吞吐量,默认值为200
  • 合并插入缓冲时,合并插入缓冲的数量为innodb_io_capacity值的5%
  • 从缓冲区刷新脏页时,刷新脏页的数量为innodb_io_capacity
  • 从1.0.x版本开始innodb_max_dirty_pages_pct默认值变为了75,加快刷新脏页的速度,同时适当保证IO的负载
  • 添加参数innodb_max_adaptive_flushing(自适应的刷新),之前的策略是通过简单的比大小来进行刷新脏页,现在通过buf_flush_get_desired_flush_rate的函数来判断需要刷新脏页最适合的数量
  • 1.0.x版本引进innodb_purge_batch_size来控制每次full merge回收的undo页的数量,默认20,可以动态修改

伪代码变为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
void master_thread(){
    goto loop;

    loop:
        for(int i=0; i<10; ++i){
            thread_sleep(1);
        }
        flush_log_buffer_to_dist();
        if(last_one_second_io < 0.05 * innodb_io_capacity){
            merge_5_percentage_innodb_io_capacity_insert_buffer();
        }
        if(buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct){
            flush_100_percentage_innodb_io_capacity_dirty_page();
        }
        else if(adaptive_flush){
            flush_desired_amount_dirty_page();
        }
        if(no_user_acitivity){
            goto background_loop;
        }

        if(last_ten_second_io < innodb_io_capacity){
            flush_100_percentage_innodb_io_capacity_dirty_page();
        }
        merge_5_percentage_innodb_io_capacity_insert_buffer();

        flush_log_buffer_to_disk();
        full_purge();

        if(buf_get_modified_ratio_pct > 0.7){
            flush_100_percentage_innodb_io_capacity_dirty_page();
        }
        else{
            flush_10_percentage_innodb_io_capacity_dirty_page();
        }
        goto loop;

    background_loop:
        full_purge();
        merge_100_percentage_innodb_io_capacity_insert_buffer();
        if(not idle){
            goto loop;
        }
        else{
            goto flush_loop;
        }
    
    flush_loop:
        flush_100_percentage_innodb_io_capacity_dirty_page();
        if(buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct){
            goto flush_loop;
        }
        goto suspend_loop;

    suspend_loop:
        suspend_thread;
        waiting_event();
        goto loop;
}

2.3.1.1.3 InnoDB 1.2.x版本

对刷新脏页的操作从Master Thread线程分离到一个单独的page cleaner thread中

2.3.1.2 I/O Thread

InnoDB使用了大量AIO(async IO)来处理 写 IO请求。IO Thread的工作主要是负责这些IO请求的回调处理。I/O Thread主要分为4种:

  • write, 默认有4个,可以通过innodb_write_io_threads进行更改
  • read, 默认有4个,可以通过innodb_read_io_threads进行更改
  • insert buffer
  • log IO thread

2.3.1.3 Purge Thread

事务被提交后,其所使用的undo log可能不再需要,因此需要PurgeThread来回收已经使用并分配的undo页
在配置文件中可以添加如下命令来启用独立的PurgeThread:

1
2
[mysqld]
innodb_purge_threads=1

2.3.1.4 Page Cleaner Thread

负责脏页的刷新工作

2.3.2 内存

内存块由三大部分组成,分别是缓冲池,重做日志缓冲,额外的内存池三部分组成 InnoDB内存数据对象

2.3.2.1 缓冲池

缓冲池用来解决磁盘读取速度与CPU速度之间的巨大鸿沟

  • 读操作:从磁盘读取数据页到缓冲区(称为fix),读取时先看是否命中缓存,再考虑是否读磁盘中的页
  • 写操作:首先修改缓存中的页,然后以一定的频率刷新到磁盘上。一定的频率指的是checkpoint技术(在下文进行讨论)

可以通过修改参数innodb_buffer_pool_size来设置缓冲池大小,可以通过修改参数innodb_buffer_size来设置缓冲池的个数

缓冲池存储的数据页类型有:

  • 索引页
  • 数据页
  • undo页
  • 插入缓冲(insert buffer)
  • 自适应哈希索引(adaptive hash index)
  • 锁信息(lock info)
  • 数据字典信息(data dictionary)

2.3.2.1.1 管理缓冲池的相关技术:

LRU List:

  • LRU列表用来管理已经读取的页
  • InnoDB使用了一种改进的LRU算法,新的页不是放在最前面,而是放在为midpoint处,midpoint为从后往前数3/8处
  • 使用midpoint的原因(即改进LRU的原因):常见的索引或数据的扫描操作会连续读取大量的页,甚至是全表扫描,这就导致缓冲区被全部替换,导致一些latest recent use也被替换了(即导致重读磁盘)。若将前面部分保留,将新的数据插到中后位置则可以避免这种情况。靠后的页仍旧被淘汰。可以通过参数innodb_old_blocks_pct修改midpoint,默认为37,表示为37%,即3/8
  • midpoint将内存块分为两部分,前面的为new表,后面的为old表。innodb_old_blocks_time用于表示当读到old表中的数据时,需要等待多久时间将这些old表中的数据提到LRU表头部(即new表头部)。默认为0,即不等待。从old表提到new表中的这种操作叫做page made young。old表中没有被提到new表中的则为page not not made young
  • 压缩页。从InnoDB 1.0.x版本开始支持压缩页功能,将原本的16kB的页压缩成1KB、2KB、4KB、8KB。对于非16KB的页,使用unzip_LRU列表对不同大小的页进行分类管理。使用伙伴算法进行内存分配,例如分配一个4KB大小的页面,进行如下步骤:1)检查unzip_LRU中是否有4KB的空闲页 2)若有则用,否则从8KB分裂为2个4KB,否则从16KB分裂为1个8KB、2个4KB

Free List:

  • 数据库刚启动时LRU表是空的,没有任何页,这时页都放在Free列表中。
  • 当需要从缓冲池中分页时,从Free表中查看是否有空闲页,若有就将其删除,放到LRU表中(即维护一个空白内存池?)

Flush List:

  • LRU列表中被修改的页被称为脏页(即内存与磁盘中的数据不一致),Flush List便是用来存储脏页的
  • 脏页既存在LRU列表中,也存在Flush列表中
  • LRU用来管理页的可用性,Flush用来管理将页刷新回磁盘,两者不影响

2.3.2.2 重做日志缓冲

重做日志信息先被放到缓冲区,然后按照一定频率刷新到重做日志文件中

参数innodb_log_buffer_size管理重做日志缓冲区的大小,默认为8MB

8MB一般够用,因为:

  • Master Thread每一秒将重做日志缓冲刷新到重做日志文件中
  • 每个事务提交时会将重做日志缓冲刷新到重做日志文件
  • 当重做日志缓冲池剩余空间小于1/2时,重做日志缓冲刷新到重做日志文件

2.3.2.3 额外的内存池

  • 一些数据结构本身的内存分配时,需要从额外的内存池中进行申请,当该区域的内存不够时,会从缓冲池中进行申请

  • 申请了很大的InnoDB缓冲池也应该相应的增加额外内存池的大小

2.4 checkpoint技术

checkpoint技术的目的是解决这几个问题:

  • 缩短数据库恢复的时间(checkpoint之前的数据已经刷新到磁盘,故可以大幅度缩减时间)
  • 缓冲池不够用时,将脏页刷新到磁盘(缓冲池不够用时,LRU将末尾的页抛弃,若它们为脏页,则强制checkpoint写回磁盘)
  • 重做日志不可用时,刷新脏页。重做日志被设计为循环使用(因为空间有限)。不可用,指的是仍被占用,不可以重新循环使用。部分重做日志可以重用,是因为它们的对应的数据已经写入磁盘。若想要使用那些仍被占用的页,则需要强制checkpoint

具体的两种checkpoint技术sharp checkpointfuzzy checkpoint

2.4.1 sharp checkpoint

  • 在数据库关闭时将所有的脏页都刷新回磁盘,这是默认的工作方式,即参数innodb_fast_shutdown=1
  • 若时时刻刻都sharp checkpoint将严重影响mysql性能,故有了fuzzy checkpoint

2.4.2 四种fuzzy checkpoint

Master Thread Checkpoint:

  • 以一定频率从缓冲池的脏页列表中刷新一定比例的页回磁盘

FLUSH_LRU_LIST checkpoint:

  • InnoDB需要保证差不多有100个空闲页,当不足时将LRU表的末尾页强制checkpoint(当他们时脏页时),然后丢弃
  • 从InnoDB1.2.x开始,用一个Page Cleaner线程进行以上操作,使用参数innodb_lru_scan_depth控制LRU列表中可用页的数量,默认为1024

Async/Sync Flush Checkpoint

  • 发生在重做日志不可用时,这时候需要将一些页刷新回磁盘,此时脏页是从脏页列表中选取。
  • 具体的执行步骤定义如下:
    定义: LSN(log sequence number),8byte长,一种记号方法。
    记已经写入到重做日志的页的LSN为redo_lsn
    记已经刷新回磁盘的页的LSN为checkpoint_lsn, 有如下定义:
    checkpoint_age = redo_lsn - checkpoint_lsn
    async_water_mark = 0.75 * total_redo_log_file_size
    sync_water_mark = 0.9 * total_redo_log_file_size
    则:
  1. 当checkpont_age < async_water_mark时,不需要刷新任何脏页到磁盘
  2. 当async_water_mark < checkpoint_age < sync_water_mark时,触发async flush, 从Flush列表中刷新足够多的脏页回磁盘,使得刷新后满足checkpoint_age < async_water_mark
  3. checkpont_age > sync_water_mark这种情况,一般出现很少(除非设置的重做文件太小,并进行类似LOAD DATA的BULK INSERT操作),触发sync flush,同样从Flush列表中刷新脏页回磁盘,使得刷新后满足checkpoint_age < async_water_mark

Dirty Page too much checkpoint:

  • 脏页太多触发checkpoint
  • 可由参数innodb_max_dirty_pages_pct进行控制

2.5 InnoDB 关键特性

2.5.1 Innsert Buffer和change buffer(待续)

  • 缓冲区中有insert buffer的信息, 但是insert buffer和数据页一样,也是物理页的一个组成部分
  • 对于非聚集索引的插入或更新操作,不是每一次直接插入到索引页中,而是先到缓冲池中寻找,若不在,则放入到一个insert buffer中,然后再以一定频率进行insert buffer和辅助索引页子节点的merger操作
  • insert buffer使用的条件:(1)索引是辅助索引(2)索引不是唯一(unique)的
  • 若数据库发生宕机,大量insert buffer中的数据没有合并到实际的非聚集索引中,这时恢复时间可能需要很长时间
  • IBUF_POOL_SIZE_PER_MAX_SIZE代表insert buffer占缓冲池内存的大小

change buffer:

  • 从1.0.x版本引进,可视为insert buffer 的升级版
  • 使用参数innodb_change_buffering来开启各种buffer选项,选项内容为:inserts,deletes,purges,changes,all,none,默认是all
  • 从1.2.x版本开始,可以通过参数innodb_change_buffer_max_size来控制change buffer最大使用内存的数量, 默认值为25,表示占用1/4的缓冲池空间

insert buffer的内部实现(待续)

2.5.2 两次写

两次写提升数据页的可靠性,解决部分写失效的问题。

部分写问题的定义:

  • 写到一半发生宕机
  • 用重做日志无法解决这个问题,因为重做日志中记录的是对页的物理操作,而这时可能页已经损坏了。

double write的大致描述:
应用(apply)重做日志之前,用户需要一个页的副本,当写入失效时,先通过页的副本还原该页,再进行重做

double write具体实现:
double write模型

  • double write由两部分组成,一部分是内存中的double write buffer,大小为2MB,另一部分是物理磁盘上共享表空间中连续的128个页,即2个区(extent),大小同样为2MB
  • 对缓冲池中的脏页进行刷新时,用memcpy函数将脏页复制到double write buffer中,再从double write buffer中分两次,每次1MB顺序的写入到共享表空间的物理磁盘上,然后马上调用fsync函数,同步磁盘,避免缓冲写带来的问题
  • 错误恢复:如果写磁盘的过程中发生了崩溃,在恢复的过程中,可以从共享表空间中的doublewrite中找到该页的一个副本,将其复制到表空间文件,然后再应用重做日志

可使用show global status like 'innodb_dblwr%';查询double write的运行情况, 生产环境中可以通过查看Innodb_dblwr_pages_written来统计写入的量

2.5.3 自适应哈希索引

  • InnoDB存储引擎会监控对表上各索引页的查询,若观察到建立哈希索引能带来速度提升,则建立哈希索引,称之为自适应哈希索引(Adaptive Hash Index, AHI)
  • AHI是通过缓冲池的B+树页构造而来,因此建立的速度很快,而且不需要对整张表构造哈希索引
  • InnoDB会自动根据访问的频率和模式来自动地为某些热点页建立哈希索引
  • AHI开启的要求: 1)对这个页的访问模式必须一致 2)以该模式访问了100次 3)页通过该模式访问了N次,N=页的记录/16

2.5.4 异步IO

  • 异步,如字面意思,避免阻塞带来的低效率
  • AIO的另一个优势是可以执行IO merge操作,如用户需要访问(space, page_no)这样格式的三个页:(8,6),(8,7),(8,8),则可以合并为读取一次。
  • 通过innodb_use_native_aio用来控制是否启用native AIO

2.5.5 刷新邻接页

  • 刷新脏页时,可以检测其他脏页中是否有这个脏页的相邻页,若有就一起刷新(即多个IO合成一个)
  • 提供参数innodb_flush_neighbors来控制是否启用这个特性
  • 推荐机械硬盘开启这个特性,而像固态硬盘那样有着超高IOPS性能的磁盘,建议关闭。

2.6 启动关闭与恢复

关闭时,参数innodb_fast_shutdown影响InnoDB的行为,可取值0,1,2;默认1:

  • 值为0时, InnoDB将完成所有的full purge和merge insert buffer,并将所有的脏页刷新回磁盘。这可能需要很长时间来完成。升级InnoDB时必须选择此项
  • 值为1时, 不需要完成full purge和merge insert buffer,但是缓冲池中的一些数据脏页还是会刷新回磁盘
  • 值为2时, 不完成full purge,merge insert buffer, 数据脏页也不处理,而是将日志写入日志文件,保证不丢失事务,下次启动时进行恢复

参数innodb_force_recovery影响整个InnoDB存储引擎的恢复状况,默认为0:

  • 值为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_SCAN)不查看undo log,将未提交的事务视为已提交
  • 值为6时(SRV_FORCE_NO_LOG_REDO)不进行前滚的操作

当设置的参数值大于0后,用户可以进行select,create和drop操作,其他DML操作如insert,update,delete是不允许的