本文来自OPPO互联网基础技术团队,转载请注名做者。同时欢迎关注咱们的公众号:OPPO_tech,与你分享OPPO前沿互联网技术及活动。
开源mongodb代码规模数百万行,本篇文章内容主要分析mongodb网络传输模块内部实现及其性能调优方法,学习网络IO处理流程,体验不一样工做线程模型性能极致设计原理。另一个目的就是引导你们快速进行百万级别规模源码阅读,作到不一样大工程源码”触类旁通”快速阅读的目的。react
此外,mognodb网络工做线程模型设计很是好,不只很是值得数据库相关研发人员学习,中间件、分布式、高并发、服务端等相关研发人员也能够借鉴,极力推荐你们学习。linux
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代码,咱们在走读代码前,建议遵循以下准则:算法
首先,咱们须要熟悉mongodb的基本功能,明白mongodb是作什么用的,用在什么地方,这样才能体现mongodb的真正价值。此外,咱们须要提早搭建一个mongodb集群玩一玩,这样也能够进一步促使咱们了解mongodb内部的一些经常使用基本功能。千万不要急于求成,若是连mongodb是作什么的都不知道,或者连mongodb的运维操做方法都没玩过,直接读取代码会很是不适合,没有目的的走读代码不利于分析整个代码,同时阅读代码过程会很是痛苦。mongodb
熟悉了mongodb的基本功能,并搭建集群简单体验后,咱们就能够从github下载源码,本身编译源码生成二进制文件,编译文档存放于docs/building.md 代码目录中,源码编译步骤以下:shell
在正在编译代码并运行的过程当中,发现如下两个问题:数据库
1)编译出的二进制文件占用空间很大,以下图所示:segmentfault
从上图能够看出,经过strip处理工具处理后,二进制文件大小已经和官方二进制包大小同样了。
2)在一些低版本操做系统运行的时候出错,找不到对应stdlib库,以下图所示:
如上图所示,当编译出的二进制文件拷贝到线上运行后,发现没法运行,提示libstdc库找不到。缘由是咱们编译代码时候依赖的stdc库版本比其余操做系统上面的stdc库版本更高,形成了不兼容。
解决办法: 编译的时候编译脚本中带上-static-libstdc++,把stdc库经过静态库的方式进行编译,而不是经过动态库方式。
因为前期咱们对代码总体实现不熟悉,不知道各个接口的调用流程,这时候就能够经过加日志打印进行调试。Mongodb的日志模块设计的比较完善,从日志中能够很明确的看出由那个功能模块打印日志,同时日志模块有多种打印级别。
1)日志打印级别设置
启动参数中verbose设置日志打印级别,日志打印级别设置方法以下:
Mongod -f ./mongo.conf -vvvv
这里的v越多,代表日志打印级别设置的越低,也就会打印更多的日志。一个v表示只会输出LOG(1)日志,-vv表示LOG(1) LOG(2)都会写日志。
2)如何在.cpp文件中使用日志模块记录日志
若是须要在一个新的.cpp文件中使用日志模块打印日志,须要进行以下步骤操做:
例如: LogComponent::kExecutor表明executor模块相关的日志,参考log_component.cpp日志模块文件实现,对应到日志文件内容以下:
Gdb是linux系统环境下优秀的代码调试工具,支持设置断点、单步调试、打印变量信息、获取函数调用栈信息等功能。gdb工具能够绑定某个线程进行线程级调试,因为mongodb是多线程环境,所以在用gdb调试前,咱们须要肯定调试的线程号,mongod进程包含的线程号及其对应线程名查看方法以下:
注意: 在调试mongod工做线程处理流程的时候,不要选择adaptive动态线程池模式,由于线程可能由于流量低引发工做线程不饱和而被销毁,从而形成调试过程由于线程销毁而中断,synchronous线程模式是一个连接一个线程,只要咱们不关闭这个连接,线程就会一直存在,不会影响咱们理解mongodb服务层代码实现逻辑。 synchronous线程模式调试的时候能够经过mongo shell连接mongod服务端端口来模拟一个连接,所以调试过程相对比较可控。
在对工做线程调试的时候,发现gdb没法查找到mongod进程的符号表,没法进行各类gdb功能调试,以下图所示:
上述gdb没法attach到指定线程调试的缘由是没法加载二进制文件符号表,这是由于编译的时候没有加上-g选项引发,mongodb经过SConstruct脚原本进行scons编译,要启编译出新的二进制文件后,就能够gdb调试了,以下图所示,能够很方便的定位到某个函数以前的调用栈信息,并进行单步、打印变量信息等调试:
在进行代码阅读前还有很重要的一步就是熟悉代码目录及文件命名实现,mongodb服务层代码目录结构及文件命名都有很严格的规范。下面以truansport网络传输模块为例,transport模块的具体目录文件结构:
从上面的文件分布内容,能够清晰的看出,整个目录中的源码实现文件大致能够分为以下几个部分:
经过上面的拆分,整个大的transport模块实现就被拆分红了7个小模块,这7个小的子模块各自负责对应功能实现,同时各个模块相互衔接,总体实现网络传输处理过程的总体实现,下面的章节将就这些子模块进行简单功能说明。
前面5个步骤事后,咱们已经熟悉了mongodb编译调试以及transport模块的各个子模块的相关代码文件实现及大致子模块做用。至此,咱们能够开始走读代码了,mongos和mongod的代码入口分别在mongoSMain()和mongoDbMain(),从这两个入口就能够一步一步了解mongodb服务层代码的总体实现。
注意: 走读代码前期不要深刻各类细节实现,大致了解代码实现便可,先大致弄明白代码中各个模块功能由那些子模块实现,千万不要深究细节。
本章节主要给出了数百万级mongodb内核代码阅读的一些建议,整个过程能够总结为以下几点:
从1.5章节中,咱们把transport功能模块细化拆分红了网络传输数据压缩子模块、服务入口子模块、线程模型子模块、状态机处理子模块、session会话信息子模块、数据分发子模块、套接字处理和传输管理子模块,总共七个子模块。
实际上mongodb服务层代码的底层网络IO实现依赖asio库完成,所以transport功能模块应该是7+1个子模块构成,也就是服务层代码实现由8个子模块支持。
Asio是一个优秀网络库,依赖于boost库的部分实现,支持linux、windos、unix等多平台,mongodb基于asio库来实现网络IO及定时器处理。asio库因为为了支持多平台,在代码实现中用了不少C++的模板,同时用了不少C++的新语法特性,所以总体代码可读性相比mongodb服务层代码差不少。
服务端网络IO异步处理流程大致以下:
服务端网络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有序而且高效的运行。
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 () |
总结:
上一节的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 |
从前面的分析能够看出,一个任务对应一个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_具体功能包含以下三种以下:
针对上面的三个网络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内核源码实现及调优系列》相关章节详细分析。
网络传输数据压缩子模块主要用于减小网络带宽占用,经过CPU来换取IO消耗,也就是以更多CPU消耗来减小网络IO压力。
鉴于篇幅,该模块的详细源码实现过程将在《mongodb内核源码实现及调优系列》相关章节分享。
transport_layer套接字处理及传输层管理子模块功能主要以下:
鉴于篇幅,该模块的详细源码实现过程将在《mongodb内核源码实现及调优系列》相关章节详细分析。
Session会话模块功能主要以下:
鉴于篇幅,该模块的详细源码实现过程将在《mongodb内核源码实现及调优系列》相关章节详细分析。
Ticket数据分发子模块主要功能以下:
鉴于篇幅,该模块的详细源码实现过程将在《mongodb内核源码实现及调优系列》相关章节详细分析。
service_state_machine状态机处理模块主要功能以下:
鉴于篇幅,该模块的详细源码实现过程将在《mongodb内核源码实现及调优系列》相关章节详细分析。
service_entry_point服务入口点子模块主要负责以下功能:
鉴于篇幅,该模块的详细源码实现过程将在《mongodb内核源码实现及调优系列》相关章节详细分析。
线程模型设计在数据库性能指标中起着很是重要的做用,所以本文将重点分析mongodb服务层线程模型设计,体验mongodb如何经过优秀的工做线程模型来达到多种业务场景下的性能极致表现。
service_executor线程子模块,在代码实现中,把线程模型分为两种:synchronous线程模式和adaptive线程模型。Mongodb启动的时候经过配置参数net.serviceExecutor来肯定采用那种线程模式运行mongo实例,配置方式以下:
net: //同步线程模式配置
serviceExecutor: synchronous
或者 //动态线程池模式配置
net:
serviceExecutor: 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表示当前进程的工做线程数。
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 | 线程池中全部线程从建立到被用来执行第一个任务的等待时间 |
上表中各个字段的都有各自的意义,咱们须要注意这些参数的如下状况:
上面三个大类中的整体反映趋势都是同样的,任何一个差值越大就说明越空闲。
在后续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内核源码实现及调优系列》相关章节详细分析。
前面对线程模型进行了分析,下面针对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
测试硬件资源,容器一台,配置以下:
Sysbench并发线程数70测试结果以下图所示(上图为adaptive模式,下图为Synchronousm线程模式):
Sysbench并发线程数500测试结果以下图所示(上图为adaptive模式,下图为Synchronousm线程模式):
Sysbench并发线程数1000测试结果以下图所示(上图为adaptive模式,下图为Synchronousm线程模式):
Sysbench并发线程数30测试结果以下图所示(上图为adaptive模式,下图为Synchronousm线程模式):
Sysbench并发线程数500测试结果以下图所示(上图为adaptive模式,下图为Synchronousm线程模式):
Sysbench并发线程数1000测试结果以下图所示(上图为adaptive模式,下图为Synchronousm线程模式):
Sysbench并发线程数5000测试结果以下图所示(上图为adaptive模式,下图为Synchronousm线程模式):
Sysbench并发线程数10000测试结果以下图所示(上图为adaptive模式,下图为Synchronousm线程模式):
测试中发现30000并发的时候synchronousm模式实际成功的链接数为24000,以下图所示:
为了测试相同并发数的真实数据对比,所以把adaptive模式的测试并发线程数调整为24000测试,同时提早把adaptive作以下最低线程数调整:
db.adminCommand( { setParameter: 1, adaptiveServiceExecutorReservedThreads: 120} )
两种测试数据结果以下(左图为adaptive模式,右图为Synchronousm线程模式):
Sysbench并发线程数5000测试结果以下图所示(上图为adaptive模式,下图为Synchronousm线程模式):
Sysbench并发线程数10000测试结果以下图所示(上图为adaptive模式,下图为Synchronousm线程模式):
Sysbench并发线程数20000测试结果以下图所示(上图为adaptive模式,下图为Synchronousm线程模式):
上面的测试数据,汇总以下表:
测试场景 | 线程模式 | 测试结果 |
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 | |
根据测试数据及其前面理论章节的分析,能够得出不一样业务场景结论:
为何并发越高,adaptive动态线程模型性能比Synchronous会更好,而并发低的时候反而更差,缘由以下:
前面3.6.2章节讲了adaptive线程模型的工做原理,其中有8个参数供咱们对线程池运行状态进行调优。大致总结以下:
参数名 | 做用 |
---|---|
adaptiveServiceExecutorReservedThreads | 若是业务场景是针对相似整点推送、电商按期抢购等超大流量冲击的场景,能够适当的调高该值,避免冲击瞬间线程池不够用引发的任务排队、瞬间建立大量线程、时延过大的状况 |
adaptiveServiceExecutorRunTimeMillis | 不建议调整 |
adaptiveServiceExecutorRunTimeJitterMillis | 不建议调整 |
adaptiveServiceExecutorStuckThreadTimeoutMillis | 能够适当调小该值,减小control控制线程休眠时间,从而能够更快的检测到线程池中工做线程数是否够用 |
adaptiveServiceExecutorMaxQueueLatencyMicros | 不建议调整 |
adaptiveServiceExecutorIdlePctThreshold | 若是流量是波浪形形式,例如上一秒tps=10万/S,下一秒降为几十,甚至跌0的状况,能够考虑调小该值,避免流量瞬间降低引发的线程瞬间批量消耗及流量上升后的大量线程建立 |
adaptiveServiceExecutorRecursionLimit | 不建议调整 |
前面的分析能够看出adaptive动态线程模型,为了获取全局任务队列op_queue_上的任务,须要进行全局锁竞争,这其实是整个线程池从队列获取任务运行最大的一个瓶颈。
优化思路: 咱们能够经过优化队列和锁来提高总体性能,当前的队列只有一个,咱们能够把单个队列调整为多个队列,每一个队列一把锁,任务入队的时候散列到多个队列,经过该优化,锁竞争及排队将会获得极大的改善。
优化前队列架构:
优化后队列架构:
如上图,把一个全局队列拆分为多个队列,任务入队的时候按照hash散列到各自的队列,工做线程获取获取任务的时候,同理经过hash的方式去对应的队列获取任务,经过这种方式减小锁竞争,同时提高总体性能。
鉴于篇幅,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