《MySQL45讲》读书笔记(二):日志的文件是如何保证不丢失的

此文为极客时间:MySQL实战45讲的23节日志相关部分的学习总结

一、持久化的过程

从总的来看,日志一般分为两部分:内存中易遗失的缓存日志和磁盘上持久化的日志文件

一次事务中,日志先被写入内存,存放在 cache/buffer 中,然后事务结束以后准备持久化:先写入磁盘的 page cache 中,这个过程叫做 write ,他仍然是内存操作,只不过从 mysql 的内存去了操作系统的内存,所以比较快;然后再调用操作系统的方法来写入磁盘,这个过程叫做 fsync ,是真正的持久化过程,比较耗费时间。

这个过程,我们可以简单的理解为下图:

日志的持久化过程

基于以上的概念,我们了解一下 binlog 和 redo log 的持久化策略。

二、binlog 的持久化

1.文件结构

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

binlog 写盘状态

每个线程有自己 binlog cache,但是共用同一份 binlog 文件。这样是因为 binlog 的执行是不允许打断的,事务必须完成后完整的写入日志,不能也不允许出现像 redo log 那样两阶段提交只完成了一个阶段就能刷盘的情况。

2.刷盘策略

当事务执行的时候,会先把日志写到内存里的 binlog cache 中,然后事务提交以后再调用 fsync 写入磁盘的 log file 上。由于一个事务的 binlog 不能拆开,因此每次写入都代表一次事务提交。

  • 图中的 write,指的就是指把日志写入到文件系统的 page cache,并没有把数据持久化到磁盘,所以速度比较快。
  • 图中的 fsync,才是将数据持久化到磁盘的操作。一般情况下,我们认为 fsync() 才占磁盘的 IOPS。

write 和 fsync 的时机,是由参数 sync_binlog 控制的:

  1. sync_binlog=0 的时候,表示每次提交事务都只 write,不 fsync
  2. sync_binlog=1 的时候,表示每次提交事务都会执行 fsync
  3. sync_binlog=N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync

因此,在出现 IO 瓶颈的场景里,将 sync_binlog 设置成一个比较大的值,可以提升性能。在实际的业务场景中,考虑到丢失日志量的可控性,一般不建议将这个参数设成 0,比较常见的是将其设置为 100~1000 中的某个数值。

但是,将 sync_binlog 设置为 N,对应的风险是:如果主机发生异常重启,会丢失最近 N 个事务的 binlog 日志。

三、redo log 的持久化

1.刷盘策略

和 binlog 相同,在事务未提交前,生成的日志也会先放在内存,不过不同于 binlog 存放在 binlog cache,redo log 存放在了 redo log buffer

在 mysql 的运行过程中,redo log 的数据可能存在三种状态:

image-20201116175630245

这三种状态分别是:

  1. 红色:存在 redo log buffer 中,物理上是在 MySQL 进程内存中,就是图中的红色部分;
  2. 黄色:写到磁盘 (write),但是没有持久化(fsync),物理上是在文件系统的 page cache 里面,也就是图中的黄色部分;
  3. 绿色:持久化到磁盘,对应的是 hard disk,也就是图中的绿色部分。

为了控制 redo log 的写入策略,InnoDB 提供了 innodb_flush_log_at_trx_commit 参数,它有三种可能取值:

  1. 设置为 0 的时候,表示每次事务提交时都只是把 redo log 留在 redo log buffer 中 ;
  2. 设置为 1 的时候,表示每次事务提交时都将 redo log 直接持久化到磁盘
  3. 设置为 2 的时候,表示每次事务提交时都只是把 redo log 写到 page cache,由后台线程一秒sync一次

一般情况下,不建议设置为0,如果需要的话可以设置为2,因为实际上 redo log 写到 page cache 也很快,而且只留在 redo log buffer 中风险太大,万一数据库崩溃就没法起到重做的效果了。

2.未提交事务写入磁盘的情况

正如前文提到的,innodb 有一个两阶段提交机制,因此在事务未提交的时候,日志是有可能直接在 prepare 阶段就被 write 的:

  1. InnoDB 有一个后台线程,每隔 1 秒,就会把 redo log buffer 中的日志刷盘
  2. redo log buffer 占用的空间即将达到 innodb_log_buffer_size 一半的时候,后台线程会主动刷盘;
  3. 并发事务提交的时候,先提交的那个事务会将 redo log buffer 中的日志全部刷盘。这个行为取决于 innodb_flush_log_at_trx_commit 参数,当该参数为 1 的时候,就会发生这种情况

由于当数据库崩溃后一个事务要同时有 prepare 阶段的 redo log 和 binlog 的记录才会被重做,因此一般情况下,我们会同时设置 sync_binloginnodb_flush_log_at_trx_commit 参数为1,也就是一次事务刷两次盘: binlog 一次,redo log 一次。

3. 组提交

按照上文的逻辑,当设置“双1”配置的时候,实际上要刷盘的次数就会两倍与看到的 TPS,但是事实并非如此。

