什么是孤儿文档

在MongoDB分片集群中,孤儿文档(Orphaned Document)是指那些存在于某个分片上但不应该存在于该分片上的文档。这些文档对应用程序是不可见的,因为MongoDB的路由服务(mongos)不会将查询路由到包含这些文档的分片。

孤儿文档的产生通常与MongoDB的负载均衡机制有关。当MongoDB的Balancer服务根据分片键(Shard Key)重新分布数据时,它会将数据块(Chunk)从一个分片迁移到另一个分片。迁移过程包括先将数据复制到目标分片,然后从源分片删除。如果在这个过程中发生网络中断或其他异常,可能会导致数据成功复制到目标分片但未能从源分片删除,从而在两个分片上都存在相同的文档。由于配置服务器(Config Server)只会记录文档应该存在于目标分片上,源分片上的文档就变成了孤儿文档。

孤儿文档的影响

孤儿文档会对MongoDB集群产生以下影响:

  1. 数据一致性问题:在数据迁移或备份过程中,孤儿文档可能导致数据内容或行数不一致。
  2. 存储空间浪费:孤儿文档占用额外的存储空间,但对应用程序不可见,造成资源浪费。
  3. 性能影响:在某些操作中,孤儿文档可能会被错误地处理,影响系统性能。

检查孤儿文档

MongoDB 5.0及以前版本

使用root或高权限账号连接到Mongos节点,执行以下命令检查指定集合的孤儿文档:

1
db.getSiblingDB("<db>").<collection>.find().readPref("secondary").readConcern("local").explain("executionStats")

在返回结果中,关注SHARDING_FILTER阶段的chunkSkips属性,其值代表该集合在当前分片上的孤儿文档数量。

  • 由于该命令会扫描所有Shard节点上的文档,为降低对业务的影响,该命令设置了readPreference参数,将会在Secondary节点上执行。
  • 该命令的执行耗时与库表的数据量和文档数成正比,当数据量较大时,查询耗时会比较长并且会对数据库实例产生一定的查询压力,需要注意不用再线上库直接执行。

MongoDB 6.0及以后版本

MongoDB 6.0及以上版本可以通过$shardedDataDistribution聚合阶段来检查孤儿文档:

1
2
3
db.getSiblingDB("admin").aggregate( [{ $shardedDataDistribution: { } }] ).pretty()
//如果只需要确认某个单独的namespace是否包含孤立文档,则按需添加过滤字段即可
db.getSiblingDB("admin").aggregate( [{ $shardedDataDistribution: { } },{ $match: { "ns": "<db>.<collection>" } }] ).pretty()

返回结果中的numOrphanedDocs参数值即是某个集合在该分片上的孤儿文档数量。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
[
  {
    "ns": "test.names",
    "shards": [
      {
        "shardName": "shard-1",
        "numOrphanedDocs": 0,
        "numOwnedDocuments": 6,
        "ownedSizeBytes": 366,
        "orphanedSizeBytes": 0
      },
      {
        "shardName": "shard-2",
        "numOrphanedDocs": 0,
        "numOwnedDocuments": 6,
        "ownedSizeBytes": 366,
        "orphanedSizeBytes": 0
      }
    ]
  }
]

清理孤儿文档(连接到每个分片节点)

清理孤儿文档需要使用MongoDB提供的cleanupOrphaned命令。以下是具体的清理方法:

使用cleanupOrphaned.js脚本

我们可以使用以下JavaScript脚本来清理孤儿文档:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function cleanupOrphaned(coll) {
  var nextKey = { };
  var result;
  
  while ( nextKey != null ) {
    result = db.adminCommand( { cleanupOrphaned: coll, startingFromKey: nextKey } );
  
    if (result.ok != 1)
       print("Unable to complete at this time: failure or timeout.")
  
    printjson(result);
  
    nextKey = result.stoppedAtKey;
  }
}

var dbName = 'test'
db = db.getSiblingDB(dbName)
db.getCollectionNames().forEach(function(collName) {
	cleanupOrphaned(dbName + "." + collName);
});

这个脚本的工作原理是:

  1. 定义cleanupOrphaned函数,该函数通过循环调用cleanupOrphaned命令来清理指定集合的孤儿文档
  2. 设置要处理的数据库名称
  3. 遍历数据库中的所有集合,对每个集合执行清理操作

