-
undo log segment(undo segment) Undo Logs 合集。undo segment 可以被重复使用,但是一次只能由一个事务占用。 -
rollback segment 也就是 Undo Logs 的物理存储区域。 -
undo tablespace rollback segment 被从系统表空间里分离出来后的实际磁盘文件表现形式。
一、MySQL 5.5 时代的 undo 日志
InnoDB 存储引擎中,undo 日志一直都是事务多版本控制中的核心组件,它具有以下的核心功能:
- 事务的回退:事务在处理过程中遇到异常的时候可以 rollback 所做的全部修改。
- 事务的恢复:数据库实例崩溃时,将磁盘的不正确数据恢复到交易前。
- 读一致性:被查询的记录有事务占用,转向回滚段找事务开始前的数据镜像。
没有被活跃事务用到的 undo 日志就可以被 purge 掉了,但 undo 在事务执行过程中,进行的空间分配如何回收,就变成了一个问题。亲历因为一个大事务,导致 ibdata file 到 800G 大小。
在 MySQL5.5 以及之前,大家会发现随着数据库上线时间越来越长,ibdata1 文件(即 InnoDB 的共享表空间,或者系统表空间)会越来越大,这会造成 2 个比较明显的问题:
- 磁盘剩余空间越来越小,到后期往往要加磁盘;
- 物理备份时间越来越长,备份文件也越来越大。
这是怎么回事呢?
原因除了数据量自然增长之外,在 MySQL 5.5 以及之前,InnoDB 的 undo 日志也是存放在 ibdata1 里面的。一旦出现大事务,这个大事务所使用的 undo 日志占用的空间就会一直在 ibdata1 里面存在,即使这个事务已经关闭。虽然这部分空间可以重用, 但文件大小不能更改。
那么问题来了,有办法把上面说的空闲的 undo 日志占用的空间从 ibdata1 里面清理掉吗?答案是没有直接的办法,只能全库导出 sql 文件,然后重新初始化 MySQL 实例,再全库导入。
关于回滚段的,只有这个主要的参数,用来设置多少个 rollback segment。
1 2 3 4 5 6 |
mysql> show global variables like '%rollback_segment%'; +----------------------------+-------+ | Variable_name | Value | +----------------------------+-------+ | innodb_rollback_segments | 128 | +----------------------------+-------+ |
二、MySQL 5.6 时代的 undo 日志
在 MySQL 5.6 中开始支持把 undo 日志分离到独立的表空间,也称之为 undo 表空间,并放到单独的文件目录下;这给我们部署不同 IO 类型的文件位置带来便利,对于并发写入型负载,我们可以把 undo 表空间部署到单独的高速存储设备上。
undo 表空间包含 undo 日志,这是一些记录的集合,包含 undo 事务对聚类索引记录的最新更改的信息。
undo 日志默认存储在系统表空间中,但也可以存储在一个或多个 undo 表空间中。增加了如下几个参数来管理 undo 日志。
1 2 3 4 5 6 7 8 |
mysql> show global variables like '%undo%'; +-------------------------+-------+ | Variable_name | Value | +-------------------------+-------+ | innodb_undo_directory | . | | innodb_undo_logs | 128 | | innodb_undo_tablespaces | 0 | +-------------------------+-------+ |
innodb_undo_directory
当开启独立 undo 表空间时,指定 undo 文件存放的目录,就是用于设置 rollback segment(回滚段)文件所在的路径。这意味着 rollback segment 可以存放在共享表空间以外的位置,即可以设置为独立表空间。该参数的默认值为 “.”,表示当前 InnoDB 存储引擎的数据目录。如果我们想转移 undo 文件的位置,只需要修改下该配置,并将 undo 文件拷贝过去就可以了。
innodb_undo_logs
用来设置 rollback segment(回滚段)的个数,默认为 128。在 InnoDB 1.2 版本中,该参数用来替换之前版本的参数 innodb_rollback_segments。该变量可以动态调整,但是物理上的回滚段不会减少,只是会控制用到的回滚段的个数。
innodb_undo_tablespaces
用于设定创建的 undo 表空间的个数,也就是用来构成 rollback segment 文件的数量,这样 rollback segment 可以较为平均地分布在多个文件中。该选项只能在初始化 MySQL 实例时配置,之后无法更改,默认值为 0,表示不独立设置 undo 的表空间,默认记录到 ibdata 中;否则,则在 undo 目录下创建这么多个 undo 文件,例如假定设置该值为 16,那么就会创建命名为 undo001~undo016 的 undo 表空间文件,每个文件的默认大小为 10M。并且会在路径 innodb_undo_directory 看到 undo 为前缀的文件。
NOTE
当前版本中一旦 MySQL 初始化以后,就不能再改变 undo 表空间的配置了。
在新版本中,我们可以拥有更多的回滚段(每个Undo tablespace可以有128个回滚段,而在之前的版本中所有tablespace的回滚段不允许超过128个
分析
在 InnoDB 启动时(innobase_start_or_create_for_mysql),会进行 undo 表空间初始化,细节见函数 srv_undo_tablespaces_init。
-> 如果是新建实例,会去创建 undo 日志文件,undo 表空间的 space id 从 1 开始;默认初始化大小为 10M,由宏 SRV_UNDO_TABLESPACE_SIZE_IN_PAGES 控制。
-> 读取当前实例的所有 undo 表空间的 space id(trx_rseg_get_n_undo_tablespaces)。
首先从 ibdata 中读取到事务系统的文件头,然后再从其中记录的回滚段信息,找到回滚段对应的 space id 和 page no(trx_sysf_rseg_get_space,trx_sysf_rseg_get_page_no),并按照 space id 排序后返回。
-> 根据上一步读到的 space id 依次打开 undo 文件(srv_undo_tablespace_open),如果不存在,就标识启动失败。
所以 undo 文件也是类似 ibdata 的重要文件,目前是不可以删除的。所以不要试图删除 undo 文件来释放空间!可以容忍定义的 table space 个数比已有的 undo 文件个数要少(但所有的 undo 文件依然会打开),反之则报错初始化失败。
如果是正常关闭重启,并且设置的回滚段个数大于目前已经使用的回滚段个数(trx_sysf_rseg_find_free),就会去新建回滚段(trx_rseg_create)。
这里总是从第一个 undo 表空间开始初始化回滚段,看起来似乎有些问题,极端情况下,如果我每次重启递增 innodb_undo_logs,是不是意味着所有的 undo 回滚段都会写入到第一个 undo 表空间中?
当有长时间运行的事务时,可能导致 purge 操作来不及回收 undo 空间,进而导致 undo 空间急剧膨胀;理论上讲,如果做一次干净的关机,应该可以安全的将将这些 undo 文件删除并重新做一次初始化;也许未来的某个 MySQL 版本可能实现这个功能,这对于某些服务(比如按磁盘空间收费的云计算提供商)是非常有必要的功能。
实际使用方面,在初始化实例之前,我们只需要设置 innodb_undo_tablespaces 参数(建议大于等于 3)即可将 undo 日志设置到单独的 undo 表空间中。如果需要将 undo 日志放到更快的设备上时,可以设置 innodb_undo_directory 参数,但是一般我们不这么做,因为现在 SSD 非常普及。innodb_undo_logs 可以默认为 128 不变。
三、MySQL 5.7 时代的 undo 日志
那么问题又来了,undo 日志单独拆出来后就能缩小了吗?MySQL 5.7 引入了新的参数,innodb_undo_log_truncate,开启后可在线收缩拆分出来的 undo 表空间。
在满足以下两个条件下,undo 表空间文件可在线收缩:
- innodb_undo_tablespaces>=2:因为truncate undo表空间时,该文件处于非活跃(inactive)状态,如果只有一个 undo 表空间,那么整个系统在此过程中将处于不可用状态。为了尽可能降低 truncate 对系统的影响,建议将该参数最少设置为 3;
- innodb_undo_logs>=35(默认128):因为在 MySQL 5.7 中,第一个 undo 日志永远在系统表空间中,另外 32 个 undo 日志分配给了临时表空间,即 ibtmp1,至少还有 2 个 undo 日志才能保证 2 个 undo 表空间中每个里面至少有 1 个undo 日志;
满足以上两个条件后,把 innodb_undo_log_truncate 设置为 ON 即可开启 undo 表空间的自动 truncate,这还跟如下两个参数有关:
- innodb_max_undo_log_size:undo 表空间文件超过此值即标记为可收缩,默认 1G,可在线修改;
- innodb_purge_rseg_truncate_frequency:指定 purge 操作被唤起多少次之后才释放 rollback segments。当 undo 表空间里面的 rollback segments 被释放时,undo 表空间才会被 truncate。由此可见,该参数越小,undo 表空间被尝试 truncate 的频率越高。
基本也就是 InnoDB 的 purge 线程,会根据 innodb_undo_log_truncate 开关的设置,和 innodb_max_undo_log_size 设置的文件大小阈值,以及 truncate 的频率来进行空间回收和 rollback segment 的重新初始化。
MySQL 5.7 关于 undo 日志参数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
mysql> show global variables like '%undo%'; +--------------------------+------------+ | Variable_name | Value | +--------------------------+------------+ | innodb_max_undo_log_size | 1073741824 | | innodb_undo_directory | ./ | | innodb_undo_log_truncate | OFF | | innodb_undo_logs | 128 | | innodb_undo_tablespaces | 0 | +--------------------------+------------+ 5 rows in set (0.00 sec) mysql> show global variables like '%truncate%'; +--------------------------------------+-------+ | Variable_name | Value | +--------------------------------------+-------+ | innodb_purge_rseg_truncate_frequency | 128 | | innodb_undo_log_truncate | OFF | +--------------------------------------+-------+ 2 rows in set (0.01 sec) |
innodb_undo_log_truncate
参数设置为 1,即开启在线自动截断 undo 日志文件,支持动态设置。
innodb_undo_tablespaces
参数必须大于或等于2,即截断一个 undo 日志文件时,要保证另一个 undo 是可用的。
innodb_undo_logs
undo 回滚段的数量, 至少大于等于 35,默认 128。
innodb_max_undo_log_size
当超过这个阀值,默认是 1G,会触发截断动作,截断后空间缩小到 10M。
innodb_undo_directory
undo 文件存放的目录位置。
innodb_purge_rseg_truncate_frequency
控制截断 undo 的频率,undo 表空间在它的回滚段没有得到释放之前不会截断,想要增加释放回滚区间的频率,就得降低 innodb_purge_rseg_truncate_frequency 设定值。
测试 undo 表空间的截断功能
1. 正确设置参数
1 2 3 4 5 6 |
# 为了实验方便,我们减小该值 innodb_max_undo_log_size = 100M innodb_undo_log_truncate = ON innodb_undo_logs = 128 innodb_undo_tablespaces = 3 innodb_purge_rseg_truncate_frequency = 10 |
2. 创建测试数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
mysql> create table t1(id int primary key auto_increment,name varchar(200)); mysql> insert into t1(name) values(repeat('a',200)); Query OK, 1 row affected (0.01 sec) mysql> insert into t1(name) select name from t1; Query OK, 1 row affected (0.00 sec) Records: 1 Duplicates: 0 Warnings: 0 mysql> insert into t1(name) select name from t1; Query OK, 2 rows affected (0.01 sec) Records: 2 Duplicates: 0 Warnings: 0 mysql> insert into t1(name) select name from t1; Query OK, 4 rows affected (0.00 sec) Records: 4 Duplicates: 0 Warnings: 0 ... mysql> insert into t1(name) select name from t1; Query OK, 8388608 rows affected (2 min 11.31 sec) Records: 8388608 Duplicates: 0 Warnings: 0 |
这时 undo 表空间文件大小如下,可以看到有一个 undo 文件已经超过了 100M。
1 2 3 |
-rw-r----- 1 mysql mysql 13M Feb 17 17:59 undo001 -rw-r----- 1 mysql mysql 128M Feb 17 17:59 undo002 -rw-r----- 1 mysql mysql 64M Feb 17 17:59 undo003 |
此时,为了,让 purge 线程运行,可以运行几个 delete 语句:
1 2 3 4 |
mysql> delete from t1 limit 1; Query OK, 1 row affected (0.00 sec) ... |
再查看 undo 文件大小:
1 2 3 |
-rw-r----- 1 mysql mysql 13M Feb 17 18:05 undo001 -rw-r----- 1 mysql mysql 10M Feb 17 18:05 undo002 -rw-r----- 1 mysql mysql 64M Feb 17 18:05 undo003 |
可以看到,超过 100M 的 undo 文件已经收缩到 10M 了。
四、MySQL 5.7 undo 日志管理
1. undo 表空间创建
设置 innodb_undo_tablespaces 的个数, 在 mysql install 的时候,创建指定数量的表空间。InnoDB 默认支持 128 个 undo logs,但表示的都是回滚段的个数。从 5.7.2 开始,其中 32 个 undo logs 为临时表的事务分配的,因为这部分 undo 不记录 redo,不需要 recovery,另外从 33-128 一共 96 个是 redo-enabled undo。
rollback segment 的分配如下:
1 2 3 |
Slot-0: reserved for system-tablespace. Slot-1....Slot-N: reserved for temp-tablespace. Slot-N+1....Slot-127: reserved for system/undo-tablespace. */ |
其中如果是临时表的事务,需要分配两个 undo logs,其中一个是 non-redo undo logs;这部分用于临时表数据的回滚。
另外一个是 redo-enabled undo log,是为临时表的元数据准备的,需要 recovery。而且, 其中 32 个 rollback segment 创建在临时表空间中,并且临时表空间中的回滚段在每次 server start 的时候,需要重建。每一个 rollback segment 可以分配 1024 个 slot,也就是可以支持 96*1024 个并发的事务同时, 但如果是临时表的事务,需要占用两个 slot。
InnoDB undo 的空间管理简图如下:
注核心结构说明:
1. rseg slot
rseg slot 一共 128 个,保存在 ibdata 系统表空间中,其位置在:
1 2 |
/*!< the start of the array of rollback segment specification slots */ #define TRX_SYS_RSEGS (8 + FSEG_HEADER_SIZE) |
每一个 slot 保存着 rollback segment header 的位置。包括 space_id + page_no,占用 8 个 bytes。其宏定义:
1 2 3 4 5 6 7 8 9 10 11 12 |
/* Rollback segment specification slot offsets */ /*-------------------------------------------------------------*/ #define TRX_SYS_RSEG_SPACE 0 /* space where the segment header is placed; starting with MySQL/InnoDB 5.1.7, this is UNIV_UNDEFINED if the slot is unused */ #define TRX_SYS_RSEG_PAGE_NO 4 /* page number where the segment header is placed; this is FIL_NULL if the slot is unused */ /* Size of a rollback segment specification slot */ #define TRX_SYS_RSEG_SLOT_SIZE 8 |
2. rseg header
rseg header 在 undo 表空间中,每一个 rseg 包括 1024 个 undo segment slot,每一个 slot 保存着 undo segment header 的位置,包括 page_no,暂用 4 个 bytes,因为 undo segment 不会跨表空间,所以 space_id 就没有必要了。
其宏定义如下:
1 2 3 4 5 6 7 |
/* Undo log segment slot in a rollback segment header */ /*-------------------------------------------------------------*/ #define TRX_RSEG_SLOT_PAGE_NO 0 /* Page number of the header page of an undo log segment */ /*-------------------------------------------------------------*/ /* Slot size */ #define TRX_RSEG_SLOT_SIZE 4 |
3. undo segment header
undo segment header page 即段内的第一个 undo page,其中包括四个比较重要的结构:undo segment header 进行段内空间的管理;undo page header page 内空间的管理,page 的类型:FIL_PAGE_UNDO_LOG;undo header 包含 undo record 的链表,以便安装事务的反顺序,进行回滚;undo record 剩下的就是 undo 记录了。
2. undo 段的分配
undo 段的分配比较简单,其过程如下:
首先是 rollback segment 的分配:
1 2 3 |
trx->rsegs.m_redo.rseg = trx_assign_rseg_low( srv_undo_logs, srv_undo_tablespaces, TRX_RSEG_TYPE_REDO); |
1. 使用 round-robin 的方式来分配 rollback segment;
2. 如果有单独设置 undo 表空间,就不使用 system 表空间中的 undo segment;
3. 如果设置的是 truncate 的就不分配;
4. 一旦分配了,就设置 trx_ref_count,不允许 truncate。
参考代码如下:
1 2 3 4 5 6 7 8 9 |
/******************************************************************//** Get next redo rollback segment. (Segment are assigned in round-robin fashion). @return assigned rollback segment instance */ static trx_rseg_t* get_next_redo_rseg( /*===============*/ ulong max_undo_logs, /*!< in: maximum number of UNDO logs to use */ ulint n_tablespaces) /*!< in: number of rollback tablespaces */ |
其次是 undo segment 的创建:
从 rollback segment 里边选择一个 free 的 slot,如果没有,就会报错,通常是并发的事务太多。
错误日志如下:
1 2 3 |
ib::warn() << "Cannot find a free slot for an undo log. Do" " you have too many active transactions running" " concurrently?"; |
如果有 free,就创建一个 undo 的 segment。
3. undo 的 truncate
undo 的 truncate 主要由下面两个参数控制:innodb_purge_rseg_truncate_frequency,innodb_undo_log_truncate。
- innodb_undo_log_truncate:是开关参数。
- innodb_purge_rseg_truncate_frequency:默认 128,表示 purge undo 轮询 128 次后,进行一次 undo 的 truncate。
当设置 innodb_undo_log_truncate=ON 的时候, undo 表空间的文件大小,如果超过了 innodb_max_undo_log_size, 就会被 truncate 到初始大小,但有一个前提,就是表空间中的 undo 不再被使用。
其主要步骤如下:
- 超过大小了之后,会被 mark truncation,一次会选择一个。
- 选择的 undo 不能再分配新给新的事务。
- purge 线程清理不再需要的 rollback segment。
- 等所有的回滚段都释放了后,truncate 操作,使其成为 install db 时的初始状态。
默认情况下, 是 purge 触发 128 次之后,进行一次 rollback segment 的 free 操作,然后如果全部 free 就进行一个 truncate。但 mark 的操作需要几个依赖条件需要满足:
- 系统至少得有两个 undo 表空间,防止一个 offline 后,至少另外一个还能工作。
- 除了ibdata里的 segment,还至少有两个 segment 可用。
- undo 表空间的大小确实超过了设置的阈值。
其核心代码参考:
1 2 3 4 5 6 7 |
/** Iterate over all the UNDO tablespaces and check if any of the UNDO tablespace qualifies for TRUNCATE (size > threshold). @param[in,out] undo_trunc undo truncate tracker */ static void trx_purge_mark_undo_for_truncate( undo::Truncate* undo_trunc) |
因为,只要你设置了 truncate=on,MySQL 就尽可能的帮你去 truncate 所有的 undo 表空间,所以它会循环的把 undo 表空间加入到 mark 列表中。
最后,循环所有的 undo 段,如果所属的表空间是 marked truncate,就把这个 rseg 标志位不可分配,加入到 trunc 队列中,在 purge 的时候,进行 free rollback segment。
注意,如果是在线库,要注意影响,因为当一个 undo tablespace 在进行 truncate 的时候,不再承担 undo 的分配。只能由剩下的 undo 表空间的 rollback segment 接受事务 undo 空间请求。