开源mongodb代码规模数百万行,本篇文章内容紧张剖析mongodb网络传输模块内部实现及其性能调优方法,学习网络IO处理流程,体验不同事情线程模型性能极致设计事理。其余一个目的便是勾引大家快速进行百万级别规模源码阅读,做到不同大工程源码”举一反三”快速阅读的目的。
此外,mognodb网络事情线程模型设计非常好,不仅非常值得数据库干系研发职员学习,中间件、分布式、高并发、做事端等干系研发职员也可以借鉴,极力推举大家学习。
Mongodb内核源码由第三方库third_party和mongodb做事层源码组成,个中mongodb做事层代码在不同模块实现中依赖不同的third_party库,第三方库是mongodb做事层代码实现的根本(例如:网络底层IO实现依赖asio-master库, 底层存储依赖wiredtiger存储引擎库),个中第三方库也会依赖部分其他库(例如:wiredtiger库依赖snappy算法库,asio-master依赖boost库)。

虽然Mongodb内核源码数百万行,工程量巨大,但是mongodb做事层代码实现层次非常清晰,代码目录构造、类命名、函数命名、文件名命名都非常一览无余,充分表示了10gen团队的专业精神。
解释:mongodb内核除第三方库third_party外的代码,这里统称为mongodb做事层代码。
本文以mongodb做事层transport实现为例来解释如何快速阅读全体mongodb代码,我们在走读代码前,建议遵照如下准则:
1.1 熟习mongodb基本功能和利用方法首先,我们须要熟习mongodb的基本功能,明白mongodb是做什么用的,用在什么地方,这样才能表示mongodb的真正代价。此外,我们须要提前搭建一个mongodb集群玩一玩,这样也可以进一步匆匆使我们理解mongodb内部的一些常用基本功能。千万不要急于求成,如果连mongodb是做什么的都不知道,或者连mongodb的运维操作方法都没玩过,直接读取代码会非常不适宜,没有目的的走读代码不利于剖析全体代码,同时阅读代码过程会非常痛楚。
1.2 下载代码编译源码熟习了mongodb的基本功能,并搭建集群大略体验后,我们就可以从github下载源码,自己编译源码天生二进制文件,编译文档存放于docs/building.md 代码目录中,源码编译步骤如下:
下载对应releases中对应版本的源码进入对付目录,参考docs/building.md文件内容进行干系依赖工具安装实行buildscripts/scons.py编译出对应二进制文件,也可以直接scons mongod mongos这样编译。编译成功后的生产可实行文件存放于./build/opt/mongo/目录在正在编译代码并运行的过程中,创造以下两个问题:
1)编译出的二进制文件占用空间很大,如下图所示:
从上图可以看出,通过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文件中利用日志模块打印日志,须要进行如下步骤操作:
添加宏定义 #define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kExecutor利用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模块的详细目录文件构造:
从上面的文件分布内容,可以清晰的看出,全体目录中的源码实现文件大体可以分为如下几个部分:
message_compressor_网络传输数据压缩子模块service_entry_point做事入口点子模块service_executor做事运行子模块,即线程模型子模块service_state_machine做事状态机处理子模块Session回话信息子模块Ticket数据分发子模块transport_layer套接字处理及传输层模式管理子模块通过上面的拆分,全体大的transport模块实现就被拆分成了7个小模块,这7个小的子模块各自大责对应功能实现,同时各个模块相互衔接,整体实现网络传输处理过程的整体实现,下面的章节姑息这些子模块进行大略功能解释。
1.6 从main入口开始大体走读代码前面5个步骤过后,我们已经熟习了mongodb编译调试以及transport模块的各个子模块的干系代码文件实现及大体子模块浸染。至此,我们可以开始走读代码了,mongos和mongod的代码入口分别在mongoSMain()和mongoDbMain(),从这两个入口就可以一步一步理解mongodb做事层代码的整体实现。
把稳: 走读代码前期不要深入各种细节实现,大体理解代码实现即可,先大体弄明白代码中各个模块功能由那些子模块实现,千万不要穷究细节。
1.7 总结本章节紧张给出了数百万级mongodb内核代码阅读的一些建议,全体过程可以总结为如下几点:
提前理解mongodb的浸染及事情事理。自己搭建集群提前学习下mongodb集群的常用运维操作,可以进一步帮助理解mongodb的功能特性,提升后期代码阅读的效率。自己下载源码编译二进制可实行文件,同时学会利用日志模块,通过加日志打印的办法逐步开始调试。学习利用gdb代码调试工具调试线程的运行流程,这样可以更进一步的匆匆使快速学习代码处理流程,特殊是一些繁芜逻辑,可以大大提升走读代码的效率。正式走读代码前,提前理解各个模块的代码目录构造,把一个大模块拆分成各个小模块,先大体浏览各个模块的代码实现。前期走读代码千万不要深入细节,捋清楚各个模块的大体功能浸染后再开始一步一步的深入细节,理解深层次的内部实现。从main()入口逐步开始走读代码,结合log日志打印和gdb调试。跳过整体流程中不熟习的模块代码,只走读本次想弄明白的模块代码实现。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异步处理流程大体如下:
调用socket()创建一个套接字,获取一个socket描述符。调用bind()绑定套接字,同时通过listen()来监听客户端链接,注册该socket描述符到epoll事宜集列表,等待accept对应的新连接读事宜到来。通过epoll_wait获取到accept对应的读事宜信息,然后调用accept()来接管客户的连接,并获取一个新的链接描述符new_fd。注册新的new_fd到epoll事宜集列表,当该new_fd描述符上有读事宜到来,于是通过epoll_wait获取该事宜,开始该fd上的数据读取。读取数据完毕后,开始内部处理,处理完后发送对应数据到客户端。如果一次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 ()
总结:
从上表的剖析可以看出,和mongodb直接干系的几个接口终极都是调用schedule类的干系接口,全体实现过程参考下一节scheduler调度实现模块。上表中的几个接口按照功能不同,可以分为入队型接口(poll、dispatch)和出队型接口(run_for、run、run_one_for)。按照和io_context的关联性不同,可以分为accept干系io(_acceptorIOContext)处理的接口(run、stop)和新链接fd对应Io(_workerIOContext)数据分发干系处理及回调处理的接口(run_for、run_one_for、poll、dispatch)。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_oneevent wakeup_event_;唤醒等待锁得线程实际event由旗子暗记量封装task_operation task_operation_;分外的operation在链表中没进行一次epoll获取到IO任务加入全局行列步队后,都会紧接着添加一个分外operationreactor 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_标识为falsescheduler::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_详细功能包含如下三种如下:
通过epoll事宜集处理底层accept获取新连接fd。fd上的数据异步吸收fd上的数据异步发送针对上面的三个网络IO处理功能,ASIO在实现的时候,分别通过三个不同的类(reactive_socket_accept_op_base、reactive_socket_recv_op_base、reactive_socket_send_op_base)实现,这三个类都继续父类reactor_op。
这三个类的功能总结如下表所示:
类名功能解释reactive_socket_accept_op_base1. Accept()系统调用获取新fd 2. 获取到一个新fd后的mongodb层逻辑回调处理Accept()系统调用由perform_func()函数处理 获取到新链接后的逻辑回调由complete_func实行reactive_socket_recv_op_base1. 读取一个完全mongodb报文读取 2. 读取完全报文后的mongodb做事层逻辑回调处理从一个链接上读取一个完全mongodb报文读取由perform_func()函数处理 读取完全报文后的mongodb做事层逻辑回调处情由complete_func实行reactive_socket_send_op_base1. 发送一个完全的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套接字处理及传输层管理子模块功能紧张如下:
套接字干系初始化处理结合asio库实现异步accept处理不同线程模型管理及初始化鉴于篇幅,该模块的详细源码实现过程将在《mongodb内核源码实现及调优系列》干系章节详细剖析。
2.4 session会话子模块Session会话模块功能紧张如下:
卖力记录HostAndPort、新连接fd信息通过和底层asio库的直接互动,实现数据的同步或者异步收发。鉴于篇幅,该模块的详细源码实现过程将在《mongodb内核源码实现及调优系列》干系章节详细剖析。
2.5 Ticket数据分发子模块Ticket数据分发子模块紧张功能如下:
调用session子模块进行底层asio库处理拆分数据吸收和数据发送到两个类,分别实现。完全mongodb报文读取吸收或者发送mongodb报文后的回调处理鉴于篇幅,该模块的详细源码实现过程将在《mongodb内核源码实现及调优系列》干系章节详细剖析。
2.6 service_state_machine状态机调度子模块service_state_machine状态机处理模块紧张功能如下:
Mongodb网络数据处理状态转换合营状态转换逻辑把一次mongodb要求拆分为二个大的状态任务: 吸收一个完全长度mongodb报文、吸收到一个完全报文后的后续所有处理(含报文解析、认证、引擎层处理、应答客户端等)。合营事情线程模型子模块,把步骤2的两个任务按照指定的状态转换关系进行调度运行。鉴于篇幅,该模块的详细源码实现过程将在《mongodb内核源码实现及调优系列》干系章节详细剖析。
2.7 service_entry_point做事入口点子模块service_entry_point做事入口点子模块紧张卖力如下功能:
连接数掌握Session会话管理吸收到一个完全报文后的回调处理(含报文解析、认证、引擎层处理等)鉴于篇幅,该模块的详细源码实现过程将在《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报文T1Task2吸收到报文后的后续所有处理(含报文解析、认证、引擎层处理、发送数据给客户端等)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可以查看,如下图所示:
上图的几个信息功能可以分类为三大类,解释如下:
大类类名字段名功能无executorAdaptive,解释是动态线程池模式线程统计threadsInUse当前正在运行task任务的线程数threadsRunning当前运行的线程数threadsPending当前创建起来,但是还没有实行过task任务的线程数行列步队统计totalExecuted线程池运行成功的任务总数tasksQueued入队到全局行列步队的任务数deferredTasksQueued等待吸收网络IO数据来读取一个完全mongodb报文的任务数韶光统计totalTimeRunningMicros所有事情线程运行总韶光(含等待网络IO的韶光T1 + 读一个mongodb报文任务的韶光T2 + 一个要求后续处理的韶光T3)totalTimeExecutingMicros也便是T2+T3,mongodb内部相应一个完全mongodb耗费的韶光totalTimeQueuedMicros线程池中所有线程从创建到被用来实行第一个任务的等待韶光
上表中各个字段的都有各自的意义,我们须要把稳这些参数的以下情形:
threadsRunning - threadsInUse的差值越大解释线程池中线程比较空闲,差值越小解释压力越大threadsPending越大,表示线程池越空闲tasksQueued - totalExecuted的差值越大解释任务行列步队上等待实行的任务越多,解释任务积压征象越明显deferredTasksQueued越大解释事情线程比较空闲,在等待客户端数据到来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
测试硬件资源,容器一台,配置如下:
CPU=32内存=64G3.1 场景一、低并发场景+将近求测试Sysbench并发线程数70测试结果如下图所示(上图为adaptive模式,下图为Synchronousm线程模式):
Sysbench并发线程数500测试结果如下图所示(上图为adaptive模式,下图为Synchronousm线程模式):
Sysbench并发线程数1000测试结果如下图所示(上图为adaptive模式,下图为Synchronousm线程模式):
3.2 场景二、低并发场景+慢要求测试
Sysbench并发线程数30测试结果如下图所示(上图为adaptive模式,下图为Synchronousm线程模式):
Sysbench并发线程数500测试结果如下图所示(上图为adaptive模式,下图为Synchronousm线程模式):
Sysbench并发线程数1000测试结果如下图所示(上图为adaptive模式,下图为Synchronousm线程模式):
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 场景四、高并发场景+慢要求测试
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,最大时延:51msAdaptive总tps(包含非常要求):18.1W/s,缺点要求总数:0,均匀时延:0.38ms 95百分位时延:0.6ms,最大时延:41ms500线程+将近求Synchronous总tps(包含非常要求):19.5W/s,缺点要求总数:0,均匀时延:2.53ms 95百分位时延:5.39ms,最大时延:4033msAdaptive总tps(包含非常要求):18.2W/s,缺点要求总数:0,均匀时延:2.7ms 95百分位时延:3.77ms,最大时延:1049ms1000线程+将近求Synchronous总tps(包含非常要求):18.4W/s,缺点要求总数:4448/s,有效要求tps:17.9W/s,均匀时延:5.41ms , 95百分位时延:20.58ms,最大时延:16595msAdaptive总tps(包含非常要求):18.8W/s,缺点要求总数:5000/s,有效要求tps:18.3W/s, 均匀时延:5.28ms , 95百分位时延:17.6ms,最大时延:4087ms5000线程+将近求Synchronous总tps(包含非常要求):18.2W/s,缺点要求总数:7000/s,有效要求tps:17.5W/s,均匀时延:27.3ms , 95百分位时延:44.1ms,最大时延:5043msAdaptive总tps(包含非常要求):18.2W/s,缺点要求总数:37000/s,有效要求tps:14.5W/s,均匀时延:27.4ms , 95百分位时延:108ms,最大时延:22226ms30000线程+将近求Synchronous总tps(包含非常要求):21W/s,缺点要求总数:140000/s,有效要求tps:6W/s,均匀时延:139ms ,95百分位时延:805ms,最大时延:53775msAdaptive总tps(包含非常要求):10W/s,缺点要求总数:80/s,有效要求tps:10W/s,均匀时延:195ms, 95百分位时延:985ms,最大时延:17030ms30线程+慢要求Synchronous总tps(包含非常要求):850/s,缺点要求总数:0,均匀时延:35ms 95百分位时延:45ms,最大时延:92msAdaptive总tps(包含非常要求):674/s,缺点要求总数:0,均匀时延:44ms 95百分位时延:52ms,最大时延:132ms500线程+慢要求Synchronous总tps(包含非常要求):765/s,缺点要求总数:0,均匀时延:652ms 95百分位时延:853ms,最大时延:2334msAdaptive总tps(包含非常要求):783/s,缺点要求总数:0,均匀时延:637ms 95百分位时延:696ms,最大时延:1847ms1000线程+慢要求Synchronous总tps(包含非常要求):2840/s,缺点要求总数:2140/s,有效要求tps:700/s,均匀时延:351ms 95百分位时延:1602ms,最大时延:6977msAdaptive总tps(包含非常要求):3604/s,缺点要求总数:2839/s,有效要求tps:800/s, 均匀时延:277ms 95百分位时延:1335ms,最大时延:6615ms5000线程+慢要求Synchronous总tps(包含非常要求):4535/s,缺点要求总数:4000/s,有效要求tps:500/s,均匀时延:1092ms 95百分位时延:8878ms,最大时延:25279msAdaptive总tps(包含非常要求):4952/s,缺点要求总数:4236/s,有效要求tps:700/s,均匀时延:998ms 95百分位时延:7025ms,最大时延:16923ms10000线程+慢要求Synchronous总tps(包含非常要求):4720/s,缺点要求总数:4240/s,有效要求tps:500/s,均匀时延:2075ms 95百分位时延:19539ms,最大时延:63247msAdaptive总tps(包含非常要求):8890/s,缺点要求总数:8230/s,有效要求tps:650/s,均匀时延:1101ms 95百分位时延:14226ms,最大时延:40895ms20000线程+慢要求Synchronous总tps(包含非常要求):7950/s,缺点要求总数:7500/s,有效要求tps:450/s,均匀时延:2413ms 95百分位时延:17812ms,最大时延:142752msAdaptive总tps(包含非常要求):8800/s,缺点要求总数:8130/s,有效要求tps:700/s,均匀时延:2173ms 95百分位时延:27675ms,最大时延:57886ms
3.6 不同线程模型总结根据测试数据及其前面理论章节的剖析,可以得出不同业务场景结论:
低并发场景(并发数小于1000),Synchronous线程模型性能更好。高并发场景(并发数大于5000),adaptive动态线程模型性能更优。adaptive动态线程模型,95分位时延和最大时延整体比Synchronous线程模型更优。并发越高,adaptive比较Synchronous性能更好。并发越高,Synchronous线程模型缺点率相对更高。空闲链接越多,Synchronous线程模型性能越差。(由于韶光问题,该场景未来得及测试,这是官方的数据总结)此外,短链接场景(例如PHP干系业务),adaptive模型性能会更优,由于该模型不会有链接关闭引起的线程销毁的开销。为什么并发越高,adaptive动态线程模型性能比Synchronous会更好,而并发低的时候反而更差,缘故原由如下:
Synchronous模型,一个链接一个线程,并发越高,链接数就会越多,系统负载、内存花费等就会更高。低并发场景下,链接数不多,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