关于数据库:浅谈Mysql读写分离的坑以及应对的方案-京东云技术团队

3次阅读

共计 5286 个字符,预计需要花费 14 分钟才能阅读完成。

一、主从架构

为什么咱们要进行读写拆散?集体感觉还是业务倒退到肯定的规模,驱动技术架构的改革,读写拆散能够加重单台服务器的压力,将读申请和写申请分流到不同的服务器,摊派单台服务的负载,进步可用性,进步读申请的性能。

下面这个图是一个根底的 Mysql 的主从架构,1 主 1 备 3 从。这种架构是客户端被动做的负载平衡,数据库的连贯信息个别是放到客户端的连贯层,也就是说由客户端来抉择数据库进行读写

上图是一个带 proxy 的主从架构,客户端只和 proxy 进行连贯,由 proxy 依据申请类型和上下文决定申请的散发路由。

两种架构计划各有什么特点:

1. 客户端直连架构,因为少了一层 proxy 转发,所以查问性能会比拟好点儿,架构简略,遇到问题好排查。然而这种架构,因为要理解后端部署细节,呈现主备切换,库迁徙的时候客户端都会感知到,并且须要调整库连贯信息

2. 带 proxy 的架构,对客户端比拟敌对,客户端不须要理解后端部署细节,连贯保护,后端信息保护都由 proxy 来实现。这样的架构对后端运维团队要求比拟高,而且 proxy 自身也要求高可用,所以整体架构相对来说比较复杂

然而不管应用哪种架构,因为主从之间存在提早,当一个事务更新实现后马上发动读申请,如果抉择读从库的话,很有可能读到这个事务更新之前的状态,咱们把这种读申请叫做过期读。呈现主从提早的状况有多种,有趣味的同学能够本人理解一下,尽管呈现主从提早咱们同样也有应答策略,然而不能 100% 防止,这些不是咱们本次探讨的范畴,咱们次要讨论一下如果呈现主从提早,刚好咱们的读走的都是从库,咱们应该怎么应答?

首先我把应答的策略总结一下:

  • 强制走主库
  • sleep 计划
  • 判断主从无提早
  • 等主库位点
  • 等 GTID 计划

接下来基于上述的几种计划,咱们一一讨论一下怎么实现和有什么问题。

二、主从同步

在开始介绍主从提早解决方案前先简略的回顾一下主从的同步

上图示意了一个 update 语句从节点 A 同步到节点 B 的残缺过程

备库 B 和主库 A 保护了一个长连贯,主库 A 外部有一个线程,专门用来服务备库 B 的连贯。一个事务日志同步的残缺流程是:

1. 在备库 B 上通过 change master 命令,设置主库 A 的 IP、端口、用户名、明码,以及要从哪个地位开始申请 binlog,这个地位蕴含文件名和日志偏移量。

2. 在备库 B 上执行 start slave 命令,这时候备库会启动两个线程,就是图中的 io\_thread 和 sql\_thread。

3. 其中 io_thread 负责与主库建设连贯。

4. 主库 A 校验完用户名、明码后,开始依照备库 B 传过来的地位,从本地读取 binlog,发给 B。备库 B 拿到 binlog 后,写到本地文件,称为直达日志(relay log)。

5.sql_thread 读取直达日志,解析出日志里的命令,并执行。

上图中红色箭头,如果用色彩深浅示意并发度的话,色彩越深并发度越高,所以主从延迟时间的长短取决于备库同步线程执行直达日志 (图中的 relay log) 的快慢。总结一下可能呈现主从提早的起因:

1. 主库并发高,TPS 大,备库压力大执行日志慢

2. 大事务,一个事务在主库执行 5s,那么同样的到备库也得执行 5s,比方一次性删除大量的数据,大表 DDL 等都是大事务

3. 从库的并行复制能力,Msyql5.6 之前的版本是不反对并行复制的也就是上图的模型。并行复制也比较复杂,就不在这儿赘述了,大家能够自行温习理解一下。

三、主从提早解决方案

1. 强制走主库

这种计划就是要对咱们的申请进行分类,通常能够将申请分成两类:

1. 对于必须要拿到最新后果的申请,能够强制走主库

2. 对于能够读到旧数据的申请,能够调配到从库

