likes
comments
collection
share

MySQL数据和日志的刷盘机制以及双一配置

作者站长头像
站长
· 阅读数 15

详细介绍了MySQL数据和日志的刷盘机制以及双一配置,双一配置可以保证Mysql日志数据不丢失。

MySQL 中数据是以页为单位,查询一条记录会将该条记录所在硬盘的数据页的全部数据加载到内存中,加载出来的数据会放入到 Buffer Pool 中。

后续的查询都是先从 Buffer Pool 中找,没有命中再去硬盘加载,这种磁盘业加载的方式可以减少磁盘IO操作,提升速度。

在更新表数据的时候,也是如此,发现 Buffer Pool 里存在要更新的数据,就直接在 Buffer Pool 里更新。

然后会把“在某个数据页上做了什么修改”记录到重做日志缓存(redo log buffer)里。如果开启了binlog,还会记录binlog逻辑日志。

此后会根据配置,在一定条件下对日志文件和表数据文件进行刷盘。通常在一次事务中,只有redo log和binlog日志文件真正发生了写盘操作,数据文件则不会。

此前我们介绍了MySQL的日志系统以及两阶段提交,现在我们来看看数据和日志的刷盘机制。

1 内存数据的刷盘机制

为了重启机器、机器故障、系统故障之后恢复数据,将内存中的数据写入到硬盘里面,这就是持久化。数据从内存持久化到磁盘,也叫刷盘,通常并不是直接写入磁盘中的,通常需要经历两步POSIX标准函数调用:

  1. write:由应用程序调用POSIX file API 中的write()函数将数据从用户进程缓冲区(程序内存)写入磁盘,这一步write()函数实际上仅仅写入到了内核缓冲区(或者说磁盘映射内存os cache,或者说文件系统的page cache),这一块区域仍然属于文件系统向内核申请的一块的内存区域,并没有真正的把数据持久化到磁盘,所以速度是比较快的。
  2. fsync:调用POSIX file API 中的fsync()函数同步的将这些缓存数据真正的持久化到物理磁盘中,这一步完成之后,数据才算真正的完成持久化。一般情况下,我们认为fsync()才占磁盘的IOPS。

一般情况下,write()函数是应用程序来调用,如果第一步完成了的时候,出现了软件层面的问题,比如进程终止或程序崩溃,我们仍然可以说数据持久化成功了,因为只要操作系统没有问题,它最终会自动的将内核缓存区中的数据持久化到磁盘,所以数据是不会丢失的。

因为当在内核缓存区中累积了足够多的数据之后(主要是为了提升性能),操作系统会自动调用fsync()函数同步的将数据真正持久化到磁盘中,也就是说应用程序无需主动调用fsync()。这是一种操作系统的“延迟写”特性,这样做的目的是提高性能,因为fsync()就是同步阻塞的,直到磁盘IO操作完毕后才返回,如果每次都调用fsync()将在一定程度上影响性能。

但这种操作系统自动批量fsync()提交的方式在提高性能的同时也埋下了隐患。默认情况下,Linux 系统将在 30 秒后实际提交写入。可以想象,如果在这三十秒之间主机出现宕机、停电等操作系统级别的事故时,那么由于此时的数据仍然在内核缓存区中(属于内存区域),那么将会导致应用丢失大量的数据。

因此,实际上只有执行同步的刷盘操作才能保证内存的数据每次都一定会真正持久化到了硬盘中。这就导致性能和数据可靠性常常不可兼得。

2 MySQL数据的刷盘

2.1 刷盘数据来源

正常运行中的实例,数据写入后的最终落盘,是从redo log更新过来的还是从buffer pool更新过来的呢?

实际上,redo log并没有记录数据页的完整数据,所以它并没有能力自己去更新磁盘数据页,也就不存在“数据最终落盘,是由redo log更新过去”的情况。

  1. 如果是正常运行的实例的话,数据页在内存中被修改以后,跟磁盘的数据页不一致,称为脏页。最终数据落盘,就是把内存中的脏数据写盘的过程。这个过程,甚至与redo log中的数据毫无关系,仅仅和buffer pool中的数据有关。
  2. 在崩溃恢复场景中,InnoDB如果判断到一个数据页可能在崩溃的时候丢失了更新,就会将它读到内存,然后才会让redo log更新内存内容。更新完成后,内存页变成脏页,就回到了第一种情况的状态。

2.2 脏页以及刷盘机制

对于表数据的增删改查都是直接在Buffer Poll中进行的,并不会马上同步到磁盘中,慢慢地,Buffer Pool 中的缓存页会因为不断被修改而导致和磁盘文件中的数据不一致了,当Buffer Pool中的内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。

