28 读写分离有哪里要注意的地方

28 读写分离有哪里要注意的地方

 在一主多从的架构中,读写分离,以及怎么处理主备延时导致的读写分离的问题。

读写分离的主要目标是分担主库压力,上图的结构是客户端主动做负载均衡,在这种模式下一般会把数据库的连接信息放在客户端的连接层,也就是由客户端来选择后端数据库进行查询。

还有一种架构,在mysql和客户端之间加入一层中间件层proxy,客户端只连接proxy,由proxy根据请求类型和上下文请求决定路由的分发。

我们看一下客户端直连和带proxy的读写分离架构,各有那些特点

1 客户端直连方案,因为少了一层proxy转发,所以查询性能稍微好一点,并且整体架构简单,排查问题更方便,但这种方案,由于要了解后端部署细节,所以在出现主备切换、库迁移等操作的时候,客户端都会感知,并且需要调整数据库连接信息。

你可能会觉得这样客户端比较麻烦,信息大量冗余,架构很丑,其实也未必,一般这样的架构,一定会伴随一个负责管理后端的组件,比如zookeeper,尽量让业务端只专注于业务逻辑开发

2 proxy的架构,对客户端比较友好,客户端不需要关注背后细节,连接维护、后端信息维护等工作,都是有proxy完成。但这样的话,对后端维护团队的要求会更高,而且,proxy也需要有高可用架构,因此,带proxy架构的整体相对比较复杂。

但是,不论使用哪种架构,都会碰到:由于主从可能存在延时,客户端执行完一个更新事务后立马发起查询,如果查询选择的是从库的话,就很有可能读的是事务更新之前的状态。

这种”在从库上会读到系统的一个过期状态”的现象,称为”过期读”。

处理过期读的方案

1 强制走主库方案

2 sleep方案

3 判断主备无延迟方案

4 配合semi-sync方案

5 等主库位点方案

6 gtid方案

强制走主库方案

将查询请求做分类。通常情况下,可以将查询分为两类

1 对于必须要拿到最新结果的请求,强制将其发到主库上,比如,一个交易平台,卖家发布商品以后,马上要返回主页面,看商品是否发布成功,那么,这个请求需要拿到最新的结果,就必须走主库。

2 对于可以读到旧数据的请求,才将其发到从库,在这个交易平台上,买家来逛商铺页面,就算晚几秒看到最新发布的商品,也是可以接受的。那么,这类请求就可以走从库。

这个方法有点取巧的意思,用的还是比较多,这个方案的最大的问题在于,有时候会碰到”所有查询都不能是过期读”的需求,比如一些金融类的业务,这样的话,要放弃读写分离,所有读写压力都在主库,等同于放弃了扩展性。

Sleep方案

主库更新后,读从库先sleep一下,具体方案就是,类似于执行一条select sleep(1)的命令。

这个方案的假设是,大多数情况下主备延迟在1秒之内,做一个sleep可以有大概率拿到新数据。这个方案的第一感觉应该是不靠谱,直接在发起查询的时候先执行一条sleep的语句,用户体验不友好。

从严格意义上来说,这个方案存在的问题就是不精确:

1 如果这个查询请求本来0.5秒就可以在从库返回拿到正确结果,也会等到1

2 如果延迟超过1秒,还是会出现过期读。

判断主备无延迟方案

要判断备库无延迟,通常有三种做法

show slave status结果里的seconds_behind_master参数的值,可以用来衡量主备延迟时间的长短。

每次从库执行查询请求前,先判断seconds_behind_master是否已经等于0,如果还不等于0,那就必须等到这个参数变0在从库执行查询。seconds_behind_master的单位是秒,如果觉得精度不够,还可以采用对比位点和gtid的方法来确保主备无延迟

(system@127.0.0.1:3306) [(none)]> show slave statusG;
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: 192.168.19.145
                  Master_User: repl
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000024
          Read_Master_Log_Pos: 239249859
               Relay_Log_File: relaylog.000006
                Relay_Log_Pos: 239250022
        Relay_Master_Log_File: mysql-bin.000024
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
              Replicate_Do_DB: 
          Replicate_Ignore_DB: 
           Replicate_Do_Table: 
       Replicate_Ignore_Table: 
      Replicate_Wild_Do_Table: 
  Replicate_Wild_Ignore_Table: 
                   Last_Errno: 0
                   Last_Error: 
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 239249859
              Relay_Log_Space: 239250351
              Until_Condition: None

第二种对比位点确保主备无延迟

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的这两组值完全相同,就表示接收到的日志已经同步完成。

第三种方法对比gtid集合确保主备不延迟

Auto_Position=1 表示这对主备关系使用了gtid协议

Retrieved_Gtid_Set,是备库收到的所有日志的gtid的集合

Executed_Gtid_Set,是备库所有已经执行完成的gtid的集合

如果这两个集合相同,也表示备库接收到的日志都已经全部同步完成。

可见,对比位点和对比gtid这两种方法,都比判断seconds_behind_master要准确

在回顾一下,一个事务的binlog在主备库之间的状态

1 主库执行完成,写入binlog,并反馈给客户端

2 binlog被主库发送到备库,备库收到

3 在备库上执行binlog完成。

