背景
最近公司的其中一个业务量暴增,MongoDB月增数据从之前的百GB级别暴增到TB级别,磁盘剩余空间马上开始预警,新硬件设备采购需要时间,无法快速扩容,另一方面为了节省成本就先尝试进行数据清理。最终决定先清理几个二三百G的集合,数据量大概在40亿左右,释放一部分磁盘空间,不至于磁盘被快速撑爆。
其中一个待集合大小:数据量35亿,占用磁盘空间300G
数据清理方案
数据清理需要进行3方面的考量,第一,这些数据需要归档吗?第二,如何平稳的清理?即保证在数据清理的过程中不会引起系统负载的升高,不会对现有业务造成冲击,甚至hang死数据库;第三,在保持平稳的前提下,如何快速的清理?
对于第一个的问题,通过和业务沟通过之后已经确认这些集合数据不需要归档,可以放心清理。
对于第二和第三个问题则主要看操作手法了,手法精妙则又快又稳,手法但凡出现偏差则连夜站票跑路。能想到的手法主要有两种:第一,通过逻辑delete删除的方式,比如一次删5000~1w,然后sleep 1s;第二,通过drop的方式直接删除集合。我们将在接下来讨论这两种方案可行性。
逻辑delete删除
对于逻辑一次性批量删除多个文档的方法主要有以下几种方法:
-
collection.remove()
-
collection.deleteMany()
-
delete命令
-
bulk remove方式
下面我们将分别测试各种方式的效率,我们将每次删除5w行,然后看谁的表现更好。
collection.remove()方法
PRIMARY> var collection = db.getCollection('my_collection');
PRIMARY> function myRemove(){
... var docsToDelete = collection.find({},{_id:1}).limit(50000).toArray();
... var idsToDelete = docsToDelete.map(function(doc) { return doc._id; });
... print(new Date());
... print(collection.remove({_id: {$in: idsToDelete}}));
... print(new Date());
... }
PRIMARY> myRemove()
Fri Aug 23 2024 14:51:14 GMT+0800 (CST)
WriteResult({ "nRemoved" : 50000 })
Fri Aug 23 2024 14:51:15 GMT+0800 (CST)
结果:删除耗时1s
collection.deleteMany()
PRIMARY> var collection = db.getCollection('my_collection');
PRIMARY> function myDeleteMany(){
... var docsToDelete = collection.find({},{_id:1}).limit(50000).toArray();
... var idsToDelete = docsToDelete.map(function(doc) { return doc._id; });
... print(new Date());
... var deleteResult=collection.deleteMany({_id: {$in: idsToDelete}})
... print(deleteResult.deletedCount);
... print(new Date());
... }
PRIMARY> myDeleteMany()
Fri Aug 23 2024 14:57:25 GMT+0800 (CST)
50000
Fri Aug 23 2024 14:57:26 GMT+0800 (CST)
结果:删除耗时1s
delete命令
PRIMARY> var collection = db.getCollection('my_collection');
PRIMARY> function myDelete(){
... var docsToDelete = collection.find({},{_id:1}).limit(50000).toArray();
... var idsToDelete = docsToDelete.map(function(doc) { return doc._id; });
... print(new Date());
... var deleteResult=db.runCommand({delete: "my_collection",deletes: [ { q: { _id: {$in: idsToDelete} }, limit: 0 } ]});
... print(deleteResult.n);
... print(new Date());
... }
PRIMARY> myDelete()
Fri Aug 23 2024 15:05:27 GMT+0800 (CST)
50000
Fri Aug 23 2024 15:05:29 GMT+0800 (CST)
结果:删除耗时2s
bulk remove方式
PRIMARY> var collection = db.getCollection('my_collection');
PRIMARY> function myBulkRemove(){
... var docsToDelete = collection.find({},{_id:1}).limit(50000).toArray();
... var idsToDelete = docsToDelete.map(function(doc) { return doc._id; });
... print(new Date());
... var bulk = collection.initializeOrderedBulkOp();
... bulk.find( {_id: {$in: idsToDelete}} ).remove();
... print(bulk.execute());
... print(new Date());
... }
PRIMARY> myBulkRemove()
Fri Aug 23 2024 14:59:50 GMT+0800 (CST)
BulkWriteResult({
"writeErrors" : [ ],
"writeConcernErrors" : [ ],
"nInserted" : 0,
"nUpserted" : 0,
"nMatched" : 0,
"nModified" : 0,
"nRemoved" : 50000,
"upserted" : [ ]
})
Fri Aug 23 2024 14:59:51 GMT+0800 (CST)
结果:删除耗时1s
对比来看,各个删除方法的效率都差不多,那么通过这种逻辑删除的方案可行吗?首先看平稳性性,通过逻辑删除的方式我们可以随时中断,随时开始,一次删多少、间隔几秒,完全自主可控,可以说是非常平稳了。但是效率方面就不太尽人意了,假设为了删除操作不会影响到线上业务,设置一次删除1w,删除间隔为1s,那么可以理解为每秒删除5000。做个简单的计算:5000*3600*24=4亿3000万,那我删除一张40亿的集合,就要删将近10天10夜,即使加加量,平均每秒删1w,也需要5天5夜,效率方面完全不能接受。
drop直接删除集合
通过drop直接删除集合和通过逻辑delete删除完全是另一个极端,直接就是一刀切,一条命令下去直接就给集合嘎了,效率非常快,但是问题是,drop的时候还会顺带的清理数据文件和索引文件,那么必然会引起磁盘IO的快速上升,进而影响到线上业务。
那么就没有一个完美解决方案吗?有!
解决方案就是我们先将待删除集合的数据文件和索引文件找出来,然后给其建立硬链接,随后再执行drop操作,drop操作在清理磁盘数据文件和索引文件的时候只会删除其链接,不会真正删除文件,等操作完成之后,我们就可以再分配一点点的删除残留的数据文件和索引文件,尽享丝滑,即不会对业务造成任何影响,删除效率也非常高。
具体操作如下:
# 确认数据文件路径
db.my_collection.stats().wiredTiger.uri
# 获取文档中的索引
db.my_collection.getIndexes()
# 确认每个索引的路径
db.my_collection.stats({indexDetails:true}).indexDetails._id_.uri
# 给数据文件和每个索引文件创建硬连接
ln collection-21--2919691543483114411.wt collection-21--2919691543483114411.wt.hdlk
ln index-28--2919691543483114411.wt index-28--2919691543483114411.wt.hdlk
# 删除文档
db.my_collection.drop()
# 分批一点点删除数据文件和索引文件
最终平稳且快速的清理了历史集合,且对线上业务没有造成任何冲击,可以说是非常优雅了。