MySQL实战-3

 

目录

主备一致性原理

高可用原理

主从复制策略

主从机制

读写分离相关的问题

健康检查的方案

 

主备一致性原理

mysql的主备切换流程

MySQL实战-3_第1张图片

上图就是主备切换的流程,主节点原来是A,现在切换成B
在状态1中,虽然B没有被直接访问,但仍然要把备库B设置成只读的

  1. 有些时候一些运营类查询会放到备库上查询,设置只读可以防止误操作
  2. 防止切换有bug,如切换中出现双写造成主备不一致
  3. 用readonly状态,来判断节点的角色

下图是一个update语句,从A节点同步到B节点的完整流程

MySQL实战-3_第2张图片

主库接收到客户端的更新请求后,执行内部事务的更新逻辑,同时写binlog
备库B跟主库A之间维持了一个长连接,主库A内部有一个线程专用用于服务备库B的这个长连接,一个事务日志同步的完整过程如下

  1. 在备库B上通过change master 命令,设置主库A的ip,端口,用户名/密码,以及要从哪个位置开始请求binlog,这个位置包含文件名和日志偏移量
  2. 在备库B上执行start slave命令,这时候备库会启动两个线程,就是图中io_thread和sql_thread,其中io_thread负责与主库建立连接
  3. 主库A校验玩用户名/密码后,开始按照备库B传过来的位置,从本地读取binlog,发给B
  4. 备库B拿到binlog后,写到本地文件,称为中专日志relay log
  5. sql_thread读取中转日志,解析出日志里的命令,并执行

binlog的三种格式

statement,记录的是sql语句的原文
mysql> show binlog events in 'master.000001';

row,记录的是记录更新后的值(不需要记录上下文信息)
如果一个delete删除了1W行数据,statement只需要记录一个语句就可以,row会记录每一条删除语句,需要更多的空间

MySQL实战-3_第3张图片

mixed模式混合了row和statement的优点

数据恢复的时候,根据insert,update,delete,在row模式下直接做反向操作就可以了

用mysqlbinlog 工具来解析binlog

双主的循环复制问题

MySQL实战-3_第4张图片

解决办法
1.规定两个库的server id必须不同,如果相同,则他们直接不能设定为主备关系
2.一个备库接到binlog并在重放的过程中,生成与原binlog的server id相同的新的binlog
3.每个库在受到从自己的主库发过来的日志后,先判断server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志,所以死循环在这里就断开了

 

高可用原理

同步延迟的概念

  1. 主库A执行完一个事务,写入binlog,把这个时刻记做T1
  2. 之后传给备库B,把备库B接受完这个binlog的时刻记做T2
  3. 备库B执行完这个事务,把这个时刻记做T3

主备延迟,就是同一个事务,在备库执行完的时间和主库执行完的时间的差值,也就是T3-T1
在备库执行
show slave status 命令,返回
second_behind_master,用于表示当前备库延迟了多少秒
如果主备时间不一致,会通过执行SELECT UNIX_TIMESTAMP()函数来获取当前主库的系统时间
网络正常的时候,日志从主库传给备库的时间很短,即T2-T1的值非常小
网络正常下,主备延迟主要是备库接收完binlog和执行完这个事务之间的时间差

主备延迟的原因

  1. 备库所在的机器性能比主库所在的机器性能差
  2. 备库的压力大,如执行了一些运营查询,多增加几个从库,或将binlog输出到Hadoop系统让外部系统提供查询
  3. 大事务如delete大量数据,大表的DDL
  4. 备库的并行复制能力
     

主备切换策略
可靠性优先策略,如下图

MySQL实战-3_第5张图片

  1. 判断备库B现在的seconds_behind_master,如果小于某个值(比如5秒)继续下一步,否则持续重试这一步
  2. 把主库A改成只读状态,即把readonly设置为true
  3. 判断备库B的second_behind_master的值,直到这个值变为0为止
  4. 把备库B改成可读状态,也就是把readonly设置为false
  5. 把业务请求切到备库B

