8 关于集群

1 MySQL的主备

基本的主备切换流程:M-S结构
在这里插入图片描述
备库设置成只读,怎么写入主库的写操作,保持同步更新:readonly设置对超级(super)权限用户是无效的,而用于同步更新的线程,就拥有超级权限。

update语句在节点A执行,然后同步到节点B的完整流程图:
在这里插入图片描述
实际生产上使用比较多的是双M结构:这样切换的时候就不用再修改主备问题
在这里插入图片描述
不过会有一个循环复制问题:业务逻辑在节点A上更新了⼀条语句,然后再把生成的binlog 发给节点B,节点B执行完这条更新语句后也会生成binlog;
节点A同时是节点B的备库,相当于又把节点B新生成的binlog拿过来执行了⼀次,然后节点A和B间,会不断地循
环执行这个更新语句。

解决:

  1. 规定两个库的server id必须不同,如果相同,则它们之间不能设定为主备关系;
  2. ⼀个备库接到binlog并在重放的过程中,生成与原binlog的server id相同的新的binlog;
  3. 每个库在收到从自己的主库发过来的日志后,先判断server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志。

因此日志的执行流程为:

  1. 从节点A更新的事务,binlog里面记的都是A的server id;
  2. 传到节点B执行⼀次以后,节点B生成的binlog 的server id也是A的server id;
  3. 再传回给节点A,A判断到这个server id与自己的相同,就不会再处理这个⽇志。所以,死循环在这里就断掉了。

2 高可用

正常情况下,只要主库执行更新生成的所有binlog,都可以传到备库并被正确地执行,备库就能达到跟主库⼀致的状态,这就是最终⼀致性:
在这里插入图片描述
但是,MySQL要提供高可用能力,只有最终⼀致性是不够的

2.1 主备延迟

数据同步的流程:

  1. 主库A执行完成⼀个事务,写⼊binlog:把这个时刻记为T1;
  2. 之后传给备库B,把备库B接收完这个binlog的时刻记为T2;
  3. 备库B执行完成这个事务:把这个时刻记为T3。

主备延迟:同⼀个事务,在备库执行完成的时间和主库执行完成的时间之间的差值,也就是T3-T1(也就是seconds_behind_maste,表示当前备库延迟了多少秒)。

主备延迟大主要是因为1和3(网络好的情况下可以忽略2),最直接的表现就是备库消费中转日志(relay log)的速度,比主库生产binlog的速度要慢。

原因:

1 备库所在机器的性能要比主库所在的机器性能差:可以加强机器

2 备库的压力大,比如读操作多:可以一主多从、通过binlog输出到外部系统,比如Hadoop这类系统,让外部系统提供统计类查询的能力

3 大事务,因为主库上必须等事务执行完成才会写入binlog,再传给备库,所以,如果⼀个主库上的语句执行10分钟,那这个事务很可能就会导致从库延迟10分钟。

比如:⼀次性地用delete语句删除太多数据、大表DDL

4 备库的并行复制能力

2.2 策略

由于主备延迟的存在,所以在主备切换的时候,就相应地有不同的策略。

2.2.1 可靠性优先策略

  1. 判断备库B现在的seconds_behind_master,如果小于某个值(比如5秒)则继续下⼀步,否则持续重试这⼀步;
  2. 把主库A改成只读状态,即把readonly设置为true;
  3. 判断备库B的seconds_behind_master的值,直到这个值变成0为止;(前三步是为了主备数据同步)
  4. 把备库B改成可读写状态,也就是把readonly 设置为false;
  5. 把业务请求切到备库B。
    在这里插入图片描述

2.2.2 可用性优先策略

如果强行把步骤4、5调整到最开始执行,也就是说不等主备数据同步,直接把连接切到备库B,并且让备库B可以读写,那么系统几乎就没有不可用时间了

可能产生数据不⼀致:

初始化:

CREATE TABLE `t` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`c` int(11) unsigned DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;

insert into t(c) values(1),(2),(3);

