MySQL 的 InnoDB 存储引擎使用 Redo Log 记录事务对数据的更改,以便在系统崩溃恢复时能够重做这些更改,从而保证事务的持久性。对于产生的 Redo Log,InnoDB 存储引擎首先将其写入内存中的 Log Buffer,随后再将 Log Buffer 中的 Redo Log 写入磁盘中的日志文件。本文首先介绍 MySQL 8.0.30 中 Redo Log 对应的日志文件的格式,然后介绍如何在 MySQL 中查看 Redo 文件的信息,最后介绍和 Redo Log 相关的自适应检查点。
如果还不了解 LSN、mini-transaction、Log Buffer 等概念,推荐阅读 “MySQL 日志篇:Redo Log Buffer”。
Redo 文件格式
在 MySQL 8.0.30 之前,Redo Log 的容量由 innodb_log_file_size
和 innodb_log_files_in_group
两个参数控制。前者设置每个 Redo 文件的大小,默认值为 48 MB;后者设置 Redo 文件的数量,默认值为 2。由于 MySQL 不允许在运行时修改这两个参数,因此只有在停止 MySQL 后才能调整 Redo Log 的容量。
从 MySQL 8.0.30 开始,新增了 innodb_redo_log_capacity
参数来设置 Redo Log 的容量,默认值为 100 MB。与上述两个参数不同的是,MySQL 允许在运行时修改 innodb_redo_log_capacity
,这意味着能在运行时动态调整 Redo Log 的容量。MySQL 将这种特性称为 “InnoDB Dynamic Redo Log”。除此之外,Redo 文件的数量也从 2 个变为了 32 个,并放在 #innodb_redo
目录下。下图展示了 Redo 文件的格式。
上图中,自底向上依次为:
- 32 个 Redo 文件,这是磁盘上实际存储 Redo Log 的位置。Redo 文件分为
#ib_redo[0-9]+
和#ib_redo[0-9]+_tmp
两类。前者是活跃的 Redo 文件,已经写入 Redo Log 并且还未被回收。后者是备用的 Redo 文件,还未写入 Redo Log 或者已经被回收; - 一个 Redo 文件由若干个大小为 512 字节的 Block 构成。前四个 Block 是文件头,负责存储元数据;后面的 Block 是数据块,负责存储 Redo Record;
- 不同类型的 Block 作用不同,格式也不同。上图展示了每种 Block 包含的字段以及每个字段的字节大小,下面逐一介绍每种 Block。
Header Block
Header Block 负责维护 Redo 文件的元数据,其关键字段包括:
start lsn
:Redo 文件的起始 LSN,加上文件大小得到终止 LSN。通过比较一个 LSN 是否落在一个 Redo 文件的起始 LSN 到终止 LSN 之间,就能确定该 LSN 是否在该 Redo 文件中;log flags
:Redo 文件的标志位,标志包括LOG_HEADER_FLAG_NO_LOGGING
(禁用 Redo Log)、LOG_HEADER_FLAG_CRASH_UNSAFE
(异常退出)、LOG_HEADER_FLAG_NOT_INITIALIZED
(未初始化 Redo 文件)、LOG_HEADER_FLAG_FILE_FULL
(Redo 文件已写满)。
创建一个 Redo 文件之后,它的起始 LSN 和终止 LSN 就固定了。除非改变这个 Redo 文件的容量,或者回收这个 Redo 文件后再次使用。
MySQL 在使用一个 Redo 文件之前,会先检查它的标志位。只有通过检查后才会使用这个 Redo 文件。一旦检查没有通过,那么就会报警甚至退出。
Checkpoint Header Block
Checkpoint Header Block 负责维护检查点对应的 LSN,其关键字段包括:
checkpoint lsn
:检查点对应的 LSN,用于在崩溃恢复时确认恢复的起点。
只有当一个检查点的 LSN 位于一个 Redo 文件的起始 LSN 和终止 LSN 之间时,才会使用这个 Redo 文件的 Checkpoint Header Block。每个 Redo 文件都有两个 Checkpoint Header Block,MySQL 会交替使用它们保存检查点的 LSN。除此之外,MySQL 还会在系统表中再记录一次检查点的 LSN。当 MySQL 启动时,会读取每个 Redo 文件的两个 Checkpoint Header Block 以及系统表,以其中最大的检查点的 LSN 作为恢复的起点。
Data Block
Data Block 负责存储 Redo Log 的内容,即 Redo Record。每个 Data Block 除了存储 Redo Record 之外,还有 12 字节的头部和 4 字节的尾部。头部负责记录该 Data Block 的元数据,尾部负责记录该 Data Block 的循环冗余校验码 (CRC)。因此,每个 Data Block 实际最多存储 496 字节的 Redo Record。Data Block 中的关键字段包括:
- (
epoch_no
,hdr_no
):唯一标识一个 Data Block 的全局位置 (跨 Redo 文件); data_len
:Data Block 已写入的字节数。若 Data Block 还没有 Redo Record,则 data_len 为 0。若 Data Block 已经有 Redo Record 但还未写满,则 data_len 为 Redo Record 的总字节数加 12 (头部)。若 Data Block 已经写满,则 data_len 为 512,即 Redo Record 的总字节数加 12 (头部) 再加 4 (尾部);first_rec_group
:Data Block 中第一组 Redo Record 在该 Data Block 中的位置。一个 mini-transaction 中的所有 Redo Record 为一组,一组 Redo Record 要么全做要么全不做 (原子性)。
查看 Redo 文件
在 MySQL 中,执行以下 SQL 可以列出全部 Redo 文件:
SELECT * FROM performance_schema.file_instances WHERE file_name LIKE '%#innodb_redo/%' ORDER BY 1;
-- output
+-------------------------------------------+-------------------------------------+------------+
| FILE_NAME | EVENT_NAME | OPEN_COUNT |
+-------------------------------------------+-------------------------------------+------------+
| /path/to/data/#innodb_redo/#ib_redo0 | wait/io/file/innodb/innodb_log_file | 20 |
| /path/to/data/#innodb_redo/#ib_redo1_tmp | wait/io/file/innodb/innodb_log_file | 1 |
| ... | | |
+-------------------------------------------+-------------------------------------+------------+
如果想要查看 Redo 文件的详细信息,那么可以执行以下 SQL:
SELECT * FROM performance_schema.file_summary_by_instance WHERE file_name LIKE '%#innodb_redo/%' ORDER BY 1 LIMIT 1\G
-- output
*************************** 1. row ***************************
FILE_NAME: /path/to/data/#innodb_redo/#ib_redo0
EVENT_NAME: wait/io/file/innodb/innodb_log_file
OBJECT_INSTANCE_BEGIN: 140737148753472
COUNT_STAR: 1765
SUM_TIMER_WAIT: 191423039274
MIN_TIMER_WAIT: 417852
AVG_TIMER_WAIT: 108454536
MAX_TIMER_WAIT: 2712355560
COUNT_READ: 7
SUM_TIMER_READ: 322288866
MIN_TIMER_READ: 550458
AVG_TIMER_READ: 46040994
MAX_TIMER_READ: 156974022
SUM_NUMBER_OF_BYTES_READ: 68608
COUNT_WRITE: 873
SUM_TIMER_WRITE: 66319956396
MIN_TIMER_WRITE: 1057032
AVG_TIMER_WRITE: 75967497
MAX_TIMER_WRITE: 2691056556
SUM_NUMBER_OF_BYTES_WRITE: 977920
COUNT_MISC: 885
SUM_TIMER_MISC: 124780794012
MIN_TIMER_MISC: 417852
AVG_TIMER_MISC: 140994999
MAX_TIMER_MISC: 2712355560
如果只想查看活跃的 Redo 文件,那么可以执行以下 SQL:
SELECT * FROM performance_schema.innodb_redo_log_files;
-- output
+---------+--------------------------+-----------+----------+---------------+---------+----------------+
| FILE_ID | FILE_NAME | START_LSN | END_LSN | SIZE_IN_BYTES | IS_FULL | CONSUMER_LEVEL |
+---------+--------------------------+-----------+----------+---------------+---------+----------------+
| 0 | ./#innodb_redo/#ib_redo0 | 8192 | 67115008 | 67108864 | 0 | 0 |
+---------+--------------------------+-----------+----------+---------------+---------+----------------+
SELECT file_id, start_lsn, end_lsn,
if(is_full=1,'100%',
concat(round((((
select VARIABLE_VALUE
from performance_schema.global_status
where VARIABLE_NAME='Innodb_redo_log_current_lsn'
)-start_lsn)/(end_lsn-start_lsn)*100),2),'%')) full,
concat(format_bytes(size_in_bytes)," / " ,
format_bytes(@@innodb_redo_log_capacity) ) file_size,
(select VARIABLE_VALUE from performance_schema.global_status
where VARIABLE_NAME='Innodb_redo_log_checkpoint_lsn') checkpoint_lsn,
(select VARIABLE_VALUE from performance_schema.global_status
where VARIABLE_NAME='Innodb_redo_log_current_lsn') current_lsn,
(select VARIABLE_VALUE from performance_schema.global_status
where VARIABLE_NAME='Innodb_redo_log_flushed_to_disk_lsn') flushed_to_disk_lsn,
(select count from information_schema.INNODB_METRICS
where name like 'log_lsn_checkpoint_age') checkpoint_age
FROM performance_schema.innodb_redo_log_files;
-- output
+---------+-----------+----------+--------+----------------------+----------------+-------------+---------------------+----------------+
| file_id | start_lsn | end_lsn | full | file_size | checkpoint_lsn | current_lsn | flushed_to_disk_lsn | checkpoint_age |
+---------+-----------+----------+--------+----------------------+----------------+-------------+---------------------+----------------+
| 0 | 8192 | 67115008 | 28.81% | 64.00 MiB / 2.00 GiB | 19344554 | 19344554 | 19344554 | 0 |
+---------+-----------+----------+--------+----------------------+----------------+-------------+---------------------+----------------+
自适应检查点
对于 Buffer Pool 中的脏页,InnoDB 会周期性地执行检查点将脏页刷入磁盘。一般情况下,InnoDB 不会一次性把所有脏页刷盘,而是每次只把一批脏页刷盘,这称为 Fuzzy Checkpointing。除此之外,InnoDB 还会根据检查点年龄决定减小或加大刷脏页的力度,这称为 Adaptive Checkpointing。自适应检查点与以下变量挂钩:
innodb_redo_log_logical_size
:Redo Log 逻辑大小,即还不能回收的 Redo Log 大小。计算公式为:
c e i l ( w r i t e _ l s n , 512 ) − f l o o r ( o l d e s t _ c o n s u m e d _ l s n , 512 ) 1 \mathrm{ceil}(write\_lsn, 512) - \mathrm{floor}(oldest\_consumed\_lsn, 512)\ \textcolor{red}{^1} ceil(write_lsn,512)−floor(oldest_consumed_lsn,512) 1log_lsn_checkpoint_age
:检查点年龄,小于但接近 Redo Log 逻辑大小。计算公式为:
c u r r e n t _ l s n − l a s t _ c h e c k p o i n t _ l s n current\_lsn - last\_checkpoint\_lsn current_lsn−last_checkpoint_lsninnodb_redo_log_capacity
:Redo Log 容量 (所有活跃的和备用的 Redo 文件的总大小)。默认 100 MB。hard_logical_capacity
:Redo Log 永远不会超过此限制。若达到此限制时等待 1 秒后仍未回收空间,则会尽可能多地写 Redo Log 或者使 InnoDB 崩溃。计算公式为:
≈ 29.8 32 × i n n o d b _ r e d o _ l o g _ c a p a c i t y \approx \frac{29.8}{32} \times innodb\_redo\_log\_capacity ≈3229.8×innodb_redo_log_capacitysoft_logical_capacity
:为避免死锁,InnoDB 禁止用户事务写入的 Redo Log 超过此限制。当超过此限制时,会暂停所有用户线程,并向 Error Log 写入一条日志。计算公式为:
95 % × h a r d _ l o g i c a l _ c a p a c i t y 95\% \times hard\_logical\_capacity 95%×hard_logical_capacityagressive_checkpoint_min_age
:Redo Log 达到该点时,InnoDB 开始全速从Buffer Pool 中清除脏页。为了能更快地回收空间,log checkpointer 线程执行检查点时不会休眠 1 秒。计算公式为:
31 32 × s o f t _ l o g i c a l _ c a p a c i t y \frac{31}{32} \times soft\_logical\_capacity 3231×soft_logical_capacityadaptive_flush_max_age
:Redo Log 达到该点时,log checkpointer 线程将请求并等待 page cleaner 线程清除尽可能多的脏页,以使 checkpoint age 低于此阈值。计算公式为:
30 32 × s o f t _ l o g i c a l _ c a p a c i t y \frac{30}{32} \times soft\_logical\_capacity 3230×soft_logical_capacityadaptive_flush_min_age
:Redo Log 达到该点时,允许继续写入,但加大刷脏页的力度,这将导致性能下降。计算公式为:
28 32 × s o f t _ l o g i c a l _ c a p a c i t y \frac{28}{32} \times soft\_logical\_capacity 3228×soft_logical_capacity
上述变量的关系如下:
c h e c k p o i n t a g e < = l o g i c a l s i z e \mathrm{checkpoint\ age} <= \mathrm{logical\ size} checkpoint age<=logical size
a s y n c f l u s h p o i n t < s y n c f l u s h p o i n t < a g r e s s i v e c h e c k p o i n t p o i n t < s o f t l o g i c a l c a p a c i t y < h a r d l o g i c a l c a p a c i t y < p h y s i c a l c a p a c i t y \mathrm{async\ flush\ point} < \mathrm{sync\ flush\ point} < \mathrm{agressive\ checkpoint\ point} < \mathrm{soft\ logical\ capacity} < \mathrm{hard\ logical\ capacity} < \mathrm{physical\ capacity} async flush point<sync flush point<agressive checkpoint point<soft logical capacity<hard logical capacity<physical capacity
1 \textcolor{red}{1} 1:Redo Log 除了用户线程作为“生产者”外,还有其他线程作为“消费者”,例如 log checkpointer 线程。每个“消费者”用 c o n s u m e d _ l s n consumed\_lsn consumed_lsn 标识其已消费的 Redo Log 的位置, o l d e s t _ c o n s u m e d _ l s n oldest\_consumed\_lsn oldest_consumed_lsn 即为所有“消费者”中最小的 c o n s u m e d _ l s n consumed\_lsn consumed_lsn。
检查点年龄越大,Redo Log 的空闲空间越小。为保证有足够的空闲空间,当检查点年龄达到 adaptive_flush_min_age
时,就会从 Fuzzy Checkpointing 变为 Adaptive Checkpointing,此时刷脏页的力度随着检查点年龄的增大而增大,如下图所示。
在 MySQL 中,执行以下 SQL 可以查看上述变量:
-- checkpoint age
SELECT count log_lsn_checkpoint_age
FROM information_schema.innodb_metrics
WHERE name LIKE 'log_lsn_checkpoint_age';
-- logical size
SELECT CONCAT(variable_value, " (", FORMAT_BYTES(variable_value), ")") innodb_redo_log_logical_size
FROM performance_schema.global_status
WHERE variable_name LIKE 'innodb_redo_log_logical_size';
-- physical capacity (#ib_redo[0-9]+ and #ib_redo[0-9]+_tmp)
SELECT CONCAT(@@innodb_redo_log_capacity, " (", FORMAT_BYTES(@@innodb_redo_log_capacity), ")") innodb_redo_log_capacity;
-- physical capacity (#ib_redo[0-9]+ only)
SELECT CONCAT(variable_value, " (", FORMAT_BYTES(variable_value), ")") innodb_redo_log_physical_size
FROM performance_schema.global_status
WHERE variable_name LIKE 'innodb_redo_log_physical_size';
-- hard limit for logical capacity
SELECT CONCAT(ROUND(@@innodb_redo_log_capacity * 29.8 / 32), " (", FORMAT_BYTES(ROUND(@@innodb_redo_log_capacity * 29.8 / 32)), ")") hard_logical_capacity;
-- soft limit for logical capacity
SELECT CONCAT(ROUND(count * 8 / 7), " (", FORMAT_BYTES(ROUND(count * 8 / 7)), ")") soft_logical_capacity
FROM information_schema.innodb_metrics
WHERE name LIKE 'log_max_modified_age_async';
-- aggressive checkpoint point
SELECT CONCAT(ROUND(count * 31 / 28), " (", FORMAT_BYTES(ROUND(count * 31 / 28)), ")") agressive_checkpoint_min_age
FROM information_schema.innodb_metrics
WHERE name LIKE 'log_max_modified_age_async';
-- sync flush point
SELECT CONCAT(count, " (", FORMAT_BYTES(count), ")") adaptive_flush_max_age
FROM information_schema.innodb_metrics
WHERE name LIKE 'log_max_modified_age_sync';
-- async flush point
SELECT CONCAT(count, " (", FORMAT_BYTES(count), ")") adaptive_flush_min_age
FROM information_schema.innodb_metrics
WHERE name LIKE 'log_max_modified_age_async';
欢迎关注微信公众号:fightingZh