这个切换流程一般是由专门的HA系统来完成的

可用性优先策略
如果把上面的4,5步调整到最开始执行,也就是不等主备同步,直接把连接切到备库B,让备库B可以读写,那么系统几乎就没有不可用时间了
但无论是用mix模式,还是row模式,都可能会造成数据不一致

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

 

主从复制策略

备库如果执行的不如主库快,时间一长就会延迟很多
解决办法是采用并行复制机制
把只有一个线程的sql_thread,拆成多个线程

MySQL实战-3_第6张图片

coordinator就是原来的sql_thread,只负责读取中转日志和分发事务,真正更新日志的是worker线程
work线程数量最好在8-16之间(32核物理机)

并行复制策略
按表分发策略
如果两个事务更新不同的表,它就可以并行,因为数据存储在表里所以按表分发,可以保证两个worker不会更新同一行
MySQL实战-3_第7张图片

每个work线程有个hash表,hask表的key是 库名.表名,value是队列中有多少事务在修改这个表
如果新来的事务T中涉及修改表t1,跟worker_1队列冲突,也跟worker_2冲突,执行策略如下

  1. 如果跟所有的worker都不冲突,coordinator线程把这个事务分配给最空闲的worker
  2. 如果跟多余一个worker冲突,coordinator线程就进入等待状态,直到和这个事务存在冲突关系的worker只剩下1个
  3. 如果只跟一个worker冲突,coordinator线程就会把这个事务分配给这个存在冲突关系的worker

大多数情况下会表现很好,但如果所有事务都都涉及某个表会被分配到一个work,就变成单线程复制了

按行分发策略
事务hash表中还需要考虑唯一键,即key应该是 库名+表名+索引a的名字+a的值
假设下面这个事务
MySQL实战-3_第8张图片

