MongoDB command命令处理模块源码实现二

2021-04-22 17:28:43 浏览数 (1)

1. Command命令处理模块回顾

《MongoDB command命令处理模块源码实现一》中我们分析了一个客户端请求到来后,mognodb服务端大体处理流程如下:

① 从message中解析初报文头部,从而确定一个完整的MongoDB报文

② 从body中解析初OpCode操作码信息,3.6版本默认OpCode操作码为OP_MSG

③ 根据解析初的OP_MSG操作码,构造对应OpMsg类,真实命令请求以bson数据格式保存在该类成员body中。

④ 从body中解析出command命令字符串信息(如“insert”、“update”等)。

⑤ 从全局_commands map表中查找是否支持该命令,如果支持则执行该命令处理,如果不支持则直接报错提示。

⑥ 最终找到对应command命令后,执行command的功能run接口。

MongoDB内核支持的command命令信息保存在一个全局map表_commands中,从命令请求bson中解析出command命令字符串后,就是从该全局map表查找,如果找到该命令则说明MongoDB支持该命令,找不到则说明不支持,整个过程归纳为下图所示:

从OpMsg类中解析出命令名字符串后(例如:”insert”、”delete”等),从全局map表_commands查找,找到则执行对应命令。如果找不到,说明不支持该命令操作,进行异常提示处理。

MongoDB不同实例支持那些command命令完全取决于全局map表_commands,下面继续分析该全局map来源。

2. Command命令处理模块源码目录结构

MongoDB集群中通常包含3种节点实例角色:mongos、mongod(ShardServer)、mongod(ConfigServer)。这3种实例校色功能如下:

① Mongos:代理,从shardServer获取路由信息,转发客户端请求到shard。

② mongod(ShardServer):数据存储节点,所有客户端数据记录到shard中。

③ mongod(ConfigServer):记录数据路由信息以及一些元数据。

Mongos代理进程名唯一,也就是”mongos”,代理mongos支持的命令信息比较好确认。但是ShardServer和ConfigServer的进程名都是”mongod”,如何区分各自支持那些命令呢?

configServer实际上是一种特殊的shardServer,它拥有shard数据分片的功能外,还拥有特殊的元数据管理功能,例如记录chunk元数据信息、mongos信息、分片操作日志信息等。因此,configServer除了支持shardServer的命令外,还会支持更多的特有命令。

mongos代理支持的命令信息全部在src/mongo/s/commands目录中实现,源码文件如下:

mongod(shardServer)支持的命令信息全部在src/mongo/db/commands目录中实现,源码文件如下:

mongod(configServer)几乎支持所有shardServer支持的命令(说明:也有个别别特例,如”mapreduce.shardedfinish”),还支持特有的一些命令,这些特意命令在src/mongo/db/s/config目录中实现,源码文件如下:

从上面的不同实例支持命令的源码目录文件可以看出,MongoDB内核源码设计之优秀,从目录结构即可一眼确定不同实例角色支持的各自不同命令信息,代码可读性非常好。目录结构可以总结为下表:

实例角色名

执行的命令源码实现目录

说明

mongos

src/mongo/s/commands

mongos代理支持的命令实现

mongod(shardServer)

src/mongo/db/commands

shardServer支持的命令实现

mongod(configServer)

src/mongo/db/s/config

configServer除了支持该目录中命令外,还支持shardServer角色的几乎所有命令

configServer和shardServer各自支持的命令范围类似于下图包含与被包含的关系,小椭圆代表shardServer,大圆代表configServer:

3. command模块类继承关系

第2章节代码目录结构可以看出,绝大部分命令功能由对应源码文件实现,例如find_cmd.cpp源码文件进行find”命令处理。此外,也有部分源码文件,一个文件对应多个命令实现,例如write_commands.cpp源码文件,同时负责”insert”、”update”、”delete”增删改处理。

由于命令众多,了解了代码目录结构后,在进行核心代码分析前,我们先了解一下command类的各种继承关系。不同命令有不同功能,也就需要不同的实现,但是所有命令也会有一些共同的接口特性,例如该命令是否需要认证、是否支持从节点操作、是否支持WriteConcern操作等。

不同command命令有相同的共性,也会有各自不同的独有特性。所以,MongoDB在源码实现中充分考虑了这些问题,抽象出一些共有的特性接口由基类实现,command用于的一些独有的特性,则在继承类中实现。command命令处理模块相关核心源码类主要继承关系图如下:

如上图,command命令处理模块相关实现类按照父子继承关系可以包含四层,每层功能说明如下:

① CommandInterface类:虚拟接口类,只定义虚拟接口,不做具体实现。

② Command类:完成一些基本功能检查,例如是否支持从节点操作、是否需要认证、是否支持WriteConcern、获取命令名、是否只能在admin库操作等。

③ BasicCommand类:认证相关接口实现、定义虚拟run接口。

④ 具体命令类:每个命令都有一个相应的类定义,都是在该层实现,真正的命令run接口实现在该层完成。

4. command命令注册核心代码实现

