mongodb網路傳輸處理原始碼實現及效能調優-體驗核心效能極致設計

y123456yzzyz發表於2020-10-24

開源mongodb 程式碼規模數百萬行 本篇文章內容主要 分析mongodb 網路傳輸模組 內部 實現 及其 效能調優方法 學習網路IO 處理流程,體驗不同工作執行緒模型效能極致設計原理 另外一個目的就是引導大家快速進行百萬級別規模原始碼閱讀,做到不同大工程原始碼 舉一反三 快速閱讀的目的。

此外,mognodb 網路工作執行緒模型設計非常好,不僅非常值得資料庫相關研發人員學習,中介軟體、分散式、高併發、服務端等相關研發人員也可以借鑑,極力推薦大家學習。

關於作者

前滴滴出行技術專家,現任OPPO 文件資料庫 mongodb 負責人,負責 oppo 千萬級峰值 TPS/ 十萬億級資料量文件資料庫 mongodb 研發和運維工作,一直專注於分散式快取、高效能服務端、資料庫、中介軟體等相關研發。後續持續分享《 MongoDB 核心原始碼設計、效能優化、最佳運維實踐》, Github 賬號地址 : https://github.com/y123456yz

1. 如何閱讀數百萬級大工程核心原始碼

      詳見我的上一篇分享:

mongodb原始碼實現、調優、最佳實踐系列-數百萬行mongodb核心原始碼閱讀經驗分享

2. mongodb 核心網路傳輸 transport 模組實現原理

1.5 章節中,我們把 transport 功能模組細化拆分成了網路傳輸資料壓縮子模組、服務入口子模組、執行緒模型子模組、狀態機處理子模組、 session 會話資訊子模組、資料分發子模組、套接字處理和傳輸管理子模組,總共七個子模組。


實際上mongodb 服務層程式碼的底層網路 IO 實現依賴 asio 庫完成,因此 transport 功能模組應該是 7+1 個子模組構成,也就是服務層程式碼實現由 8 個子模組支援。

2.1 asio 網路 IO 庫實現原理

     Asio 是一個優秀網路庫,依賴於boost 庫的部分實現,支援 linux windos unix 等多平臺, mongodb 基於 asio 庫來實現網路 IO 及定時器處理。 asio 庫由於為了支援多平臺,在程式碼實現中用了很多 C++ 的模板,同時用了很多 C++ 的新語法特性,因此整體程式碼可讀性相比 mongodb 服務層程式碼差很多。

服務端網路IO 非同步處理流程大體如下:

     1. 呼叫 socket() 建立一個套接字,獲取一個 socket 描述符。

       2. 呼叫 bind () 繫結 套接字 ,同時通過 listen () 來監聽客戶端連結,註冊該 socket 描述符到 epoll 事件集列表,等待 accept 對應的新連線讀事件到來。

3.  通過epoll_wait 獲取到 accept 對應的讀事件資訊,然後 呼叫accept () 來接受客戶的連線 ,並獲取一個新的連結描述符new_fd

4.  註冊新的new_fd epoll 事件集列表,當該 new_fd 描述符上有讀事件到來,於是通過 epoll_wait 獲取該事件,開始該 fd 上的資料讀取。

5.  讀取資料完畢後,開始內部處理,處理完後傳送對應資料到客戶端。如果一次write 資料到核心協議棧寫太多,造成協議棧寫滿,則新增寫事件到 epoll 事件列表。

服務端網路IO 同步方式處理流程和非同步流程大同小異,少了 epoll 註冊和 epoll 事件通知過程,直接同步呼叫 accept() recv() send() 進行 IO 處理。

同步IO 處理方式相對比較簡單,下面僅分析和 mongodb 服務層 transport 模組結合比較緊密的 asio 非同步 IO 實現原理。

 

Mongodb 服務層用到的 Asio 庫功能中最重要的幾個結構有 io_context scheduler epoll_reactor Asio 把網路 IO 處理任務、狀態機排程任務做為 2 種不同操作,分別由兩個繼承自 operation 的類結構管理,每種型別的操作也就是一個任務 task io_context scheduler epoll_reactor 最重要的功能就是管理和排程這些 task 有序並且高效的執行。

2.1.1  io_context 類實現及其作用

io_context 上下文類是 mongodb 服務層和 asio 網路庫互動的樞紐,是 mongodb 服務層和 asio 庫進行 operation 任務互動的入口。該類負責 mongodb 相關任務的入隊、出隊,並與 scheduler 排程處理類配合實現各種任務的高效率執行。 Mongodb 服務層在實現的時候, accept 新連線任務使用 _acceptorIOContext 這個IO 上下文成員實現,資料分發及其相應回撥處理由 _workerIOContext 上下文成員實現。

該類的幾個核心介面功能如下表所示:

Io_context 類成員 / 函式名

功能

備註說明

impl_type&  impl_;

Mongodb 對應的 type 型別為 scheduler

通過該成員來呼叫 scheduler 排程類的介面

io_context::run()

負責 accept 對應非同步回撥處理

1.mongodb 中該介面只針對 accept 對應 IO 非同步處理

2. 呼叫 scheduler::run() 進行 accept 非同步讀操作

io_context::stop()

停止 IO 排程處理

呼叫 scheduler::stop() 介面

io_context::run_one_until()

1.  從全域性佇列上獲取一個任務執行

2.  如果全域性佇列為空,則呼叫 epoll_wait() 獲取網路 IO 事件處理

呼叫 schedule::wait_one()

io_context::post()

任務入隊到全域性佇列

呼叫 scheduler::post_immediate_completion()

io_context::dispatch()

1. 如果呼叫該介面的執行緒已經執行過全域性佇列中的任務,則直接繼續由本執行緒執行該入隊的任務

2. 如果不滿足條件 1 條件,則直接入隊到全域性佇列,等待排程執行

如果條件 1 滿足,則直接由本執行緒執行

如果條件 1 不滿足,則呼叫 scheduler::do_dispatch ()

總結:

1. 從上表的分析可以看出,和 mongodb 直接相關的幾個介面最終都是呼叫 schedule 類的相關介面,整個實現過程參考下一節 scheduler 排程實現模組。

2. 上表中的幾個介面按照功能不同,可以分為入隊型介面 (poll dispatch) 和出隊型介面 (run_for run run_one_for)

3. 按照和 io_context 的關聯性不同,可以分為 accept 相關 io(_acceptorIOContext) 處理的介面 (run stop) 和新連結 fd 對應 Io(_workerIOContext) 資料分發相關處理及回撥處理的介面 (run_for run_one_for poll dispatch)

4. io_context 上下文的上述介面,除了 dispatch 在某些情況下直接執行 handler 外,其他介面最終都會間接呼叫 scheduler 排程類介面。

2.1.2 asio 排程模組 scheduler 實現

上一節的io_context 上下文中提到 mongodb 操作的 io 上下文最終都會呼叫 scheduler 的幾個核心介面, io_context 只是起銜接 mongodb asio 庫的連結橋樑。 scheduler 類主要工作在於完成任務排程,該類和 mongodb 相關的幾個主要成員變數及介面如下表:

scheduler 類主要成員 / 介面

功能

備註說明

mutable mutex mutex_;

互斥鎖,全域性佇列訪問保護

多執行緒從全域性佇列獲取任務的時候加鎖保護

op_queue<operation> op_queue_;

全域性任務佇列,全域性任務和網路事件相關任務都新增到該佇列

3.1.1 中的 5 種型別的任務都入隊到了該全域性佇列

bool stopped_;  

執行緒是否可排程標識

true 後,將不再處理 epoll 相關事件,參考 scheduler::do_run_one

event wakeup_event_;

喚醒等待鎖得執行緒

實際 event 由訊號量封裝

task_operation task_operation_;

特殊的 operation

在連結串列中沒進行一次 epoll 獲取到 IO 任務加入全域性佇列後,都會緊接著新增一個特殊 operation

reactor* task_;

也就是 epoll_reactor

藉助 epoll 實現網路事件非同步處理

atomic_count outstanding_work_;

套接字描述符個數

accept 獲取到的連結數 fd 個數 +1( 定時器 fd)

scheduler::run()

迴圈處理 epoll 獲取到的 accept 事件資訊

迴圈呼叫 scheduler::do_run_one() 介面

scheduler::do_dispatch()

任務入隊

任務入隊到全域性佇列 op_queue_

scheduler::do_wait_one()

任務出隊執行

如果佇列為空則獲取 epoll 事件集對應的網路 IO 任務放入全域性 op_queue_ 佇列

scheduler::restart()

重新啟用排程

實際上就是修改 stopped_ 標識為 false

scheduler::stop_all_threads()

停止排程

實際上就是修改 stopped_ 標識為 true

 

2.1.3 operation 任務佇列

從前面的分析可以看出,一個任務對應一個operation 類結構, asio 非同步實現中 schduler 排程的任務分為 IO 處理任務 (accept 處理、讀 io 處理、寫 io 處理、網路 IO 處理回撥處理 ) 和全域性狀態機任務,總共 2 種任務小類。

此外,asio 還有一種特殊的 operation ,該 Operastion 什麼也不做,只是一個特殊標記。網路 IO 處理任務、狀態機處理任務、特殊任務這三類任務分別對應三個類結構,分別是: reactor_op completion_handler task_operation_ ,這三個類都會繼承基類 operation

 

1.  operation 基類實現

operation 基類實際上就是 scheduler_operation 類,通過 typedef scheduler_operation operation 指定,是其他三個任務的父類,其主要實現介面如下:

operation 類主要成員 / 介面

功能

備註說明

unsigned int task_result_

Epoll_wait 獲取到的事件點陣圖資訊記錄到該結構中

descriptor_state::do_complete 中取出點陣圖上的事件資訊做底層 IO 讀寫處理

func_type func_;

需要執行的任務


scheduler_operation ::complete()

執行 func_()

任務的內容在 func() 中執行

1.  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 工作執行緒執行,狀態機具體任務內容在後面《狀態機實現》章節實現。

1.  網路IO 事件處理任務

網路IO 事件對應的 Opration 任務最終由 reactor_op 類實現,該類主要成員及介面如下:

reactor_op 類主要成員 / 介面

功能

備註說明

asio::error_code ec_;

全域性狀態機任務函式

這個 handler 就相當於一個任務,實際上是一個函式

std::size_t bytes_transferred_;

讀取或者傳送的資料位元組數

Epoll_wait 返回後獲取到對應的讀寫事件,然後進行資料分發操作

enum status;

