MongoDb整体性能优化及基于查看执行计划的索引优化(含非范式物理设计,版本,监控维度)

--MongoDb整体性能优化及基于查看执行计划的索引优化(含物理设计,版本选择,范式策略和建议)
【官网】:https://www.mongodb.com/

应用场景

很多时候MongoDb用上了,但还是遇到性能问题。这时我们就需要至少考虑以下情况了: 1)内存是否充足. 2)(BTree 树)索引不命中所占百分比. 3)全局写入锁占用了机器多少时间。发生全局写入锁时,所有查询都将等待,直到锁解除. 4)等待处理的查询请求队列大小. 5)当前连接数.

基础资源

使用须知

Mongodb性能调优不是最终或最有效的手段,最高效的方法是做出好的物理设计。而什么样的物理设计适合Mongodb,最后还是由当前业务及业务未来发展趋势决定的。最后送给大家一句话“好的性能不是调出来的,更多是设计出来的”!

配置步骤

A)MongoDb查询慢,性能不好的监测点。

mongostat:

A1)faults/s:每秒访问失败数,

即数据被交换出物理内存,放到SWAP。若过高(一般超过100),则意味着内存不足。
vmstat & iostat & iotop

[注]

si:每秒从磁盘读入虚拟内存的大小,若大于0,表示物理内存不足。
so:每秒虚拟内存写入磁盘的大小,若大于0,同上。

A2) idx miss %:BTree 树未命中的比例,即索引不命中所占百分比.

若过高,则意味着索引建立或使用不合理。
db.serverStatus()
indexCounters” : {
“btree” : {
“accesses” : 2821726, #索引被访问数
“hits” : 2821725, #索引命中数
“misses” : 1, #索引偏差数
“resets” : 0, #复位数
“missRatio” : 3.543930204420982e-7 #未命中率
}

A3)locked %:全局写入锁占用了机器多少时间.

当发生全局写入锁时,所有查询操作都将等待,直到写入锁解除。
若过高(一般超过50%),则意味着程序存在问题。
db.currentOp()
{
“inprog” : [ ],
“fsyncLock” : 1, #为1表示MongoDB的fsync进程(负责将写入改变同步到磁盘)不允许其他进程执行写数据操作
“info” : “use db.fsyncUnlock() to terminate the fsync write/snapshot lock”
}

A4)q r|w :等待处理的查询请求队列大小.

若过高,则意味着查询会过慢。
db.serverStatus()
“currentQueue” : {
“total” : 1024, #当前需要执行的队列
“readers” : 256, #读队列
“writers” : 768 #写队列
}

A5)conn :当前连接数.

高并发下,若连接数上不去,则意味着Linux系统内核需要调优。
db.serverStatus()
“connections” : {
“current” : 3, #当前连接数
“available” : 19997 #可用连接数
}

A6)连接数使用内存.

cat /proc/$(pidof mongod)/limits | grep stack | awk -F ‘size‘ ‘{print int($NF)/1024}‘

将连接数使用Linux栈内存设小,默认为10MB(10240)

shell> ulimit -s 1024

B)查看执行计划,并分析结果。

B1)优化器的设置。

db.setProfilingLevel(2);
0 – 不开启
1 – 记录慢命令 (默认为>100ms)
2 – 记录所有命令
info: #本命令的详细信息
reslen: #返回结果集的大小
nscanned: #本次查询扫描的记录数
nreturned: #本次查询实际返回的结果集
millis: #该命令执行耗时(毫秒)

B2)监控一个集合时一般关注的信息有哪些?.


  1. 集合 collect_x 未建立有效索引(建议考虑使用组合索引)
  2. 存在大量慢查询,均为collect_x读操作,且响应超过1秒
  3. 每次读操作均为全集合扫描,意味着耗用CPU(25% * 8核)
  4. 每次返回的记录字节数近1KB,建议过滤不必要的字段,提高传输效率。


B3)如何查询执行计划?

>db.Book.find({CharsCount: “200000”}).hint({CharsCount:1 }).explain();

B4)分析查询计划。

分析案例1)


