Page Cleaner

InnoDB 通过独立的线程将Buffer Pool中的脏页刷入存储中。这些线程称作Page Cleaner. Page Cleaner的线程数量通过系统参数--innodb-page-cleaners控制。刷脏是以Buffer Pool实例为单位进行的。一个Buffer Pool实例同时只能有一个Page Cleaner进行刷脏操作。因此Page Cleaner的数量最多和Buffer Pool实例的数量相同(--innodb-buffer-pool-instances)。


角色

Page Cleaner有两种角色:

  • Coordinator

  • Worker

Coordinator负责决策何时进行刷脏、刷入的脏页数量。Workers则负责刷脏的具体动作。Coordinator只有一个,Workers可以有多个。Page Cleaner的线程中有一个线程同时承担Coordinator和Worker两个角色。


Coordinator和Workers的交互过程

InnoDB的刷脏机制_java

Page Cleaner模块有一个Buffer Pool的Flush状态数组,每个元素对应一个Buffer Pool实例。

  • 当开始一轮刷脏时,Coordinator会设置所有Buffer Pool的Flush状态为Requested,以及刷脏的数量参数。然后唤醒Workers.

  • 在完成Coordinator的工作后,转变为Worker进行刷脏。

  • Worker线程被唤醒后,遍历Buffer Pool的Flush状态数组。找到第一个状态为Requested的Buffer Pool 实例,将其状态设置为Flushing,然后开始刷脏。刷脏完毕后,将其状态设置为Finished。接着查找下一个状态为Requested的Buffer Pool.

  • 当所有的Buffer Pool都刷完脏页后,Worker进入休眠状态。Coordinator 线程重新切换为Coordinator,计算下一轮刷脏的时间。


刷脏的模式

InnoDB的刷脏可以归纳为两大类:

  • 异步刷脏

  • 同步刷脏

异步刷脏是指系统没有强制的刷脏要求,Page Cleaner根据自己的节奏刷脏。而同步刷脏是指系统明确要求要将哪些脏页立刻刷入存储中,这时Page Cleaner要根据系统的要求进行刷脏。

异步刷脏又可以分为3个小类:

  • 基本模式

  • 脏页自适应模式

  • Redo自适应模式

  • 空闲模式

基本模式主要根据Redo的产生速度来决定刷脏速度。脏页自适应模式根据Redo的产生速度和脏页的使用比例共同来决定刷脏速度。通过innodb_max_dirty_pages_pct_lwm和innodb_max_dirty_pages_pct来控制。Redo自适应模式根据Redo的产生速度和Redo的使用比例共同来决定刷脏速度。通过innodb_adaptive_flushing和innodb_adaptive_flushing_lwm参数控制。脏页自适应模式和Redo自适应模式可以同时开启。自适应模式下的刷脏会更加平稳。


刷脏频率

通常情况下,Coordinator每1秒启动一轮刷脏,完成刷脏后,进入休眠状态。在这过程中其他线程也可以唤醒Coordinator,立刻启动一轮刷脏。立刻启动刷脏的情形有:

  • DDL创建内部临时表时(用在重建表或者创建索引上),每个Page(PageBulk)操作完成后。

  • 用户线程无法获取到空闲页时。

  • Checkpointer线程需要发起同步刷脏时。



最大频率刷脏

最大频率刷脏也称作(IO Burst)。它指的是:在一轮刷脏完成后不再休眠,立即启动下一轮刷脏。在同步刷脏时,会采用最大频率来刷脏。其他模式下都是1秒刷脏一次。


刷脏数量

#define PCT_IO(p) ((ulong)(innodb_io_capacity * ((double)(p) / 100.0)))n_pages = (PCT_IO(pct_total) + avg_page_rate + pages_for_lsn) / 3;if (n_pages > innodb_max_io_capacity) {   n_pages = innodb_max_io_capacity;}
刷脏数量是基于以下的几个条件计算的:
  • 根据LSN的平均产生速度(lsn_avg_rate)计算的刷脏数量

  • 根据IO Capacity、Redo的使用比例以及脏页的比例计算的刷脏数量

  • 平均每秒的刷脏数量(avg_page_rate)