我们上面判断主备无延迟的逻辑,是”备库接收到的日志都执行完成了”。但是,从binlog中间状态的分析中,有一部分日志,处于客户端已经接收到提交确认,而备库还没有收到日志的状态。

这时候,主库上执行完成了三个事务,trx1,trx2trx3

1 trx1trx2已经传到从库,并且已经执行完成

2 trx3在主库执行完成,并且已经恢复给客户端,但是还没有传到从库。

如果这时候在从库B上执行查询请求,按照上面的逻辑,从库认为已经没有同步延时,但还是查不到trx3的事务。

配合semi-sync

要解决这个问题,就要引入半同步复制,semi-sync replication

semi-sync的设计:

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

2 从库收到binlog后,发回给主库一个ack,表示收到了

3 主库收到这个ack后,才能给客户端返回”事务完成”的确认。

也就是说,如果启用了semi-sync,就表示所有给客户端发送过确认的事务,都确保备库已经收到了这个日志。

其实,判断同步位点的方案还有一个潜在的问题,即:如果在业务更新的高峰期,主库的位点或者gtid集合更新很快,那么上面的两个位点等值判断就会一直不成立,很可能出现从库无法响应查询请求的情况。

等主库位点方案

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

这条命令的逻辑:

1 在从库执行

2 参数filepos指的是主库上的文件名和位置

3 timeout可选,设置为正整数N表示这个函数最多等待N

这个命令正常返回的结果是一个正整数M,表示从命令开始执行,到应用完filepos表示的binlog位置,执行了多少个事务

当然,除了正常返回一个正整数M外,还返回一些其他结果

1 如果执行期间,备库同步线程发生异常,则返回null

2 如果等待超过N秒,就返回-1

3 如果刚开始执行的时候,就发现已经执行过这个位置了,则返回0

1 trx1事务更新完成后,马上执行show master status得到当前主库执行的fileposition

2 选定一个从库执行查询语句

3 在从库上执行select master_pos_wait(file,position,1)

4 如果返回值>=0的正整数,则在这个从库执行查询语句

5 否则,到主库执行查询语句

假设,这条select查询最多在从库上等待1秒,那么,如果1秒内master_pos_wait返回一个大于等于0的整数,就确保了从库上执行的这个查询结果一定包含了trx1的数据。

步骤5到主库执行查询语句,是这类方案常用的退化机制,因为从库的延时不可控,不能无限等待,所以如果等待超时,就应该去主库查询。

GTID方案

数据库开启gtidmysql提供了一个类似的命令

 select wait_for_executed_gtid_set(gtid_set, 1);

1 等待,直到这个库执行的事务中包含传入的gtid_set,返回0

2 超时返回1

在前面等待位点的方案中,我们执行完事务后,需要执行show master status,而mysql 5.6开始,允许在执行完更新类事务后,把这个事务的gtid返回给客户端,这样等待gtid的方案就可以减少一次查询。

这时,等gtid的执行流程就变成了:

1 trx1事务更新完成,从返回包直接获取这个事务的gtid,记为gtid1

2 选定一个从库执行查询语句

3 在从库上执行select wait_for_executed_gtid_set(gtid1, 1);

4 如果返回值0,则在这个从库执行查询

5 否则,到主库执行查询语句

在上面的第一步中,trx1事务更新完成后,从返回包直接获取这个事务的gtid,问题是,怎么能让mysql在执行事务后,返回包中带上gtid

你只需要将参赛session_track_gtids位置为OWN_GTID,然后通过api结构mysql_session_track_get_first从返回包解析出gtid的值即可

其实,mysql并没有提供这类接口的sql用户,是提供给程序的API(https://dev.mysql.com/doc/refman/5.7/en/c-api-functions.html)

比如,为了让客户端在事务提交后,返回的gtid能够在客户端显示出来,对mysql客户端代码做点修改

看到语句执行完成之后,显示出gtid的值

 

上次问题

gtid模式下,如果从一个新的从库接上主库,但是需要的binlog已经丢失,需要怎么做?

1 如果业务允许主从不一致的情况,那么可以在主库上先执行show global variables like ‘gtid_purged’,得到主库已经删除的gtid集合,假设是gtid_purged1,然后在从库上执行reset master,再执行set global gtid_purged=’gtid_purged1’;最后执行start slave,就会从主库现存的binlog开始同步,binlog缺少的那一部分,数据在从库上就可能会有丢失,造成主从不一致

2 如果需要主从数据一致,最好还是通过重新搭建主从来做

3 如果有其他的从库保留的全量的binlog,可以把新的从库先连接到这个保存了全量的binlog的从库,追上日志,有需要在接回主库

4 如果binlog有备份的话,可以现在从库上应用缺少的binlog,然后在执行start slave

Mysql是怎么快速定位binlog里面的某一个gtid位置?

binlog文件头部的previous_gtids可以解决这个问题,每个文件都有一个这个值

sql_slave_skip_counter跳过的是一个event,由于mysql总部能执行一半的事务,所以既然跳过了一个event,就会跳到这个事务的末尾,因此,set global sql_slave_skip_countre=1;start slave是可以跳过整个事务的。

原文地址:https://www.cnblogs.com/yhq1314/p/10711808.html