最终一致性:在一个主备关系中,每个备库接收主库的binlog并执行,正常情况下,只要主库执行更新生成的所有binlog,都可以传到备库并被正确地执行,备库就能 达到跟主库一致的状态,这就是最终一致性。
而MySQL提供高可用只有最终一致性是不够的,咱们来看一下示例,双M结构的主备切换流程图
1.主备延迟:
主备切换可能是一个主动运维操作,软件升级、主库所在机器按计划下线等,也可能是被动操作,比如主库所在机器掉电。
接下来看看主动切换场景,在介绍之前,我们说一下同步延迟,与数据同步有关的时间点主要包括以下三个:
1.主库A执行完成一个事务,写入binlog,把这个时刻记T1。
2.之后传给备库B,备库B接收完这个binlog时刻记T2。
3.备库B执行完这个事务,这个时刻记T3
主备延迟就是,同一个事务在备库执行完的时间和主库执行完时间的差值,也就是t3-t1。
备库上执行show slave status,它返回结果里面会显示seconds_behind_master,用于表示当前备库延迟了多少秒。
seconds_behind_master 也是这样计算的,每个事务binlog都有时间用于记录主库的写入时间,备库取出当前正在执行的事务时间字段的值,计算它与当前系统时间的差值,得到seconds_behind_master 。
1.1 主备延迟的来源
1.有些部署情况下,备库所在的机器性能比主库所在的机器性能差。
一般情况下这么部署是因为备库没有请求可以用差一点的机器,或者主库有几台机器,而备库集中在一台上。但实际上,更新过程中也会触发大量的读操作。所以,当备库主机上的多个备库都在争抢资源的 时候,就可能会导致主备延迟了。
当然,这种部署现在比较少了。因为主备可能发生切换,备库随时可能变成主库,所以主备库选 用相同规格的机器,并且做对称部署,是现在比较常见的情况。
2.备库压力大
一般想法是,主库提供写能力,备库提供读能力,那一些运营后台分析的语句,不能影响正常业务,所以只能在备库上跑。
有不少这样的情况。由于主库直接影响业务,大家使用起来会比较克制,反而忽视了备 库的压力控制。结果就是,备库上的查询耗费了大量的CPU资源,影响了同步速度,造成主备延迟。
遇到这种情况,怎么处理呢?
1.一主多从。除了备库外,可以多接几个从库,让这些从库来分担读的压力。
2.通过binlog输出到外部系统,比如Hadoop这类系统,让外部系统提供统计类查询的能力。
3.大事务导致主备延迟
大事务就是主库必须等待事务执行完才写入binlog,再传给备库,所以,一个主库上执行语句10分钟,那这个事务很可能就会导致从库延迟10分钟。
不知道你所在公司的DBA有没有跟你这么说过:不要一次性地用delete语句删除太多数据。其实,这就是一个典型的大事务场景。DBA团队就要求你删除数据的时候,要控制每个事务删除的数据量,分成多次删除。
另一种典型的大事务场景,就是大表DDL。这个场景,我在前面的文章中介绍过。处理方案 就是,计划内的DDL,建议使用gh-ost方案
gh-ost(GitHub Online Schema Migrations)是一个开源工具,用于在生产环境中执行在线数据库模式迁移。它由GitHub开发的,旨在解决传统DDL(定义语言操作可能导致的长时间锁定和中断的问题。
**4.造成主备延迟还有一个大方向的原因,就是备库的并行复制能力,**之后会详细讲解这里
保证高可用性如果主库出现故障,那么就需要主备切换,而又因为主备延迟的存在,主备切换就有不同的策略。
2.主备切换策略
2.1 可靠性优先策略
在如上这个图,双M主备切换流程中,如果是可靠性优先策略则是这样的流程:
1.判断备库B现在的seconds_behind_master,如果小于某个值(比如5秒)继续下一步,否则持续重试这一步;
2.继续的话把主库A改成只读状态,即把readonly设置为true;
3.判断备库B的seconds_behind_master的值,直到这个值变成0为止;
4.把备库B改成可读写状态,也就是把readonly设置为false;
5.把业务请求切到备库B。
这个切换流程,一般是由专门的HA系统来完成的,我们暂时称之为可靠性优先流程。
可以看到这个切换过程中是有不可用时间段的,因为在步骤2之后,主库A和备库B都处于 readonly状态,也就是说这时系统处于不可写状态,直到步骤5完成后才能恢复。
在这个不可用状态中,比较耗费时间的是步骤3,可能需要耗费好几秒的时间。这也是为什么需 要在步骤1先做判断,确保seconds_behind_master的值足够小。
试想如果一开始主备延迟就长达30分钟,而不先做判断直接切换的话,系统的不可用时间就会 长达30分钟,这种情况一般业务都是不可接受的。
当然,系统的不可用时间,是由这个数据可靠性优先的策略决定的。你也可以选择可用性优先的 策略,来把这个不可用时间几乎降为0。
2.2 可用性优先策略
可以不等数据同步,发生主机问题直接切换,将主库A设置为只读,然后把备库B改成可读写状态,把业务请求到B,但是这种切换可能会出现数据不一致问题。
因为有可能主库事务执行完但还没来的及备份就导致切换主库,这段时间请求时就会出现数据不一致,直到主库启动起来开始同步。
在满足数据可靠性的前提下,MySQL高可用系统的可用性,是依赖于主 备延迟的。延迟的时间越小,在主库故障的时候,服务恢复需要的时间就越短,可用性就越高。
一般是使用可靠性策略,因为对数据来说可靠是最重要的。
3.备库为什么延迟好几个小时?
如果备库执行日志的速度持续低于主库生成日志的速度,那这个延迟就有可能成了小时级别。而且对于一个压力持续比较高的主库来说,备库很可能永远都追不上主库的节奏。
这就需要并行复制能力,日志在备库上的执行,就是备库上sql_thread更新数据(DATA)的逻辑。如果是用单线程的 话,就会导致备库应用日志不够快,造成主备延迟。
MySQL5.6版本之前是单线程复制能力,由此在主库并发高、TPS高时就会出现严重的主备延迟问题。MySQL5.7之后支持了多线程并行复制能力,我们来看下如何实现的。
coordinator:就是原来的sql_thread, 不过现在它不再直接更新数据了,只负责读取中转日志和分发事务。真正更新日志的,变成了worker线程。
work线程的个数:由参数 slave_parallel_workers决定的。根据我的经验,把这个值设置为8~16之间最好(32核物理机的 情况)毕竟备库还有可能要提供读查询,不能把CPU都吃光了。
那么还是多线程一直会出现的问题,如果不加处理,事务随意发给不同的work线程,则会出现数据不一致问题。
所以,coordinator在分发的时候,需要满足以下这两个基本要求:
-
不能造成更新覆盖。这就要求更新同一行的两个事务,必须被分发到同一个worker中。
-
同一个事务不能被拆开,必须放到同一个worker中。
作者服务的行业因为遇到了主备严重延迟,因为单线程的原因,而那时候MySQL还没有发布多线程版本,作者就写了两种并发复制思路,在这里讲一下思路,方便理解MySQL官方版本并行复制策略的迭代。
有表并行方式和行的并行方式
3.1 按表分发策略
按表分发,如果两个事务更新不同的表,它们就可以并行。如果更新同一张表或者是跨表就会出现冲突等待或者是放入同一worker线程。咱们看下示例:
这里边每个worker线程对应一个hash表,用于保存当前正在这个worker的"执行队列"里的事务所涉及的表。hash表的key是一个库和表名,value是一个数字,表示队列中有多少个事务修改这个表。
在有事务分配给worker时,事务里边涉及的表会加入到这个hash表里面,worker执行完成后,这个表会被从hash表中去掉。
图中,hashtable1表示现在worker_1的"待执行事务队列"里,db1.t1表中有4个要修改的事务,表db1.t2有一个要修改的事务,hashtable2中worker_2有一个t3表的事务等待执行。
此时,假设在图中的情况下,coordinator从中转日志中读入一个新事务T,这个事务修改的行涉及到表 t1和t3。这时就会跟work1和worker2有冲突。
1.由于事务T中涉及修改表t1,而worker_1队列中有事务在修改表t1,事务T和队列中的某个事 务要修改同一个表的数据,这种情况我们说事务T和worker_1是冲突的。
2.按照这个逻辑,顺序判断事务T和每个worker队列的冲突关系,会发现事务T跟worker_2也冲 突。
3.事务T跟多于一个worker冲突,coordinator线程就进入等待。
4.每个worker继续执行,同时修改hash_table。假设hash_table_2里面涉及到修改表t3的事务先执行完成,就会从hash_table_2中把db1.t3这一项去掉。
5.这样coordinator会发现跟事务T冲突的worker只有worker_1了,因此就把它分配给worker_1。
6..coordinator继续读下一个中转日志,继续分配事务。
也就是说,每个事务在分发的时候,跟所有worker的冲突关系包括以下三种情况:
1.如果跟多于一个worker冲突,coordinator线程就进入等待状态,直到和这个事务存在冲突关 系的worker只剩下1个;
2.如果只跟一个worker冲突,coordinator线程就会把这个事务分配给这个存在冲突关系的 worker。
3.如果跟所有worker都不冲突,coordinator线程就会把这个事务分配给最空闲的woker;
这个按表分发的方案,在多个表负载均匀的场景里应用效果很好。但是,如果碰到热点表,比如 所有的更新事务都会涉及到某一个表的时候,所有事务都会被分配到同一个worker中,就变成单 线程复制了。
3.2 按行分发策略
按行分发的话主要是不修改一个行的数据就可以并行,它的处理方式和按表差不多,不同的是hash的key则需要 库名.表名.主键id,但是其实有这些还不够,如果要是有唯一索引的列,则要把唯一索引的列加上。
并且这个设计的要求会很多,要求binlog必须是row格式,能给记录修改前和后的很多信息,方便这个策略计算处理不出问题。
还要求不能有外建,外建情况就无法处理冲突了
必须有主键。
虽然上面的三种情况都支持线上的配置,但是如果数据量很大因为计算要求高导致耗费内存和cpu,所以就需要考虑退化到单线程。
作者的这两周方案非常非常不错,也看得出内功深厚。接下来有了上面基础就好介绍下面的MySQL的并行复制策略方案了。
4.MySQL并行复制策略
4.1 MySQL5.6版本
官方MySQL5.6版本,支持了并行复制,只是支持的粒度是按库并行。这个很好理解用于决定分发策略的hash表里,key就是数据库名。、
这个策略的并行效果,取决于压力模型。如果在主库上有多个DB,并且各个DB的压力均衡,使
用这个策略的效果会很好。 相比于按表和按行分发,这个策略有两个优势:
-
构造hash值的时候很快,只需要库名;而且一个实例上DB数也不会很多,不会出现需要构 造100万个项这种情况。
-
不要求binlog的格式。因为statement格式的binlog也可以很容易拿到库名。
但是,如果你的主库上的表都放在同一个DB里面,这个策略就没有效果了;或者如果不同DB的热点不同,比如一个是业务逻辑库,一个是系统配置库,那也起不到并行的效果。
这个策略用的人不多,毕竟不能为了这个策略而去需要特地移动数据。
4.2 MariaDB的并行复制策略
MariaDB是一个开源的关系型数据库管理系统(RDBMS),它是MySQL的一个分支。它由MySQL的原始开发者之一Michael Widenius创建,旨在提供一个自由、开放和稳定的数据库解决方案。
还记得redo log组提交(group commit)优化, 而MariaDB的并行复制策略利用的就是这个特性:
-
能够在同一组里提交的事务,一定不会修改同一行;
-
主库上可以并行执行的事务,备库上也一定是可以并行执行的。
在实现上,MariaDB是这么做的:
-
在一组里面一起提交的事务,有一个相同的commit_id,下一组就是commit_id+1;
-
commit_id直接写到binlog里面;
-
传到备库应用的时候,相同commit_id的事务分发到多个worker执行;
-
这一组全部执行完成后,coordinator再去取下一批。
当时,这个策略出来的时候是相当惊艳的。因为,之前业界的思路都是在"分析binlog,并拆分到 worker"上。而MariaDB的这个策略,目标是"模拟主库的并行模式"。
但是这个没有实现真正的模拟主库并发度这个目标,因为它必须在一组事务提交后才能准备下一组事务,正常是这组事务处于commit时,下一组应该是"执行中"的状态。
假设了三组事务在主库的执行情况,你可以看到在trx1、trx2和trx3提交的时 候,trx4、trx5和trx6是在执行的。这样,在第一组事务提交完成的时候,下一组事务很快就会进 入commit状态。
而按照MariaDB的并行复制策略,备库上的执行效果如图下所示。
可以看到,在备库上执行的时候,要等第一组事务完全执行完成后,第二组事务才能开始执行,这样系统的吞吐量就不够。
另外,这个方案很容易被大事务拖后腿。假设trx2是一个超大事务,那么在备库应用的时 候,trx1和trx3执行完成后,就只能等trx2完全执行完成,下一组才能开始执行。这段时间,只有 一个worker线程在工作,是对资源的浪费。
不过即使如此,这个策略仍然是一个很漂亮的创新。因为,它对原系统的改造非常少,实现也很 优雅。
4.3 MySQL 5.7的并行复制策略
在MariaDB并行复制实现之后,官方的MySQL5.7版本也提供了类似的功能,由参数slave-parallel-type来控制并行复制策略:
-
配置为DATABASE,表示使用MySQL5.6版本的按库并行策略;
-
配置为 LOGICAL_CLOCK,表示的就是类似MariaDB的策略。不过,MySQL 5.7这个策略,针对并行度做了优化。
同时处于"执行状态"的所有事务,是不能并行的。因为,这里面可能有由于锁冲突而处于锁等待状态的事务。如果这些事务在备库上被分配到不同 的worker,就会出现备库跟主库不一致的情况。而上面提到的MariaDB这个策略的核心,是"所有处于commit"状态的事务可以并行。事务处于 commit状态,表示已经通过了锁冲突的检验了。
而它的优化则考虑了两阶段提交。
其实,不用等到commit阶段,只要能够到达redo log prepare阶段,就表示事务已经通过锁冲突的检验了。
因此,MySQL 5.7并行复制策略的思想是:
1.同时处于prepare状态的事务,在备库执行时是可以并行的。
- 处于prepare状态的事务,与处于commit状态的事务之间,在备库执行时也是可以并行的。
binlog的组提交的时候,有两个参数:
-
binlog_group_commit_sync_delay参数,表示延迟多少微秒后才调用fsync;
-
binlog_group_commit_sync_no_delay_count参数,表示累积多少次以后才调用fsync。
这两个参数是用于故意拉长binlog从write到fsync的时间,以此减少binlog的写盘次数。在MySQL 5.7的并行复制策略里,它们可以用来制造更多的"同时处于prepare阶段的事务"。这样就增加了 备库复制的并行度。
也就是说,这两个参数,既可以"故意"让主库提交得慢些,又可以让备库执行得快些。在MySQL 5.7处理备库延迟的时候,可以考虑调整这两个参数值,来达到提升备库复制并发度的目的。
4.4 MySQL 5.7.22的并行复制策略
MySQL增加了一个新的并行复制策略,基于WRITESET的并行复制。
在MySQL中,WRITESET(集)是一个用于记录事务中所有修改过的数据的数据结构。它用于实现事务的隔离性和持久性。当一个事务开始时,MySQL会创建WRITESET,用于跟踪该事务所修改的数据。
相应地,新增了一个参数binlog-transaction-dependency-tracking,用来控制是否启用这个新策略。这个参数的可选值有以下三种。
1.COMMIT_ORDER,表示的就是前面介绍的,根据同时进入prepare和commit来判断是否可以并行的策略。
2.WRITESET,表示的是对于事务涉及更新的每一行,计算出这一行的hash值,组成集合 writeset。如果两个事务没有操作相同的行,也就是说它们的writeset没有交集,就可以并行。
3.WRITESET_SESSION,是在WRITESET的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序。
当然为了唯一标识,这个hash值是通过"库名+表名+索引名+值"计算出来的。如果一个表上除了 有主键索引外,还有其他唯一索引,那么对于每个唯一索引,insert语句对应的writeset就要多增 加一个hash值。
你可能看出来了,这跟我们前面介绍的基于MySQL 5.5版本的按行分发的策略是差不多的。不 过,MySQL官方的这个实现还是有很大的优势:
-
writeset是在主库生成后直接写入到binlog里面的,这样在备库执行的时候,不需要解析 binlog内容(event里的行数据),节省了很多计算量;
-
不需要把整个事务的binlog都扫一遍才能决定分发到哪个worker,更省内存;
-
由于备库的分发策略不依赖于binlog内容,所以binlog是statement格式也是可以的。
对于"表上没主键"和"外键约束"的场景,WRITESET策略也是没法并行的,也会暂时退化 为单线程模型。
哈哈,内容性的有点多,大家可以慢慢消化,本节由张磊老师的《MySQL 实战 45 讲》学习笔记,学习完还是受益非浅的!