mongodb内核源码实现、性能调优、最佳运维实践系列-数百万行mongodb内核源码阅读经验分享

关于做者

       前滴滴出行技术专家,现任OPPO 文档数据库 mongodb 负责人,负责 oppo 千万级峰值 TPS/ 十万亿级数据量文档数据库 mongodb 研发和运维工做,一直专一于分布式缓存、高性能服务端、数据库、中间件等相关研发。后续持续分享《 MongoDB 内核源码设计、性能优化、最佳运维实践》, Github 帐号地址 : https://github.com/y123456yzlinux

序言

      Mongodb 内核源码由第三方库 third_party  mongodb 服务层源码组成,其中 mongodb 服务层代码在不一样模块实现中依赖不一样的third_party 库,第三方库是 mongodb 服务层代码实现的基础 ( 例如 : 网络底层 IO 实现依赖 asio-master  , 底层存储依赖 wiredtiger 存储引擎库 ) ,其中第三方库也会依赖部分其余库 ( 例如: wiredtiger 库依赖 snappy 算法库, asio-master 依赖 boost  ) c++

      虽然Mongodb 内核源码数百万行,工程量巨大,可是 mongodb 服务层代码实现层次很是清晰,代码目录结构、类命名、函数命名、文件名命名都很是一目了然,充分体现了 10gen 团队的专业精神。git

      说明:mongodb 内核除第三方库 third_party 外的代码,这里统称为 mongodb 服务层代码。github

      本文以mongodb 服务层 transport 实现为例来讲明如何快速阅读整个 mongodb 代码,咱们在走读代码前,建议遵循以下准则。算法

1. 熟悉 mongodb 基本功能和使用方法

      首先,咱们须要熟悉mongodb 的基本功能,明白 mongodb 是作什么用的,用在什么地方,这样才能体现 mongodb 的真正价值。此外,咱们须要提早搭建一个 mongodb 集群玩一玩,这样也能够进一步促使咱们了解 mongodb 内部的一些经常使用基本功能。千万不要急于求成,若是连mongodb 是作什么的都不知道,或者连 mongodb 的运维操做方法都没玩过,直接读取代码会很是不适合,没有目的的走读代码不利于分析整个代码,同时阅读代码过程会很是痛苦。mongodb

2. 下载代码编译源码

      熟悉了mongodb 的基本功能,并搭建集群简单体验后,咱们就能够从 github 下载源码,本身编译源码生成二进制文件,编译文档存放于docs/building.md 代码目录中,源码编译步骤以下 :shell

1.  下载对应releases 中对应版本的源码数据库

2.  进入对于目录,参考docs/building.md 文件内容进行相关依赖工具安装缓存

3.  执行buildscripts/scons.py 编译出对应二进制文件,也能够直接 scons mongod mongos 这样编译。性能优化

4.  编译成功后的生产可执行文件存放于./build/opt/mongo/ 目录

      在正在编译代码并运行的过程当中,发现如下两个问题:

1.  编译出的二进制文件占用空间很大,以下图所示:

      从上图能够看出,经过strip处理工具处理后,二进制文件大小已经和官方二进制包大小同样了。

2. 在一些低版本操做系统运行的时候出错,找不到对应stdlib库,以下图所示:

      如上图所示,当编译出的二进制文件拷贝到线上运行后,发现没法运行,提示libstdc库找不到。缘由是咱们编译代码时候依赖的stdc库版本比其余操做系统上面的stdc库版本更高,形成了不兼容。

       解决办法:编译的时候编译脚本中带上-static-libstdc++,把stdc库经过静态库的方式进行编译,而不是经过动态库方式。

3. 了解代码日志模块使用方法,试着加打印调试

      因为前期咱们对代码总体实现不熟悉,不知道各个接口的调用流程,这时候就能够经过加日志打印进行调试。Mongodb的日志模块设计的比较完善,从日志中能够很明确的看出由那个功能模块打印日志,同时日志模块有多种打印级别。

1. 日志打印级别设置

       启动参数中verbose设置日志打印级别,日志打印级别设置方法以下:Mongod -f ./mongo.conf -vvvv    

这里的v越多,代表日志打印级别设置的越低,也就会打印更多的日志。一个v表示只会输出LOG(1)日志,-vv表示LOG(1) LOG(2)都会写日志。

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日志模块文件实现,对应到日志文件内容以下:

4. 学会用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调试了,以下图所示,能够很方便的定位到某个函数以前的调用栈信息,并进行单步、打印变量信息等调试:

5. 熟悉代码目录结构、模块细化拆分

       在进行代码阅读前还有很重要的一步就是熟悉代码目录及文件命名实现,mongodb服务层代码目录结构及文件命名都有很严格的规范。下面以truansport网络传输模块为例,transport模块的具体目录文件结构:

       从上面的文件分布内容,能够清晰的看出,整个目录中的源码实现文件大致能够分为以下几个部分:

  1. message_compressor_*网络传输数据压缩子模块
  2. service_entry_point*服务入口点子模块
  3. service_executor*服务运行子模块,即线程模型子模块
  4. service_state_machine*服务状态机处理子模块
  5. Session*回话信息子模块
  6. Ticket*数据分发子模块
  7. transport_layer*套接字处理及传输层模式管理子模块

       经过上面的拆分,整个大的transport模块实现就被拆分红了7个小模块,这7个小的子模块各自负责对应功能实现,同时各个模块相互衔接,总体实现网络传输处理过程的总体实现,下面的章节将就这些子模块进行简单功能说明。

6. 从main入口开始大致走读代码

       前面5个步骤事后,咱们已经熟悉了mongodb编译调试以及transport模块的各个子模块的相关代码文件实现及大致子模块做用。至此,咱们能够开始走读代码了,mongos和mongod的代码入口分别在mongoSMain()和mongoDbMain(),从这两个入口就能够一步一步了解mongodb服务层代码的总体实现。

       注意:走读代码前期不要深刻各类细节实现,大致了解代码实现便可,先大致弄明白代码中各个模块功能由那些子模块实现,千万不要深究细节。

7. 总结

       本章节主要给出了数百万级mongodb内核代码阅读的一些建议,整个过程能够总结为以下几点:

  1. 提早了解mongodb的做用及工做原理。
  2. 本身搭建集群提早学习下mongodb集群的经常使用运维操做,能够进一步帮助理解mongodb的功能特性,提高后期代码阅读的效率。
  3. 本身下载源码编译二进制可执行文件,同时学会使用日志模块,经过加日志打印的方式逐步开始调试。
  4. 学习使用gdb代码调试工具调试线程的运行流程,这样能够更进一步的促使快速学习代码处理流程,特别是一些复杂逻辑,能够大大提高走读代码的效率。
  5. 正式走读代码前,提早了解各个模块的代码目录结构,把一个大模块拆分红各个小模块,先大致浏览各个模块的代码实现。
  6. 前期走读代码千万不要深刻细节,捋清楚各个模块的大致功能做用后再开始一步一步的深刻细节,了解深层次的内部实现。
  7. 从main()入口逐步开始走读代码,结合log日志打印和gdb调试。
  8. 跳过总体流程中不熟悉的模块代码,只走读本次想弄明白的模块代码实现。