前面分析提到,当解析到对应命令字符串(如:”insert”、”update”等)后,从全局map表中commands查找,找到说明支持该命令,找不到则不支持。全局commands表中保存了实例支持的command命令信息,不同命令需要提前注册到该map表中,注册方式有两种:

① 每个命令定义一个对应全局类变量

② new()一个该命令类信息

类注册过程源码实现由command类初始化构造接口完成,注册过程核心代码如下所示:

代码语言:javascript复制
//命令注册,所有注册的命令最终全部保存到_commands全局map表中  
//name和oldName实际上是同一个command,只是可能因为历史原因,命令名改名了  
Command::Command(StringData name, StringData oldName)  
  //命令名字符串  
  : _name(name.toString()),  
    //对应命令执行统计,total代表总的,failed代表执行失败的次数  
    _commandsExecutedMetric("commands."   _name   ".total", &_commandsExecuted),  
    _commandsFailedMetric("commands."   _name   ".failed", &_commandsFailed) {  
  //如果_commands map表还没有生成,则new一个  
  if (_commands == 0)  
      _commands = new CommandMap();  
  ......  
  //把name命令对应的command添加到map表中  
  Command*& c = (*_commands)[name];  
  if (c)  
      log() << "warning: 2 commands with name: " << _name;  
  c = this;  
  ......  
 
  //大部分命令name和oldName是一样的,所以在数组中只会记录一个  
  //如果改名过,则name和oldName就不一样,这时候都需要注册到map表,对应同一个command  
  if (!oldName.empty()) //也就是name和oldName两个命令对应的是同一个this类  
      (*_commands)[oldName.toString()] = this;  
}  

command初始化构造函数中有两个入参,分表代表当前命令名和老旧命令名称,这样设计是为了兼容处理。

4.1 command注册方式一

超过99%的command命令通过定义一个全局类变量来完成注册,本文以shardServer实例的”insert”、”update”、”delete”、“find”为例,这几个命令注册方式如下:

代码语言:javascript复制
//insert命令初始化  
class CmdInsert : public WriteCommand { //  
public:  
  //insert命令初始化构造  
  CmdInsert() : WriteCommand("insert") {}  
  ......  
  //认证检查  
  Status checkAuthForRequest(...) final {  
      ......  
  }  
 
  //真正的Insert插入文档会走这里面  
  void runImpl(...);  
  }  
} cmdInsert; //直接定义一个cmdInsert全局变量  
 
//update命令初始化  
class CmdUpdate: public WriteCommand { //  
public:  
  //update命令初始化构造  
  CmdUpdate() : WriteCommand("update") {}  
  ......  
  //认证检查  
  Status checkAuthForRequest(...) final {  
      ......  
  }  
  //查询计划执行过程  
  Status explain(...) const override {  
        ......  
  }  
  //真正的update插入文档会走这里面  
  void runImpl(...);  
  }  
} cmdUpdate; //直接定义一个cmdUpdate全局变量  
 
//delete命令初始化  
class CmdDelete: public WriteCommand { //  
public:  
  //delete命令初始化构造  
  CmdDelete() : WriteCommand("delete") {}  
  ......  
  //认证检查  
  Status checkAuthForRequest(...) final {  
      ......  
  }  
  //查询计划执行过程  
  Status explain(...) const override {  
        ......  
  }  
 
  //真正的delete插入文档会走这里面  
  void runImpl(...);  
  }  
} cmdDelete; //直接定义一个cmdDelete全局变量

“find”命令也是通过定义一个全局FindCmd类变量来完成该命令的注册过程,注册过程代码如下:

代码语言:javascript复制
//find命令实现类  
class FindCmd : public BasicCommand {  
public:  
  //初始化构造  
  FindCmd() : BasicCommand("find") {}  
  ......  
     
  //查询计划执行过程  
  Status explain(...) const override {  
        ......  
  }  
} findCmd; //直接定义一个findCmd全局变量

上面的类除了可以确定shardServer读写命令的注册方式外,还可以看出读写命令实现过程中,类继承关系稍微有点区别。主要体现在:FindCmd (查)命令类直接继承BasicCommand 命令类,而CmdInsert(增) 、CmdDelete(删)、CmdUpdate(改)这三个写相关的命令,则通过继承WriteCommand 来中转一次,WriteCommand 实现WriteCommand 共性接口,而三个子类则实现自己特有的功能。

shardServer实例,增、删、改、查四个级别命令的继承关系图可以总结为下图所示:

4.2 command注册方式二

除了直接定义一个全局命令类变量外,MongoDB内核命令注册实现的时候,部分命令注册通过new一个命令类实现,例如planCache执行计划对应的几个命令就是通过该方式实现,代码实现如下:

代码语言:javascript复制
//执行计划相关的几个command注册过程,通过new实现  
MONGO_INITIALIZER_WITH_PREREQUISITES(SetupPlanCacheCommands, MONGO_NO_PREREQUISITES)  
(InitializerContext* context) {  
  //执行计划相关的几个命令注册  
  new PlanCacheListQueryShapes();  
  new PlanCacheClear();  
  new PlanCacheListPlans();  
  return Status::OK();  
}  
 