coordinator在解析这个语句的binlog时,这个事务的hash表就有三项

  1. key=hash_func(db1+t1+ primary + 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的行

按行分发会消耗更多的计算资源,而且有一些下限制条件

  1. 主库的binlog格式必须是row
  2. 必须有主键
  3. 不能有外键

如果一个事务需要执行10W行,那么coordinator会等待其他worker执行完,然后退化为单线程模式,等执行完后再恢复成并行模式

mysql5.6采用并行复制策略,以库做维度
MariaDB采用组提交并行复制策略

  1. 在一组里面一起提交的事务,有一个相同的commit_id,下一组就是commit_id+1
  2. commit_id直接写到binlog里面
  3. 传到备库应用的时候,相同commit_id的事务分发到多个worker执行
  4. 这一组全部执行完后,coordinator再去取下一批

下面这三组事务是在主库执行的,trx1,trx2,trx3提交的时候,trx4,trx5和trx6是在执行的,第一组事务提交完成的时候,下一组事务很快进入commit状态

MySQL实战-3_第9张图片

MariaDB并行复制策略,要等第一组事务完全执行完,第二组事务才能开始执行,这样吞吐量会下降,尤其是大事务会拖后腿

MySQL实战-3_第10张图片

mysql5.7的复制策略是优化了MariaDB复制策略
再回顾一下完整的两阶段提交策略

MySQL实战-3_第11张图片


mysql5.7并行复制的策略是

  1. 同时处于prepare状态的事务,在备库执行时是可以并行的
  2. 处于prepare状态的事务,与处于commit状态的事务之间,在备库执行时也可以并行

binlog组提交的时候,有两个参数

  1. binlog_group_commit_sync_delay参数,多少微妙后才调用fsync
  2. binlog_group_commit_sync_no_delay_count参数, 积累多少次后才调用fsync

这两个参数故意拉长binlog从write到fsync的时间,以此减少binlog的写盘次数,这两个参数可以故意让主库提交的慢些,也可以让备库执行的快一些,主从延迟的时候可以适当调整这两个参数

mysql5.7.22支持了行模式的并行复制策略

 

主从机制

下图是基本的一主多从的结构
虚线是主备关系,A和A'互为主备,从库B,C,D指向的是主库A
一主多从的设置,一般用于读写分离

MySQL实战-3_第12张图片


下图是主库发生故障,主备切换后的结果
一主多从切换完之后,A'就变成新的主库,从库B,C,D也要改连接到A'
正是多了从库B,C,D重新指向的这个过程,所以主备切换的复杂性也增加了

MySQL实战-3_第13张图片

把节点B设置成节点A'的从库时,需要执行一条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  

最后两个参数 MASTER_LOG_FILE和MASTER_LOG_POS表示,要从主库的master_log_name文件的master_log_pos这个文职的日志继续同步
这个位置是一个大致的位置并不精确
一种获取方式如下

  1. 等待新主库A’把中转日志relay log全部同步完
  2. 在A'上执行show master status命令,得到当前A'上最新的File和Position
  3. 取原主库A故障时刻T
  4. 用mysqlbinglog工具解析A'的File,得到T时刻的位点

在切换任务时,要先跳过这些错误

  1. 主动跳过一个事务
  2. 通过slave_skip_error参数,直接设置要跳过的错误

GTID 是Global Transaction Identifier,也就是全局事务ID
一个事务在提交的时候生成的,格式是
GTID=server_uuid:gno
基于GTID的切换,也需要找位点,这个工作是在实例A'内部自动完成的,对HA来说是很友好的
在实例B上执行start slave取binlog的逻辑如下

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

 

读写分离相关的问题

读写分离可以用 客户端直连 或者 proxy方式,他们的对比

  1. 客户端少了一层转发性能更好,但需要感知后端架构,一般会搭配zookeeper使用
  2. 使客户端无须关心后端细节,但对团队要求比较高,而且proxy也有高可用架构

目前的趋势是向proxy架构方向发展
无论采用哪种架构,都会碰到读写延迟,客户端再一个更新事务后马上发起查询,如果查询选择的是从库就有可能读到刚更新之前的状态,这种现象被称为 过期读
目前的解决方案包括

  1. 强制走主库方案
  2. sleep方案
  3. 判断主备无延迟方案
  4. 配合semi-sync方案
  5. 等主库位点方案
  6. 等GTID方案


强制走主库
对于可以读到旧数据的请求,继续读从库,对于必须拿最新结果的请求就走主库
但如果是金融业务,所有的请求必须是最新的,就等于是放弃读写分离方案了

sleep方案
主库更新后,读从库之前先sleep 1秒
这种方案简单,但是时间很难确定,不知道1秒是长了还是短了
另外可以配合AJAX提升用户体验

判断主备无延迟方案
show slave status的结果里
seconds_behind_master参数的值,可以用来衡量主备延迟时间的长短
对于每次查询先判断 seconds_behind_master如果不等于0就得先等这个参数为0再查询
这个参数的单位是秒不够精确
还有其他两种方式

对比位点
master_log_file和read_master_log_pos 表示读到的主库的最新位点
relay_master_log_file和exec_master_log_pos,表示备库执行的最新位点
如果第一组两个参数的值,第二组两个参数的值一样,比较接收到的日志已经同步完成

对比GTID集合
retrieved_gtid_set 是备库收到所有日志的GTID集合
executed_gtid_set  是备库所有已经执行完成的GTID集合

判断主备延迟的方案尤其是对比位点会很精确,但如果一个事务刚在主库commit,还没发送给从库,那么判断主备延迟就不管用了,这样仍会读到过期数据

配合semi-sync
这个是半同步复制,机制如下

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

如果主库掉电的时候有些binlog还来不及发给从库,就有可能导致数据丢失,如果是semi-sync方案就不会
半同步复制方案的缺点是
如果查询落在响应ack的从库是能读到最新数据
如果落在其他从库,则仍可能读到过期数据
如果是业务高峰,主库位点或GTID更新很快,位点等值判断会一直不成立,可能出现从库一直无法响应,过度等待的问题


等主库位点方案
使用下面命令

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

执行方式如下

  1. 事务更新完成后马上执行show master status得到当前主库执行的file和position
  2. 选择一个从库执行查询语句
  3. 在从库上执行 select master_pos_wait(file,position,1)
  4. 如果返回的是>=0的正整数,则在这个从库执行查询
  5. 如果小于0则表示超时,则在主库执行

最坏的结果是所有查询都超时1秒然后落在主库上执行

GTID方案
跟等主库位点方案类似,也有一个查询

select wait_for_executed_gtid_set(gtid_set, 1);

执行流程如下

  1. 事务更新完后,从返回包直接获取这个事务的GTID
  2. 选择一个从库执行查询语句
  3. 在从库上执行select wait_for_executed_gtid_set(gtid,1)
  4. 如果返回值是0,则在这个从库执行查询
  5. 否则到主库上执行查询

实际使用中,是几个方案混合用的
先对客户端做分类,哪些请求可以接受过期读,哪些不可以
对于不能接受过期的请求,再使用GTID或者等位点的方案
如果极端情况所有GTID查询后都失败,再查询主库,这样主库压力仍会非常大,需要做限流处理

 

健康检查的方案

select 1 方案
考虑到下面这个问题,将innodb_thread_concurrrency设置为3,这个是并发线程的上限
此时三个事务都在卡住了,第四个任务做普通的查询会被卡住,但select 1 就不会有问题

MySQL实战-3_第14张图片

连接可能有很多,但并发查询必须设置一个上限,比如128,这个是参数 innodb_thread_concurrency来控制的
进入等待队列后,并发线程会减1,也就是等行锁不算在128个并发线程里面

查表判断
增加一个健康检查表

mysql> select * from mysql.health_check; 

使用这个方案,可以检测出并发线程多多导致的数据库不可用情况
但存储空间满了的话,这种普通查询是检查不出来的

更新判断
把上面那个查询判断改成查询

mysql> update mysql.health_check set t_modified=now();

如果是主库的双M设计,这个表就不对了,必须增加server_id做主键

mysql> CREATE TABLE `health_check` (
  `id` int(11) NOT NULL,
  `t_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

/* 检测命令 */
insert into mysql.health_check(id, t_modified) values (@@server_id, now()) on duplicate key update t_modified=now();

以上还是有 判断慢的问题
如果IO利用率是100%表示系统系统已经满负荷了,简单的update需要的资源少就能拿到IO资源,于是返回系统正常
但其他的请求可能就不行了
而且这种情况可能需要好几轮才能检测出来

内部统计方案
mysql5.6之后提供了performance_schema库
在 file_sunnmary_by_event_name 表里统计了每次IO请求的时间
MySQL实战-3_第15张图片

图中这一行表示redo log的写入时间,第一列EVENT_NAME表示统计的类型
然后是IO类型统计(sun,min,avg等),再是读,写操作统计
与binlog对应的是event_name="wait/io/file/sql/binlog" 这一行

打开redo log监控

mysql> update setup_instruments set ENABLED='YES', Timed='YES' where name like '%wait/io/file/innodb/innodb_log_file%';

下面语句用来做监控

mysql> select event_name,MAX_TIMER_WAIT  FROM performance_schema.file_summary_by_event_name where event_name in ('wait/io/file/innodb/innodb_log_file','wait/io/file/sql/binlog') and MAX_TIMER_WAIT>200*1000000000;

发现异常后,清空信息

mysql> truncate table performance_schema.file_summary_by_event_name;

如果后面的监控中,再次出现这个异常,就可以加入监控累积值了
select 1 这个方案最简单,虽然不是很准,但使用的很多
每个改进方案,都会增加额外损耗,所以需要根据实际情况做权衡
推荐使用update系统表,再加上performance_schema的信息

 

 

你可能感兴趣的:(MySql)