mongodb核心原始碼實現、效能調優、最佳運維實踐系列-網路傳輸層模組原始碼實現四

y123456yzzyz發表於2020-11-06

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/,如需轉載,請註明出處,否則將追究法律責任。

相關文章