InnoDB 存储引擎 Master Thread 做了什么

InnoDB 1.0 版本前的 Master Thread

  • loop 主循环
  • background loop 后台循环
  • flush loop 刷新循环
  • suspend loop 暂停循环

loop 是主循环,有两大部分操作,每 1 秒钟的操作,每 10 秒钟的操作,伪代码如下

void master_thread(){
	loop:
	for(int i = 0; i < 10 ;i++){
		do thing once per second // 每秒一次的操作
		thread_sleep(1)
	}
	do things once per ten seconds // 每十秒一次的操作
	goto loop;
}

每一秒一次的操作包括什么呢?— loop 主循环

① 日志缓冲刷新到磁盘,无论事务有没有提交(总是,即使事务没有提交,InnoDB 存储引擎依然每秒将重做日志刷新到磁盘,这就解释了,为什么在大的事务提交[ commit ] 时间都很短)

② 合并插入缓冲 - merge insert buffer (可能,前一秒发生的 IO 是否小于 5 次,如果小于 5 次,InnoDB 认为当前 IO 压力很小,可以执行合并插入缓冲操作 - merge insert buffer)

③ 最多刷新 100 个InnoDB 的缓冲池中的脏页到磁盘(可能,如果缓冲池中的脏页比例 buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct,后面的参数是系统变量,默认是 90%)

