各版本 MySQL 并行复制的实现及优缺点
MySQL 并行复制已经是老生常谈,笔者从 2010 年开始就着手处理线上这个问题,刚开始两三年也乐此不疲分享,现在再提这个话题本来是难免 “炒冷饭” 嫌疑。
最近触发再谈这个话题,是因为有些同学觉得 “5.7 的并行复制终于彻底解决了复制并发性问题”, 感觉还是有必要分析一下。大家都说没有银弹,但是又期待银弹。。
既然要说 5.7 的并行复制,干脆顺手把各个版本的并行复制都说明一下,也好有个对比。便是本次分享的初衷。 【背景】 一句话说完,因为这几年太多这样文章了, 就是 MySQL 一直以来的备库复制都是单线程 apply。
【解决基本思路】 改成多线程复制。
备库有两个线程与复制相关:io_thread 负责从主库拿 binlog 并写到 relaylog, sql_thread 负责读 relaylog 并执行。 多线程的思路就是把 sql_thread 变成分发线程,然后由一组 worker_thread 来负责执行。 几乎所有的并行复制都是这个思路,有不同的,便是 sql_thread 的分发策略。 而这些策略里面又分成两类:利用传统 binlog 格式、修改 binlog。 使用传统的 binlog 格式的几类,由于 binlog 里面的信息就那些,因此只能按照粒度来分,也就是:按库、按表、按行 另外有两个策略是修改了 binlog 格式的,在 binlog 里面增加了别的信息,用于体现提交分组。 下面我们分别介绍几个并行复制的实现。 【5.5】 MySQL 官方 5.5 是不支持并行复制的。但是在阿里的业务需要并行复制的年份,还没有官方版本支持,只好自己实现。而且从兼容性角度说,不修改 binlog 格式,所以采用的是利用传统 binlog 格式的改造。 阿里的版本支持两种分发策略:按表和按行。 前情说明,由于 MySQLbinlog 日志还有用于别的系统的要求,因此阿里的 binlog 格式都是 row---- 这也给并行复制的实现减少了难度。 按表分发策略:row 格式的 binlog,每个 DML 前面都是有 Table_map event 的。因此很容易拿到库名 / 表名。一个简单的思路是,不同表的更新之间是不需要严格按照顺序的。 因此按照表名 hash,hash key 是 库名 表名,相同的表的更新放到同一个 worker 上。这样就保证同一个表的更新顺序,跟主库上是一样的。 应用场景:对于多表更新的场景效果特别好。缺点是反之的,若是热点表更新,则本策略无效。而且由于 hash 表的维护,性能反而下降。 按行分发策略:row 格式的 binlog 中,也不难拿到主键 ID. 有同学说如果没有主键怎么办,答案是 "起开,现在谁还没主键:)"。好吧,正经答案是没有主键就不支持这个策略。 同样的,我们认为不同行的更新,可以无序并发的。只要保证同一行的数据更新,在备库上的顺序与主库上的相同即可。 因此按照主键 id hash,所以这个 hash key 更长,必须是 库名 表名 主键 id。相同行的更新放到同一个 worker 上。
需要注意的是,上面的描述看上去都是对单个 event 的操作,实际上并不能!因为备库可能接受读,因此事务的原子性是要保证的,也就是说,对于涉及多个更新操作的事务,每次用于决策的不是一个 hash key,而是一组。 应用场景:热点表更新。缺点,hash key 计算冲突的代价大。尤其是大事务,计算 hash key 的 cpu 消耗大,而且耗内存。这需要业务 DBA 做判断得失。 【5.6】 官方的 5.6 支持的是按库分发。有了上面的背景,大家就知道,这个 feature 出来以后,在中国并没有什么反响。
但是这个策略也要说也是有优点的:
1、对于可以按表分发的场景,可以通过将表迁到不同的库,来应用此策略,有可操作性
2、速度更快,因为 hash key 就一个库名
3、不要求 binlog 格式,大家知道不论是 row 还是 statement 格式,都是能够轻松获取库名的。 所以并不是完全没有用的。还是习惯问题。 【MariaDB】
MariaDB 的并行复制策略看上去有好几个选项,然而生产上可用的也就是默认值的 CONSERVATIVE。
由于 maraiaDB 支持多主复制,一个 domain_id 字段是用来标示事务来源的。如果来自于不同的主,自然可以并行(这个其实也是通用概念,还得业务 DBA 自己判断)。
对于同一个主库来的 binlog,用 commit_id 来决定分组。
想法是这样的:在主库上同时提交的事务设置成相同的 commit_id。在备库上 apply 时,相同的 commit_id 可以并行执行,因为这意味着这些事务之间是没有行冲突的(否则不可能同时提交)。
这个思路跟最初从单线程改成多线程一样,个人认为是划时代的。
但是也并没有解决了所有的问题。这个策略最怕的是,拖后腿事务。
设想一下这个场景,假设某个 DB 里面正在作大量小更新事务(比如每个事务更新一行),这样在备库就并行得很欢乐。
然后突然,在同一个实例,另外一个库下,或者同一个库的另外一个跟目前的更新无关的表,突然有一个 delte 操作删除了 10w 行。
delete 事务在提交的时候,跟当时一起提交的事务都算同一个 commit_id。假设为 N.
之后的小事务更新提交组 commit_id 为 N 1。
到备库 apply 时,就会发现 N 这个组里面,其他小事务都执行完了,线程进入空闲状态,但是不能继续执行 N 1 这个 commit_id 的事务,因为 N 里面还有一个大事务没有执行完成,这个我们认为是拖后腿的。
而基于传统 binlog 格式的上面三个策略,反而没有这个问题。只要是策略上能够判断不冲突,大事务自己有个线程跑,其他事务继续并行。 【5.7】
MySQL 官方 5.7 版本也是及时跟进,先引入了上述 MariaDB 的策略。当然从版权安全上,oracle 是不会允许直接 port 代码的。 然后官方 5.7 的新版本在此之上继续优化。 实际上按组直接分段这个策略略显粗暴。实际上事务提交并不是一个点,而是一个阶段。至少我们可以分成:准备提交、提交中、提交完成。
这三个阶段都是在事务已经完成了主要操作逻辑,进入 commit 状态了。
同时进入 “提交中” 状态的算同一个 commit_id. 但是实际上,在任意时刻,处于” 准备提交” 的事务,与 “提交中” 的事务,也是可以并行的。但是明显他们会被分成两个不同的 commit_id。
这意味着这个策略还有提升并发度的空间。
我们来看一下两种策略的对比差别。
假设主库有如下面示意图的事务序列。每个事务提交过程看成两个阶段,prepare ... commit. 分别给不同的编号。其中 commit 对应的数字是自然数递增,sequence_no。而 prepare 是对应的数字是 X 1,这个 X 表示的是当前已经提交完成的 sequence_no。 trx1 1…..2 trx2 1………….3 trx3 1…………………….4 trx4 2………………………….5 trx5 3………………………………..6 trx6 3………………………………………………7 trx7 6……………………..8
分析: 在 MariaDB 的策略里面,并发执行序列如下: trx1, trx2, trx3 ----group 1 trx4 -----group 2 trx 5, trx6 ----group 3 trx 7 ----group 4 每个 group 执行完成后,下一个 group 才可以开始。 完全执行完成的时间是每个 group 的最大事务时间之和,即 trx3 trx4 trx6 trx7。 因此,如果某个 group 里面有一个很大的事务,则整个序列的执行时间就会被拖久。
再来看 5.7 的改进策略: 虽然也是 group1 先启动,但是在 trx1 完成后, trx4 就可以开始执行; 同样的,trx7 可以在 trx4 执行完成后就开始执行,与 trx5 和 trx6 并发。 因此可以说上面这个例子中,备库 apply 过程完全达到了主库执行的并发度。 但是对于大事务,比如 trx2 commit 非常久的情况,仍然存在拖后腿的问题。 【小结】 我们看到,就并行复制,有 5 种策略。 按粒度区分的三个策略,粒度从粗到细是按库、按表、按行。 这三个的对比中,并行度越来越大,额外损耗也是。无关大事务不会影响并发度。
按照 commit_id 的两个策略,适用范围更广,额外消耗也低。 5.7 的改进策略并发性更优。但出现大事务会拖后腿。 另外,很重要的一点,5.7 的策略目的是 “模拟主库并发”,所以对于主库单线程更新是无加速作用的。而基于冲突的前三个策略,若满足并发条件,会出现备库比主库执行速度快的情况。这种需求在搭备库或者延迟复制的场景中可能触发。
实际上还是老话,没有万用的策略。策略的选择取决于应用场景,这是架构师的工作之一。
PS:具体 5.7 的实现原理可参考我们团队的 @印风 同学的博客 http://mysqllover.com/?p=1370 (最后一个例子的 case 也从此摘录) http://dinglin.iteye.com/blog/2272079 sohu-dba 本文转载自:微信公众账号 - SOHU-DBA,版权归原作者所有! |
|
声明:文章版权归原作者所有 部分文章转自互联网 如有侵权请联系
[邮箱地址] 删除
|