mongodb 源码实现系列 - command 命令处理模块源码实现二
关于作者
前滴滴出行技术专家,现任OPPO文档数据库mongodb负责人,负责oppo千万级峰值TPS/十万亿级数据量文档数据库mongodb内核研发及运维工作,一直专注于分布式缓存、高性能服务端、数据库、中间件等相关研发。后续持续分享《MongoDB内核源码设计、性能优化、最佳运维实践》,Github账号地址:https://github.com/y123456yz
背景
<<transport_layer网络传输层模块源码实现>>中分享了mongodb内核底层网络IO处理相关实现,包括套接字初始化、一个完整mongodb报文的读取、获取到DB数据发送给客户端等。Mongodb支持多种增、删、改、查、聚合处理、cluster处理等操作,每个操作在内核实现中对应一个command,每个command有不同的功能,mongodb内核如何进行command源码处理将是本文分析的重点
此外,mongodb提供了mongostat工具来监控当前集群的各种操作统计。Mongostat监控统计如下图所示:
其中,insert、delete、update、query这四项统计比较好理解,分别对应增、删、改、查。但是,comand、getmore不是很好理解,command代表什么统计?getMore代表什么统计?,这两项相对比较难理解。
此外,通过本文字分析,我们将搞明白这六项统计的具体含义,同时弄清这六项统计由那些操作进行计数。
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内核源码设计之优秀,从目录结构即可一眼确定不同实例角色支持的各自不同命令信息,代码可读性非常好。目录结构可以总结为下表:
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类初始化构造接口完成,注册过程核心代码如下所示:
command初始化构造函数中有两个入参,分表代表当前命令名和老旧命令名称,这样设计是为了兼容处理。
4.1 command注册方式一
超过99%的command命令通过定义一个全局类变量来完成注册,本文以shardServer实例的”insert”、”update”、”delete”、“find”为例,这几个命令注册方式如下:
“find”命令也是通过定义一个全局FindCmd类变量来完成该命令的注册过程,注册过程代码如下:
上面的类除了可以确定shardServer读写命令的注册方式外,还可以看出读写命令实现过程中,类继承关系稍微有点区别。主要体现在:FindCmd (查)命令类直接继承BasicCommand 命令类,而CmdInsert(增) 、CmdDelete(删)、CmdUpdate(改)这三个写相关的命令,则通过继承WriteCommand 来中转一次,WriteCommand 实现WriteCommand 共性接口,而三个子类则实现自己特有的功能。
shardServer实例,增、删、改、查四个级别命令的继承关系图可以总结为下图所示:
4.2 command注册方式二
除了直接定义一个全局命令类变量外,mongodb内核命令注册实现的时候,部分命令注册通过new一个命令类实现,例如planCache执行计划对应的几个命令就是通过该方式实现,代码实现如下:
至此,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)支持的命令列表总结如下:
从上面的命名文件和命令类名可以看出,大多数mongos代理相关命令会增加”cluster”标记(但是也有部分个例,例如findAndModify对应类命就没带改标记)。
此外,也有部分mongos和mongod实例命令不满足上面的命名规范,例如"dropIndexes"、"createIndexes"、"reIndex"、"create"、"renameCollection"等命令,各自命名规则如下:
如上,绝大多数mongos命令源码文件和命令实现类命名相比mongod实例,都带有”cluster”标识,但是还是有部分命令命名不准寻该规则。如果想知道某个命令的源码实现文件,可以在前面提到的三个实例中搜索相应字符串即可定位到。注意:搜索的时候需要带上双引号。
5.2 mongod(configServer)特有命令命名规则
和mongos命名规则类似,configServer支持的独有命令源码文件命名规则相比shardServer增加了”configsvr”特性,从源码文件名即可明显的看出是configServer独有的命令。
此外,命令对应类命命名也带有”ConfigSvr”特性,例如class ConfigSvrAddShardCommand{}、class ConfigSvrMoveChunkCommand{}等,命名规则和mongos代理支持的command命名规则类似。
5.3命名规则总结
上面的命名规则可以总结为如下图解信息:
6. command默认接口类核心代码实现及基本接口功能说明
每个命令都对应一个command基类,该类中完成命令的一些基本接口功能初始化,核心接口实现如下:
command作为默认接口类,主要完成一些命令基本接口初始化操作及默认配置设置,该类最基本的接口主要如下:
reserveBytesForReply
ReserveBytesForReply()接口主要完成该命令应答填充字段长度,默认值为0。对应命令可以在具体命令类中修改。
adminOnly
该命令是否只能在admin库操作,默认为false。也可以在对应命令继承类中修改,例如"moveChunk"命令则在MoveChunkCommand继承类中设置为true,也就是该命令只能在admin库操作。
localHostOnlyIfNoAuth
该命令是否支持在实例所在本机不认证操作,默认值false。对应命令可以在具体继承类中修改。
l 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类函数接口功能总结如下表所示:
7. 命令run
结合《命令处理模块源码实现一》和本章节对command处理流程可以得出,runCommandImpl接口通过如下调用流程最终执行特定命令的run接口,这里以insert写入和读取流程为例,mongod实例写入调用过程如下图所示:
最终,mongod和mongos实例调用相关命令得run接口完成具体的command命令处理操作。mongos、mongod(shardServer)、mongod(configServer)相关常用的操作命令(以最基本的读写命令为例)入口及功能说明总结如下表所示:
8. command模块统计信息
mongodb command命令处理模块相关统计包含三类:单个命令统计、汇总型统计、读写时延统计。其中,单个命令统计针对所有接受到的命令名字符串进行统计,汇总型统计则是把同一类型的命令总结为一个整体统计(例如commands统计)。
8.1单个命令统计
mongodb会收集所有操作命令执行结果,如果本次命令执行成功,则该命令成功统计自增加1,同理如果该命令执行过程失败,则失败统计自增加1,这些统一归类为”单个命令统计信息”。
单个命令统计由command类的_commandsExecuted和_commandsFailed实现命令执行成功统计和失败统计,相关核心代码实现如下:
mongodb默认会统计每个客户端发往服务端的命令,即使是无法识别的命令也会统计,命令统计可以通过db.serverStatus().metrics.commands获取,如下图所示:
8.2汇总型commands命令统计
从前面的单个命令统计可以看出,单个命令会记录所有发送给mongodb的命令信息。mongodb支持的命令百余个,由于命令众多,因此mongodb为了更加直观明了的获取统计信息,除了提供单个命令统计外,还对外提供汇总型命令统计。
汇总型命令统计可以通过db.serverStatus().opcounters命令获取,mongostat中的增删改查等信息也来自于该统计,如下图:
从上图可以看出,整个mongostat监控统计可以归类为小表:
insert、delete、update、find分别对应增删改查四个命令操作,getMore对应批量游标操作命令。这五个命令,对应命令执行的时候统计信息自增,核心代码实现如下:
8.2.1 insert操作统计
insert操作统计在代理mongos和分片存储节点mongod都会统计,两种角色的insert统计核心代码如下:
1. 代理mongos insert统计核心代码实现
2. 分片存储节点mongod insert统计核心代码实现
8.2.2 query操作统计
1. 代理mongos query统计核心代码实现:
2. 分片存储节点mongod query统计核心代码实现:
8.2.3 update操作统计
1. 代理mongos update统计核心代码实现:
2. 分片存储节点mongod update统计核心代码实现:
8.2.4 delete操作统计
1. 代理mongos delete统计核心代码实现:
2. 分片存储节点mongod delete统计核心代码实现:
8.2.5 getMore操作统计
1. 代理mongos getMore统计核心代码实现:
2. 分片存储节点mongod getMore统计核心代码实现:
8.2.6 command操作统计
前面五种操作统计都很好理解,commands统计由那些命令操作组成,本节将重点分析commands如何实现统计。commands统计核心代码实现如下:
1. 代理mongos commands统计核心代码实现:
2. 存储节点mongod commands统计核心代码实现:
从上面的代码可以看出,只有对应命令类中shouldAffectCommandCounter()为true的命令才会进行commands计数。前面章节中我们提到,所有命令都有一个对应类实现相应功能,所有命令实现类都继承一个功能class command {}类,该类对shouldAffectCommandCounter()接口进行初始化。代码实现如下:
该接口默认为true,如果对应命令不需要进行commands计数统计,则需要在对应命令实现类中把该接口置为false。通过分析代码,可以看出,只有以下命令子类把shouldAffectCommandCounter()接口设置为false,搜索结果如下:
分析代码可以得出如下结论:
1) mongos代理中的clase Clusterfindcmd { }类和class Clustergetmorecmd {}类的shouldAffectCommandCounter()接口置为false,这两个类分别对应代理的“find”和“getMore”命令操作,也就说明mongos代理de这两个命令操作不会统计到commands中。
2) mongod分片存储节点的clase Findcmd{}、class Getmorecmd {}、class Write_commands{}三个类中把shouldAffectCommandCounter()接口置为false。这三个类分别对应mongod存储实例的如下几个命令:“find”、“getMore”、“insert”、“update”、“delete”五个命令。
mongos和mongod实例commands统计信息总结如下:
8.3慢日志、时延统计
每次客户端请求执行实践如果超过了log level配置的最大慢日志时间,则会把该操作详细信息记录下来,同时把本操作执行时间添加到对应的读或者写计数及时延统计中。命令处理模块中,时延相关统计包括以下两种统计:
① 慢日志统计
② 读写计数及时延统计
8.3.1慢日志统计
当启用了慢日志记录功能后,mongod会把执行时间超过指定阀值的慢日志记录下来。慢日志默认记录到服务日志文件(systemLog.path配置项设置),同时会记录日志到”system.profile”集合中。慢日志核心代码实现如下:
8.3.2读写操作计数及时延统计
根据请求command命令类型(包含读命令、写命令、command命令),以及命令执行时间,可以计算出不同类型命令的读写执行时间,从而计算出集群的读时延、写时延、command时延。mongodb所有命令可以归纳为读、写、command三类,核心代码如下:
从上面的代码可以看出,读、写、command分别对应以下命令:
读(read):包含getMore、find。
写(write):包含insert、delete、update。
command:读和写以外的所有命令。
命令执行完计算出命令运行时间后,mongod实例会记录下这个时延,累加到历史统计OperationLatencyHistogram中,读、写、command操作计数及时延统计分别记录到_reads、_writes、_commands三个变量成员中。该统计核心代码实现如下:
命令请求执行过程及其对应的读写请求操作计数、时延累加衔接代码实现如下:
mongod实例读、写、command操作计数及其各自时延统计可以通过db.serverStatus()接口获取,用户可用采样来计算对应的tps和平均时延信息。获取操作统计和时延统计的命令如下:
db.serverStatus().opLatencies
9. 问题回顾
《Mongodb command命令处理模块源码实现一》一文中提到的commands统计信息到这里就可以得到答案了,如下表所示:
mongos和mongod实例commands统计信息总结如下:
mongos代理mongostat统计可以汇总为下图所示:
mongod代理mongostat统计可以汇总为下图所示:
版权声明: 本文为 InfoQ 作者【杨亚洲(专注mongodb及高性能中间件)】的原创文章。
原文链接:【http://xie.infoq.cn/article/6de3aef4978a5078da44a8228】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论