执行sql:

insert into t(c) values(4);
insert into t(c) values(5);

假设现在主库上其他的数据表有大量的更新,导致主备延迟达到5秒。在插⼊⼀条c=4的语句后,发起了主备切换:binlog_format=mixed
在这里插入图片描述

2.3 总结

在满足数据可靠性的前提下,MySQL高可用系统的可用性,是依赖于主备延迟的。延迟的时间越小,在主库故障的时候,服务恢复需要的时间就越短,可用性就越高。

⼤多数情况下建议使用可靠性优先策略(保证一致性)

3 备库延迟:并行复制

一般情况下备库延迟是分钟级的;
但是如果备库执行日志的速度持续低于主库生成日志的速度,这个延迟有可能成了小时级别;
而且对于⼀个压力持续比较高的主库来说,备库很可能永远都追不上主库的节奏。
在这里插入图片描述
如何提高备库的速度:如图的主备流程图中,如果sql_thread使用多线程复制,那么备库就能快很多:coordinator就是原来的sql_thread
在这里插入图片描述
事务不能按照轮询的方式分发给各个worker;
因为,事务被分发给worker以后,不同的worker就独立执行了;
但是,由于CPU的调度策略,很可能第⼆个事务最终比第⼀个事务先执行;
如果这时候刚好这两个事务更新的是同⼀行,也就意味着,同⼀行上的两个事务,在主库和备库上的执行顺序相反,会导致主备不⼀致的问题。

同⼀个事务的多个更新语句不能分给不同的worker来执行,比如⼀个事务更新了表t1和表t2中的各⼀行,如果这两条更新语句被分到不同worker的话,虽然最终的结果是主备⼀致的,但如果表t1执行完成的瞬间,备库上收到⼀个查询请求,就会看到这个事务“更新了⼀半的结果”,破坏了事务逻辑的原子性。

结合两个”不能“,可以看到coordinator在分发的时候,需要满足以下这两个基本要求:

  1. 不能造成更新覆盖,这要求更新同⼀行的两个事务,必须被分发到同⼀个worker中。(主备一致)
  2. 同⼀个事务(的更新)不能被拆开,必须放到同⼀个worker中。(事务原子性)

有多种策略进行分发

3.1 按表分发

在这里插入图片描述
如果两个事务更新不同的表,它们就可以并行;
因为数据是存储在表里的,所以按表分发,可以保证两个worker不会更新同⼀行。

每个worker线程对应⼀个hash表,用于保存当前正在这个worker的“执行队列”里的事务所涉及的表;
hash表的key是“库名.表名”,value是⼀个数字,表示队列中有多少个事务修改这个表。

每个事务在分发的时候,跟所有worker的冲突关系包括以下三种情况:

  1. 如果跟所有worker都不冲突,coordinator线程就会把这个事务分配给最空闲的woker;
  2. 如果跟多于⼀个worker冲突(事务涉及多个表的时候),coordinator线程就进⼊等待状态,直到和这个事务存在冲突关系的worker只剩下1个;
  3. 如果只跟⼀个worker冲突(此worker操作的表与事务相同),coordinator线程就会把这个事务分配给这个存在冲突关系的worker。

此方案适合的场景:多个表负载均匀

不适合的场景:如果碰到热点表,比如所有的更新事务都会涉及到某⼀个表的时候,所有事务都会被分配到同⼀个worker中,就变成单线程复制了。

这个模式要求binlog格式必须是row

3.2 按行分发

如果两个事务没有更新相同的行,它们在备库上可以并行执行;
这个模式也要求binlog格式必须是row。

