前言
这篇文章是讲述 InnoDB 刷盘策略系列文章的第三篇。本文主要讲述 性能调优。另外2篇文章参考
https://www.percona.com/blog/2020/01/22/innodb-flushing-in-action-for-percona-server-for-mysql/
https://www.percona.com/blog/2019/12/18/give-love-to-your-ssds-reduce-innodb_io_capacity_max/
理解调优原理是非常重要的,因为我们不想将业务性能变的更糟糕,也不想SSD 磁盘被烧坏。我们将按照小节来介绍每个优化参数。
了解调优原理非常重要,因为我们不想让事情变得更糟或烧毁我们的 SSD。我们将每个变量或密切相关的变量按照章节的方式进行介绍。这些变量根据 MySQL 或Percona Server for MySQL版本和是否有效进行分组。如果给定变量的含义在版本之间发生变化,则给定变量可能会多次出现。可能还有其他变量会影响 InnoDB 处理写入密集型工作负载的方式。希望本文能涵盖最重要的内容。
MySQL 8.0.19 之前的版本
innodb_io_capacity
该参数的默认值是200,如果你阅读过我们之前写的文章, innodb_io_capacity
定义了 InnoDB 后台线程刷脏页时的 iops 个数。
The
innodb_io_capacity
variable defines the number of I/O operations per second (IOPS) available to InnoDB background tasks, such as flushing pages from the buffer pool and merging data from the change buffer.
自适应刷新独立于 innodb_io_capacity
。一般来说 调大 innodb_io_capacity
的原因很少:
减少变更缓冲滞后
增加空闲刷新率(当 LSN 恒定时)
提高脏页百分比刷新率
我们很难找到任何理由来增加空闲刷新率 。此外,不应使用脏页刷新百分比(见下文)。这会导致更换器缓冲滞后。如果在执行“show engine innodb status\G”时,更改缓冲区中经常有大量条目,如下所示:
-------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
-------------------------------------
Ibuf: size 1229, free list len 263690, seg size 263692, 40050265 merges
merged operations:
insert 98911787, delete mark 40026593, delete 4624943
discarded operations:
insert 11, delete mark 0, delete 0
如果有 1229 个未应用的更改,您可以考虑提高 innodb_io_capacity
。
innodb_io_capacity_max
当 innodb_io_capacity
的值为默认值时, innodb_io_capacity_max
的值为2000。该参数控制每秒 InnoDB 刷新多少 IOPS。(官方文档提示 以iops 为单位,原博里面的单位是 page )
If flushing activity falls behind, InnoDB can flush more aggressively, at a higher rate of I/O operations per second (IOPS) than defined by the
innodb_io_capacity
variable. Theinnodb_io_capacity_max
variable defines a maximum number of IOPS performed by InnoDB background tasks in such situations.
这与 InnoDB 生成的写入 IOPS 大致匹配。您的硬件应该能够每秒执行那么多页面刷新。如果您看到以下消息:
[Note] InnoDB: page_cleaner: 1000ms intended loop took 4460ms. The settings might not be optimal. (flushed=140, during the time.)
从上面的 MySQL 日志中可以看出来, 硬件的 IO 能力跟不上InnoDB 刷脏的速度,(理论上应该1000毫秒内完成的动作实际上花费4460毫秒将脏页刷新到磁盘,它接受脏页的数量远远大于它每秒能够处理脏页的能力。) 所以你应该降低 innodb_io_capacity_max
的值。
理想值应允许checkpoint age尽可能高,同时保持在最大检checkpoint age的 75% 以下。checkpoint age 越高,数据库需要执行的写入操作就越少。同时,如果 checkpoint age 超过 75%,您将面临停顿的风险。需要进行权衡,写入性能与短期停顿的风险。根据您面临的性能痛点和短停顿的风险选择合理的值。如果您部署了Percona 监控和管理(PMM),“MySQL InnoDB 详细信息”仪表板中的 "InnoDB Checkpoint Age" 非常有用。下面是一个借助 sysbench 生成的简单示例:
负载从 15:02 开始,checkpoint age 迅速达到 20% 左右。然后,在 15:13,innodb_io_capacity_max
降低,checkpoint age 提高到 50% 左右。这种提高导致刷新率下降了大约一半,这可以在另一个 PMM 图“InnoDB Data I/O”中看到
Percona Server for MySQL 还公开了所有相关变量:
mysql> show global status like 'innodb_ch%';
+---------------------------+------------+
| Variable_name | Value |
+---------------------------+------------+
| Innodb_checkpoint_age | 400343763 |
| Innodb_checkpoint_max_age | 1738750649 |
+---------------------------+------------+
2 rows in set (0.00 sec)
mysql> show global variables like 'innodb_adaptive_fl%';
+------------------------------+-------+
| Variable_name | Value |
+------------------------------+-------+
| innodb_adaptive_flushing | ON |
| innodb_adaptive_flushing_lwm | 10 |
+------------------------------+-------+
2 rows in set (0.00 sec)
使用上面的示例,checkpoint age 大约是最大值的 23%,远高于 innodb_adaptive_flushing_lwm
中定义的 10%。如果checkpoint age是数据库负载下的典型值并且它永远不会提高到更高的值,则 innodb_io_capacity_max
可以降低一点。相反,如果 checkpoint age 经常接近甚至超过 75%,则 innodb_io_capacity_max
的值太小。
page_cleaner_thread
:脏页清理线程负责将脏页从内存写到磁盘。因此为了避免该问题,可降低每秒循环期间搜索脏页的深度(innodb_lru_scan_depth
)和 降低innodb_io_capacity
的值,减少每秒 io 负载。
innodb_max_dirty_pages_pct
innodb_max_dirty_pages_pct_lwm
这2个参数控制 MySQL 5.0 版本的刷新方式,作为缓冲池中脏页百分比的函数。应该通过将 innodb_max_dirty_pages_pct_lwm
设置为 0 来禁用它。5.7 中的默认值为0,但奇怪的是,在 8.0 中的默认值是10 。
注释:
1 当innodb中的脏页比例超过
innodb_max_dirty_pages_pct_lwm
的值时,这个时候innodb就会开始刷新脏页到磁盘。2 当innodb中的脏页比例超过
innodb_max_dirty_pages_pct_lwm
的值,而且还超过innodb_max_dirty_pages_pct
时 innodb就会把脏页更快的刷新到磁盘。3 还有一种情况叫做sharp checkpoint ,当innodb要重用之前的redo文件时,就会把
innodb_buffer_pool
中所有与这个文件有关的页面都要刷新到磁盘,这样有可能引起磁盘的IO风暴,影响性能和系统可用性。
innodb_max_dirty_pages_pct_lwm
=0 时,表示禁用,此时innodb_buffer_pool
中的脏页比例会操持在75%左右。
innodb_page_cleaners
innodb_page_cleaners
控制刷新脏页的线程数,专门用于扫描缓冲池实例中的脏页并刷新它们。此外,您不能拥有比缓冲池实例更多的 刷新线程数。此变量的默认值为 4。
当该参数为4时,InnoDB使用 4 线程 能够以非常高的速率刷新。实际上,除非您使用 Percona Server for MySQL parallel doublewrite buffers feature
功能,否则双写缓冲区很可能会在更干净的线程之前成为瓶颈。
innodb_purge_threads
innodb_purge_threads
控制 用于清除操作的线程数。清除操作 清理已经被删除的行和 undo log。这些线程还会截断磁盘上撤消空间。清除线程的默认数量为 4。
通常情况下,业务压力很难能够使 4 个清除线程达到饱和状态,默认值是足够的。清除线程的大部分工作都在内存中完成。除非您的业务数据库有非常大的缓冲池和高写入负载,否则默认值 4 就足够了。如果您在“show engine innodb status”中看到比较高的 history length 的值而没有任何长时间运行的事务,则可以考虑增加清除线程的数量
大多数业务场景可以不用调整,写入负载比较大(比如 tps 大于 2000 甚至5000 以上 )的还是需要调整到合适的值。
innodb_read_io_threads
和 innodb_read_io_threads
令人惊讶的是,在具有异步 IO 的 Linux 上,这些线程几乎没有相关性。例如,我们的一个客户的实例配置 16 个读取 io 线程和 16 个写入 io 线程。数据库实例正常运行时间约为 35 天,mysqld 进程消耗 104 小时 CPU,读取 281 TB,并将 48 TB 写入磁盘。数据集主要由压缩的 InnoDB 表组成。您可能认为对于这样的服务器,IO 线程会很忙,对吧?让我们来看看:
root@prod:~# ps -To pid,tid,stime,time -p $(pidof mysqld)
PID TID STIME TIME
72734 72734 Mar07 00:23:07
72734 72752 Mar07 00:00:00 -> ibuf (unused)
72734 77733 Mar07 00:01:16
72734 77734 Mar07 00:01:51
72734 77735 Mar07 00:05:46 -> read_io
72734 77736 Mar07 00:06:04 -> read_io
72734 77737 Mar07 00:06:06 -> read_io
… 11 lines removed
72734 77749 Mar07 00:06:04 -> read_io
72734 77750 Mar07 00:06:04 -> read_io
72734 77751 Mar07 00:35:36 -> write_io
72734 77752 Mar07 00:34:02 -> write_io
72734 77753 Mar07 00:35:14 -> write_io
… 11 lines removed
72734 77766 Mar07 00:35:18 -> write_io
72734 77767 Mar07 00:34:15 -> write_io
我们添加了注释以帮助识别 IO 线程,并删除了许多无关的行。读取 IO 线程的平均 CPU 时间为 6 分钟,而写入 IO 线程的 CPU 时间稍高一些,为 35 分钟。这些都是非常小的数字,显然,设置这两个参数的值为 16 在这两种情况下都太多了。默认值为 4 时,读取 IO 线程的 CPU 时间约为 25 分钟,写入 IO 线程的 CPU 时间约为 2 小时。在 34 天的正常运行时间中,这些时间不到正常运行时间的 1%。
innodb_lru_scan_depth
innodb_lru_scan_depth
是一个命名非常糟糕的变量。更好的名称是 innodb_free_page_target_per_buffer_pool
,该参数控制InnoDB 试图在每个缓冲池实例中保持空闲的页面数量,以加快读取和页面创建操作。该变量的默认值为 1024。
这个变量的危险在于,如果该参数设置比较大的值 且数据库实例 有多个 buffer pool instances ,可能会导致浪费大量内存。对于 64 个缓冲池实例,默认值1024 表示 1GB 空闲内存。设置此变量的最佳方法是通过查看“show engine innodb status\G” ,并使用 grepping 查找“Free buffers”。这是一个例子。
mysql> pager grep 'Free buffers'
PAGER set to 'grep 'Free buffers''
mysql> show engine innodb status\G
Free buffers 8187
Free buffers 1024
Free buffers 1023
Free buffers 1022
Free buffers 1020
Free buffers 1024
Free buffers 1025
Free buffers 1025
Free buffers 1024
1 row in set (0.00 sec)
第一行是所有buffer pools free pages 的总数,InnoDB 为每个缓冲池实例保留大约 1024 个空闲页面。所有buffer pool instance 最低保留1020个page 。根据经验,您可以使用当前变量值(此处为 1024)减去相当数量样本中的最小观察值,然后将结果乘以 4。如果1020 是最小值,根据算法得到 (1024-1020)*4 ~ 16
。变量的最小允许值是 128。我们计算了 16,小于 128 的值,所以应该使用 128。
innodb_flush_sync
设置 innodb_flush_sync
=ON , 当checkpoint age 大于 94% 时 ,允许 InnoDB 刷新磁盘忽略 innodb_io_capacity_max
值。通常这是正确的,因此默认值为“ON”。唯一的例外是读取的 SLA 非常严格的情况。允许 InnoDB 使用超出 innodb_io_capacity_max
的 IOPS 进行刷新会与读取负载竞争,并且会增加读的响应时间。
innodb_log_file_size
and innodb_log_files_in_group
参数 innodb_log_file_size
and innodb_log_files_in_group
控制 redo log 文件大小以及 redo日志组内的文件数量。本质上,redo log 记录最新的数据库改变的日志。日志文件最大大小为 512GB 减去一个字节。较大的重做日志环形缓冲区允许页面在缓冲池中保持更长时间的脏状态。如果在此期间,数据库接收更多的更新写入操作,则对磁盘的写压力基本上是减弱的。减轻写负载有利于提高性能,尤其是数据库是IO-bound 类型的。当然一个非常大的重做日志会导致崩溃后的恢复时间需要更多时间,但现在这已经不是什么问题了。
这里翻译不是很通顺, 我在理解是 如果写压力比较大,可以提高
innodb_log_buffer_size
的值 64M 甚至更高,redo log file 设置1-2G ,减少日志频繁覆盖切换。
官方文档的解释:
The size in bytes of the buffer that InnoDB uses to write to the log files on disk. The default is 16MB. A large log buffer enables large transactions to run without the need to write the log to disk before the transactions commit.
innodb_adaptive_flushing_lwm
innodb_adaptive_flushing_lwm
参数控制 adaptive flushing 的最低水位,到达该值,则触发 adaptive flushing。默认值是10 ,最大值为70 。
设置 innodb_adaptive_flushing_lwm
比较高的好处和设置一个比较大的 innodb_log_file_size
类似,但它使数据库更接近最大checkpoint age 的边缘。使用较高的低水位值,一般情况下,写入性能会更好,但可能会出现短暂的hang。对于生产环境,增加 Innodb_log_file_size
优于增加 innodb_adaptive_flushing_lwm
。但是,暂时地,动态提高该值可以加速逻辑导出或提高slave 追赶master 的速度。(打开非双1模式和并行复制更有效)
innodb_flushing_avg_loops
该参数控制 adaptive flushing 的算法,innodb_flushing_avg_loops
定义了InnoDB保持先前计算的刷新状态快照的迭代次数,控制自适应刷新对前台工作负载变化的响应速度。就是说控制统计前N个page flush速率,避免太快flush。
高的值意味着InnoDB保持先前计算的快照的时间更长,因此自适应刷新响应更慢。如日志空间利用率未达到75%,则应该使用较高的 innodb_flushing_avg_loops
值来保持尽可能平滑的刷新。对于具有极端负载峰值或日志文件比较小的数据库系统,应设置该参数为比较小的值允许 flush 以密切跟踪工作负载更改,并有助于避免达到 75% 的日志空间利用率。
调优的时候很少修改该值。
MySQL Community After 8.0.19
innodb_io_capacity
在 MySQL 8.0.19 之前,当 InnoDB 需要在刷新磁盘时,它会刷新大批量的页面。这种方法的问题是刷新顺序可能不是最佳的,太多的页面可能来自同一个缓冲池实例。
从 MySQL 8.0.19 开始,刷新是以 innodb_io_capacity
大小的块完成的。块之间没有等待时间,因此与 IO 容量没有关系。这种方式有助于更好地平衡 flush 顺序并降低潜在的时间。
这点还没研究 两者之间的差异。
innodb_idle_flush_pct
在 MySQL 8.0.19 中,空闲刷新率不再由 innodb_io_capacity
直接控制,而是乘以 innodb_idle_flush_pct
表示的百分比。InnoDB 将剩余的 IO 容量用于 insert buffer thread. 。当数据库变得空闲时,意味着 LSN 值不会移动,InnoDB 通常会先刷新脏页,然后应用存储在更改缓冲区中的二级索引更改。如果这不是您想要的行为,请降低该参数以便给 change buffer. 提供一些 IO。
Percona Server for MySQL 5.7.x and 8.0.x
innodb_cleaner_lsn_age_factor
Percona Server for MySQL 有一个新的默认自适应刷新算法 high_checkpoint
,它本质上允许更多的脏页。此行为由变量 innodb_cleaner_lsn_age_factor
控制。您可以通过将变量设置为 legacy
来恢复默认的 MySQL 算法。由于您的目标应该是拥有尽可能多的脏页,而不会遇到刷新风暴和停顿,因 high_checkpoint
算法将为您提供帮助。如果你的实例有比较高的 checkpoint age,也许旧算法会更好。有关此主题的更多信息,请参阅我们之前的帖子。
innodb_empty_free_list_algorithm
该变量控制 InnoDB 在缓冲池实例中寻找空闲页面的行为。它有两个值 : legacy 和 backoff 。
设置为legacy :只有在“show engine innodb status\G”中经常有“接近0”时才有意义。在这种情况下,LRU 管理器线程可能会获取 free list mutex 并导致与其他线程的争用。
设置为backoff时,线程将在找不到空闲页面后休眠一段时间以降低争用。默认为backoff ,通常在大多数情况下没问题。
该参数是Percona Server 独有的。MySQL 社区版无该参数。
结论
还有其他的参数影响 InnoDB 的写操作,但是本文提到的 这些参数 应该是比较重要的。通常来说,不要过度调整数据库实例并尝试一次更改一个设置。
以上我们对影响刷新的 InnoDB 变量的理解,并且这些参数在各种 Percona 客户场景中得到实际验证。我们还花费大量时间阅读代码以了解 InnoDB 的运行机制。像往常一样,我们对评论持开放态度,但如果可能,请尝试通过引用代码或可重现的用例来支持任何反对我们观点的论点。
参考
https://dev.mysql.com/doc/refman/5.7/en/optimizing-innodb-diskio.html https://dev.mysql.com/doc/refman/5.7/en/innodb-buffer-pool-flushing.html https://www.percona.com/blog/2020/01/22/innodb-flushing-in-action-for-percona-server-for-mysql/