将内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”,这个过程称为“刷脏页”。

MySQL 偶尔慢一下的那个瞬间,可能在刷脏页(flush),也就是将Buffer Poll中的脏数据写入磁盘中实现数据同步。

那么什么时候会触发刷脏?

  1. innodb的redo log写满了,需要把checkpoint往前推进。这种情况是InnoDB要尽量避免的。因为出现这种情况的时候,整个系统就不能再接受更新了,所有的更新都必须堵住。如果你从监控上看,这时候更新数会跌为0。
  2. 一个查询buffer pool缓冲池内存不足时,会淘汰一些数据页缓存(最近最久未使用算法,LRU),有可能会淘汰到脏页缓存,此时就要先把脏页刷到磁盘。刷脏页一定会写盘,这是为了保证每个数据页有两种状态:1、内存里的一定是正确数据;2、内存里没有,磁盘上的一定是正确数据。
  3. MySQL认为系统空闲时,会刷盘。当然系统繁忙时,也会见缝插针刷盘。
  4. MySQL正常关闭时。

根据上面四种情况分析,可知出现以下这两种情况,都是会明显影响性能的:

  1. 一个没有内存缓存的查询要淘汰的脏页个数太多,会导致查询的响应时间明显变长;
  2. 日志写满,更新全部堵住,写性能跌为0,这种情况对敏感业务来说,是不能接受的。

我们可以设置InnoDB的某些参数,来尽量避免上面的这两种情况。

首先需要告诉InnoDB所在主机的IO能力,这样InnoDB才能知道需要全力刷脏页的时候,可以刷多快。可以使用innodb_io_capacity参数,它会告诉InnoDB你的磁盘能力。这个值建议设置成磁盘的IOPS。磁盘的IOPS可以通过fio这个工具来测试,下面的语句是用来测试磁盘随机读写的命令:

fio -filename=$filename -direct=1 -iodepth 1 -thread -rw=randrw -ioengine=psync -bs=16k -size=500M -numjobs=10 -runtime=10 -group_reporting -name=mytest

参数innodb_max_dirty_pages_pct是脏页比例上限,默认值是75%。脏页比例是通过Innodb_buffer_pool_pages_dirty/Innodb_buffer_pool_pages_total 参数得到。

另外还有一个策略,当刷脏页时,该页边上也是脏页,也会把边上的脏页一起刷掉。而且该逻辑会一直蔓延。innodb_flush_neighbors 参数就是来控制该行为的,值为1会有上述机制,0则不会。机械硬盘可能会有不错的效果,但ssd建议设置为0。并且mysql 8.0 innodb_flush_neighbors 默认为0。

3 MySQL日志的刷盘以及双一配置

3.1 redo log buffer

在一个事务的更新过程中,日志可能是要写多次的。比如下面这个事务:

begin;
insert into t1 ...
insert into t2 ...
commit;

这个事务要往两个表中插入记录,插入数据的过程中,生成的日志都得先保存起来,但又不能在还没commit的时候就直接写到redo log文件里。

所以,redo log buffer就是一块内存,用来先存redo日志的。也就是说,在执行第一个insert的时候,数据的内存被修改了,redo log buffer也写入了日志。

但是,真正把日志写到redo log文件(文件名是 ib_logfile+数字),是在执行commit语句的时候做的。

3.2 日志的刷盘和双一配置

MySQL的"双1"指的是innodb_flush_log_at_trx_commitsync_binlog两个参数都设置为1,这两个参数是控制MySQL 磁盘写入策略以及数据安全性的关键参数

3.3 redo log刷盘

事务执行过程中,InnoDB会先把redo log日志写到InnoDB的log buffer内存中。MySQL支持用户自定义在commit(这里的commit指的是sql中的commit,在具体的两阶段提交中对应的prepare阶段)时将log buffer中的日志刷log file中的策略,通过innodb_flush_log_at_trx_commit参数设置:

  1. 设置为0:仅将日志写入log file buffer中。该模式下,在事务提交的时候,不会主动触发写入磁盘的操作,仅依靠InnoDB 的后台线程每秒执行一次刷盘操作,即每秒一次write cache和flush disk
  2. 设置为1:每次事务commit时MySQL都会把log buffer的数据立即写入log file的os cache中,并且立即flush刷到磁盘中去。即每次commit都write cache和flush disk,这是默认设置
  3. 设置为2:每次事务commit时MySQL都会把log buffer的数据写入log file的os cache 缓存,但是flush刷到磁盘的操作并不会同时进行,仅依靠InnoDB 的后台线程每秒执行一次真正的刷盘操作。即每次commit都write cache,每秒一次flush disk