三者的平均值即为本次的刷脏量(n_pages),而n_pages又受innodb_io_capacity_max的限制,不能超过innodb_io_capacity_max

这三者中,lsn_avg_rate 和 avg_page_rate计算比较简单,有固定的算法进行计算。而IO Capacity的使用比例(pct_total)的计算则要区分不同的模式和情形。


Redo Log中的几个概念

  • Checkpoint 是一个lsn,这个lsn之前的Redo空间是Free部分,可以被覆盖了。recovery 时从这个lsn开始重做redo.

  • Lastest lsn 是所有脏页的最小lsn。这个lsn之前产生的脏页都已经持久化。

  • Current lsn是当前产生的最大lsn.

  • Modified Age是指所有脏页的Redo Log占用的空间,即Lastest lsn和Current lsn之间的部分。它是判断是否要使用同步刷脏的重要指标。

  • 另外还有一个Checkpoint Age是Checkpoint到Current lsn的空间,即redo的使用空间。Checkpoint Age和Modified Age差别不大。状态信息中显示给用户的是Checkpoint Age.


LSN的平均产生速度

lsn的平均速度由Coordinator进行统计。计算方法如下:

// 一个计算周期内的lsn产生速度lsn_rate = cur_lsn - prev_lsn / time_elapsed;// 平均速度lsn_avg_rate = (lsn_avg_rate + lsn_rate) / 2;
  • coordinator每隔一段时间计算一次这个时间间隔内的lsn产生速率(lsn_rate)。

  • lsn_rate和lsn_avg_rate平均,得出新的lsn_avg_rate。



lsn_avg_rate转换为脏页数

用lsn_avg_rate来计算脏页的逻辑是:一秒中产生了多少redo,就刷多少redo对应的脏页。

lsn_t target_lsn = oldest_lsn + lsn_avg_rate;sum_pages_for_lsn = 计算flush list中所有小于targe_lsn的脏页数pages_for_lsn = min(sum_pages_for_lsn, innodb_max_io_capacity * 2);
  • 首先根据lsn_avg_rate计算出oldest_lsn后1秒的lsn位值(target_lsn).

  • 然后统计flush_lish中小于targe_lsn的所有脏页数sum_pages_for_lsn。

  • 根据lsn_avg_rate计算的脏页数如果超过了innodb_io_capacity_max的2倍,则按innodb_io_capacity_max的2倍算。



刷脏的平均速度

刷脏的平均速度也由Coordinator进行统计。计算方法如下:

// 一个计算周期内的刷脏速度page_rate = sum_pages / time_elapsed;// 平均速度avg_page_rate = (avg_page_rate + page_rate) / 2;
  • coordinator每隔一段时间计算一次这个时间间隔内刷的刷脏速率(page_rate)。

  • page_rate和avg_page_rate平均,得出新的avg_page_rate。

lsn_avg_rate受应用的影响,可能变化比较剧烈。用avg_page_rate参与计算,可以让刷脏数量的变化比较平稳。


innodb_flushing_avg_loops

avg_page_rate和lsn_avg_rate计算间隔由innodb_flushing_avg_loops来控制。这个参数有两个含义:

  • 时间:当间隔时间超过innodb_flushing_avg_loops时计算lsn_avg_rate和avg_page_rate。

  • 刷脏次数:coordiantor刷脏的次数超过innodb_flushing_avg_loops时,计算lsn_avg_rate和avg_page_rate。因为通常的情况下1秒刷脏一次,所以刷脏1次就等同于1秒

innodb_flushing_avg_loops的缺省值是30。每30秒才会计算一次,而且要求平均,所以lsn_avg_rate和avg_page_rate的增加是比较缓慢的。这样做是为了让刷脏尽量的平稳,不至于有太大的抖动。但是这也带来了一个副作用。


