MySQL 高可用

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_FileRead_Master_Log_Pos,表示的是读到的主库的最新位点;
  • Relay_Master_Log_FileExec_Master_Log_Pos,表示的是备库执行的最新位点。

如果 Master_Log_FileRelay_Master_Log_FileRead_Master_Log_PosExec_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);