底層資料讀寫狀態

標識讀寫資料的狀態

perform_func_type perform_func_;

底層 IO 操作的函式指標

perform() 中執行

status perform()

執行 perform_func_ 函式

perform 實際上就是資料讀寫的底層實現

reactor_op(perform_func_type perform_func, func_type complete_func)

類初始化

這裡有兩個 func:

1.  底層資料讀寫實現的介面,也就是 perform_func

2.  讀取或者傳送一個完整 mongodb 報文的回撥介面,也就是 complete_func

reactor_op 類可以看出,該類的主要兩個函式成員: perform_func_ complete_func 。其中 perform_func_ 函式主要負責非同步網路 IO 底層處理, complete_func 用於獲取到一個新連結、接收或者傳送一個完整 mongodb 報文後的後續回撥處理邏輯。

perform_func_ 具體功能包含如下三種如下:

  1. 通過 epoll 事件集處理底層 accept 獲取新連線 fd

  2. fd 上的資料非同步接收

  3. fd 上的資料非同步傳送

針對上面的三個網路IO 處理功能, ASIO 在實現的時候,分別通過三個不同的類 (reactive_socket_accept_op_base reactive_socket_recv_op_base

reactive_socket_send_op_base) 實現,這三個類都繼承父類 reactor_op 。這三個類的功能總結如下表所示:

類名

功能

說明

reactive_socket_accept_op_base

1.  Accept() 系統呼叫獲取新 fd

2.  獲取到一個新 fd 後的 mongodb 層邏輯回撥處理

Accept() 系統呼叫由 perform_func() 函式處理

獲取到新連結後的邏輯回撥由 complete_func 執行

reactive_socket_recv_op_base

1.  讀取一個完整 mongodb 報文讀取

2.  讀取完整報文後的 mongodb 服務層邏輯回撥處理

從一個連結上讀取一個完整 mongodb 報文讀取由 perform_func() 函式處理

讀取完整報文後的 mongodb 服務層邏輯回撥處理由 complete_func 執行

reactive_socket_send_op_base

1.  傳送一個完整的 mongodb 報文

2.  傳送完一個完整 mongodb 報文後的 mongodb 服務層邏輯回撥處理

Accept() 系統呼叫由 perform_func() 函式處理

獲取到新連結後的邏輯回撥由 complete_func 執行

    總結: asio 在實現的時候,把 accept 處理、資料讀、資料寫分開處理,都繼承自公共基類 reactor_op ,該類由兩個操作組成:底層 IO 操作和回撥處理。其中, asio 的底層 IO 操作最終由 epoll_reactor 類實現,回撥操作最終由 mongodb 服務層指定,底層 IO 操作的回撥對映表如下:

底層 IO 操作型別

Mongodb 服務層回撥

說明

Accept( reactive_socket_accept_op_base )

ServiceEntryPointImpl::startSession ,回撥中進入狀態機任務流程

Listener 執行緒獲取到一個新連結後 mongodb 的回撥處理

Recv( reactive_socket_recv_op_base )

ServiceStateMachine::_sourceCallback ,回撥中進入狀態機任務流程

接收一個完整 mongodb 報文的回撥處理

Send( reactive_socket_send_op_base )

ServiceStateMachine::_sinkCallback ,回撥中進入狀態機任務流程

傳送一個完整 mongodb 報文的回撥處理

   

說明: 網路IO 事件處理任務實際上在狀態機任務內執行,也就是狀態機任務中呼叫 asio 庫進行底層 IO 事件執行處理。   

 

4.  特殊任務task_operation

前面提到,ASIO 庫中還包含一種特殊的 task_operation 任務, asio 通過 epoll_wait 獲取到一批 IO 事件後,會新增到 op_queue_ 全域性佇列,工作執行緒從佇列取出任務有序執行。每次通過 epoll_wait 獲取到 IO 事件資訊後,除了新增這些讀寫事件對應的底層 IO 處理任務到全域性佇列外,每次還會額外生成一個特殊 task_operation 任務新增到佇列中。

為何引入一個特殊任務的Opration

工作執行緒變數全域性op_queue_ 佇列取出任務執行,如果從佇列頭部取出的是特殊 Op 操作,就會立馬觸發獲取 epoll 網路事件資訊,避免底層網路 IO 任務長時間不被處理引起的 " 飢餓 " 狀態,保證狀態機任務和底層 IO 任務都能 平衡 執行。

asio 庫底層處理實際上由 epoll_reactor 類實現,該類主要負責 epoll 相關非同步 IO 實現處理, 鑑於篇幅epoll reactor 相關實現將在後續《 mongodb 核心原始碼實現及調優系列》相關章節詳細分析。

2.2 message_compressor 網路傳輸資料壓縮子模組

網路傳輸資料壓縮子模組主要用於減少網路頻寬佔用,通過CPU 來換取 IO 消耗,也就是以更多 CPU 消耗來減少網路 IO 壓力。

鑑於篇幅,該模組的詳細原始碼實現過程將在《mongodb 核心原始碼實現及調優系列》相關章節分享。

2.3 transport_layer 套接字處理及傳輸層管理子模組

transport_layer 套接字處理及傳輸層管理子模組功能主要如下 :

1.  套接字相關初始化處理

2.  結合asio 庫實現非同步 accept 處理

