背景 adaptive flushing 分析 InnoDB 采用 adaptive flushing (自适应刷脏)的刷脏策略来处理从 Buffer Pool 写入脏页至磁盘. 如何理解 adaptive flushing 的作用,我们可以假设不采用自适应刷脏策略,我们该如何进行刷脏? 假如没有自适应刷脏算法,我们可以利用阈值的方式来进行刷脏,比如 Buffer Pool 的脏页比例达到了 70% 就触发刷脏,在一般的业务压力下,这个方法没有问题. 但是对于用户业务不确定的场景, 简单的采用阈值的方式容易造成在用户业务压力大的情况下数据库的剧烈抖动. 所以采用自适应的刷脏策略,尽可能在所有的用户场景达到系统平滑运行.
相关参数
innodb_flushing_avg_loops: 重新生成刷脏建议的间隔时间, 默认30s.
innodb_adaptive_flushing: 是否打开自适应刷脏.
innodb_adaptive_flushing_lwm: 设置 Redo Log 空闲容量的低水位.
innodb_max_dirty_pages_pct_lwm: 设置一个脏页低水位, 当 InnoDB中 的脏页比例超过innodb_max_dirty_pages_pct_lwm
的值时, InnoDB 就会触发刷脏.
innodb_max_dirty_pages_pct: 设置一个脏页比例上限,假如脏页比例超过这个值,将会触发激烈刷脏(达到系统 IO 上限).
innodb_io_capacity: 系统 IO 吞吐能力.
innodb_io_capacity_max: 系统最大的 IO 吞吐能力.
原理分析 当启用 innodb_max_dirty_pages_pct_lwm
参数时, 表示设置了预刷脏,Buffer Pool 的刷脏线程会避免脏页比超过这个值. 后台刷脏的动作由后台刷脏协调线程触发,该线程的所有工作内容均由buf_flush_page_cleaner_coordinator()
函数完成. 在执行刷脏任务前,会调用 page_cleaner_flush_pages_recommendation()
生成刷脏建议.
函数page_cleaner_flush_pages_recommendation()
生成的建议刷脏的 Page 数量是 adaptive flushing 自适应刷脏策略的核心,它每隔srv_flushing_avg_loops
秒(默认30s)重新根据redo log产生的速度 ,参考当前刷脏的平均数量 和设置的系统IO参数(innodb_io_capacity
, innodb_io_capacity_max
) 三者的平均值生成一个合理的建议刷脏的Page数量. 下面我们分别对这三个调节因子做出对应的解释.
Redo Log 产生的平均速度 因为刷脏协调线程会每隔srv_flushing_avg_loops
生成一次刷脏建议,关于 Redo Log 产生的平均速度公式即为:
1 2 3 4 5 6 7 8 9 lsn_rate = static_cast <lsn_t >(static_cast <double >(cur_lsn - prev_lsn) / time_elapsed); lsn_avg_rate = (lsn_avg_rate + lsn_rate) / 2 ;
计算 Redo Log 产生的平均速度这个比较好理解,Redo Log 产生的平均速度反应了当前系统的压力情况,压力越大,Redo Log 产生的速度越快. 在 MySQL 中 Redo Log 是复用的,经过 Log Checkpoint 操作之前的 Redo Log 都可以被复用, 所以 Log Checkpoint 本身就会推进 Buffer Pool 的刷脏, 所以为了保证数据库有足够空闲的 Redo Log 空闲, 自适应刷脏同样需要考虑 Redo Log 产生的速度.
InnoDB 根据当前lsn_avg_rate
来估算一个target_lsn, flush_list
所有oldest_modification_lsn
小于该 lsn 值的 Page 都被考虑进行刷盘. 估算公式如下:
1 lsn_t target_lsn = oldest_lsn + lsn_avg_rate * buf_flush_lsn_scan_factor;
因子buf_flush_lsn_scan_factor
被硬编码为3. 由上面的公式 InnoDB 的刷脏协调线程遍历 Buffer Pool 估算出flush_list
需要被刷脏的 Page 数量, 但最后的数量会再除以buf_flush_lsn_scan_factor
.
当前刷脏的平均速度 考虑将当前刷脏的平均速度作为影响因子可能的原因应该是避免生成的建议刷脏 Page 数量与上次刷脏的数量差距过大或过小. 这也符合自适应刷脏的初衷,尽力避免IO抖动.
系统IO参数 通过我们设置的innodb_io_capacity
和innodb_io_capacity_max
可以得出系统IO的能力, 通过计算flush_list
占所有 Page 数量的百分比我们可以得出脏页比. 根据脏页比与innodb_max_dirty_pages_pct
大小比较我们决定是否触发激烈刷脏, 假如超过了innodb_max_dirty_pages_pct
设定的大小,我们即认为需要全力进行刷脏了, 这里会充分调动系统的IO能力. 否则则需要与innodb_max_dirty_pages_pct_lwm
比较,从而考虑利用系统多少的IO带宽.
通过上面的计算,我们将这三个因子的建议刷脏 Page 数量计算平均值,得出综合建议刷脏Page数量,由变量n_pages
保存. 接下来,这个建议刷新的总量n_pages
与innodb_io_capacity_max
这个参数进行比较,即建议刷新的总量最大不能超过所设置的磁盘最大随机IO能力。
最后我们需要为每个 Buffer Pool 设置n_pages_requested
, 即要求的刷脏 Page 数量. 具体的细节我们将在下节的源码分析展出.
源码分析 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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 static ulint page_cleaner_flush_pages_recommendation (lsn_t *lsn_limit, ulint last_pages_in) { static lsn_t prev_lsn = 0 ; static ulint sum_pages = 0 ; static ulint avg_page_rate = 0 ; static ulint n_iterations = 0 ; static time_t prev_time; lsn_t oldest_lsn; lsn_t cur_lsn; lsn_t age; lsn_t lsn_rate; ulint n_pages = 0 ; ulint pct_for_dirty = 0 ; ulint pct_for_lsn = 0 ; ulint pct_total = 0 ; cur_lsn = log_buffer_dirty_pages_added_up_to_lsn (*log_sys); if (prev_lsn == 0 ) { prev_lsn = cur_lsn; prev_time = ut_time (); return (0 ); } if (prev_lsn == cur_lsn) { return (0 ); } sum_pages += last_pages_in; time_t curr_time = ut_time (); double time_elapsed = difftime (curr_time, prev_time); if (++n_iterations >= srv_flushing_avg_loops || time_elapsed >= srv_flushing_avg_loops) { if (time_elapsed < 1 ) { time_elapsed = 1 ; } avg_page_rate = static_cast <ulint>( ((static_cast <double >(sum_pages) / time_elapsed) + avg_page_rate) / 2 ); lsn_rate = static_cast <lsn_t >(static_cast <double >(cur_lsn - prev_lsn) / time_elapsed); lsn_avg_rate = (lsn_avg_rate + lsn_rate) / 2 ; prev_lsn = cur_lsn; prev_time = curr_time; n_iterations = 0 ; sum_pages = 0 ; } oldest_lsn = buf_pool_get_oldest_modification_approx (); ut_ad (oldest_lsn <= log_get_lsn (*log_sys)); age = cur_lsn > oldest_lsn ? cur_lsn - oldest_lsn : 0 ; pct_for_dirty = af_get_pct_for_dirty (); pct_for_lsn = af_get_pct_for_lsn (age); pct_total = ut_max (pct_for_dirty, pct_for_lsn); ulint sum_pages_for_lsn = 0 ; lsn_t target_lsn = oldest_lsn + lsn_avg_rate * buf_flush_lsn_scan_factor; for (ulint i = 0 ; i < srv_buf_pool_instances; i++) { buf_pool_t *buf_pool = buf_pool_from_array (i); ulint pages_for_lsn = 0 ; buf_flush_list_mutex_enter (buf_pool); for (buf_page_t *b = UT_LIST_GET_LAST (buf_pool->flush_list); b != NULL ; b = UT_LIST_GET_PREV (list, b)) { if (b->oldest_modification > target_lsn) { break ; } ++pages_for_lsn; } buf_flush_list_mutex_exit (buf_pool); sum_pages_for_lsn += pages_for_lsn; mutex_enter (&page_cleaner->mutex); ut_ad (page_cleaner->slots[i].state == PAGE_CLEANER_STATE_NONE); page_cleaner->slots[i].n_pages_requested = pages_for_lsn / buf_flush_lsn_scan_factor + 1 ; mutex_exit (&page_cleaner->mutex); } sum_pages_for_lsn /= buf_flush_lsn_scan_factor; if (sum_pages_for_lsn < 1 ) { sum_pages_for_lsn = 1 ; } ulint pages_for_lsn = std::min <ulint>(sum_pages_for_lsn, srv_max_io_capacity * 2 ); n_pages = (PCT_IO (pct_total) + avg_page_rate + pages_for_lsn) / 3 ; if (n_pages > srv_max_io_capacity) { n_pages = srv_max_io_capacity; } mutex_enter (&page_cleaner->mutex); for (ulint i = 0 ; i < srv_buf_pool_instances; i++) { page_cleaner->slots[i].n_pages_requested = pct_for_lsn > 30 ? page_cleaner->slots[i].n_pages_requested * n_pages / sum_pages_for_lsn + 1 : n_pages / srv_buf_pool_instances; } mutex_exit (&page_cleaner->mutex); *lsn_limit = LSN_MAX; return (n_pages); }
总结 InnoDB 的自适应刷脏比较容易理解,重要的是提供了一种对于系统开发过程中对于容易造成性能瓶颈的关键路径优化思路,例如基于 LSM 设计的 RocksDB 中的 compaction 过程经常造成IO瓶颈从而饱受诟病,参考 InnoDB 的自适应刷脏算法针对不同的IO压力选择合适的 compaction 时机是否能使系统更平滑?