亿级数据处理
2021-04-06
MongoDB中存在一张数据量过亿的表
目前文档数量 232,656,863
文档容量:46.35Gb
运行环境:
操作系统:Ubuntu 20.04.1 LTS
CPU: i5-4590 @ 3.30 Ghz
内存: 32G
硬盘:M.2 SSD
已在2个字段上创建索引,命中索引的话响应3ms,没命中索引80s以上。
批量导入优化
有百万条数据需导入,为加快导入速度,我们执行批量导入,一次1万条试试:
导入: 610000 [run 5309.4ms]
导入: 620000 [run 5290.9ms]
导入: 630000 [run 4837.2ms]
导入: 640000 [run 4927.6ms]
导入: 650000 [run 5749.3ms]
导入: 660000 [run 4754.5ms]
导入: 670000 [run 5378.4ms]
导入: 680000 [run 4548.4ms]
导入: 690000 [run 4558.4ms]
可以看到1万条一次需要大概5秒。
如果把索引去掉:
导入: 1200000 [run 52.5ms]
导入: 1210000 [run 53.6ms]
导入: 1220000 [run 52.5ms]
导入: 1230000 [run 53.7ms]
导入: 1240000 [run 53ms]
导入: 1250000 [run 52.8ms]
导入: 1260000 [run 60.8ms]
导入: 1270000 [run 52.7ms]
导入: 1280000 [run 55.3ms]
导入: 1290000 [run 55.3ms]
快了近100倍。
像一些冷数据,导入量比较大时,把索引去除加快导入速度。等导入完,重建索引,效率反而提升。
大量数据查询优化
当数据量返回过大时,除了必要的索引优化外,对数据集返回数的限制尤为必要。
请使用skip和limit做数据限制。
MongoDB性能分析和优化
explain
在查询语句后面跟上explain()能获取查询相关诊断信息
比如:
> db.getCollection("m_pass_base").find({_id:{$regex:/^malu/}}).explain(true)
返回:
{
"queryPlanner": {
"plannerVersion": NumberInt("1"),
"namespace": "d1.m_pass_base", // 查询的集合
"indexFilterSet": false, // 索引过滤
"parsedQuery": { // 查询条件
"_id": {
"$regex": "^malu"
}
},
"winningPlan": { // 最佳执行计划
"stage": "FETCH",
"inputStage": {
"stage": "IXSCAN",
"keyPattern": {
"_id": NumberInt("1")
},
"indexName": "_id_",
"isMultiKey": false,
"multiKeyPaths": {
"_id": [ ]
},
"isUnique": true,
"isSparse": false,
"isPartial": false,
"indexVersion": NumberInt("2"),
"direction": "forward",
"indexBounds": { // 当前查询具体使用的索引
"_id": [
"[\"malu\", \"malv\")",
"[/^malu/, /^malu/]"
]
}
}
},
"rejectedPlans": [ ] // 拒绝执行计划
},
"executionStats": { // executionStats会返回最佳执行计划的一些统计信息
"executionSuccess": true, // 是否执行成功
"nReturned": NumberInt("466"), // 返回结果数
"executionTimeMillis": NumberInt("0"),
"totalKeysExamined": NumberInt("467"), // 索引扫描数
"totalDocsExamined": NumberInt("466"), // 文档扫描数
"executionStages": {
"stage": "FETCH", // 扫描方式
"nReturned": NumberInt("466"),
"executionTimeMillisEstimate": NumberInt("0"),
"works": NumberInt("468"),
"advanced": NumberInt("466"),
"needTime": NumberInt("1"),
"needYield": NumberInt("0"),
"saveState": NumberInt("3"),
"restoreState": NumberInt("3"),
"isEOF": NumberInt("1"),
"invalidates": NumberInt("0"),
"docsExamined": NumberInt("466"),
"alreadyHasObj": NumberInt("0"),
"inputStage": {
"stage": "IXSCAN",
"nReturned": NumberInt("466"),
"executionTimeMillisEstimate": NumberInt("0"),
"works": NumberInt("468"),
"advanced": NumberInt("466"),
"needTime": NumberInt("1"),
"needYield": NumberInt("0"),
"saveState": NumberInt("3"),
"restoreState": NumberInt("3"),
"isEOF": NumberInt("1"),
"invalidates": NumberInt("0"),
"keyPattern": {
"_id": NumberInt("1")
},
"indexName": "_id_",
"isMultiKey": false,
"multiKeyPaths": {
"_id": [ ]
},
"isUnique": true,
"isSparse": false,
"isPartial": false,
"indexVersion": NumberInt("2"),
"direction": "forward",
"indexBounds": { // 当前查询具体使用的索引
"_id": [
"[\"malu\", \"malv\")",
"[/^malu/, /^malu/]"
]
},
"keysExamined": NumberInt("467"),
"seeks": NumberInt("2"),
"dupsTested": NumberInt("0"),
"dupsDropped": NumberInt("0"),
"seenInvalidated": NumberInt("0")
}
},
"allPlansExecution": [ ] // 所有执行计划
},
"serverInfo": {
"host": "M1",
"port": NumberInt("27017"),
"version": "3.6.8",
"gitVersion": "8e540c0b6db93ce994cc548f000900bdc740f80a"
},
"ok": 1
}
扫描方式stage有如下几种:
COLLSCAN:全表扫描
IXSCAN:索引扫描
FETCH:根据索引去检索指定document
SHARD_MERGE:将各个分片返回数据进行merge
SORT:表明在内存中进行了排序
LIMIT:使用limit限制返回数
SKIP:使用skip进行跳过
IDHACK:针对_id进行查询
SHARDING_FILTER:通过mongos对分片数据进行查询
COUNT:利用db.coll.explain().count()之类进行count运算
COUNTSCAN:count不使用Index进行count时的stage返回
COUNT_SCAN:count使用了Index进行count时的stage返回
SUBPLA:未使用到索引的$or查询的stage返回
TEXT:使用全文索引进行查询时候的stage返回
PROJECTION:限定返回字段时候stage的返回
所以对于查询优化,我们希望看到stage的组合是(查询的时候尽可能用上索引):
Fetch+IDHACK
Fetch+ixscan
Limit+(Fetch+ixscan)
PROJECTION+ixscan
SHARDING_FITER+ixscan
COUNT_SCAN
而不希望看到包含如下的stage:
COLLSCAN(全表扫描)
SORT(使用sort但是无index)
不合理的SKIP
SUBPLA(未用到index的$or)
COUNTSCAN(不使用index进行count)
hint
hint 可以强制 MongoDB 使用一个指定的索引,一般我们在联合索引上做优化。
hint({“$natural”:true}) 可以强制查询走全表扫描,这种情况适合在返回数据集很大的时候,不走索引反而效率更高。
MongoDB慢查询
官方文档:https://docs.mongodb.com/manual/reference/database-profiler/
开启慢查询Profiling
Profiling级别说明
0:关闭,不收集任何数据。
1:收集慢查询数据,默认是100毫秒。
2:收集所有数据
方式一:配置文件开启Profiling
修改启动mongo.conf,插入以下代码
#开启慢查询,200毫秒的记录
profile = 1
slowms = 200
方式二:通过命令开启
注意该方式只保留在内存中,重启mongo将失效
#查看状态:级别和时间
drug:PRIMARY> db.getProfilingStatus()
{ "was" : 1, "slowms" : 100 }
#查看级别
drug:PRIMARY> db.getProfilingLevel()
1
#设置级别
drug:PRIMARY> db.setProfilingLevel(2)
{ "was" : 1, "slowms" : 100, "ok" : 1 }
#设置级别和时间
drug:PRIMARY> db.setProfilingLevel(1,200)
{ "was" : 2, "slowms" : 100, "ok" : 1 }
修改“慢查询日志”的大小
#关闭Profiling
drug:PRIMARY> db.setProfilingLevel(0)
{ "was" : 0, "slowms" : 200, "ok" : 1 }
#删除system.profile集合
drug:PRIMARY> db.system.profile.drop()
true
#创建一个新的system.profile集合
drug:PRIMARY> db.createCollection( "system.profile", { capped: true, size:4000000 } )
{ "ok" : 1 }
#重新开启Profiling
drug:PRIMARY> db.setProfilingLevel(1)
{ "was" : 0, "slowms" : 200, "ok" : 1 }
慢日志示例:
{
"op": "command", // 操作类型,有insert、query、update、remove、getmore、command
"ns": "d1.m_pass_base", // 操作的集合
"command": { // 查询语句
"aggregate": "m_pass_base",
"pipeline": [
{
"$match": {
"_id": /^1/
}
},
{
"$group": {
"_id": NumberInt("1"),
"n": {
"$sum": NumberInt("1")
}
}
}
],
"allowDiskUse": false,
"cursor": { },
"$db": "d1",
"lsid": {
"id": UUID("e4f7b72e-b69a-42a9-91e5-ec85c7c11f2a")
}
},
"keysExamined": NumberInt("77810366"),
"docsExamined": NumberInt("0"),
"cursorExhausted": true,
"numYield": NumberInt("607894"),
"locks": {
"Global": {
"acquireCount": {
"r": NumberLong("1215792")
}
},
"Database": {
"acquireCount": {
"r": NumberLong("607896")
}
},
"Collection": {
"acquireCount": {
"r": NumberLong("607896")
}
}
},
"nreturned": NumberInt("1"),
"responseLength": NumberInt("111"),
"protocol": "op_msg",
"millis": NumberInt("42190"), // 消耗的时间(毫秒)
"planSummary": "IXSCAN { _id: 1 }",
"ts": ISODate("2021-04-06T13:43:56.792Z"), // 语句执行的时间
"client": "192.168.50.1",
"allUsers": [ ],
"user": ""
}
日常使用的查询
#返回最近的10条记录
db.system.profile.find().limit(10).sort({ ts : -1 }).pretty()
#返回所有的操作,除command类型的
db.system.profile.find( { op: { $ne : 'command' } } ).pretty()
#返回特定集合
db.system.profile.find( { ns : 'mydb.test' } ).pretty()
#返回大于5毫秒慢的操作
db.system.profile.find( { millis : { $gt : 5 } } ).pretty()
#从一个特定的时间范围内返回信息
db.system.profile.find(
{
ts : {
$gt : new ISODate("2021-04-06T03:00:00Z") ,
$lt : new ISODate("2021-04-06T03:40:00Z")
}
}
).pretty()
#特定时间,限制用户,按照消耗时间排序
db.system.profile.find(
{
ts : {
$gt : new ISODate("2021-04-06T03:00:00Z") ,
$lt : new ISODate("2021-04-06T03:40:00Z")
}
},
{ user : 0 }
).sort( { millis : -1 } )