在做sysbench测试时,prepare阶段会创建Index,coordinator在1秒内会被唤醒很多次。从而导致avg_page_rate非常的大。测试刚开始时,仍然会快速刷脏。快速刷脏会导致性能下降,因此测试的结果不准确。如果是从小并发开始测试,性能下降会非常明显。所以在测试前要休眠足够的时间来等待avg_page_rate回归到一个正常的值。休眠的过程中,每innodb_flushing_avg_loops秒,avg_page_rate会下降一半。当然也可以将innodb_flushing_avg_loops设置为一个较小的值来减少等待时间。


IO Capacity的使用比例

IO Capacity有两个参数Innodb_io_capacity_max和Innodb_io_capacity。lsn_avg_rate仅仅受到应用的影响,和用户配置无关。Innodb_io_capacity_max和Innodb_io_capacity则是MySQL提供给用户的接口,用户可以通过这两个参数来影响和限制刷脏的数量。

#define PCT_IO(p) ((ulong)(innodb_io_capacity * ((double)(p) / 100.0)))n_pages = (PCT_IO(pct_total) + avg_page_rate + pages_for_lsn) / 3;if (n_pages > innodb_io_capacity_max)   n_pages = innodb_io_capacity_max

Innodb_io_capacity用来计算刷脏数量,Innodb_io_capacity_max用来限制一次的刷脏数量不能超过Innodb_io_capacity_max页。

IO Capacity使用的比例的计算依赖于两个参数:

  • 根据Redo使用比例计算的IO比例(pct_for_lsn)

  • 根据脏页比例计算的IO比例(pct_for_dirty)



根据Redo使用情况计算的IO比例(pct_for_lsn)

Redo的使用比例是Redo自适应模式下的重要计算依据。


当innodb_adaptive_flushing关闭时分为两种情况:

  • 如果modified_age < max_modified_age_async时,pct_for_lsn为0max_modified_age_async为redo空间的14/16。

  • 如果modified_age >= max_modified_age_async时, pct_for_lsn使用adptive flushing相同的计算方法。(同步模式也符合这个条件)


当innodb_adaptive_flushing开启时也分为两种情况:

  • 如果modified_age < innodb_adaptive_flushing_lwm,pct_for_lsn为0.

  • 如果modified_age >= innodb_adaptive_flushing_lwm, 采用下面的计算公式。(同步模式也符合这个条件)

  lsn_age_factor = (age * 100) / max_modified_age_async;  pct_for_lsn = (innodb_max_io_capacity / innodb_io_capacity) *                (lsn_age_factor * sqrt((double)lsn_age_factor)) /                7.5

为了理解这个公式的作用,我用lsn_age_factor的一些值画了个图。

  • 图中黑色线表示lsn_age_fact和pct_for_lsn按1:1线性增长。

  • 和黑色线相比,蓝色线和橙色线上随着Modified Age(lsn_age_factor)的增大,pct_for_lsn的增大幅度变的更大。

InnoDB adaptive flushing(Redo自适应模式)的本质就是根据Redo的使用情况(Modified Age),动态的调整刷脏速度。Redo使用的少,刷脏速度慢。这时Redo的产生速度 大于刷脏速度,Redo使用空间逐渐变大。随着Redo使用空间的变大,刷脏速度也变快。当刷脏速度大于Redo产生速度后,Redo的使用空间逐渐变小,刷脏速度也变慢。最终,Redo的使用大小(Modified Age)和刷脏速度会固定在一个较小的范围内,上下变化达到平衡。

#define PCT_IO(p) ((ulong)(srv_io_capacity * ((double)(p) / 100.0)))
lsn_age_factor = (age * 100) / limit_for_age;pct_for_lsn = (srv_max_io_capacity / srv_io_capacity) *              (lsn_age_factor * sqrt((double)lsn_age_factor)) /              7.5
n_pages = PCT_IO(pct_for_lsn)        = srv_io_capacity *          (srv_max_io_capacity / srv_io_capacity) *          (lsn_age_factor * sqrt((double)lsn_age_factor)) /          7.5 / 100        = srv_max_io_capacity *          (lsn_age_factor * sqrt((double)lsn_age_factor)) /          7.5 / 100