[注]
cursor: 返回游标类型(BasicCursor 或 BtreeCursor)
nscanned: 被扫描的文档数量
n: 返回的文档数量
millis: 耗时(毫秒)
indexBounds: 所使用的索引.


分析案例2)


{
    "queryPlanner" : {
        "plannerVersion" : 1,
        "namespace" : "myDatabase.myColl",
        "indexFilterSet" : false,
        "parsedQuery" : ...
        "winningPlan" : {
            "stage" : "LIMIT",
            "limitAmount" : 1,
            "inputStage" : {
                "stage" : "FETCH",
                "filter" : ...,
                "inputStage" : {
                    "stage" : "IXSCAN",
                    "keyPattern" : {
                        "_id" : 1
                    },
                    "indexName" : "_id_",
                    ...
                    "direction" : "backward",
                    "indexBounds" : {
                        "_id" : [
                            "[MaxKey, MinKey]"
                        ]
                    }
                }
            }
        },
        "rejectedPlans" : ...,
    },
    "executionStats" : {
        "executionSuccess" : true,
        "nReturned" : 1,
        "executionTimeMillis" : 0,
        "totalKeysExamined" : 8,
        "totalDocsExamined" : 8,
        "executionStages" : {
            "stage" : "LIMIT",
            ...
            "inputStage" : {
                "stage" : "FETCH",
                ...
                "inputStage" : {
                    "stage" : "IXSCAN",
                    ...
                    "direction" : "backward",
                    "indexBounds" : {
                        "_id" : [
                            "[MaxKey, MinKey]"
                        ]
                    },
                    "keysExamined" : 8,
                    ...
                }
            }
        }
    },
    "serverInfo" : ...,
    "ok" : 1
}
[注]

explain()结果分为四部分:queryPlanner、executionStats、serverInfo、ok,仅关注queryPlanner、executionStats这两部分。
executionStats就是执行queryPlanner.winningPlan这个计划时的统计信息。
1)从indexBounds看到good query在索引扫描(IXSCAN)阶段,使用的索引是_id主键索引。

    "indexName" : "_id_",



    "indexBounds" : {

2)从IXSCAN这个阶段的keysExamined统计可以解释为什么good query执行的这么快,只扫描了8条数据。 


   //"keysExamined" : 8,

B5)查询计划explain结果结构描述.

  

对queryPlanner分析

    queryPlanner: queryPlanner的返回

    queryPlanner.namespace:该值返回的是该query所查询的表

    queryPlanner.indexFilterSet:针对该query是否有indexfilter

    queryPlanner.winningPlan:查询优化器针对该query所返回的最优执行计划的详细内容。

    queryPlanner.winningPlan.stage:最优执行计划的stage,这里返回是FETCH,可以理解为通过返回的index位置去检索具体的文档(stage有数个模式,将在后文中进行详解)。

    queryPlanner.winningPlan.inputStage:用来描述子stage,并且为其父stage提供文档和索引关键字。

    queryPlanner.winningPlan.stage的child stage,此处是IXSCAN,表示进行的是index scanning。

    queryPlanner.winningPlan.keyPattern:所扫描的index内容,此处是did:1,status:1,modify_time: -1与scid : 1

    queryPlanner.winningPlan.indexName:winning plan所选用的index。

    queryPlanner.winningPlan.isMultiKey是否是Multikey,此处返回是false,如果索引建立在array上,此处将是true。

    queryPlanner.winningPlan.direction:此query的查询顺序,此处是forward,如果用了.sort({modify_time:-1})将显示backward。

    queryPlanner.winningPlan.indexBounds:winningplan所扫描的索引范围,如果没有制定范围就是[MaxKey, MinKey],这主要是直接定位到mongodb的chunck中去查找数据,加快数据读取。

    queryPlanner.rejectedPlans:其他执行计划(非最优而被查询优化器reject的)的详细返回,其中具体信息与winningPlan的返回中意义相同,故不在此赘述。