key为“库名+表名+唯一键的名字+唯一键的值”,唯一键为主键或唯一索引;
唯一键需要包含唯一索引,否则不同事务更新之间可能出错:
在这里插入图片描述
如图,假设a为唯一索引,id=1的a为1,如果两个事务被分到不同的worker,那么sessionB可能先执行,此时id=1的行的a的值还是1,会报唯一键冲突;
所以如update t1 set a=1 where id=2,coordinator在解析这个语句的binlog的时候,这个事务的hash表就有三个项:

  1. key=hash_func(db1+t1+“PRIMARY”+2), value=2; value为2是因为修改前后的行id值不变,出现了两次。
  2. key=hash_func(db1+t1+“a”+2), value=1,表示会影响到这个表a=2的行。
  3. key=hash_func(db1+t1+“a”+1), value=1,表示会影响到这个表a=1的行。

相比于按表并行分发策略,按行并行策略在决定线程分发的时候,需要消耗更多的计算资源。

3.3 两种方案总结

这两个方案的些约束条件:

  1. 要能够从binlog里面解析出表名、主键值和唯⼀索引的值。也就是说,主库的binlog格式必须是row;
  2. 表必须有主键;
  3. 不能有外键。表上如果有外键,级联更新的行不会记录在binlog中,这样冲突检测就不准确。

对比按表分发和按行分发这两个方案的话,按行分发策略的并行度更高。不过,如果是要操作很多行的大事务的话,按行分发的策略有两个问题:

  1. 耗费内存。比如⼀个语句要删除100万行数据,这时候hash表就要记录100万个项。
  2. 耗费CPU。解析binlog,然后计算hash值,对于大事务,这个成本还是很高的;
    所以实现这个策略的时候会设置⼀个阈值,单个事务如果超过设置的行数阈值(比如,如果单个事务更新的⾏数超过10万行),就暂时退化为单线程模式,退化过程的逻辑⼤概是这样的:
    1. coordinator暂时先停住这个事务;
    2. 等待所有worker都执行完成,变成空队列;
    3. coordinator直接执行这个事务;
    4. 恢复并行模式。

4 一主多从

一主多从基本结构:
在这里插入图片描述

4.1 切换

主库发生故障,主备切换后的结果:
在这里插入图片描述
A’会成为新的主库,从库B、C、D也要改接到A’;
由于多了从库B、C、D重新指向的这个过程,所以主备切换的复杂性也相应增加了。

基于位点的主备切换来解决同步位点问题的话容易出现主键冲突之类的错误,因为此方法并不精确,其解决方案复杂,也容易出错

MySQL5.6之后使用GTID来解决寻找同步位点的问题:全局事务ID,一个事务提交的时候生成,是这个事务的唯一标识,其组成有两部分:

GTID=server_uuid:gno

server_uuid是⼀个实例第⼀次启动时自动生成的,是⼀个全局唯⼀的值;(MySQL官方是source_id)
gno是⼀个整数,初始值是1,每次提交事务的时候分配给这个事务,并加1。(MySQL官方是transaction_id)

GTID生成方式:

  1. 如果gtid_next=automatic,代表使用默认值;
    这时,MySQL就会把server_uuid:gno分配给这个事务;
    a. 记录binlog的时候,先记录⼀行 SET @@SESSION.GTID_NEXT=‘server_uuid:gno’;即下一个
    b. 把这个GTID加⼊本实例的GTID集合。
  2. 如果gtid_next是⼀个指定的GTID的值,比如通过set gtid_next='current_gtid’指定为current_gtid,那么就有两种可能;
    a. 如果current_gtid已经存在于实例的GTID集合中,接下来执行的这个事务会直接被系统忽略;
    b. 如果current_gtid没有存在于实例的GTID集合中,就将这个current_gtid分配给接下来要执行的事务,也 就是说系统不需要给这个事务生成新的GTID,因此gno也不用加1。

这样,每个MySQL实例都维护了⼀个GTID集合,用来对应“这个实例执行过的所有事务”。

如果出现冲突,可以通过提交空事务的方式:比如
实例X是另外⼀个实例Y的从库,此时在实例Y上执行了下面这条插入语句:

insert into t values(1,1);

这条语句在实例Y上的GTID是 aaaaaaaa-cccc-dddd-eeee-ffffffffffff:10
实例X作为Y的从库,要同步这个事务过来执行,显然会出现主键冲突,导致实例X的同步线程停止;
解决:执行下面的这个语句序列