将pct_for_lsn的计算公式代入刷脏数量的计算公式后,会发现刷脏量的多少实际上是由innodb_io_capacity_max来控制的。而innodb_io_capacity仅仅影响了pct_for_lsn的大小。innodb_io_capacity_max和innodb_io_capacity的差距越大,pct_for_lsn的值就越大。pct_total就越可能选择pct_for_lsn。


根据脏页计算的IO比例(pct_for_dirty)

脏页的比例是脏页自适应模式下的重要计算依据。

当innodb_max_dirty_pages_pct_lwm为0时:

  • 如果dirty_pct < innodb_max_dirty_pages_pct,pct_for_dirty为0

  • 如果dirty_pct >= innodb_max_dirty_pages_pct, pct_for_dirty为100

当innodb_max_dirty_pages_pct_lwm不为0时:

  • 如果dirty_pct < innodb_max_dirty_pages_pct_lwm,pct_for_dirty为0

  • 如果dirty_pct >= innodb_max_dirty_pages_pct_lwm,则用下面的公式计算。

pct_for_dirty = (dirty_pct * 100) /                (innodb_max_dirty_pages_pct + 1)

脏页自适应模式的本质就是根据脏页的比例,动态的调整刷脏速度。脏页少,刷脏速度慢。这时脏页的产生速度 大于刷脏速度,脏页逐渐变多。随着脏页变多,刷脏速度也变快。当刷脏速度大于脏页的产生速度后,脏页的数量逐渐变少,刷脏速度也变慢。最终,脏页的数量和刷脏速度会固定在一个较小的范围内,上下变化达到平衡。

和Redo自适应模式不同,在脏页自适应模式下,刷脏量是由innodb_io_capacity来控制的。


脏页自适应和Redo自适应同时开启

pct_total = max(pct_for_dirty, pct_for_lsn);n_pages = (PCT_IO(pct_total) + avg_page_rate + pages_for_lsn) / 3;

当两种模式同时开启时,两者会分别计算出一个IO的使用比例。coordinator取两者中较大的来计算刷脏量。哪种模式的比例更大,用的更多主要取决于以下几个因素:

  • Buffer Pool和Redo Log大小的比例

  • innodb_io_capacity_max和innodb_io_capacity之间的比例

  • 操作类型

通常来说,Redo Log的大小比较固定,不会特别大,也不会特别小。大实例的Buffer Pool很大,所以大实例主要以Redo自适应模式刷脏。小实例,主要以脏页自适应模式刷脏。


MySQL手册上说将innodb_io_capacity_max设置为innodb_io_capacity的两倍是个不错的选择。如果采用这个设置,pct_for_lsn会翻倍那实例用Redo自适应模式刷脏的可能性就增加了很多。


另外操作类型也会有影响,如果短时间内对同一个数据页做多次修改,脏页数就比较少,比如全表更新,或者自增数据插入操作。


同步刷脏速度的最小值

同步刷脏时,除了刷脏频率会增大。刷脏的数量也会提高到innodb_io_capacity以上。刷脏数量的计算仍然采用Redo自适应模式的计算公式。但当计算的刷脏量小于innodb_io_capacity时,则将innodb_io_capacity记做刷脏数量。

#define PCT_IO(p) ((ulong)(innodb_io_capacity * ((double)(p) / 100.0)))  n_pages = (PCT_IO(pct_total) + avg_page_rate + pages_for_lsn) / 3;  ...if (n_pages < innodb_io_capacity)  n_pages = innodb_io_capacity;


空闲刷脏速度

当系统没有数据页的修改、事务提交、回滚等写相关的操作时,会被认为是空闲状态。空闲状态下,coordinator仍然是1秒执行一轮刷脏。刷脏的速度会提升,计算方法如下:

n_pages = innodb_io_capacity * innodb_idle_flush_pct / 100

innodb_idle_flush_pct的缺省值是100,可以认为当系统空闲时,次刷innodb_io_capacity个脏页。如果innodb_idle_flush_pct设置为0,则空闲状态是不刷脏的。所以永远不要设置为0.


IO Capacity的设置

