勿忘初心

个人签名

530篇博客

什么鬼!基于备份恢复的实例数据还能变多?

勿忘初心2018-10-19 10:01

此文已由作者温正湖授权网易云社区发布。

欢迎访问网易云社区,了解更多网易技术产品运营经验。


对数据库进行数据备份无非两种方式,一种是逻辑备份,也就是直接连上数据库导出所有的数据,对于MySQL,就是通过MySQL客户端或JDBC等MySQL驱动进行全表Select,将查询结果转化为Insert语句并保存到文件中,实际场景下,一般使用mysqldump或mydumper等工具来实现。同样的,对于MongoDB也是如此,可以借助mongodump工具来进行数据逻辑备份;另一种是物理备份,通俗地讲,就是通过dd或cp等方式直接拷贝数据库文件,  对于MySQL,可以使用Percona Xtrabackup工具,对于MongoDB,目前还没有成熟的物理备份工具,不过目前Percona正在开发中,相信很快就能用上。目前主流的MongoDB数据物理备份方式还是基于块设备或文件系统的snapshot机制。相比逻辑备份,物理备份在性能上更好,对数据库的侵入性小,不足之处在于,相比逻辑备份,物理备份数据的跨版本兼容性较弱,备份所占的空间较大。一般建议在备份大库时,采用物理备份更佳。本文的重点不在对比两种备份方式的优劣,所以在此不展开分析。


本文要介绍和分析的问题发现在使用lvm snapshot对MongoDB实例进行物理备份的场景。更确切的说,是在恢复MongoDB sharding集群的物理备份时发生的。在此还得简单介绍几句MongoDB sharding cluster,sharding cluster又称分片集群。数据库一旦成为瓶颈就需要进行扩展,扩展包括垂直扩展,如下:

另一种是水平扩展,将一个复制集实例扩展为一个集群,原来的复制集变为其下的一个shard,分片集群就是MongoDB官方提供的数据在线水平扩展方案,可以理解为MongoDB线上部署的终极方式:

如下图所示,集群由三个部分组成,分别是mongos(Router)、config server和shard组成。

mongos是用户访问分片集群中数据的入口,该节点不会持久化数据,仅缓存config server上的元数据,比如查询的路由表(Router Table)等,用户发起的读写操作通过mongos路由到一个或多个Shard上。


config server是集群的大脑,保存了集群相关的元数据信息,这些信息包括在admin和config库下。其中admin保存账号和权限定义相关信息,config保存集群的mongos、shard节点信息,数据库和集合信息,如集群中保存的所有数据库,集群中启用分片的数据库,启用分片的集合等,还保存了每个数据chunk在shard上的分布信息。数据库、集合和chunk信息组成了查询的路由表,该路由表再mongos、config server和每个shard上均会缓存。


shard是真正存储数据的组件,熟悉MySQL的同学可以将每个shard想象成MySQL的分区表。MongoDB中对集合启用分片就相当于MySQL中对某个表进行了分区。每个shard对应一个分区。shard上的数据是由一系列chunk组成的,每个chunk默认64MB。

chunk在某些场景下会进行分裂,由mongos将一个chunk分裂为2个chunk。chunk也会由config server发起在不同的shard上进行迁移。在MongoDB 3.4中,config server和每个shard一般部署为3个节点的MongoDB复制集。chunk的迁移操作由config server复制集的Primary节点上的balancer线程来执行。通过分裂和迁移,实现MongoDB分片集群中数据在各个shard上的均匀分布。


做了上面这么多铺垫,终于可以开始详细描述问题了。首先对这个分片集群使用ycsb工具压入一定量的数据,确保能够触发其chunk分裂条件。在config server完成chunk分裂,balancer线程running状态为false后,在mongos节点使用db.find().count()命令获取usertable集合文档个数。然后使用lvm snapshot分别在config server和各shard上进行数据备份。备份前,已将balancer disable掉,确保在备份期间不会发生chunk迁移导致config server上的chunk元数据和shard上的chunk信息不一致。备份完成后,基于集群备份恢复出一个新的分片集群,再用count()命令在mongos上做查询,发现usertable集合的文档个数变多了。需要说明的是,上面的这一系列不是我操作的。而是我们外号架构师的QA MM操作的。发现这个现象后,她跟我说:“基于集群备份恢复出来的实例,数据变多了!”。乍一听这个这句话,我的表情瞬间变成下面这样,听说过基于备份恢复出来的数据库实例少数据的情况,数据多出来却很少见,为什么凭空多出来呢:


是不是搞错了,问了句:“Are you Ok!”


不过作为资深的测试人员,相信我们的QA不会犯那种低级错误。既然出现这个情况,那么肯定是有原因的。于是了解了整个测试的经过。经过初步分析,大致可以确定,问题虽然发生在备份恢复的时候,但症结却在执行lvm snapshot的那一刻就已经埋下了。那么,这是我们那里做错了吗?应该说这是我们还做得不够好。这一切都跟MongoDB chunk迁移的机制有关系。在前面我们提到,为了确保数据在各个shard上保存均衡。config server会将一个shard上的chunk迁移到另一个shard上。迁移有两个重点关注的点:1、对于不同的存储引擎,迁移的行为是不一样的,对于目前默认的WiredTiger存储引擎,chunk数据默认仅写到目标shard的Primary节点即返回(可以通过参数_secondaryThrottle来调整,但调后会影响迁移性能)。由于我们备份是在Secondary节点进行的,所以必须确保chunk数据已经从Primary复制到Secondary后才开始备份。这已经在我们的设计方案中得到保障。对于已经逐步弃用的MMAPv1存储引擎,chunk数据会同时写到Primary和Secondary上。2、迁移完成后,源shard上的这个chunk数据如何处理?MongoDB分片集群默认是在后台完成源shard上该chunk数据的删除的。不需要等到数据删除后才commit本次迁移。这么设计的目的是为了提高chunk迁移的效率,因为MongoDB规定一个shard上同时只能存在一个正在进行的chunk迁移操作,这么处理就无需等待本次chunk迁移完全结束即可开始下一个chunk迁移。

根据理论分析,本次出现的问题就是由于删除源shard的已迁移chunk数据环节导致的。下图截自MongoDB官方文档

这些待删除的chunk会加入到位于shard复制集Primary节点上的一个队列中,删除线程逐个获取队列中的删除请求并执行。但这个队列是非持久化的,如果Primary节点Crash了,而Crash时队列中还有请求。那么这部分删除请求再也得不到处理。在MongoDB 分配集群中,将这些未被删除的冗余数据成为orphan文档。


但在备份前后,并没有发生过任意一个节点Crash啊?扯了半天,似乎还没有跟问题建立直接关系。其实对复制集节点做snapshot,在这个场景下,基本上可以等同为一次节点Crash,因为snapshot是对mongod数据盘做快照,这样能够确保硬盘上的数据不丢失,但此时内存中的信息是无法保存下来的。由于chunk数据删除全过程都仅在内存中进行,不会将进展持久化到硬盘上,而且我们又是在Secondary节点上进行snapshot,所以这部分内存信息肯定是丢失的。


好了,接下来就是如何证明上面的推断是对的。MongoDB官方针对orphan文档专门提供了一个命令cleanupOrphaned来进行删除。如下所示:

如果描述的问题确是该原因导致的,那么执行该命令后,集合usertable的文档数应该恢复到跟备份前的connt()一样。显然,答案是肯定的。相比MongoDB复制集实例,MongoDB分片集群复杂度提升数倍,在使用过程中需要关注的坑也较多,如果不是MongoDB老司机,最好悠着点玩。网易云MongoDB服务新推出分片集群实例,欢迎大家到时试用,让我们为您填坑。


网易云MongoDB 服务为开发者提供了一站式的 MongoDB 云端解决方案,包括提供三节点复制集的高可用架构,故障切换,并提供专业的备份、监控以及性能优化方案,彻底免除开发者的运维烦恼,点击可免费试用



网易云免费体验馆,0成本体验20+款云产品! 

更多网易技术、产品、运营经验分享请点击


相关文章:
【推荐】 探一探快应用的虚实