当事务提交的时候,会通过日志逻辑序列号(LSN)来记录一条日志的长度,通过日志的长度确定事务的起始点和结束点。

假如我们现在有三个日志等待提交:

image-20201116200506743
image-20201116200520213
image-20201116200601118

以上三张图就是三个事务的提交过程。我们可以看到:

  • trx1 是第一个到达的,会被选为这组的 leader;
  • 等 trx1 要开始写盘的时候,这个组里面已经有了三个事务,这时候 LSN 也变成了 160;
  • trx1 去写盘的时候,带的就是 LSN=160,因此等 trx1 返回时,所有 LSN 小于等于 160 的 redo log,都已经被持久化到磁盘;
  • 这时候 trx2 和 trx3 就可以直接返回了。

所以,一次组提交里面,组员越多,节约磁盘 IOPS 的效果越好。但如果只有单线程压测,那就只能老老实实地一个事务对应一次持久化操作了。

在并发更新场景下,第一个事务写完 redo log buffer 以后,接下来这个 fsync 越晚调用,组员可能越多,节约 IOPS 的效果就越好。为此,mysql 在两阶段提交做了一个优化:

image-20201116200841655

我们可以看到,在 redo log 一阶段提交的时候,并没有在 write 完以后立刻就调用 fsync 刷盘,而是等到 binlog 的 write 结束以后才刷盘;binlog 则等 redo log 一阶段刷盘以后才刷盘,因此可以减少 IOPS 的消耗。

不过通常情况下第 3 步执行得会很快,所以 binlog 的 write 和 fsync 间的间隔时间短,导致能集合到一起持久化的 binlog 比较少,因此 binlog 的组提交的效果通常不如 redo log 的效果那么好。

如果你想提升 binlog 组提交的效果,可以通过设置 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 来实现。

  1. binlog_group_commit_sync_delay 参数,表示延迟多少微秒后才调用 fsync;
  2. binlog_group_commit_sync_no_delay_count 参数,表示累积多少次以后才调用 fsync。

这两个条件是或的关系,也就是说只要有一个满足条件就会调用 fsync

所以,当 binlog_group_commit_sync_delay 设置为 0 的时候,binlog_group_commit_sync_no_delay_count 也无效了。

4. redo log 提交策略的选择

适当的刷盘策略可以降低 IO 带来的性能压力。结合以上的内容,我们可以有以下三种策略:

  1. 增大binlog_group_commit_sync_no_delay_countbinlog_group_commit_sync_delay 参数,减少 binlog 刷盘次数,提高组提交的效果,但是这样虽然不会丢失数据,却会增加响应的时间;
  2. 增大 sync_binlog ,但是这样万一数据库崩溃会丢失 binlog 日志;
  3. innodb_flush_log_at_trx_commit 设置为 2,但是这样数据库崩溃会丢失数据,无法重做。

四、总结

持久化过程

日志的持久化分为三步:

  • 写入内存里的 buffer/cache 中,此时仍然归于 mysql 进程;
  • 从内存写入操作系统的 page cache,此操作为 write;
  • 从 page cache 持久化到磁盘,此操作为 fsync,是真正持久化;

日志文件结构

所有线程持久化同一个 binlog 日志文件,但是每个线程都有自己的 binlog cache,这是因为 binlog 必须保证每一次写入都是完整的事务。而 redo log 存在两阶段提交,并且需要 prepare 阶段的 redo log 来重做,所以允许未提交的事务被 write,因此线程共享一个 redo log buffer 和 redo log 日志文件。

刷盘策略

binlog 无论 sync_binlog参数怎么设置,都必须要 write:

  1. sync_binlog=0 的时候,表示每次提交事务都只 write,不 fsync
  2. sync_binlog=1 的时候,表示每次提交事务都会执行 fsync
  3. sync_binlog=N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync

redo log 通过 innodb_flush_log_at_trx_commit 参数控制:

  1. 设置为 0 的时候,表示每次事务提交时都只是把 redo log 留在 redo log buffer 中 ;
  2. 设置为 1 的时候,表示每次事务提交时都将 redo log 直接持久化到磁盘
  3. 设置为 2 的时候,表示每次事务提交时都只是把 redo log 写到 page cache

组提交

mysql 通过日志逻辑序列号(LSN)去根据事务数据长度来记录事务在日志中的开始和结束位置。因此,假如有三个并发事务先后准备写入,第一个被 fsync 的日志可以作为 leader,直接携带从一号到三号事务的 LSN 去刷盘,这样后两个事务就不必再单独刷盘,减少的写磁盘的次数。

针对组提交,二阶段提交进行了优化,redo log 的 write 以后,会等到 binlog 的 wirte 后才调用 fsync,binlog 再等 redo log 的 fsync 完成后才进行 fsync。不过由于时间太短, binlog 的组提交效果不显著,可以通过增大:

  1. binlog_group_commit_sync_delay 参数,表示延迟多少微秒后才调用 fsync;
  2. binlog_group_commit_sync_no_delay_count 参数,表示累积多少次以后才调用 fsync。

两个参数来提高组提交效率,不过这样会降低语句的响应时间。

0%