从上面的计算过程可以知道innodb_io_capacity和innodb_io_capacity_max和系统的IOPS相关,但又不等同于IOPS。它们指的是数据页的数量。因此在设置之前要测试数据页块大小的IOPS。比如数据页是16K的大小,则要测一下16K块大小下的系统IOPS。IO工具fio的功能很丰富,非常适合用来模拟InnoDB的IO情形。

MySQL中主要的IO读写包括:

  • Binlog写操作

  • Redo写操作

  • InnoDB数据页的写操作

  • InnoDB数据页的Double Write

  • InnoDB数据页的读操作

InnoDB的读写信息可以从information_schema.innodb_metrics中查询到。

InnoDB的刷脏机制_java_02

  • buffer_data_written是包括redo在内的所有写的总和

  • os_log_bytes_written是Redo记录的数据量

  • log_padded是Redo write ahead多写部分,无效数据量。

  • innodb_dblwr_page_written是double write的数据量。脏页的数据量和double write是一样的。


同步刷脏(Sync Flush)

InnoDB的刷脏机制_java_03

同步刷脏是Checkpointer和Page Cleaner之间的同步机制。Checkpointer通常情况下做的是Fuzzy Checkpoint。Checkpoint时只是将checkpoint lsn前进到脏页的最小lsn(Latest lsn)。但是在有些情形下,需要执行sharp checkpoing,即checkpoint 要前进到一个特定的lsn。而往往有一些小于这个lsn的脏页存在,需要立刻持久化。这时checkpointer线程会唤醒Page Cleaner的Coordinator线程,并且告诉Coordinator要将小于这个特定lsn(buf_flush_sync_lsn)前的所有脏页刷入存储。需要使用同步刷脏的情形包括:

  • 系统初始化和关闭的过程中

  • 导入Tablespace

  • 设置变量innodb_rollback_segments

  • 当Log Writer发现Redo空间用完时

  • Checkpointer发现Redo空间快用完时


max_modified_age_sync

为了避免Redo空间用完,InnoDB中定义了max_modified_age_sync等于redo空间的15/16.当Modified Age超过了max_modified_age_sync时,Checkpointer会发起同步刷脏。此时checkpointer 会将buf_flush_sync_lsn设置为:

buf_flush_sync_lsn=(current_lsn-log.max_modified_age_sync)+lsn_avg_rate*3

Coordinator进入同步刷脏模式

当Coordinator发现buf_flush_sync_lsn大于latest lsn时,进入同步刷脏模式,即采用最大频率刷脏,直到buf_flush_sync_lsn小于latest lsn. 代码中coordinator并没有直接使用latest lsn。而是用recent_closed的大小来判断,latest_lsn就在checkpoint_lsn和checkpoint_lsn+recent_closted.capacity()之间。

lsn_limit = buf_flush_sync_lsn;lag = recent_closed.capacity();if (srv_flush_sync && lsn_limit > checkpoint_lsn + lag) {  最大频率刷脏}

同步刷脏导致性能抖动

同步刷脏会导致瞬间的大量IO,导致性能的抖动,如下图所示。用户可以通过 innodb_flush_sync参数控制是否要开启同步刷脏功能。

根据InnoDB的性能监控信息可以看出是由于Modified Age超过max_modified_age_sync而导致的同步刷脏。


8.0以前的同步刷脏

8.0以前的同步刷脏,并不会采用最大频率刷脏。也不会用Redo自适应模式的计算公式来计算刷脏量。8.0以前的同步刷脏是一次性的将同步要求的所有脏页刷入存储中。


LRU刷脏

Bufffer Pool中有LRU List和Flush List。前面说的刷脏都是按照Flush List来刷脏的。当Buffer Pool的Free Page快用完时,也会按照LRU来刷脏。

LRU刷脏的数量不通过Coordinator来控制,是worker根据Buffer Pool的Free Page的多少,自动进行的。

innodb-lru-scan-depth是用来控制LRU刷脏的,有2个含义:

  • 当Buffer Pool的Free Page数量小于innodb-lru-scan-depth时,才进行LRU刷脏

  • 每次LRU刷脏,扫描的页数不超过innodb-lru-scan-depth个。