前滴滴出行技术专家,现任OPPO 文档数据库 mongodb 负责人,负责 oppo 千万级峰值 TPS/ 十万亿级数据量文档数据库 mongodb 内核研发及运维工作,一直专注于分布式缓存、高性能服务端、数据库、中间件等相关研发。后续持续分享《 MongoDB 内核源码设计、性能优化、最佳运维实践》, Github 账号地址 :
《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 来源。
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 :
第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 接口实现在该层完成。
前面分析提到,当解析到对应命令字符串( 如: ” 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 初始化构造函数中有两个入参,分表代表当前命令名和老旧命令名称,这样设计是为了兼容处理。
超过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 类变量来完成该命令的注册过程,注册过程代码如下:
1. //find命令实现类 2. class FindCmd : public BasicCommand { 3. public: 4. //初始化构造 5. FindCmd() : BasicCommand("find") {} 6. ...... 7. 8. //查询计划执行过程 9. Status explain(...) const override { 10. ...... 11. } 12. } findCmd; //直接定义一个findCmd全局变量
上面的类除了可以确定shardServer 读写命令的注册方式外,还可以看出读写命令实现过程中,类继承关系稍微有点区别。主要体现在: FindCmd ( 查 ) 命令类直接继承 BasicCommand 命令类,而 CmdInsert (增) 、 CmdDelete ( 删 ) 、 Cmd Update( 改 ) 这三个写相关的命令,则通过继承 WriteCommand 来中转一次, WriteCommand 实现 WriteCommand 共性接口,而三个子类则实现自己特有的功能。
shardServer 实例,增、删、改、查四个级别命令的继承关系图可以总结为下图所示:
除了直接定义一个全局命令类变量外,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 命令注册过程就分析完毕,如果想新注册一个新的命令,可以模仿这个流程实现即可。
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 命令对应的源码文件命名和命令类命名。
提前梳理好各个校色实例的命名规范,对我们理解整个代码具有事半功倍的效果,同时也可以方便我们快速找到任何一个命令的代码文件及其对应命令的核心代码实现,具有 ” 举一反三 ” 的效果。
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 ” 标识,但是还是有部分命令命名不准寻该规则。如果想知道某个命令的源码实现文件,可以在前面提到的三个实例中搜索相应字符串即可定位到。注意:搜索的时候需要带上双引号。
和mongos 命名规则类似, configServer 支持的独有命令源码文件命名规则相比 shardServer 增加了 ”configsvr” 特性,从源码文件名即可明显的看出是configServer 独有的命令。
此外,命令对应类命命名也带有 ”ConfigSvr” 特性,例如class ConfigSvrAddShardCommand{} 、 class ConfigSvrMoveChunkCommand{} 等,命名规则和 mongos 代理支持的 command 命名规则类似。
上面的命名规则可以总结为如下图解信息:
来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/69984922/viewspace-2737718/,如需转载,请注明出处,否则将追究法律责任。