一、explain 执行计划
MongoDB 提供了一个 explain 命令让我们获知系统如何处理查询请求。利用 explain 命令,我们可以很好地观察系统如何使用索引来加快检索,同时可以针对性优化索引。
> db.t5.ensureIndex({name:1})
> db.t5.ensureIndex({age:1})
> db.t5.find({age:{$gt:45}}, {name:1}).explain()
{
"cursor" : "BtreeCursor age_1",
"nscanned" : 0,
"nscannedObjects" : 0,
"n" : 0,
"millis" : 0,
"nYields" : 0,
"nChunkSkips" : 0,
"isMultiKey" : false,
"indexOnly" : false,
"indexBounds" : {
"age" : [
[
45,
1.7976931348623157e+308
]
]
}
}
字段说明:
- cursor:返回游标类型(BasicCursor 或 BtreeCursor) ;
- nscanned:被扫描的文档数量;
- n:返回的文档数量;
- millis:耗时(毫秒) ;
- indexBounds:所使用的索引;
二、优化器 profile
在 MySQL 中,慢查询日志是经常作为我们优化数据库的依据,那在 MongoDB 中是否有类似的功能呢?
答案是肯定的,那就是 MongoDB Database Profiler。所以 MongoDB 不仅有,而且还有一些比 MySQL 的 Slow Query Log 更详细的信息。
1、开启 Profiling 功能
有两种方式可以控制 Profiling 的开关和级别,第一种是直接在启动参数里直接进行设置。启动 MongoDB 时加上–profile=级别 即可。
也可以在客户端调用 db.setProfilingLevel(级别) 命令来实时配置,Profiler 信息保存在system.profile 中。
我们可以通过 db.getProfilingLevel()命令来获取当前的 Profile 级别,类似如下操作:
> db.setProfilingLevel(2);
{ "was" : 0, "slowms" : 100, "ok" : 1 }
上面 profile 的级别可以取 0,1,2 三个值,他们表示的意义如下:
- 0 – 不开启;
- 1 – 记录慢命令 (默认为>100ms);
- 2 – 记录所有命令;
Profile 记录在级别 1 时会记录慢命令,那么这个慢的定义是什么?上面我们说到其默认为100ms,当然有默认就有设置,其设置方法和级别一样有两种,一种是通过添加–slowms 启动参数配置。第二种是调用 db.setProfilingLevel 时加上第二个参数:
db.setProfilingLevel( level , slowms )
db.setProfilingLevel( 1 , 10 );
2、查询 Profiling 记录
与 MySQL 的慢查询日志不同,MongoDB Profile 记录是直接存在系统 db 里的,记录位置
system.profile ,所以,我们只要查询这个 Collection 的记录就可以获取到我们的 Profile 记录了。
列出执行时间长于某一限度(5ms)的 Profile 记录:
db.system.profile.find( { millis : { $gt : 5 } } )
查看最新的 Profile 记录:
db.system.profile.find().sort({$natural:-1}).limit(1)
> db.system.profile.find().sort({$natural:-1}).limit(1)
{ "ts" : ISODate("2018-05-20T16:50:36.321Z"), "info" : "query test.system.profile reslen:1219
nscanned:8 \nquery: { query: {}, orderby: { $natural: -1.0 } } nreturned:8 bytes:1203", "millis" :
0 }
>
字段说明:
- ts: 该命令在何时执行;
- info: 本命令的详细信息;
- reslen: 返回结果集的大小;
- nscanned: 本次查询扫描的记录数;
- nreturned: 本次查询实际返回的结果集;
- millis: 该命令执行耗时,以毫秒记;
MongoDB Shell 还提供了一个比较简洁的命令 show profile,可列出最近 5 条执行时间超过1ms 的 Profile 记录。
三、性能优化案例
如果 nscanned(扫描的记录数)远大于 nreturned(返回结果的记录数)的话,那么我们就要考虑通过加索引来优化记录定位了。reslen 如果过大,那么说明我们返回的结果集太大了,这时请查看 find 函数的第二个参数是否只写上了你需要的属性名。
对于创建索引的建议是:如果很少读,那么尽量不要添加索引,因为索引越多,写操作会越慢。如果读量很大,那么创建索引还是比较划算的。
假设我们按照时间戳查询最近发表的 10 篇博客文章:
articles = db.posts.find().sort({ts:-1});
for (var i=0; i< 10; i++) {
print(articles[i].getSummary());
}
1、优化方案1 :创建索引
在查询条件的字段上,或者排序条件的字段上创建索引,可以显著提高执行效率:
db.posts.ensureIndex({ts:1});
2、优化方案 2:限定返回结果条数
使用 limit()限定返回结果集的大小,可以减少 database server 的资源消耗,可以减少网络传输数据量。
articles = db.posts.find().sort({ts:-1}).limit(10);
3、优化方案 3: 只查询使用到的字段,而不查询所有字段
在本例中,博客日志记录内容可能非常大,而且还包括了评论内容(作为 embeded 文档)。
所以只查询使用的字段,比查询所有字段效率更高:
articles = db.posts.find({}, {ts:1,title:1,author:1,abstract:1}).sort({ts:-1}).limit(10);
注意:如果只查询部分字段的话,不能用返回的对象直接更新数据库。
下面的代码是错误的:
a_post = db.posts.findOne({}, Post.summaryFields);
a_post.x = 3;
db.posts.save(a_post);
4、优化方案 4:采用 capped collection
capped Collections 比普通 Collections 的读写效率高。Capped Collections 是高效率的 Collection
类型,它有如下特点:
- 固定大小;Capped Collections 必须事先创建,并设置大小:
db.createCollection("mycoll", {capped:true, size:100000})
- Capped Collections 可以 insert 和 update 操作;不能 delete 操作。只能用 drop()方法删除整个 Collection。
- 默认基于 Insert 的次序排序的。如果查询时没有排序,则总是按照 insert 的顺序返回。
- FIFO:如果超过了 Collection 的限定大小,则用 FIFO 算法,新记录将替代最先 insert 的记录。
5、优化方案 5:采用 Server Side Code Execution
Server-Side Processing 类似于 SQL 数据库的存储过程,使用 Server-Side Processing 可以减小网络通讯的开销。
6、优化方案 6:Hint
一般情况下 MongoDB query optimizer 都工作良好,但有些情况下使用 hint()可以提高操作效率。
Hint 可以强制要求查询操作使用某个索引。例如,如果要查询多个字段的值,如果在其中一个字段上有索引,可以使用 hint:
db.collection.find({user:u, foo:d}).hint({user:1});
7、优化方案 7:采用 Profiling
Profiling 功能肯定是会影响效率的,但是不太严重,原因是他使用的是 system.profile 来记录,而 system.profile 是一个 capped collection 这种 collection 在操作上有一些限制和特点,但是效率更高。
四、性能监控
1、mongosniff
此工具可以从底层监控到底有哪些命令发送给了 MongoDB 去执行,从中就可以进行分析。
以 root 身份执行:
./mongosniff --source NET lo
然后其会监控位到本地以 localhost 监听默认 27017 端口的 MongoDB 的所有包请求,如执行”show dbs” 操作。
[root@localhost bin]# ./mongo
MongoDB shell version: 1.8.1
connecting to: test
> show dbs
admin 0.0625GB
foo 0.0625GB
local (empty)
test 0.0625GB
>
那么你可以看到如下输出:
[root@localhost bin]# ./mongosniff --source NET lo
sniffing... 27017
127.0.0.1:38500 -->> 127.0.0.1:27017 admin.$cmd 60 bytes id:537ebe0f 1400815119
query: { whatsmyuri: 1 } ntoreturn: 1 ntoskip: 0
127.0.0.1:27017 <<-- 127.0.0.1:38500 78 bytes id:531c3855 1394358357 - 1400815119
reply n:1 cursorId: 0
{ you: "127.0.0.1:38500", ok: 1.0 }
127.0.0.1:38500 -->> 127.0.0.1:27017 admin.$cmd 80 bytes id:537ebe10 1400815120
query: { replSetGetStatus: 1, forShell: 1 } ntoreturn: 1 ntoskip: 0
127.0.0.1:27017 <<-- 127.0.0.1:38500 92 bytes id:531c3856 1394358358 - 1400815120
reply n:1 cursorId: 0
{ errmsg: "not running with --replSet", ok: 0.0 }
127.0.0.1:38500 -->> 127.0.0.1:27017 admin.$cmd 67 bytes id:537ebe11 1400815121
query: { listDatabases: 1.0 } ntoreturn: -1 ntoskip: 0
127.0.0.1:27017 <<-- 127.0.0.1:38500 293 bytes id:531c3857 1394358359 - 1400815121
reply n:1 cursorId: 0
{ databases: [ { name: "foo", sizeOnDisk: 67108864.0, empty: false }, { name: "test",
sizeOnDisk: 67108864.0, empty: false }, { name: "admin", sizeOnDisk: 67108864.0, empty: false },
{ name: "local", sizeOnDisk: 1.0, empty: true } ], totalSize: 201326592.0, ok: 1.0 }
127.0.0.1:38500 -->> 127.0.0.1:27017 admin.$cmd 80 bytes id:537ebe12 1400815122
query: { replSetGetStatus: 1, forShell: 1 } ntoreturn: 1 ntoskip: 0
127.0.0.1:27017 <<-- 127.0.0.1:38500 92 bytes id:531c3858 1394358360 - 1400815122
reply n:1 cursorId: 0
{ errmsg: "not running with --replSet", ok: 0.0 }
如果将这些输出到一个日志文件中,那么就可以保留下所有数据库操作的历史记录,对于后期的性能分析和安全审计等工作将是一个巨大的贡献。
2、Mongostat
此工具可以快速的查看某组运行中的 MongoDB 实例的统计信息,用法如下:
[root@localhost bin]# ./mongostat
下面是执行结果(部分):
[root@localhost bin]# ./mongostat
insert query update delete ...... locked % idx miss % qr|qw ar|aw conn time
*0 *0 *0 *0 ...... 0 0 0|0 1|0 4 01:19:15
*0 *0 *0 *0 ...... 0 0 0|0 1|0 4 01:19:16
*0 *0 *0 *0 ...... 0 0 0|0 1|0 4 01:19:17
字段说明:
- insert: 每秒插入量;
- query: 每秒查询量;
- update: 每秒更新量;
- delete: 每秒删除量;
- locked: 锁定量;
- qr | qw: 客户端查询排队长度(读|写);
- ar | aw: 活跃客户端量(读|写) ;
- conn: 连接数;
- time: 当前时间;
它每秒钟刷新一次状态值,提供良好的可读性,通过这些参数可以观察到一个整体的性能情况。
3、db.serverStatus
这个命令是最常用也是最基础的查看实例运行状态的命令之一,下面我们看一下它的输出:
> db.serverStatus()
{
"host" : "localhost.localdomain",
"version" : "1.8.1", --服务器版本
"process" : "mongod",
"uptime" : 3184, --启动时间(秒)
"uptimeEstimate" : 3174,
"localTime" : ISODate("2018-05-28T11:20:22.819Z"),
"globalLock" : {
"totalTime" : 3183918151,
"lockTime" : 10979,
"ratio" : 0.000003448267034299149,
"currentQueue" : {
"total" : 0, --当前全部队列量
"readers" : 0, --读请求队列量
"writers" : 0 --写请求队列量
},
"activeClients" : {
"total" : 0, --当前全部客户端连接量
"readers" : 0, --客户端读请求量
"writers" : 0 --客户端写请求量
}
},
"mem" : {
"bits" : 32, --32 位系统
"resident" : 20, --占用物量内存量
"virtual" : 126, --虚拟内存量
"supported" : true, --是否支持扩展内存
"mapped" : 32
},
"connections" : {
"current" : 1, --当前活动连接量
"available" : 818 --剩余空闲连接量
},
……
"indexCounters" : {
"btree" : {
"accesses" : 0, --索引被访问量
"hits" : 0, --索引命中量
"misses" : 0, --索引偏差量
"resets" : 0,
"missRatio" : 0 --索引偏差率(未命中率)
}
},
……
"network" : {
"bytesIn" : 1953, --发给此服务器的数据量(单位:byte)
"bytesOut" : 25744, --此服务器发出的数据量(单位:byte)
"numRequests" : 30 --发给此服务器的请求量
},
"opcounters" : {
"insert" : 0, --插入操作的量
"query" : 1, --查询操作的量
"update" : 0, --更新操作的量
"delete" : 0, --删除操作的量
"getmore" : 0,
"command" : 31 --其它操作的量
},
……
"ok" : 1
}
>
此工具提了比较详细的信息,以上已经对主要的一些参数做了说明,请大家参考。
4、db.stats
db.stats 查看数据库状态信息。使用样例如下:
> db.stats()
{
"db" : "test",
"collections" : 7, --collection 数量
"objects" : 28, --对象数量
"avgObjSize" : 50.57142857142857, --对象平均大小
"dataSize" : 1416, --数据大小
"storageSize" : 31744, --数据大小(含预分配空间)
"numExtents" : 7, --事件数量
"indexes" : 7, --索引数量
"indexSize" : 57344, --索引大小
"fileSize" : 50331648, --文件大小
"ok" : 1 --本次取 stats 是否正常
}
>
通过这个工具,可以查看所在数据库的基本信息。
5、第三方工具
MongoDB 从一面世就得到众多开源爱好者和团队的重视,在常用的监控框架如 cacti、Nagios、Zabbix 等基础上进行扩展,进行 MongoDB 的监控都是非常方便的,有兴趣的朋友可以自已试试。