MySQL服务器日志

分类

MySQL Server 主要有以下几类日志:

日志类型 作用
错误日志(Error log) 记录启动、运行、停止 mysqld 时遇到的问题
查询日志(General query log) 已建立的客户端连接和从客户端收到的语句
二进制日志(Binary log) 更改数据的语句,主要用于备份恢复、主从复制
中继日志(Relay log) 从复制源服务器接收到的数据更改
慢查询日志(Slow query log) 执行时间超过long_query_time的所有查询或者不使用索引的查询
DDL日志(DDL log (metadata log)) DDL 语句执行的元数据操作

除了上述日志类型以外,对于 InnoDB 引擎,有两种重要的事务日志:

日志类型 作用 区别
重做日志(Redo log) 主要用于掉电等故障恢复 记录了此次事务完成后的数据状态,记录的是更新之后的值。
回滚日志(Undo log) 主要用于事务回滚,基于 Undo log 和 Read View 实现了 MVCC。 记录了此次事务开始前的数据状态,记录的是更新之前的值。

这里,我们主要介绍其中的三种日志:二进制日志(Binary log)、重做日志(Redo log)和回滚日志(Undo log)。

二进制日志(bin log)

MySQL 在完成一条更新操作后,Server 层还会生成一条 binlog,等之后事务提交的时候,会将该事物执行过程中产生的所有 binlog 统一写 入 binlog 文件。

binlog 文件是记录了所有数据库表结构变更和表数据修改的日志,不会记录查询类的操作,比如 SELECT 和 SHOW 操作。

最开始 MySQL 里并没有 InnoDB 引擎,MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。

而 InnoDB 是另一个公司以插件形式引入 MySQL 的,只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用 redo log 来实现 crash-safe 能力。

binlog 有 3 种格式类型,分别是:STATEMENT(默认格式)、ROWMIXED,三种日志格式的区别如下:

格式 特点 缺点
STATEMENT 每一条修改数据的 SQL 都会被记录到 binlog 中,相当于记录了逻辑操作,所以,binlog 也称为逻辑日志,主从复制时,slave 节点可根据日志中的SQL语句重现。 存在动态函数的问题,比如SQL语句中调用了 uuid 或者 now 这些函数,这种随时在变的函数,会导致复制的数据在主库上执行的结果与从库执行的结果不一致。
ROW 记录行数据最终被修改成什么样,不会出现动态函数的问题。 每行数据的变化结果都会被记录,比如执行批量 update 语句,更新多少行数据就会产生多少条记录,会使 binlog 文件过大
MIXED 包含了 STATEMENT 和 ROW 模式,它会根据不同的情况自动使用 ROW 模式和 STATEMENT 模式。

注意:binlog 是追加写入,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存的是全量的日志。

数据恢复

如果不小心整个数据库的数据被删除了,不可以使用 redo log 文件恢复,只能使用 binlog 文件恢复。

因为 redo log 文件是循环写,是会边写边擦除日志的,只记录未被刷入磁盘的数据的物理日志,已经刷入磁盘的数据都会从 redo log 文件里擦除。

binlog 文件保存的是全量的日志,也就是保存了所有数据变更的情况,理论上只要记录在 binlog 上的数据,都可以恢复,所以如果不小心整个数据库的数据被删除了,得用 binlog 文件恢复数据。

主从复制

MySQL 的主从复制依赖于 binlog ,也就是将 MySQL 上的所有变化以二进制形式保存在磁盘上。复制的过程就是将 binlog 中的数据从主库传输到从库上。

这个过程一般是异步的,也就是主库上执行事务操作的线程不会等待复制 binlog 的线程同步完成。

alt master-slave-sync

具体详细过程如下:

  • MySQL 主库在收到客户端提交事务的请求之后,会先写入 binlog,再提交事务,更新存储引擎中的数据,事务提交完成后,返回给客户端“操作成功”的响应。

  • 从库会创建一个专门的 I/O 线程,连接主库的 log dump 线程,来接收主库的 binlog 日志,再把 binlog 信息写入 relay log 的中继日志里,再返回给主库“复制成功”的响应。

  • 从库会创建一个用于回放 binlog 的线程,去读 relay log 中继日志,然后回放 binlog 更新存储引擎中的数据,最终实现主从的数据一致性。

