InnoDB 存储引擎的主要工作都是在一个单独的后台线程 Master Thread 中完成的,这一节将具体解释该线程的具体实现及该线程可能存在的问题。
1、InnoDB 1.0.x 版本之前的Master Thread
Master Thread 具有最高的线程优先级别。其内部由多个循环( loop )组成:主循环 ( loop )、后台循环( background loop )、刷新循环( flush loop )、暂停循环( suspend loop )。 Master Thread 会根据数据库运行的状态在 loop 、 background loop 、 flush loop 和 suspend loop 中进行切换。
Loop被称为主循环,因为大多数的操作是在这个循环中,其中有两大部分作 ― 每秒钟的操作和每 10 秒的操作。伪代码如下:
vold master thread() {
loop : for (int i = 0 ; i < 10 ; i ++){
do thing once per second
sleep 1 second if necessary
}
dothings once per ten seconds
goto loop;
}
可以看到, for循环通过 thread sleep来实现,这意味着所谓的每秒一次或每 10 秒一次的操作是不精确的。在负载很大的情况下可能会有延迟( delay ) ,只能说大概在这个频率下。当然, InnoDB 源代码中还通过了其他的方法来尽量保证这个频率。每秒一次的操作包括:
- 日志缓冲刷新到磁盘,即使这个事务还没有提交(总是) ;
- 合并插人缓冲(可能) ;
- 至多刷新 100 个 InnoDB 的缓冲池中的脏页到磁盘(可能) ;
- 如果当前没有用户活动,则切换到 background loop (可能)。
即使某个事务还没有提交, InnoDB 存储引擎仍然每秒会将重做日志缓冲中的内容刷新到重做日志文件。这一点是必须要知道的,因为这可以很好地解释为什么再大的事务提交( commit )的时间也是很短的。合并插入缓冲( Insert Buffer )并不是每秒都会发生的。 InnoDB 存储引擎会判断当前一秒内发生的 IO 次数是否小于 5 次,如果小于 5 次, InnoDB 认为当前的 IO压力很小,可以执行合并插人缓冲的操作。同样,刷新 100 个脏页也不是每秒都会发生的。 InnoDB 存储引擎通过判断当前缓冲池中脏页的比例( buf_get_modified_ratio_pct )是否超过了配置文件中 innodb_max_dirty_pages_pct 这个参数(默认为 90 ,代表 90 % ) ,如果超过了这个阑值, InnoDB 存储引擎认为需要做磁盘同步的操作,将 100 个脏页写人磁盘中。总结上述操作,伪代码可以进一步具体化,如下所示:
void master thread() {
goto loop;
loop:
for (int i = 0; i < 10;i++){
thread_sleep(1);
do log buffer flush to disk
if (last_one_second_ios < 5)
do merge at most 5 instert buffer;
if (buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct)
do buffer pool flush 100 dirty page
if (no user activity )
goto backgroud loop
}
do things once per ten seconds
background loop:
do something
goto loop:
}
接着来看每 10 秒的操作,包括如下内容:
- 刷新 100 个脏页到磁盘(可能的情况下) ;
- 合并至多 5 个插人缓冲(总是) ;
- 将日志缓冲刷新到磁盘(总是) ;
- 删除无用的 Undo页(总是) ;
- 刷新 100 个或者 10 个脏页到磁盘(总是)。
在以上的过程中, InnoDB 存储引擎会先判断过去10秒之内磁盘的 IO 操作是否小于 200 次,如果是, InnoDB 存储引擎认为当前有足够的磁盘IO操作能力,因此将 100 个脏页刷新到磁盘。接着, InnoDB 存储引擎会合并插人缓冲。不同于每秒一次操作时可能发生的合并插人缓冲操作,这次的合并插入缓冲操作总会在这个阶段进行。之后, InnoDB 存储引擎会再进行一次将日志缓冲刷新到磁盘的操作。这和每秒一次时发生的操作是一样的。
接着 InnoDB 存储引擎会进行一步执行 full purge 操作,即删除无用的 undo 页。对表进行 update 、 delete 这类操作时,原先的行被标记为删除,但是因为一致性读( consistent read )的关系,需要保留这些行版本的信息。但是在 full purge 过程中, InnoDB 存储引擎会判断当前事务系统中已被删除的行是否可以删除,比如有时候可能还有查询操作需要读取之前版本的 undo 信息,如果可以删除, InnoDB 会立即将其删除。
从源代码中可以发现, InnoDB 存储引擎在执行 full purge操作时,每次最多尝试回收20个undo页。
然后InnoDB 存储引擎会判断缓冲池中脏页的比例( buf_get _modified_ratio_pct ) , 如果有超过 70 %的脏页刷新 100个脏页到磁盘,如果脏页的比例小于 70 % ,则只需刷新10%个脏页到磁盘。
现在我们可以完整地把主循环( main loop)的伪代码写出来了,内容如下:
接着来看 background loop ,若当前没有用户活动(数据库空闲时)或者数据库关闭 ( shutdown ) ,就会切换到这个循环。 background loop 会执行以下操作:
- 删除无用的 Undo页(总是) ;
- 合并 20 个插人缓冲(总是) ;
- 跳回到主循环(总是) ;
- 不断刷新 100 个页直到符合条件(可能跳转到 flush loop 中完成)。
若 flush foop 中也没有什么事情可以做了, InnoDB 存储引擎会切换到 loop ,将 Master Thread 挂起,等待事件的发生。若用户启用( enable )了 InnoDB 存储引擎,却没有使用任何 InnoDB 存储引擎的表,那么 Master Thread 总是处于挂起的状态。最后,Master Thread 完整的伪代码如下:
2、InnoDB 1.2.x版本之前的 Master Thread
在了解了 1.0.x 版本之前的 Master Thread 的具体实现过程后,细心的读者会发现 InnoDB 存储引擎对于IO其实是有限制的,在缓冲池向磁盘刷新时其实都做了一定的硬编码( hard coding )。在磁盘技术飞速发展的今天,当固态磁盘( SSD )出现时,这种规定在很大程度上限制了 InnoDB 存储引擎对磁盘 IO 的性能,尤其是写入性能。
从前面的伪代码来看,无论何时, InnoDB 存储引擎最大只会刷新 100 个脏页到磁盘,合并 20 个插人缓冲。如果是在写入密集的应用程序中,每秒可能会产生大于 100 个的脏页,如果是产生大于 20 个插人缓冲的情况, Master Thread 似乎会“忙不过来” , 或者说它总是做得很慢。即使磁盘能在 l 秒内处理多于 100 个页的写入和 20 个插人缓冲的合并,但是由于 hard coding , Master Thread 也只会选择刷新 100 个脏页和合并 20 个插入缓冲。同时,当发生宕机需要恢复时,由于很多数据还没有刷新回磁盘,会导致恢复的时间可能需要很久,尤其是对于 insert buffer 来说。
这个问题最初由 Google 的工程师 Mark Callaghan 提出,之后 InnoDB 官方对其进行了修正并发布了补丁( patch )。 InnoDB 存储引擎的开发团队参考了 Google 的 patch ,提供了类似的方法来修正该问题。因此 InnoDB Plugin (从 InnoDB 1.0.x 版本开始)供了参数 innodb_io_capacity ,用来表示磁盘IO的吞吐量,默认值为 200 。对于刷新到磁盘页的数量,会按照 innodb_io_capaoity 的百分比来进行控制。规则如下:
- 在合并插人缓冲时,合并插人缓冲的数量为 innodb_io_capacity 值的 5 % ;
- 在从缓冲区刷新脏页时,刷新脏页的数量为 innodb_io_capacity 。
若用户使用了 SSD 类的磁盘,或者将几块磁盘做了 RAID ,当存储设备拥有更高的 IO 速度时,完全可以将 innodb_io_capacity的值调得再高点,直到符合磁盘IO的吞吐量为止。另一个问题是,参数 innodb_max_dirty_pages_pct 默认值的问题,在 InnoDB 1.0.x 版本之前,该值的默认为 90 ,意味着脏页占缓冲池的 90 %。但是该值“太大”了,因为 InnoDB 存储引擎在侮秒刷新缓冲池和 flush loop 时会判断这个值,如果该值大于innodb_max_dirty_pages_pct ,才刷新 1 00 个脏页,如果有很大的内存,或者数据库服一务器的压力很大,这时刷新脏页的速度反而会降低。同样,在数据库的恢复阶段可能需要更多的时间。
在很多论坛上都有对这个问题的讨论,有人甚至将这个值调到了 20 或 10 ,然后测试发现性能会有所提高,但是将 innodb_max_dirty_pages_pct 调到 20 或 10 会增加磁盘的压力,系统的负担还是会有所增加的。 Google 在这个问题上进行了测试,证明 20 并不是一个最优值。而从 InnoDB 1.0.x 版本开始,innodb_max_dirty_pages_pct默认值变为了 75 ,和 Google测试的 80 比较接近。这样既可以加快刷新脏页的频率,又能保证了磁盘 IO的负载。
InnoDB 1.0.x 版本带来的另一个参数是 innodb_adaptive_fiushing (自适应地刷新) , 该值影响每秒刷新脏页的数量。原来的刷新规则是:脏页在缓冲池所占的比例小于innodb_max_dirty_pages_pct时,不刷新脏页;大于innodb_max_dirty_pages_pct时,刷新 100 个脏页。随着innodb_adaptive_fiushing参数的引入, InnoDB 存储引擎会通过一个名为 buf_fiush_get_desired_flush_rate 的函数来判断需要刷新脏页最合适的数量。粗略地翻阅源代码后发现 buf_fiush_get_desired_flush_rate 通过判断产生重做日志( redo log )的速度来决定最合适的刷新脏页数量。因此,当脏页的比例小于 innodb_max_dirty_pages_pct 时,也会刷新一定量的脏页。
还有一个改变是:之前每次进行 full purge 操作时,最多回收 20个Undo 页,从 InnoDB 1.0.x版本开始引人了参数 innodb_purge_batch_size ,该参数可以控制每次 full purge 回收的 undo页的数量。该参数的默认值为 20(MySQL5.7为300) ,并可以动态地对其进行修改,具体如下:
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=50;
Query OK, 0 rows affected (0.01 sec)
通过上述的讨论和解释我们知道,从 InnoDB 1.0.X版本开始, Master Thread伪代码必将有所改变,最终变成:
很多测试都显示, InnoDB 1.0.x 版本在性能方面取得了极大的提高,和前面提到的 Master Thread 的改动是密不可分的,因为 InnoDB 存储引擎的核心操作大部分都集中在 Master Thread 后台线程中。从 InnoDB 1.0.x 开始,命令 SHOW ENGINE INNODB STATUS 可以查看当前 Master Thread 的状态信息,如下所示:
这里可以看到主循环进行了 45 次,每秒挂起( sleep )的操作进行了 45 次(说明负载不是很大) , 10秒一次的活动进行了4 次,符合 1 : 10 。 background loop 进行了 6 次, flushfo 叩也进行了 6 次。因为当前这台服务器的压力很小,所以能在理论值上运行。如果是在一台压力很大的 MySQL 数据库服务器上,看到的可能会是下面的情景:
可以看到当前主循环运行了 2188 次,但是循环中的每秒挂起( sleep )的操作只运行了 1537 次。这是因为 lnnoDB 对其内部进行了一些优化,当压力大时并不总是等待1秒。因此,并不能认为1_second 和 sleeps 的值总是相等的。在某些情况下,可以通过两者之间差值的比较来反映当前数据库的负载压力。
3、InnoDB 1. 2.x 版本的 Master Thread
在 InnoDB 1. 2.x 版本中再次对 Maste Thread 进行了优化,由此也可以看出 Master Thread 对性能所起到的关键作用。在 InnoDB 1. 2.x 版本中,Master Thread 的伪代码如下:
其中 sry_master_do_idle_tasks()就是之前版本中每 10 秒的操作, sry_master_do_active_tasks()处理的是之前每秒中的操作。同时对于刷新脏页的操作,从 Master Thread 线程分离到一个单独的 Page Cleaner Thread ,从而减轻了 Master Thread 的工作,同时进一步提高了系统的并发性。