MongoDB磁盘空间瘦身指南

      ☕ 3 分钟

这篇文章献给正为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,运维眼中的愁绪也消散不见了。去喝一杯吧!

参考资料


nanmu42
作者
nanmu42
用心构建美好事物。

目录