3.  不同執行緒模型管理及初始化

     鑑於篇幅,該模組的詳細原始碼實現過程將在《mongodb 核心原始碼實現及調優系列》相關章節詳細分析。

2.4 session 會話子模組

Session 會話模組功能主要如下:

1.  負責記錄HostAndPort 、新連線 fd 資訊

2.  通過和底層asio 庫的直接互動,實現資料的同步或者非同步收發。

鑑於篇幅,該模組的詳細原始碼實現過程將在《mongodb 核心原始碼實現及調優系列》相關章節詳細分析。

2.5 Ticket 資料分發子模組

Ticket 資料分發子模組主要功能如下:

1.  呼叫session 子模組進行底層 asio 庫處理

2.  拆分資料接收和資料傳送到兩個類,分別實現。

3.  完整mongodb 報文讀取

4.  接收或者傳送mongodb 報文後的回撥處理

鑑於篇幅,該模組的詳細原始碼實現過程將在《mongodb 核心原始碼實現及調優系列》相關章節詳細分析。

2.6 service_state_machine 狀態機排程子模組

service_state_machine 狀態機處理模組主要功能如下:

1.  Mongodb 網路資料處理狀態轉換

2.  配合狀態轉換邏輯把一次mongodb 請求拆分為二個大的狀態任務 : 接收一個完整長度 mongodb 報文、接收到一個完整報文後的後續所有處理 ( 含報文解析、認證、引擎層處理、應答客戶端等 )

3.  配合工作執行緒模型子模組,把步驟2 的兩個任務按照指定的狀態轉換關係進行排程執行。

鑑於篇幅,該模組的詳細原始碼實現過程將在《mongodb 核心原始碼實現及調優系列》相關章節詳細分析。

2.7 service_entry_point 服務入口點子模組

service_entry_point 服務入口點子模組主要負責如下功能:

1.  連線數控制

2.  Session 會話管理

3.  接收到一個完整報文後的回撥處理( 含報文解析、認證、引擎層處理等 )

鑑於篇幅,該模組的詳細原始碼實現過程將在《mongodb 核心原始碼實現及調優系列》相關章節詳細分析。

2.8 service_executor 服務執行子模組,即執行緒模型子模組

執行緒模型設計在資料庫效能指標中起著非常重要的作用,因此本文將重點分析mongodb 服務層執行緒模型設計,體驗 mongodb 如何通過優秀的工作執行緒模型來達到多種業務場景下的效能極致表現。

service_executor 執行緒子模組,在程式碼實現中,把執行緒模型分為兩種: synchronous 執行緒模式和 adaptive 執行緒模型。 Mongodb 啟動的時候通過配置引數 net. serviceExecutor 來確定採用那種執行緒模式執行mongo 例項,配置方式如下:

net:   // 同步執行緒模式配置

  serviceExecutor: synchronous

或者  // 動態執行緒池模式配置

net:

  serviceExecutor: adaptive

2.8.1 synchronous 同步執行緒模型 ( 一個連結一個執行緒 ) 實現原理

synchronous 同步執行緒模型, listener 執行緒每接收到一個連結就會建立一個執行緒,該連結上的所有資料讀寫及內部請求處理流程將一直由本執行緒負責,整個執行緒的生命週期就是這個連結的生命週期。

1. 網路 IO 操作方式

synchronous 同步執行緒模型實現過程比較簡單,執行緒循迴圈以 同步IO 操作方式 fd 讀取資料,然後處理資料,最後返回客戶端對應得資料。同步執行緒模型方式針對某個連結的系統呼叫如下圖所示 (mongo shell 建立連結後 show dbs)

2.  效能極致提升小細節

雖然synchronous 執行緒模型比較簡單,但是 mongodb 服務層再實現的時候針對細節做了極致的優化,主要體現在如下程式碼實現細節上面:

具體實現中,mongodb 執行緒每處理 16 次使用者請求,就讓執行緒空閒一會兒。同時,當總的工作執行緒數大於 cpu 核數後,每次都做讓出一次 CPU 排程。通過這兩種方式,在效能測試中可以提升 5% 的效能,雖然提升效能不多,但是充分體現了 mongodb 在效能優化提升方面所做的努力。

2.  同步執行緒模型監控統計

可以通過如下命令獲取同步執行緒模型方式獲取當前mongodb 中的連結數資訊:

    該監控中主要由兩個欄位組成:passthrough 代表同步執行緒模式, threadsRunning 表示當前程式的工作執行緒數。

 

2.8.2 adaptive 非同步執行緒模型 ( 動態執行緒池 ) 實現原理

adaptive 動態執行緒池模型,核心實現的時候會根據當前系統的訪問負載動態的調整執行緒數。當執行緒 CPU 工作比較頻繁的時候,控制執行緒增加工作執行緒數;當執行緒 CPU 比較空閒後,本執行緒就會自動消耗退出。下面一起體驗 adaptive 執行緒模式下, mongodb 是如何做到效能極致設計的。

 

1.  執行緒池初始化

Mongodb 預設初始化後,執行緒池執行緒數預設等於 CPU 核心數 /2 ,主要實現如下:

從程式碼實現可以看出,執行緒池中最低執行緒數可以通過adaptiveServiceExecutorReservedThreads 配置,如果沒有配置則預設設定為 CPU/2

 

1.  工作執行緒執行時間相關的幾個統計