对executionStats返回逐层分析

    第一层,executionTimeMillis

    最为直观explain返回值是executionTimeMillis值,指的是我们这条语句的执行时间,这个值当然是希望越少越好。

    其中有3个executionTimeMillis,分别是:

    executionStats.executionTimeMillis

    该query的整体查询时间。

    executionStats.executionStages.executionTimeMillisEstimate

    该查询根据index去检索document获得2001条数据的时间。

    executionStats.executionStages.inputStage.executionTimeMillisEstimate

    该查询扫描2001行index所用时间。

    第二层,index与document扫描数与查询返回条目数

    这个主要讨论3个返回项,nReturned、totalKeysExamined、totalDocsExamined,分别代表该条查询返回的条目、索引扫描条目、文档扫描条目。

    这些都是直观地影响到executionTimeMillis,我们需要扫描的越少速度越快。

    对于一个查询,我们最理想的状态是:

    nReturned=totalKeysExamined=totalDocsExamined

    第三层,stage状态分析

    那么又是什么影响到了totalKeysExamined和totalDocsExamined?是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)




常见问题

快速入门

A)MongoDb引擎版本的选择.

  1. Mongodb 3.0.X版本性能较Mongodb 2.0.X有7-10倍提升,引入WiredTiger新引擎,同时支持MMAPv1内存映射引擎

 默认MMAPv1,切换至WiredTiger:mongod –dbpath /usr/local/mongodb/data –storageEngine wiredTiger
备注:若更换新引擎,则之前使用旧引擎建立的DB数据库无法使用。 建议先通过Mongodb的同步机制,将旧引擎建立的DB数据同步到从库, 且从库使用新引擎.
 选择 Windows 2008 R2 x64 或 Linux x64,Linux版本性能优于 Windows,建议基于Linux系统进行架构选型
 根据RHEL版本号选择Mongodb相应Linux版本
 Mongodb Driver 与 Mongodb 版本一致


B)Mongodb的集合物理设计怎么做才有利于提升性能(范式,反范式,折中)?.

       范式: 更新时速度快,只更新一条(因为遵循一事一地原则).

     反范式:   查询时速度快,只查询一条(或者关联更少),因为都在个结构中。

     折中:  对于静态的(无法修改或者几乎不需要修改)的数据进行反范式设计(旨在提高查询效率),对于经常更新的则遵从范式。

C)如何设计并创建索引?

c1)在查询条件、排序条件、统计条件的字段上选择创建索引.

  示例:  db.Book.ensureIndex({ BookPrice:1,VisitCount:500} , {backgroud:true});

 最新或最近记录查询,结合业务需要正确使用索引方向:逆序或顺序
 建议索引建立操作置于后台运行,降低影响
 实际应用过程中多考虑使用复合索引
 使用limit()限定返回结果集的大小,减少数据库服务器的资源消耗,以及网络传输的数据量

 颗粒度越小越好,也就是值的分布比较均匀,重复度比较小。
db.posts.find().sort({ts:-1}).limit(10);


c2)只查询使用到的字段,而不查询所有字段.

参考语法:


db.collection.find(query, projection);//projection是投影,也就是返回的列。
示例:


db.Book.find({"_id":"0bd937c891bd3135e02"},{"BookName":true,"PublishDate":true});  //注:对于_id来说,不管有没有设置是否返回都会返回。

D)分布式环境下考虑使用MapReduce


基于Mongodb分布式集群做数据分析时,MapReduce性能优于count、distinct、group等聚合函数


E)考虑使用Capped Collections (固定长度,环形存储,过期后会覆盖之前的).

  1. Capped Collections比普通Collections的读写效率高
    db.createCollection(“mycoll”, {capped:true, size:100000});
    例:system.profile 是一个Capped Collection。
    注意:
     固定大小;Capped Collections 必须事先创建,并设置大小。
     Capped Collections可以insert和update操作;不能delete操作。只能用 drop()方法删除整个Collection。
     默认基于 Insert 的次序排序的。如果查询时没有排序,则总是按照insert的顺序返回。
     FIFO。如果超过了Collection的限定大小,则用 FIFO 算法,新记录将替代最先 insert的记录。






参考资料