redo log
简单来讲:
redo log是mysql中重要的日志模块,是innodb引擎特有的,记录着事务对数据的修改,保证了事务的原子性和持久性。
mysql里面经常说的日志先行策略(Write-Ahead Logging,WAL),关键点就在于先写日志,再写磁盘。WAL把随机写转化为了顺序写,因为redo log是顺序写的,往磁盘写数据是随机的,这样一来性能提高了不少。当需要更新一条数据的时候,innodb会把更新记录写入到redo log中,并更新内存,这样就相当于更新完成了,innodb在适当的时候会进行刷盘操作。
修改redo log的个数和大小
步骤:
①关闭mysql实例,确保没有报错;
②修改参数文件相关参数:innodb_log_file_size和innodb_log_file_group
③启动mysql实例。
如果innodb检测到innodb_log_file_size和当前重做日志文件的大小不一致的话,就会写一个日志检查点,关闭并删除当前的日志文件,按照参数的大小创建新的日志文件,然后开打新的日志文件。
参数解释:
innodb_log_file_size:
全局的参数,代表日志组中每个日志文件的字节大小,默认50331648字节=48MB,innodb_log_file_size * innodb_log_files_in_group不能超过最大512GB。该值越大,缓冲池需要的检查点刷新就越少,就越节省磁盘IO,但是如果较大的日志文件,也会使崩溃恢复变得很慢很慢。在mysql5.7.11中innodb_log_file_size最小值从1MB增加到4MB。
innodb_log_files_in_group:
全局参数,默认是2,最小是2,最大是100。假如设置为2,也就是有两个redo log日志,在该组中的两个redo log可以用来循环写入。文件位置由innodb_log_group_home_dir指定,如果没有指定,默认是在数据目录下创建两个ib_logfile0和ib_logfile1文件。
redo log的刷新
相关参数:
①innodb_flush_log_at_trx_commit:
共有3个值:0、1、2
0:每隔1s,会将redo log buffer中的日志,刷新到redo log file(os page cache中),并flush到磁盘;
1:每次事务提交,会将redo log buffer中的日志,刷新到redo log file,并flush到磁盘;
2:每次事务提交,会将redo log buffer中的日志,刷新到redo log file,并且每隔1s将redo log file进行flush到磁盘。
master thread会每秒和每10秒对redo log进行刷新到磁盘,并且redo log buffer使用超过一半的时候也会触发刷新到磁盘。
②innodb_log_buffer_size:
全局参数,默认16MB,最大可设置4GB,最小可设置1MB。所有线程共用一个redo log buffer。
刷新流程:
在写redo的时候是先写入到redo log buffer当中,然后再写入os page cache,再fsync到磁盘。redo log的刷新除了上面innodb_flush_log_at_trx_commit参数控制之外,还有几种情况会进行刷新:
①master thread每秒和每10秒都会进行redo log的刷新,由于共用一个redo log buffer所以会导致有的没有提交的事务的redo也会被刷新到磁盘;
②当redo log buffer使用量达到一半的时候,会将redo log buffer中的日志写入到os page cache中;
③当有并行事务执行的时候,并且innodb_flush_log_at_trx_commit=1,当该事务提交的时候会将redo log buffer中的日志写入到os page chache并fsync到磁盘,所以没有提交的redo会一并刷盘。
redo log是循环写的,假如有4个redo log file,下面是一张经典的图:
write pos是当前的位置,一边写一边后移,checkpoint是当前要擦除的位置,也是往后推移并且是循环的,擦除记录之前要把内存中的记录(也就是脏页)更新到数据文件,它们两个之间的位置是空闲的位置,是可以写入新的日志的,当write pos追上了checkpoint代表redo log满了,需要推进checkpoint,此时是不能写入的。推进checkpoint之后,之前的页都已经刷新回磁盘,所以这部分redo log是可以被覆盖的,checkpoint也减少了crash recovery的时间。
binlog
binlog也是重要的日志模块,binlog称之为二进制日志,是server层面的日志,跟引擎无关,记录的是对表的操作的语句,并且binlog是追加写的。它有三种格式:statement、row和mixed格式。
binlog格式解释
由参数binlog_format控制。
①statement格式:
binlog记录的是操作的语句,而不记录具体的行,这样做的优点就是不需要记录每一行的变化,减少了binlog日志量,节约了IO,提高了性能。但是可能会导致主备不一致的情况,因为可能备库和主库用到的索引不同,假如delete ... where a>=1 and b<=4 limit 1;在主库用的a索引那么删除的是a=1这个条件的第一行,如果传到备库使用的是b索引那么删除的是b=4这个条件的第一行,可能a=1和b=4不是同一行数据,这样就造成了主备不一致了。
再有对于特定函数uuid()或者user()不能被正确复制。
②row格式:
binlog记录的是行的前后的变化,不记录具体的sql语句,这样做的优点是日志内容清楚的记录每一行数据的变化,不会出现某些情况下或者使用的特定函数无法复制的情况。缺点就是可能会产生大量的日志内容,比如delete 100多万行的数据,对于statement格式来说就只记录一条sql语句,而对于row格式的来说需要记录每条记录的信息,可能产生的binlog是非常大的。
③mixed格式:
mixed格式是statement和row两种的混合模式,一般的语句修改使用statement格式保存binlog,mysql自己会判断sql语句是否会影响主备不一致,如果可能导致不一致,则采用row格式来保存,会根据执行的每一条具体的sql语句来区分对待记录的日志形式。
binlog的刷新
相关参数:
①sync_binlog:
有0、1、N三个值。
0:每次提交事务的时候都只会写入到os page cache中,不进行fsync操作,由os决定fsync的时机,或者当cache满了之后才同步磁盘;
1:每次提交事务都会进行fsync操作;
N:每次提交都是都写入到os page cache中,当累计到N个事务后才执行fsync操作。
因此,如果在出现IO瓶颈的场景中,会设置该参数为一个较大的值,可以提升性能,但是设置为N的缺点是:如果主机发生异常重启,可能会丢失最近N个事务的binlog。
②binlog_cache_size:
默认32K,最小4K(块大小),最大18446744073709547520,如果设置的值不是块大小的整数倍,那么会四舍五入成块大小的整数倍。对于每个线程都会分配一个binlog cache(但是都是共用一个binlog文件),如果放入cache的binlog的大小超过了cache的大小,就要暂存到磁盘了。
刷新流程:
简单总结:
事务执行过程中,会先把binlog写入到binlog cache中,事务提交的时候,再把binlog cache中的内容写入到binlog文件,具体如何fsync的由sync_binlog参数控制,但是一个事务的binlog是不能被拆分的,无论这个事务多大,也要保证一次性写入binlog。
两阶段提交(2PC)
以update的流程图来看2PC:
如上图可以看出,在写日志的时候分为了prepare和commit两个阶段,这就是两阶段提交。
两阶段提交整体上是先写redo log进入prepare阶段,再写binlog,binlog写入成功后,再把redo log commit。但是真正的是redo log在prepare阶段就会进行fsync(因为有一个崩溃恢复会依赖prepare阶段的redo)。
在图中设置两个时间点如下:
比如在T1和T2 mysql异常重启会有什么情况发生:
①T1时刻,也就是在redo log prepare之后,写binlog之前,发生了重启,此时redo log还没有提交,binlog也没有记录也不会传递到备库,所以重启时这个事务就会回滚;
②T2时刻,也就是binlog写完,但是redo log还没有commit,此时发生重启:
a、如果redo log里面事务是完整的,并且已经有了commit表示,则直接提交;
b、如果redo log里面的事务只有完整的prepare,则判断对应的事务的binlog是否是完整的:
如果是,则提交事务;
如果不是,则回滚事务。
那么现在反向来考虑一下,为什么redo log会在prepare阶段进行一次提交呢?
答:假如在T2时刻发生重启了,如果这部分redo log丢失了,就无法判断redo log是否有prepare或者commit标志,也就是说,这个时刻binlog已经fsync了,已经传递到备库了,如果redo log不能进行判断状态,那么将导致主备不一致,也就是redo 在prepare阶段就需要fsync一次了。那么当commit的时候redo log也就只写入os page cache就可以了。
undo
undo log又称为回滚日志。保证了事务的原子性以及隔离性(配合MVCC)。
不同版本的undo
(1)mysql5.5版本及以前版本:
在mysql5.5以及之前的版本中,undo log是存放在了ibdata1中,所以当有大事务的时候就会发现ibdata1越来越大,并且当事务结束后,空间也不会释放,想要释放空间只能导入导出数据来解决 。
参数innodb_rollback_segments默认128,也就是有128个回滚段(后面废弃了该参数由innodb_undo_logs代替)。
(2)mysql5.6版本:
在mysql5.6版本中增加了参数innodb_undo_directory、innodb_undo_logs、innodb_undo_tablespaces。
参数:
① innodb_undo_directory:指定单独存放undo的目录,默认为数据目录,实例初始化后不可改动。可以通过停止实例然后移动目录,修改参数文件,重启,来重新指定目录。
② innodb_undo_logs:该参数替换了innodb_rollback_segments,用来指定回滚段的个数,默认128个,最大也是128个(但是在mysql8.0该参数弃用了,在5.7版本又改为了innodb_rollback_segments)。
每个回滚段支持undo的个数:
③ innodb_undo_tablespaces:指定单独存放的undo log表空间的个数,默认是0,也就是存放在ibdata1中,该参数只能在mysql实例初始化之前配置,之后不能修改,如果使用的是默认值0,那么在初始化后试图去修改,会报错innodb没有找到预期的undo表空间数(该参数会在mysql5.7.21开始弃用但可配置,并在8.0.14不再可配置)。
但是在5.6版本虽然可以将undo在系统表空间中独立出来,但是也没有解决回收undo表空间的问题。
(3)mysql5.7版本:
在5.7版本中引入了undo表空间的截断。
参数:
① innodb_undo_tablespaces:该参数上面已经说过了,只不过在mysql5.7.21开始被弃用,但是还是可以配置的。
② innodb_undo_directory:和上面的一样,也是指定独立undo表空间的目录。
③ innodb_rollback_segments:用来指定分配给undo表空间的回滚段的数量(替换了innodb_undo_logs),默认128个,最大也是128个。其中一个回滚段总是分配给系统表空间,32个回滚段被保留给临时表使用,并且驻留在临时表空间ibtmp1中。为了分配额外的回滚段,该参数必须大于33,如果配置了单独的undo表空间,那么系统表空间中的回滚段将变成非活动的。当该参数设置为32或者更小的时候,innodb会给系统表空间分配一个回滚段,给临时表空间分配32个回滚段。当该参数设置为大于32的时候,innodb会给系统表空间分配一个回滚段,给临时表空间分配32个回滚段,如果还有回滚段的话 ,就会给undo表空间分配额外的回滚段,如果没有undo表空间,就会给系统表空间分配额外的回滚段。
每个回滚段支持undo的个数:
④ innodb_max_undo_log_size:定义undo表空间的阈值大小,默认1024MB
⑤ innodb_purge_rseg_truncate_frequency:默认128,最小1,最大128。该参数是用来定义purge线程释放回滚段的频率的。通常每调用purge 128次就回释放回滚段一次。
undo表空间的截断:
要截断undo表空间,需要mysql实例至少有两个活动的undo表空间,这可以确保其中一个undo表空间保持活动,而另外一个脱机被截断,undo表空间数量由上面参数innodb_undo_tablespaces控制。
要截断undo表空间,需要启用参数:set global innodb_undo_log_truncate=on;如果想要该参数起作用,那么需要满足innodb_undo_tablespaces>=2,innodb_rollback_segments>=35。
当innodb_undo_log_truncate参数启用时,超过变量innodb_max_undo_log_size大小限制的undo表空间将被截断。以循环的方式选择用于截断的undo表空间,以避免每次截断相同的undo表空间。当脱机截断的时候该undo表空间中的回滚段被设置为不活动,这样就不会为该回滚段分配事务,只在活跃的回滚段上分配事务。然后purge线程将释放不再使用的undo日志来清空回滚段。在释放回滚段之后,运行truncate操作,并将undo表空间截断为初始大小。在执行截断后,undo表空间的大小可能比初始大小要大,因为截断操作完成后会立即使用该undo表空间。
加速undo表空间的截断:
purge线程负责清空和截断undo表空间,默认情况下purge线程将查找undo表空间128次后截断一次,该频率由innodb_purge_rseg_truncate_frequency该参数控制。如果要增加截断频率可降低该参数配置,例如设置为32:set global innodb_purge_rseg_truncate_frequency=32;这样可以加快截断undo 表空间。
截断undo表空间文件对性能的影响:
因为需要被截断的undo表空间中的回滚段处于不活跃状态,这样其他表空间中活跃的回滚段就承担了整个系统的负载,这可能导致性能下降。主要取决于以下几个因素:
①undo表空间文件数量;
②undo表空间大小;
③IO速度;
④长时间运行的事务;
⑤系统负载。
另外,undo表空间截断操作期间会执行两个检查点操作,第一个检查点操作从缓冲池中删除旧的undo表空间页,第二个检查点将新的undo表空间的初始页刷新到磁盘,在繁忙的系统上,如果有大量页面需要删除,那么第一个检查点尤其会影响系统性能。
(4)mysql8.0版本:
参数:
innodb_directories:定义表空间文件启动时扫描的目录。当服务器脱机时,将表空间文件移动或恢复到新位置时使用此选项。它还用于指定使用绝对路径创建的表空间文件的目录或位于数据目录之外的目录。默认值是null,但是innodb_data_home_dir、innodb_undo_directory和datadir总是会被追加到该参数值得后面无论是否指定了该参数值。设置的时候需要用引号引起来,否则分号可能被示意为特殊符号:
mysqld --innodb-directories="directory_path_1;directory_path_2"
或者
[mysqld]
innodb_directories="directory_path_1;directory_path_2"
不能使用通配符来指定该目录。
默认的undo表空间:
在初始化mysql实例的时候,会创建两个默认的undo表空间(这也就是innodb_undo_tablespaces该参数为什么在mysql5.7.21开始被弃用了),因为要支持自动截断undo表空间,至少需要两个。
默认情况下是在 innodb_undo_directory参数定义的目录下创建,如果该参数没有定义,那么会在数据目录下创建,默认文件名为:undo_001、undo_002。从mysql8.0.14开始,可以在运行的时候使用sql创建额外的undo表空间。
undo 表空间大小:
在mysql8.0.23之前,undo表空间的初始大小依赖于innodb_page_size的值。
Innodb_page_size | Default Undo log file size |
4KB | 7MB |
8KB | 8MB |
16KB | 10MB |
32KB | 20MB |
64KB | 40MB |
从mysql8.0.23开始,undo的初始大小通常为16MB。如果被截断后重新创建undo表空间文件的时候初始大小可能与默认值不同,如果该文件大于16MB并且它的前一个文件时在最后一秒生成的,那么这个undo表空间将以innodb_max_undo_log_size的四分之一大小创建。
增加undo表空间:
由于长时间运行事务,undo日志可能变得很大,因此创建额外的undo表空间可以防止单个undo表空间过大,在mysql8.0.14中,可以使用下面语法来进行创建:
create undo tablespace tablespace_name add datafile 'file_name.ibu';
undo表空间文件的扩展名必须是.ibu,在定义undo表空间文件名时不允许指定相对路径,只支持绝对路径,且必须是该参数:innodb_directories指定的。如果没有设置innodb_directories并且undo表空间文件命中不包含路径,则undo表空间创建在变量innodb_undo_directory定义的目录下,如果该变量也未定义则在数据目录下创建。
如果需要查看undo表空间名字和路径,可以执行如下sql:
SELECT TABLESPACE_NAME, FILE_NAME FROM INFORMATION_SCHEMA.FILES
WHERE FILE_TYPE LIKE 'UNDO LOG';
一个mysql实例最多支持127个undo表空间,包括初始化的时候默认创建的两个。
创建一个undo表空间:
删除undo表空间:
在mysql8.0.14中,使用create undo tablespace可以创建undo,也可以使用drop undo tablespace来删除undo,undo表空间必须是空的才能被删除,要清空一个undo表空间,必须先使用alter undo tablespace语法将该undo表空间标记为非活动,以便该表空间不再用于新事物分配回滚段:
ALTER UNDO TABLESPACE tablespace_name SET INACTIVE;
在undo表空间被标记为非活动后,等待使用当前undo的事务结束,然后清楚系统释放undo表空间中的回滚段,undo表空间被截断为初始大小,一旦undo表空间为空就可以删除它。
DROP UNDO TABLESPACE tablespace_name;
或者,undo表空间可以一致保持为空的状态,然后在需要的时候使用:alter undo tablespace tablespace_name active 语句重新激活。
表空间状态可以通过下面语句来查询:
SELECT NAME, STATE FROM INFORMATION_SCHEMA.INNODB_TABLESPACES
WHERE NAME LIKE 'tablespace_name';
删除一个undo表空间:
mysql实例初始化默认创建的两个undo表空间是不能被删除的,但是可以使用语句设置非活动状态,默认有两个undo表空间,当其中一个被设置为不活动的时候,必须有另一个来替代它,始终要保持有两个undo表空间是活动的用来支持undo表空间的自动截断。
undo表空间移动:
使用create undo语法创建的undo表空间,可以移动到innodb_directories参数定义的任意一个目录,默认值是null,但是innodb_data_home_dir、innodb_undo_directory和datadir总是会被追加到该参数值得后面无论是否指定了该参数值。初始化创建的两个默认undo表空间必须位于innodb_undo_directory定义的目录下,如果该变量未定义则默认未数据目录,如果移动了这两个默认的undo表空间,那么实例启动的时候必须将innodb_undo_directory变量指定为该目录。
配置回滚段数:
innodb_rollback_segments变量定义了分配给每个undo表空间和全局临时表空间的回滚段的数量(这里要区别于mysql5.7,在5.7中该变量定义的是整个实例的分配给undo的回滚段个数,而这里是为每个undo分配的),可以在启动的时候配置也可以在运行中配置,默认128也是最大值。
截断undo表空间:
有两种方法来截断undo表空间,可以单独使用。一种是自动化截断通过参数,另一种是手动截断通过sql语句。
自动的方法不需要监视undo表空间的大小,并且一旦启用,不需要人工干预等。
①自动截断:
自动截断至少需要两个活动的undo表空间,需要启用参数(同5.7版本):
mysql> SET GLOBAL innodb_undo_log_truncate=ON;
当该变量启用时超过innodb_max_undo_lost_size限制的undo将被截断(过程和原理同5.7版本)。
②手动截断:
手动截断undo表空间需要至少三个活动的undo表空间,在任何时候都需要两个活动的undo表空间来支持自动截断的可能性。
使表空间变为不活动:
ALTER UNDO TABLESPACE tablespace_name SET INACTIVE;
在该undo表空间变为非活动后,是允许当前使用该undo表空间中回滚段的事务结束的,事务完成后,清除系统释放undo表空间中的回滚段,undo表空间被截断为初始大小,状态由非活动变为空。
如下语句可检查undo表空间状态:
SELECT NAME, STATE FROM INFORMATION_SCHEMA.INNODB_TABLESPACES
WHERE NAME LIKE 'tablespace_name';
一旦undo表空间处于空状态,就可以通过以下语句重新激活它:
ALTER UNDO TABLESPACE tablespace_name SET ACTIVE;
处于空状态的undo表空间也可以被删除。
加速undo表空间的自动截断:
purge线程负责清空和截断undo表空间,默认情况下,purge线程将查找undo表空间,以便在每128次调用purge时截断一次。purge线程要查找截断的undo表空间的频率由innodb_purge_rseg_truncate_frequency变量控制,默认值为128。如果要增加截断频率,可以降低该设置。
截断undo表空间对性能的影响:
同5.7版本。
监控undo表空间截断:
从mysql8.0.16开始,提供了undo和purge子系统计数器,用于监视与undo日志截断相关的后台活动,可查询该表:
SELECT NAME, SUBSYSTEM, COMMENT FROM INFORMATION_SCHEMA.INNODB_METRICS WHERE NAME LIKE '%truncate%';
undo表空间截断限制:
从mysql8.0.21开始,检查点之间在同一个undo表空间上截断操作的次数限制为64。该限制可以防止由于undo表空间截断操作过多而导致的潜在问题,例如:在繁忙的系统中,innodb_max_undo_log_size设置过低就可能发生这种情况,如果超过了这个限制,undo表空间仍然可以被设置为不活动的,但是它直到下一个检查点之后才会被截断,在mysql8.0.22中限制从64提高到了50000。
undo表空间状态变量:
mysql> SHOW STATUS LIKE 'Innodb_undo_tablespaces%';
+----------------------------------+-------+
| Variable_name | Value |
+----------------------------------+-------+
| Innodb_undo_tablespaces_total | 2 | #表空间总量
| Innodb_undo_tablespaces_implicit | 2 | #隐式创建
| Innodb_undo_tablespaces_explicit | 0 | #显式创建
| Innodb_undo_tablespaces_active | 2 | #活跃的数量
+----------------------------------+-------+
参考:
https://dev.mysql.com/doc/refman/5.7/en/innodb-undo-tablespaces.html
https://dev.mysql.com/doc/refman/8.0/en/innodb-undo-tablespaces.html