MySQL-16.binlog和redo log写入机制以及性能
WAL 机制:只要 redo log 和 binlog 保证持久化到磁盘,就能确保 MySQL 异常重启后,数据可以恢复。
1.binlog 的写入机制
- step1.事务执行过程中,先把日志写到 binlog cache
- step2.事务提交的时候,再把 binlog cache 写到 binlog 文件中
一个事务的 binlog 是不能被拆开的,因此不论这个事务多大,也要确保一次性写入。这就涉及到了 binlog cache 的保存问题。系统给 binlog cache 分配了一片内存,每个线程一个,参数 binlog_cache_size 用于控制单个线程内 binlog cache 所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。
每个线程有自己 binlog cache,但是共用同一份 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 日志。
2.redo log 的写入机制
事务在执行过程中,生成的redo log 是要先写到 redo log buffer 的。redo log buffer里面的内容,不是每次生成后都要直接持久化到磁盘。
如果事务执行期间 MySQL 发生异常重启,那这部分日志就丢了。由于事务并没有提交,所以这时日志丢了也不会有损失。
事务还没提交的时候,redo log buffer 中的部分日志有可能被持久化到磁盘。
redo log 可能存在的三种状态:
- 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。
InnoDB 有一个后台线程,每隔 1 秒,就会把 redo log buffer 中的日志,调用 write 写到文件系统的 page cache,然后调用 fsync 持久化到磁盘。
让一个没有提交的事务的 redolog 写入到磁盘中的三种情况:
- 1)事务执行中间过程的 redo log 也是直接写在 redo log buffer 中的,这些redo log 也会被后台线程一起持久化到磁盘。也就是说,一个没有提交的事务的 redo log,也是可能已经持久化到磁盘的。
- 2)redo log buffer 占用的空间即将达到 innodb_log_buffer_size 一半的时候,后台线程会主动写盘。注意,由于这个事务并没有提交,所以这个写盘动作只是 write,而没有调用 fsync,也就是只留在了文件系统的 page cache。
- 3)并行的事务提交的时候,顺带将这个事务的 redo log buffer 持久化到磁盘。假设一个事务 A 执行到一半,已经写了一些 redo log 到 buffer 中,这时候有另外一个线程的事务 B 提交,如果 innodb_flush_log_at_trx_commit 设置的是 1,那么按照这个参数的逻辑,事务 B 要把 redo log buffer 里的日志全部持久化到磁盘。这时候,就会带上事务A 在 redo log buffer 里的日志一起持久化到磁盘。
两阶段提交:时序上 redo log 先 prepare, 再写binlog,最后再把 redo log commit。
如果把 innodb_flush_log_at_trx_commit 设置成 1,那么 redo log 在 prepare 阶段就要持久化一次,因为有一个崩溃恢复逻辑是要依赖于 prepare 的 redo log,再加上 binlog 来恢复的。每秒一次后台轮询刷盘,再加上崩溃恢复这个逻辑,InnoDB 就认为 redo log 在 commit 的时候就不需要 fsync 了,只会 write 到文件系统的 page cache 中就够了。
通常我们说 MySQL 的“双 1”配置,指的就是 sync_binlog 和innodb_flush_log_at_trx_commit 都设置成 1。也就是说,一个事务完整提交前,需要等待两次刷盘,一次是 redo log(prepare 阶段),一次是 binlog。
从 MySQL 看到的 TPS 是每秒两万的话,每秒就会写四万次磁盘。但是,用工具测试出来,磁盘能力也就两万左右,怎么能实现两万的 TPS?
Ans:组提交(group commit)机制
- 日志逻辑序列号(log sequence number,LSN),是单调递增的,用来对应 redo log 的一个个写入点。每次写入长度为 length 的 redo log, LSN的值就会加上 length。LSN 也会写到 InnoDB 的数据页中,来确保数据页不会被多次执行重复的 redo log。
三个并发事务 (trx1, trx2, trx3) 在 prepare 阶段,都写完 redo log buffer,持久化到磁盘的过程,对应的 LSN 分别是 50、120 和 160。
- 1)trx1 是第一个到达的,会被选为这组的 leader;
- 2)等 trx1 要开始写盘的时候,这个组里面已经有了三个事务,这时候 LSN 也变成了 160;
- 3)trx1 去写盘的时候,带的就是 LSN=160,因此等 trx1 返回时,所有 LSN 小于等于 160 的redo log,都已经被持久化到磁盘;
- 4)这时候 trx2 和 trx3 就可以直接返回了。
所以,一次组提交里面,组员越多,节约磁盘 IOPS 的效果越好。但如果只有单线程压测,那就只能老老实实地一个事务对应一次持久化操作了。
在并发更新场景下,第一个事务写完 redo log buffer 以后,接下来这个 fsync 越晚调用,组员可能越多,节约 IOPS 的效果就越好。
为了让一次 fsync 带的组员更多,MySQL 有一个很有趣的优化:拖时间。
实际上,写 binlog 是分成两步的:
- 1)先把 binlog 从 binlog cache 中写到磁盘上的 binlog 文件;
- 2)调用 fsync 持久化。
MySQL 为了让组提交的效果更好,把 redo log 做 fsync 的时间拖到了步骤 1 之后。
binlog 也可以组提交了。在执行中第 4 步把 binlog fsync 到磁盘时,如果有多个事务的 binlog 已经写完了,也是一起持久化的,这样也可以减少 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。
WAL 机制主要得益于两个方面:
- 1) redo log 和 binlog 都是顺序写,磁盘的顺序写比随机写速度要快;
- 2)组提交机制,可以大幅度降低磁盘的 IOPS 消耗。
3.性能瓶颈在 IO上
如果 MySQL 现在出现了性能瓶颈,而且瓶颈在 IO上,可以通过哪些方法来提升性能呢?
Ans:
- 1)设置 binlog_group_commit_sync_delay 和binlog_group_commit_sync_no_delay_count 参数,减少 binlog 的写盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间,但没有丢失数据的风险。
- 2) 将 sync_binlog 设置为大于 1 的值(比较常见是 100~1000)。这样做的风险是,主机掉电时会丢 binlog 日志。
- 3)将 innodb_flush_log_at_trx_commit 设置为 2。这样做的风险是,主机掉电的时候会丢数据。
在什么时候会把线上生产库设置成“非双 1”?
Ans:
- 1)业务高峰期。一般如果有预知的高峰期,DBA 会有预案,把主库设置成“非双 1”。
- 2)备库延迟,为了让备库尽快赶上主库。
由于从库设置了binlog_group_commit_sync_delay 和binlog_group_commit_sync_no_delay_count 导致一直延迟的情况。我们在主库设置这两个参数,是为了减少 binlog 的写盘压力。备库这么设置,尤其在“快要追上”的时候,就反而会受这两个参数的拖累。一般追主备就用“非双 1”(追上记得改回来)。 - 3)用备份恢复主库的副本,应用 binlog 的过程,这个跟上一种场景类似。
- 4)批量导入数据的时候。
一般情况下,把生产库改成“非双 1”配置,是设置innodb_flush_logs_at_trx_commit=2、sync_binlog=1000。
4.相关问题
Q1.执行一个 update 语句以后,我再去执行 hexdump 命令直接查看 ibd 文件内容,为
什么没有看到数据有改变呢?
Ans:
- 这可能是因为 WAL 机制的原因。update 语句执行完成后,InnoDB 只保证写完了 redolog、内存,可能还没来得及将数据写到磁盘。
Q2.为什么 binlog cache 是每个线程自己维护的,而 redo log buffer 是全局共用的?
Ans:
- MySQL 这么设计的主要原因是,binlog 是不能“被打断的”。一个事务的 binlog 必须
连续写,因此要整个事务完成后,再一起写到文件里。
而 redo log 并没有这个要求,中间有生成的日志可以写到 redo log buffer 中。redo log
buffer 中的内容还能“搭便车”,其他事务提交的时候可以被一起写到磁盘中。
Q3.事务执行期间,还没到提交阶段,如果发生 crash 的话,redo log 肯定丢了,这会不
会导致主备不一致呢?
Ans:
- 不会。因为这时候 binlog 也还在 binlog cache 里,没发给备库。crash 以后 redo log
和 binlog 都没有了,从业务角度看这个事务也没有提交,所以数据是一致的。
Q4.如果 binlog 写完盘以后发生 crash,这时候还没给客户端答复就重启了。等客户端再
重连进来,发现事务已经提交成功了,这是不是 bug?
Ans:
- 不是。
- 你可以设想一下更极端的情况,整个事务都提交成功了,redo log commit 完成了,备库也收到 binlog 并执行了。但是主库和客户端网络断开了,导致事务成功的包返回不回去,这时候客户端也会收到“网络断开”的异常。这种也只能算是事务成功的,不能认为是 bug。
- 实际上数据库的 crash-safe 保证的是:
1)如果客户端收到事务成功的消息,事务就一定持久化了;
2)如果客户端收到事务失败(比如主键冲突、回滚等)的消息,事务就一定失败了;
3)如果客户端收到“执行异常”的消息,应用需要重连后通过查询当前状态来继续后续的逻辑。此时数据库只需要保证内部(数据和日志之间,主库和备库之间)一致就可以了。