set gtid_next='aaaaaaaa-cccc-dddd-eeee-ffffffffffff:10';
begin;
commit;
set gtid_next=automatic;
start slave;

通过提交⼀个空事务,前三条语句把这个GTID加到实例X的GTID集合中,这样,最后执行start slave命令让同步线程执行起来的时候,虽然实例X上还是会继续执行实例Y传过来的事务,但是由于“aaaaaaaa-cccc-dddd-eeee-ffffffffffff:10”已经存在于实例X的GTID集合中了,所以实例X就会直接跳过这个事务,也就不会再出现主键冲突的错误

  • 基于GTID的主备切换

备库B要设置为新主库A’的从库,把实例A’的GTID集合记为set_a,实例B的GTID集合记为set_b,实例B执行CHANGE MASTER

主备切换逻辑:

  1. 实例B指定主库A’,基于主备协议建连接。
  2. 实例B把set_b发给主库A’。
  3. 实例A’算出set_a与set_b的差集,也就是所有存在于set_a,但是不存在于set_b的GITD的集合,判断A’本地是否包含了这个差集需要的所有binlog事务。
    a. 如果不包含,表示A’已经把实例B需要的binlog给删掉了,直接返回错误;
    b. 如果确认全部包含,A’从自己的binlog文件里面,找出第⼀个不在set_b的事务,发给B;
  4. 之后就从这个事务开始,往后读文件,按顺序取binlog发给B去执行。

设计思想:在基于GTID的主备关系里,系统认为只要建立主备关系,就必须保证主库发给备库的日志是完整的;
因此,如果实例B需要的日志已经不存在,A’就拒绝把日志发给B。

如果有多个从库,由于不需要手动找位点了,所以从库只需要分别执行change master命令指向实例A’即可。

之后这个系统就由新主库A’写⼊,主库A’的自己生成的binlog中的GTID集合格式是:server_uuid_of_A’:1-M
如果之前从库B的GTID集合格式是 server_uuid_of_A:1-N, 那么切换之后GTID集合的格式就变成了server_uuid_of_A:1-N,server_uuid_of_A’:1-M
当然,主库A’之前也是A的备库,因此主库A’和从库B的GTID集合是⼀样的。这就达到了预期。

4.2 读写分离带来的问题

4.1的架构即读写分离的基本结构:
在这里插入图片描述
还有一种架构:MySQL和客户端之间有⼀个中间代理层proxy,客户端只连接proxy, 由proxy根据请求类型和上下文决定请求的分发路由:
在这里插入图片描述
对比:

  1. 客户端直连方案:
    因为少了⼀层proxy转发,所以查询性能稍微好⼀点,并且整体架构简单,排查问题更方便;
    但是此方案要了解后端部署细节,所以在出现主备切换、库迁移等操作的时候,客户端都会感知到,并且需要调整数据库连接信息。
    ⼀般采用这样的架构,⼀定会伴随⼀个负责管理后端的组件,比如Zookeeper,尽量让业务端只专注于业务逻辑开发。
  2. 带proxy的架构方案:
    对客户端比较友好。客户端不需要关注后端细节,连接维护、后端信息维护等⼯作,都是由proxy完成的;
    不过对后端维护团队的要求会更高;
    而且,proxy也需要有高可用架构。因此,带proxy架构的整体就相对比较复杂。

两种方案都会遇到“在从库上会读到系统的⼀个过期状态”的现象,本文暂且称之为“过期读”:
由于主从可能存在延迟,客户端执行完⼀个更新事务后马上发起查询,如果查询选择的是从库的话,就有可能读到刚刚的事务更新之前的状态

处理过期读的方案:

强制走主库
sleep方案
判断主备无延迟
配合semi-sync
等主库位点
等GTID

4.2.1 强制走主库