//test命令相关的几个command注册过程,也是通过new实现  
MONGO_INITIALIZER(RegisterEmptyCappedCmd)(InitializerContext* context) {  
  //必须使能testCommandsEnabled,该命令才有效  
  if (Command::testCommandsEnabled) {  
      new CapTrunc();  
      new CmdSleep();  
      new EmptyCapped();  
      new GodInsert();  
  }  
  return Status::OK();  
}

至此,MongoDB内核command命令注册过程就分析完毕,如果想新注册一个新的命令,可以模仿这个流程实现即可。

5. mongos、mongod(shardServer)、mongod(configServer)命名规范

MongoDB不同校色得二进制实例支持的命令有所差异,分别由不同的代码文件实现对应命令功能。MongoDB内核设计非常优秀,通过文件名即可确定对应的命令,以及该命令归属于那个角色实例。这里回顾一下前面提到的不同校色实例对应的命令代码目录实现:

① mongos代理:代码目录src/mongo/s/commands

② mongod(shardServer):代码目录src/mongo/db/commands

③ mongod(configServer):代码目录src/mongo/db/s/config

除了代码目录有明确的区别外,代码文件名及命令类名也各不相同。但是,命令类名和文件名也有特定的命名规范,有一定的命名规律,下面还是以mongod(含shardServer和configServer)和mongos代理为例,来说明最常用的增、删、改、查command命令对应的源码文件命名和命令类命名。

提前梳理好各个校色实例的命名规范,对我们理解整个代码具有事半功倍的效果,同时也可以方便我们快速找到任何一个命令的代码文件及其对应命令的核心代码实现,具有”举一反三”的效果。

5.1 mongos、mongod(含shardServer和configServer)命名规范

mongod实例的写操作命令(增、删、改)由write_commands.cpp文件实现,该文件中的CmdInsert、CmdDelete、CmdUpdate类分别对应具体的增、删、改命令操作。读操作命令由find_cmd.cpp文件实现,对应命令类为FindCmd

除了mongod实例,mongos作为代理转发节点,同样支持增、删、改操作。MongoDB内核实现的时候,如果集群部署是sharding集群模式,则需要mongos代理,客户端访问入口为代理。正是因为代理模式为sharding分片集群模式,所以mongos支持的命令在源文件命名和命令类命名的时候,做了特殊标记。相比mongod实例,所有mongos支持的命令相关原文件和类实现基本上都增加”cluster”特殊标记。

以增、删、改、查、isMaster、getMore、findAndModify为例,mongos和mongod(含shardServer和configServer)支持的命令列表总结如下:

命令名

mongod实例对应命令文件/命令类

mongos实例对应命令文件/命令类

insert操作

write_commands.cpp/(CmdInsert)

cluster_write_cmd.cpp/ (ClusterCmdInsert)

delete操作

write_commands.cpp/(CmdDelete)

cluster_write_cmd.cpp/ (ClusterCmdDelete)

update操作

write_commands.cpp/(CmdUpdate)

cluster_write_cmd.cpp/ (ClusterCmdUpdate)

find操作

find_cmd.cpp/(FindCmd)

cluster_find_cmd.cpp/ (ClusterFindCmd)

getMore操作

getmore_cmd.cpp/(GetMoreCmd)

cluster_getmore_cmd.cpp/(ClusterGetMoreCmd)

findAndModify操作

find_and_modify.cpp /(CmdFindAndModify)

cluster_find_and_modify_cmd.cpp/ (FindAndModifyCmd)

......

......

......

从上面的命名文件和命令类名可以看出,大多数mongos代理相关命令会增加”cluster”标记(但是也有部分个例,例如findAndModify对应类命就没带改标记)。

此外,也有部分mongos和mongod实例命令不满足上面的命名规范,例如"dropIndexes"、"createIndexes"、"reIndex"、"create"、"renameCollection"等命令,各自命名规则如下:

命令名

mongod实例对应命令文件/命令类

mongos实例对应命令文件/命令类

dropIndexes

drop_indexes.cpp/(CmdDropIndexes)

commands_public.cpp/(DropIndexesCmd)

createIndexes

create_indexes.cpp(CmdCreateIndex)

commands_public.cpp/(CreateIndexesCmd)

reIndex

drop_indexes.cpp/(CmdReIndex)

commands_public.cpp/(ReIndexCmd)

create

Dbcommands.cpp/(CmdCreate)

commands_public.cpp/(CreateCmd)

renameCollection

rename_collection_cmd.cpp/ (CmdRenameCollection)

commands_public.cpp/(RenameCollectionCmd)

......

......

......

如上,绝大多数mongos命令源码文件和命令实现类命名相比mongod实例,都带有”cluster”标识,但是还是有部分命令命名不准寻该规则。如果想知道某个命令的源码实现文件,可以在前面提到的三个实例中搜索相应字符串即可定位到。注意:搜索的时候需要带上双引号。

5.2 mongod(configServer)特有命令命名规则