主从复制策略,有三种策略:同步复制异步复制(默认模型)、半同步复制,差异如下:

策略 作用 备注
同步复制 MySQL 主库提交事务的线程需要等待所有从库的复制成功响应,才返回客户端结果。 实际项目基本不使用,首先,是性能很差,需要复制到所有节点才返回响应;其次,是可用性也很差,主库和所有从库任何一个数据库出问题,都会影响业务。
异步复制 MySQL 主库提交事务的线程并不会等待 binlog 同步到各从库,就返回客户端结果。 一旦主库宕机,数据就会发生丢失。
半同步复制 MySQL 5.7 版本之后增加的一种复制方式,介于两者之间,事务线程不用等待所有的从库复制成功响应,只要一部分复制成功响应回来就行,比如一主二从的集群,只要数据成功复制到任意一个从库上,主库的事务线程就可以返回给客户端。 兼顾了异步复制和同步复制的优点,即使出现主库宕机,至少还有一个从库有最新的数据,不存在数据丢失的风险。

bin log 刷盘策略

MySQL 给每个线程分配了一片内存用于缓冲 binlog ,该内存叫 binlog cache,参数 binlog_cache_size 用于控制单个线程内 binlog cache 所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。

事务执行过程中,先把日志写到 bin log cache(Server 层的 cache),事务提交的时候,再把 binlog cache 写到 binlog 文件中。

一个事务的 binlog 是不能被拆开的,因此无论这个事务有多大(比如有很多条语句),也要保证一次性写入。

在事务提交的时候,执行器把 binlog cache 里的完整事务写入到 binlog 文件中,并清空 binlog cache。如下图:

alt binlog cache

虽然每个线程有自己 binlog cache,但是最终都写到同一个 binlog 文件:

  • 调用 write() 时,会把日志写入到 binlog 文件,但是,并没有把数据持久化到磁盘,因为数据还缓存在操作系统的 Page Cache 里,write() 的写入速度还是比较快的,因为不涉及磁盘 I/O;

  • 调用 fsync() 时,才会将数据持久化到磁盘的操作,这里就会涉及磁盘 I/O,所以频繁地执行 fsync() 会导致磁盘的 I/O 升高。

可以通过 sync_binlog 参数控制 binlog 的刷盘频率:

作用 优缺点
0 默认配置,每次提交事务都只执行 write(),不执行 fsync(),由操作系统决定何时将数据持久化到磁盘。 性能是最好,但风险最大,因为一旦主机发生异常重启,未持久化到磁盘的数据就会丢失。
1 每次提交事务都执行 write(),然后立即执行 fsync() 最安全,但性能损耗最大,即使主机发生异常重启,最多丢失一个事务的 binlog。
N 每次提交事务都执行 write(),但需要累积 N 个事务后才执行 fsync() 通过容忍少量事务的 binlog 日志丢失的风险,来提高写入的性能。

读写分离

通过主从复制的机制,可以实现数据库的读写分离,即写入操作请求在主库执行,读操作请求在从库执行,即:

一般情况下,系统大部分场景都是“读多写少”,通过读写分离提升数据库的吞吐量,这样就可以提升系统整体的性能。在实际使用中,一个主库一般跟 2~3 个从库(1 套数据库,1 主 2 从 1 备主),这就是一主多从的 MySQL 集群结构。

重做日志(Redo log)

重做日志(redo log)是一种基于磁盘的数据结构,在崩溃恢复期间用于纠正不完整事务写入的数据。在正常操作期间,重做日志会对由 SQL 语句或低级 API 调用产生的更改表数据的请求进行编码。在初始化期间和接受连接之前,会自动重放在意外关闭之前还没有更新到数据文件的修改。

重做日志是一个磁盘上的物理文件,写入重做日志文件中的数据,会根据数据影响的记录进行编码,这些数据统称为重做(redo)。

写入重做日志文件的数据会通过一个递增的 日志序列号(log sequence number,LSN) 来表示。当数据修改发生时,重做日志数据会被追加到重做日志文件中,并且随着检查点(checkpoint)的移动,最旧的数据会被截断。

重做日志配置

innodb_redo_log_capacity