④ 如果当前没有用户活动,则切换到 background loop(可能

伪代码细化

void master_thread(){
	loop:
	for(int i = 0; i < 10 ;i++){
		thread_sleep(1)
		do log buffer flush to disk                  // 1. 日志缓冲刷新到磁盘
		if ( last_one_second_ios < 5 )
			do merge insert buffer		     // 2. 合并插入缓冲
		if ( buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct )  
			do buffer pool flush 100 dirty pages // 3. 刷新缓冲池中的脏页到磁盘
		if ( no user activity )
			goto background loop                 // 4. 没有用户活动,切换到 background loop
	}
	do things once per ten seconds       // 每十秒一次的操作
	goto loop;
}

每十秒一次的操作包括什么呢?— loop 主循环

① 刷新 100 个InnoDB 的缓冲池中的脏页到磁盘(可能,前十秒发生的 IO 是否小于 200 次,如果小于 200 次,InnoDB 认为当前 IO 压力不大,刷新 100 个 InnoDB 缓冲池中的脏页到磁盘)

② 合并插入缓冲 - merge insert buffer (总是,最多合并 5 个插入缓冲,不同于上面的每一秒操作的第二步,这个操作是必然发生的)

③ 日志缓冲刷新到磁盘,无论事务有没有提交 (总是,和上面的每一秒操作的第一步相同,发生在合并插入缓冲后)

④ 删除无用的 Undo 页(总是,也叫做 full purge 操作,对于表 udpate / delete 的操作,原先的行会被记删除,但为了一致性读 [consistent read] 的关系,需要保留这些行版本的信息 )

在 full purge 操作时,要判断当前事务系统中已经被删除的行是否可以删除,比如,如果有查询操作还需要查询之前版本的行记录 undo 信息,那就不能删除,如果没有,则可以删除

⑤ 刷新 100 个或者 10 个脏页到磁盘(总是,判断缓冲池 [buffer pool] 中脏页的比例 [buf_get_modified_ratio_pct],如果有超过 70% 的脏页,则刷新 100 个脏页;如果比例小于 70%,则只刷新 10个脏页到磁盘)

伪代码再次细化

void master_thread(){
	loop:
	for(int i = 0; i < 10 ;i++){
		thread_sleep(1)
		do log buffer flush to disk                     // 1. 日志缓冲刷新到磁盘
		if ( last_one_second_ios < 5 ) 
			do merge insert buffer		        // 2. 合并插入缓冲
		if ( buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct )  // 默认情况下 innodb_max_dirty_pages_pct = 90%
			do buffer pool flush 100 dirty pages    // 3. 刷新缓冲池中的脏页到磁盘
		if ( no user activity )
			goto background loop                    // 4. 没有用户活动,切换到后台循环 background loop
	}
	// do things once per ten seconds // 每十秒一次的操作
	if ( last_ten_seconds_ios < 200 )
		do buffer pool flush 100 dirty pages	一. 刷新缓冲池中的脏页到磁盘  每 1 秒操作中的 3. 但条件不同,要参考 I/O
	do merge at most 5 insert buffer 		二. 合并插入缓冲 每 1 秒操作中的 2. 
	do log buffer flush to disk			三. 日志缓冲刷新到磁盘 每 1 秒操作中的 1.
	do full purge					四. 删除无用的 Undo 页
	if ( buf_get_modified_ratio_pct > 70% )		五. 刷新缓冲池中的脏页到磁盘  每 1 秒操作中的 3. 条件相似但不同
		do buffer pool flush 100 dirty pages
	else
		do buffer poll flush 10 dirty pages
	goto loop;	
	background loop:			        // 后台循环 background loop
	do something for background loop;
	goto background loop;
}

数据库空闲或者关闭的情况下 — background loop 后台循环

① 删除无用的 Undo 页(总是

② 合并插入缓冲 20 个(总是

③ 跳回到主循环(总是

④ 不断刷新 100 个页直到符合条件(可能,跳转到 flush loop 中完成)

如果 flush loop 中也没有事情做,InnoDB 存储引擎会将 Master Thread 切换到 suspend loop,即挂起 Master Thread

如果用户启用(Enabled)了 InnoDB 存储引擎,但没有一张表是 InnoDB 存储引擎的,那么 Master Thread 一直是 suspend loop (挂起)状态的

伪代码(含有 background loop)

void master_thread(){
	loop:
	for(int i = 0; i < 10 ;i++){
		thread_sleep(1)								// 每 1 秒一次的操作 loop
		do log buffer flush to disk 						// 1. 日志缓冲刷新到磁盘
		if ( last_one_second_ios < 5 )
			do merge insert buffer						// 2. 合并插入缓冲
		if ( buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct )  // 默认情况下 innodb_max_dirty_pages_pct = 90%
			do buffer pool flush 100 dirty pages 			        // 3. 刷新缓冲池中的脏页到磁盘
		if ( no user activity )
			goto background loop 						// 4. 没有用户活动,切换到后台循环 background loop
	}
	// do things once per ten seconds 	        // 每十秒一次的操作 loop
	if ( last_ten_seconds_ios < 200 )
		do buffer pool flush 100 dirty pages	// 一. 刷新缓冲池中的脏页到磁盘 => 每 1 秒操作中的 3. 但条件不同,要参考 I/O
	do merge at most 5 insert buffer 		// 二. 合并插入缓冲 => 每 1 秒操作中的 2. 
	do log buffer flush to disk			// 三. 日志缓冲刷新到磁盘 => 每 1 秒操作中的 1.
	do full purge					// 四. 删除无用的 Undo 页
	if ( buf_get_modified_ratio_pct > 70% )		// 五. 刷新缓冲池中的脏页到磁盘 => 每 1 秒操作中的 3. 条件相似但不同
		do buffer pool flush 100 dirty pages
	else
		do buffer poll flush 10 dirty pages
	goto loop;	
	background loop:					// 后台循环 background loop
	do full purge						// Ⅰ 删除无用的 Undo 页 ==> 每 10 秒操作中的 四.
	do merge 20 insert buffer				// Ⅱ 合并插入缓冲 20 个页 ==> 每 10 秒操作中的 二. => 每 1 秒操作中的 2.
	if ( not idle )
		goto loop;				        // Ⅲ 跳到主循环
	else:
		flush loop:				        // Flush循环 flush loop ,通过 background loop 才能进入,且状态是 idle 空闲的		
		do buffer pool flush 100 dirty pages	        // Ⅳ 不断刷新缓冲池中的 100 个脏页到磁盘,直到跳出循环 ==> 每 10 秒操作中的 五. => 每 1 秒操作中的 3. 
		if ( buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct )
			goto flush loop;
		else:
			suspend loop:                           // Suspend循环 suspend loop,通过 flush loop 才能进入,且 buf_get_modified_ratio_pct < innodb_max_dirty_pages_pct
			suspend_thread()
			waiting event
			if ( event arrived )
				goto loop;
                        goto suspend loop;
	goto background loop;
}

InnoDB 1.0 — InnoDB 1.2.* 版本的 Master Thread

InnoDB 1.0 版本前的 Master Thread 具体实现,从缓冲池(buffer pool)刷新脏页时,很多都是硬编码(hard coding)。

硬编码即没有根据实际运行环境,采取相应的数值配置,比如当前已经出现了 SSD 固态磁盘,举个栗子,如果在 SSD 固态磁盘上,主循环 loop 的每一秒操作,依然采用最多刷新 100 个 InnoDB 的缓冲池中的脏页到磁盘,其实是限制了磁盘性能的,因为 SSD 能做到的不止这些


因此,InnoDB 1.0 版本开始,提供了一个参数 innodb_io_capacity 表示磁盘的 IO 吞吐量,默认值是 200,对于刷新到磁盘的页的数量,会按照 innodb_io_capacity 的百分比进行控制

  • ① 合并插入缓冲操作中,刷新到磁盘的页的数量是 innodb_io_capacity 的 5%
  • ② 缓冲池中刷新脏页时,刷新到磁盘的页的数量时 innodb_io_capacity

还有个问题,innodb_max_dirty_pages_pct 在 innodb 1.0 版本之前都是 90 ,即脏页占用缓冲池 90% ,才刷新脏页到磁盘,这个值太大了,内存越大,该值越大,数据库宕机恢复过程越慢 。

如果 innodb_max_dirty_pages_pct 设置为 10 / 20 会怎样?这样会增加磁盘的 IO 压力,同样不大合适 。

Innodb 1.0 以后,该值被改为 75,即保证了刷新脏页的频率,也保证了磁盘 IO 的负载 。


同时,InnoDB 1.0 引入了一个新的参数,innodb_adaptive_flushing (自适应刷新),影响着每秒刷新脏页的数量 。

原来的刷新规则是:脏页在缓冲池比例 buf_get_modified_ratio_pct 小于 innodb_max_dirty_pages_pct 时,不刷新脏页;大于 innodb_max_dirty_pages_pct 时,刷新 100 个脏页 。

当引入 innodb_adaptive_flushing 后,则通过一个函数 buf_flush_get_desired_flush_rate 通过判断产生重做日志(redo log)的速度觉得最合适的刷新脏页的数量 。因此,当脏页的比例小于 innodb_max_dirty_pages_pct 时,也会刷新一定量的页 。


还有就是,之前每次 full purge 操作,最多回收 20 个 Undo 页,从 InnoDB 1.0 后开始引入了参数 innodb_purge_batch_size,该参数可以控制每次 full purge 回收的 Undo 页的数量,默认是 20,可以动态修改

# 5.7.22 默认已经是 300 个 Undo 页
mysql> show variables like "innodb_purge_batch_size";
+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| innodb_purge_batch_size | 300   |
+-------------------------+-------+
1 row in set (0.01 sec)

mysql> set global innodb_purge_batch_size=350;
Query OK, 0 rows affected (0.00 sec)

那么,InnoDB 1.0 版本开始,到 InnoDB 1.2.* 版本之前 Master Thead 伪代码有所改变,如下

# 表示 * 部分是有改动的内容,或者新增的内容
void master_thread(){
	loop:
	for(int i = 0; i < 10 ;i++){
		thread_sleep(1)								// 每 1 秒一次的操作 loop
		do log buffer flush to disk 						// 1. 日志缓冲刷新到磁盘
*		if ( last_one_second_ios < %5 innodb_io_capacity )
			do merge 5% innodb_io_capacity insert buffer			// 2. 合并插入缓冲
*		if ( buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct )  // * 默认情况下 innodb_max_dirty_pages_pct = 该值被改为 75 %
* 			do buffer pool flush 100% innodb_io_capacity dirty pages 	// 3. 刷新缓冲池中的脏页到磁盘
*		else if ( enable adaptive flush )	                        // * Innodb 1.0 - Innodb 1.2 才新出现
*			do buffer pool desired amount dirty pages
		if ( no user activity )
			goto background loop 						// 4. 没有用户活动,切换到后台循环 background loop
	}
	// do things once per ten seconds 	        // 每十秒一次的操作 loop
*	if ( last_ten_seconds_ios < innodb_io_capacity )
*		do buffer pool flush 100% innodb_io_capacity dirty pages	// 一. 刷新缓冲池中的脏页到磁盘 => 每 1 秒操作中的 3. 但条件不同,要参考 I/O
*	do merge at most 5% innodb_io_capacity insert buffer 		        // 二. 合并插入缓冲 => 每 1 秒操作中的 2. 
	do log buffer flush to disk			                        // 三. 日志缓冲刷新到磁盘 => 每 1 秒操作中的 1.
	do full purge					                        // 四. 删除无用的 Undo 页
	if ( buf_get_modified_ratio_pct > 70% )		                        // 五. 刷新缓冲池中的脏页到磁盘 => 每 1 秒操作中的 3. 条件相似但不同
*		do buffer pool flush 100% innodb_io_capacity dirty pages
	else
*		do buffer poll flush 10% innodb_io_capacity dirty pages
	goto loop;	
	background loop:					// 后台循环 background loop
	do full purge						// Ⅰ 删除无用的 Undo 页 ==> 每 10 秒操作中的 四.
*	do merge 100% innodb_io_capacity insert buffer		// Ⅱ 合并插入缓冲页 ==> 每 10 秒操作中的 二. => 每 1 秒操作中的 2.
	if ( not idle )
		goto loop;				        // Ⅲ 跳到主循环
	else:
		flush loop:				        // Flush循环 flush loop ,通过 background loop 才能进入,且状态是 idle 空闲的		
*		do buffer pool flush 100% innodb dirty pages	// Ⅳ 不断刷新缓冲池中的脏页到磁盘,直到跳出循环 ==> 每 10 秒操作中的 五. => 每 1 秒操作中的 3. 
		if ( buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct )
			goto flush loop;
		else:
			suspend loop:                           // Suspend循环 suspend loop,通过 flush loop 才能进入,且 buf_get_modified_ratio_pct < innodb_max_dirty_pages_pct
			suspend_thread()
			waiting event
			if ( event arrived )
				goto loop;		        // 事件来临,跳入主循环
                        goto suspend loop;	
	goto background loop;
}

可以通过 SQL 语句查询和观察主循环的循环次数

# 5.7.22 版本
mysql > SHOW ENGINE INNODB STATUS\G
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 12 srv_active, 0 srv_shutdown, 754567 srv_idle
srv_master_thread log flush and writes: 754550

# InnoDB 1.0 — InnoDB 1.2.* 版本,书中案例一
mysql > SHOW ENGINE INNODB STATUS\G
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 45 1_second, 45 sleeps, 4 10_second, 6 background, 6 flush
srv_master_thread log flush and wirites: 45 log wirte only: 69

# 主循环进行了 45 次
# 每秒挂起(sleep)的操作进行了 45 次(说明负载不大)
# 10 秒一次的活动进行了 4 次,符合 1 : 10
# background loop 进行了 6 次
# flush loop 也进行了 6 次



# InnoDB 1.0 — InnoDB 1.2.* 版本,书中案例二
mysql > SHOW ENGINE INNODB STATUS\G
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 2188 1_second, 1537 sleeps, 218 10_second, 2 background, 2 flush
srv_master_thread log flush and wirites: 1777 log wirte only: 5816

# 主循环进行了 2188 次
# 每秒挂起(sleep)的操作进行了 1537 次(说明负载压力较大)
### 当负责压力大时,并不总是等待 1 秒(属于 InnoDB 优化)
# 2188 秒一次的活动进行了 218 次,符合 1 : 10
# background loop 进行了 2 次
# flush loop 也进行了 2 次

InnoDB 1.2 版本后的 Master Thread

Master Thread 对性能起关键作用,InnoDB 1.2 版本后的 Master Thread 伪代码如下

if InnoDB is idle
	src_master_do_idle_tasks(); // 之前版本每 10 秒操作
else
	src_master_do_active_tasks(); // 之前版本每 1 秒操作

同时,对于刷新脏页的操作,从 Master Thread 线程分离到一个单独的 Page Cleaner Thread 线程,减轻了 Master Thread 工作,提搞了系统的并发性 。

什么是合并插入缓冲?(更多详情看这里

首先我们创建一个表,表的主键是 id

当执行插入操作时,id 列会自动增长

页中的行记录,会按照 id 顺序存放,不需要随机读取其他页的数据,这样的情况下(聚集索引),插入效率很高

但是,在大部分的应用中,不会只有一个聚集索引,更多情况下,少不了 secondary index - 辅助索引,如下表所示

# 如下表,除了主键聚集索引,还有辅助索引
mysql> create table sec_test (
    -> id int(4) not null primary key auto_increment,
    -> name varchar(20) not null,
    -> age tinyint unsigned not null,
    -> key  idx_name(name));

这时,除了主键聚集索引,还有辅助索引,对于该非聚集索引来讲,叶子节点的插入不是有序的,这时候需要离散的访问非聚集索引页,插入性能变得很低

因此,InnoDB 设计出插入缓冲技术,对于非聚集索引的插入和更新,不是每一次都直接插入索引页,而是先插入到内存,具体做法:

  • ① 索引页在缓存中,直接插入

  • ② 索引页不在缓存中,先将其放入插入缓冲区,再以一定频率和索引页合并

这样将同一个索引页多个插入合并到一个 IO 操作中,提高写性能

这个设计思路和HBase中的 LSM 树有相似之处,都是通过先在内存中修改,到达一定量后,再和磁盘中的数据合并,目的都是为了提高写性能 。具体可参考《HBase LSM 树》,这又再一次说明,学到最后,技术都是相通的(我不懂 LSM 树)

插入缓冲的启用需要满足一下两个条件

  • ① 索引是辅助索引(secondary index)

  • ② 索引不适合唯一的

如果辅助索引是唯一的,就不能使用该技术,原因很简单,因为如果这样做,整个索引数据被切分为 2 部分(一部分在插入缓存,一部分在磁盘上还未加载到缓存),无法保证唯一性

插入缓冲带来的问题,任何一项技术在带来好处的同时,必然也带来坏处。插入缓冲主要带来如下两个坏处:

  • ① 可能导致数据库宕机后实例恢复时间变长。如果应用程序执行大量的插入和更新操作,且涉及非唯一的聚集索引,一旦出现宕机,这时就有大量内存中的插入缓冲区数据没有合并至索引页中,导致实例恢复时间会很长。
  • ② 在写密集的情况下,插入缓冲会占用过多的缓冲池内存,默认情况下最大可以占用1/2,这在实际应用中会带来一定的问题。
posted @ 2021-04-21 21:27  拨云见日z  阅读(139)  评论(0编辑  收藏  举报