ITPub博客

首页 > 数据库 > NoSQL > mongodb内核源码实现、性能调优、最佳运维实践系列-网络传输层模块源码实现四

mongodb内核源码实现、性能调优、最佳运维实践系列-网络传输层模块源码实现四

原创 NoSQL 作者:y123456yzzyz 时间:2020-11-06 20:40:06 0 删除 编辑

transport_layer 网络传输层模块源码实现

关于作者

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

mongodb内核源码实现、性能调优、最佳运维实践系列》文章有前后逻辑关系,请阅读本篇文章前,提前阅读如下模块:

mongodb网络传输层模块源码实现一

mongodb网络传输层模块源码实现二

mongodb网络传输层模块源码实现三

1.  说明

本文分析网络传输层模块中的最后一个子模块:service_executor 服务运行子模块,即线程模型子模块。在阅读该文章前,请提前阅读下 <<Mongodb 网络传输处理源码实现及性能调优 - 体验内核性能极致设计 >> << transport_layer 网络传输层模块源码实现 >> << transport_layer 网络传输层模块源码实现 >> ,这样有助于快速理解本文分享的线程模型子模块。

  线程模型设计在数据库性能指标中起着非常重要的作用,因此本文将重点分析mongodb 服务层线程模型设计,体验 mongodb 如何通过优秀的工作线程模型来达到多种业务场景下的性能极致表现。该模块主要代码实现文件如下:

service_executor 线程模型子模块,在代码实现中,把线程模型分为两种: synchronous 线程模式和 adaptive 线程模型,这两种线程模型中用于任务调度运行的线程统称为 worker 工作线程。 Mongodb 启动的时候通过配置参数 net. serviceExecutor 来确定采用那种线程模式运行mongo 实例,配置方式如下:

1. //synchronous同步线程模式配置,一个链接已给线程  
2. net:     
3.   serviceExecutor: synchronous  
4.   
5. //动态线程池模式配置  
6. net:  
7.   serviceExecutor: adaptive

2. synchronous 同步线程模型 ( 一个链接已给线程 ) 设计原理及核心代码实现

Synchronous 同步线程模型也就是每接收到一个链接,就创建一个线程专门负责该链接对应所有的客户端请求,也就是该链接的所有访问至始至终由同一个线程负责处理。

2.1 核心代码实现原理

该线程模型核心代码实现由ServiceExecutorSynchronous 类负责,该类注意成员变量和重要接口如下:

1. //同步线程模型对应ServiceExecutorSynchronous类  
2. class ServiceExecutorSynchronous final : public ServiceExecutor {  
3. public:  
4.     //ServiceExecutorSynchronous初始化  
5.     explicit ServiceExecutorSynchronous(ServiceContext* ctx);  
6.     //获取系统CPU个数  
7.     Status start() override;  
8.     //shutdown处理  
9.     Status shutdown(Milliseconds timeout) override;  
10.     //线程管理及任务入队处理  
11.     Status schedule(Task task, ScheduleFlags flags) override;  
12.     //同步线程模型对应mode  
13.     Mode transportMode() const override {  
14.         return Mode::kSynchronous;  
15.     }  
16.     //获取该模型统计信息  
17.     void appendStats(BSONObjBuilder* bob) const override;  
18.   
19. private:  
20.     //私有线程队列  
21.     static thread_local std::deque<Task> _localWorkQueue;  
22.     //递归深度  
23.     static thread_local int _localRecursionDepth;  
24.     //空闲线程数,例如某个链接当前没有请求,则该线程阻塞在读操作上面等待数据读到来  
25.     static thread_local int64_t _localThreadIdleCounter;  
26.     //shutdown的时候设置为false,链接没关闭前一直为true  
27.     AtomicBool _stillRunning{false};    
28.     //当前conn线程数,参考ServiceExecutorSynchronous::schedul   
29.     AtomicWord<size_t> _numRunningWorkerThreads{0};  
30.     //cpu个数  
31.     size_t _numHardwareCores{0};   
32. };

ServiceExecutorSynchronous 类核心成员变量及其功能说明如下:

每个链接对应的线程都有三个私有成员,分别是:线程队列、递归深度、idle 频度,这三个线程私有成员的作用如下:

1)  _localWorkQueue :线程私有队列, task 任务入队及出队执行都是通过该队列完成

2)  _localRecursionDepth :任务递归深度控制,避免堆栈溢出

3)  _localThreadIdleCounter :当线程运行多少次任务后,需要短暂的休息一会儿,默认运行 0xf task 任务就调用 markThreadIdle() 一次

同步线程模型子模块最核心的代码实现如下:

1. //ServiceStateMachine::_scheduleNextWithGuard 启动新的conn线程  
2. Status ServiceExecutorSynchronous::schedule(Task task, ScheduleFlags flags) {  
3.     //如果_stillRunning为false,则直接返回  
4.     if (!_stillRunning.load()) {  
5.         return Status{ErrorCodes::ShutdownInProgress, "Executor is not running"};  
6.     }  
7.     //队列不为空,说明由任务需要运行,同步线程模型只有新连接第一次通过SSM进入该函数的时候为空  
8.     //其他情况都不为空  
9.     if (!_localWorkQueue.empty()) {  
10.         //kMayYieldBeforeSchedule标记当返回客户端应答成功后,开始接收下一个新请求,这时候会设置该标记  
11.         if (flags & ScheduleFlags::kMayYieldBeforeSchedule) {  
12.             //也就是如果该链接对应的线程如果连续处理了0xf个请求,则需要休息一会儿  
13.             if ((_localThreadIdleCounter++ & 0xf) == 0) {  
14.                 //短暂休息会儿后再处理该链接的下一个用户请求  
15.                 //实际上是调用TCMalloc MarkThreadTemporarilyIdle实现  
16.                 markThreadIdle();  
17.             }  
18.             //链接数即线程数超过了CPU个数,则每处理完一个请求,就yield一次      
19.             if (_numRunningWorkerThreads.loadRelaxed() > _numHardwareCores) {  
20.                 stdx::this_thread::yield();//线程本次不参与CPU调度,也就是放慢脚步  
21.             }  
22.         }  
23.         //带kMayRecurse标识,说明即将调度执行的是dealTask  
24.         //如果递归深度小于synchronousServiceExecutorRecursionLimit,则执行task  
25.         if ((flags & ScheduleFlags::kMayRecurse) &&    
26.             (_localRecursionDepth < synchronousServiceExecutorRecursionLimit.loadRelaxed())) {  
27.             ++_localRecursionDepth;  
28.             //递归深度没有超限,则直接执行task,不用入队  
29.             task();  
30.         } else {  
31.             //入队,等待  
32.             _localWorkQueue.emplace_back(std::move(task));   
33.         }  
34.         return Status::OK();  
35.     }  
36.     //创建conn线程,线程名conn-xx(实际上是从listener线程继承过来的,这时候的Listener线程是父线程,在  
37.     //ServiceStateMachine::start中已通过线程守护ThreadGuard改为conn-xx),执行对应的task  
38.     Status status = launchServiceWorkerThread([ this, task = std::move(task) ] {  
39.         //说明来了一个新链接,线程数自增  
40.         int ret = _numRunningWorkerThreads.addAndFetch(1);  
41.         //新链接到来的第一个任务实际上是readTask任务  
42.         _localWorkQueue.emplace_back(std::move(task));  
43.         while (!_localWorkQueue.empty() && _stillRunning.loadRelaxed()) {  
44.              //每次任务如果是通过线程私有队列获取运行,则恢复递归深度为初始值1
45.             _localRecursionDepth = 1;  
46.             //取出该线程拥有的私有队列上的第一个任务运行  
47.             _localWorkQueue.front()();   
48.            //该任务已经执行完毕,把该任务从队列移除          
49.             _localWorkQueue.pop_front();     
50.         }  
51.         //走到这里说明线程异常了或者需要退出,如链接关闭,需要消耗线程  
52.         ......  
53.     });  
54.     return status;  
55. }

从上面的代码可以看出,worker 工作线程通过 _localRecursionDepth 控制 task 任务的递归深度,当递归深度超过最大深度 synchronousServiceExecutorRecursionLimit 值,则把任务到 _localWorkQueue 队列,然后从队列获取task 任务执行。

此外,为了达到性能的极致发挥,在每次执行task 任务的时候做了如下细节设计,这些细节设计在高压力情况下,可以提升 5% 的性能提升:

1)  每运行oxf 次任务,就通过 markThreadIdle() 让线程 idle 休息一会儿

2)  如果线程数大于CPU 核数,则每执行一个任务前都让线程 yield() 一次

2.2 该模块函数接口总结大全

synchronous 同步线程模型所有接口及其功能说明如下表所示:

3.  Adaptive 动态线程模型设计原理及核心代码实现

adaptive 动态线程模型,会根据当前系统的访问负载动态的调整线程数,当线程 CPU 工作比较频繁的时候,控制线程增加工作线程数;当线程 CPU 比较空闲后,本线程就会自动销毁退出,总体 worker 工作线程数就会减少。

3.1 动态线程模型核心源码实现

动态线程模型核心代码实现由ServiceExecutorAdaptive 负责完成,该类核心成员变量及核心函数接口如下:

1. class ServiceExecutorAdaptive : public ServiceExecutor {  
2. public:  
3.     //初始化构造  
4.     explicit ServiceExecutorAdaptive(...);  
5.     explicit ServiceExecutorAdaptive(...);  
6.     ServiceExecutorAdaptive(...) = default;  
7.     ServiceExecutorAdaptive& operator=(ServiceExecutorAdaptive&&) = default;  
8.     virtual ~ServiceExecutorAdaptive();  
9.     //控制线程及worker线程初始化创建  
10.     Status start() final;  
11.     //shutdown处理  
12.     Status shutdown(Milliseconds timeout) final;  
13.     //任务调度运行  
14.     Status schedule(Task task, ScheduleFlags flags) final;  
15.     //adaptive动态线程模型对应Mode  
16.     Mode transportMode() const final {  
17.         return Mode::kAsynchronous;  
18.     }  
19.     //统计信息  
20.     void appendStats(BSONObjBuilder* bob) const final;  
21.     //获取runing状态  
22.     int threadsRunning() {  
23.         return _threadsRunning.load();  
24.     }  
25.     //新键一个worker线程  
26.     void _startWorkerThread();  
27.     //worker工作线程主循环while{}处理  
28.     void _workerThreadRoutine(int threadId, ThreadList::iterator it);  
29.     //control控制线程主循环,主要用于控制什么时候增加线程  
30.     void _controllerThreadRoutine();  
31.     //判断队列中的任务数和可用线程数大小,避免任务task饥饿  
32.     bool _isStarved() const;  
33.     //asio网络库io上下文  
34.     std::shared_ptr<asio::io_context> _ioContext; //早期ASIO中叫io_service   
35.     //TransportLayerManager::createWithConfig赋值调用  
36.     std::unique_ptr<Options> _config;  
37.     //线程列表及其对应的锁  
38.     mutable stdx::mutex _threadsMutex;  
39.     ThreadList _threads;  
40.     //控制线程  
41.     stdx::thread _controllerThread;  
42.   
43.     //TransportLayerManager::createWithConfig赋值调用  
44.     //时间嘀嗒处理  
45.     TickSource* const _tickSource;  
46.     //运行状态  
47.     AtomicWord<bool> _isRunning{false};  
48.     //kThreadsRunning代表已经执行过task的线程总数,也就是这些线程不是刚刚创建起来的  
49.     AtomicWord<int> _threadsRunning{0};  
50.     //代表当前刚创建或者正在启动的线程总数,也就是创建起来还没有执行task的线程数  
51.     AtomicWord<int> _threadsPending{0};  
52.     //当前正在执行task的线程  
53.     AtomicWord<int> _threadsInUse{0};  
54.     //当前入队还没执行的task数  
55.     AtomicWord<int> _tasksQueued{0};  
56.     //当前入队还没执行的deferredTask数  
57.     AtomicWord<int> _deferredTasksQueued{0};  
58.     //TransportLayerManager::createWithConfig赋值调用  
59.     //没什么实际作用  
60.     TickTimer _lastScheduleTimer;  
61.     //记录这个退出的线程生命期内执行任务的总时间  
62.     AtomicWord<TickSource::Tick> _pastThreadsSpentExecuting{0};  
63.     //记录这个退出的线程生命期内运行的总时间(包括等待IO及运行IO任务的时间)  
64.     AtomicWord<TickSource::Tick> _pastThreadsSpentRunning{0};  
65.     //完成线程级的统计  
66.     static thread_local ThreadState* _localThreadState;  
67.   
68.     //总的入队任务数  
69.     AtomicWord<int64_t> _totalQueued{0};  
70.     //总执行的任务数  
71.     AtomicWord<int64_t> _totalExecuted{0};  
72.     //从任务被调度入队,到真正被执行这段过程的时间,也就是等待被调度的时间  
73.     AtomicWord<TickSource::Tick> _totalSpentQueued{0};  
74.   
75.     //shutdown的时候等待线程消耗的条件变量  
76.     stdx::condition_variable _deathCondition;  
77.     //条件变量,如果发现工作线程压力大,为了避免task饥饿  
78.     //通知controler线程,通知见ServiceExecutorAdaptive::schedule,等待见_controllerThreadRoutine  
79.     stdx::condition_variable _scheduleCondition;  
80. };

ServiceExecutorAdaptive 类核心成员变量及其功能说明如下:

    从上面的成员变量列表看出,队列、线程这两个大类可以进一步细化为不同的小类,如下:

1)  线程: _threadsRunning threadsPending _threadsInUsed

2)  队列:_totalExecuted _tasksQueued deferredTasksQueued

从上面的 ServiceExecutorAdaptive 类中的核心接口函数代码实现可以归纳为如下三类:

1)  时间计数相关核心代码实现

2)  Worker 工作线程创建及任务调度相关核心接口代码实现

3)  controler 控制线程设计原理及核心代码实现

3.1.1 线程运行时间计算相关核心代码实现

线程运行时间计算核心算法如下:

1. //线程运行时间统计,包含两种类型时间统计  
2. enum class ThreadTimer   
3. {   
4.     //线程执行task任务的时间+等待数据的时间  
5.     Running,   
6.     //只包含线程执行task任务的时间  
7.     Executing   
8. };  
9.   
10. //线程私有统计信息,记录该线程运行时间,运行时间分为两种:  
11. //1. 执行task任务的时间 2. 如果没有客户端请求,线程就会等待,这就是线程等待时间  
12. struct ThreadState {  
13.     //构造初始化  
14.     ThreadState(TickSource* ts) : running(ts), executing(ts) {}  
15.     //线程一次循环处理的时间,包括IO等待和执行对应网络事件对应task的时间   
16.     CumulativeTickTimer running;  
17.     //线程一次循环处理中执行task任务的时间,也就是真正工作的时间  
18.     CumulativeTickTimer executing;  
19.     //递归深度  
20.     int recursionDepth = 0;  
21. };  
22. 
23. //获取指定which类型的工作线程相关运行时间,  
24. //例如Running代表线程总运行时间(等待数据+任务处理)   
25. //Executing只包含执行task任务的时间  
26. TickSource::Tick ServiceExecutorAdaptive::_getThreadTimerTotal(ThreadTimer which) const {  
27.     //获取一个时间嘀嗒tick  
28.     TickSource::Tick accumulator;  
29.     //先把已消耗的线程的数据统计出来  
30.     switch (which) {   
31.     //获取生命周期已经结束的线程执行任务的总时间(只包括执行任务的时间)  
32.     case ThreadTimer::Running:  
33.           accumulator = _pastThreadsSpentRunning.load();  
34.           break;  
35.      //获取生命周期已经结束的线程整个生命周期时间(包括空闲时间+执行任务时间)  
36.      case ThreadTimer::Executing:   
37.           accumulator = _pastThreadsSpentExecuting.load();  
38.           break;  
39.      }  
40.      //然后再把统计当前正在运行的worker线程的不同类型的统计时间统计出来  
41.      stdx::lock_guard<stdx::mutex> lk(_threadsMutex);  
42.      for (auto& thread : _threads) {   
43.         switch (which) {  
44.             //获取当前线程池中所有工作线程执行任务时间  
45.             case ThreadTimer::Running:  
46.                 accumulator += thread.running.totalTime();  
47.                 break;  
48.            //获取当前线程池中所有工作线程整个生命周期时间(包括空闲时间+执行任务时间)  
49.             case ThreadTimer::Executing:   
50.                 accumulator += thread.executing.totalTime();  
51.                 break;  
52.         }  
53.     }  
54.     //返回的时间计算包含了已销毁的线程和当前正在运行的线程的相关统计  
55.     return accumulator;  
56. }

Worker 工作线程启动后的时间可以包含两类: 1. 线程运行 task 任务的时间; 2. 线程等待客户端请求的时间。一个线程创建起来,如果没有客户端请求,则线程就会等待接收数据。如果有客户端请求,线程就会通过队列获取 task 任务运行。这两类时间分别代表线程 “空闲”。

线程总的“忙”状态时间 = 所有线程运行 task 任务的时间,包括已经销毁的线程。线程总的“空闲”时间 = 所有线程等待获取任务执行的时间,也包括已销毁的线程,线程空闲一般是没有客户端请求,或者客户端请求很少。 Worker 工作线程对应 while(){} 循环每循环一次都会进行线程私有运行时间 ThreadState 计数,总的时间统计就是以该线程私有统计信息为基准求和而来。

3.1.2 worker 工作线程创建、销毁及 task 任务处理

worker 工作线程在如下情况下创建或者销毁: 1. 线程池初始化; 2. controler 控制线程发现当前线程池中线程比较 ,则会动态创建新的工作线程;3. 工作线程在 while 体中每循环一次都会判断当前线程池是否很 ,如果很 则本线程直接销毁退出。

Worker 工作线程创建核心源码实现如下:

1. Status ServiceExecutorAdaptive::start() {  
2.     invariant(!_isRunning.load());  
3.     //running状态  
4.     _isRunning.store(true);  
5.     //控制线程初始化创建,线程回调ServiceExecutorAdaptive::_controllerThreadRoutine  
6.     _controllerThread = stdx::thread(&ServiceExecutorAdaptive::_controllerThreadRoutine, this);  
7.     //启动时候默认启用CPU核心数/2个worker线程  
8.     for (auto i = 0; i < _config->reservedThreads(); i++) {  
9.         //创建一个工作线程  
10.         _startWorkerThread();   
11.     }  
12.     return Status::OK();  
13. }

    worker 工作线程默认初始化为 CPU/2 个,初始工作线程数也可以通过指定的命令行参数来配置: adaptiveServiceExecutorReservedThreads 。此外, start() 接口默认也会创建一个 controler 控制线程。

Task 任务通过 SSM 状态机调用 ServiceExecutorAdaptive::schedule () 接口入队,该函数接口核心代码实现如下:

1. Status ServiceExecutorAdaptive::schedule(ServiceExecutorAdaptive::Task task, ScheduleFlags flags) {  
2.     //获取当前时间  
3.     auto scheduleTime = _tickSource->getTicks();  
4.     //kTasksQueued: 普通tak,也就是dealTask  
5.     //_deferredTasksQueued: deferred task,也就是readTask  
6.     //defered task和普通task分开记录   _totalQueued=_deferredTasksQueued+_tasksQueued  
7.     auto pendingCounterPtr = (flags & kDeferredTask) ? &_deferredTasksQueued : &_tasksQueued;  
8.     //相应队列  
9.     pendingCounterPtr->addAndFetch(1);     
10.     ......  
11.     //这里面的task()执行后-task()执行前的时间才是CPU真正工作的时间  
12.     auto wrappedTask = [ this, task = std::move(task), scheduleTime, pendingCounterPtr ] {  
13.         //worker线程回调会执行该wrappedTask,  
14.         pendingCounterPtr->subtractAndFetch(1);  
15.         auto start = _tickSource->getTicks();  
16.         //从任务被调度入队,到真正被执行这段过程的时间,也就是等待被调度的时间  
17.         //从任务被调度入队,到真正被执行这段过程的时间  
18.         _totalSpentQueued.addAndFetch(start - scheduleTime);   
19.         //recursionDepth=0说明开始进入调度处理,后续有可能是递归执行  
20.         if (_localThreadState->recursionDepth++ == 0) {  
21.             //记录wrappedTask被worker线程调度执行的起始时间  
22.             _localThreadState->executing.markRunning();  
23.             //当前正在执行wrappedTask的线程加1  
24.             _threadsInUse.addAndFetch(1);  
25.         }  
26.         //ServiceExecutorAdaptive::_workerThreadRoutine执行wrappedTask后会调用guard这里的func   
27.         const auto guard = MakeGuard([this, start] { //改函数在task()运行后执行  
28.             //每执行一个任务完成,则递归深度自减  
29.             if (--_localThreadState->recursionDepth == 0) {  
30.                  //wrappedTask任务被执行消耗的总时间     
31.                 //_localThreadState->executing.markStopped()代表任务该task执行的时间  
32.                 _localThreadState->executingCurRun += _localThreadState->executing.markStopped();  
33.                 //下面的task()执行完后,正在执行task的线程-1  
34.                  _threadsInUse.subtractAndFetch(1);  
35.             }  
36.             //总执行的任务数,task每执行一次增加一次  
37.             _totalExecuted.addAndFetch(1);  
38.         });  
39.         //运行任务
40.         task();  
41.     };  
42.     //kMayRecurse标识的任务,会进行递归调用   dealTask进入调度的时候调由该标识  
43.     if ((flags & kMayRecurse) && //递归调用,任务还是由本线程处理  
44.         //递归深度还没达到上限,则还是由本线程继续调度执行wrappedTask任务  
45.         (_localThreadState->recursionDepth + 1 < _config->recursionLimit())) {  
46.         //本线程立马直接执行wrappedTask任务,不用入队到boost-asio全局队列等待调度执行  
47.         //io_context::dispatch   io_context::dispatch   
48.         _ioContext->dispatch(std::move(wrappedTask));    
49.     } else { //入队   io_context::post  
50.         //task入队到schedule得全局队列,等待工作线程调度  
51.         _ioContext->post(std::move(wrappedTask));  
52.     }  
53.     //  
54.     _lastScheduleTimer.reset();  
55.     //总的入队任务数  
56.     _totalQueued.addAndFetch(1);   
57.     //kDeferredTask真正生效在这里  
58.     //如果队列中的任务数大于可用线程数,说明worker压力过大,需要创建新的worker线程  
59.     if (_isStarved() && !(flags & kDeferredTask)) {//kDeferredTask真正生效在这里  
60.         //条件变量,通知controler线程,通知_controllerThreadRoutine控制线程处理  
61.         _scheduleCondition.notify_one();  
62.     }  
63.     return Status::OK();  
}

从上面的分析可以看出, schedule () 主要完成 task 任务入队处理。如果带有递归标识 kMayRecurse ,则通过 _ioContext->dispatch( ) 接口入队,该接口再 ASIO 底层实现的时候实际上没有真正把任务添加到全局队列,而是直接当前线程继续处理,这样就实现了递归调用。如果没有携带 kMayRecurse 递归标识,则task 任务通过 _ioContext->post () 需要入队到全局队列。 ASIO 库的 dispatch 接口和 post 接口的具体实现可以参考:

<<Mongodb 网络传输处理源码实现及性能调优 - 体验内核性能极致设计 >>

如果任务入队到全局队列,则线程池中的worker 线程就会通过全局锁竞争从队列中获取 task 任务执行,该流程通过如下接口实现:

1. //创建线程的回掉函数,线程循环主体,从队列获取task任务执行  
2. void ServiceExecutorAdaptive::_workerThreadRoutine(  
3.     int threadId, ServiceExecutorAdaptive::ThreadList::iterator state) {  
4.     //设置线程模  
5.     _localThreadState = &(*state);  
6.     {  
7.     //worker-N线程名  
8.         std::string threadName = str::stream() << "worker-" << threadId;  
9.         setThreadName(threadName);  
10.     }  
11.     //该线程第一次执行while中的任务的时候为ture,后面都是false  
12.     //表示该线程是刚创建起来的,还没有执行任何一个task  
13.     bool stillPending = true;   
14.       
15.     //线程退出的时候执行以下{},都是一些计数清理  
16.     const auto guard = MakeGuard([this, &stillPending, state] {  
17.         //该worker线程退出前的计数清理、信号通知处理  
18.         //......  
19.     }  
20.     while (_isRunning.load()) {  
21.         ......  
22.         //本次循环执行task的时间,不包括网络IO等待时间  
23.         state->executingCurRun = 0;  
24.         try {  
25.             //通过_ioContext和入队的任务联系起来  
26.             asio::io_context::work work(*_ioContext);  
27.             //记录开始时间,也就是任务执行开始时间  
28.             state->running.markRunning();   
29.             //执行ServiceExecutorAdaptive::schedule中对应的task  
30.             //线程第一次运行task任务,最多从队列拿一个任务执行  
31.             //runTime.toSystemDuration()指定一次run最多运行多长时间  
32.         if (stillPending) {   
33.             //执行一个任务就会返回  
34.             _ioContext->run_one_for(runTime.toSystemDuration());  
35.          } else {  // Otherwise, just run for the full run period  
36.                 //_ioContext对应的所有任务都执行完或者toSystemDuration超时后才会返回  
37.                 _ioContext->run_for(runTime.toSystemDuration()); //io_context::run_for  
38.          }  
39.             ......  
40.         }  
41.         //该线程第一次执行while中的任务后设置ture,后面都是false  
42.         if (stillPending) {   
43.             _threadsPending.subtractAndFetch(1);  
44.             stillPending = false;  
45.         //当前线程数比初始线程数多  
46.         } else if (_threadsRunning.load() > _config->reservedThreads()) {   
47.             //代表本次循环该线程真正处理任务的时间与本次循环总时间(总时间包括IO等待和IO任务处理时间)  
48.             double executingToRunning = state->executingCurRun / static_cast<double>(spentRunning);  
49.             executingToRunning *= 100;  
50.             dassert(executingToRunning <= 100);  
51.   
52.             int pctExecuting = static_cast<int>(executingToRunning);  
53.             //线程很多,超过了指定配置,并且满足这个条件,该worker线程会退出,线程比较空闲,退出  
54.             //如果线程真正处理任务执行时间占比小于该值,则说明本线程比较空闲,可以退出。  
55.             if (pctExecuting < _config->idlePctThreshold()) {  
56.                 log() << "Thread was only executing tasks " << pctExecuting << "% over the last "  
57.                       << runTime << ". Exiting thread.";  
58.                 break;  //退出线程循环,也就是线程自动销毁了
59.             }  
60.         }  
61.     }  
62. }

线程主循环主要工作内容:1. ASIO 库的全局队列获取任务执行; 2. 判断本线程是否比较 ,如果是则直接销毁退出。3. 线程创建起来进行初始线程名设置、线程主循环一些计数处理等。

3.2.3 controller 控制线程核心代码实现

控制线程用于判断线程池是线程是否压力很大,是否比较 ,如果是则增加线程数来减轻全局队列中task 任务积压引起的延迟处理问题。控制线程核心代码实现如下:

1. //controller控制线程  
2. void ServiceExecutorAdaptive::_controllerThreadRoutine() {  
3.     //控制线程线程名设置  
4.     setThreadName("worker-controller"_sd);   
5.     ......  
6.     //控制线程主循环  
7.     while (_isRunning.load()) {  
8.         //一次while结束的时候执行对应func ,也就是结束的时候计算为起始时间  
9.         const auto timerResetGuard =  
10.             MakeGuard([&sinceLastControlRound] { sinceLastControlRound.reset(); });  
11.          //等待工作线程通知,最多等待stuckThreadTimeout  
12.         _scheduleCondition.wait_for(fakeLk, _config->stuckThreadTimeout().toSystemDuration());  
13.         ......  
14.         double utilizationPct;  
15.         {  
16.             //获取所有线程执行任务的总时间  
17.             auto spentExecuting = _getThreadTimerTotal(ThreadTimer::Executing);  
18.             //获取所有线程整个生命周期时间(包括空闲时间+执行任务时间+创建线程的时间)  
19.             auto spentRunning = _getThreadTimerTotal(ThreadTimer::Running);  
20.             //也就是while中执行一次这个过程中spentExecuting差值,  
21.             //也就是spentExecuting代表while一次循环的Executing time开始值,   
22.             //lastSpentExecuting代表一次循环对应的结束time值  
23.             auto diffExecuting = spentExecuting - lastSpentExecuting;  
24.             //也就是spentRunning代表while一次循环的running time开始值,   
25.             //lastSpentRunning代表一次循环对应的结束time值  
26.             auto diffRunning = spentRunning - lastSpentRunning;  
27.             if (spentRunning == 0 || diffRunning == 0)  
28.                 utilizationPct = 0.0;  
29.             else {  
30.                 lastSpentExecuting = spentExecuting;  
31.                 lastSpentRunning = spentRunning;  
32.   
33.                  //一次while循环过程中所有线程执行任务的时间和线程运行总时间的比值  
34.                 utilizationPct = diffExecuting / static_cast<double>(diffRunning);  
35.                 utilizationPct *= 100;  
36.             }  
37.         }  
38.         //也就是本while()执行一次的时间差值,也就是上次走到这里的时间和本次走到这里的时间差值大于该阀值  
39.         //也就是控制线程太久没有判断线程池是否够用了  
40.         if (sinceLastControlRound.sinceStart() >= _config->stuckThreadTimeout()) {  
41.             //use中的线程数=线程池中总的线程数,说明线程池中线程太忙了  
42.             if ((_threadsInUse.load() == _threadsRunning.load()) &&  
43.                 (sinceLastSchedule >= _config->stuckThreadTimeout())) {  
44.                 log() << "Detected blocked worker threads, "  
45.                       << "starting new reserve threads to unblock service executor";  
46.                  //一次批量创建这么多线程,如果我们配置adaptiveServiceExecutorReservedThreads非常大  
47.                 //这里实际上有个问题,则这里会一次性创建非常多的线程,可能反而会成为系统瓶颈  
48.                 //建议mongodb官方这里最好做一下上限限制  
49.                 for (int i = 0; i < _config->reservedThreads(); i++) {  
50.                    //创建新的worker工作线程  
51.                     _startWorkerThread();  
52.                 }  
53.             }  
54.             continue;  
55.         }  
56.          //当前的worker线程数  
57.         auto threadsRunning = _threadsRunning.load();  
58.         //保证线程池中worker线程数最少都要reservedThreads个  
59.         if (threadsRunning < _config->reservedThreads()) {  
60.             //线程池中线程数最少数量不能比最低配置少  
61.             while (_threadsRunning.load() < _config->reservedThreads()) {  
62.                 _startWorkerThread();  
63.             }  
64.         }  
65.          //worker线程非空闲占比小于该阀值,说明压力不大,不需要增加worker线程数  
66.         if (utilizationPct < _config->idlePctThreshold()) {  
67.             continue;  
68.         }  
69.         //走到这里,说明整体worker工作压力还是很大的  
70.         //我们在这里循环stuckThreadTimeout毫秒,直到我们等待worker线程创建起来并正常运行task  
71.         //因为如果有正在创建的worker线程,我们等待一小会,最多等待stuckThreadTimeout ms  
72.          //保证一次while循环时间为stuckThreadTimeout  
73.         do {  
74.             stdx::this_thread::sleep_for(_config->maxQueueLatency().toSystemDuration());  
75.         } while ((_threadsPending.load() > 0) &&  
76.                  (sinceLastControlRound.sinceStart() < _config->stuckThreadTimeout()));  
77.         //队列中任务数多余可用空闲线程数,说明压力有点大,给线程池增加一个新的线程  
78.         if (_isStarved()) {  
79.             _startWorkerThread();  
80.         }  
81.     }  
82. }

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

控制线程每过一定时间循环检查线程池中的线程压力状态,实现原理就是简单的实时记录线程池中的线程当前运行情况,为以下两类计数:总线程数_threadsRunning

当前正在运行task 任务的线程数 _threadsInUse 。如果 _threadsRunning=_threadsRunning ,说明所有工作线程当前都在处理 task 任务,这时候就会创建新的 worker 线程来减轻任务因为排队引起的延迟。

2.1.4 adaptive 线程模型函数接口大全

  前面只分析了核心的几个接口,下表列出了该模块的完整接口功能说明:

3.  总结

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

3.1 synchronous 同步线程模型总结

Sync 线程模型也就是一个链接一个线程,实现比较简单。该线程模型, listener 线程每接收到一个链接就会创建一个线程,该链接上的所有数据读写及内部请求处理流程将一直由本线程负责,整个线程的生命周期就是这个链接的生命周期。

3.2 adaptive 线程模型 worker 线程运行时间相关的几个统计

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

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

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

单个工作线程如何判断自己处于 空闲 状态

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

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

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

如何判断线程池中工作线程“太忙”

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

控制线程每过一定时间循环检查线程池中的线程压力状态,实现原理就是简单的实时记录线程池中的线程当前运行情况,为以下两类计数:总线程数_threadsRunning

当前正在运行task 任务的线程数 _threadsInUse 。如果 _threadsRunning=_threadsRunning ,说明所有工作线程当前都在处理 task 任务,这时候已经没有多余线程去 asio 库中的全局任务队列 op_queue_ 中取任务执行了,这时候队列中的任务就不会得到及时的执行,就会成为响应客户端请求的瓶颈点。

如何判断线程池中所有线程比较“空闲”

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

control 控制线程如何动态增加线程池中线程数

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

1. control控制线程循环主体主要压力判断控制流程如下:  
2. while {  
3.     #等待工作线程唤醒条件变量,最长等待stuckThreadTimeout  
4.     _scheduleCondition.wait_for(stuckThreadTimeout)  
5.     ......  
6.     #获取线程池中所有线程最近一次运行任务的总有效时间TT1  
7.     Executing = _getThreadTimerTotal(ThreadTimer::Executing);  
8.     #获取线程池中所有线程最近一次运行任务的总运行时间TT2  
9.     Running = _getThreadTimerTotal(ThreadTimer::Running);  
10.     #线程池中所有线程的总有效工作时间占比 = TT1/TT2  
11.     utilizationPct = Executing / Running;  
12.     ......  
13.     #代表control线程太久没有进行线程池压力检查了  
14.     if(本次循环到该行代码的时间 > stuckThreadTimeout阀值) {  
15.         #说明太久没做压力检查,造成工作线程不够用了  
16.         if(_threadsInUse == _threadsRunning) {  
17.             #批量创建一批工作线程  
18.             for(; i < reservedThreads; i++)  
19.                 #创建工作线程  
20.                 _startWorkerThread();  
21.         }  
22.         #control线程继续下一次循环压力检查  
23.         continue;  
24.     }     
25.     ......  
26.     #如果当前线程池中总线程数小于最小线程数配置  
27.     #则创建一批线程,保证最少工作线程数达到要求  
28.     if (threadsRunning < reservedThreads) {  
29.         while (_threadsRunning < reservedThreads) {  
30.             _startWorkerThread();  
31.         }  
32.     }  
33.     ......  
34.     #检查上一次循环到本次循环这段时间范围内线程池中线程的工作压力  
35.     #如果压力不大,则说明无需增加工作线程数,则继续下一次循环  
36.     if (utilizationPct < idlePctThreshold) {  
37.         continue;  
38.     }  
39.     ......  
40.     #如果发现已经有线程创建起来了,但是这些线程还没有运行任务  
41.     #这说明当前可用线程数可能足够了,我们休息sleep_for会儿在判断一下  
42.     #该循环最多持续stuckThreadTimeout时间  
43.     do {  
44.         stdx::this_thread::sleep_for();  
45.     } while ((_threadsPending.load() > 0) &&  
46.         (sinceLastControlRound.sinceStart() < stuckThreadTimeout)  
47.       
48.     #如果tasksQueued队列中的任务数大于工作线程数,说明任务在排队了  
49.     #该扩容线程池中线程了  
50.     if (_isStarved()) {  
51.         _startWorkerThread();  
52.     }  
53. }

实时serviceExecutorTaskStats 线程模型统计信息

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

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

     上表中各个字段的都有各自的意义,我们需要注意这些参数的以下情况:

1.  threadsRunning - threadsInUse 的差值越大说明线程池中线程比较空闲,差值越小说明压力越大

2.  threadsPending 越大,表示线程池越空闲

3.  tasksQueued - totalExecuted 的差值越大说明任务队列上等待执行的任务越多,说明任务积压现象越明显

4.  deferredTasksQueued 越大说明工作线程比较空闲,在等待客户端数据到来

5.  totalTimeRunningMicros - totalTimeExecutingMicros 差值越大说明越空闲

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

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

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

Mongodb-3.6 早期版本 control 线程动态调整动态增加线程缺陷 1

从步骤6 中可以看出, control 控制线程创建工作线程的第一个条件为:如果该线程超过 stuckThreadTimeout 阀值都没有做线程压力控制检查,并且线程池中线程数全部在处理任务队列中的任务,这种情况 control 线程一次性会创建 reservedThreads 个线程。 reservedThreads adaptiveServiceExecutorReservedThreads 配置,如果没有配置,则采用初始值 CPU/2

那么问题来了,如果我提前通过命令行配置了这个值,并且这个值配置的非常大,例如一百万,这里岂不是要创建一百万个线程,这样会造成操作系统负载升高,更容易引起耗尽系统pid 信息,这会引起严重的系统级问题。

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

3.3 adaptive 线程模型实时参数调优

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

     命令行实时参数调整方法如下,以adaptiveServiceExecutorReservedThreads 为例,其他参数调整方法类似: db.adminCommand( { setParameter: 1, adaptiveServiceExecutorReservedThreads: xx} )

    Mongodb 服务层的 adaptive 动态线程模型设计代码实现非常优秀,有很多实现细节针对不同应用场景做了极致优化。

3.4 不同线程模型性能多场景 PK

详见: <<Mongodb 网络传输处理源码实现及性能调优 - 体验内核性能极致设计 >>

3.5 Asio 网络库全局队列锁优化,性能进一步提升

通过 <<Mongodb 网络传输处理源码实现及性能调优 - 体验内核性能极致设计 >> 一文中的ASIO 库实现和 adaptive 动态线程模型实现,可以看出为了获取全局任务队列上的任务,需要进行全局锁竞争,这实际上是整个线程池从队列获取任务运行最大的一个瓶颈。

优化思路: 我们可以通过优化队列和锁来提升整体性能,当前的队列只有一个,我们可以把单个队列调整为多个队列,每个队列一把锁,任务入队的时候通过把链接session 散列到多个队列,通过该优化,锁竞争及排队将会得到极大的改善。

优化前队列架构:

优化后队列架构:

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

由于篇幅原因,本文只分析了主要核心接口源码实现,更多接口的源码实现可以参考如下地址,详见: mongodb adaptive 动态线程模型源码详细分析



来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/69984922/viewspace-2732739/,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论
前滴滴出行专家工程师,OPPO文档数据库mongodb负责人,负责近千万级峰值TPS/数万亿级数据量文档数据库mongodb研发和运维工作,专注于分布式缓存、高性能中间件、数据库等研发,持续分享《MongoDB内核源码设计、性能优化、最佳运维实践》。git账号:y12345yz

注册时间:2020-10-04

  • 博文量
    33
  • 访问量
    25623