这篇文章献给正为MongoDB磁盘占用率偏高而忧心忡忡的运维。
本文适用于MongoDB 3.2及以上,所有操作都在Mongo Shell中进行,你需要确认你的Mongo引擎是WiredTiger:
1
2
|
db.serverStatus().storageEngine
// 输出中应含有: { "name" : "wiredTiger" }
|
哪儿最胖?
找主要矛盾,使用show dbs
定位大库,use your_big_db
后使用下面这个函数定位大表:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// 在当前会话中,你可以多次使用这个函数
function CollectionSizes(collectionNames) {
let stats = []
collectionNames.forEach(function (n) {
stats.push(db[n].stats(1024 * 1024 * 1024)) // show size in GB
})
stats = stats.sort(function (a, b) {
return b['size'] - a['size']
})
print(`name: DB size in GB, disk size in GB`)
for (let c of stats) {
print(`${c['ns']}: ${c['size']} (${c['storageSize']})`)
}
}
CollectionSizes(db.getCollectionNames())
|
开始瘦身
降低磁盘占用率的手段无非两种,优化MongoDB的存储结构(Compact),移走或删除数据。
在我们开始之前,你需要留意关注瘦身操作带来的额外IO压力,太大的压力会提升数据库访问延时,让从库同步延迟提升,延迟太大的从库需要重新全量同步。集群的同步状况可以在集群中任何一个节点上查询:
1
2
3
4
5
6
7
|
// 如果你在从库上进行查询,需要运行:
// rs.slaveOk();
// 查询集群各个节点的同步情况
rs.status().members.map((x) => {
return {name: x.name, stateStr: x.stateStr, optimeDate: x.optimeDate, syncingTo: x.syncingTo}
})
|
MongoDB Compact
MongoDB一般不会在删除数据时将磁盘空间返还给系统,这部分页面会被打上脏标记预留给未来的写入,使用compact命令可以让WiredTiger引擎归还这部分空间。(类似于PostgreSQL的Vacuum命令)
Compact需要在集群的各个节点上分别执行。
Compact命令会锁定整个数据库导致服务中断,执行compact的从库的同步会暂停。如果你的Mongo不是集群,你需要在业务的中断时间执行操作。
你可以在Mongo集群的节点中滚动执行本命令,先轮流在从库中执行(别忘了rs.slaveOk()
),最后在主库中使用rs.stepDown()
转移主库身份再执行compact。这样操作不会带来业务中断。
使用下列命令可以compact节点上的所有非系统数据库中的所有表,在compact之间进行sleep可以降低从库同步压力:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
function compactDB(dbName) {
if ('local' !== dbName && 'admin' !== dbName && 'system' !== dbName) {
let subject = db.getSiblingDB(dbName)
subject.getCollectionNames().forEach(function (collectionName) {
let taskName = dbName + ' - ' + collectionName
let startAt = new Date()
print('compacting: ' + taskName)
subject.runCommand({compact: collectionName})
let elapsed = ((new Date()) - startAt) / 1000
print(taskName + ', finished in ' + elapsed + ' second(s).')
if (elapsed > 30) {
print('sleep a while for OpLog sync...')
sleep(8000)
}
})
}
}
db.getMongo().getDBNames().forEach(compactDB)
|
移动或删除数据
移动和删除数据需要在业务上评估可行性,移动数据需要业务支持分库分表,删除陈旧数据需要确定数据范围。
数据的移动和删除应当在Compact操作之前进行。
删除陈旧数据时,如果业务可以暂时停止对数据表的读写,我们可以有策略地用一张新表顶替旧表,drop整张旧表比逐条删除数据高效很多,几乎不会带来额外IO压力,并且旧表所占的磁盘空间会立即返还给操作系统:
1
2
3
4
5
6
7
8
9
10
|
// 将要留下来的数据放到新表
db.big_table.aggregate([{$match: {timestamp: {$gt: some_time}}}, {$out: 'big_table_left'}])
// 为新表建立和旧表相同的索引
db.big_table_left.createIndex({timestamp: -1}, {background: 1})
// 重命名旧表,让出名字
db.big_table.renameCollection('big_table_legacy')
// 新表顶替旧表
db.big_table_left.renameCollection('big_table')
// 确认一切正常后drop旧表
db.big_table_legacy.drop()
|
如果无法暂停业务对旧表的读写,那么只能逐条删除数据了,在删除之间进行sleep可以降低整体IO压力:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
let count = 0
let toDelete = []
db.big_table.find({timestamp: {$gt: some_time}}, {_id: 1}).forEach(function (item) {
count++
toDelete.push(item._id)
if (count % 10000 === 0) {
print(`progress: ${count}`)
db.big_table.deleteMany({_id: {$in: toDelete}})
toDelete.splice(0, toDelete.length)
// sleep a while to ease the stress on IO
sleep(100)
}
})
|
大功告成,现在你有了一个BMI正常的MongoDB,运维眼中的愁绪也消散不见了。去喝一杯吧!
参考资料