关于作者
前滴滴出行技术专家,现任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类初始化构造接口完成,注册过程核心代码如下所示:
1.//命令注册,所有注册的命令最终全部保存到_commands全局map表中
2.//name和oldName实际上是同一个command,只是可能因为历史原因,命令名改名了
3.Command::Command(StringData name, StringData oldName)
4. //命令名字符串
5. : _name(name.toString()),
6. //对应命令执行统计,total代表总的,failed代表执行失败的次数
7. _commandsExecutedMetric("commands." + _name + ".total", &_commandsExecuted),
8. _commandsFailedMetric("commands." + _name + ".failed", &_commandsFailed) {
9. //如果_commands map表还没有生成,则new一个
10. if (_commands == 0)
11. _commands = new CommandMap();
12. ......
13. //把name命令对应的command添加到map表中
14. Command*& c = (*_commands)[name];
15. if (c)
16. log() << "warning: 2 commands with name: " << _name;
17. c = this;
18. ......
19.
20. //大部分命令name和oldName是一样的,所以在数组中只会记录一个
21. //如果改名过,则name和oldName就不一样,这时候都需要注册到map表,对应同一个command
22. if (!oldName.empty()) //也就是name和oldName两个命令对应的是同一个this类
23. (*_commands)[oldName.toString()] = this;
24.}
command初始化构造函数中有两个入参,分表代表当前命令名和老旧命令名称,这样设计是为了兼容处理。
4.1 command注册方式一
超过99%的command命令通过定义一个全局类变量来完成注册,本文以shardServer实例的”insert”、”update”、”delete”、“find”为例,这几个命令注册方式如下:
1.//insert命令初始化
2.class CmdInsert : public WriteCommand { //
3.public:
4. //insert命令初始化构造
5. CmdInsert() : WriteCommand("insert") {}
6. ......
7. //认证检查
8. Status checkAuthForRequest(...) final {
9. ......
10. }
11.
12. //真正的Insert插入文档会走这里面
13. void runImpl(...);
14. }
15.} cmdInsert; //直接定义一个cmdInsert全局变量
16.
17.//update命令初始化
18.class CmdUpdate: public WriteCommand { //
19.public:
20. //update命令初始化构造
21. CmdUpdate() : WriteCommand("update") {}
22. ......
23. //认证检查
24. Status checkAuthForRequest(...) final {
25. ......
26. }
1. //查询计划执行过程
2. Status explain(...) const override {
3. ......
4. }
27. //真正的update插入文档会走这里面
28. void runImpl(...);
29. }
30.} cmdUpdate; //直接定义一个cmdUpdate全局变量
31.
32.//delete命令初始化
33.class CmdDelete: public WriteCommand { //
34.public:
35. //delete命令初始化构造
36. CmdDelete() : WriteCommand("delete") {}
37. ......
38. //认证检查
39. Status checkAuthForRequest(...) final {
40. ......
41. }
5. //查询计划执行过程
6. Status explain(...) const override {
7. ......
8. }
42.
43. //真正的delete插入文档会走这里面
44. void runImpl(...);
45. }
46.} cmdDelete; //直接定义一个cmdDelete全局变量
“find”命令也是通过定义一个全局FindCmd类变量来完成该命令的注册过程,注册过程代码如下:
9.//find命令实现类
10.class FindCmd : public BasicCommand {
11.public:
12. //初始化构造
13. FindCmd() : BasicCommand("find") {}
14. ......
15.
16. //查询计划执行过程
17. Status explain(...) const override {
18. ......
19. }
20.} findCmd; //直接定义一个findCmd全局变量
上面的类除了可以确定shardServer读写命令的注册方式外,还可以看出读写命令实现过程中,类继承关系稍微有点区别。主要体现在:FindCmd (查)命令类直接继承BasicCommand 命令类,而CmdInsert(增) 、CmdDelete(删)、CmdUpdate(改)这三个写相关的命令,则通过继承WriteCommand 来中转一次,WriteCommand 实现WriteCommand 共性接口,而三个子类则实现自己特有的功能。
shardServer实例,增、删、改、查四个级别命令的继承关系图可以总结为下图所示:
4.2 command注册方式二
除了直接定义一个全局命令类变量外,mongodb内核命令注册实现的时候,部分命令注册通过new一个命令类实现,例如planCache执行计划对应的几个命令就是通过该方式实现,代码实现如下:
1.//执行计划相关的几个command注册过程,通过new实现
2.MONGO_INITIALIZER_WITH_PREREQUISITES(SetupPlanCacheCommands, MONGO_NO_PREREQUISITES)
3.(InitializerContext* context) {
4. //执行计划相关的几个命令注册
5. new PlanCacheListQueryShapes();
6. new PlanCacheClear();
7. new PlanCacheListPlans();
8. return Status::OK();
9.}
10.
11.//test命令相关的几个command注册过程,也是通过new实现
12.MONGO_INITIALIZER(RegisterEmptyCappedCmd)(InitializerContext* context) {
13. //必须使能testCommandsEnabled,该命令才有效
14. if (Command::testCommandsEnabled) {
15. new CapTrunc();
16. new CmdSleep();
17. new EmptyCapped();
18. new GodInsert();
19. }
20. return Status::OK();
21.}
至此,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 命名规则总结
上面的命名规则可以总结为如下图解信息:
7. 命令run
结合《命令处理模块源码实现一》和本章节对command处理流程可以得出,runCommandImpl接口通过如下调用流程最终执行特定命令的run接口,这里以insert写入和读取流程为例,mongod实例写入调用过程如下图所示:
最终,mongod和mongos实例调用相关命令得run接口完成具体的command命令处理操作。mongos、mongod(shardServer)、mongod(configServer)相关常用的操作命令(以最基本的读写命令为例)入口及功能说明总结如下表所示: