互联网软件本身具有快速迭代、持续交付等特点,加上数据库的表结构(DDL)发布无法做到灰度发布,且回退困难、试错成本高,一个稳定可靠的数据库发布系统对于互联网公司显得尤其重要。本文将介绍携程MySQL数据库发布系统从无到有,版本不断迭代的演进之路,希望对读者有所参考和帮助。
我们先后设计了三个版本,最新的版本具有以下功能和特点:
介绍整个系统之前,首先对携程数据库环境和发布流程做一个简单的介绍。系统的数据库环境主要分成Dev、测试环境(含三个子环境,功能性测试(FAT)/压力测试(LPT)/UAT 三个环境)、Product:
1)数据库表设计在Dev环境完成,期间包含数据库规范检测
2)然后发布到其它测试环境(FAT→LPT→UAT)
3)测试环境都验证通过后,最后发布到生产环境
表发布流程图
携程成立以来一直使用SQL Server 数据库,2014年左右开始使用MySQL数据库,为后面转型MySQL做准备。这时期接入MySQL的业务量很小,数据量不大,都是非核心业务,所以整个发布过程可以概括为“简单粗暴”:
1)开发人员通过直连DEV环境数据库,直接对数据库表进行修改
2)DBA通过自动化工具捕捉到表的变化,将变更同步到测试环境
3)开发测试完后,将变化同步到生产环境
这个阶段只是简单把表的变更传递到其他环境,对发布期间业务和性能方面的影响没有考虑太多。
1.0 版本发布流程
随着业务接入MySQL不断增加,MySQL数据库越来越多,到2016年下半年为止,MySQL 数据库数量已经有800+,很多核心业务也转到MySQL,包含很多读写分离架构。此时原生的DDL发布已经无法满足业务需求,这时引入了业界流行的pt-online-schema-change(pt-osc)。
2.0版本发布流程
pt-osc是percona开发的一款比较成熟的产品,业界使用也较多。其采用触发器的方式将所有的增量DML应用到了影子表,这种实现方式会加大对语句的开销,并发过高时甚至会影响数据库正常提供服务,因此往往会出现发布一半最后还是不得不终止发布的现象,线上遇到核心的表或者大表往往需要晚上留守来进行发布,这极大的提高了DBA的运维负担。
为了进一步提升发布稳定性,我们在2017年调研了当时刚开源不久的gh-ost,由于产品非常新,因此做了大量的调研和测试工作,也发现提交了多个高优先级Bug(包括GBK字符集支持、bad connection以及column case-sensitive issue导致数据丢失等),都已得到作者的修复。
那么gh-ost对比pt-osc具体有哪些优势呢?下面先简单介绍下它的两个最核心的特性。
在gh-ost出现之前第三方MySQL DDL工具均采用触发器的方式进行实现,包括前面percona的pt-osc,Facebook的OSC等等。而gh-ost采用的机制和他们完全不同:它通过MySQL binlog来同步数据,gh-ost本身注册为一个fake slave,可以从集群中的master或者slave上拉取binlog,并实时解析,将变更表的所有DML操作都重新apply到影子表上面。因此对于发布期间变更表上发生的DML操作,可以完全避免由于触发器而产生的性能开销,以及锁的争抢。
除此之外,一般我们选择目标发布机器通常会选择集群中slave节点,而slave一般不会承载业务,这样binlog解析的开销也不会落在提供业务的master上面,而仅仅是一次异步的DML语句重放。
另一个最重要的特性是动态调控,这是此前其他第三方开源工具所不具备的。
之前通过pt-osc发布时,命令执行后参数就没法修改,除非停止重来。假设发布进行到90%,突然由于其他各种原因导致服务器负载上升,为不影响业务,只能选择将发布停掉,等性能恢复再重来。
通过pt-osc发布的表都是很大的表,耗时较长,所以遇到这类场景很尴尬。因此发布中参数如果可动态调控将变得非常重要。gh-ost另外实现了一个socket server,我们可以在发布过程中,通过socket和发布进程进行实时交互,它可以支持实时的暂停,恢复,以及很多参数的动态调整,来适应外界变化。
在了解完其重要特性后,简单介绍下其实现原理。
其原理很好理解,首先建两张表,一张_gho的影子表,gh-ost会将原表数据以及增量数据都应用到这个表,最后会将这个表和原表做次表名切换,另一张是_ghc表,这个表是存放changelog的数据,包括信号标记,心跳等。
其次,gh-ost会开两个goroutine,一个用于拷贝原表数据,一个用于apply增量的binlog到_gho表,并且两个goroutine的并行在跑的,也就是不用关心数据是先拷贝过去还是先apply binlog过去。
因为这里会对insert语句做调整,首先我们拷贝的insert into会改写成insert ignore into,而binlog内insert into会改写成replace into,这样可以很好的支持两个goroutine的并行。但这样的调整能适用所有的DDL吗?答案是否定的,大家可以思考下,下面案例部分会给出详细解释。
最后,当原表数据全部拷贝完成后,gh-ost会进入到表交换阶段,采用更加安全的原子交换。
Gh-ost 架构图
为了确保每次发布符合数据库规范,确保发布可以顺利完成,发布前我们做了很多检查工作,发布过程中会有线程实时侦听发布状态。通过producer,consumer,listener如下三个组件来协同完成发布的顺利进行。
任务运行架构图
我们消费线程(consumer)会在发布前做满足发布的前置校验,选择合适的目标主机进行发布。
1)MySQL环境变量的校验:检查当前实例变量配置是否满足发布要求。
2)冲突表校验:检查集群中是否存在已发布相冲突的表,存在的话自动进行清理。
3)冲突标记文件校验:检查发布机器上是否存在冲突的标记文件,存在的话自动进行清理。
4)磁盘容量校验:预估集群所有节点的磁盘空间是否足够
5)任务并行校验:检查集群是否存在其他发布,多实例会检查所有实例所属集群是否存在发布,为避免并行发布导致的性能影响,以及磁盘容量难以预估,我们会限制单个集群只能有串行发布。
6)DRC成员状态校验:对于已接入DRC的DB,会在发布前先初始化所有成员状态,并随机选择一个成员成为leader,仅当所有成员所属集群均已满足前置校验,才会进入真正发布阶段。
注:DRC(Data Replicate Center),想了解更多DRC相关的技术戳这里。这里主要负责支持多数据中心同时发起以及结束发布流程。
整个发布过程采用的是生产消费模型,当每个消费线程运行任务时,同时会生成一个其对应的监听线程(listener),用于监听该任务的运行状态。
1)磁盘容量监听:当低于某阈值时将终止发布,并会清理发布产生的残留表来释放空间。
2)服务器性能监听:当服务器负载过高,将会自动触发throttle,等性能恢复再重新解除throttle。
3)副本延迟监听:延迟阈值默认初始1.5s,后续在一个阈值上限内会动态增减,避免延迟一直波动时影响发布效率,但最终交换前会回置到默认1.5s。
4)时间监听:当前时间若处于业务高峰期,会通过自动加大nice-ratio的值来进行“限流”,等业务低峰期后再做置回。
5)DRC成员状态监听:对于接入DRC的DB,会侦听partner的运行状态,等所有成员均已进入postponing状态后,再由drc选举出来的leader统一触发表名交换。
6)集群拓扑监听:线上我们往往会碰到正在发布的DB进行了变更维护,包括主从切换,DB拆分到其他集群上等等。这时我们发现gh-ost会hang在那,也不会报错,往往会等到提交发布的人员反馈才会发现,因此我们这里加了对集群拓扑的监听,来及时发现拓扑的变更并终止发布。
目前gh-ost接入发布系统已接近两年,运行非常稳定。但慢慢的我们会发现原生gh-ost没办法满足我们所有需求,所以做了一些二次开发。
下面通过几个典型案例来简要介绍下。
案例1、发布后自增列值保留
默认gh-ost 发布时新表并没有保留原表自增值,部分业务是依赖自增列的值,这种场景会出现较大的问题。
要解决这个问题其实不难,只需要在建_gho表后设置一把AUTO_INCREMENT值即可。我们添加了一个- reset-original-auto-increment 参数开关,默认false,即保留原始自增值。
代码示例如下,先查找原表的有效自增值,并应用给新的_gho表即可。
案例2、含唯一键表发布
我们知道唯一键发布有两大前提,首先,表中已有的存量数据必须满足新增的唯一键约束;其次,发布过程中出现的DML增量数据也需保证满足新增的唯一键约束。
默认gh-ost对表添加唯一键是无法保证数据的完整性的。为什么呢?前面我们简单提过gh-ost发布会做语句转换,并且rowCopy 中insert into 会转为 insert ignore into,而binlogApply中insert into会转为replace into。当表结构变更中包含新增唯一键的话,这种转换就显然不够了,它会将冲突数据全部自然抹掉,而这显然是不合理的,是很严重的data integrity问题。
工具的预期应该是出现数据冲突即退出,说明这个发布并没有发布条件。而官方并没有做唯一索引发布的特殊支持,那我们是否可以实现这一部分逻辑?问题的关键在于我们要对原主键继续支持insert ignore into/ replace into的逻辑保证数据一致且不失败,另外新增唯一键部分又不能通过这种逻辑处理,保证冲突数据要及时发现。
后面通过分析我们想了一种方案,首先通过如下一条正则解析命令是否包含新增唯一键。
其次对写入逻辑进行如下改写:
1)原数据拷贝(rowCopy)从insert ignore into 调整为 insert into … andnot exists PK的方式,如下示例。
2)DML增量应用(binlogApply)从 replace into 调整为 delete from + insert into的方式,如下示例。
下面对原数据拷贝(A),原表DML(B),Binlog应用到新表© 三个过程先后顺序不固定时做下推演。首先C肯定在B后面,因此可能的顺序是ABC,BCA,BAC 三种可能情况。
原表b, 2个列,col1 PK,col2 计划新增Uniquekey,原表数据是(1,a), (3,c)。
ABC:先完成拷贝,再对原表DML,最后应用binlog
BCA:先原表DML,再应用binlog,最后拷贝
BAC:先原表DML,再拷贝,最后应用binlog
经过过程推演,我们发现这个方案可以解决新增唯一键时可能存在的问题。
案例3、活学活用,大表发布+数据清理
我们经常会碰到一些大表的发布,发布系统一般会对超大表做拦截,建议清理些无效数据。那这里分为两个过程,即先清理无效数据,再进行发布。那我们是否可以将这两个过程合并发布呢?答案是可行的,而且可以极大的提升发布效率。
逻辑可以很容易理解,见下图,即拷贝你所需要的数据,而增量部分不做变化。我们可以加个参数-where-reserve-clause,代表你需要的数据。那这里有一个问题,拷贝范围是先去根据-where-reserve-clause去限定,还是实际insert的时候去限定?有何区别?
发布+清理逻辑图
区别在于如果根据-where-reserve-clause去限定范围的性能很差,往往查主键范围需要花很久,如果主键范围又很分散,那选择先查这个范围是比较差的。而如果实际insert的时候去限定实际需写入的数据的话,则只是在每个chunk 写入时附加上这个条件,可能一个chunk没有一条数据符合条件,那即产生一次空跑,也没有任何影响。
但如果用户明确知道要保留的主键范围,那先去限定范围可以避免大量的空跑。因此添加了-force-query-migration-range-values-on-master来确定使用哪种方式,而具体选择需具体案例具体分析。
除此之外,我们知道数据清理表空间并不会自动瘦身,往往需要配合optimize table来进行表收缩。而添加的这个功能本身既支持数据清理,又支持表结构变更,而支持了表结构变更也就支持了表收缩。因此对-alter做了下扩展,允许noop,来支持不变更结构仅数据清理或者表收缩等场景。
下面有个线上数据清理的测试数据对比(表大小在300GB左右,需清理80%左右的数据):
表大小 | 总行数/保留行数 | 处理方式 | 耗时 | 备注 |
---|---|---|---|---|
290GB | 68666w/17327w | 数据清理工具 | 25h | 后续还需optimize |
320GB | 75128w/19542w | gh-ost | 2h30m | 后续清理老表即可 |
通过对比,我们可以看到效率提升了10倍以上,其中还不算optimize的开销。
以上是携程数据库发布系统的整个演进过程,希望对读者有所参考和帮助,新的3.0MySQL数据库发布系统从2018年开始研发上线并持续改进,功能上已经较为完善,适应了业务快速迭代的要求,规避了发布可能造成的业务故障,覆盖了携程绝大多数类型的DDL。
面向未来,我们的发布系统会持续改进:更加友好的交互、更加智能的throttle,我们已经在路上。
作者介绍:
天浩,携程数据库专家,专注数据库自动化运维研发工作。
晓军,携程数据库专家,主要负责运维及分布式数据库研究。
本文转载自公众号携程技术(ID:ctriptech)。
原文链接:
领取专属 10元无门槛券
私享最新 技术干货