MySQL 中数据是以页为单位,查询一条记录会将该条记录所在硬盘的数据页的全部数据加载到内存中,加载出来的数据会放入到 Buffer Pool 中。
后续的查询都是先从 Buffer Pool 中找,没有命中再去硬盘加载,这种磁盘业加载的方式可以减少磁盘IO操作,提升速度。
在更新表数据的时候,也是如此,发现 Buffer Pool 里存在要更新的数据,就直接在 Buffer Pool 里更新。
然后会把“在某个数据页上做了什么修改”记录到重做日志缓存(redo log buffer)里。如果开启了binlog,还会记录binlog逻辑日志。
此后会根据配置,在一定条件下对日志文件和表数据文件进行刷盘。通常在一次事务中,只有redo log和binlog日志文件真正发生了写盘操作,数据文件则不会。
此前我们介绍了MySQL的日志系统以及两阶段提交,现在我们来看看数据和日志的刷盘机制。
为了重启机器、机器故障、系统故障之后恢复数据,将内存中的数据写入到硬盘里面,这就是持久化。数据从内存持久化到磁盘,也叫刷盘,通常并不是直接写入磁盘中的,通常需要经历两步POSIX标准函数调用:
一般情况下,write()函数是应用程序来调用,如果第一步完成了的时候,出现了软件层面的问题,比如进程终止或程序崩溃,我们仍然可以说数据持久化成功了,因为只要操作系统没有问题,它最终会自动的将内核缓存区中的数据持久化到磁盘,所以数据是不会丢失的。
因为当在内核缓存区中累积了足够多的数据之后(主要是为了提升性能),操作系统会自动调用fsync()函数同步的将数据真正持久化到磁盘中,也就是说应用程序无需主动调用fsync()。这是一种操作系统的“延迟写”特性,这样做的目的是提高性能,因为fsync()就是同步阻塞的,直到磁盘IO操作完毕后才返回,如果每次都调用fsync()将在一定程度上影响性能。
但这种操作系统自动批量fsync()提交的方式在提高性能的同时也埋下了隐患。默认情况下,Linux 系统将在 30 秒后实际提交写入。可以想象,如果在这三十秒之间主机出现宕机、停电等操作系统级别的事故时,那么由于此时的数据仍然在内核缓存区中(属于内存区域),那么将会导致应用丢失大量的数据。
因此,实际上只有执行同步的刷盘操作才能保证内存的数据每次都一定会真正持久化到了硬盘中。这就导致性能和数据可靠性常常不可兼得。
正常运行中的实例,数据写入后的最终落盘,是从redo log更新过来的还是从buffer pool更新过来的呢?
实际上,redo log并没有记录数据页的完整数据,所以它并没有能力自己去更新磁盘数据页,也就不存在“数据最终落盘,是由redo log更新过去”的情况。
对于表数据的增删改查都是直接在Buffer Poll中进行的,并不会马上同步到磁盘中,慢慢地,Buffer Pool 中的缓存页会因为不断被修改而导致和磁盘文件中的数据不一致了,当Buffer Pool中的内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。
将内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”,这个过程称为“刷脏页”。
MySQL 偶尔慢一下的那个瞬间,可能在刷脏页(flush),也就是将Buffer Poll中的脏数据写入磁盘中实现数据同步。
那么什么时候会触发刷脏?
根据上面四种情况分析,可知出现以下这两种情况,都是会明显影响性能的:
我们可以设置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。
在一个事务的更新过程中,日志可能是要写多次的。比如下面这个事务:
begin;insert into t1 ...insert into t2 ...commit;
这个事务要往两个表中插入记录,插入数据的过程中,生成的日志都得先保存起来,但又不能在还没commit的时候就直接写到redo log文件里。
所以,redo log buffer就是一块内存,用来先存redo日志的。也就是说,在执行第一个insert的时候,数据的内存被修改了,redo log buffer也写入了日志。
但是,真正把日志写到redo log文件(文件名是 ib_logfile+数字),是在执行commit语句的时候做的。
MySQL的"双1"指的是innodb_flush_log_at_trx_commit和sync_binlog两个参数都设置为1,这两个参数是控制MySQL 磁盘写入策略以及数据安全性的关键参数。
事务执行过程中,InnoDB会先把redo log日志写到InnoDB的log buffer内存中。MySQL支持用户自定义在commit(这里的commit指的是sql中的commit,在具体的两阶段提交中对应的prepare阶段)时将log buffer中的日志刷log file中的策略,通过innodb_flush_log_at_trx_commit参数设置:
实际上第一和第三种的情况,是因为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,也是可能已经持久化到磁盘的,有三个地方可以导致:
redo log并没有连续性这个要求,某个事务A在提交前,即使有一部分redo log被事务B提前持久化,但是事务A还是不会进入prepare阶段,该阶段是在事务A自己提交的时候,我们才会走到事务A的redo log prepare这个阶段。
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写入磁盘的策略:
如果sync_binlog=1,则相当于是同步写入磁盘,保证日志的正确持久化,保证存储下了所有提交的事务,也可以保证主从复制的一致性,但性能最低。
在sync_logbin = 0或者>1时,很有可能机器出现crash,日志并没有同步到磁盘,重启后,有丢失事务。不能够用来进行数据恢复,对于主从配置也会由于二进制日志的position比备库同步过去的position小,进而造成主从数据不一致情况。
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”配置。
作者:刘Java
链接:https://juejin.cn/post/7126357291456004110
来源:稀土掘金
留言与评论(共有 0 条评论) “” |