执行清理脚本

要执行清理脚本,需要连接到分片节点并运行脚本:

1
mongo --host ShardIP --port Primaryport --authenticationDatabase database -u username -p password cleanupOrphaned.js

参数说明:

  • ShardIP:分片节点的IP地址
  • Primaryport:分片节点中Primary节点的服务端口
  • database:鉴权数据库名
  • username:登录数据库的账号
  • password:登录数据库的密码

清理孤儿文档(通过 mongos 节点执行)

MongoDB 4.4及以后版本

对于MongoDB 4.4及以后版本,可以使用以下脚本通过mongos连接清理多个分片节点上的孤儿文档:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 分片实例名称列表,需要按照实际情况修改
var shardNames = ["shardName1", "shardName2"];
// 数据库列表,需要按照实际情况修改
var databasesToProcess = ["database1", "database2", "database3"];

shardNames.forEach(function(shardName) {
  // 遍历指定的数据库列表
  databasesToProcess.forEach(function(dbName) {
    var dbInstance = db.getSiblingDB(dbName);
    // 获取该数据库实例的所有集合名称
    var collectionNames = dbInstance.getCollectionNames();

    // 遍历每个集合
    collectionNames.forEach(function(collectionName) {
      // 完整的集合名称
      var fullCollectionName = dbName + "." + collectionName;
      // 构建 cleanupOrphaned 命令
      var command = {
        runCommandOnShard: shardName,
        command: { cleanupOrphaned: fullCollectionName }
      };

      // 执行命令
      var result = db.adminCommand(command);
      printjson(result);
      if (result.ok) {
        print("Cleaned up orphaned documents for collection " + fullCollectionName + " on shard " + shardName);
      } else {
        print("Failed to clean up orphaned documents for collection " + fullCollectionName + " on shard " + shardName);
      }
    });
  });
});

MongoDB 4.2及以前版本

对于MongoDB 4.2及以前版本,可以使用以下脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function cleanupOrphanedOnShard(shardName, fullCollectionName) {
  var nextKey = { };
  var result;

  while ( nextKey != null ) {
    var command = {
      runCommandOnShard: shardName,
      command: { cleanupOrphaned: fullCollectionName, startingFromKey: nextKey }
    };

    result = db.adminCommand(command);
    printjson(result);

    if (result.ok != 1 || !(result.results.hasOwnProperty(shardName)) || result.results[shardName].ok != 1 ) {
      print("Unable to complete at this time: failure or timeout.")
      break
    }

    nextKey = result.results[shardName].stoppedAtKey;
  }

  print("cleanupOrphaned done for coll: " + fullCollectionName + " on shard: " + shardName)
}

// 分片实例名称列表,需要按照实际情况修改
var shardNames = ["shardName1", "shardName2", "shardName3"]
// 需要清理的数据库指定集合,可以按照实际情况修改
var fullCollectionName = "database.collection"

shardNames.forEach(function(shardName) {
  cleanupOrphanedOnShard(shardName, fullCollectionName);
});

执行步骤

  1. 修改脚本参数

    • shardNames:分片集群实例中待清理孤儿文档的分片节点ID数组
    • databasesToProcess:待清理孤儿文档的数据库名称数组(仅适用于4.4及以后版本)
    • fullCollectionName:待清理孤儿文档的集合名称,格式为"数据库名称.集合名称"(仅适用于4.2及以前版本)
  2. 通过Mongo Shell命令运行脚本

    mongo --host <Mongoshost> --port <Primaryport> --authenticationDatabase <database> -u <username> -p <password> cleanupOrphaned.js > output.txt

参数说明:

  • <Mongoshost>:分片集群实例mongos节点的连接地址
  • <Primaryport>:分片集群实例mongos节点的端口号,默认为3717
  • <database>:鉴权数据库名,即数据库账号所属的数据库
  • <username>:数据库账号
  • <password>:数据库账号的密码

注意事项

  1. 版本要求:不同版本存在一些差异,需要仔细验证测试后再正式环境执行。
  2. 执行时机:建议在业务低峰期执行清理操作,以减少对业务的影响。
  3. 权限要求:执行清理操作需要具有足够权限的数据库账号。
  4. 备份建议:在执行清理操作前,建议先备份重要数据。

附录