innodb_redo_log_capacity 变量控制重做日志文件占用的磁盘空间大小,单位:字节,默认值:104857600(100MB),可以设置的值范围:[8388608, 137438953472],即 [8MB, 128GB]。

如果重做日志文件占用的空间小于指定值,则脏页会不太积极地从缓冲池刷新到表空间数据文件,最终会增加重做日志文件占用的磁盘空间。如果重做日志文件占用的空间超过指定值,则会更积极地刷新脏页,最终减少重做日志文件占用的磁盘空间。

重做日志文件有两种类型:

  • 普通重做日志文件(ordinary redo log file):是正在使用的文件。

    重做日志文件使用 #ib_redoN 格式的命名,其中, N 是重做日志文件编号。

  • 备用重做日志文件(spare redo log file):是那些等待使用的文件。

    备用重做日志文件带有 _tmp 后缀。

InnoDB 尝试总共维护 32 个重做日志文件,也就是说,每个文件的大小 = innodb_redo_log_capacity / 32。

例如,如下面的示例所示,重做日志目录中的重做日志文件,共有 25 个活跃重做日志文件和 7 个备用重做日志文件,它们是按顺序编号的。

larrychen@LARRYCHEN-YKtAJ:/var/lib$ sudo ls mysql/'#innodb_redo'
'#ib_redo305'  '#ib_redo308'  '#ib_redo311'  '#ib_redo314'  '#ib_redo317'  '#ib_redo320'  '#ib_redo323'  '#ib_redo326'  '#ib_redo329'      '#ib_redo332_tmp'  '#ib_redo335_tmp'
'#ib_redo306'  '#ib_redo309'  '#ib_redo312'  '#ib_redo315'  '#ib_redo318'  '#ib_redo321'  '#ib_redo324'  '#ib_redo327'  '#ib_redo330_tmp'  '#ib_redo333_tmp'  '#ib_redo336_tmp'
'#ib_redo307'  '#ib_redo310'  '#ib_redo313'  '#ib_redo316'  '#ib_redo319'  '#ib_redo322'  '#ib_redo325'  '#ib_redo328'  '#ib_redo331_tmp'  '#ib_redo334_tmp'

每个普通重做日志文件都与特定范围的 LSN 值相关联。例如,下面的查询显示了,上一示例中列出的活动重做日志文件的 START_LSN 和 END_LSN 值:

mysql> SELECT FILE_NAME, START_LSN, END_LSN FROM performance_schema.innodb_redo_log_files;
+----------------------------+-----------+-----------+
| FILE_NAME                  | START_LSN | END_LSN   |
+----------------------------+-----------+-----------+
| ./#innodb_redo/#ib_redo305 | 353681408 | 356956160 |
...
| ./#innodb_redo/#ib_redo329 | 429000704 | 432275456 |
+----------------------------+-----------+-----------+

执行检查点时,InnoDB 将检查点 LSN 存储在包含该 LSN 的文件头部。在恢复期间,所有的重做日志文件都会被核对,并从最新的检查点 LSN 开始恢复。

WAL(Write-Ahead Logging)

WAL(Write-Ahead Logging)技术是指:InnoDB 的所有的修改都先被写入到日志中,然后再写磁盘,用于保证数据操作的原子性持久性。其过程如下图所示:

alt WAL

在事务提交时,只要先将 redo log 持久化到磁盘即可,可以不需要等到将缓存在 Buffer Pool 里的脏页数据持久化到磁盘。当系统崩溃时,虽然脏页数据没有持久化,但是 redo log 已经持久化,接着 MySQL 重启后,可以根据 redo log 的内容,将所有数据恢复到最新的状态。

注意,InnoDB会先将 redo log 日志写入 redo log buffer,每当产生一条 redo log 时,会先写入到 redo log buffer,后续在持久化到磁盘如下图:

alt redo-log-buffer

可以通过如下参数调整缓冲区未提交事务的大小:

参数 默认值 作用
innodb_log_Buffer_size 16777216 还未提交的事务的缓冲区的大小,单位字节,默认16MB

事务提交之前发生了崩溃,重启后会通过 undo log 回滚事务,事务提交之后发生了崩溃,重启后会通过 redo log 恢复事务,如下图:

有了 redo log,再通过 WAL 技术,InnoDB 就可以保证即使数据库发生异常重启,之前已提交的记录都不会丢失,这个能力称为 crash-safe(崩溃恢复)。

redo log 要写到磁盘,数据也要写磁盘,为什么要多此一举?

写入 redo log 的过程使用了追加操作, 对应的磁盘操作是顺序写入;而写入数据过程需要先找到写入位置,然后再写入磁盘,对应的磁盘操作是随机写入

磁盘的顺序写入随机写入效率更高,因此 redo log 写入磁盘的开销更小。

通过redo解决了两个问题:

  • 实现事务的持久性,让 InnoDB 有 crash-safe 的能力,能够保证 MySQL 在任何时间段突然崩溃,重启后之前已提交的记录都不会丢失;

  • 将写操作从随机写入变成了顺序写入,提升 InnoDB 写入磁盘的性能。

redo log的刷盘时机

redo log的刷盘时机如下:

  • MySQL 正常关闭之前;
  • 当 redo log buffer 中记录的写入量大于 redo log buffer 内存空间的一半时,会触发落盘;
  • InnoDB 的后台线程每隔 1 秒,将 redo log buffer 持久化到磁盘。
  • 由innodb的日志刷盘策略决定(通过参数innodb_flush_log_at_trx_commit控制)。

其中,redo log日志刷盘策略可以通过参数 innodb_flush_log_at_trx_commit 控制:

参数 默认值 作用
innodb_flush_log_at_trx_commit 1 redo log日志刷盘策略

其中,不同的取值策略如下:

取值 日志写入策略 刷盘策略 影响
0 事务提交时,日志只记录在 redo log buffer 中,不会主动触发磁盘写入操作。 InnoDB 的后台线程每隔 1 秒,通过调用 write() 写到操作系统的 Page Cache,然后调用 fsync() 持久化到磁盘。 MySQL 进程的崩溃会导致上一秒钟所有事务数据的丢失
1 事务提交时,会将 redo log buffer 中日志持久化到磁盘中
2 事务提交时,会将 redo log buffer 中日志写入redo log文件,不会立即写入磁盘 InnoDB 的后台线程每隔 1 秒,通过执行系统调用 fsync(),将缓存在操作系统中 Page Cache 里的 redo log 持久化到磁盘。 MySQL 进程的崩溃并不会丢失数据,只有在操作系统崩溃或者系统断电的情况下,上一秒钟所有事务数据才可能丢失

redo log 日志刷盘的三种策略的写入过程,如下图所示:

redo log 文件记录策略

默认情况下, InnoDB 存储引擎有 1 个重做日志文件组( redo log group),重做日志文件组由有 2 个 redo log 文件组成 :ib_logfile0 和 ib_logfile1 。

alt redo-log-group

在重做日志组中,每个 redo log File 的大小是固定且相同的。假设每个 redo log File 设置的上限是 1 GB,那么总共就可以记录 2GB 的操作。

重做日志文件组是以循环写的方式工作的,从头开始写,写到末尾就又回到开头,相当于一个环形。

所以 InnoDB 存储引擎会先写 ib_logfile0 文件,当 ib_logfile0 文件被写满的时候,会切换至 ib_logfile1 文件,当 ib_logfile1 文件也被写满时,会切换回 ib_logfile0 文件。

alt ib_logfile

我们知道 redo log 是为了防止 Buffer Pool 中的脏页丢失而设计的,那么如果随着系统运行,Buffer Pool 的脏页刷新到了磁盘中,那么 redo log 对应的记录也就没用了,这时候我们擦除这些旧记录,以腾出空间记录新的更新操作。

redo log 是循环写的方式,相当于一个环形,InnoDB 用 write pos 表示 redo log 当前记录写到的位置,用 checkpoint 表示当前要擦除的位置,如下图:

write pos 和 checkpoint 的移动都是顺时针方向,其中:

  • 图中红色部分(在write pos ~ checkpoint 之间):用于记录新的更新操作
  • 图中蓝色部分(check point ~ write pos 之间):用于记录待落盘的脏数据页

