MySQL 高可用
主备 M/S 结构
update 语句在节点 A 执行,然后同步到节点 B 的完整流程图:
备库 B 跟主库 A 之间维持了一个长连接。主库 A 内部有一个线程,专门用于服务备库 B 的这个长连接。
binlog 格式
statement
:记录 SQL 语句,容易导致主备不一致row
:记录了真实删除行的主键id
,缺点占用空间。现在越来越多场景要求使用row
,便于恢复数据。mixed
: MySQL 自己判断是否会引起主备不一致
如下是可能引起主备不一致的语句示例:
-- 很可能主库和备库走的索引不一样,导致删除的数据不一致
mysql> delete from t where a>=4 and t_modified<='2018-11-10' limit 1;
mysql> insert into t values(10,10, now());
主备双 M 结构
生产上多用的是互为主备的结构:这样在切换的时候就不用再修改主备关系。
双 M 结构有一个问题需要解决。业务逻辑在节点 A 上更新了一条语句,然后再把生成的 binlog 发给节点 B,节点 B 执行完这条更新语句后也会生成 binlog。如果节点 A 同时是节点 B 的备库,相当于又把节点 B 新生成的 binlog 拿过来执行了一次,然后节点 A 和 B 间,会不断地循环执行这个更新语句,也就是循环复制了。
因此,我们可以用下面的逻辑,来解决两个节点间的循环复制的问题:
- 规定两个库的 server id 必须不同,如果相同,则它们之间不能设定为主备关系;
- 一个备库接到 binlog 并在重放的过程中,生成与原 binlog 的 server id 相同的新的 binlog;
- 每个库在收到从自己的主库发过来的日志后,先判断 server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志。
主备延迟
主备延迟,就是同一个事务,在备库执行完成的时间和主库执行完成的时间之间的差值。你可以在备库上执行 show slave status
命令,它的返回结果里面会显示 seconds_behind_master
,用于表示当前备库延迟了多少秒。
主备延迟的来源:
- 有些部署条件下,备库所在机器的性能要比主库所在的机器性能差。
- 备库的压力大。主库直接影响业务,大家使用起来会比较克制,反而忽视了备库的压力控制。结果就是,备库上的查询耗费了大量的 CPU 资源,影响了同步速度,造成主备延迟。这种情况,我们一般可以这么处理:一主多从/binlog 输出到 Hadoop。
- 大事务。主库上必须等事务执行完成才会写入 binlog,再传给备库。
可靠性切换逻辑
- 判断备库 B 现在的
seconds_behind_master
,如果小于某个值(比如 5 秒)继续下一步,否则持续重试这一步; - 把主库 A 改成只读状态,即把
readonly
设置为true
; - 判断备库 B 的
seconds_behind_master
的值,直到这个值变成 0 为止; - 把备库 B 改成可读写状态,也就是把
readonly
设置为false
; - 把业务请求切到备库 B。
这个切换流程中是有不可用时间的。这个切换流程,一般是由专门的 HA 系统来完成的
可用性切换逻辑
如果我强行把步骤 4、5 调整到最开始执行,也就是说不等主备数据同步,直接把连接切到备库 B,并且让备库 B 可以读写,那么系统几乎就没有不可用时间了。
一般现在的数据库运维系统都有备库延迟监控,其实就是在备库上执行
show slave status
,采集seconds_behind_master
的值。
带 Proxy 的读写分离架构
从库过期读
强制走主库方案
必须要拿到最新结果的请求,强制将其发到主库上,这个方案用得是最多的。这个方案最大的问题在于,有时候你会碰到“所有查询都不能是过期读”的需求,比如一些金融类的业务。这样的话,你就要放弃读写分离,所有读写压力都在主库,等同于放弃了扩展性。
sleep
方案
读从库之前先 sleep(1)
一下。这个方案的假设是,大多数情况下主备延迟在 1 秒之内,做一个 sleep
可以有很大概率拿到最新的数据。
判断主备无延迟方案
(1) seconds_behind_master
每次从库执行查询请求前,先判断 seconds_behind_master
是否已经等于 0
。如果还不等于 0
,那就必须等到这个参数变为 0
才能执行查询请求。
(2) 位点
Master_Log_File
和Read_Master_Log_Pos
,表示的是读到的主库的最新位点;Relay_Master_Log_File
和Exec_Master_Log_Pos
,表示的是备库执行的最新位点。
如果 Master_Log_File
和 Relay_Master_Log_File
、Read_Master_Log_Pos
和 Exec_Master_Log_Pos
这两组值完全相同,就表示接收到的日志已经同步完成。
(3) GTID 集合
Auto_Position=1
,表示这对主备关系使用了 GTID 协议。Retrieved_Gtid_Set
,是备库收到的所有日志的 GTID 集合;Executed_Gtid_Set
,是备库所有已经执行完成的 GTID 集合。
如果这两个集合相同,也表示备库接收到的日志都已经同步完成。
在执行查询请求之前,先判断从库是否同步完成的方法,相比于 sleep
方案,准确度确实提升了不少,但还是没有达到“精确”的程度。
因为没有同步延迟,不代表备库收到 binlog
后一定把这个事务执行完成了。
配合 semi-sync
方案
要解决这个问题,就要引入半同步复制,也就是 semi-sync replication。
semi-sync
做了这样的设计:
- 事务提交的时候,主库把
binlog
发给从库; - 从库收到
binlog
以后,发回给主库一个ack
,表示收到了; - 主库收到这个
ack
以后,才能给客户端返回“事务完成”的确认。
也就是说,如果启用了 semi-sync
,就表示所有给客户端发送过确认的事务,都确保了备库已经收到了这个日志。
semi-sync
配合前面关于位点的判断,就能够确定在从库上执行的查询请求,可以避免过期读。
但是,semi-sync + 位点判断的方案,只对一主一备的场景是成立的。在一主多从场景中,主库只要等到一个从库的 ack,就开始给客户端返回确认。这时,在从库上执行查询请求,就有两种情况:
- 如果查询是落在这个响应了 ack 的从库上,是能够确保读到最新数据;
- 但如果是查询落到其他从库上,它们可能还没有收到最新的日志,就会产生过期读的问题。
其实,判断同步位点的方案还有另外一个潜在的问题,即:如果在业务更新的高峰期,主库的位点或者 GTID 集合更新很快,那么上面的两个位点等值判断就会一直不成立,很可能出现从库上迟迟无法响应查询请求的情况。
等主库位点方案
select master_pos_wait(file, pos[, timeout]);
等 GTID
方案
select wait_for_executed_gtid_set(gtid_set, 1);