MySQL高可用,就这么完美???
MySQL以其容易学习和高可用,被开发人员青睐。它的几乎所有的高可用架构,都直接依赖于 binlog。MySQL 能够成为现下最流行的开源数据库,binlog 功不可没。MySQL是怎样实现高可用的?这种高可用足够完美吗?
主备同步流程
流程
主库为A,备库为B,其同步流程如下图所示,这张图也很好的阐明一条更新语句,在master会执行哪些动作:
备库 B 跟主库 A 之间维持了一个长连接。主库 A 内部有一个线程,专门用于服务备库 B 的这个长连接。一个事务日志同步的完整过程是这样的:
-
在备库 B 上通过 change master 命令,设置主库 A 的 IP、端口、用户名、密码,以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量。
-
在备库 B 上执行 start slave 命令,这时候备库会启动两个线程,就是图中的 io_thread和 sql_thread。其中 io_thread 负责与主库建立连接。
-
主库 A 校验完用户名、密码后,开始按照备库 B 传过来的位置,从本地读取 binlog,发给 B。
-
备库 B 拿到 binlog 后,写到本地文件,称为中转日志(relay log)。
-
sql_thread 读取中转日志,解析出日志里的命令,并执行。
同步位置
主备切换后,从库需要从新的主库同步数据。即上面流程第一步,需要指定从哪个位置开始请求binlog。主要有两种方案:
基于位点
MySQL5.6之前,使用change master命令更换主库。
CHANGE MASTER TO
MASTER_HOST=$host_name
MASTER_PORT=$port
MASTER_USER=$user_name
MASTER_PASSWORD=$password
MASTER_LOG_FILE=$master_log_name
MASTER_LOG_POS=$master_log_pos
操作流程如下:
-
等待新主库 A’把中转日志(relay log)全部同步完成;
-
在 A’上执行 show master status 命令,得到当前 A’上最新的 File 和 Position;
-
取原主库 A 故障的时刻 T;
-
用 mysqlbinlog 工具解析 A’的 File,得到 T 时刻的位点。
基于GTID
基于位点的方案太过繁琐,MySQL 5.6 版本引入了 GTID,无需人工计算位点。
GTID 的全称是 Global Transaction Identifier,也就是全局事务 ID,是一个事务在提交的时候生成的,是这个事务的唯一标识。每个 MySQL 实例都维护了一个 GTID 集合,用来对应“这个实例执行过的所有事务”。
CHANGE MASTER TO
MASTER_HOST=$host_name
MASTER_PORT=$port
MASTER_USER=$user_name
MASTER_PASSWORD=$password
master_auto_position=1
master_auto_position=1 就表示这个主备关系使用的是 GTID 协议。
在实例 B 上执行 start slave 命令,取 binlog 的逻辑如下所示,其中set_a和set_b为执行过的事务的 GTID 集合:
-
实例 B 指定主库 A’,基于主备协议建立连接。
-
实例 B 把 set_b 发给主库 A’。
-
实例 A’算出 set_a 与 set_b 的差集,也就是所有存在于 set_a,但是不存在于 set_b的 GTID 的集合,判断 A’本地是否包含了这个差集需要的所有 binlog 事务。
a. 如果不包含,表示 A’已经把实例 B 需要的 binlog 给删掉了,直接返回错误;
b. 如果确认全部包含,A’从自己的 binlog 文件里面,找出第一个不在 set_b 的事务,发给 B;
-
之后就从这个事务开始,往后读文件,按顺序取 binlog 发给 B 去执行。
基于GTID的操作,可以认为是系统自行计算出对应位点。
循环问题
参数 log_slave_updates 设置为 on,表示备库执行 relay log 后生成 binlog。在主主复制+主从复制情况下,有时会发现主从没有同步,很可能是因为有的主库没有将log_slave_updates设置为on。
既然消费relay log会生成新的binlog,那双master情况下为何没有产生节点间循环复制情况?
主要是因为MySQL 在 binlog 中记录了这个命令第一次执行时所在实例的server id。
-
规定两个库的 server id 必须不同,如果相同,则它们之间不能设定为主备关系;
-
一个备库接到 binlog 并在重放的过程中,生成与原 binlog 的 server id 相同的新的binlog;
-
每个库在收到从自己的主库发过来的日志后,先判断 server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志。
高可用(HA)
现在大家都用MySQL,主要是因其高可用。高可用原因有两个,一个是主备一致,一个是主备切换。这两者缺一不可。
-
正常情况下,只要主库执行更新生成的所有 binlog,都可以传到备库并被正确地执行,备库就能达到跟主库一致的状态,这就是最终一致性。
-
主库出现问题,可以将备库作为主库,继续提供服务。
MySQL 高可用系统的基础,就是主备切换逻辑,但主备切换又很依赖主备延迟。
原因很容易理解,如果备库同步没有完成,此时将备库更改为主库,会产生数据丢失、数据不一致问题。
同步延迟
根据上面提到的主备同步流程,我们能够看出与数据同步有关的时间点主要包括以下三个:
-
主库 A 执行完成一个事务,写入 binlog,我们把这个时刻记为 T1;
-
之后传给备库 B,我们把备库 B 接收完这个 binlog 的时刻记为 T2;
-
备库 B 执行完成这个事务,我们把这个时刻记为 T3。
所谓主备延迟,就是同一个事务,在备库执行完成的时间和主库执行完成的时间之间的差值,也就是 T3-T1。所以主库和备库之间,必然会有延迟。
延迟时间可通过在备库上执行 show slave status 命令查看,它的返回结果里会显示seconds_behind_master,用于表示当前备库延迟了多少秒。DB监控上显示的时间,就是seconds_behind_master。
延迟原因
主备延迟必然会有,但不应该延迟太长。主备延迟长,一般有如下几个原因:
- 备库所在机器的性能要比主库所在的机器性能差
-
备库的机器配置本身就比主库差
-
主库多机器部署,备库单机部署
- 备库的压力大
- 备库上的查询耗费了大量的 CPU 资源
- 主库执行大事务或大表 DDL
- 语句在主库执行多久,便会导致从库延迟多久
- 备库的并行复制能力
-
备库使用单线程复制还是多线程复制
-
从MySQL5.7.22开始,可以通过 binlog-transaction dependency-tracking 参数的 COMMIT_ORDER、WRITESET 和 WRITE_SESSION,选择使用哪种并行复制策略
主备切换
下面是双Master之间主备切换流程。一般说双M是指AB之间设置为互为主备,不过任何时刻只有一个节点在接受更新。
主备切换主要有两种策略,可靠性优先和可用性优先策略。
可靠性优先
从状态 1 到状态 2 切换的详细过程是这样的:
-
判断备库 B 现在的 seconds_behind_master,如果小于某个值(比如 5 秒)继续下一步,否则持续重试;
-
把主库 A 改成只读状态,即把 readonly 设置为 true;
-
判断备库 B 的 seconds_behind_master 的值,直到这个值变成 0 为止;
-
把备库 B 改成可读写状态,也就是把 readonly 设置为 false;
-
把业务请求切到备库 B。
可靠性优先的好处是,主备的数据完全一致后再进行切换,不会引起系统问题。但缺点是系统有段时间不可用。
可用性优先
从状态 1 到状态 2 切换的详细过程是这样的:
-
把备库 B 改成可读写状态,也就是把 readonly 设置为 false;
-
把业务请求切到备库 B;
-
把主库 A 改成只读状态,即把 readonly 设置为 true;
可用性优先的好处是,系统几乎就没有不可用时间,坏处是系统可能出现数据不一致情况。
之所以出现数据不一致,是因为备库B接收业务请求的同时,还会继续消费未完成的binlog日志,新的请求和老的请求之间可能存在冲突。将binlog设置为row能够更加及时的发现这种问题,减少问题的加剧。
异常情况
假设,主库 A 和备库 B 间的主备延迟是 30 分钟,这时候主库 A 掉电了,HA 系统要切换B 作为主库。这时候切和不切都会有问题。
-
切:有些数据在备库无法查到,而且会产生数据不一致问题
-
不切:数据库不可使用
这也是为什么说MySQL 高可用系统的可用性,是依赖于主备延迟的。延迟的时间越小,在主库故障的时候,服务恢复需要的时间就越短,可用性就越高。所以无论是对DBA还是对研发人员而言,需要重点关注同步延迟。
问题
主从同步虽然给MySQL带来了高可用,但因为必然存在延迟问题,所以会导致更新完主库后,立即查从库,此时从库并没有更新后的数据。这个问题无法避免,但可以想办法优化,需要大家在付出和收益间做好权衡。
强制走主库方案
- 查找操作不查从库,查主库
sleep 方案
- 客户端更新成功后,过一小会再做查找操作
判断主备无延迟方案
-
每次从库执行查询请求前,先判断seconds_behind_master 是否已经等于 0。如果还不等于 0 ,那就必须等到这个参数变为0 才能执行查询请求。
-
判断位点:读到的主库的最新位点与备库执行的最新位点做比较,相等即可读
-
判断GTID:备库收到的所有日志的 GTID 集合与备库所有已经执行完成的 GTID 集合是否一致,一致即可读
配合 semi-sync 方案
- 一主一备下,半同步复制能确保主备全都收到了更新
等主库位点方案
- 从库上执行select master_pos_wait(file, pos[, timeout]), 返回值是 >=0 的正整数则查询,其中参数 file 和 pos 指的是主库上的文件名和位置
等 GTID 方案
- 从库上执行select wait_for_executed_gtid_set(gtid_set, 1),如果返回值是 0,则在这个从库执行查询语句,其中gtid_set是主库事务更新完成后,从返回包直接获取到事务的 GTID
总结
其实MySQL主从同步和开发人员相关性不大,但了解其中的不完美,对于发生异常时进行问题追查是很有帮助的。而且,能够扩展出许多新的玩法,如业务消费binlog日志,实现很多功能。
资料
-
主从同步设置的重要参数log_slave_updates
-
MySQL45讲
-
MySQL DDL--ghost工具学习
最后
大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)
我的个人博客为:shidawuhen.github.io/
往期文章回顾:
转载自:https://juejin.cn/post/7017728423346831374