和mongos命名规则类似,configServer支持的独有命令源码文件命名规则相比shardServer增加了”configsvr”特性,从源码文件名即可明显的看出是configServer独有的命令。

此外,命令对应类命命名也带有”ConfigSvr”特性,例如class ConfigSvrAddShardCommand{}、class ConfigSvrMoveChunkCommand{}等,命名规则和mongos代理支持的command命名规则类似。

5.3 命名规则总结

上面的命名规则可以总结为如下图解信息:

6. command默认接口类核心代码实现及基本接口功能说明

每个命令都对应一个command基类,该类中完成命令的一些基本接口功能初始化,核心接口实现如下:

代码语言:javascript复制
命令模块基类基础接口初始化实现  
ass Command : public CommandInterface {  
blic:  
//获取集合名collection  
static std::string parseNsFullyQualified(...);  
//获取DB.COLLECTION  
static NamespaceString parseNsCollectionRequired(...);  
//map表结构  
using CommandMap = StringMap<Command*>;  
......  
//获取命令名  
const std::string& getName() const final {  
    return _name;  
}  
......  
//应答保留填充字段长度  
std::size_t reserveBytesForReply() const override {  
    return 0u;  
}  
//该命令是否只能在admin库执行,默认不可以  
bool adminOnly() const override {  
    return false;  
}  
//该命令是否需要权限认证检查?默认不需要  
bool localHostOnlyIfNoAuth() override {  
    return false;  
}  
//该命令执行后是否进行command操作计数  
bool shouldAffectCommandCounter() const override {  
    return true;  
}  
//该命令是否需要认证  
bool requiresAuth() const override {  
    return true;  
}  
//help帮助信息  
void help(std::stringstream& help) const override;  
//执行计划信息  
Status explain(...) const override;  
//日志信息相关  
void redactForLogging(mutablebson::Document* cmdObj) override;  
BSONObj getRedactedCopyForLogging(const BSONObj& cmdObj) override;  
//该命令是否为maintenance模式,默认false  
bool maintenanceMode() const override {  
    return false;  
}  
//maintenance是否支持,默认支持  
bool maintenanceOk() const override {  
    return true;  
}  
//本地是否支持非本地ReadConcern,默认不支持  
bool supportsNonLocalReadConcern(...) const override {  
    return false;  
}  
//是否允许AfterClusterTime,默认允许  
bool allowsAfterClusterTime(const BSONObj& cmdObj) const override {  
    return true;  
}  
//3.6版本默认opCode=OP_MSG,所以对应逻辑操作op为LogicalOp::opCommand  
LogicalOp getLogicalOp() const override {  
    return LogicalOp::opCommand;  
}  
//例如find就是kRead,update delete insert就是kWrite,非读写操作就是kCommand  
ReadWriteType getReadWriteType() const override {  
    return ReadWriteType::kCommand;  
}  
//该命令执行成功统计  
void incrementCommandsExecuted() final {  
    _commandsExecuted.increment();  
}  

//该命令执行失败统计  
void incrementCommandsFailed() final {  
    _commandsFailed.increment();  
}  

//真正得命令运行  
bool publicRun(OperationContext* opCtx, const OpMsgRequest& request, BSONObjBuilder& result);  

//获取支持的所有命令信息 ListCommandsCmd获取所有支持的命令 db.listCommands()  
static const CommandMap& allCommands() {  
    return *_commands;  
}  

//没用  
static const CommandMap& allCommandsByBestName() {  
    return *_commandsByBestName;  
}  

//收到不支持命令的统计,例如mongo shell敲一个mongodb无法识别得命令,这里就会统计出来  
static Counter64 unknownCommands;  
//根据命令字符串名查找对应命令  
static Command* findCommand(StringData name);  
//执行结果  
static void appendCommandStatus(...);  
//是否启用了command test功能  
static bool testCommandsEnabled;  
//help帮助信息  
static bool isHelpRequest(const BSONElement& helpElem);  
  static const char kHelpFieldName[];  
  //认证检查,检查是否有执行该命令得权限  
  static Status checkAuthorization(Command* c,  
                                    OperationContext* opCtx,  
                                    const OpMsgRequest& request);  
  ......  
private:  
  //添加地方见Command::Command(    
  //所有的command都在_commands中保存  
  static CommandMap* _commands;  
  //暂时没用  
  static CommandMap* _commandsByBestName;  
  //执行对应命令run接口  
  virtual bool enhancedRun(OperationContext* opCtx,  
                            const OpMsgRequest& request,  
                            BSONObjBuilder& result) = 0;  
  //db.serverStatus().metrics.commands命令查看,本命令的执行统计,包括执行成功和执行失败的  
  Counter64 _commandsExecuted;  
  Counter64 _commandsFailed;  
  //命令名,如"find" "insert" "update" "createIndexes" "deleteIndexes"  
  const std::string _name;  
 
  //每个命令执行是否成功通过MetricTree管理起来,也就是db.serverStatus().metrics.commands统计信息  
  //通过MetricTree特殊二叉树管理起来  
  ServerStatusMetricField<Counter64> _commandsExecutedMetric;  
  ServerStatusMetricField<Counter64> _commandsFailedMetric;  
}

