MySQL 的主备架构
参考:
# 1. MySQL 是怎么保证主备一致的?
MySQL 能够成为现下最流行的开源数据库,binlog 功不可没,binlog 可以用于归档,也可以用来做主备同步。MySQL 在最开始以方便的高可用架构而被青睐,而它几乎所有的高可用架构都直接依赖于 binlog。虽然这些高可用架构已经呈现出越来越复杂的趋势,但都是从最基本的一主一备演化过来的。
这一章将介绍一下主备的基本原理以及设计思想。
# 1.1 MySQL 主备的基本原理
在 MySQL 基本的主备架构中,主节点处理 client 的读写请求,其余节点作为主节点的备库,同步主节点的更新并在本地执行,从而保证主节点与从节点的数据是相同的。
下图展示了主备切换流程:
我们往往将备库设置为 readonly。readonly 对超级权限无效,而同步更新的线程具有超级权限,因此不用担心 readonly 会影响备库对主库的同步。
下面看一下主库 A 到备库 B 这条线的内部流程是怎样的。下图展示了主节点 A 执行 update 语句后同步到备库 B 节点的完整主备流程图:
从上图可以看到,主库接收到 client 的更新请求后,执行内部的更新逻辑,同时写 binlog。然后备库 B 跟主库 A 之间维持了一个长连接,一个事务日志同步的完整过程如下:
- 备库 B 通过 change master 命令,设置主库 A 的 IP、端口、用户名密码,以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量。
- 备库 B 执行 start slave 命令,这是备库会启动两个线程:io thread 和 sql thread,其中 io thread 负责与主库建立连接。
- 主库 A 校验 B 发送过来的用户名和密码后,开始按照备库 B 传来的位置读取 binlog,并发送给 B。
- B 拿到 binlog 后,写到本地文件,称为中转日志(relay log)。
- sql thread 读取中转日志,解析出日志中的命令,并执行。
由于后来多线程复制方案的引入,这里的 sql thread 演化成了多个线程,但原理不变,就不再展开了。
# 1.2 binlog 的三种格式对比
binlog 有三种格式:statement、row、mixed。其中 mixed 是前两种的混合。
# 1.2.1 三种不同格式
- statement 格式:该格式下,binlog 中记录的是 SQL 语句原文。这在主从同步时可能会出现问题,同一个 SQL 在主库与从库的执行结果可能不同,从而导致主从不一致,比如 DELETE 语句的执行可能会因为主从库所选择的索引不同导致删除的内容不同。
- row 格式:该格式下,binlog 中记录的是 events,即在哪个表里改动了哪些数据。这样,binlog 里就记录了主库删除的真实行的主键 ID,当 binlog 传到备库时,执行结果相同,不会导致主备不一致。
- mix 格式:这种格式是 statement 格式和 row 格式的混合。statement 格式可能会导致主备不一致,而 row 格式会记录所有数据(比如删除了 10w 行数据)导致空间占用大,于是 MySQL 取了一个折中方案:mixed 格式。MySQL 会自己判断这条 SQL 语句是否可能引起主备不一致,如果有可能,就用 row 格式,否则就用 statement 格式。
# 1.2.2 线上 MySQL 使用的格式
所以说,mixed 格式利用了 statement 格式的优点,又避免了数据不一致的风险。因此你的线上 MySQL 的 binlog 格式不应该是 statement 格式,至少应该设置为 mixed 格式。
但现在越来越多的场景要求把 MySQL binlog 格式设置为 row 格式。原因有很多,其中一个是可以恢复数据,比如当你执行 DELETE 语句后,row 格式的 binlog 日志记录了删除前后的数据,这样就可以精准恢复了。
# 1.2.3 SQL 中的 now()
的问题
假如我们使用 mixed 格式的 binlog,那对于下面带有 now()
的 SQL 语句,binlog 会怎样记录呢:
INSERT INTO t VALUES(10,10, now())
猜一下 MySQL 会将其记录为 statement 格式还是 row 格式呢?
下面是查看的执行效果:
可以看到,MySQL 居然将其记录为了 statement 格式。那就有了一个问题:主从同步的延迟会导致 now()
在主从库的执行结果不一样吗?
如果只是这一个 SQL 的话,那肯定会不一样,但如果我们用 mysqlbinlog 工具来看一下 binlog 中记录的 SQL 的话:
可以看到 binlog 多记录了一个命令:SET TIMESTAMP=1546103491;
。它用 SET TIMESTAMP 命令约定了接下来的 now() 函数的返回时间。所以无论这个 binlog 是多长时间之后被备库执行,now()
的结果是固定的,这样就确保了主备数据的一致性。
由此可以看出,很多 SQL 语句的执行结果是依赖上下文命令的。所以当我们需要使用 binlog 恢复数据时,不能只是简单地将 mysqlbinlog 解析出来的日志中的 statement 语句拷贝出来并执行,因为这会因为上下文的确实导致结果出现错误。
使用 binlog 来恢复数据的标准做法是:用 mysqlbinlog 工具解析出来,然后把解析结果整个发给 MySQL 执行。类似命令如下:
mysqlbinlog master.000001 --start-position=2738 --stop-position=2973 | mysql -h127.0.0.1 -P13000 -u$user -p$pwd;
这个命令的意思是,将 master.000001 文件里面从第 2738 字节到第 2973 字节中间这段内容解析出来,放到 MySQL 去执行。
# 1.3 循环复制问题
上面讲解让我知道了通过让备库执行主库相同的 binlog 从而得到与主库相同的状态,因此我们可以认为正常情况下主备的数据是一致的。
前面我们介绍的是 M-S 结构(Master-Slave 结构),但在实际生产中使用比较多的是双 M 结构(双 Master)。下图展示了双 M 结构的主备切换流程:
可以看到,双 M 结构与 M-S 结构的区别只是多了一条线,即 A 与 B 之间总是互为主备关系,这样在切换时就不用再修改主备关系了。
但,双 M 结构存在循环复制问题:A 执行更新后把生成的 binlog 发送给 B,B 执行完同步的更新后又会生成 binlog,这时 B 又想把这个 binlog 发给 A。这样 A 与 B 之间就会不断循环,也就是循环复制了。
解决方式就是:让每个 MySQL 实例有一个独一无二的 server id,并在第一次执行更新时在 binlog 中记录下这个 server id,备库的重放会生成与原 binlog 的 server id 相同的 binlog,而每个库在重放 binlog 会过滤掉 server id 是自己的 binlog。
这样就解决了循环复制的问题。
# 1.4 小结
这一大节介绍了 MySQL binlog 的格式以及使用 binlog 实现 MySQL 主备架构的机制。这些机制在 MySQL 各种高可用方案上扮演了重要角色,是多节点、MySQL group replication、读写分离等方案的基础。
另外,MySQL 在实现主备架构所采用的方案,也是我们在系统开发时可以借鉴的思想。
# 2. MySQL 是怎么保证高可用的?
正常情况下,主库执行更新产生的 binlog 会被同步到备库中并被正确执行,从而让备库达到与主库一致的状态,这就是最终一致性。但 MySQL 要提供高可用能力的话,只有最终一致性是不够的,因为主备延迟期间的崩溃故障可能会产生问题。
# 2.1 主备延迟
主备切换可能是一个:
- 主动运维动作,比如软件升级、主库所在机器按计划下线等;
- 被动操作,比如主库所在机器掉电。
下面主要先主要看一下主动切换的场景。
主备延迟:指的是同一个事务,在备库完成的时间和在主库完成的时间之间的差值。
你可以在备库上执行 show slave status 命令,查看里面的 seconds_behind_master:表示当前备库延迟了多少秒。
即使主备及其的系统时间设置不一样,也不会导致这里的 seconds_behind_master 计算出现问题,因为系统时间的差值会在主从连接时自动计算出来并做校正。详细来说就是,备库连接到主库的时候,会通过执行
SELECT UNIX_TIMESTAMP()
函数来获得当前主库的系统时间。如果这时候发现主库的系统时间与自己不一致,备库在执行 seconds_behind_master 计算的时候会自动扣掉这个差值。
需要说明的是,在网络正常的时候,日志从主库传给备库所需的时间是很短的。所以在网络正常情况下,主备延迟的关键来源是备库接受完 binlog 和执行完这个事务之间的时间差。
所以,主备延迟最直接的表现是,备库消费中转日志(relay log)的速度,比主库生产 binlog 的速度要慢。接下来,我就和你一起分析下,这可能是由哪些原因导致的。
# 2.2 主备延迟的来源
主备延迟产生的主要原因有如下:
- 备库的机器性能比主库的机器差。很多人认为备库没有接受请求就将其放在了性能较差的机器上,但其实备份过程也存在大量读写操作。现在主备库往往是对称部署,采用的机器规格是一样的。
- 备库的压力大。一般想法是,主库既然提供了写能力,那备库可以提供一些读能力,或者运行一些 AP 操作。但这些行为忽视了备库的压力控制,占用过多 CPU 资源,导致影响了同步速度,进而造成主备延迟。解决办法就是:一主多从分担读压力、将数据外接到 OLAP 数据库来做 AP 操作。
- 大事务。大事务这种情况很好理解。因为主库上必须等事务执行完成才会写入 binlog,再传给备库。所以,如果一个主库上的语句执行 10 分钟,那这个事务很可能就会导致从库延迟 10 分钟。所以这就是为什么不能一次性地用 DELETE 语句删除太多数据,而是要分多次小批量删除。另外也要对大表做 DDL 操作也是典型的大事务场景。
- 备库的并行复制能力。这个后面再讲。
由于主备延迟的存在,所以在主备切换的时候,就相应的有不同的策略。
# 2.3 主备切换的策略 —— 可靠性优先策略
采用可靠性优先策略的话,主从切换的流程如下:
- 判断备库 B 现在的 seconds_behind_master,如果小于某个值(比如 5 秒)继续下一步,否则持续重试这一步;
- 把主库 A 改成只读状态,即把 readonly 设置为 true;
- 判断备库 B 的 seconds_behind_master 的值,直到这个值变成 0 为止;
- 把备库 B 改成可读写状态,也就是把 readonly 设置为 false;
- 把业务请求切到备库 B。
这个切换流程,一般是由专门的 HA 系统来完成的,我们暂时称之为可靠性优先流程。如下图所示(注:SBM 是 seconds_behind_maste 的缩写):
可以看到,这个切换流程存在数据库不可用的时间,在这个不可用时间中,比较耗费时间的是步骤 3,所以我们需要在步骤 1 时先做判断,确保 seconds_behind_master 的值足够小。试想如果一开始主备延迟就长达 30 分钟,而不先做判断直接切换的话,系统的不可用时间就会长达 30 分钟,这种情况一般业务都是不可接受的。
# 2.4 主备切换的策略 —— 可用性优先策略
如果不允许系统存在不可用时间,那可以采用可用性优先策略:强行把步骤 4、5 调整到最开始执行,也就是说不等主备数据同步,直接把连接切到备库 B,并且让备库 B 可以读写,那么系统几乎就没有不可用时间了。
这个主备切换的代价是:可能出现数据不一致的情况。如果 binlog 使用 statement 或 mixed 格式的话,这种不一致可能会悄悄过去导致很久之后才能被发现,而是用 row 格式的话,这种不一致也许会更容易被发现。
正是因为主备切换的可用性优先策略会导致数据不一致。因此,大多数情况下,我都建议你使用可靠性优先策略。毕竟对数据服务来说的话,数据的可靠性一般还是要优于可用性的。
也有一些情况采用可用性优先的策略,先强行切换,再事后补数据,或者引入其他的补救措施。
# 2.5 小结
MySQL 高可用的基础就是主备切换逻辑,其可用性依赖于主备延迟,延迟的时间越小,在主库故障的时候,服务恢复需要的时间就越短,可用性就越高。
由于主备延迟的存在,导致切换策略有了不同的选择,在一般情况下,更建议使用可靠性优先的策略。
# 3. MySQL 备库的多线程复制策略
之前介绍的主备延迟一般是分钟级的,而且在备库恢复正常以后一会也就追上了。但对于一个请求压力持续较高的主库来说,主库会持续产生 binlog,而备库就有可能持续落后,最终很可能就永远追不上主库了。
为了解决这个问题,MySQL 5.6 增加了备库并行复制的能力。
# 3.1 问题产生的原因
我们关注主备同步之间的流程的两个步骤:
- 客户端请求主库
- 备库上 sql thread 执行中转日志写入数据
也就是下图中两个黑粗线标识的箭头:
而现实上,这两个流程存在并行度的差别:
- 在主库上,影响并发度的主要是 locks,而其实除非某些极端场景下,它对业务并发度的支持是很友好的,所以总体吞吐是高于备库上的执行的。
- 在备库上,如果 sql thread 更新数据的逻辑是单线程的话,在主库并发高时会导致备库应用日志地不够快,从而造成主备延迟。
由此可以看出,本应同步的两个流程很容易出现速率的不匹配进而造成主备延迟。为了解决这个问题,MySQL 推出了多线程复制的方案。
# 3.2 备库的多线程复制方案
多线程复制方案的思路如下图:
它将原来的 sql thread 功能拆分成了一个 coordinator 线程和多个 worker 线程:
- coordinator 线程只负责读取中转日志和分发事务
- worker 线程负责更新日志。worker 线程的个数由参数
slave_parallel_workers
决定。
对于 32 核的机器,常将 worker 线程的数量设置为 8 ~ 16,毕竟备库还要提供读查询的能力
那 coordinator 如何分发执行任务给 worker 呢?
- 可以轮询将各个事务发给各个 worker 吗?由于 CPU 的调度策略不可控制,导致不同 worker 间执行事务的顺序与分发的顺序不同,进而导致主备不一致。
- 可以将一个事务的多个语句发给多个 worker 吗?一看就不行。
所以,coordinator 在分发的时候,需要满足以下这两个基本要求:
- 不能造成更新覆盖。这就要求更新同一行的两个事务,必须被分发到同一个 worker 中。
- 同一个事务不能被拆开,必须放到同一个 worker 中。
各个版本的多线程复制,都遵循了这两条基本原则。
# 3.3 两个简单的并行策略
下面介绍两个在 MySQL 还不支持多线程并行复制时,人们自行采用并实现的并行复制策略:
- 按表分发策略:基本思路是,如果两个事务更新不同的表,它们就可以并行。这可以保证两个 worker 不会更新同一行,但如果有跨表的事务,还是要把两张表放在一起考虑。
- 按行分发策略:如果两个事务没有更新相同的行,它们在备库上可以并行执行。显然,这个模式要求 binlog 格式必须是 row。
更详细的实现细节以及 MySQL 所采用的方案,可以参考原文 (opens new window)。