这种计划是最简略的计划,然而这种计划有一个毛病就是,对于所有的申请都不能是过期读的申请,那么所有的压力就又来到了主库,就得放弃读写拆散,放弃扩展性

2.sleep 计划

sleep 计划就是每次查问从库之前都先执行一下:select sleep(1),相似这样的命令,这种形式有两个问题:

1. 如果主从提早大于 1s,那么仍然读到的是过期状态

2. 如果这个申请可能 0.5s 就能在从库拿到后果,依然要等 1s

这种计划看起来非常的不靠谱,不业余,然而这种计划的确也有应用的场景。

之前在做我的项目的时候,有这样么一种场景,就是咱们先写主库,写完后,发送一个 MQ 音讯,而后生产方接到音讯后,调用咱们的查问接口查数据,当然咱们也是读写拆散的模式,就呈现了查不到数据的状况,这个时候倡议生产方对音讯进行一个提早生产,比方提早 30ms,而后问题就解决了,这种形式相似 sleep 计划,只不过把 sleep 放到了调用方

3. 判断主从无提早计划

  1. 命令判断

show slave status,这个命令是在从库上执行的,执行的后果外面有个 seconds\_behind\_master 字段,这个字段示意主从提早多少 s, 留神单位是秒。所以这种计划就是通过判断以后这个值是否为 0,如果为 0 则间接查问获取后果,如果不为 0,则始终期待,直到主从提早变为 0

因为这个值是秒级的,然而咱们的一些场景下是毫秒级的申请,所以通过这个形式判断,不是特地准确

  1. 比照位点判断主从无提早

上图是执行一次 show slave status 局部后果

  • 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 汇合

通过比拟 Retrieved\_Gtid\_Set 和 Executed\_Gtid\_Set 汇合是否统一,来确定主从是否存在提早。

可见比照位点和比照 GTID 汇合,比 sleep 要精确一点儿,在查问之前都能够先判断一下是否接管到的日志都执行实现了,尽管准确度晋升了,然而还达不到准确,为啥这么说呢?

先回顾一下 binlog 在一个事物下的状态

1. 主库执行实现,写入 binlog,反馈给客户端

2.binlog 被从主库发送到备库,备库接管到日志

3. 备库执行 binlog

咱们下面判断主备无提早计划,都是判断备库收到的日志都执行过了,然而从 binlog 在主备之间的状态剖析,能够看出,还有一部分日志处于客户端曾经收到提交确认,然而备库还没有收到日志的状态

这个时候主库执行了 3 个事物,trx1,trx2,trx3,其中

  • trx1,trx2 曾经传到从库,并且从库曾经执行实现
  • trx3 主库曾经执行实现,并且曾经给客户端回复,然而还没有传给从库

这个时候如果在从库 B 执行查问,依照下面咱们判断位点的形式,这个时候主从是没有提早的,然而还查不到 trx3, 严格说就是呈现了 ” 过期读 ”。那么这个问题有什么办法能够解决么?

