谁也不能保证计算机系统能够永远无故障的执行下去。网络波动、磁盘损坏等现网高频故障,机房掉电、服务器硬件失效等低频却又致命的故障,时刻考验着我们的系统。
不涉及存储的纯计算系统崩溃/失效之后,隔离故障节点或者重启故障节点后就能恢复业务。
存储系统却没有那么简单。比如碰到掉电,没有考虑过数据一致性的系统成功重启后,业务调用方大概率会发现数据丢失,这在某些业务场景下是不能接受的。成熟的存储系统一定会根据使用场景的需求对数据一致性做一些文章。
作为互联网公司使用得最多的通用数据库系统,MySQL,在数据一致性方面就有较多的考虑,同时也给了用户较多的设置选项,用来满足不同业务场景下数据一致性和性能的需求(业务需要对数据一致性和性能做权衡,这里不展开)。MySQL数据一致性大体上包括两方面:单机数据一致性和集群数据一致性,本文就围绕这两方面进行说明。
单机数据一致性
MySQL崩溃后,保证单机数据一致性主要包括两个机制:“MySQL binary log和InnoDB redo log的一致性”和“InnoDB数据文件的一致性”。
MySQL binary log和InnoDB redo log的一致性
MySQL binary log,简称binlog,是MySQL Server层维护的一种二进制日志,记录了对MySQL数据库执行更改的所有操作相关的信息。binlog的作用主要是:数据恢复、数据复制和审计等。数据恢复的一个场景是,MySQL崩溃后对数据进行数据恢复,MySQL Server层通过binlog恢复已经写入binlog却没有写入数据文件的数据(简单这么说)。
InnoDB redo log,简称redolog,是InnoDB(存储引擎层)用来实现事务持久性,既事务ACID中的D,它由两部分组成:一是redo log file,保证存储引擎管理的数据落盘,是持久的;二是内存中的redo log buffer,是易失的,log buffer中的数据会按一定的机制批量刷新到磁盘,这样做可以提高吞吐效率。
有了MySQL Server层的binlog后为什么还需要InnoDB(存储引擎层)的redolog呢?这是因为MySQL架构将存储引擎插件化了,真正管理存储数据的是存储引擎,这就导致MySQL Server层不能做到crash-safe,需要存储引擎根据需求场景实现crash-safe。InnoDB作为一个通用场景的存储引擎,有的场景需要保证较强的数据一致性,所以它就实现了redolog(当然还包括undolog等)。
那为什么需要让redolog和binlog保持一致呢?这是因为binlog会被用来复制数据,常见的场景是“1主N备”中的备机通过binlog同步主机的数据。如果binlog和redlog不一致,会导致主备数据不一致。在一致性要求高的场景,保证binlog和redolog的一致性就非常重要了。
MySQL使用内部XA事务(两阶段提价,2PC)保证要么binlog和redolog同时成功或者同时失败。图1展示了这个2PC过程。在做Crash recovery时有如下场景:
- binlog有记录,redolog状态commit:正常完成的事务,不需要恢复;
- binlog有记录,redolog状态prepare:在binlog写完提交事务之前的crash,恢复操作:提交事务;
- binlog无记录,redolog状态prepare:在binlog写完之前的crash,恢复操作:回滚事务;
- binlog无记录,redolog无记录:在redolog写之前crash,恢复操作:回滚事务;
可以看到,数据是以binlog为准的,这是保证集群数据一致性的基础。
InnoDB数据文件的一致性
数据文件的写操作,可能会将块写坏,InnoDB使用双写缓冲(double write buffer)来确保数据的安全,避免损坏块。双写缓冲是InnoDB表空间的一个特殊的区域,主要用于写入页的备份,并且是顺序写入。当InnoDB刷新数据(从InnoDB缓冲池到磁盘)时,首先写入双写缓冲,然后写入实际数据文件。这样既可确保所有写操作的原子性和持久性。
MySQL崩溃重启后,InnoDB会检查每个块(page)的校验和,判断块是否损坏,如果写入双写缓冲的是坏块,那么一定没有写入实际数据文件,就要用实际数据文件的块来恢复双写缓冲,如果写入了双写缓冲,但是数据文件写的是坏块,那么就用双写缓冲的块来重写数据文件。这个机制提升了数据灾难恢复机制,也就提升了数据一致性。
集群数据一致性
原生MySQL有很多种搭建集群的方式,这里为了把原理说清楚,只对“1主N备”的集群形式做说明。这种形态的集群主要考虑的是master和slave的数据一致性。
首先介绍一下主备数据同步的流程,图2简化地展示了这个过程。
- 主库(master)把数据更改记录到二进制日志(binlog)中;
- 在每次准备提交事务完成数据更新前,主库将数据更新的事件记录到二进制中。MySQL会按事务提交的顺序而非每条语句的执行顺序来记录二进制日志。在记录二进制日志后,主库会告诉存储引擎可以提交事务了。
- 从库(slave)把主库的二进制日志复制到自己的中继日志(relaylog)中;
- 从库将主库的二进制日志复制到其本地的中继日志中。首先,从库会启动一个工作线程,称为IO线程,IO线程跟主库建立一个普通的客户端连接,然后在主库上启动一个特殊的二进制转储(binlog dump)线程,这个二进制转储线程会读取主库上二进制日志中的事件。IO线程不会对事件进行轮询,如果追赶上了主库,它就会进入休眠状态,等到主库发送信号量通知其有新的事件产生时才会被唤醒。IO线程会将接收到的事件记录到中继日志中。
- 从库读取中继日志中的事件,将其重放到本地数据;
- 从库的SQL线程执行最后一步,它从中继日志中读取事件并在备库执行,实现备库的数据更新。
MySQL主备同步的方式主要有两种:异步复制,半同步复制(包括增强半同步复制)。在主备同步过程中,主机、备机、主机备机之间的网络可能会出现各种问题,也就导致了潜在的数据不一致。下面我们依次分析“异步复制”和“半同步复制”面对不同的故障场景,集群的数据一致性问题。
1. 异步复制
主库写binlog成功之后就返回客户端结果,不会确认从库是否收到。下面来看看异步复制里的具有代表性异常场景。
1.1 异常场景
异常描述:
主库写入binlog并返回客户端结果后崩溃了,从库并没有收到主库的二进制日志事件。
恢复影响:
- 切换主库。数据丢失;
- 恢复主库。磁盘不损坏时数据不丢失,但相对于主备切换,恢复时间较长;磁盘损坏时,主库无法恢复,数据丢失;
2. 半同步复制
半同步有两种方式:AFTER_COMMIT和AFTER_SYNC。
2.1 AFTER_COMMIT
执行流程如下:
- 主库将事务写入binlog;
- 通知从库写relaylog,同时主库提交事务;
- 主库等待至少N个从库返回已写入relaylog的回复;
- 主库将操作结果返回给客户端。
2.1.1 异常场景
异常描述:
主库开启事务a,写入binlog并提交事务后,客户端开启事务b进行查询,且事务b看见了事务a做的修改,这时主库崩溃了,从库并没有收到主库事务a的二进制日志事件。
恢复影响:
- 切换主库。数据丢失。这里说数据丢失的理由是:事务b已经看见了事务a做的修改;
- 恢复主库。磁盘不损坏时数据不丢失,但相对于主备切换,恢复时间较长;磁盘损坏时,主库无法恢复,数据丢失;
2.2 AFTER_SYNC
为了解决AFTER_COMMIT会造成数据丢失的问题,MySQL5.7版本新增的半同步方式AFTER_SYNC,它也被叫做无损复制(lossless replication),没有同步给备库的事务所做的修改在后续的事务中都不可见。
执行流程如下:
- 主库将事务写入binlog;
- 通知从库写relaylog;
- 主库等待至少N个从库返回已写入relaylog的回复,主库提交事务;
- 主库将操作结果返回给客户端。
2.2.1 异常场景
异常描述:
主库开启事务a,写入binlog并提交事务后,客户端开启事务b进行查询,事务b一定看不见事务a做的修改,这时主库崩溃了,从库并没有收到主库事务a的二进制日志事件。
恢复影响:
- 切换主库。数据不丢失。这里说数据不丢失的理由是:事务b看不见事务a做的修改;
- 恢复主库。磁盘不损坏时数据不丢失,但相对于主备切换,恢复时间较长;磁盘损坏时,主库无法恢复,数据已存储到从库,数据不丢失;
半同步AFTER_SYNC,看起来能够完全解决数据一致性问题,但它的前提条件是:半同步复制不退化成异步复制。MySQL存在一个柔性机制,从库响应时间太长或者不响应会大大降低主库的吞吐量,在从库长时间未响应时复制会退化成异步复制。
本文介绍了MySQL数据一致性的大部分原理,MySQL原生的一致性保障有时还是无法满足生产环境的需求,因此各大公司还会通过修改MySQL复制机制、实现同步插件等方式做到应用场景匹配的一致性需求。
参考文档:
- 《高性能MySQL》
- 《MySQL技术内幕:InnoDB存储引擎》
- 《MySQL DBA修炼之道》
- MySQL 5.7/8.0 Reference Manual
- MySQL5.7 semi-sync replication功能增强,https://blog.51cto.com/linzhijian/1909552
- MySQL背后的数据一致性分析,https://zhuanlan.zhihu.com/p/22290294