如果 write pos 追上了 checkpoint,就意味着 redo log 文件满了,这时 MySQL 不能再执行新的更新操作,也就是说 MySQL 会被阻塞(因此所以针对并发量大的系统,适当设置 redo log 的文件大小非常重要),此时会停下来将 Buffer Pool 中的脏页刷新到磁盘中,然后标记 redo log 哪些记录可以被擦除,接着对旧的 redo log 记录进行擦除,等擦除完旧记录腾出了空间,checkpoint 就会往后移动(图中顺时针),然后 MySQL 恢复正常运行,继续执行新的更新操作。

所以,一次 checkpoint 的过程就是脏页刷新到磁盘中变成干净页,然后标记 redo log 哪些记录可以被覆盖的过程。

撤消日志(Undo log)

撤消日志(undo log)是与单个读-写事务关联的撤消日志记录的集合,撤消日志记录中包含了有关如何撤消事务对聚集索引记录的最新修改的信息。如果另一个事务需要查看原始数据作为一致性读取操作(consistent read operation)的一部分,则将从撤消日志记录中检索未修改的数据。

撤消日志存在于 撤消日志段(undo log segments)中,而撤消日志段又包含在 回滚段(rollback segment) 中,其中,回滚段驻留在撤消表空间和全局临时表空间中。

撤消日志段(undo log segments):是撤消日志的集合,撤消日志段存在于回滚段(rollback segments)内,也称为“撤销段”(undo segment)。撤消日志段可能包含来自多个事务的撤消日志。一个撤消日志段一次只能由一个事务使用,但在该事务提交或回滚时释放后,该撤销日志段可以重用。

撤消日志驻留在全局临时表空间中,用于修改用户定义的临时表中数据的事务。撤消日志不会被重做日志记录,因为崩溃恢复不需要它们,它们仅用于服务器运行时的回滚。撤消日志通过避免重做日志记录 I/O,提高了服务器的性能。

每个 undo 表空间和全局临时表空间分别支持最多 128 个回滚段,

innodb_rollback_segments 变量定义了回滚段的数量,默认值为:128,可选值为:[1, 128]。

回滚段支持的事务数量取决于回滚段中的撤消槽数量以及每个事务所需的撤消日志数量,回滚段中的撤消槽数量根据 InnoDB 页大小而不同。回滚段中撤销插槽的数量 = InnoDB 的页面大小 / 16。

InnoDB 的页面大小 回滚段中撤销插槽的数量
4096 (4KB) 256
8192 (8KB) 512
16384 (16KB) 1024
32768 (32KB) 2048
65536 (64KB) 4096

一个事务最多分配四种撤消日志,即一个撤销日志类型对应以下四种操作类型:

  • 用户定义表的 INSERT 操作;

  • 用户定义表的 UPDATE 和 DELETE 操作;

  • 用户定义的临时表进行 INSERT 操作;

  • 用户定义的临时表进行 UPDATE 和 DELETE 操作。

撤消日志根据需要分配,例如,对常规表和临时表执行 INSERT、UPDATE 和 DELETE 操作的事务需要完全分配四种撤消日志。对常规表执行 INSERT 操作的事务只需要单个撤消日志。

对常规表执行操作的事务将会在撤消表空间的回滚段中分配撤消日志;对临时表执行操作的事务将会在全局临时表空间回滚段中分配撤消日志。

一个分配给事务的撤消日志在其持续时间内保持关联到该事务。例如,分配给常规表上的 INSERT 操作的事务的撤消日志,将用于该事务在常规表上执行的所有 INSERT 操作。

考虑到上述因素,可以使用以下公式来估计 InnoDB 能够支持的并发读写事务数:

  • 如果每个事务只执行 INSERT 、 UPDATE 或 DELETE 中的一个操作,则 InnoDB 能够支持的并发读写事务数为:

    (innodb_page_size / 16) * innodb_rollback_segments * number of undo tablespaces
    
  • 如果每个事务执行一个 INSERT 和一个 UPDATE 或 DELETE 操作,则 InnoDB 能够支持的并发读写事务数为:

    (innodb_page_size / 16 / 2) * innodb_rollback_segments * number of undo tablespaces
    
  • 如果每个事务对临时表执行一个 INSERT 操作,则 InnoDB 能够支持的并发读写事务数为:

    (innodb_page_size / 16) * innodb_rollback_segments
    
  • 如果每个事务对临时表执行一个 INSERT 和一个 UPDATE 或 DELETE 操作,则 InnoDB 能够支持的并发读写事务数为:

    (innodb_page_size / 16 / 2) * innodb_rollback_segments
    

