mysql日志——如何保证数据不丢失

crash safe

crash safe,即崩溃恢复,InnoDB可以保证即使数据库发生了异常重启,之前提交的记录都不会丢失。
InnoDB有这个能力得益于redo log,前面说过mysql会先将具体的操作记录写入到redo log,这些记录最后都是要刷到磁盘上去的。
下面再看一下两阶段提交,来看一下mysql崩溃恢复的规则判断:
两阶段提交补充
1.如果redo log事务是完整的,即有了commit标志,则直接提交。
2.如果redo log中没有commit标志,只到了prepare阶段,则判断对应的binlog日志是否完整:
如果完整,则提交事务
如果不完整,则回滚事务
以上判断的两种情况分别对应了上图的时刻A和时刻B mysql的异常重启。

mysql是如何判断bin log是否完整的?
bin log有3中格式的记录(通过配置binlog_format修改记录格式),分别是rows,statement,mixed。statement格式的binlog,最后会有COMMIT,row格式的binlog,最后会有一个XID event。

mysql中redo log和bin log是如何关联起来的?
它们有一个共同的数据字段,叫XID。崩溃恢复的时候,会按顺序扫描redo log:
如果碰到既有prepare、又有commit的redo log,就直接提交
如果碰到只有parepare、而没有commit的redo log,就拿着XID去binlog找对应的事务

从crash safe的逻辑看,只要保证bin log和redo log持久化到磁盘,就是保证已经commit的事务,在mysql异常重启后数据能恢复。

bin log持久化机制

前面说过,bin log是在两阶段提交中,redo log已经prepare后再写磁盘的。其实bin log并非直接写的磁盘,而是先写到binlog cache,事务提交的时候再把binlog cache写到bin log file。
mysql给每个线程提供一个binlog cache,通过binlog_cache_size控制每个binlog cache的大小,默认是32kb。如果单个线程的日志binlog cache放不下,比如说大事务,binlog cache数据会暂存到磁盘。

事务提交的时候,binlog cache和暂存在磁盘上的数据会持久化到bin log file,并且清空binlog cache和暂存的磁盘数据。示意图如下所示:
binlog写入机制
图中,write是把数据写入操作系统文件系统的page cache,速度比较快;fsync才是真正写入磁盘的操作,这个过程才占用磁盘的IOPS。

通过参数sync_binlog可以控制bin log的write和fsync策略:

  • sync_binlog=0的时候,表示只write,不fsync
  • sync_binlog=1的时候,表示每次事务提交都会fsync
  • sync_binlog=N(N>1)的时候,表示每次提交都write,但是要累积到N个事务才fsync

不建议将sync_binlog设置为0!sync_binlog默认设置是1,如果发生IO瓶颈,可以尝试将sync_binlog调整到比1大的数字,风险是:如果mysql异常重启,将丢失最近的N个bin log日志。

redo log持久化机制

和bin log写入一样,为了提升写入速度减少不必要的IO,redo log先将日志写入redolog buffer。
redo log依次会经过redo log buffer,fs page cache,hard disk,示意图如下:
redolog写入机制
这三种状态分别代表:
1.存在redo log buffer中,物理上是在MySQL进程内存中,就是图中的红色部分
2.写到磁盘(write),但是没有持久化(fsync),物理上是在文件系统的page cache里面,也就是图中的黄色部分
3.持久化到磁盘,对应的是hard disk,也就是图中的绿色部分

和bin log类似,redo log通过innodb_flush_log_at_trx_commit参数控制write和fsync策略:
1.innodb_flush_log_at_trx_commit=0,表示每次事务提交时都只是把redo log留在redo log buffer中
2.innodb_flush_log_at_trx_commit=1,表示每次事务提交时都将redo log直接持久化到磁盘
3.innodb_flush_log_at_trx_commit=2,表示每次事务提交时都将redo log写到page cache

InnoDB后台有线程每隔1s将redo log buffer中日志write到page cache,然后调用fsync持久化到磁盘。但是也存在日志被迫写盘的情况:
1.redo log buffer占用的空间即将达到innodb_log_buffer_size一半的时候,后台线程会主动写盘。只是write到page cache,等待事务提交后再fsync持久化到磁盘
2.并行的事务提交的时候,顺带将这个事务的redo log buffer持久化到磁盘
画外音:由于后台的线程刷盘和并行事务的提交,还未提交的事务的一部分redo log也可能持久化到磁盘上。如果被迫刷盘的事务回滚了,就没有commit标志,也不会应用其中那部分被迫刷盘的redo log。

innodb_flush_log_at_trx_commit=1的时候,由于崩溃恢复逻辑中需要用到redo log日志,所以在两阶段prepare阶段就需要fsync到磁盘,由于存在后台线程每个1s的刷盘,在commit阶段就只需要write到page cache即可。

组提交

一般建议将sync_binlog和innodb_flush_log_at_trx_commit都配置成1,也就是mysql所谓的双1配置。这样事务在每次提交的时候需要进行两次fsync(redo log prepare阶段一次,bin log一次)。但是,在磁盘IOPS上限是每秒2w的配置下,mysql依然可以支持TPS每秒2w的请求,这是因为一次fsync可能持久化多个事务的redo log,这就是mysql的组提交优化。

前面flush脏页时有提到过LSN(log sequense number)的概念,其是单调递增的,对应redo log上一个个写入的点,每次写入长度为length的redo log, LSN的值就会加上length。LSN也会写入到数据页中,避免redo log中的日志重复应用到数据页磁盘上。
在innodb_flush_log_at_trx_commit=1的时候,假如有3个并发的事务(trx1,trx2,trx3)在prepare 阶段,都写完redo log buffer,对应的LSN分别是50,120,160:
redo log组提交
1.trx1第一个达到,被选为group commit的leader
2.等trx1开始fsync的时候,总共有3个事务在redo log buffer,LSN=160
3.trx1 fsync携带的LSN是160,即trx2和trx3的redo log一起被写入了redo log file
4.此时LSN小于160的redo log都已经刷盘成功了,trx2和trx3可以直接返回了
所以一次组提交中事务越多,节约磁盘IOPS的效果越好。

基于上面的组提交,mysql在两阶段提交时又进行拖时间的优化,即将write和fsync拆开:
两阶段提交优化
这么一来,binlog也可以组提交了。在执行图5中第4步把binlog fsync到磁盘时,如果有多个事务的binlog已经写完了,也是一起持久化的,这样也可以减少IOPS的消耗。
不过通常情况下第3步执行得会很快,所以binlog的write和fsync间的间隔时间短,导致能集合到一起持久化的binlog比较少,因此binlog的组提交的效果通常不如redo log的效果那么好。

但是你可以设置以下两个参数来增加bin log cache中的事务:
1.binlog_group_commit_sync_delay表示延迟多少微秒后才调用fsync
2.binlog_group_commit_sync_no_delay_count表示累积多少个后才调用fsync
两者是或的关系,有一个满足了就调用fsync。

小结

今天一开始介绍了mysql的crash safe能力,为了拥有这种能力,必须保证bin log和redo log持久化到磁盘。
后续讲述了bin log和redo log刷到对应日志文件的具体细节,以及它们各自刷盘的控制策略,一般建议使用双1配置
在顺序写的基础上,mysql为了进一步节约IOPS,进行了日志的组提交优化,在此基础上两阶段提交时将write和fsync拆开,拖时间来增加group commit中日志的数量。

显示 Gitment 评论