什么是孤儿文档
在MongoDB分片集群中,孤儿文档(Orphaned Document)是指那些存在于某个分片上但不应该存在于该分片上的文档。这些文档对应用程序是不可见的,因为MongoDB的路由服务(mongos)不会将查询路由到包含这些文档的分片。
孤儿文档的产生通常与MongoDB的负载均衡机制有关。当MongoDB的Balancer服务根据分片键(Shard Key)重新分布数据时,它会将数据块(Chunk)从一个分片迁移到另一个分片。迁移过程包括先将数据复制到目标分片,然后从源分片删除。如果在这个过程中发生网络中断或其他异常,可能会导致数据成功复制到目标分片但未能从源分片删除,从而在两个分片上都存在相同的文档。由于配置服务器(Config Server)只会记录文档应该存在于目标分片上,源分片上的文档就变成了孤儿文档。
孤儿文档的影响
孤儿文档会对MongoDB集群产生以下影响:
- 数据一致性问题:在数据迁移或备份过程中,孤儿文档可能导致数据内容或行数不一致。
- 存储空间浪费:孤儿文档占用额外的存储空间,但对应用程序不可见,造成资源浪费。
- 性能影响:在某些操作中,孤儿文档可能会被错误地处理,影响系统性能。
检查孤儿文档
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);
});
|
这个脚本的工作原理是:
- 定义
cleanupOrphaned函数,该函数通过循环调用cleanupOrphaned命令来清理指定集合的孤儿文档 - 设置要处理的数据库名称
- 遍历数据库中的所有集合,对每个集合执行清理操作
执行清理脚本
要执行清理脚本,需要连接到分片节点并运行脚本:
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);
});
|
执行步骤
修改脚本参数:
shardNames:分片集群实例中待清理孤儿文档的分片节点ID数组databasesToProcess:待清理孤儿文档的数据库名称数组(仅适用于4.4及以后版本)fullCollectionName:待清理孤儿文档的集合名称,格式为"数据库名称.集合名称"(仅适用于4.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>:数据库账号的密码
注意事项
- 版本要求:不同版本存在一些差异,需要仔细验证测试后再正式环境执行。
- 执行时机:建议在业务低峰期执行清理操作,以减少对业务的影响。
- 权限要求:执行清理操作需要具有足够权限的数据库账号。
- 备份建议:在执行清理操作前,建议先备份重要数据。
附录