3.6 狀態機排程模組中提到,一個完整的客戶端請求處理可以轉換為2 個任務:通過 asio 庫接收一個完整 mongodb 報文、接收到報文後的後續所有處理 ( 含報文解析、認證、引擎層處理、傳送資料給客戶端等 ) 。假設這兩個任務對應的任務名、執行時間分別如下表所示:

任務名

功能

執行時間

Task1

呼叫底層 asio 庫接收一個完整 mongodb 報文

T1

Task2

接收到報文後的後續所有處理 ( 含報文解析、認證、引擎層處理、傳送資料給客戶端等 )

T2

客戶端一次完整請求過程中,mongodb 內部處理過程 =task1 + task2 ,整個請求過程中 mongodb 內部消耗的時間 T1+T2

實際上如果fd 上沒有資料請求,則工作執行緒就會等待資料,等待資料的過程就相當於空閒時間,我們把這個時間定義為 T3 。於是一個工作執行緒總執行時間 = 內部任務處理時間 + 空閒等待時間,也就是執行緒總時間 =T1+T2+T3 ,只是 T3 是無用等待時間。

 

2.  單個工作執行緒如何判斷自己處於 空閒 狀態

步驟2 中提到,執行緒執行總時間 =T1 + T2 +T3 ,其中 T3 是無用等待時間。如果 T3 的無用等待時間佔比很大,則說明執行緒比較空閒。

Mongodb 工作執行緒每次執行完一次 task 任務後,都會判斷本執行緒的有效執行時間佔比,有效執行時間佔比 =(T1+T2)/(T1+T2+T3) ,如果有效執行時間佔比小於某個閥值,則該執行緒自動退出銷燬,該閥值由 adaptiveServiceExecutorIdlePctThreshold 引數指定。該引數線上調整方式:

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

 

3.  如何判斷執行緒池中工作執行緒“太忙”

Mongodb 服務層有個專門的控制執行緒用於判斷執行緒池中工作執行緒的壓力情況,以此來決定是否線上程池中建立新的工作執行緒來提升效能。

控制執行緒每過一定時間迴圈檢查執行緒池中的執行緒壓力狀態,實現原理就是簡單的實時記錄執行緒池中的執行緒當前執行情況,為以下兩類計數:匯流排程數_threadsRunning

當前正在執行task 任務的執行緒數 _threadsInUse 。如果 _threadsRunning=_threadsRunning ,說明所有工作執行緒當前都在處理 task 任務,這時候已經沒有多餘執行緒去 asio 庫中的全域性任務佇列 op_queue_ 中取任務執行了,這時候佇列中的任務就不會得到及時的執行,就會成為響應客戶端請求的瓶頸點。

 

5. 如何判斷執行緒池中所有執行緒比較“空閒”

control 控制執行緒會在收集執行緒池中所有工作執行緒的有效執行時間佔比,如果佔比小於指定配置的閥值,則代表整個執行緒池空閒。

前面已經說明一個執行緒的有效時間佔比為:(T1+T2)/(T1+T2+T3) ,那麼所有執行緒池中的執行緒總的有效時間佔比計算方式如下:

所有執行緒的總有效時間TT1 = ( 執行緒池中工作執行緒 1 的有效時間 T1+T2)  +  ( 執行緒池中工作執行緒 2 的有效時間 T1+T2)  + ..... + ( 執行緒池中工作執行緒 n 的有效時間 T1+T2)

所有執行緒總執行時間TT2 =  ( 執行緒池中工作執行緒 1 的有效時間 T1+T2+T3)  +  ( 執行緒池中工作執行緒 2 的有效時間 T1+T2+T3)  + ..... + ( 執行緒池中工作執行緒 n 的有效時間 T1+T2+T3)

執行緒池中所有執行緒的總有效工作時間佔比 = TT1/TT2

 

6.  control 控制執行緒如何動態增加執行緒池中執行緒數

Mongodb 在啟動初始化的時候,會建立一個執行緒名為 ”worker-controller” 的控制執行緒,該執行緒主要工作就是判斷執行緒池中是否有充足的工作執行緒來處理asio 庫中全域性佇列 op_queue_ 中的 task 任務,如果發現執行緒池比較忙,沒有足夠的執行緒來處理佇列中的任務,則線上程池中動態增加執行緒來避免 task 任務在佇列上排隊等待。

control 控制執行緒迴圈主體主要壓力判斷控制流程如下:

