高可用架构
主备一致
基本原理
M-S架构:客户端的读写都直接访问A库,直到切换时把客户端读写切换给B库,A变成备库
备库设置为readonly状态:防止切换过程出现双写,可以用readonly状态判断节点的角色
基本原理:主库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 读取中转日志,解析出日志里的命令,并执行
双M架构:库A和库B总是互为主备关系,在切换时就不需要再修改主备关系
基本原理:
-
判断备库 B 现在的 seconds_behind_master,即主备延迟时间,如果小于某个值(比如 5 秒)继续下一步,否则持续重试这一步;
之所以到等到该值小于某个值后再往下执行,是为了尽量缩短第三步中A和B库都不可用的时间
-
把主库 A 改成只读状态,即把 readonly 设置为 true;
-
判断备库 B 的 seconds_behind_master 的值,直到这个值变成 0 为止;
在这个等待该值变0的过程,就是等待备库进行数据同步的过程,这个阶段A和B库都是readonly阶段,都不能接受写操作
-
把备库 B 改成可读写状态,也就是把 readonly 设置为 false;
-
把业务请求切到备库 B。
可用性优先策略:不等待主备数据同步,直接把连接切换到备库B并让B库可以读写,在statement和mixed格式下的binlog日志可能会导致数据不一致问题,在row格式会报错,尽量使用可靠性优先策略
双M架构可能会导致循环复制问题:当节点A把binlog发给B,B在执行A-binlog时也会记录下这些操作为B-binlog,而A又同时是B的备库,所以它又会执行一遍B-binlog,即重复执行A-binlog
解决方法:一个备库接收到binlog并在重放过程中,生成与原binlog的server id相同的binlog,而每个库收到主库的binlog会抛弃其中server id等于自己的部分
这样上文的B-binlog里是A执行过的操作会将server id记为A,A再收到B-binlog时就能抛弃掉这重复的部分
一主多从架构:一般用于读写分离,主库负责所有的写入和一部分读请求,其他读请求由从库承担
在该架构下的主库切换:当主库A宕机时,A‘会成为新主库,同时B、C、D库也需要改接到新主库A’
基于位点的主备切换:当A‘成为新主库时,其他节点要设置为A’的从库需要知道主库对应的文件名和日志偏移量(即同步点位)
取同步点位的方法是等待A’将relay log全部同步完成,执行show master status
得到当前A‘上最新的点位,再取原主库A故障的时刻T,用工具解析A’的文件得到T时刻的点位
但这个点位是不准确的,在从库B上执行时可能会出现主键冲突等错误,有两种常用的方法跳过错误:
- 主动跳过一个事务,
set global sql_slave_counter=1;
- 通过设置slave_skip_errors参数,直接设置跳过指定的错误:在主备切换时常出现的错误有1062,插入数据时唯一键冲突和1032删除数据时找不到行
GTID
主备延迟
主备延迟:同一个事务,在备库执行完成的时间和主库执行完成的时间之间的差值,在网络正常的情况下主要表现在备库消费relay log的速度比主库生产binlog的速度要慢,导致主备延迟的主要来源:
- 在早期的部署中,备库所在机器的性能比主库所在的机器性能差
- 做对称部署
- 备库提供一些读能力,在备库上的查询耗费大量CPU资源,影响同步速度
- 做一主多从
- 执行时间很长的大事务(因为要执行完事务再写入binlog)如一次性用delete删除大量数据、大表DDL
- MySQL5.6之前,sql_thread单线程更新数据,导致备库应用日志不够快
- 做多线程的并行复制
在双M架构下,如果备库B的 seconds_behind_master还很大时,主库A断电下线,此时B库还未完全重放relay log,因此无法切换A的连接到B库上,系统会处于完全不可用的情况
所以MySQL 高可用系统的可用性,是依赖于主备延迟的。延迟的时间越小,在主库故障的时候,服务恢复需要的时间就越短,可用性就越高
并行复制
在多线程模式下,sql_thread 充当 coordinator,负责读取relay log并分发事务给worker具体执行,它在分发时要满足以下两个基本要求:
- 不能造成覆盖更新,更新同一行的两个事务必须分发到同一个worker中(可能会导致更新同一行的两个事务在主库和备库上的执行顺序不相同)
- 同一个事务不能拆开必须分发到同一个worker中(最终是一致的,但在过程中客户端可能会访问到中间结果)
读写分离
主要目标是为了分摊主库的压力,有两种架构:客户端直连主动做负载均衡(即前文的一主多从架构) 和 在MySQL和客户端之间有一个proxy中间代理层,客户端只连接proxy,由proxy根据请求类型和上下文决定请求的分发路由
客户端直连:
- 减少proxy层转发,查询性能稍好
- 通常使用zookeeper负责管理后端组件的信息
- 但客户端需要调整数据库的连接信息等
proxy层转发
- 对客户端友好,客户端不需要关注后端细节
- 由proxy维护连接以及后端信息
- 但对proxy有高可用的要求,相对复杂
由于主从延迟无法100%避免,那在读写分离下就有可能会发生过期读,即在从库上读到一个系统的过期现象,共有五种解决方案:
强制走主库
将查询请求做分类,对于需要拿到最新结果的请求,强制分发到主库上;对于可以读到旧数据的请求,才分发到从库上
问题在于遇到“所有查询都不能是过期读”的情况,就必须要放弃读写分离,由主库承担所有
Sleep方案
假设大多数情况下,主备延迟在1s之内,所以在主库更新后,读从库前先select sleep(1)
,让从库sleep一下,这样就有很大概率拿到最新的数据
但并不精确:如果主备延迟只有0.5s,还是要等1s;如果主备延迟超过1s,依旧会发生过期读
判断主备无延迟方案
-
在从库执行查询请求前,先使用
show slave status
查看seconds_behind_master的值,如果不等于0则等待该值等于0后执行查询请求 -
Master_Log_File 和 Read_Master_Log_Pos,表示的是读到的主库的最新位点;Relay_Master_Log_File 和 Exec_Master_Log_Pos,表示的是备库执行的最新位点
如果这两组值完全相同,说明接收到的日志已经同步完成
这种方案虽然比sleep方案更加精确,但仍不严谨,因为可能有一部分binlog日志属于主库执行结束,但还未发送给备库的状态,而备库在未收到这部分日志前可能会认为自己已经执行完relay log处于无主备延迟的阶段(实际上并没有)
配合semi-sync replication:引入半同步复制
- 在事务提交时,主库把binlog发给从库
- 从库收到binlog以后,回复给主库一个ack,表示收到
- 主库收到这个ack之后,才能给客户端返回事务完成的确认
在这种模式下,就表示所有给客户端发送过确认的事务,都确保了备库已经收到了这个日志
但semi-sync + 位点判断 的方案只针对一主一备的场景是严格成立的,在一主多从下主库只要收到一个从库的ack就会返回确认,但其他从库并不一定都收到了这个binlog;且如果在业务高峰期位点持续变化,可能会导致从库发生过度等待的问题(比如我查询的是trx1的结果,但从库因为还没执行完trx2,就不能回复trx1的结果)
等主库位点方案
使用select master_pos_wait(file, pos, timeout);
会等待timeout秒,然后返回从命令开始执行到应用完file和pos表示的binlog位置时,执行了多少事务
- 如果等待事件结束,还没有执行到file和pos表示的位点时,返回-1
- 如果刚开始执行时,发现已经执行过这个位点了,返回0
利用这个命令,可以使用等待主库位点的方案解决半同步复制方案下过度等待的问题:
- trx1 事务更新完成后,马上执行 show master status 得到当前主库执行到的 File 和 Position;
- 选定一个从库执行查询语句;
- 在从库上执行 select master_pos_wait(File, Position, 1);
- 如果返回值是 >=0 的正整数,则在这个从库执行查询语句;
- 否则,到主库执行查询语句