将查询请求做分类:

  1. 对于必须要拿到最新结果的请求,强制将其发到主库上。
    比如,在⼀个交易平台上,卖家发布商品以后,马上要返回主页面,看商品是否发布成功,那么,这个请求需要拿到最新的结果,就必须走主库。
  2. 对于可以读到旧数据的请求,才将其发到从库上。
    比如在这个交易平台上,买家来逛商铺页面,就算晚几秒看到最新发布的商品,也是可以接受的。那么,这类请求就可以走从库。

4.2.2 sleep方案

主库更新后,读从库之前先sleep⼀下

优化:以卖家发布商品为例,商品发布后,⽤Ajax直接把客户端输⼊的内容作为“新的商品”显示在页面上,而不是真正地去数据库做查询;
这样,卖家就可以通过这个显示,来确认产品已经发布成功了。等到卖家再刷新页面,去查看商品的时候,其实已经过了⼀段时间,也就达到了sleep的⽬的,进而也就解决了过期读的问题。

不精确的地方:

  1. 如果这个查询请求本来0.5秒就可以在从库上拿到正确结果,也会等1秒;
  2. 如果延迟超过1秒,还是会出现过期读。

4.2.3 判断主备无延迟

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

第⼀种确保主备无延迟的方法:每次从库执行查询请求前,先判断seconds_behind_master是否已经等于0;
如果还不等于0 ,那就必须等到这个参数变为0才能执行查询请求。

第二种确保主备无延迟的方法:对比位点确保主备无延迟,即判断读到的主库的最新位点和备库执行的最新位点是否相同

第三种确保主备无延迟的方法:对比GTID集合确保主备无延迟,即判断备库收到的所有日志的GTID集合和所有已经执行完成的GTID集合是否相同

不精确的地方:
上面判断主备无延迟的逻辑,是“备库收到的日志都执行完成了”;
但是,从binlog在主备之间状态的分析中可以看出还有⼀部分日志,处于客户端已经收到提交确认,而备库还没收到日志的状态:trx1和trx2已经传到从库,并且已经执行完成了,trx3在主库执行完成,并且已经回复给客户端,但是还没有传到从库中。
在这里插入图片描述
这时候在从库B上执行查询请求,按照上面的逻辑,从库认为已经没有同步延迟,但还是查不到trx3的;
严格地说,就是出现了过期读。

复习:⼀个事务的binlog在主备库之间的状态:

  1. 主库执行完成,写⼊binlog,并反馈给客户端;
  2. binlog被从主库发送给备库,备库收到;
  3. 在备库执行binlog完成。

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

4.2.4 配合semi-sync(半同步复制)

4.2.3加上4.2.4,可以解决4.2.3不精确的地方:

  1. 事务提交的时候,主库把binlog发给从库;
  2. 从库收到binlog以后,发回给主库⼀个ack,表示收到了;
  3. 主库收到这个ack以后,才能给客户端返回“事务完成”的确认。

但是,semi-sync+位点判断的方案,只对⼀主⼀备的场景是成立的;
在⼀主多从场景中,主库只要等到⼀个从库的ack,就开始给客户端返回确认;
这时,在从库上执行查询请求,就有两种情况:

  1. 如果查询是落在这个响应了ack的从库上,是能够确保读到最新数据;
  2. 但如果是查询落到其他从库上,它们可能还没有收到最新的日志,就会产生过期读的问题。

semi-sync+位点判断的方案还会出现如下问题:如果在业务更新的高峰期,主库的位点或者GTID集合更新很快,
那么上面的两个位点等值判断就会⼀直不成立,很可能出现从库上迟迟无法响应查询请求的情况。
在这里插入图片描述
如图,从状态1到状态4,⼀直处于延迟⼀个事务的状态。
但其实客户端是在发完trx1更新后发起的select语句,只需要确保trx1已经执行完成就可以执行select语句了;
也就是说,如果在状态3执行查询请求,得到的就是预期结果了。

使用等主库位点可以解决这两个问题

4.2.5 等主库位点

