king

MongoDB源码分析–Command体系架构

king 运维技术 2022-11-19 405浏览 0

Command在Mongodb中是一类特殊操作,它提供了强大的管理及各项操作(比如建库,索引,删除集合等)。可以说通过Command可以完成几乎所有想做的事情。同时Mongodb开发者在Command上又做了非常清晰体系架构和设计,便于管理和高效执行各种类型的Command。

今天就专门用一篇篇幅来着重介绍一下其Command的体系架构,并用例子来介绍mongod是如何将Command引入其中的。

为了对其中大部分command对一个大致的了解,我们可以用下面指令来显示一个command列表:

  1. mongod –dbpath d:\mongodb\db –port 27017 –rest
  2. 在浏览器上输入链接地址:http://localhost:28017/_commands

    这里mongod就会为我们显示command列表,大约有90多个,这是显示截图:

    MongoDB源码分析–Command体系架构

    上面90多个类中,按其使用场景可以为分如下几类,分别是:

    1. dbcommand.cpp:一般数据库指令,如数据库,索引的创建,重建,打开/关闭等
    2. dbcommands_admin.cpp:管理指令,如CleanCmd,JournalLatencyTestCmd,ValidateCmd,FSyncCommand
    3. dbcommands_generic.cpp:常用指令,ListCommandsCmd,LogRotateCmd,PingCommand,CmdSet,CmdGet等
    4. replset_commands.cpp:复制集指令,CmdReplSetTest,CmdReplSetGetStatus,CmdReplSetReconfig等
    5. security_commands.cpp:安全指令,CmdGetNonce,CmdLogout,CmdAuthenticate

      1. commands_admin.cpp:shard管理操作,因其位于mongos项目,这里暂不介绍
      2. commands_public.cpp:shard公用操作,因其位于mongos项目,这里暂不介绍

        下面是相关类图:

        MongoDB源码分析–Command体系架构

        —————————–分割线——————————–

        MongoDB源码分析–Command体系架构

        —————————–分割线——————————–

        MongoDB源码分析–Command体系架构

        —————————–分割线——————————–

        MongoDB源码分析–Command体系架构

        —————————–分割线——————————–

        MongoDB源码分析–Command体系架构

        #p#

        首先我们看一下在Command的基类,其用于定义子类要实现的方法及属性,自身也实现了一些通用方法,比如htmlHelp(用于以html方法显示该command的帮助信息),构造方法,findCommand(查询命令)等,其声明如下:

        //commands.h 
        classCommand{ 
        public: 
        //执行当前Command时所使用的锁类型 
        enumLockType{READ=-1/*读*/,NONE=0/*无锁*/,WRITE=1/*写*/}; 
        
        conststringname; 
        
        /*运行指定的命令,需要子类实现 
        fromRepl-commandisbeinginvokedaspartofreplicationsyncing.Inthissituationyou 
        normallydonotwanttologthecommandtothelocaloplog. 
        
        如执行成功返回true,否则为false,errmsg记录错误信息 
        */
        virtualboolrun(conststring&db,BSONObj&cmdObj,string&errmsg,BSONObjBuilder&result,boolfromRepl)=0; 
        
        /* 
        note:logTheTop()MUSTbefalseifREAD 
        ifNONE,can'tuseClient::Contextsetup 
        usewithcaution 
        */
        virtualLockTypelocktype()const=0; 
        
        /*是否有管理特权才可运行该命令hasprivilegestorunthiscommand.*/
        virtualbooladminOnly()const{ 
        returnfalse; 
        } 
        //html格式的帮助信息 
        voidhtmlHelp(stringstream&)const; 
        
        /*与adminOnly相似,但更严格:要么被验证,要么只运行在本地接口(localinterface) 
        注:当本属性为true时,adminOnly()也必须为true. 
        */
        virtualboollocalHostOnlyIfNoAuth(constBSONObj&cmdObj){returnfalse;} 
        
        /*如果replicationpair的slaves可以运行命令的,则返回true 
        (thecommanddirectlyfromaclient--iffromRepl,alwaysallowed). 
        */
        virtualboolslaveOk()const=0; 
        
        /*通过在查询命令中打开'slaveok'选项,客户端强制在一个slave上运行一个命令时,返回true. 
        */
        virtualboolslaveOverrideOk(){ 
        returnfalse; 
        } 
        
        /*Overrideandreturntruetoiftrue,logtheoperation(logOp())tothereplicationlog. 
        (notdoneiffromReplofcourse) 
        
        Noteifrun()returnsfalse,wedoNOTlog. 
        */
        virtualboollogTheOp(){returnfalse;} 
        
        virtualvoidhelp(stringstream&help)const; 
        
        /*Returntrueifauthenticationandsecurityappliestothecommands.Somecommands 
        (e.g.,getnonce,authenticate)canbedonebyanyoneevenunauthorized. 
        */
        virtualboolrequiresAuth(){returntrue;} 
        
        /**@paramwebUI:在web上暴露当前command,形如localhost:28017/<name> 
        @paramoldName:旧选项,表示当前command的旧(已弃用)名称 
        */
        Command(constchar*_name,boolwebUI=false,constchar*oldName=0); 
        
        virtual~Command(){} 
        
        protected: 
        BSONObjgetQuery(constBSONObj&cmdObj){ 
        if(cmdObj["query"].type()==Object) 
        returncmdObj["query"].embeddedObject(); 
        if(cmdObj["q"].type()==Object) 
        returncmdObj["q"].embeddedObject(); 
        returnBSONObj(); 
        } 
        
        staticvoidlogIfSlow(constTimer&cmdTimer,conststring&msg); 
        //commandmap,其包含系统实现的所有command对象,以便findCommand查询时使用 
        //注意也包含该command的旧名称(构造方法中的oldName参数)所对应的对象, 
        staticmap<string,Command*>*_commands; 
        //与上面形同,但不含旧名称的commandmap 
        staticmap<string,Command*>*_commandsByBestName; 
        //将web类型的command放到该map中 
        staticmap<string,Command*>*_webCommands; 
        
        public: 
        staticconstmap<string,Command*>*commandsByBestName(){return_commandsByBestName;} 
        staticconstmap<string,Command*>*webCommands(){return_webCommands;} 
        /**@return返回是否找到或已执行command*/
        staticboolrunAgainstRegistered(constchar*ns,BSONObj&jsobj,BSONObjBuilder&anObjBuilder); 
        staticLockTypelocktype(conststring&name); 
        //根据命令名称在集合中找到相应Command对象 
        staticCommand*findCommand(conststring&name); 
        };
        

        Command基类中提供了几个map<string,Command*>类型的集合map,用于将系统实现的Command进行收集,以便后面findCommand进行便历查询时使用。如下:

        //commands.cpp 
        Command*Command::findCommand(conststring&name){ 
        //从_commandsmap中找到指定name的Command对象 
        map<string,Command*>::iteratori=_commands->find(name); 
        if(i==_commands->end())//如果已到结尾,表示未找到 
        return0; 
        returni->second;//返回Command对象 
        } 
        

        看到上面代码中的_commands大家可能要问,该map是如何初始化并将系统实现的各个Command注册到其中呢?答案就在Command的构造方法中,如下:

        //command.cpp 
        Command::Command(constchar*_name,boolweb,constchar*oldName):name(_name){ 
        //registerourself. 
        //如为空(系统刚启动时)则实例化_commands 
        if(_commands==0) 
        _commands=newmap<string,Command*>; 
        //如为空(系统刚启动时)则实例化_commandsByBestName 
        if(_commandsByBestName==0) 
        _commandsByBestName=newmap<string,Command*>; 
        Command*&c=(*_commands)[name];//获取指定名称的command对象 
        if(c)//如有,表示之前已注册了该command 
        log()<<"warning:2commandswithname:"<<_name<<endl; 
        //将当前command(this)赋值到map中相应name的command上 
        c=this; 
        //绑定到_commandsByBestName中的相应name上 
        (*_commandsByBestName)[name]=this; 
        //如果命令支持web方式 
        if(web){ 
        //如为空(系统刚启动时)则实例化_webCommands 
        if(_webCommands==0) 
        _webCommands=newmap<string,Command*>; 
        //绑定到_webCommands中的相应name上 
        (*_webCommands)[name]=this; 
        } 
        //如有旧名称,则也绑到_commands的oldName所指向的command 
        if(oldName) 
        (*_commands)[oldName]=this; 
        } 
        

        有了这些还不够,我们还要从90多个command子类中找出一个来实际分析其实现的方式,这里以最经常使用的count(获取指定条件的记录数)来分析其向map中注册command的流程,参见下面代码段:

        //dbcommands.cpp 
        /*selectcount(*)*/
        classCmdCount:publicCommand{ 
        public: 
        virtualLockTypelocktype()const{returnREAD;} 
        //调用基类的构造方法 
        CmdCount():Command("count"){} 
        
        virtualboollogTheOp(){ 
        returnfalse; 
        } 
        virtualboolslaveOk()const{ 
        //okon--slavesetups,notokfornonmasterofareplpair(unlessoverride) 
        returnreplSettings.slave==SimpleSlave; 
        } 
        virtualboolslaveOverrideOk(){ 
        returntrue; 
        } 
        virtualbooladminOnly()const{ 
        returnfalse; 
        } 
        virtualvoidhelp(stringstream&help)const{help<<"countobjectsincollection";} 
        virtualboolrun(conststring&dbname,BSONObj&cmdObj,string&errmsg,BSONObjBuilder&result,bool){ 
        stringns=dbname+'.'+cmdObj.firstElement().valuestr(); 
        stringerr; 
        longlongn=runCount(ns.c_str(),cmdObj,err);//执行查询 
        longlongnn=n; 
        boolok=true; 
        if(n==-1){ 
        nn=0; 
        result.appendBool("missing",true); 
        } 
        elseif(n<0){ 
        nn=0; 
        ok=false; 
        if(!err.empty()) 
        errmsg=err; 
        } 
        result.append("n",(double)nn); 
        returnok; 
        } 
        }cmdCount;
        

        上面的CmdCount类即是在命令行模式下使用count指令时对应的代码块,其自身的构造函数就直接调用了基类(Command)的构造方法。但这里只是定义了还不够,还需要一个定义类实例代码(用于启动构造函数),而这个任务就交给了该类定义的代码结尾处的下面代码来实现了:

        }cmdCount;
        

        可以看到,这里使用的是在类声明后定义对象的方式来执行构造方法(这时并未使用new实例化方式来创建对象指针),进而注册该command到map。当然继承自Command的子类必须要实现其中的run()方法,因为只有它是具体command要执行的具体逻辑(可参见上面CmdCount的具体实现)。

        到这里只能说mongod在系统启动到实始化了相应的Command集合map信息,但mongod是如何将client发来的操作请求进行转换并进而执行相应的command指令的呢?我们接下来继续分析。

        之前看过我的这篇文章的朋友可能还有印象,在mongod启动之后,会循环侦听指向端口上的用户(client)请求,这些请求在mongod中被改装成了message在各个功能类中传递。当用户发送一个count指令操作时,其会在query.cpp中执行下面方法(以count查询指令的执行流程为例来进行分析):

        //query.cpp 
        constchar*runQuery(Message&m,QueryMessage&q,CurOp&curop,Message&result){ 
        StringBuilder&ss=curop.debug().str; 
        //构造ParsedQuery查询对象,该对象包括查询记录数字,以及记录跳转偏移量等信息, 
        
        //这些值会在访问磁盘查询时使用,用法参见:query.cpp662行的virtualvoid_init()方法 
        shared_ptr<ParsedQuery>pq_shared(newParsedQuery(q)); 
        ParsedQuery&pq(*pq_shared); 
        ...... 
        //对查询命令判断,指令形如abc.$cmd.findOne({ismaster:1}) 
        if(pq.couldBeCommand()){//_ns中包括$cmd字符串 
        BufBuilderbb; 
        bb.skip(sizeof(QueryResult)); 
        BSONObjBuildercmdResBuf; 
        //对查询权限判断,并执行相应查询指令 
        if(runCommands(ns,jsobj,curop,bb,cmdResBuf,false,queryOptions)){ 
        ss<<"command:"; 
        jsobj.toString(ss); 
        curop.markCommand(); 
        auto_ptr<QueryResult>qr; 
        qr.reset((QueryResult*)bb.buf()); 
        bb.decouple(); 
        qr->setResultFlagsToOk(); 
        qr->len=bb.len(); 
        ss<<"reslen:"<<bb.len(); 
        qr->setOperation(opReply); 
        qr->cursorId=0; 
        qr->startingFrom=0; 
        qr->nReturned=1; 
        result.setData(qr.release(),true);//设置返回结果 
        } 
        else{ 
        uasserted(13530,"badormalformedcommandrequest?"); 
        } 
        return0; 
        } 
        ..... 
        }
        

        上面代码对传递来的查询消息QueryMessage进行分析之后,如果发现其为command时,执行runCommands方法:

        //query.cpp 
        boolrunCommands(constchar*ns,BSONObj&jsobj,CurOp&curop,BufBuilder&b,BSONObjBuilder&anObjBuilder,boolfromRepl,intqueryOptions){ 
        try{ 
        return_runCommands(ns,jsobj,b,anObjBuilder,fromRepl,queryOptions); 
        } 
        catch(AssertionException&e){ 
        e.getInfo().append(anObjBuilder,"assertion","assertionCode"); 
        } 
        curop.debug().str<<"assertion"; 
        anObjBuilder.append("errmsg","dbassertionfailure"); 
        anObjBuilder.append("ok",0.0); 
        BSONObjx=anObjBuilder.done(); 
        b.appendBuf((void*)x.objdata(),x.objsize()); 
        returntrue; 
        } 
        

        接着其会执行dbcommands.cpp中的_runCommands()方法

        //dbcommands.cpp 
        bool_runCommands(constchar*ns,BSONObj&_cmdobj,BufBuilder&b,BSONObjBuilder&anObjBuilder,boolfromRepl,intqueryOptions){ 
        cc().curop()->ensureStarted(); 
        stringdbname=nsToDatabase(ns); 
        
        if(logLevel>=1) 
        log()<<"runcommand"<<ns<<''<<_cmdobj<<endl; 
        
        constchar*p=strchr(ns,'.'); 
        if(!p)returnfalse; 
        //再次进行cmd判断,以确定是command 
        if(strcmp(p,".$cmd")!=0)returnfalse; 
        
        BSONObjjsobj; 
        { 
        BSONElemente=_cmdobj.firstElement(); 
        if(e.type()==Object&&string("query")==e.fieldName()){ 
        jsobj=e.embeddedObject(); 
        } 
        else{ 
        jsobj=_cmdobj; 
        } 
        } 
        
        Client&client=cc(); 
        boolok=false; 
        
        BSONElemente=jsobj.firstElement(); 
        //根据command名称从map中找出相应的command对象 
        Command*c=e.type()?Command::findCommand(e.fieldName()):0; 
        
        if(c){ 
        //执行该对象 
        ok=execCommand(c,client,queryOptions,ns,jsobj,anObjBuilder,fromRepl); 
        } 
        else{ 
        anObjBuilder.append("errmsg",str::stream()<<"nosuchcmd:"<<e.fieldName()); 
        anObjBuilder.append("badcmd",_cmdobj); 
        } 
        
        //switchtobool,butwaitabitlongerbeforeswitching? 
        //anObjBuilder.append("ok",ok); 
        anObjBuilder.append("ok",ok?1.0:0.0); 
        BSONObjx=anObjBuilder.done(); 
        b.appendBuf((void*)x.objdata(),x.objsize()); 
        
        returntrue; 
        

        上面代码主要是从map中找出相应的command对象,并将该对象及操作命令参数和client(用于获取其中的认证信息,以确定其执行权限)作为参数,来调用 execCommand方法:

        //dbcommands.cpp 
        boolexecCommand(Command*c, 
        Client&client,intqueryOptions, 
        constchar*cmdns,BSONObj&cmdObj, 
        BSONObjBuilder&result/*返回command执行结果*/, 
        boolfromRepl){ 
        
        stringdbname=nsToDatabase(cmdns); 
        
        AuthenticationInfo*ai=client.getAuthenticationInfo(); 
         
        if(c->adminOnly()/*如果需要有管理特权开可运行*/
        &&c->localHostOnlyIfNoAuth(cmdObj)/*要么被验证,要么只运行在本地接口*/
        &&noauth&&!ai->isLocalHost){//未认证且不是在本地运行 
        result.append("errmsg", 
        "unauthorized:thiscommandmustrunfromlocalhostwhenrunningdbwithoutauth"); 
        log()<<"commanddenied:"<<cmdObj.toString()<<endl; 
        returnfalse; 
        } 
        
        if(c->adminOnly()&&!fromRepl&&dbname!="admin"){ 
        result.append("errmsg","accessdenied;useadmindb"); 
        log()<<"commanddenied:"<<cmdObj.toString()<<endl; 
        returnfalse; 
        } 
        
        if(cmdObj["help"].trueValue()){ 
        stringstreamss; 
        ss<<"helpfor:"<<c->name<<""; 
        c->help(ss); 
        result.append("help",ss.str()); 
        result.append("lockType",c->locktype()); 
        returntrue; 
        } 
        
        boolcanRunHere= 
        isMaster(dbname.c_str())/*如为master库*/|| 
        c->slaveOk()/*如果replicationpair的slaves可以运行命令*/|| 
        (c->slaveOverrideOk()&&(queryOptions&QueryOption_SlaveOk))|| 
        fromRepl; 
        
        if(!canRunHere){ 
        result.append("errmsg","notmaster"); 
        returnfalse; 
        } 
        
        if(c->adminOnly()) 
        log(2)<<"command:"<<cmdObj<<endl; 
        
        //如当前command无须锁时 
        if(c->locktype()==Command::NONE){ 
        //wealsotrustthatthiswon'tcrash 
        stringerrmsg; 
        //运行当前command 
        intok=c->run(dbname,cmdObj,errmsg,result,fromRepl); 
        if(!ok) 
        result.append("errmsg",errmsg); 
        returnok; 
        } 
        //判断执行当前command是否需要'写锁'(每个command子类都有该属性),枚举定义如下(command.h): 
        //enumLockType{READ=-1/*读*/,NONE=0/*无锁*/,WRITE=1/*写*/}; 
        boolneedWriteLock=c->locktype()==Command::WRITE; 
        
        if(!needWriteLock){ 
        assert(!c->logTheOp()); 
        } 
        
        mongolocklk(needWriteLock);//声明锁对象 
        Client::Contextctx(dbname,dbpath,&lk,c->requiresAuth()); 
        
        try{ 
        stringerrmsg; 
        //运行当前command(本文中提到的count命令) 
        if(!c->run(dbname,cmdObj,errmsg,result,fromRepl)){ 
        result.append("errmsg",errmsg); 
        returnfalse; 
        } 
        } 
        catch(DBException&e){ 
        stringstreamss; 
        ss<<"exception:"<<e.what(); 
        result.append("errmsg",ss.str()); 
        result.append("code",e.getCode()); 
        returnfalse; 
        } 
        
        if(c->logTheOp()&&!fromRepl){ 
        logOp("c",cmdns,cmdObj); 
        } 
        
        returntrue; 
        }
        

        到这里,流程基本就执行完毕了,之后它会将结果传给result(其传参为引用类型,即:"& result"方式).

        #p#

        ***用一张时间序来大体回顾一下这***程:

        MongoDB源码分析&#8211;Command体系架构

        好了,今天的内容到这里就告一段落了。

继续浏览有关 MongoDB 的文章
发表评论