实际上第一和第三种的情况,是因为InnoDB有一个后台线程,每隔1秒,就会把redo log buffer中的日志,调用write写到文件系统的page cache,然后调用fsync持久化到磁盘。即每秒一次write cache和flush disk

innodb_flush_log_at_trx_commit设置为0,mysqld进程的崩溃会导致上一秒钟所有事务数据的丢失。当innodb_flush_log_at_trx_commit设置为2,只有在操作系统崩溃或者系统掉电的情况下,上一秒钟所有事务数据才可能丢失(已经写入磁盘缓存中的数据,即使应用崩溃了,操作系统最终还是会将磁盘缓存中的数据持久化到磁盘中)。

如果把innodb_flush_log_at_trx_commit设置成1,那么所有的redo log事物都不会丢失。那么redo log在prepare阶段就要持久化一次。由于每秒一次后台轮询刷盘,再加上崩溃恢复这个逻辑,InnoDB就认为redo log在commit的时候就不需要fsync了,只会write到文件系统的page cache中就够了,后续等着操作系统的刷盘。

当然,一个没有提交的事务的内存中的redo log,也是可能已经持久化到磁盘的,有三个地方可以导致:

  1. 如果innodb_flush_log_at_trx_commit=0,那么事务执行中间过程的redo log也是直接写在redo log buffer中的,这些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在redolog buffer里的日志一起持久化到磁盘。

redo log并没有连续性这个要求,某个事务A在提交前,即使有一部分redo log被事务B提前持久化,但是事务A还是不会进入prepare阶段,该阶段是在事务A自己提交的时候,我们才会走到事务A的redo log prepare这个阶段。

3.4 binlog刷盘

binlog日志同样有自己的binlog cache,为了保证binlog的连续性,每一个线程都分配了自己的binlog cache,但共用一份binlog 文件。参数binlog_cache_size用于控制单个线程内binlog cache所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。

binlog 不能“被打断”,主要原因是一个线程只能同时有一个事务在执行。由于这个设定,所以每当执行一个begin/start transaction的时候,就会默认提交上一个事务;如果一个事务的binlog被拆开的时候,在备库执行就会被当做多个事务分段执行,这样破坏了原子性,是有问题的。

事务执行过程中,会先把日志写到线程自己的binlog cache,事务提交的时候,再把binlog cache写到binlog文件中,随后清空binlog cache。

sync_binlog参数控制着事务提交时binlog写入磁盘的策略:

  1. sync_binlog 的默认值是0,将二进制日志从binlog cache写入log file的os cache,但是不主动进行flush刷盘操作,而是依赖操作系统来判断什么时候刷盘,若操作系统宕机则会丢失部分二进制日志。
  2. 当sync_binlog =N (N>0) ,MySQL在每写N次二进制日志binary log时,会使用fsync()函数将它同步刷到磁盘中去。

如果sync_binlog=1,则相当于是同步写入磁盘,保证日志的正确持久化,保证存储下了所有提交的事务,也可以保证主从复制的一致性,但性能最低。

在sync_logbin = 0或者>1时,很有可能机器出现crash,日志并没有同步到磁盘,重启后,有丢失事务。不能够用来进行数据恢复,对于主从配置也会由于二进制日志的position比备库同步过去的position小,进而造成主从数据不一致情况。

3.5 总结

innodb_flush_log_at_trx_commit这个参数设置成1的时候,可以保证MySQL异常重启之后redo log数据不丢失。sync_binlog这个参数设置成1的时候,可以保证MySQL异常重启之后binlog不丢失。

如果采用"双1"配置,那么MySQL在每一个事务完整commit提交前,需要至少进行两次即时刷盘,一次是 redo log(prepare 阶段),一次是 binlog,此时安全性最高,在mysql服务崩溃或者服务器主机crash的情况下,不会丢失语句和事务。由于每次commot都有两次即时的刷盘操作,导致了大量的IO操作,导致性能急剧下降。

"双1"设置适合数据安全性要求非常高,而且磁盘IO写能力足够支持业务,比如订单,交易,充值,支付消费系统。双1模式下,当磁盘IO无法满足业务需求时 比如11.11 活动的压力。推荐的做法是 innodb_flush_log_at_trx_commit=2 ,sync_binlog=N (N为500 或1000) 且使用带蓄电池后备电源的缓存cache,防止系统断电异常。

在主从复制结构中,要保证事务的持久性和一致性,应该使用“双1”配置。

参考资料:

  1. 《 MySQL 技术内幕: InnoDB 存储引擎》
  2. 《高性能 MySQL》
  3. 《MySQL实战45讲 | 极客时间 | 丁奇》
  4. JavaGuide

如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!