对于4.2.4中的图,先执行trx1,再执行⼀个查询请求的逻辑,要保证能够查到正确的数据,可以使用如下逻辑:

  1. trx1事务更新完成后,马上执行show master status得到当前主库执行到的File和Position;
  2. 选定⼀个从库执行查询语句;
  3. 在从库上执行select master_pos_wait(File, Position, 1)
  4. 如果返回值是>=0的正整数,则在这个从库执行查询语句;
  5. 否则,到主库执行查询语句。
    在这里插入图片描述
    等待超时后是否直接到主库查询,需要业务开发来做限流考虑。

select master_pos_wait(file, pos[, timeout]);逻辑如下:

  1. 它是在从库执行的;
  2. 参数file和pos指的是主库上的文件名和位置;
  3. timeout可选,设置为正整数N表示这个函数最多等待N秒。

正常返回的结果是⼀个正整数M,表示从命令开始执行,到应用完file和pos表示的binlog位置,执行了多少事务;
除了正常返回⼀个正整数M外,这条命令还会返回⼀些其他结果,包括:

  1. 如果执行期间,备库同步线程发生异常,则返回NULL;
  2. 如果等待超过N秒,就返回-1;
  3. 如果刚开始执行的时候,就发现已经执行过这个位置了,则返回0。

4.2.6 等GTID

命令:

select wait_for_executed_gtid_set(gtid_set, 1);
  1. 等待,直到这个库执行的事务中包含传⼊的gtid_set,返回0;
  2. 超时返回1。

优化:在4.2.5等位点的方案中,执行完事务后,还要主动去主库执行show master status
而MySQL 5.7.6版本开始,允许在执行完更新类事务后,把这个事务的GTID返回给客户端,这样等GTID的方案就可以减少⼀次查询。

执行流程:

  1. trx1事务更新完成后,从返回包直接获取这个事务的GTID,记为gtid1;
  2. 选定⼀个从库执行查询语句;
  3. 在从库上执行 select wait_for_executed_gtid_set(gtid1, 1);
  4. 如果返回值是0,则在这个从库执行查询语句;
  5. 否则,到主库执行查询语句。
    在这里插入图片描述

5 如何判断主库出问题了

注:MySQL中,在线程进⼊锁等待以后,并发线程的计数会减⼀,即等行锁(也包括间隙锁)的线程是不算在128里面的。(由把innodb_thread_concurrency指定)

5.1 查表判断

在系统库(mysql库)里创建⼀个表,比如命名为health_check,里面只放⼀行数据,然后定期执行:

select * from mysql.health_check;

使用这个方法可以检测出由于并发线程过多导致的数据库不可用的情况。

问题:空间满了以后,这种方法会变得不好使
因为更新事务要写binlog,而⼀旦binlog所在磁盘的空间占用率达到100%,那么所有的更新语句和事务提交的commit语句就都会被堵住,但是,系统这时候还是可以正常读数据的,因此需要改进

5.2 更新判断

改进为更新语句:常见做法是放⼀个timestamp字段,用来表示最后⼀次执行检测的时间。

主备要同时检查,如果主库A和备库B都用相同的更新命令,就可能出现行冲突,也即可能会导致主备同步停止;
所以,mysql.health_check 这个表就不能只有⼀行数据了;
可以在mysql.health_check表上存⼊多行数据,并用A、B的server_id做主键;
由于MySQL规定了主库和备库的server_id必须不同(否则创建主备关系的时候就会报错),这样就可以保证主、备库各自的检测命令不会发生冲突:

insert into mysql.health_check(id, t_modified) values (@@server_id, now()) on duplicate key update t_modified=now();

5.3 内部统计

5.1和5.2都是外部统计,需要定时轮询,所以系统可能已经出问题了,但是却需要等到下⼀个检测发起执行语句的时候,才有可能发现问题;
而且,可能第⼀次轮询还不能发现,这就会导致切换慢的问题。

MySQL 5.6版本以后提供的performance_schema库的file_summary_by_event_name表里统计了每次IO请求的时间,即磁盘利用率

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页