要解决这个问题,能够引入半同步复制,也就是 semi-sync repliacation(参考:https://dev.mysql.com/doc/refman/8.0/en/replication-semisync.html)。

能够通过

show variables like '%rpl_semi_sync_master_enabled%'
show variables like '%rpl_semi_sync_slave_enabled%'


这两个命令来查看主从是否都开启了半同步复制。

semi-sync 做了这样的设计:

1. 事物提交的时候,主库把 binlog 发给从库

2. 从库接管到主库发过来的 binlog,给主库一个 ack 确认,示意收到了

3. 主库收到这个 ack 确认后,才给客户端返回一个事物实现的确认

也就是启用了 semi-sync,示意所有返回给客户端曾经确认实现的事物,从库都收到了 binlog 日志,这样通过 semi-sync 配合判断位点的形式,就能够确定在从库上的查问,防止了过期读的呈现。

然而 semi-sync 配合判断位点的形式,只实用一主一备的状况,在一主多从的状况下,主库只有收到一个从库的 ack 确认,就给客户端返回事物执行实现的确认,这个时候在从库上执行查问就有两种状况

  • 如果查问刚好是在给主库响应 ack 确认的从库上,那么能够查问到正确的数据
  • 然而如果申请落到其余的从库上,他们可能还没收到日志,所以仍然可能存在过期读

其实通过判断同步位点或者 GTID 汇合的计划,还存在一个潜在的问题,就是业务高峰期,主库的位点或者 GITD 汇合更新的十分快,那么两个位点的判断始终不相等,很可能呈现从库始终无奈响应查问申请的状况。

下面的两种计划在靠谱水平和精确性上都差了一点儿,接下来介绍两种绝对靠谱和准确一点儿的计划

4. 等主库位点

要了解等主库位点,先介绍一条命令

select master_pos_wait(file, pos[, timeout]);

这条命令执行的逻辑是:

1. 首先是在从库执行的

2. 参数 file 和 pos 是主库的 binlog 文件名和执行到的地位

3.timeout 参数是非必须,设置为正整数 N,示意这个函数最多等到 N 秒

这个命令执行后果 M 可能存在的状况:

  • M>0 示意从命令执行开始,到利用完 file 和 pos 示意的 binlog 地位,一共执行了 M 个事务
  • 如果执行期间,备库的同步线程产生异样,则返回 null
  • 如果期待超过 N 秒,返回 -1
  • 如果刚开始执行的时候,发现曾经执行了过了这个 pos,则返回 0

当一个事务执行实现后,咱们要马上发动一个查问申请,能够通过上面的步骤实现:

1. 当一个事务执行实现后,马上执行 show master status,获取主库的 File 和 Position

2. 抉择一个从库执行查问

3. 在从库上执行 select master\_pos\_wait(File,Poistion,1)

4. 如果返回的值 >=0,则在这个从库上执行

5. 否则回主库查问

这里咱们假如,这条查问申请在从库上最多期待 1s, 那么如果 1s 内 master\_pos\_wait 返回一个大于等于 0 的数,那么就能保障在这个从库上能查到刚执行完的事务的最新的数据。

上述的步骤 5 是这类计划的兜底计划,因为从库的延迟时间不可控,不能有限期待,所以如果超时,就应该放弃,到主库查问。

可能有同学会觉的,如果所有的提早都超过 1s,那么所有的压力都到了主库,的确是这样的,然而依照咱们设定的不容许呈现过期读,那么就只有两种抉择,要么超时放弃,要么转到主库,具体抉择哪种,须要咱们依据业务进行具体的剖析。

5. 等 GTID 计划

如果数据库开启的 GTID 模式,那么相应的也有等 GTID 的计划

 select wait_for_executed_gtid_set(gtid_set, 1);


这条命令的逻辑是:

1. 期待,直到这个库执行的事务中蕴含传入的 giid_set 汇合,返回 0

2. 超时返回 1

在后面期待主库位点的计划中,执行完事务后,须要到主库执行 show master status。从 mysql5.7.6 开始,容许事务执行实现后,把这个事务执行的 GTID 返回给客户端,这样期待 GTIID 的计划就缩小了一次查问。

这时等 GTID 计划的流程就变成这样:

1. 事务执行实现后,从返回包解析获取这个事务的 GTID, 记为 gtid1

2. 选定一个从库执行查问

3. 在从库上执行 select wait\_for\_executed\_gtid\_set(gtid1,1)

4. 如果返回 0,则在这个从库上执行查问

5. 否则回到主库查问

和期待主库位点计划一样,最初的兜底计划都是转到主库查问了,须要综合业务思考确定计划

下面的事物执行实现后,从返回的包中解析 GTID,mysql 其实没有提供对应的命令,能够参考 Mysql 提供的 api(https://dev.mysql.com/doc/c-api/8.0/en/mysql-session-track-get-first.html), 在咱们的客户端能够调用这个函数获取 GTID

四、总结

以上简略介绍了读写拆散架构,和呈现主从提早后,如果咱们用的读写拆散的架构,那么咱们应该怎么解决这种状况,置信在日常咱们的主从还是或多或少的存在提早。下面介绍的几种计划,有些计划看上去非常不靠谱,有些计划做了一些斗争,然而都有理论的利用场景,须要咱们依据本身的业务状况,正当抉择对应的计划。

但话说回来,导致过期读的实质还是一写多读导致的,在理论的利用中,可能有别的不必期待就能够程度扩大的数据库计划,但这往往都是通过就义写性能取得的,也就是须要咱们在读性能和写性能之间做个衡量。

文中有不太谨严或者谬误的中央还望大家多多斧正。

作者:京东批发 尚有智

起源:京东云开发者社区 转载请注明起源

正文完
 0