介绍

undo log 是一种用于撤销回退的日志。在事务没提交之前,InnoDB 引擎会先记录更新前的数据到 undo log 日志文件里面,当事务回滚时,可以利用 undo log 来进行回滚。

如下图:

alt undo log

每当 InnoDB 引擎对一条记录进行操作(修改、删除、新增)时,要把回滚时需要的信息都记录到 undo log 里,比如:

  • INSERT:在插入一条记录时,要把这条记录的主键值记下来,这样之后回滚时,只需要把这个主键值对应的记录删除即可;

  • DELETE:在删除一条记录时,要把这条记录中的内容都记下来,这样之后回滚时,再把由这些内容组成的记录插入到表中即可;

  • UPDATE:在更新一条记录时,要把被更新的列的旧值记下来,这样之后回滚,时再把这些列更新为旧值即可。

在发生回滚时,就读取 undo log 里的数据,然后做原先相反操作。比如当 delete 一条记录时,undo log 中会把记录中的内容都记下来,然后执行回滚操作的时候,就读取 undo log 里的数据,然后进行 insert 操作。

不同的操作,需要记录的内容也是不同的,所以不同类型的操作(修改、删除、新增)产生的 undo log 的格式也是不同的。

一条记录的每一次更新操作产生的 undo log 格式都有一个 DB_ROLL_PTR 指针和一个 DB_TRX_ID 事务id:

  • 通过 DB_TRX_ID, 可以知道该记录是被哪个事务修改的;
  • 通过 DB_ROLL_PTR 指针,可以将这些 undo log 串成一个链表,这个链表就被称为版本链;

版本链如下图:

alt undo log

另外,undo log 还有一个作用,通过 ReadView 和 undo log 实现 MVCC(多版本并发控制)。

对于 RC 和 RR 隔离级别的事务来说,它们的快照读(普通 select 语句)是通过 Read View + undo log 来实现的,它们的区别在于创建 Read View 的时机不同:

  • 读提交隔离级别,在每个 select 语句执行时,都会生成一个新的 Read View,也意味着,事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。
  • 可重复读隔离级别,在启动事务时,生成一个 Read View,在整个事务期间都在用这个 Read View,这样就保证了在事务期间读到的数据都是事务启动前的记录。

这两个隔离级别实现是通过事务的 Read View 中的字段和记录中的两个隐藏列(DB_TRX_ID 和 DB_ROLL_PTR)的比对,如果不满足可见行,就会顺着 undo log 版本链里找到满足其可见性的记录,从而控制并发事务访问同一个记录时的行为,从而实现了 MVCC(多版本并发控制)。

因此,undo log 两大作用:

  • 实现事务回滚,保障事务的原子性

    事务处理过程中,如果出现了错误或者用户执行了 ROLLBACK 语句,MySQL 可以利用 undo log 中的历史数据将数据恢复到事务开始之前的状态。

  • 实现 MVCC(多版本并发控制)关键因素之一

    MVCC 是通过 ReadView + undo log 实现的。undo log 为每条记录保存多份历史数据,MySQL 在执行快照读的时候,会根据事务的 Read View 里的信息,顺着 undo log 的版本链找到满足其可见性的记录。

undo log 刷盘时机

开启事务后,InnoDB 引擎更新记录前,首先会记录相应的 undo log,如果是更新操作,需要把被更新的列的旧值记下来,即生成一条 undo log,并写入 Buffer Pool 中的 Undo 页。

alt buffer-pool

undo log 和数据页的刷盘策略是一样的,都需要通过 redo log 保证持久化。

buffer pool 中有 undo 页,对 undo 页的修改也都会记录到 redo log。redo log 会每秒刷盘,提交事务时也会刷盘,数据页和 undo 页都是靠这个机制保证持久化的。

参考:

posted @ 2023-03-28 21:40  LARRY1024  阅读(114)  评论(0编辑  收藏  举报