while {
#等待工作執行緒喚醒條件變數,最長等待stuckThreadTimeout
_scheduleCondition.wait_for(stuckThreadTimeout)
#獲取執行緒池中所有執行緒最近一次執行任務的總有效時間TT1
Executing = _getThreadTimerTotal(ThreadTimer::Executing);
#獲取執行緒池中所有執行緒最近一次執行任務的總執行時間TT2
Running = _getThreadTimerTotal(ThreadTimer::Running);
#執行緒池中所有執行緒的總有效工作時間佔比 = TT1/TT2
utilizationPct = Executing / Running;
#代表control執行緒太久沒有進行執行緒池壓力檢查了
if(本次迴圈到該行程式碼的時間 > stuckThreadTimeout閥值) {
#說明太久沒做壓力檢查,造成工作執行緒不夠用了
    if(_threadsInUse == _threadsRunning) {
#批量建立一批工作執行緒
for(; i < reservedThreads; i++)
#建立工作執行緒
_startWorkerThread();
}
#control執行緒繼續下一次迴圈壓力檢查
continue;
} 
#如果當前執行緒池中匯流排程數小於最小執行緒數配置
#則建立一批執行緒,保證最少工作執行緒數達到要求
if (threadsRunning < reservedThreads) {
while (_threadsRunning < reservedThreads) {
_startWorkerThread();
}
}
#檢查上一次迴圈到本次迴圈這段時間範圍內執行緒池中執行緒的工作壓力
#如果壓力不大,則說明無需增加工作執行緒數,則繼續下一次迴圈
if (utilizationPct < idlePctThreshold) {
continue;
}
#如果發現已經有執行緒建立起來了,但是這些執行緒還沒有執行任務
#這說明當前可用執行緒數可能足夠了,我們休息sleep_for會兒在判斷一下
#該迴圈最多持續stuckThreadTimeout時間
do {
stdx::this_thread::sleep_for();
} while ((_threadsPending.load() > 0) &&
        (sinceLastControlRound.sinceStart() < stuckThreadTimeout)
#如果tasksQueued佇列中的任務數大於工作執行緒數,說明任務在排隊了
#該擴容執行緒池中執行緒了
if (_isStarved()) {
_startWorkerThread();
}
}

7. 實時 serviceExecutorTaskStats 執行緒模型統計資訊

 

   本文分析的mongodb 版本為 3.6.1 ,其 network.serviceExecutorTaskStats 網路執行緒模型相關統計通過 db.serverStatus().network.serviceExecutorTaskStats 可以檢視,如下圖所示:

上圖的幾個資訊功能可以分類為三大類,說明如下:

大類類名

欄位名

功能

       無

executor

Adaptive ,說明是動態執行緒池模式

 

   執行緒統計

threadsInUse

當前正在執行 task 任務的執行緒數

threadsRunning

當前執行的執行緒數

threadsPending

當前建立起來,但是還沒有執行過 task 任務的執行緒數

 

    佇列統計

totalExecuted

執行緒池執行成功的任務總數

tasksQueued

入隊到全域性佇列的任務數

deferredTasksQueued

等待接收網路 IO 資料來讀取一個完整 mongodb 報文的任務數

 

 

    時間統計

totalTimeRunningMicros

所有工作執行緒執行總時間 ( 含等待網路 IO 的時間 T1 + 讀一個 mongodb 報文任務的時間 T2 + 一個請求後續處理的時間 T3)

totalTimeExecutingMicros

也就是 T2+T3 mongodb 內部響應一個完整 mongodb 耗費的時間

totalTimeQueuedMicros

執行緒池中所有執行緒從建立到被用來執行第一個任務的等待時間

   上表中各個欄位的都有各自的意義,我們需要注意這些引數的以下情況:

1.  threadsRunning - threadsInUse 的差值越大說明執行緒池中執行緒比較空閒,差值越小說明壓力越大

2.  threadsPending 越大,表示執行緒池越空閒

3.  tasksQueued - totalExecuted 的差值越大說明任務佇列上等待執行的任務越多,說明任務積壓現象越明顯

4.  deferredTasksQueued 越大說明工作執行緒比較空閒,在等待客戶端資料到來

5.  totalTimeRunningMicros - totalTimeExecutingMicros 差值越大說明越空閒

   上面三個大類中的總體反映趨勢都是一樣的,任何一個差值越大就說明越空閒。

在後續mongodb 最新版本中,去掉了部分重複統計的欄位,同時也增加了以下欄位,如下圖所示:

新版本增加的幾個統計項實際上和3.6.1 大同小異,只是把狀態機任務按照不通型別進行了更加詳細的統計。新版本中,更重要的一個功能就是 control 執行緒在發現執行緒池壓力過大的時候建立新執行緒的觸發情況也進行了統計,這樣我們就可以更加直觀的檢視動態建立的執行緒是因為什麼原因建立的。

 

8.  Mongodb-3.6 早期版本 control 執行緒動態調整動態增加執行緒缺陷 1

從步驟6 中可以看出, control 控制執行緒建立工作執行緒的第一個條件為:如果該執行緒超過 stuckThreadTimeout 閥值都沒有做執行緒壓力控制檢查,並且執行緒池中執行緒數全部在處理任務佇列中的任務,這種情況 control 執行緒一次性會建立 reservedThreads 個執行緒。 reservedThreads adaptiveServiceExecutorReservedThreads 配置,如果沒有配置,則採用初始值 CPU/2

那麼問題來了,如果我提前通過命令列配置了這個值,並且這個值配置的非常大,例如一百萬,這裡豈不是要建立一百萬個執行緒,這樣會造成作業系統負載升高,更容易引起耗盡系統pid 資訊,這會引起嚴重的系統級問題。

不過,不用擔心,最新版本的mongodb 程式碼,核心程式碼已經做了限制,這種情況下建立的執行緒數變為了 1 ,也就是這種情況只建立一個執行緒。

 

9.  adaptive 執行緒模型實時引數

動態執行緒模設計的時候,mongodb 設計者考慮到了不通應用場景的情況,因此在核心關鍵點增加了實時線上引數調整設定,主要包含如下 7 種引數,如下表所示:

引數名

作用

adaptiveServiceExecutorReservedThreads

預設執行緒池最少執行緒數

adaptiveServiceExecutorRunTimeMillis

工作執行緒從全域性佇列中獲取任務執行,如果佇列中沒有任務則需要等待,該配置就是限制等待時間的最大值

adaptiveServiceExecutorRunTimeJitterMillis

如果配置為 0 ,則任務入隊從佇列獲取任務等待時間則不需要新增一個隨機數

adaptiveServiceExecutorStuckThreadTimeoutMillis

保證 control 執行緒一次 while 迴圈操作 ( 迴圈體裡面判斷是否需要增加執行緒池中執行緒,如果發現執行緒池壓力大,則增加執行緒 ) 的時間為該配置的值

adaptiveServiceExecutorMaxQueueLatencyMicros

如果 control 執行緒一次迴圈的時間不到 adaptiveServiceExecutorStuckThreadTimeoutMillis ,則 do {} while() ,直到保證本次 while 迴圈達到需要的時間值。 {} 中就是簡單的 sleep sleep 的值就是本配置項的值。

adaptiveServiceExecutorIdlePctThreshold

單個執行緒迴圈從全域性佇列獲取 task 任務執行,同時在每次迴圈中會判斷該本工作執行緒的有效執行時間佔比,如果佔比小於該配置值,則本執行緒自動退出銷燬。

adaptiveServiceExecutorRecursionLimit

由於 adaptive 採用非同步 IO 操作,因此可能存線上程同時處理多個請求的情況,這時候我們就需要限制這個遞迴深度,如果深度過大,容易引起部分請求慢的情況。

    命令列實時引數調整方法如下,以adaptiveServiceExecutorReservedThreads 為例,其他引數調整方法類似:

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

    Mongodb 服務層的 adaptive 動態執行緒模型設計程式碼實現非常優秀,有很多實現細節針對不同應用場景做了極致優化, 鑑於篇幅,該模組的詳細原始碼實現過程將在《mongodb 核心原始碼實現及調優系列》相關章節詳細分析。

3. 不同執行緒模型效能多場景 PK

前面對執行緒模型進行了分析,下面針對Synchronous adaptive 兩種模型設計進行不同場景和不同緯度的測試,總結兩種模型各種的使用場景,並根據測試結果結合前面的理論分析得出不同場景下那種執行緒模型更合適。

測試緯度主要包括:併發數、請求快慢。本文的壓力測試工具採用sysbench 實現,以下是這幾種緯度的名稱定義:

併發數: 也就是sysbench 啟動的執行緒數,預設一個執行緒對應一個連結

請求快慢: 快請求就是請求返回比較快,sysbench lua 測試指令碼通過 read 同一條資料模擬快請求 ( 走儲存引擎快取 ) ,內部處理時延小於 1ms 。 慢請求也通過 sysbench 測試,測試指令碼做 range 操作,單次操作時延幾十 ms

sysbench 慢操作測試原理 : 首先寫 20000 萬資料到庫中,然後通過 range 操作測試, range 操作比較慢,慢操作啟動方式:

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

測試硬體資源,容器一臺,配置如下:

1.  CPU=32

2.  記憶體=64G

3.1 測試總結

上面的測試資料,彙總如下表:

測試場景

執行緒模式

測試結果

70 執行緒 + 快請求

Synchronous

tps( 包含異常請求 ):19.8W/s ,錯誤請求總數 :0 ,平均時延: 0.35ms  95 百分位時延 :0.57ms, 最大時延: 51ms

Adaptive

tps( 包含異常請求 ):18.1W/s ,錯誤請求總數 :0 ,平均時延: 0.38ms  95 百分位時延 :0.6ms, 最大時延: 41ms

500 執行緒 + 快請求

Synchronous

tps( 包含異常請求 ):19.5W/s ,錯誤請求總數 :0 ,平均時延: 2.53ms  95 百分位時延 :5.39ms, 最大時延: 4033ms

Adaptive

tps( 包含異常請求 ):18.2W/s ,錯誤請求總數 :0 ,平均時延: 2.7ms  95 百分位時延 :3.77ms, 最大時延: 1049ms

1000 執行緒 + 快請求

Synchronous

tps( 包含異常請求 ):18.4W/s ,錯誤請求總數 :4448/s ,有效請求 tps:17.9W/s ,平均時延: 5.41ms , 95 百分位時延 :20.58ms, 最大時延: 16595ms

Adaptive

tps( 包含異常請求 ):18.8W/s ,錯誤請求總數 :5000/s ,有效請求 tps:18.3W/s, 平均時延: 5.28ms , 95 百分位時延 :17.6ms, 最大時延: 4087ms

5000 執行緒 + 快請求

Synchronous

tps( 包含異常請求 ):18.2W/s ,錯誤請求總數 :7000/s ,有效請求 tps:17.5W/s ,平均時延: 27.3ms , 95 百分位時延 :44.1ms, 最大時延: 5043ms

Adaptive

tps( 包含異常請求 ):18.2W/s ,錯誤請求總數 :37000/s ,有效請求 tps:14.5W/s ,平均時延: 27.4ms , 95 百分位時延 :108ms, 最大時延: 22226ms

30000 執行緒 + 快請求

Synchronous

tps( 包含異常請求 ):21W/s ,錯誤請求總數 :140000/s ,有效請求 tps:6W/s ,平均時延: 139ms ,95 百分位時延 :805ms, 最大時延: 53775ms

Adaptive

tps( 包含異常請求 ):10W/s ,錯誤請求總數 :80/s ,有效請求 tps:10W/s ,平均時延: 195ms,  95 百分位時延 :985ms, 最大時延: 17030ms

30 執行緒 + 慢請求

Synchronous

tps( 包含異常請求 ):850/s ,錯誤請求總數 :0 ,平均時延: 35ms  95 百分位時延 :45ms, 最大時延: 92ms

Adaptive

tps( 包含異常請求 ):674/s ,錯誤請求總數 :0 ,平均時延: 44ms  95 百分位時延 :52ms, 最大時延: 132ms

500 執行緒 + 慢請求

 

Synchronous

tps( 包含異常請求 ):765/s ,錯誤請求總數 :0 ,平均時延: 652ms  95 百分位時延 :853ms, 最大時延: 2334ms

Adaptive

tps( 包含異常請求 ):783/s ,錯誤請求總數 :0 ,平均時延: 637ms  95 百分位時延 :696ms, 最大時延: 1847ms

1000 執行緒 + 慢請求

Synchronous

tps( 包含異常請求 ):2840/s ,錯誤請求總數 :2140/s ,有效請求 tps:700/s ,平均時延: 351ms  95 百分位時延 :1602ms, 最大時延: 6977ms

Adaptive

tps( 包含異常請求 ):3604/s ,錯誤請求總數 :2839/s ,有效請求 tps:800/s, 平均時延: 277ms  95 百分位時延 :1335ms, 最大時延: 6615ms

5000 執行緒 + 慢請求

Synchronous

tps( 包含異常請求 ):4535/s ,錯誤請求總數 :4000/s ,有效請求 tps:500/s ,平均時延: 1092ms  95 百分位時延 :8878ms, 最大時延: 25279ms

Adaptive

tps( 包含異常請求 ):4952/s ,錯誤請求總數 :4236/s ,有效請求 tps:700/s ,平均時延: 998ms  95 百分位時延 :7025ms, 最大時延: 16923ms

10000 執行緒 + 慢請求

Synchronous

tps( 包含異常請求 ):4720/s ,錯誤請求總數 :4240/s ,有效請求 tps:500/s ,平均時延: 2075ms  95 百分位時延 :19539ms, 最大時延: 63247ms

Adaptive

tps( 包含異常請求 ):8890/s ,錯誤請求總數 :8230/s ,有效請求 tps:650/s ,平均時延: 1101ms  95 百分位時延 :14226ms, 最大時延: 40895ms

20000 執行緒 + 慢請求

Synchronous

tps( 包含異常請求 ):7950/s ,錯誤請求總數 :7500/s ,有效請求 tps:450/s ,平均時延: 2413ms  95 百分位時延 :17812ms, 最大時延: 142752ms

Adaptive

tps( 包含異常請求 ):8800/s ,錯誤請求總數 :8130/s ,有效請求 tps:700/s ,平均時延: 2173ms  95 百分位時延 :27675ms, 最大時延: 57886ms

 

3.2 不同執行緒模型總結

     根據測試資料及其前面理論章節的分析,可以得出不同業務場景結論:

1.    低併發場景( 併發數小於 1000) Synchronous 執行緒模型效能更好。

2.    高併發場景( 併發數大於 5000) adaptive 動態執行緒模型效能更優。

3.   adaptive 動態執行緒模型, 95 分位時延和最大時延整體比 Synchronous 執行緒模型更優。

4.  併發越高,adaptive 相比 Synchronous 效能更好。

5.  併發越高,Synchronous 執行緒模型錯誤率相對更高。

6.  空閒連結越多,Synchronous 執行緒模型效能越差。 ( 由於時間問題,該場景未來得及測試,這是官方的資料總結 )

7.  此外,短連結場景( 例如 PHP 相關業務 ) adaptive 模型效能會更優,因為該模型不會有連結關閉引起的執行緒銷燬的開銷。

    

為什麼併發越高,adaptive 動態執行緒模型效能比 Synchronous 會更好,而併發低的時候反而更差,原因如下:

1.  Synchronous 模型,一個連結一個執行緒,併發越高,連結數就會越多,系統負載、記憶體消耗等就會更高。

2.  低併發場景下,連結數不多,Synchronous 模式執行緒數也不多,系統 CPU 排程幾乎不會受到影響,負載也影響不大。而在 adaptive 場景下,由於 asio 庫在設計的時候,任務放入全域性佇列 op_queue_ 中,工作執行緒每次獲取任務執行,都會有鎖競爭,因此在低併發場景下效能不及 adaptive 模式。      

3.3 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/reading-and-annotate-mongodb-3.6.1/blob/master/README.md

 

本文mongodb對應的sysbench程式碼目錄(該工具來自Percona,本文只是簡單做了改動):

https://github.com/y123456yz/reading-and-annotate-mongodb-3.6.1/tree/master/mongo/sysbench-mongodb

Sysbench-mongodb對應的lua指令碼目錄:

https://github.com/y123456yz/reading-and-annotate-mongodb-3.6.1/tree/master/mongo/sysbench-mongodb/sysbench/tests/mongodb

 


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69984922/viewspace-2729400/,如需轉載,請註明出處,否則將追究法律責任。

相關文章