上一篇文章分析了mongod的数据库加载部分,下面这一篇文章将继续分析mongod cursor的产生,这里cursor
的生成应该是mongodb系统中最复杂的部分.下面先介绍几个关于mongodb的游标概念.
basicCursor: 直接扫描整个collection的游标,可设置初始的扫描位置,扫描为顺序扫描.
ReverseCursor: 反向扫描游标,相对于顺序扫描,这里是反向扫描.
ReverseCappedCursor: cap集合的反向扫描游标.
ForwardCappedCursor: cap集合的顺序扫描游标.
GeoCursorBase: 空间地理索引游标的基类,我并未阅读相关代码,感兴趣的自己研究吧.
BtreeCursor: mongodb的一般数据索引扫描游标,这个游标完成对于索引的扫描.
MultiCursor: 有待研究.
QueryOptimizerCursor: 经过优化的扫描游标,多plan扫描时或者对于查询中有$or的语句且$or语句其作用时由于
优化查询的游标. 这里将简单描述其流程.
1. 如果是类似这种db.coll.find()的查询则将直接返回一个BasicCursor的游标扫描全表.
2. 如果是简单的id查询如db.coll.find(_id:xxx),且允许_id查询plan的情况下直接查询_id索引,返回一个_id索引
的BtreeCursor.
3.根据查询整理出查询值的范围,作为优化查询范围的依据,如:db.coll.find({x:{$lt:100,$gt:20}}),那么这里其范围就是[20.100],这个范围只有在对应的变量是索引的情况下起作用,如x为其中的一个索引,那么这里的范围将帮助其
游标BtreeCursor首先直接将查询范围定位到[20,100]的位置,这个工作对于由Btree组织的索引来说很简单.简单
来说就是优化查询范围.但是若x不是索引那么这里得到的查询范围将是无用的,这里将返回一个BasicCursor的
游标执行全表扫描.
4.根据得到的所有的查询域的范围比如说x:[10,20],y:[4,6]这种选取查询计划(QueryPlan).查询计划的选取这里举个
例子,有x,y两个查询域.index有{x:1},{y:1},{x:1,y:1}这几个索引,那么选取计划时发现只有索引{x:1,y:1}完全满足查
询计划,其是最优的,那么确定选取这个索引为查询索引.返回唯一的QueryPlan,最后生成一个确切的
BtreeCursor.但是如果没有{x:1,y:1}这个索引怎么办呢?那么剩下两个索引{x:1},{y:1}都部分包含了查询域,他们
都是有作用的,于是乎生成了两个QueryPlan,一个对应于索引{x:1},一个对应于索引{y:1},于是乎使用
QueryOptimizerCursor这个cursor管理两个BtreeCursor,每次交替执行两个BtreeCursor的查询,直到一个
BtreeCursor查询完毕,那么这个plan是所有plan中最优的,将其缓存起来,下一次同样查询时直接选择这个plan作
为查询的plan.因为两个plan中首先完成扫描的plan查询的次数最少.那么两个plan都查询到的同一条满足查询要
求的数据怎么办,查询结尾会有一个对于满足要求的document地址的记录,如果一条满足要求的document的地址
已经在记录中了,就不再记录这个document.
5.$or查询的优化,对于一个$or举例来说明:{$or:[{x:1},{y:2},{z:3,a:4}]}这样的查询请求,这样要当$or中的每一个查
询域,中至少一个域是可用的索引比如说有索引x,y,a那么这个$or才是有意义的.如果这个$or有意义,那么这里将
使用QueryOptimizerCursor来处理每一个$or中的查询域,比如说{x:1},然后这里退化到4,plan的选取,$or中的查
询一个一个的执行.回过头来分析,如果没有索引y,那么对于这个$or的查询因为满足y:2的文档将会被返回,那么
只能扫描全表,这时即使有索引x,z或者a这种也不能避免全表的扫描,那么这里的$or就变得没有优化的意义了.
另外如果查询指定了sort(xxx:1)按照某些域排序或者设定了最大值最小值$or也是无意义的.
6. 查询结束后的排序,当指定了如db.coll.find({x:1,y:2}).sort(z:1),这种需要按照z升序排列的查询时,这种情况就要
考虑当没有索引z时,那么排序是肯定避免不了的,查询的结果会放到一个map中,map按照z的升序来排序,当排序
的文档总大小超过了默认热32M最大值时会返回错误,提醒你应该为z域建立索引了.下面来看有索引时的状况.
(1),索引为{x:1},{z:1},如果这里两个索引查询的文档数目一样多,那么优先选择{x:1},因为建立索引时其比较靠前,然
后还是得排序.
(2)索引{x:1,z:1},{z:1,x:1},由于第一个索引查出来的顺序是按照x的顺序来排列的,那么还是得排序,第二个索引不需
要排序,但是考虑最优QueryPlan的选取是找到最先执行完plan的索引,这里仍然不会选取{z:1,x:1}这个plan,而
是会选取{x:1,z:1}这个plan.考虑到两个索引还不直观,这里加入一个{x:1},{x:1,z:1},{z:1,x:1},那么这里将会选择第
一个索引{x:1}.要让mongod选择{z:1,x:1}这plan只能使用db.coll.find({x:{$lt:5,$gt:0}).sort({z:1}).hint({z:1,x:1}),
总觉得这是一个bug,mongod应该能够修正这种情况才对,应该能自己选择最优的索引{z:1,x:1}才对.这里有一篇
10gen的工程师谈mongodb索引优化的文章可以一看:
http://www.youkuaiyun.com/article/2012-11-09/2811690-optimizing-mongodb-compound
上面介绍了那么多的流程情况下面正式进入代码分析阶段.接上篇文章runQuery->queryWithQueryOptimizer
string queryWithQueryOptimizer( int queryOptions, const string& ns,
const BSONObj &jsobj, CurOp& curop,
const BSONObj &query, const BSONObj &order,
const shared_ptr<ParsedQuery> &pq_shared,
const BSONObj &oldPlan,
const ConfigVersion &shardingVersionAtStart,
scoped_ptr<PageFaultRetryableSection>& parentPageFaultSection,
scoped_ptr<NoPageFaultsAllowed>& noPageFault,
Message &result ) {
const ParsedQuery &pq( *pq_shared );
shared_ptr<Cursor> cursor;
QueryPlanSummary queryPlan;
if ( pq.hasOption( QueryOption_OplogReplay ) ) {//用于oplog的回放的游标.
cursor = FindingStartCursor::getCursor( ns.c_str(), query, order );
}
else {
cursor =//本文的主要分析的部分,游标的获取
NamespaceDetailsTransient::getCursor( ns.c_str(), query, order, QueryPlanSelectionPolicy::any(),
0, pq_shared, false, &queryPlan );
}
scoped_ptr<QueryResponseBuilder> queryResponseBuilder
( QueryResponseBuilder::make( pq, cursor, queryPlan, oldPlan ) );
bool saveClientCursor = false;
OpTime slaveReadTill;
ClientCursor::Holder ccPointer( new ClientCursor( QueryOption_NoCursorTimeout, cursor,
ns ) );
for( ; cursor->ok(); cursor->advance() ) {
bool yielded = false;//这里的查询机制,当查询时间超过了一个给定的值,这里为10ms或者在一段时间内调用该函数超过了128次,又或者cursor指向的文档不在内存中,那么这里将睡眠一会儿,睡眠的时间由当前系统中读取读者数目r和写者数目w,由10*r+w决定,单位为ms,最大值不超过1000000.
if ( !ccPointer->yieldSometimes( ClientCursor::MaybeCovered, &yielded ) ||//睡眠前会通知游标保存当前位置
!cursor->ok() ) {//这里是睡眠完成后发现当前游标失效了
cursor.reset();
queryResponseBuilder->noteYield();
// !!! TODO The queryResponseBuilder still holds cursor. Currently it will not do
// anything unsafe with the cursor in handoff(), but this is very fragile.
//
// We don't fail the query since we're fine with returning partial data if the
// collection was dropped.
// NOTE see SERVER-2454.
// TODO This is wrong. The cursor could be gone if the closeAllDatabases command
// just ran.
break;
}
if ( yielded ) {//发生过yield,这是由两种情况构成,要么关心的数据不在内存,要么
queryResponseBuilder->noteYield();//clientCursor超过了ccPointer->_yieldSometimesTracker规定的yield时间
}
if ( pq.getMaxScan() && cursor->nscanned() > pq.getMaxScan() ) {//超过了用户查询时设置的最大的扫描扫描数目
break;
}
if ( !queryResponseBuilder->addMatch() ) {//具体查询的文档的匹配过程,下一篇文章将介绍
continue;
}
// Note slave's position in the oplog.
if ( pq.hasOption( QueryOption_OplogReplay ) ) {
BSONObj current = cursor->current();
BSONElement e = current["ts"];
if ( e.type() == Date || e.type() == Timestamp ) {
slaveReadTill = e._opTime();
}
}
if ( !cursor->supportGetMore() || pq.isExplain() ) {
if ( queryResponseBuilder->enoughTotalResults() ) {
break;
}
}
else if ( queryResponseBuilder->enoughForFirstBatch() ) {
// if only 1 requested, no cursor save