command作为默认接口类,主要完成一些命令基本接口初始化操作及默认配置设置,该类最基本的接口主要如下:

reserveBytesForReply

ReserveBytesForReply()接口主要完成该命令应答填充字段长度,默认值为0。对应命令可以在具体命令类中修改。

adminOnly

该命令是否只能在admin库操作,默认为false。也可以在对应命令继承类中修改,例如"moveChunk"命令则在MoveChunkCommand继承类中设置为true,也就是该命令只能在admin库操作。

localHostOnlyIfNoAuth

该命令是否支持在实例所在本机不认证操作,默认值false。对应命令可以在具体继承类中修改。

shouldAffectCommandCounter

该命令是否需要command统计,也就是mongostat中的command统计计数是否需要使能。默认值true,也就是该命令会进行command计数统计。对应命令可以在具体继承类中修改。

requiresAuth

该命令是否需要认证,默认为true。对应命令可以在具体继承类中修改。

allowsAfterClusterTime

该命令是否支持AfterClusterTime,默认为true。对应命令可以在具体继承类中修改。

getLogicalOp

该命令是否为逻辑opCommand命令。3.6版本默认opCode=OP_MSG,所以对应逻辑操作op为LogicalOp::opCommand。

getReadWriteType

如果为读命令则type对应kRead,写命令type对应kWrite,其他读写以外的命令对应kCommand。

incrementCommandsExecuted

该命令执行成功统计,通过db.serverStatus().metrics.commands获取该命令统计。

_commandsFailed

该命令执行失败统计,通过db.serverStatus().metrics.commands获取该命令统计。

以上列举除了command基类的几个核心功能默认值信息,如果继承类中没有修改这些接口值,则该命令对应功能就是这些默认值。

说明:各种不同命令如果不适用command基类的默认接口,则可以在继承类中修改对应接口值即可更改对应功能。

命令除了上面提到的基本功能是否支持外,command类还有其他几个核心接口功能。例如,该命令是否认证成功、是否有操作权限、允许对应run命令等。command类函数接口功能总结如下表所示:

类名

函数接口

功能说明

Command类

appendPassthroughFields(...)

向请求bson中追加PassthroughField信息

appendMajorityWriteConcern(...)

向bson中追加writeConcern信息

parseNsFullyQualified(...)

获取表名

parseNsCollectionRequired(...)

获取db.collection信息

parseNsOrUUID(...)

或者db.collection或者uuid信息

parseNs(...)

获取表名

parseResourcePattern(...)

获取ResourcePattern资源匹配信息

Command(...)

命令初始化及构造

findCommand(...)

从全局map表中根据命令字符串查找是否支持该命令

appendCommandStatus(...)

生成应答给客户端的命令执行结果信息

checkAuthForCommand(...)

检查是否有执行该命令的权限

checkAuthorization(...)

检查认证是否通过,实际上直接调用了checkAuthForCommand

publicRun(...)

执行对应命令的run接口,直接调用子类run接口,可以是BasicCommand::enhancedRun()接口或者对应命令(例如ClusterWriteCmd)的enhancedRun()

BasicCommand类

enhancedRun(...)

直接运行basicCommand类的继承类run接口

7. 命令run

结合《命令处理模块源码实现一》和本章节对command处理流程可以得出,runCommandImpl接口通过如下调用流程最终执行特定命令的run接口,这里以insert写入和读取流程为例,mongod实例写入调用过程如下图所示:

最终,mongod和mongos实例调用相关命令得run接口完成具体的command命令处理操作。mongos、mongod(shardServer)、mongod(configServer)相关常用的操作命令(以最基本的读写命令为例)入口及功能说明总结如下表所示:

实例名

command命令run入口

功能说明

mongod

FindCmd::run(...)

Mongod(含shardServer和configServer)实例读操作入口

CmdInsert::runImpl(...) CmdUpdate::runImpl(...) CmdDelete::runImpl(...)

Mongod(含shardServer和configServer)实例写(增、删、改)操作入口

mongos

FindCmd::run(...)

mongos代理读操作入口

ClusterWriteCmd::enhancedRun(...)

mongos代理写(增、删、改)操作入口

8. command模块统计信息

MongoDB command命令处理模块相关统计包含三类:单个命令统计、汇总型统计、读写时延统计。其中,单个命令统计针对所有接受到的命令名字符串进行统计,汇总型统计则是把同一类型的命令总结为一个整体统计(例如commands统计)。

8.1 单个命令统计

MongoDB会收集所有操作命令执行结果,如果本次命令执行成功,则该命令成功统计自增加1,同理如果该命令执行过程失败,则失败统计自增加1,这些统一归类为”单个命令统计信息”。

单个命令统计由command类的commandsExecuted和commandsFailed实现命令执行成功统计和失败统计,相关核心代码实现如下:

代码语言:javascript复制
//该命令执行成功统计  
void incrementCommandsExecuted() final {  
  _commandsExecuted.increment();  
}  
 
