今年2月下旬以来,本文作者(我)研究和改造了Percona-MySQL-8.0.18-9的若干新功能实现,主要是MySQL Group Replication(MGR)和clone等功能,并且在Percona-MySQL-8.0.18-9在分布式事务容灾方面填补了功能空白,修复了其漏洞缺陷,以及做了其它若干针对昆仑分布式数据库整体规划的功能开发。现在把我的一些基于Percona-MySQL-8.0.18-9的发现和想法分享一下。本文不打算完整介绍MySQL8.0的任何新功能,因为网上已经有其他同行写的这方面的文章若干篇,并且最权威最完整的介绍始终是MySQL官方文档。本文假设读者已经熟知这些概念和功能,我着重介绍我为了昆仑分布式数据库的需求而对Percona-MySQL-8.0.18-9的MGR和clone功能做的探索和发现以及对MGR在分布式事务容灾方面的改进。


尽管仍然在分布式事务容灾恢复处理方面有诸多漏洞和功能空白,MGR single primary mode(单主模式)具备非常好的数据一致性保障以及容灾能力,并保持了很高的性能,适合在生产实践中广泛使用。MGR多主模式(multi-primary mode)在写入负载很重的时候,会由于多个节点的数据写入冲突导致的大量事务回滚而产生比较大的TPS和延时的抖动,所以只适合写入非常少,主要是读负载的场景,比如少量参数配置的存储,类似zookeeper的使用场景。多主模式下如果为了避免写入冲突而在应用层巧妙的安排每个主节点写入或者修改的数据集合使它们没有交集,那么应用层的开发复杂度会大大提升,并且对已有应用来说也无法自动适应,所以相信不会有多少用户这么做的。最后,无论单主模式还是多主模式还是传统的异步复制模式,一个MySQL集群也只能处理若干个TB以内的数据,因为集群中所有节点都存储了完全相同的N份数据,集群的数据处理能力仍然受限于每个节点的数据处理能力,而单节点的能力又受限于服务器节点的计算资源以及硬件成本等因素,并不能持续增加其数据存储和处理能力。因此,当数据规模增大到一定规模以上之后,还是需要使用分布式数据库集群。


在昆仑分布式数据库中,我们将会使用MGR单主模式,每个storage shard就是一个MGR单主集群。全部数据存储在多个storage shard中,计算节点会确保不同shard的数据没有交集。


MGR 自动集群管理

MGR的一个优点是其自动的集群管理能力。这不仅包括我们常说的自动的主备切换能力,还包括自动化的组员身份(group membership)管理和基于分布式恢复(distributed recovery)技术的provisioning能力,也就是可以在集群运行期间自动增加备机节点。

先说主备切换:集群所有节点通过定期运行paxos协议交换节点状态,MGR的每个节点就可以自动发现其他节点的消失或者新增并且在集群内同步这一信息。如果发现主节点消失了,那么集群中当前所有节点就会各自独立运行选主算法从而选出一致的新的主节点并在集群内同步新主决议。如果发现若干个节点消失导致失去quorum了,那么集群会自动转为只读防止脑裂。这就把TDSQL或者各种分布式中间件多年以来使用zookeeper/etcd做的大量工作以更高的质量和可靠性给完成了。要知道在使用和维护zookeeper方面,没有谁敢说自己没踩过坑的。MGR通过在MySQL节点集群中运行paxos协议,避免了对外部paxos集群的依赖,可以减少运维工作量并且减少错误源。同时MGR把paxos与主备复制做了有机的融合,从而实现了事务binlog的原子广播,大大提升了集群的容灾能力。这部分后面会介绍。

通过MGR的组成员管理,MGR集群可以跟踪节点的加入和离开,并且在集群内所有节点同步这些状态。在主节点离开后自动执行主选举;在集群因为网络分区或者节点宕机而失去quorum后,可以自动把主节点也设置为只读,避免脑裂。由于组员的加入或者离开而产生的每一个组成员集合,在MGR中被称为一个view,view的每一次变化就是一个view change事件,代表了一次组员加入或者离开。该事件被记录在binlog中,所以组拓扑结构变化是持久和全局一致的信息。因此view change事件在MGR 的分布式恢复中被用作group_replication_recovery channel的数据同步的中止点。

