目录
主备一致性原理
高可用原理
主从复制策略
主从机制
读写分离相关的问题
健康检查的方案
mysql的主备切换流程
上图就是主备切换的流程,主节点原来是A,现在切换成B
在状态1中,虽然B没有被直接访问,但仍然要把备库B设置成只读的
下图是一个update语句,从A节点同步到B节点的完整流程
主库接收到客户端的更新请求后,执行内部事务的更新逻辑,同时写binlog
备库B跟主库A之间维持了一个长连接,主库A内部有一个线程专用用于服务备库B的这个长连接,一个事务日志同步的完整过程如下
binlog的三种格式
statement,记录的是sql语句的原文
mysql> show binlog events in 'master.000001';
row,记录的是记录更新后的值(不需要记录上下文信息)
如果一个delete删除了1W行数据,statement只需要记录一个语句就可以,row会记录每一条删除语句,需要更多的空间
mixed模式混合了row和statement的优点
数据恢复的时候,根据insert,update,delete,在row模式下直接做反向操作就可以了
用mysqlbinlog 工具来解析binlog
双主的循环复制问题
解决办法
1.规定两个库的server id必须不同,如果相同,则他们直接不能设定为主备关系
2.一个备库接到binlog并在重放的过程中,生成与原binlog的server id相同的新的binlog
3.每个库在受到从自己的主库发过来的日志后,先判断server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志,所以死循环在这里就断开了
同步延迟的概念
主备延迟,就是同一个事务,在备库执行完的时间和主库执行完的时间的差值,也就是T3-T1
在备库执行
show slave status 命令,返回
second_behind_master,用于表示当前备库延迟了多少秒
如果主备时间不一致,会通过执行SELECT UNIX_TIMESTAMP()函数来获取当前主库的系统时间
网络正常的时候,日志从主库传给备库的时间很短,即T2-T1的值非常小
网络正常下,主备延迟主要是备库接收完binlog和执行完这个事务之间的时间差
主备延迟的原因
主备切换策略
可靠性优先策略,如下图
这个切换流程一般是由专门的HA系统来完成的
可用性优先策略
如果把上面的4,5步调整到最开始执行,也就是不等主备同步,直接把连接切到备库B,让备库B可以读写,那么系统几乎就没有不可用时间了
但无论是用mix模式,还是row模式,都可能会造成数据不一致
在满足数据可靠性的前提下,MySQL高可用系统的可用性,是依赖主备延迟的,延迟的时间越小,在主库故障的时候,服务恢复的时间越端,可用性越高
备库如果执行的不如主库快,时间一长就会延迟很多
解决办法是采用并行复制机制
把只有一个线程的sql_thread,拆成多个线程
coordinator就是原来的sql_thread,只负责读取中转日志和分发事务,真正更新日志的是worker线程
work线程数量最好在8-16之间(32核物理机)
并行复制策略
按表分发策略
如果两个事务更新不同的表,它就可以并行,因为数据存储在表里所以按表分发,可以保证两个worker不会更新同一行
每个work线程有个hash表,hask表的key是 库名.表名,value是队列中有多少事务在修改这个表
如果新来的事务T中涉及修改表t1,跟worker_1队列冲突,也跟worker_2冲突,执行策略如下
大多数情况下会表现很好,但如果所有事务都都涉及某个表会被分配到一个work,就变成单线程复制了
按行分发策略
事务hash表中还需要考虑唯一键,即key应该是 库名+表名+索引a的名字+a的值
假设下面这个事务
coordinator在解析这个语句的binlog时,这个事务的hash表就有三项
按行分发会消耗更多的计算资源,而且有一些下限制条件
如果一个事务需要执行10W行,那么coordinator会等待其他worker执行完,然后退化为单线程模式,等执行完后再恢复成并行模式
mysql5.6采用并行复制策略,以库做维度
MariaDB采用组提交并行复制策略
下面这三组事务是在主库执行的,trx1,trx2,trx3提交的时候,trx4,trx5和trx6是在执行的,第一组事务提交完成的时候,下一组事务很快进入commit状态
MariaDB并行复制策略,要等第一组事务完全执行完,第二组事务才能开始执行,这样吞吐量会下降,尤其是大事务会拖后腿
mysql5.7的复制策略是优化了MariaDB复制策略
再回顾一下完整的两阶段提交策略
mysql5.7并行复制的策略是
binlog组提交的时候,有两个参数
这两个参数故意拉长binlog从write到fsync的时间,以此减少binlog的写盘次数,这两个参数可以故意让主库提交的慢些,也可以让备库执行的快一些,主从延迟的时候可以适当调整这两个参数
mysql5.7.22支持了行模式的并行复制策略
下图是基本的一主多从的结构
虚线是主备关系,A和A'互为主备,从库B,C,D指向的是主库A
一主多从的设置,一般用于读写分离
下图是主库发生故障,主备切换后的结果
一主多从切换完之后,A'就变成新的主库,从库B,C,D也要改连接到A'
正是多了从库B,C,D重新指向的这个过程,所以主备切换的复杂性也增加了
把节点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这个文职的日志继续同步
这个位置是一个大致的位置并不精确
一种获取方式如下
在切换任务时,要先跳过这些错误
GTID 是Global Transaction Identifier,也就是全局事务ID
一个事务在提交的时候生成的,格式是
GTID=server_uuid:gno
基于GTID的切换,也需要找位点,这个工作是在实例A'内部自动完成的,对HA来说是很友好的
在实例B上执行start slave取binlog的逻辑如下
读写分离可以用 客户端直连 或者 proxy方式,他们的对比
目前的趋势是向proxy架构方向发展
无论采用哪种架构,都会碰到读写延迟,客户端再一个更新事务后马上发起查询,如果查询选择的是从库就有可能读到刚更新之前的状态,这种现象被称为 过期读
目前的解决方案包括
强制走主库
对于可以读到旧数据的请求,继续读从库,对于必须拿最新结果的请求就走主库
但如果是金融业务,所有的请求必须是最新的,就等于是放弃读写分离方案了
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
这个是半同步复制,机制如下
如果主库掉电的时候有些binlog还来不及发给从库,就有可能导致数据丢失,如果是semi-sync方案就不会
半同步复制方案的缺点是
如果查询落在响应ack的从库是能读到最新数据
如果落在其他从库,则仍可能读到过期数据
如果是业务高峰,主库位点或GTID更新很快,位点等值判断会一直不成立,可能出现从库一直无法响应,过度等待的问题
等主库位点方案
使用下面命令
select master_pos_wait(file, pos[, timeout]);
执行方式如下
最坏的结果是所有查询都超时1秒然后落在主库上执行
GTID方案
跟等主库位点方案类似,也有一个查询
select wait_for_executed_gtid_set(gtid_set, 1);
执行流程如下
实际使用中,是几个方案混合用的
先对客户端做分类,哪些请求可以接受过期读,哪些不可以
对于不能接受过期的请求,再使用GTID或者等位点的方案
如果极端情况所有GTID查询后都失败,再查询主库,这样主库压力仍会非常大,需要做限流处理
select 1 方案
考虑到下面这个问题,将innodb_thread_concurrrency设置为3,这个是并发线程的上限
此时三个事务都在卡住了,第四个任务做普通的查询会被卡住,但select 1 就不会有问题
连接可能有很多,但并发查询必须设置一个上限,比如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请求的时间
图中这一行表示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的信息