万字长文 | MongoDB络传输处理源码实现及性能调优

本文来自OPPO互联网基础技术团队,转载请注名做者。同时欢迎关注咱们的公众号:OPPO_tech,与你分享OPPO前沿互联网技术及活动。

开源mongodb代码规模数百万行,本篇文章内容主要分析mongodb网络传输模块内部实现及其性能调优方法,学习网络IO处理流程,体验不一样工做线程模型性能极致设计原理。另一个目的就是引导你们快速进行百万级别规模源码阅读,作到不一样大工程源码”触类旁通”快速阅读的目的。react

此外,mognodb网络工做线程模型设计很是好,不只很是值得数据库相关研发人员学习,中间件、分布式、高并发、服务端等相关研发人员也能够借鉴,极力推荐你们学习。linux

1. 如何阅读数百万级大工程内核源码

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.1 熟悉mongodb基本功能和使用方法

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

1.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)编译出的二进制文件占用空间很大,以下图所示:segmentfault

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

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

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

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

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

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

1)日志打印级别设置

启动参数中verbose设置日志打印级别,日志打印级别设置方法以下:

Mongod -f ./mongo.conf -vvvv

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

2)如何在.cpp文件中使用日志模块记录日志

若是须要在一个新的.cpp文件中使用日志模块打印日志,须要进行以下步骤操做:

  1. 添加宏定义 #define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kExecutor
  2. 使用LOG(N)或者log()来记录想要输出的日志内容,其中LOG(N)的N表明日志打印级别,log()对应的日志全记录到文件。

例如: LogComponent::kExecutor表明executor模块相关的日志,参考log_component.cpp日志模块文件实现,对应到日志文件内容以下:

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

1.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个小的子模块各自负责对应功能实现,同时各个模块相互衔接,总体实现网络传输处理过程的总体实现,下面的章节将就这些子模块进行简单功能说明。

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

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

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

1.7 总结

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

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

2. mongodb内核网络传输transport模块实现原理

从1.5章节中,咱们把transport功能模块细化拆分红了网络传输数据压缩子模块、服务入口子模块、线程模型子模块、状态机处理子模块、session会话信息子模块、数据分发子模块、套接字处理和传输管理子模块,总共七个子模块。

实际上mongodb服务层代码的底层网络IO实现依赖asio库完成,所以transport功能模块应该是7+1个子模块构成,也就是服务层代码实现由8个子模块支持。

2.1 asio网络IO库实现原理

Asio是一个优秀网络库,依赖于boost库的部分实现,支持linux、windos、unix等多平台,mongodb基于asio库来实现网络IO及定时器处理。asio库因为为了支持多平台,在代码实现中用了不少C++的模板,同时用了不少C++的新语法特性,所以总体代码可读性相比mongodb服务层代码差不少。

服务端网络IO异步处理流程大致以下:

  1. 调用socket()建立一个套接字,获取一个socket描述符。
  2. 调用bind()绑定套接字,同时经过listen()来监听客户端连接,注册该socket描述符到epoll事件集列表,等待accept对应的新链接读事件到来。
  3. 经过epoll_wait获取到accept对应的读事件信息,而后调用accept()来接受客户的链接,并获取一个新的连接描述符new_fd。
  4. 注册新的new_fd到epoll事件集列表,当该new_fd描述符上有读事件到来,因而经过epoll_wait获取该事件,开始该fd上的数据读取。
  5. 读取数据完毕后,开始内部处理,处理完后发送对应数据到客户端。若是一次write数据到内核协议栈写太多,形成协议栈写满,则添加写事件到epoll事件列表。

服务端网络IO同步方式处理流程和异步流程大同小异,少了epoll注册和epoll事件通知过程,直接同步调用accept()、recv()、send()进行IO处理。

同步IO处理方式相对比较简单,下面仅分析和mongodb服务层transport模块结合比较紧密的asio异步IO实现原理。

Mongodb服务层用到的Asio库功能中最重要的几个结构有io_context、scheduler、epoll_reactor。Asio把网络IO处理任务、状态机调度任务作为2种不一样操做,分别由两个继承自operation的类结构管理,每种类型的操做也就是一个任务task。io_context、scheduler、epoll_reactor最重要的功能就是管理和调度这些task有序而且高效的运行。

2.1.1 io_context类实现及其做用

io_context 上下文类是mongodb服务层和asio网络库交互的枢纽,是mongodb服务层和asio库进行operation任务交互的入口。该类负责mongodb相关任务的入队、出队,并与scheduler调度处理类配合实现各类任务的高效率运行。Mongodb服务层在实现的时候,accept新链接任务使用_acceptorIOContext这个IO上下文成员实现,数据分发及其相应回调处理由_workerIOContext上下文成员实现。

该类的几个核心接口功能以下表所示:

Io_context类成员/函数名 功能 备注说明
impl_type& impl_; Mongodb对应的type类型为scheduler 经过该成员来调用scheduler调度类的接口
io_context::run() 负责accept对应异步回调处理 1.mongodb中该接口只针对accept对应IO异步处理 2.调用scheduler::run()进行accept异步读操做
io_context::stop() 中止IO调度处理 调用scheduler::stop()接口
io_context::run_one_until() 1. 从全局队列上获取一个任务执行 2. 若是全局队列为空,则调用epoll_wait()获取网络IO事件处理 调用schedule::wait_one()
io_context::post() 任务入队到全局队列 调用scheduler::post_immediate_completion()
io_context::dispatch() 1.若是调用该接口的线程已经运行过全局队列中的任务,则直接继续由本线程运行该入队的任务 2.若是不知足条件1条件,则直接入队到全局队列,等待调度执行 若是条件1知足,则直接由本线程执行 若是条件1不知足,则调用scheduler::do_dispatch ()

总结:

  1. 从上表的分析能够看出,和mongodb直接相关的几个接口最终都是调用schedule类的相关接口,整个实现过程参考下一节scheduler调度实现模块。
  2. 上表中的几个接口按照功能不一样,能够分为入队型接口(poll、dispatch)和出队型接口(run_for、run、run_one_for)。
  3. 按照和io_context的关联性不一样,能够分为accept相关io(_acceptorIOContext)处理的接口(run、stop)和新连接fd对应Io(_workerIOContext)数据分发相关处理及回调处理的接口(run_for、run_one_for、poll、dispatch)。
  4. io_context上下文的上述接口,除了dispatch在某些状况下直接运行handler外,其余接口最终都会间接调用scheduler调度类接口。

2.1.2 asio调度模块scheduler实现

上一节的io_context上下文中提到mongodb操做的io上下文最终都会调用scheduler的几个核心接口,io_context只是起衔接mongodb和asio库的连接桥梁。scheduler类主要工做在于完成任务调度,该类和mongodb相关的几个主要成员变量及接口以下表:

scheduler类主要成员/接口 功能 备注说明
mutable mutex mutex_; 互斥锁,全局队列访问保护 多线程从全局队列获取任务的时候加锁保护
op_queue<operation> op_queue_; 全局任务队列,全局任务和网络事件相关任务都添加到该队列 3.1.1中的5种类型的任务都入队到了该全局队列
bool stopped_; 线程是否可调度标识 为true后,将再也不处理epoll相关事件,参考scheduler::do_run_one
event wakeup_event_; 唤醒等待锁得线程 实际event由信号量封装
task_operation task_operation_; 特殊的operation 在链表中没进行一次epoll获取到IO任务加入全局队列后,都会紧接着添加一个特殊operation
reactor* task_; 也就是epoll_reactor 借助epoll实现网络事件异步处理
atomic_count outstanding_work_; 套接字描述符个数 accept获取到的连接数fd个数+1(定时器fd)
scheduler::run() 循环处理epoll获取到的accept事件信息 循环调用scheduler::do_run_one()接口
scheduler::do_dispatch() 任务入队 任务入队到全局队列op_queue_
scheduler::do_wait_one() 任务出队执行 若是队列为空则获取epoll事件集对应的网络IO任务放入全局op_queue_队列
scheduler::restart() 从新启用调度 实际上就是修改stopped_标识为false
scheduler::stop_all_threads() 中止调度 实际上就是修改stopped_标识为true

2.1.3 operation任务队列

从前面的分析能够看出,一个任务对应一个operation类结构,asio异步实现中schduler调度的任务分为IO处理任务(accept处理、读io处理、写io处理、网络IO处理回调处理)和全局状态机任务,总共2种任务小类。

此外,asio还有一种特殊的operation,该Operastion什么也不作,只是一个特殊标记。网络IO处理任务、状态机处理任务、特殊任务这三类任务分别对应三个类结构,分别是:reactor_op、completion_handler、task_operation_,这三个类都会继承基类operation。

1. operation基类实现

operation基类实际上就是scheduler_operation类,经过typedef scheduler_operation operation指定,是其余三个任务的父类,其主要实现接口以下:

operation类主要成员/接口 功能 备注说明
unsigned int task_result_ Epoll_wait获取到的事件位图信息记录到该结构中 在descriptor_state::do_complete中取出位图上的事件信息作底层IO读写处理
func_type func_; 须要执行的任务
scheduler_operation::complete() 执行func_() 任务的内容在func()中运行

2. completion_handler状态机任务

当mongodb经过listener线程接受到一个新连接后,会生成一个状态机调度任务,而后入队到全局队列op_queue_,worker线程从全局队列获取到该任务后调度执行,从而进入状态机调度流程,在该流程中会触发epoll相关得网络IO注册及异步IO处理。一个全局状态机任务对应一个completion_handler类,该类主要成员及接口说明以下表所示:

completion_handler类主要成员/接口 功能 备注说明
Handler handler_; 全局状态机任务函数 这个handler就至关于一个任务,其实是一个函数
completion_handler(Handler& h) 构造初始化 启用该任务,等待调度
completion_handler::do_complete() 执行handler_回调 任务的内容在handler_()中运行

completion_handler状态机任务类实现过程比较简单,就是初始化和运行两个接口。全局任务入队的时候有两种方式,一种是io_context::dispatch方式,另外一种是io_context::post。从前面章节对这两个接口的代码分析能够看出,任务直接入队到全局队列op_queue_中,而后工做线程经过scheduler::do_wait_one从队列获取该任务执行。

注意: 状态机任务入队由Listener线程(新连接到来的初始状态机任务)和工做线程(状态转换任务)共同完成,任务出队调度执行由mongodb工做线程执行,状态机具体任务内容在后面《状态机实现》章节实现。

3. 网络IO事件处理任务

网络IO事件对应的Opration任务最终由reactor_op类实现,该类主要成员及接口以下:

reactor_op类主要成员/接口 功能 备注说明
asio::error_code ec_; 全局状态机任务函数 这个handler就至关于一个任务,其实是一个函数
std::size_t bytes_transferred_; 读取或者发送的数据字节数 Epoll_wait返回后获取到对应的读写事件,而后进行数据分发操做
enum status; 底层数据读写状态 标识读写数据的状态
perform_func_type perform_func_; 底层IO操做的函数指针 perform()中运行
status perform(); 运行perform_func_函数 perform实际上就是数据读写的底层实现
reactor_op(perform_func_type perform_func, func_type complete_func) 类初始化 这里有两个func: 1. 底层数据读写实现的接口,也就是perform_func 2. 读取或者发送一个完整mongodb报文的回调接口,也就是complete_func

从reactor_op类能够看出,该类的主要两个函数成员:perform_func_和complete_func。其中perform_func_函数主要负责异步网络IO底层处理,complete_func用于获取到一个新连接、接收或者发送一个完整mongodb报文后的后续回调处理逻辑。

perform_func_具体功能包含以下三种以下:

  1. 经过epoll事件集处理底层accept获取新链接fd。
  2. fd上的数据异步接收
  3. fd上的数据异步发送

针对上面的三个网络IO处理功能,ASIO在实现的时候,分别经过三个不一样的类(reactive_socket_accept_op_base、reactive_socket_recv_op_base、reactive_socket_send_op_base)实现,这三个类都继承父类reactor_op。

这三个类的功能总结以下表所示:

类名 功能 说明
reactive_socket_accept_op_base 1. Accept()系统调用获取新fd 2. 获取到一个新fd后的mongodb层逻辑回调处理 Accept()系统调用由perform_func()函数处理 获取到新连接后的逻辑回调由complete_func执行
reactive_socket_recv_op_base 1. 读取一个完整mongodb报文读取 2. 读取完整报文后的mongodb服务层逻辑回调处理 从一个连接上读取一个完整mongodb报文读取由perform_func()函数处理 读取完整报文后的mongodb服务层逻辑回调处理由complete_func执行
reactive_socket_send_op_base 1. 发送一个完整的mongodb报文 2. 发送完一个完整mongodb报文后的mongodb服务层逻辑回调处理 Accept()系统调用由perform_func()函数处理 获取到新连接后的逻辑回调由complete_func执行

总结: asio在实现的时候,把accept处理、数据读、数据写分开处理,都继承自公共基类reactor_op,该类由两个操做组成:底层IO操做和回调处理。其中,asio的底层IO操做最终由epoll_reactor类实现,回调操做最终由mongodb服务层指定,底层IO操做的回调映射表以下:

底层IO操做类型 Mongodb服务层回调 说明
Accept(reactive_socket_accept_op_base) ServiceEntryPointImpl::startSession,回调中进入状态机任务流程 Listener线程获取到一个新连接后mongodb的回调处理
Recv(reactive_socket_recv_op_base) ServiceStateMachine::_sourceCallback,回调中进入状态机任务流程 接收一个完整mongodb报文的回调处理
Send(reactive_socket_send_op_base) ServiceStateMachine::_sinkCallback,回调中进入状态机任务流程 发送一个完整mongodb报文的回调处理

说明: 网络IO事件处理任务实际上在状态机任务内运行,也就是状态机任务中调用asio库进行底层IO事件运行处理。

4. 特殊任务task_operation

前面提到,ASIO库中还包含一种特殊的task_operation任务,asio经过epoll_wait获取到一批IO事件后,会添加到op_queue_全局队列,工做线程从队列取出任务有序执行。每次经过epoll_wait获取到IO事件信息后,除了添加这些读写事件对应的底层IO处理任务到全局队列外,每次还会额外生成一个特殊task_operation任务添加到队列中。

为什么引入一个特殊任务的Opration

工做线程变量全局op_queue_队列取出任务执行,若是从队列头部取出的是特殊Op操做,就会立马触发获取epoll网络事件信息,避免底层网络IO任务长时间不被处理引发的"饥饿"状态,保证状态机任务和底层IO任务都能”平衡”运行。

asio库底层处理实际上由epoll_reactor类实现,该类主要负责epoll相关异步IO实现处理,鉴于篇幅epoll reactor相关实现将在后续《mongodb内核源码实现及调优系列》相关章节详细分析。

2.2 message_compressor网络传输数据压缩子模块

网络传输数据压缩子模块主要用于减小网络带宽占用,经过CPU来换取IO消耗,也就是以更多CPU消耗来减小网络IO压力。

鉴于篇幅,该模块的详细源码实现过程将在《mongodb内核源码实现及调优系列》相关章节分享。

2.3 transport_layer套接字处理及传输层管理子模块

transport_layer套接字处理及传输层管理子模块功能主要以下:

  1. 套接字相关初始化处理
  2. 结合asio库实现异步accept处理
  3. 不一样线程模型管理及初始化

鉴于篇幅,该模块的详细源码实现过程将在《mongodb内核源码实现及调优系列》相关章节详细分析。

2.4 session会话子模块

Session会话模块功能主要以下:

  1. 负责记录HostAndPort、新链接fd信息
  2. 经过和底层asio库的直接互动,实现数据的同步或者异步收发。

鉴于篇幅,该模块的详细源码实现过程将在《mongodb内核源码实现及调优系列》相关章节详细分析。

2.5 Ticket数据分发子模块

Ticket数据分发子模块主要功能以下:

  1. 调用session子模块进行底层asio库处理
  2. 拆分数据接收和数据发送到两个类,分别实现。
  3. 完整mongodb报文读取
  4. 接收或者发送mongodb报文后的回调处理

鉴于篇幅,该模块的详细源码实现过程将在《mongodb内核源码实现及调优系列》相关章节详细分析。

2.6 service_state_machine状态机调度子模块

service_state_machine状态机处理模块主要功能以下:

  1. Mongodb网络数据处理状态转换
  2. 配合状态转换逻辑把一次mongodb请求拆分为二个大的状态任务: 接收一个完整长度mongodb报文、接收到一个完整报文后的后续全部处理(含报文解析、认证、引擎层处理、应答客户端等)。
  3. 配合工做线程模型子模块,把步骤2的两个任务按照指定的状态转换关系进行调度运行。

鉴于篇幅,该模块的详细源码实现过程将在《mongodb内核源码实现及调优系列》相关章节详细分析。

2.7 service_entry_point服务入口点子模块

service_entry_point服务入口点子模块主要负责以下功能:

  1. 链接数控制
  2. Session会话管理
  3. 接收到一个完整报文后的回调处理(含报文解析、认证、引擎层处理等)

鉴于篇幅,该模块的详细源码实现过程将在《mongodb内核源码实现及调优系列》相关章节详细分析。

2.8 service_executor服务运行子模块,即线程模型子模块

线程模型设计在数据库性能指标中起着很是重要的做用,所以本文将重点分析mongodb服务层线程模型设计,体验mongodb如何经过优秀的工做线程模型来达到多种业务场景下的性能极致表现。

service_executor线程子模块,在代码实现中,把线程模型分为两种:synchronous线程模式和adaptive线程模型。Mongodb启动的时候经过配置参数net.serviceExecutor来肯定采用那种线程模式运行mongo实例,配置方式以下:

net: //同步线程模式配置

serviceExecutor: synchronous

或者 //动态线程池模式配置

net:

serviceExecutor: synchronous

2.8.1 synchronous同步线程模型(一个连接一个线程)实现原理

synchronous同步线程模型,listener线程每接收到一个连接就会建立一个线程,该连接上的全部数据读写及内部请求处理流程将一直由本线程负责,整个线程的生命周期就是这个连接的生命周期。

1. 网络IO操做方式

synchronous同步线程模型实现过程比较简单,线程循循环以同步IO操做方式从fd读取数据,而后处理数据,最后返回客户端对应得数据。同步线程模型方式针对某个连接的系统调用以下图所示(mongo shell创建连接后show dbs):

2. 性能极致提高小细节

虽然synchronous线程模型比较简单,可是mongodb服务层再实现的时候针对细节作了极致的优化,主要体如今以下代码实现细节上面:


具体实现中,mongodb线程每处理16次用户请求,就让线程空闲一下子。同时,当总的工做线程数大于cpu核数后,每次都作让出一次CPU调度。经过这两种方式,在性能测试中能够提高5%的性能,虽然提高性能很少,可是充分体现了mongodb在性能优化提高方面所作的努力。

3. 同步线程模型监控统计

能够经过以下命令获取同步线程模型方式获取当前mongodb中的连接数信息:

该监控中主要由两个字段组成:passthrough表明同步线程模式,threadsRunning表示当前进程的工做线程数。

2.8.2 adaptive异步线程模型(动态线程池)实现原理

adaptive动态线程池模型,内核实现的时候会根据当前系统的访问负载动态的调整线程数。当线程CPU工做比较频繁的时候,控制线程增长工做线程数;当线程CPU比较空闲后,本线程就会自动消耗退出。下面一块儿体验adaptive线程模式下,mongodb是如何作到性能极致设计的。

1. 线程池初始化

Mongodb默认初始化后,线程池线程数默认等于CPU核心数/2,主要实现以下:

从代码实现能够看出,线程池中最低线程数能够经过adaptiveServiceExecutorReservedThreads配置,若是没有配置则默认设置为CPU/2。

2. 工做线程运行时间相关的几个统计

3.6状态机调度模块中提到,一个完整的客户端请求处理能够转换为2个任务:经过asio库接收一个完整mongodb报文、接收到报文后的后续全部处理(含报文解析、认证、引擎层处理、发送数据给客户端等)。假设这两个任务对应的任务名、运行时间分别以下表所示:

任务名 功能 运行时间
Task1 调用底层asio库接收一个完整mongodb报文 T1
Task2 接收到报文后的后续全部处理(含报文解析、认证、引擎层处理、发送数据给客户端等) T2

客户端一次完整请求过程当中,mongodb内部处理过程=task1 + task2,整个请求过程当中mongodb内部消耗的时间T1+T2。

实际上若是fd上没有数据请求,则工做线程就会等待数据,等待数据的过程就至关于空闲时间,咱们把这个时间定义为T3。因而一个工做线程总运行时间=内部任务处理时间+空闲等待时间,也就是线程总时间=T1+T2+T3,只是T3是无用等待时间。

3. 单个工做线程如何判断本身处于”空闲”状态

步骤2中提到,线程运行总时间=T1 + T2 +T3,其中T3是无用等待时间。若是T3的无用等待时间占比很大,则说明线程比较空闲。

Mongodb工做线程每次运行完一次task任务后,都会判断本线程的有效运行时间占比,有效运行时间占比=(T1+T2)/(T1+T2+T3),若是有效运行时间占比小于某个阀值,则该线程自动退出销毁,该阀值由adaptiveServiceExecutorIdlePctThreshold参数指定。该参数在线调整方式:

db.adminCommand( { setParameter: 1, adaptiveServiceExecutorIdlePctThreshold: 50} )

4. 如何判断线程池中工做线程“太忙”

Mongodb服务层有个专门的控制线程用于判断线程池中工做线程的压力状况,以此来决定是否在线程池中建立新的工做线程来提高性能。

控制线程每过必定时间循环检查线程池中的线程压力状态,实现原理就是简单的实时记录线程池中的线程当前运行状况,为如下两类计数:总线程数_threadsRunning、当前正在运行task任务的线程数_threadsInUse。若是_threadsRunning=_threadsRunning,说明全部工做线程当前都在处理task任务,这时候已经没有多余线程去asio库中的全局任务队列op_queue_中取任务执行了,这时候队列中的任务就不会获得及时的执行,就会成为响应客户端请求的瓶颈点。

5. 如何判断线程池中全部线程比较“空闲”

control控制线程会在收集线程池中全部工做线程的有效运行时间占比,若是占比小于指定配置的阀值,则表明整个线程池空闲。

前面已经说明一个线程的有效时间占比为:(T1+T2)/(T1+T2+T3),那么全部线程池中的线程总的有效时间占比计算方式以下:

全部线程的总有效时间TT1 = (线程池中工做线程1的有效时间T1+T2) + (线程池中工做线程2的有效时间T1+T2) + ..... + (线程池中工做线程n的有效时间T1+T2)

全部线程总运行时间TT2 = (线程池中工做线程1的有效时间T1+T2+T3) + (线程池中工做线程2的有效时间T1+T2+T3) + ..... + (线程池中工做线程n的有效时间T1+T2+T3)

线程池中全部线程的总有效工做时间占比 = TT1/TT2

6. control控制线程如何动态增长线程池中线程数

Mongodb在启动初始化的时候,会建立一个线程名为”worker-controller”的控制线程,该线程主要工做就是判断线程池中是否有充足的工做线程来处理asio库中全局队列op_queue_中的task任务,若是发现线程池比较忙,没有足够的线程来处理队列中的任务,则在线程池中动态增长线程来避免task任务在队列上排队等待。

control控制线程循环主体主要压力判断控制流程以下:

while {
    #等待工做线程唤醒条件变量,最长等待stuckThreadTimeout
    _scheduleCondition.wait_for(stuckThreadTimeout)
    
    #获取线程池中全部线程最近一次运行任务的总有效时间TT1
    Executing = _getThreadTimerTotal(ThreadTimer::Executing);
    #获取线程池中全部线程最近一次运行任务的总运行时间TT2
    Running = _getThreadTimerTotal(ThreadTimer::Running);
    #线程池中全部线程的总有效工做时间占比 = TT1/TT2
    utilizationPct = Executing / Running;
    
    #表明control线程过久没有进行线程池压力检查了
    if(本次循环到该行代码的时间 > stuckThreadTimeout阀值) {
        #说明过久没作压力检查,形成工做线程不够用了
        if(_threadsInUse == _threadsRunning) {
            #批量建立一批工做线程
            for(; i < reservedThreads; i++)
                #建立工做线程
                _startWorkerThread();
        }
        #control线程继续下一次循环压力检查
        continue;
    }    
    
    #若是当前线程池中总线程数小于最小线程数配置
    #则建立一批线程,保证最少工做线程数达到要求
    if (threadsRunning < reservedThreads) {
        while (_threadsRunning < reservedThreads) {
            _startWorkerThread();
        }
    }
    
    #检查上一次循环到本次循环这段时间范围内线程池中线程的工做压力
    #若是压力不大,则说明无需增长工做线程数,则继续下一次循环
    if (utilizationPct < idlePctThreshold) {
        continue;
    }
    
    #若是发现已经有线程建立起来了,可是这些线程尚未运行任务
    #这说明当前可用线程数可能足够了,咱们休息sleep_for会儿在判断一下
    #该循环最多持续stuckThreadTimeout时间
    do {
        stdx::this_thread::sleep_for();
    } while ((_threadsPending.load() > 0) &&
        (sinceLastControlRound.sinceStart() < stuckThreadTimeout)
    
    #若是tasksQueued队列中的任务数大于工做线程数,说明任务在排队了
    #该扩容线程池中线程了
    if (_isStarved()) {
        _startWorkerThread();
    }
}

7. 实时serviceExecutorTaskStats线程模型统计信息

本文分析的mongodb版本为3.6.1,其network.serviceExecutorTaskStats网络线程模型相关统计经过db.serverStatus().network.serviceExecutorTaskStats能够查看,以下图所示:

上图的几个信息功能能够分类为三大类,说明以下:

大类类名 字段名 功能
executor Adaptive,说明是动态线程池模式
线程统计 threadsInUse 当前正在运行task任务的线程数
threadsRunning 当前运行的线程数
threadsPending 当前建立起来,可是尚未执行过task任务的线程数
队列统计 totalExecuted 线程池运行成功的任务总数
tasksQueued 入队到全局队列的任务数
deferredTasksQueued 等待接收网络IO数据来读取一个完整mongodb报文的任务数
时间统计 totalTimeRunningMicros 全部工做线程运行总时间(含等待网络IO的时间T1 + 读一个mongodb报文任务的时间T2 + 一个请求后续处理的时间T3)
totalTimeExecutingMicros 也就是T2+T3,mongodb内部响应一个完整mongodb耗费的时间
totalTimeQueuedMicros 线程池中全部线程从建立到被用来执行第一个任务的等待时间

上表中各个字段的都有各自的意义,咱们须要注意这些参数的如下状况:

  1. threadsRunning - threadsInUse的差值越大说明线程池中线程比较空闲,差值越小说明压力越大
  2. threadsPending越大,表示线程池越空闲
  3. tasksQueued - totalExecuted的差值越大说明任务队列上等待执行的任务越多,说明任务积压现象越明显
  4. deferredTasksQueued越大说明工做线程比较空闲,在等待客户端数据到来
  5. totalTimeRunningMicros - totalTimeExecutingMicros差值越大说明越空闲

上面三个大类中的整体反映趋势都是同样的,任何一个差值越大就说明越空闲。

在后续mongodb最新版本中,去掉了部分重复统计的字段,同时也增长了如下字段,以下图所示:

新版本增长的几个统计项实际上和3.6.1大同小异,只是把状态机任务按照不通类型进行了更加详细的统计。新版本中,更重要的一个功能就是control线程在发现线程池压力过大的时候建立新线程的触发状况也进行了统计,这样咱们就能够更加直观的查看动态建立的线程是由于什么缘由建立的。

8. Mongodb-3.6早期版本control线程动态调整动态增长线程缺陷1例

从步骤6中能够看出,control控制线程建立工做线程的第一个条件为:若是该线程超过stuckThreadTimeout阀值都没有作线程压力控制检查,而且线程池中线程数所有在处理任务队列中的任务,这种状况control线程一次性会建立reservedThreads个线程。reservedThreads由adaptiveServiceExecutorReservedThreads配置,若是没有配置,则采用初始值CPU/2。

那么问题来了,若是我提早经过命令行配置了这个值,而且这个值配置的很是大,例如一百万,这里岂不是要建立一百万个线程,这样会形成操做系统负载升高,更容易引发耗尽系统pid信息,这会引发严重的系统级问题。

不过,不用担忧,最新版本的mongodb代码,内核代码已经作了限制,这种状况下建立的线程数变为了1,也就是这种状况只建立一个线程。

9. adaptive线程模型实时参数

动态线程模设计的时候,mongodb设计者考虑到了不通应用场景的状况,所以在核心关键点增长了实时在线参数调整设置,主要包含以下7种参数,以下表所示:

参数名 做用
adaptiveServiceExecutorReservedThreads 默认线程池最少线程数
adaptiveServiceExecutorRunTimeMillis 工做线程从全局队列中获取任务执行,若是队列中没有任务则须要等待,该配置就是限制等待时间的最大值
adaptiveServiceExecutorRunTimeJitterMillis 若是配置为0,则任务入队从队列获取任务等待时间则不须要添加一个随机数
adaptiveServiceExecutorStuckThreadTimeoutMillis 保证control线程一次while循环操做(循环体里面判断是否须要增长线程池中线程,若是发现线程池压力大,则增长线程)的时间为该配置的值
adaptiveServiceExecutorMaxQueueLatencyMicros 若是control线程一次循环的时间不到adaptiveServiceExecutorStuckThreadTimeoutMillis,则do {} while(),直到保证本次while循环达到须要的时间值。 {}中就是简单的sleep,sleep的值就是本配置项的值。
adaptiveServiceExecutorIdlePctThreshold 单个线程循环从全局队列获取task任务执行,同时在每次循环中会判断该本工做线程的有效运行时间占比,若是占比小于该配置值,则本线程自动退出销毁。
adaptiveServiceExecutorRecursionLimit 因为adaptive采用异步IO操做,所以可能存在线程同时处理多个请求的状况,这时候咱们就须要限制这个递归深度,若是深度过大,容易引发部分请求慢的状况。

命令行实时参数调整方法以下,以adaptiveServiceExecutorReservedThreads为例,其余参数调整方法相似:

db.adminCommand( { setParameter: 1, adaptiveServiceExecutorReservedThreads: xx} )

Mongodb服务层的adaptive动态线程模型设计代码实现很是优秀,有不少实现细节针对不一样应用场景作了极致优化,鉴于篇幅,该模块的详细源码实现过程将在《mongodb内核源码实现及调优系列》相关章节详细分析。

3. 不一样线程模型性能多场景PK

前面对线程模型进行了分析,下面针对Synchronous和adaptive两种模型设计进行不一样场景和不一样纬度的测试,总结两种模型各类的使用场景,并根据测试结果结合前面的理论分析得出不一样场景下那种线程模型更合适。

测试纬度主要包括:并发数、请求快慢。本文的压力测试工具采用sysbench实现,如下是这几种纬度的名称定义:

并发数: 也就是sysbench启动的线程数,默认一个线程对应一个连接

请求快慢: 快请求就是请求返回比较快,sysbench的lua测试脚本经过read同一条数据模拟快请求(走存储引擎缓存),内部处理时延小于1ms。 慢请求也经过sysbench测试,测试脚本作range操做,单次操做时延几十ms。

sysbench慢操做测试原理: 首先写20000万数据到库中,而后经过range操做测试,range操做比较慢,慢操做启动方式:

./sysbench --mongo-write-concern=1 --mongo-url="mongodb://xxx" --mongo-database-name=sbtest11 --oltp_table_size=600 --rand-type=pareto --report-interval=2 --max-requests=0 --max-time=200 --test=./tests/mongodb/ranges_ro.lua --oltp_range_size=2000 --num-threads=xx run

测试硬件资源,容器一台,配置以下:

  1. CPU=32
  2. 内存=64G

3.1 场景1、低并发场景+快请求测试

Sysbench并发线程数70测试结果以下图所示(上图为adaptive模式,下图为Synchronousm线程模式):

Sysbench并发线程数500测试结果以下图所示(上图为adaptive模式,下图为Synchronousm线程模式):

Sysbench并发线程数1000测试结果以下图所示(上图为adaptive模式,下图为Synchronousm线程模式):

3.2 场景2、低并发场景+慢请求测试

Sysbench并发线程数30测试结果以下图所示(上图为adaptive模式,下图为Synchronousm线程模式):

Sysbench并发线程数500测试结果以下图所示(上图为adaptive模式,下图为Synchronousm线程模式):

Sysbench并发线程数1000测试结果以下图所示(上图为adaptive模式,下图为Synchronousm线程模式):

3.3 场景3、高并发场景+快请求测试

Sysbench并发线程数5000测试结果以下图所示(上图为adaptive模式,下图为Synchronousm线程模式):

Sysbench并发线程数10000测试结果以下图所示(上图为adaptive模式,下图为Synchronousm线程模式):

测试中发现30000并发的时候synchronousm模式实际成功的链接数为24000,以下图所示:

为了测试相同并发数的真实数据对比,所以把adaptive模式的测试并发线程数调整为24000测试,同时提早把adaptive作以下最低线程数调整:

db.adminCommand( { setParameter: 1, adaptiveServiceExecutorReservedThreads: 120} )

两种测试数据结果以下(左图为adaptive模式,右图为Synchronousm线程模式):

3.4 场景4、高并发场景+慢请求测试

Sysbench并发线程数5000测试结果以下图所示(上图为adaptive模式,下图为Synchronousm线程模式):

Sysbench并发线程数10000测试结果以下图所示(上图为adaptive模式,下图为Synchronousm线程模式):


Sysbench并发线程数20000测试结果以下图所示(上图为adaptive模式,下图为Synchronousm线程模式):

3.5 测试总结

上面的测试数据,汇总以下表:

测试场景 线程模式 测试结果
70线程+快请求 Synchronous 总tps(包含异常请求):19.8W/s,错误请求总数:0,平均时延:0.35ms 95百分位时延:0.57ms,最大时延:51ms
Adaptive 总tps(包含异常请求):18.1W/s,错误请求总数:0,平均时延:0.38ms 95百分位时延:0.6ms,最大时延:41ms
500线程+快请求 Synchronous 总tps(包含异常请求):19.5W/s,错误请求总数:0,平均时延:2.53ms 95百分位时延:5.39ms,最大时延:4033ms
Adaptive 总tps(包含异常请求):18.2W/s,错误请求总数:0,平均时延:2.7ms 95百分位时延:3.77ms,最大时延:1049ms
1000线程+快请求 Synchronous 总tps(包含异常请求):18.4W/s,错误请求总数:4448/s,有效请求tps:17.9W/s,平均时延:5.41ms , 95百分位时延:20.58ms,最大时延:16595ms
Adaptive 总tps(包含异常请求):18.8W/s,错误请求总数:5000/s,有效请求tps:18.3W/s, 平均时延:5.28ms , 95百分位时延:17.6ms,最大时延:4087ms
5000线程+快请求 Synchronous 总tps(包含异常请求):18.2W/s,错误请求总数:7000/s,有效请求tps:17.5W/s,平均时延:27.3ms , 95百分位时延:44.1ms,最大时延:5043ms
Adaptive 总tps(包含异常请求):18.2W/s,错误请求总数:37000/s,有效请求tps:14.5W/s,平均时延:27.4ms , 95百分位时延:108ms,最大时延:22226ms
30000线程+快请求 Synchronous 总tps(包含异常请求):21W/s,错误请求总数:140000/s,有效请求tps:6W/s,平均时延:139ms ,95百分位时延:805ms,最大时延:53775ms
Adaptive 总tps(包含异常请求):10W/s,错误请求总数:80/s,有效请求tps:10W/s,平均时延:195ms, 95百分位时延:985ms,最大时延:17030ms
30线程+慢请求 Synchronous 总tps(包含异常请求):850/s,错误请求总数:0,平均时延:35ms 95百分位时延:45ms,最大时延:92ms
Adaptive 总tps(包含异常请求):674/s,错误请求总数:0,平均时延:44ms 95百分位时延:52ms,最大时延:132ms
500线程+慢请求 Synchronous 总tps(包含异常请求):765/s,错误请求总数:0,平均时延:652ms 95百分位时延:853ms,最大时延:2334ms
Adaptive 总tps(包含异常请求):783/s,错误请求总数:0,平均时延:637ms 95百分位时延:696ms,最大时延:1847ms
1000线程+慢请求 Synchronous 总tps(包含异常请求):2840/s,错误请求总数:2140/s,有效请求tps:700/s,平均时延:351ms 95百分位时延:1602ms,最大时延:6977ms
Adaptive 总tps(包含异常请求):3604/s,错误请求总数:2839/s,有效请求tps:800/s, 平均时延:277ms 95百分位时延:1335ms,最大时延:6615ms
5000线程+慢请求 Synchronous 总tps(包含异常请求):4535/s,错误请求总数:4000/s,有效请求tps:500/s,平均时延:1092ms 95百分位时延:8878ms,最大时延:25279ms
Adaptive 总tps(包含异常请求):4952/s,错误请求总数:4236/s,有效请求tps:700/s,平均时延:998ms 95百分位时延:7025ms,最大时延:16923ms
10000线程+慢请求 Synchronous 总tps(包含异常请求):4720/s,错误请求总数:4240/s,有效请求tps:500/s,平均时延:2075ms 95百分位时延:19539ms,最大时延:63247ms
Adaptive 总tps(包含异常请求):8890/s,错误请求总数:8230/s,有效请求tps:650/s,平均时延:1101ms 95百分位时延:14226ms,最大时延:40895ms
20000线程+慢请求 Synchronous 总tps(包含异常请求):7950/s,错误请求总数:7500/s,有效请求tps:450/s,平均时延:2413ms 95百分位时延:17812ms,最大时延:142752ms
Adaptive 总tps(包含异常请求):8800/s,错误请求总数:8130/s,有效请求tps:700/s,平均时延:2173ms 95百分位时延:27675ms,最大时延:57886ms

3.6 不一样线程模型总结

根据测试数据及其前面理论章节的分析,能够得出不一样业务场景结论:

  1. 低并发场景(并发数小于1000),Synchronous线程模型性能更好。
  2. 高并发场景(并发数大于5000),adaptive动态线程模型性能更优。
  3. adaptive动态线程模型,95分位时延和最大时延总体比Synchronous线程模型更优。
  4. 并发越高,adaptive相比Synchronous性能更好。
  5. 并发越高,Synchronous线程模型错误率相对更高。
  6. 空闲连接越多,Synchronous线程模型性能越差。(因为时间问题,该场景将来得及测试,这是官方的数据总结)
  7. 此外,短连接场景(例如PHP相关业务),adaptive模型性能会更优,由于该模型不会有连接关闭引发的线程销毁的开销。

为何并发越高,adaptive动态线程模型性能比Synchronous会更好,而并发低的时候反而更差,缘由以下:

  1. Synchronous模型,一个连接一个线程,并发越高,连接数就会越多,系统负载、内存消耗等就会更高。
  2. 低并发场景下,连接数很少,Synchronous模式线程数也很少,系统CPU调度几乎不会受到影响,负载也影响不大。而在adaptive场景下,因为asio库在设计的时候,任务放入全局队列op_queue_中,工做线程每次获取任务运行,都会有锁竞争,所以在低并发场景下性能不及adaptive模式。

3.7 adaptive动态线程模式在线调优实践总结

前面3.6.2章节讲了adaptive线程模型的工做原理,其中有8个参数供咱们对线程池运行状态进行调优。大致总结以下:

参数名 做用
adaptiveServiceExecutorReservedThreads 若是业务场景是针对相似整点推送、电商按期抢购等超大流量冲击的场景,能够适当的调高该值,避免冲击瞬间线程池不够用引发的任务排队、瞬间建立大量线程、时延过大的状况
adaptiveServiceExecutorRunTimeMillis 不建议调整
adaptiveServiceExecutorRunTimeJitterMillis 不建议调整
adaptiveServiceExecutorStuckThreadTimeoutMillis 能够适当调小该值,减小control控制线程休眠时间,从而能够更快的检测到线程池中工做线程数是否够用
adaptiveServiceExecutorMaxQueueLatencyMicros 不建议调整
adaptiveServiceExecutorIdlePctThreshold 若是流量是波浪形形式,例如上一秒tps=10万/S,下一秒降为几十,甚至跌0的状况,能够考虑调小该值,避免流量瞬间降低引发的线程瞬间批量消耗及流量上升后的大量线程建立
adaptiveServiceExecutorRecursionLimit 不建议调整

4. Asio网络库全局队列锁优化,性能进一步提高

前面的分析能够看出adaptive动态线程模型,为了获取全局任务队列op_queue_上的任务,须要进行全局锁竞争,这其实是整个线程池从队列获取任务运行最大的一个瓶颈。

优化思路: 咱们能够经过优化队列和锁来提高总体性能,当前的队列只有一个,咱们能够把单个队列调整为多个队列,每一个队列一把锁,任务入队的时候散列到多个队列,经过该优化,锁竞争及排队将会获得极大的改善。

优化前队列架构:

优化后队列架构:

如上图,把一个全局队列拆分为多个队列,任务入队的时候按照hash散列到各自的队列,工做线程获取获取任务的时候,同理经过hash的方式去对应的队列获取任务,经过这种方式减小锁竞争,同时提高总体性能。

5. 网络传输模块源码详细注释

鉴于篇幅,transport模块的详细源码实现过程将在《mongodb内核源码实现及调优系列》相关章节详细分析。

网络传输各个子模块及Asio库源码详细注释详见:

https://github.com/y123456yz/...

本文mongodb对应的sysbench代码目录(该工具来自Percona,本文只是简单作了改动):

https://github.com/y123456yz/...

Sysbench-mongodb对应的lua脚本目录:

https://github.com/y123456yz/...

最后

欢迎加入OPPO互联网数据库团队,一块儿参与公司千万级峰值tps/万亿级数据量文档数据库研发工做,想加入咱们,请联系邮箱:yangyazhou#oppo.com