MySQL通过binlog实现主备同步,其中实现的主要原理是binlog。在主备同步中从库B会设置为readonly。在状态 1 中,客户端的读写都直接访问节点 A,而节点 B 是 A 的备库,只是将 A 的更新都同步过来,到本地执行。这样可以保持节点 B 和 A 的数据是相同的。当需要切换的时候,就切成状态 2。这时候客户端读写访问的都是节点 B,而节点 A 是 B 的备库。
备库 B 跟主库 A 之间维持了一个长连接。主库 A 内部有一个线程,专门用于服务备库 B 的这个长连接。一个事务日志同步的完整过程是这样的:
binlog_format=statement 时,binlog 里面记录的就是 SQL 语句的原文。(可能造成主备不一致,原因在于如果删除语句加了limit,那么主库备库可能使用的索引不一样,导致主备不一致)。
binlog_format=‘row’,借助 mysqlbinlog 工具,binlog 里面记录了真实删除行的主键 id(但是占空间较大)。
binlog_format=‘mixed’:statement格式会造成主备不一致,row格式又比较占空间,设置为 mixed 后,就会记录为 row 格式;而如果执行的语句去掉 limit 1,就会记录为 statement 格式。
目前最优row,其原因在于:
文章中第一张图描述的是M-S结构下的主备同步,即一个Master一个Slave,而双M结构下互为主备关系:
问题背景:业务逻辑在节点 A 上更新了一条语句,然后再把生成的 binlog 发给节点 B,节点 B 执行完这条更新语句后也会生成 binlog。那么,如果节点 A 同时是节点 B 的备库,相当于又把节点 B 新生成的 binlog 拿过来执行了一次,然后节点 A 和 B 间,会不断地循环执行这个更新语句,也就是循环复制了。
解决办法(判断serverid,相同则丢弃):
与主备同步有关的时间点主要包括以下三个:
主备延迟,就是同一个事务,在备库执行完成的时间和主库执行完成的时间之间的差值,也就是 T3-T1。seconds_behind_master(这个参数很关键,通常用作判断备库与主库的时间差),用于表示当前备库延迟了多少秒。
延迟原因分析:
解决方法可靠性优先:
这个切换流程中是有不可用时间的。因为在步骤 2 之后,主库 A 和备库 B 都处于 readonly 状态,也就是说这时系统处于不可写状态,直到步骤 5 完成后才能恢复。
解决方法可用性优先:
如果我强行把步骤 4、5 调整到最开始执行,也就是说不等主备数据同步,直接把连接切到备库 B,并且让备库 B 可以读写,那么系统几乎就没有不可用时间了。我们把这个切换流程,暂时称作可用性优先流程。这个切换流程的代价,就是可能出现数据不一致的情况。
总结使用 row 格式的 binlog 时,数据不一致的问题更容易被发现。而使用 mixed 或者 statement 格式的 binlog 时,数据很可能悄悄地就不一致了。如果你过了很久才发现数据不一致的问题,很可能这时的数据不一致已经不可查,或者连带造成了更多的数据逻辑不一致。主备切换的可用性优先策略会导致数据不一致。因此,大多数情况下,我都建议你使用可靠性优先策略。毕竟对数据服务来说的话,数据的可靠性一般还是要优于可用性的。
所谓并行复制能力,及sql_thread的写能力。在官方的 5.6 版本之前,MySQL 只支持单线程复制,由此在主库并发高、TPS 高时就会出现严重的主备延迟问题。所有的多线程复制机制,都是要把图中只有一个线程的 sql_thread,拆成多个线程,也就是都符合下面的这个模型:
coordinator 就是原来的 sql_thread, 不过现在它不再直接更新数据了,只负责读取中转日志和分发事务。真正更新日志的,变成了 worker 线程。而 work 线程的个数,就是由参数 slave_parallel_workers 决定的。
coordinator 在分发基本要求:
按表分发策略:按表分发事务的基本思路是,如果两个事务更新不同的表,它们就可以并行。因为数据是存储在表里的,所以按表分发,可以保证两个 worker 不会更新同一行。
每个 worker 线程对应一个 hash 表,用于保存当前正在这个 worker 的“执行队列”里的事务所涉及的表。hash 表的 key 是“库名. 表名”,value 是一个数字,表示队列中有多少个事务修改这个表。在有事务分配给 worker 时,事务里面涉及的表会被加到对应的 hash 表中。worker 执行完成后,这个表会被从 hash 表中去掉。
每个事务在分发的时候,跟所有 worker 的冲突关系包括以下三种情况:
缺点:如果碰到热点表,比如所有的更新事务都会涉及到某一个表的时候,所有事务都会被分配到同一个 worker 中,就变成单线程复制。
官方 MySQL5.6 版本,支持了并行复制,只是支持的粒度是按库并行。用于决定分发策略的 hash 表里,key 就是数据库名。
优点:构造 hash 值的时候很快,只需要库名;而且一个实例上 DB 数也不会很多,不会出现需要构造 100 万个项这种情况。不要求 binlog 的格式。因为 statement 格式的 binlog 也可以很容易拿到库名。
缺点:主库上的表都放在同一个 DB 里面,这个策略就没有效果
mariaDB的并行复制策略: (1)能够在同一个组里提交的事务,一定不会修改同一行 (2)主库上可以执行的事务,备库上也一定可以并行执行,即redo log组提交。
做法:
缺点:这个方案很容易被大事务拖后腿。假设 trx2 是一个超大事务,那么在备库应用的时候,trx1 和 trx3 执行完成后,就只能等 trx2 完全执行完成,下一组才能开始执行。这段时间,只有一个 worker 线程在工作,是对资源的浪费。
参数 slave-parallel-type 来控制并行复制策略:
MariaDB 这个策略的核心,是“所有处于 commit”状态的事务可以并行。事务处于 commit 状态,表示已经通过了锁冲突的检验了。因此,MySQL 5.7 并行复制策略的优化思想:
新增了一个参数 binlog-transaction-dependency-tracking,用来控制是否启用这个新策略。
hash 值是通过“库名 + 表名 + 索引名 + 值”计算出来的。如果一个表上除了有主键索引外,还有其他唯一索引,那么对于每个唯一索引,insert 语句对应的 writeset 就要多增加一个 hash 值。
优点:
缺点:表是否有主键,也是影响主备同步延迟原因之一。对于“表上没主键”和“外键约束”的场景,WRITESET 策略也是没法并行的,也会暂时退化为单线程模型。因为hash值计算需要到,没主键和外键,同一个库表的行hash值一样了。
大多数的互联网应用场景都是读多写少,因此需要一主多从的架构。在一主多从架构下,主库故障后的主备切换问题:比于一主一备的切换流程,一主多从结构在切换完成后,A’会成为新的主库,从库 B、C、D 也要改接到 A’。正是由于多了从库 B、C、D 重新指向的这个过程,所以主备切换的复杂性也相应增加了。
图中,虚线箭头表示的是主备关系,也就是 A 和 A’互为主备, 从库 B、C、D 指向的是主库 A。一主多从的设置,一般用于读写分离,主库负责所有的写入和一部分读,其他的读请求则由从库分担。
把节点 B 设置成节点 A’的从库的时候,需要执行一条 change master 命令:
在切换的时候会遇到问题:
传统解决措施:
缺点:通过 sql_slave_skip_counter 跳过事务和通过 slave_skip_errors 忽略错误的方法,虽然都最终可以建立从库 B 和新主库 A’的主备关系,但这两种操作都很复杂,而且容易出错。所以,MySQL 5.6 版本引入了 GTID,彻底解决了这个困难。
GTID 的全称是 Global Transaction Identifier,也就是全局事务 ID,是一个事务在提交的时候生成的,是这个事务的唯一标识。它由两部分组成,格式是:GTID=server_uuid:gno
每个 MySQL 实例都维护了一个 GTID 集合,用来对应“这个实例执行过的所有事务”。在基于 GTID 的主备关系里,系统认为只要建立主备关系,就必须保证主库发给备库的日志是完整的。因此,如果实例 B 需要的日志已经不存在,A’就拒绝把日志发给 B。这跟基于位点的主备协议不同。基于位点的协议,是由备库决定的,备库指定哪个位点,主库就发哪个位点,不做日志的完整性判断。主备切换不是不需要找位点了,而是找位点这个工作,在实例 A’内部就已经自动完成了。但由于这个工作是自动的,所以对 HA 系统的开发人员来说,非常友好。
**切换流程:**我们把现在这个时刻,实例 A’的 GTID 集合记为 set_a,实例 B 的 GTID 集合记为 set_b。接下来,我们就看看现在的主备切换逻辑。我们在实例 B 上执行 start slave 命令,取 binlog 的逻辑是这样的
目前主流的读写分离架构,客户端直连和带 proxy 的读写分离架构:
客户端直连方案,因为少了一层 proxy 转发,所以查询性能稍微好一点儿,并且整体架构简单,排查问题更方便。但是这种方案,由于要了解后端部署细节,所以在出现主备切换、库迁移等操作的时候,客户端都会感知到,并且需要调整数据库连接信息。你可能会觉得这样客户端也太麻烦了,信息大量冗余,架构很丑。其实也未必,一般采用这样的架构,一定会伴随一个负责管理后端的组件,比如 Zookeeper,尽量让业务端只专注于业务逻辑开发。
带 proxy 的架构,对客户端比较友好。客户端不需要关注后端细节,连接维护、后端信息维护等工作,都是由 proxy 完成的。但这样的话,对后端维护团队的要求会更高。而且,proxy 也需要有高可用架构。因此,带 proxy 架构的整体就相对比较复杂。
过期读问题:由于主从可能存在延迟,客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,就有可能读到刚刚的事务更新之前的状态,其解决方法如下:
强制走主库方案:强制走主库方案其实就是,将查询请求做分类。通常情况下,我们可以将查询请求分为这么两类:
Sleep 方案:主库更新后,读从库之前先 sleep 一下。具体的方案就是,类似于执行一条 select sleep(1) 命令(并不靠谱)。
判断主备无延迟方案:要确保备库无延迟,通常有三种做法。show slave status 结果里的 seconds_behind_master 参数的值,可以用来衡量主备延迟时间的长短。
配合 semi-sync方案(半同步复制),semi-sync 做了这样的设计:
但是,semi-sync+ 位点判断的方案,只对一主一备的场景是成立的。在一主多从场景中,主库只要等到一个从库的 ack,就开始给客户端返回确认。这时,在从库上执行查询请求,就有两种情况:如果查询是落在这个响应了 ack 的从库上,是能够确保读到最新数据;但如果是查询落到其他从库上,它们可能还没有收到最新的日志,就会产生过期读的问题。
除此之外还有等待主库位点方案以及GTID方案,感觉有点复杂,就不展开详细说了。如果有人想了解,可以私信我或者观看丁奇老师的课程。其思想大概是通过位点或者GTID判断从库此时是否能读数据,否则从主库上读。自己本身并不是专业的DBA,只是从老师的课程中提取了对自己有用的信息,如果大家想学习更多关于MySQL的可以点击。