//该命令执行失败统计  
void incrementCommandsFailed() final {  
  _commandsFailed.increment();  
}  

//命令入口  
void execCommandDatabase(...)  
{  
  ......  
  //该命令执行次数统计 db.serverStatus().metrics.commands可以获取统计信息  
  command->incrementCommandsExecuted();  
  ......  
  //真正的命令执行在这里面  
  retval = runCommandImpl(opCtx, command, request, replyBuilder, startOperationTime);  
 
  //该命令失败次数统计  
  if (!retval) {  
      command->incrementCommandsFailed();  
  }  
  ......  
}  

MongoDB默认会统计每个客户端发往服务端的命令,即使是无法识别的命令也会统计,命令统计可以通过db.serverStatus().metrics.commands获取,如下图所示:

8.2 汇总型commands命令统计

从前面的单个命令统计可以看出,单个命令会记录所有发送给MongoDB的命令信息。MongoDB支持的命令百余个,由于命令众多,因此MongoDB为了更加直观明了的获取统计信息,除了提供单个命令统计外,还对外提供汇总型命令统计。

汇总型命令统计可以通过db.serverStatus().opcounters命令获取,mongostat中的增删改查等信息也来自于该统计,如下图:

从上图可以看出,整个mongostat监控统计可以归类为小表:

统计项

包含命令

说明

insert

"insert"

query

"find"

update

"update"

delete

"delete"

getMore

"getMore"

游标批量操作

commands

参考后面分析,汇总多个命令

后文分析

insert、delete、update、find分别对应增删改查四个命令操作,getMore对应批量游标操作命令。这五个命令,对应命令执行的时候统计信息自增,核心代码实现如下:

8.2.1 insert操作统计

insert操作统计在代理mongos和分片存储节点mongod都会统计,两种角色的insert统计核心代码如下:

1. 代理mongos insert统计核心代码实现

代码语言:javascript复制
bool insertBatchAndHandleErrors(...) {  
  ......  
  //一次性一条一条插入,上面的固定集合是一次性插入  
  for (auto it = batch.begin(); it != batch.end();   it) {  
      //insert操作计数  
      globalOpCounters.gotInsert();  
  }  
  ......  
}

2. 分片存储节点mongod insert统计核心代码实现

代码语言:javascript复制
//mongod代理insert统计核心流程  
bool ClusterWriteCmd::enhancedRun(...) {  
  ......  
  if (_writeType == BatchedCommandRequest::BatchType_Insert) {  
      //insert计数  
      for (size_t i = 0; i < numAttempts;   i) {  
          globalOpCounters.gotInsert();  
      }  
  }  
  ......  
}

8.2.2 query操作统计

1. 代理mongos query统计核心代码实现:

代码语言:javascript复制
  bool ClusterFindCmd::run(...) {  
  //find操作统计,也就是query统计  
  globalOpCounters.gotQuery();  
  ......  
}

2. 分片存储节点mongod query统计核心代码实现:

代码语言:javascript复制
bool FindCmd::run(...) {  
  //find操作统计,也就是query统计  
  globalOpCounters.gotQuery();  
  ......  
}

8.2.3 update操作统计

1. 代理mongos update统计核心代码实现:

代码语言:javascript复制
bool ClusterWriteCmd::enhancedRun(...) {  
  //update操作统计
  globalOpCounters.gotUpdate();  
  ......  
}

2. 分片存储节点mongod update统计核心代码实现:

代码语言:javascript复制
//mongos代理update统计核心流程  
bool ClusterWriteCmd::enhancedRun(...) {  
  ......  
  if (_writeType == BatchedCommandRequest::BatchType_Update) {  
      //insert计数  
  for (size_t i = 0; i < numAttempts;   i) {  
        globalOpCounters.gotUpdate();  
      }  
  }  
  ......  
}

8.2.4 delete操作统计

1. 代理mongos delete统计核心代码实现:

代码语言:javascript复制
//mongos代理update统计核心流程  
bool ClusterWriteCmd::enhancedRun(...) {  
  ......  
  if (_writeType == BatchedCommandRequest::BatchType_Update) {  
      //insert计数  
  for (size_t i = 0; i < numAttempts;   i) {  
        globalOpCounters.gotUpdate();  
      }  
  }  
  ......  
}

2. 分片存储节点mongod delete统计核心代码实现:

代码语言:javascript复制
static SingleWriteResult performSingleDeleteOp(...) {  
  ......  
  //分片mongod实例delete操作统计  
  globalOpCounters.gotDelete();  
  ......  
}

8.2.5 getMore操作统计

1. 代理mongos getMore统计核心代码实现:

代码语言:javascript复制
//mongos代理getMore统计核心流程  
bool ClusterGetMoreCmd::enhancedRun(...) {  
  //代理getMore统计  
  globalOpCounters.gotGetMore();  
  ......  
}

2. 分片存储节点mongod getMore统计核心代码实现:

