如何学习MongoDB源码
如何阅读数百万级代码量mongodb内核源码
熟悉mongodb基本功能和使用方法
首先,我们需要熟悉mongodb的基本功能,明白mongodb是做什么用的,用在什么地方,这样才能体现mongodb的真正价值。此外,我们需要提前搭建一个mongodb集群玩一玩,这样也可以进一步促使我们了解mongodb内部的一些常用基本功能。千万不要急于求成,如果连mongodb是做什么的都不知道,或者连mongodb的运维操作方法都没玩过,直接读取代码会非常不适合,没有目的的走读代码不利于分析整个代码,同时阅读代码过程会非常痛苦。
下载代码编译源码
熟悉了mongodb的基本功能,并搭建集群简单体验后,我们就可以从github下载源码,自己编译源码生成二进制文件,编译文档存放于docs/building.md 代码目录中,源码编译步骤如下:
- 下载对应releases中对应版本的源码
- 进入对于目录,参考docs/building.md文件内容进行相关依赖工具安装
- 执行buildscripts/scons.py编译出对应二进制文件,也可以直接scons mongod mongos这样编译。
- 编译成功后的生产可执行文件存放于./build/opt/mongo/目录
在正在编译代码并运行的过程中,发现以下两个问题:
- 编译出的二进制文件占用空间很大,通过strip处理工具处理后,二进制文件大小已经和官方二进制包大小一样了。
- 在一些低版本操作系统运行的时候出错,找不到对应stdlib库,当编译出的二进制文件拷贝到线上运行后,发现无法运行,提示libstdc库找不到。原因是我们编译代码时候依赖的stdc库版本比其他操作系统上面的stdc库版本更高,造成了不兼容。 解决办法:编译的时候编译脚本中带上-static-libstdc++,把stdc库通过静态库的方式进行编译,而不是通过动态库方式。
了解代码日志模块使用方法,试着加打印调试
由于前期我们对代码整体实现不熟悉,不知道各个接口的调用流程,这时候就可以通过加日志打印进行调试。Mongodb的日志模块设计的比较完善,从日志中可以很明确的看出由那个功能模块打印日志,同时日志模块有多种打印级别。
- 日志打印级别设置
启动参数中verbose设置日志打印级别,日志打印级别设置方法如下:Mongod -f ./mongo.conf -vvvv
这里的v越多,表明日志打印级别设置的越低,也就会打印更多的日志。一个v表示只会输出LOG(1)日志,-vv表示LOG(1) LOG(2)都会写日志。
- 如何在.cpp文件中使用日志模块记录日志 如果需要在一个新的.cpp文件中使用日志模块打印日志,需要进行如下步骤操作:
i) 添加宏定义 #define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kExecutor
ii) 使用LOG(N)或者log()来记录想要输出的日志内容,其中LOG(N)的N代表日志打印级别,log()对应的日志全记录到文件。
例如: LogComponent::kExecutor代表executor模块相关的日志,参考log_component.cpp日志模块文件实现,对应到日志文件内容如下:
学会用gdb调试mongodb代码
Gdb是linux系统环境下优秀的代码调试工具,支持设置断点、单步调试、打印变量信息、获取函数调用栈信息等功能。gdb工具可以绑定某个线程进行线程级调试,由于mongodb是多线程环境,因此在用gdb调试前,我们需要确定调试的线程号,mongod进程包含的线程号及其对应线程名查看方法如下:
注意:在调试mongod工作线程处理流程的时候,不要选择adaptive动态线程池模式,因为线程可能因为流量低引起工作线程不饱和而被销毁,从而造成调试过程因为线程销毁而中断,synchronous线程模式是一个链接一个线程,只要我们不关闭这个链接,线程就会一直存在,不会影响我们理解mongodb服务层代码实现逻辑。 synchronous线程模式调试的时候可以通过mongo shell链接mongod服务端端口来模拟一个链接,因此调试过程相对比较可控。
在对工作线程调试的时候,发现gdb无法查找到mongod进程的符号表,无法进行各种gdb功能调试,如下图所示: 上述gdb无法attach到指定线程调试的原因是无法加载二进制文件符号表,这是因为编译的时候没有加上-g选项引起,mongodb通过SConstruct脚本来进行scons编译,要启用gdb功能需要在scons编译代码的时候指定gdbserver选项:scons –gdbserver=GDBSERVER -j 2。
编译出新的二进制文件后,就可以gdb调试了,如下图所示,可以很方便的定位到某个函数之前的调用栈信息,并进行单步、打印变量信息等调试:
熟悉代码目录结构、模块细化拆分
在进行代码阅读前还有很重要的一步就是熟悉代码目录及文件命名实现,mongodb服务层代码目录结构及文件命名都有很严格的规范。下面以truansport网络传输模块为例,transport模块的具体目录文件结构: 从上面的文件分布内容,可以清晰的看出,整个目录中的源码实现文件大体可以分为如下几个部分:
-
message_compressor_*网络传输数据压缩子模块
-
service_entry_point*服务入口点子模块
-
service_executor*服务运行子模块,即线程模型子模块
-
service_state_machine*服务状态机处理子模块
-
Session*回话信息子模块
-
Ticket*数据分发子模块
-
transport_layer*套接字处理及传输层模式管理子模块
通过上面的拆分,整个大的transport模块实现就被拆分成了7个小模块,这7个小的子模块各自负责对应功能实现,同时各个模块相互衔接,整体实现网络传输处理过程的整体实现,下面的章节将就这些子模块进行简单功能说明。
从main入口开始大体走读代码
前面5个步骤过后,我们已经熟悉了mongodb编译调试以及transport模块的各个子模块的相关代码文件实现及大体子模块作用。至此,我们可以开始走读代码了,mongos和mongod的代码入口分别在mongoSMain()和mongoDbMain(),从这两个入口就可以一步一步了解mongodb服务层代码的整体实现。
注意:走读代码前期不要深入各种细节实现,大体了解代码实现即可,先大体弄明白代码中各个模块功能由那些子模块实现,千万不要深究细节。
##