通过分布式恢复(distributed recovery)我们可以用一条SQL语句就轻松加入一个新的备机节点,或者把一个老备机节点带到最新状态。这个功能意味着两个复杂而重要的事情被自动化和简化了。

首先,我们可以使用它来做数据全量备份,代替Percona Xtrabackup — 只要新做一个DB实例,作为备机加入MGR集群,执行一条语句‘start group_replication’,完成distributed recovery后停机后,这个实例就具有了主节点当前的全部数据。然后归档存储其数据目录即可当作全量备份。

其次,重做备机这个DBA常用操作,现在可以非常简单地用一条命令就完成了。当然,在MGR单主模式下,相信DBA也不再需要经常重做备机了—过去重做备机通常是MySQL DBA的大杀器和终极武器——当备机复制线程由于各种原因卡住走不下去时,当主备断连后备机无法连上主继续复制时(比如该备机所需的主的部分binlog 被purge了),当使用了MyISAM表然后备机非正常退出导致备机数据错乱时,或者当备机复制太慢与主节点数据版本的距离越来越远已经没有希望追上主的时候,DBA都需要重做备机。可以说对于一个管理着几百个MySQL集群的DBA来说,重做备机是他的日常工作。如果哪天他没有重做任何一个备机,那才是一个怪事,他一定会想会不会是哪里发生了什么问题。以前DBA重做备机使用的工具基本都是Percona Xtrabackup配合 mysqlbinlog工具去追(apply) binlog;在TDSQL中还有更炫酷的玩法来追binlog — 把从主机复制过来的binlog当作relay log,对使用全量数据做的DB节点做一个change master to操作,把它模拟成一个备机去追(apply)这些binlog(这还需要对MySQL server 开发一处特定功能)。

现在所有的这些奇技淫巧现在都不需要了,用户只需要新建一个空的DB实例,配置好MGR的参数,然后运行‘start group_replication’ 即可。MGR会做distributed recovery:如果发现备机全新或者与主机最新binlog 距离太远或者任何组节点都没有新节点所需的binlog,就克隆(clone)一个donor的全量InnoDB数据(这一步类似于以前使用Persona Xtrabackup做全量复制),然后使用传统的异步复制与MGR相结合的方式来重放事务 binlog 完成DB实例的recovery。这一步类似于以前的追binlog,但是分为两小步,先做异步复制追binlog到本节点加入时刻(view change标识),然后重放前面两个阶段期间收到的binlog。

与业界以前的做备机节点的方法不同的是,MySQL MGR的distributed recovery 更加高效,更加用户友好。比如binlog和clone过程都可以多线程并发传输binlog,还都可以压缩传输binlog和InnoDB数据文件,还自带限流配置避免耗尽磁盘和网络带宽而影响业务请求处理。同时,clone的过程中完全不阻塞DML语句(会阻塞DDL语句),也就是不像Percona Xtrabackup那样需要在全量备份期间一段时间内做FTWRL 操作来锁住一切DML 写操作以及正要提交的事务,因此对业务请求处理的影响很小。之所以能避免FTWRL,得益于MySQL-8.0 的transactional data dictionary(数据字典存储在InnoDB表中),以及所有元数据表都存储在InnoDB中的现实。不过MySQL-8.0的clone功能与Percona Xtrabackup相比的劣势就是不支持其它存储引擎,只支持InnoDB,它是绑定innodb的。


MGR的事务原子广播

MGR的另一大优点是它基于paxos协议实现的原子广播功能,也就是确保了任何一个事务在master上面提交时,MGR会先确保简单多数(quorum)的节点都收到了这个事务的完整的binlog,然后各个节点才真正开始本地提交。这样就避免了以前异步复制模式下的诸多主备容灾问题,这些问题本质上都是因为主节点crash时刻,那些正在提交的事务的binlog在主和一部分或者全部备节点上面可能不一致。

比如主节点crash时候一个正在提交的事务T,有的备节点收到了完整的T的binlog,有的备节点完全没有收到T的binlog,另一些备节点收到了T的一部分 binlog。如果T是XA事务,则MySQL-5.7中XA事务不完整的情况下如果主备断链,备机的事件分发线程在一些条件下会持续等待该事务剩余的binlog事件却不能回滚该事务并重新执行它,导致备机复制彻底卡死,这个备节点就无法使用了。如果此备机此刻要被选为主节点时,这个备机就会因为无法完成stop slave而无法转为主节点,从而导致这个storage shard不可写入。

这类问题我在TDSQL中都已经解决了。而在MySQL 8.0中这类问题已经被MGR的原子广播能力彻底解决了。


MGR对备机复制性能的提升

MySQL-5.7的备机复制基于逻辑时钟算法,有一个潜在的缺陷不易察觉,就是备机复制的并发度受限于主机的客户端连接数。这是因为logical-clock算法中任意时刻其序列号不大于当前系统全局序列号的事务总数无法超过客户端连接数。比如如果主机始终只有20个连接在执行事务,那么备机的并发度一定不会超过20,这就导致如果备机需要重放少量连接产生的大量binlog,就会很慢。另外如果表没有主键和唯一索引那么备机复制遇到删除或者更新大量行的事务,其复制会很慢,导致备机落后于主机越来越多,然后就没有备机可以迅速做主备切换了。一旦主节点故障,那么集群一段比较长的时间内就不可写入了。每到此时DBA就会很紧张,有时对于重要业务会新做一个备机节点来压压惊。

MGR基于write set可以轻松判别事务的依赖关系,从而做到只要两个事务不冲突就可以并发执行。如果事务T1和T2先后修改了同一行R,那么事务T2依赖于T1,也就是只有T1执行完毕后才可以执行T2。如果T1和T2的writ set没有交集,那么T1和T2就没有依赖关系,就可以并发执行。MGR甚至可以可选地并发执行同一个连接上面产生的不冲突的事务(binlog_transaction_dependency_tracking = writes_set),不过考虑到这样做并不符合时序逻辑,特别是如果备机会用做备机读的话这么做并不适合,还是推荐使用binlog_transaction_dependency_tracking=write_set_session。基于write_set和write_set_session的备机复制完全摆脱了客户端并发数的限制,可以达到非常高的复制性能。再加上MGR的强制表有主键或者唯一索引的要求,这就使得备机落后于主机无法追上这个问题被彻底解决了。可以预期生产系统中备机与主机的距离基本上总可以保持很近,这就让主备切换在任何情况下都可以快速实施。不会出现没有比较新的备机可以立即使用导致无法迅速完成主备切换的问题。再加上完全使用事务存储引擎和MGR的原子广播能力,可以预期以后DBA很少需要重做备机了,一切都自动化之后不知道DBA们是该高兴呢还是高兴呢 ;)

MGR single primary的代价

当然,任何好东西都有代价。使用MGR single primary模式比传统的binlog复制模式的性能开销和延时略大,并且有一些功能约束。这些额外开销主要是因为certify过程导致的—除了需要传输事务的binlog之外,certify在主节点上还需要计算和传输write set,slave端需要接收和存储write set(即使单主模式也需要计算,传输和存储write set);另外还有paxos协议运行导致的额外的网络延时,这部分延时会导致每个事务提交的延时略大。并且为了保持MGR的高性能,要求半数的备机必须与主机同机房,否则paxos协议导致的延时会更大 — 当然,这个要求并不算过分。

MGR的功能约束是指表必须要有主键或者唯一索引,并且使用MGR事实上由于其distributed recovery所使用的clone插件的要求因此只能使用InnoDB存储引擎。不过这些约束完全可以接受 — 即使从replication的性能角度,每个表也应该有主键或者唯一索引,TDSQL也有此要求。从MySQL-8.0开始由于数据字典存储在了InnoDB中,InnoDB已经完全与MySQL绑定了。那么其他存储引擎存在的意义,似乎只剩下当作临时表或者log表(general log, slow log如果存储在表中的好,表是存储在csv存储引擎的)。其他满足特定需求的事务引擎—比如myrocks的高压缩率适合存储历史数据—就需要在clone插件中加入myrocks引擎的支持,否则就无法在MGR中被使用到了。但问题是clone 的工作原理与InnoDB有比较深的绑定,比如InnoDB的undo log专门做了修改来支持clone功能,其他引擎该如何接入到clone插件中还是一个问题,至少是需要比较大的工作量的。

最后,MGR对事务binlog的数据量有约束,这是因为如果事务binlog传输的时耗超过了paxos协议的超时,那么其他节点就会发现自己超时没有受到主节点的任何消息,误以为主节点已经宕机了。基于完全相同的原因,尽管TDSQL不使用paxos协议,这个约束在TDSQL中也是存在的 —如果正在提交的事务的binlog太大导致传输超时,那么DB实例的agent就会误以为主节点与备机断连了,于是会误触发主备切换。因此必须限制事务binlog的规模。


我对MySQL 8.0 的改造

不过遗憾的是,MySQL8.0的MGR仍然有XA事务的binlog容灾恢复的诸多缺陷,这些bug我在TDSQL-Percona-MySQL-5.7.17-11中已经做了修复并且报给了MySQL官方,还提交了修复的patch。这些修复经过TDSQL团队的充分测试以及腾讯公司内外大量用户的长期验证证明是可靠的。但是MySQL官方团队截止MySQL8.0.18并没有做修复,这些bug在MySQL-8.0中仍然存在,并且由于一些新功能的加入而有了新的问题需要处理。我花了很多时间修复了这些bug,并且通过了MySQL的测试包以及容灾测试。这样,昆仑分布式数据库就有了坚不可摧的容灾能力。

除此之外还做了一些MySQL 功能来适应昆仑分布式数据库整体需求,此处不再赘述。

MGR的single primary模式并不适合用户直接使用,这是因为用户的程序需要去适配主备切换导致的主节点变化,以及需要完成shard集群管理(比如集群启停就有不少讲究)等任务,会导致开发难度增大并且容易出错。MySQL官方文档也是因此推荐配合MGR使用MySQL router或者InnoDB cluster。但是由于MySQL官方版本的XA 事务处理方面的缺失(即容灾能力有缺陷),所以使用MySQL router或者InnoDB cluster并不能可靠地在同一个事务中写入到多个shard中。我没有使用过这两个工具,不过想必它们也并没有支持分布式事务。不知道是不是为了避免与Oracle 数据库产生公司内的产品竞争。


昆仑数据库中将会使用MGR single primary模式,并且自动处理MGR的主备切换适配,分布式事务容灾和恢复,集群管理等所有通用任务,成为用户和DBA的一个简单易用并且具备完整的容灾能力的分布式数据库系统。


最后,说一下MGR的地位和贡献。我认为MGR进一步巩固了binlog 子系统在MySQL生态中的地位,避免了binlog子系统的边缘化。要知道本来随着MySQL 逐步转向完全使用InnoDB存储引擎(数据字典使用innodb,干掉了frm文件;业界逐步弃用MyISAM),binlog系统的必要性变的很低了,用户完全可以使用InnoDB redo log复制来挂载备机,也可以做到HA和备机读,完全可以代替传统异步复制(async replication)。而且这么做还可以避免binlog 写入和存储的资源消耗,特别是它对事务提交产生的延时是显著的 —主节点为了产生binlog 事务序列,把本可以并发执行的多事务提交给串行化了,单线程执行binlog flush阶段和可选的单线程执行引擎层提交阶段。尽管innodb redo log占用存储空间预期比binlog更大(主要是btree页面分裂等页级别的redo log导致的)等缺点,也可以通过压缩来抵消。再考虑到传统异步复制时常出现的备机卡住或者落后主机太多无法追上等问题,而innodb redo log的重放(replay)会因为只做页面级别的修改而非常快速,省去了上层代码的开销,于是innodb redo log复制就变的更有吸引力了。在去年规划昆仑分布式数据库期间我曾经考虑过只使用InnoDB不使用MySQL这个选项。要知道innodb还自带一个简单的SQL处理器,完全可以执行单表查询和简单的表连接。不过最终就是考虑到MGR的上述诸多优点而选择使用MGR。

相信MGR在未来会不断发展,进一步提升MySQL在数据库业界的地位和价值。我也相信昆仑数据库能够把MySQL MGR的价值充分发挥出来,并且基于MGR实现更大规模的高效而且自动化的数据管理。