代码语言:javascript复制
//mongod分片存储节点getMore统计  
bool ClusterGetMoreCmd::enhancedRun(...) {  
  //存储节点mongod getMore统计  
  globalOpCounters.gotGetMore();  
  ......  
}

8.2.6 command操作统计

前面五种操作统计都很好理解,commands统计由那些命令操作组成,本节将重点分析commands如何实现统计。commands统计核心代码实现如下:

1. 代理mongos commands统计核心代码实现:

代码语言:javascript复制
//mongos代理commands统计  
void execCommandDatabase(...) {  
  ......  
  if (command->shouldAffectCommandCounter()) {  
      OpCounters* opCounters = &globalOpCounters;  
      opCounters->gotCommand();  
  }  
  ......  
}

2. 存储节点mongod commands统计核心代码实现:

代码语言:javascript复制
void execCommandDatabase(...) {  
  ......  
  //是否进行command统计  
  if (command->shouldAffectCommandCounter()) {  
      OpCounters* opCounters = &globalOpCounters;  
      //commands计数自增
      opCounters->gotCommand();  
  }  
  ......  
}  

从上面的代码可以看出,只有对应命令类中shouldAffectCommandCounter()为true的命令才会进行commands计数。前面章节中我们提到,所有命令都有一个对应类实现相应功能,所有命令实现类都继承一个功能class command {}类,该类对shouldAffectCommandCounter()接口进行初始化。代码实现如下:

代码语言:javascript复制
class Command : {  
  ......  
  //该命令是否进行command操作计数,默认需要。如果不需要进行command统计,可在命令继承类中置为false  
  bool shouldAffectCommandCounter() const override {  
      return true;  
  }  
  ......  
}

该接口默认为true,如果对应命令不需要进行commands计数统计,则需要在对应命令实现类中把该接口置为false。通过分析代码,可以看出,只有以下命令子类把shouldAffectCommandCounter()接口设置为false,搜索结果如下:

分析代码可以得出如下结论:

1) mongos代理中的clase Cluster_find_cmd { }类和class Cluster_getmore_cmd {}类的shouldAffectCommandCounter()接口置为false,这两个类分别对应代理的“find”和“getMore”命令操作,也就说明mongos代理de 这两个命令操作不会统计到commands中。

2) mongod分片存储节点的clase Find_cmd{}、class Getmore_cmd {}、class Write_commands{}三个类中把shouldAffectCommandCounter()接口置为false。这三个类分别对应mongod存储实例的如下几个命令:“find”、“getMore”、“insert”、“update”、“delete”五个命令。

mongos和mongod实例commands统计信息总结如下:

实例名

commands统计包含那些命令操作

mongos代理

“find”和“getMore”以外的所有命令

mongod分片存储节点

“find”、“getMore”、“insert”、“update”、“delete”以外的所有命令

8.3 慢日志、时延统计

每次客户端请求执行实践如果超过了log level配置的最大慢日志时间,则会把该操作详细信息记录下来,同时把本操作执行时间添加到对应的读或者写计数及时延统计中。命令处理模块中,时延相关统计包括以下两种统计:

① 慢日志统计

② 读写计数及时延统计

8.3.1 慢日志统计

当启用了慢日志记录功能后,mongod会把执行时间超过指定阀值的慢日志记录下来。慢日志默认记录到服务日志文件(systemLog.path配置项设置),同时会记录日志到”system.profile”集合中。慢日志核心代码实现如下:

代码语言:javascript复制
DbResponse ServiceEntryPointMongod::handleRequest(...) {  
  ......  
  //记录开始时间  
  //获取当前操作对应curop  
  CurOp& currentOp = *CurOp::get(opCtx);  
 
  ......  
  //执行请求对应命令  
  runCommands(opCtx, m);  
  ......  
 
  //记录结束时间  
  currentOp.ensureStarted();  
  currentOp.done();  
  //获取开始和结束时间差,也就是命令执行时间  
  debug.executionTimeMicros = durationCount<Microseconds>  
                (currentOp.elapsedTimeExcludingPauses());  
  //记录超过阀值的慢日志到日志文件  
  if (shouldLogOpDebug || (shouldSample && debug.executionTimeMicros > logThresholdMs * 1000LL)) {  
      ......  
  //记录慢日志到日志文件  
      log() << debug.report(&c, currentOp, lockerInfo.stats);  
  }  
  //记录慢日志到system.profile集合  
  if (currentOp.shouldDBProfile(shouldSample)) {  
      ......  
      //记录慢日志到system.profile集合中  
      profile(opCtx, op);  
  }  
  ......  
}  

8.3.2 读写操作计数及时延统计

根据请求command命令类型(包含读命令、写命令、command命令),以及命令执行时间,可以计算出不同类型命令的读写执行时间,从而计算出集群的读时延、写时延、command时延。MongoDB所有命令可以归纳为读、写、command三类,核心代码如下:

代码语言:javascript复制
//获取操作类型  
Command::ReadWriteType CurOp::getReadWriteType() const {  
  ......  
  switch (_logicalOp) {  
  //getmore find归纳为读  
      case LogicalOp::opGetMore:  
      case LogicalOp::opQuery:  
          return Command::ReadWriteType::kRead;  
  //增删改统一归纳为写  
      case LogicalOp::opUpdate:  
      case LogicalOp::opInsert:  
      case LogicalOp::opDelete:  
          return Command::ReadWriteType::kWrite;  
  //增删改以外的归纳为command  
      default:  
          return Command::ReadWriteType::kCommand;  
  }  
}  

从上面的代码可以看出,读、写、command分别对应以下命令:

读(read):包含getMore、find。

写(write):包含insert、delete、update。

command:读和写以外的所有命令。

命令执行完计算出命令运行时间后,mongod实例会记录下这个时延,累加到历史统计OperationLatencyHistogram中,读、写、command操作计数及时延统计分别记录到reads、writes、_commands三个变量成员中。该统计核心代码实现如下:

代码语言:javascript复制
//Top::_incrementHistogram调用  
//操作和时延计数操作  
void OperationLatencyHistogram::increment(uint64_t latency, Command::ReadWriteType type) {  
  int bucket = _getBucket(latency);  
  switch (type) {  
      //读时延累加,读计数自增  
      case Command::ReadWriteType::kRead:  
          _incrementData(latency, bucket, &_reads);  
          break;  
      //写时延累加,写计数自增    
      case Command::ReadWriteType::kWrite:  
          _incrementData(latency, bucket, &_writes);  
          break;  
      //command时延累加,command计数自增    
      case Command::ReadWriteType::kCommand:  
          _incrementData(latency, bucket, &_commands);  
          break;  
      default:  
          MONGO_UNREACHABLE;  
  }  
}

命令请求执行过程及其对应的读写请求操作计数、时延累加衔接代码实现如下:

代码语言:javascript复制
DbResponse ServiceEntryPointMongod::handleRequest(...) {  
  ......  
  //记录开始时间  
  //获取当前操作对应curop  
  CurOp& currentOp = *CurOp::get(opCtx);  
 
  ......  
  //执行请求对应命令  
  runCommands(opCtx, m);  
  ......  
 
  //记录结束时间  
  currentOp.ensureStarted();  
  currentOp.done();  
  //获取开始和结束时间差,也就是命令执行时间  
  debug.executionTimeMicros = durationCount<Microseconds>  
                (currentOp.elapsedTimeExcludingPauses());  
  ......  
  //记录慢日志到system.profile集合  
  //mongod读写的时间延迟统计 db.serverStatus().opLatencies获取  
  Top::get(opCtx->getServiceContext())  
      .incrementGlobalLatencyStats(   //读写统计
          opCtx,  
            //时延            
          durationCount<Microseconds>(currentOp.elapsedTimeExcludingPauses()),  
          currentOp.getReadWriteType()); //读写类型
  ......  
}  

mongod实例读、写、command操作计数及其各自时延统计可以通过db.serverStatus()接口获取,用户可用采样来计算对应的tps和平均时延信息。获取操作统计和时延统计的命令如下:

db.serverStatus().opLatencies

9. 问题回顾

《MongoDB command命令处理模块源码实现一》一文中提到的commands统计信息到这里就可以得到答案了,如下表所示:

mongos和mongod实例commands统计信息总结如下:

实例名

commands统计包含那些命令操作

mongos代理

“find”和“getMore”以外的所有命令

mongod分片存储节点

“find”、“getMore”、“insert”、“update”、“delete”以外的所有命令

统计项

包含命令

说明

insert

"insert"

query

"find"

update

"update"

delete

"delete"

getMore

"getMore"

游标批量操作

commands

mongos:“find”和“getMore”以外的所有命令 mongod: “find”、“getMore”、“insert”、“update”、“delete”以外的所有命令

1. mongos的commands统计包含读(find、getmore)以外的所有命令 2. mongod的commands统计包含增删改查及getMore以外的所有命令

mongos代理mongostat统计可以汇总为下图所示:

mongod代理mongostat统计可以汇总为下图所示:

更多文章:

MongoDB Command命令处理模块源码实现一

常用高并发网络线程模型设计及MongoDB线程模型优化实践

MongoDB网络传输处理源码实现及性能调优-体验内核性能极致设计

OPPO百万级高并发MongoDB集群性能数十倍提升优化实践

盘点 2020 | 我要为分布式数据库 MongoDB 在国内影响力提升及推广做点事

MongoDB网络传输层模块源码实现二

MongoDB网络传输层模块源码实现三

MongoDB网络传输层模块源码实现四

作者:杨亚洲

前滴滴出行技术专家,现任OPPO文档数据库MongoDB负责人,负责oppo千万级峰值TPS/十万亿级数据量文档数据库MongoDB内核研发及运维工作,一直专注于分布式缓存、高性能服务端、数据库、中间件等相关研发。Github账号地址:

https://github.com/y123456yz

MongoDB-全球领先的现代通用数据库

点击访问MongoDB官网www.mongodb.com/zh

Tapdata-异构数据库实时同步工具

点击访问Tapdata官网https://tapdata.net/

0 人点赞