概要:MySQL 5.5 支持单线程模式复制,MySQL 5.6 支持库级别的并行复制,MySQL 5.7 支持事务级别并行复制。结合这个主线我们可以来分析一下MySQL以及社区发展的一个前因后果。
MySQL5.5,对于复制我们可以这样理解:主库有个 dump binlog thread 不停的 dump binlog,然后以event为单位发送给从库 的 iothread,iothread 收到主库传过来的event写入relaylog ,随后sql_thread 读取relaylog 对这些event以事务为单位进行回放。
那么对于MySQL 5.5这个版本,在我们的使用过程中遇到那些问题,或者有那些不便呢?
- 首先DB压力偏大时,从库带来的延迟较大,影响只读业务
由于新硬件的发展,SSD的引入和多core的CPU,master节点的并发处理能力持续提升,slave节点完全按照binlog写入顺序的单线程回放,已完全跟不上master节点的吞吐能力。
在不考虑主从硬件配置差异情况下,延迟大的其根本原因在于:Master压力过大,而Slave是单线程回放日志。那么要解决这个问题,从技术上来说可以把单线程变为多线程,利用并行带来的优势;从业务上来说可以进行拆库,把一些业务线或者功能模块独立出去;更进一步我们可以拆表,把压力分担到多个Master上去。
- 假如我们在不变动业务的情况下,从技术面来解决这个问题有哪些方向呢:
- 社区的解决方案:阿里开源的canal,基于表级别并行同步,可以减小同步延迟时间
- 官方的解决方案:在2011年10月份发布了一个里程碑版本基于schema级别的并行复制[MySQL5.6.3 (multi-threaded slave)],以及基于group Commit的 MySQL5.7版本,最大化还原主库并行度。
MySQL5.6,对于复制我们可以这样理解,主库有个 dump binlog thread 不停的 dump binlog,然后以event为单位发送给从库 的 iothread,iothread 收到主库传过来的event写入relaylog。【随后的事情和MySQL5.5就发生了一些变化】,由coordinator线程来读取relaylog,然后根据不同的db以事务为单位分配到不同的work线程。如果binlog row event操作的是不同的schema的对象,在确定没有DDL和foreign key依赖的情况下,就可以实现并行复制。
MySQL5.7可以说是最大还原了主库上的并行,在基于Group Commit的基础上,所有在主库上能够完成prepared的语句表示没有数据冲突,分配成相同的lastcommitted,就可以在slave节点并行复制。 那么它是如何识别那些事务是一起提交的呢?其实就是在gtid event 中增加了两个字段【int64 lastcommitted;int64 sequencenumber】,当slave的coordinator线程在分发这些event的时候,具有相同lastcommitted 的事务(event的集合)就可以同时发送给不同的work线程,达到并行同步的目的。
小结:就并行复制,按粒度区分有三种策略,粒度从粗到细是按库、按表、按行。 这三个的对比中,并行度越来越大,额外损耗也是。无关大事务不会影响并发度。按照commit_id 的策略,适用范围更广,额外消耗也低。5.7的改进策略并发性更优。但出现大事务会拖后腿。
- 那么我们只有一实例只有一个database,这种情况下我们就只有拆库拆表了:
对于这种情况下,我们可以选择在应用层做分库分表,也可以选择搞个中间层。不同的方案有不同的优劣。
- 应用层具有较好的性能,但是代码耦合在业务,如果后续扩容还需该代码,不能做到平滑扩容拆分,假如有多个业务都需要实现同样的功能,那么会带来重复的工作量,而且工作难度也上升一个台阶。
- 中间件层具有较好的扩展性,低耦合性,如果DB扩容拆分,应用可以做到无感知,无改动。那么也有一些成熟的开源方案,比如MyCAT,Cobar,Atlas,kingshard等。
- 假如我们在不变动业务的情况下,从技术面来解决这个问题有哪些方向呢:
- 其次主从切换时带来的复杂度较大,需要计算position或者重做从库
一般情况下我们的MySQL都是一主多从架构,这样既能给我们提供读写分离、负载均衡的便利,也能给我们提供容灾的能力。但是假如我们的主库挂掉,这时我们会把从库提升为主库,但是在把从库提升为新主的时候带来了架构的微变化。为了还能利用以上便利、提供容灾能力我们还得重新构建这个新主的多个从库。此时问题就来了,我们从库必须知道我当前应该从Master 的那个位置开始复制,也就是说必须拿到Master的position 。为了拿到这个位置我们有两种办法,一种简单粗暴,重做Slave;另一种是通过一些列复杂计算、补回差异数据,算出当前数据和新主数据的差异点,从而得到新主库position,导致HA切换和数据保护带来巨大的挑战。
- MMM架构(Master-Master replication manager for MySQL)
MMM是一套支持双主故障切换和双主日常管理的脚本程序,可以再主库故障时保证热备切换为新主库,并且自动的将从库指向新主。但是这个架构本身不能保证数据的一致性。
- MHA架构(Master High Availability)
MHA目前在MySQL高可用方面是一个相对成熟的解决方案,在自动进行故障切换的过程中,能最大程度上保证数据的一致性,以达到真正意义上的高可用。
那么HMA是如何最大程度保证数据一致的呢?当主库down掉时,MHA试图从宕机的主服务器上保存二进制日志,最大程度的保证数据的不丢失,但这并不总是可行的。如果主库发送down机,日志会出现不同程度的丢失,有个解决办法就是设置半同步复制。MHA在把从提升为主的过程中,会进行一系列日志对比,找到最接近主库的从库提升为新主库,把从库间差异化的数据拿出来进行应用等等。
- GTID (Global Transaction ID)
在MySQL 5.6 以后官方引入了GTID,即在整个集群内部,每个事务都有全局唯一的一个标识,这样一来,当我们主库发送down掉,或者MySQL架构有调整的时候,我们就不用很头疼的去计算position;或者去配置略为复杂的MHA。我们只需要轻轻松松敲个CHANGE MASTER 命令带上AUTO_POSITION就可以了,然后关于MASTER该从哪个binlog开始推送event给Slave这个完全由MySQL来帮我们计算。这个真是DBA们的福音啊。
简单看看,为什么这么GTID这么神奇吧。在MySQL内部帮我们记录着 gtidpurged 和gtidexecuted 两个集合。顾名思义,gtidexecuted 代表的时当前已经执行过的GTID的集合;一般情况下我们binlog不可能永久保存,那么gtidpurged代表的就是当前binlog已经没有的GTID集合,它是gtidexecuted的子集。我们知道在事务是不能跨binlog存在的,意味着每个binlog都会有一个完整的事务集合,同样每个binlog文件的 header 部分,也都存放着这个binlog以前的 gtidexecuted 集合。我们的Slave 在应用Binlog的时候都会记录自己当前已经执行过的最后一个事务GTID,那么我们在切换主库的时候,Slave就会把这个ID给带上,然后Master端就会拿到这个GTID和自己当前的gtidexecuted、gtidpurged 集合进行对比,从而给到Slave一个合理的解释。
- MMM架构(Master-Master replication manager for MySQL)
OK,到这里MySQL从5.5的单线程复制,到5.6基于Schema级别的复制,再到5.7最大化还原主库的并行就接近尾声了。同时在这期间我们还给出了一些社区上、或者非技术上的解决方案。