萬字長文 | MongoDB絡傳輸處理原始碼實現及效能調優

OPPO網際網路技術發表於2020-06-19
本文來自OPPO網際網路基礎技術團隊,轉載請註名作者。同時歡迎關注我們的公眾號:OPPO_tech,與你分享OPPO前沿網際網路技術及活動。

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

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

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

Mongodb核心原始碼由第三方庫third_party和mongodb服務層原始碼組成,其中mongodb服務層程式碼在不同模組實現中依賴不同的third_party庫,第三方庫是mongodb服務層程式碼實現的基礎(例如:網路底層IO實現依賴asio-master庫, 底層儲存依賴wiredtiger儲存引擎庫),其中第三方庫也會依賴部分其他庫(例如:wiredtiger庫依賴snappy演算法庫,asio-master依賴boost庫)。

雖然Mongodb核心原始碼數百萬行,工程量巨大,但是mongodb服務層程式碼實現層次非常清晰,程式碼目錄結構、類命名、函式命名、檔名命名都非常一目瞭然,充分體現了10gen團隊的專業精神。

說明:mongodb核心除第三方庫third_party外的程式碼,這裡統稱為mongodb服務層程式碼。

本文以mongodb服務層transport實現為例來說明如何快速閱讀整個mongodb程式碼,我們在走讀程式碼前,建議遵循如下準則:

1.1 熟悉mongodb基本功能和使用方法

首先,我們需要熟悉mongodb的基本功能,明白mongodb是做什麼用的,用在什麼地方,這樣才能體現mongodb的真正價值。此外,我們需要提前搭建一個mongodb叢集玩一玩,這樣也可以進一步促使我們瞭解mongodb內部的一些常用基本功能。千萬不要急於求成,如果連mongodb是做什麼的都不知道,或者連mongodb的運維操作方法都沒玩過,直接讀取程式碼會非常不適合,沒有目的的走讀程式碼不利於分析整個程式碼,同時閱讀程式碼過程會非常痛苦。

1.2 下載程式碼編譯原始碼

熟悉了mongodb的基本功能,並搭建叢集簡單體驗後,我們就可以從github下載原始碼,自己編譯原始碼生成二進位制檔案,編譯文件存放於docs/building.md 程式碼目錄中,原始碼編譯步驟如下:

  1. 下載對應releases中對應版本的原始碼
  2. 進入對於目錄,參考docs/building.md檔案內容進行相關依賴工具安裝
  3. 執行buildscripts/scons.py編譯出對應二進位制檔案,也可以直接scons mongod mongos這樣編譯。
  4. 編譯成功後的生產可執行檔案存放於./build/opt/mongo/目錄

在正在編譯程式碼並執行的過程中,發現以下兩個問題:

1)編譯出的二進位制檔案佔用空間很大,如下圖所示:

從上圖可以看出,通過strip處理工具處理後,二進位制檔案大小已經和官方二進位制包大小一樣了。

2)在一些低版本作業系統執行的時候出錯,找不到對應stdlib庫,如下圖所示:

如上圖所示,當編譯出的二進位制檔案拷貝到線上執行後,發現無法執行,提示libstdc庫找不到。原因是我們編譯程式碼時候依賴的stdc庫版本比其他作業系統上面的stdc庫版本更高,造成了不相容。

解決辦法: 編譯的時候編譯指令碼中帶上-static-libstdc++,把stdc庫通過靜態庫的方式進行編譯,而不是通過動態庫方式。

1.3 瞭解程式碼日誌模組使用方法,試著加列印除錯

由於前期我們對程式碼整體實現不熟悉,不知道各個介面的呼叫流程,這時候就可以通過加日誌列印進行除錯。Mongodb的日誌模組設計的比較完善,從日誌中可以很明確的看出由那個功能模組列印日誌,同時日誌模組有多種列印級別。

1)日誌列印級別設定

啟動引數中verbose設定日誌列印級別,日誌列印級別設定方法如下:

Mongod -f ./mongo.conf -vvvv

這裡的v越多,表明日誌列印級別設定的越低,也就會列印更多的日誌。一個v表示只會輸出LOG(1)日誌,-vv表示LOG(1) LOG(2)都會寫日誌。

2)如何在.cpp檔案中使用日誌模組記錄日誌

如果需要在一個新的.cpp檔案中使用日誌模組列印日誌,需要進行如下步驟操作:

  1. 新增巨集定義 #define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kExecutor
  2. 使用LOG(N)或者log()來記錄想要輸出的日誌內容,其中LOG(N)的N代表日誌列印級別,log()對應的日誌全記錄到檔案。

例如: LogComponent::kExecutor代表executor模組相關的日誌,參考log_component.cpp日誌模組檔案實現,對應到日誌檔案內容如下:

1.4 學會用gdb除錯mongodb程式碼

Gdb是linux系統環境下優秀的程式碼除錯工具,支援設定斷點、單步除錯、列印變數資訊、獲取函式呼叫棧資訊等功能。gdb工具可以繫結某個執行緒進行執行緒級除錯,由於mongodb是多執行緒環境,因此在用gdb除錯前,我們需要確定除錯的執行緒號,mongod程式包含的執行緒號及其對應執行緒名檢視方法如下:

注意: 在除錯mongod工作執行緒處理流程的時候,不要選擇adaptive動態執行緒池模式,因為執行緒可能因為流量低引起工作執行緒不飽和而被銷燬,從而造成除錯過程因為執行緒銷燬而中斷,synchronous執行緒模式是一個連結一個執行緒,只要我們不關閉這個連結,執行緒就會一直存在,不會影響我們理解mongodb服務層程式碼實現邏輯。 synchronous執行緒模式除錯的時候可以通過mongo shell連結mongod服務端埠來模擬一個連結,因此除錯過程相對比較可控。

在對工作執行緒除錯的時候,發現gdb無法查詢到mongod程式的符號表,無法進行各種gdb功能除錯,如下圖所示:

上述gdb無法attach到指定執行緒除錯的原因是無法載入二進位制檔案符號表,這是因為編譯的時候沒有加上-g選項引起,mongodb通過SConstruct指令碼來進行scons編譯,要啟編譯出新的二進位制檔案後,就可以gdb除錯了,如下圖所示,可以很方便的定位到某個函式之前的呼叫棧資訊,並進行單步、列印變數資訊等除錯:

1.5 熟悉程式碼目錄結構、模組細化拆分

在進行程式碼閱讀前還有很重要的一步就是熟悉程式碼目錄及檔案命名實現,mongodb服務層程式碼目錄結構及檔案命名都有很嚴格的規範。下面以truansport網路傳輸模組為例,transport模組的具體目錄檔案結構:

從上面的檔案分佈內容,可以清晰的看出,整個目錄中的原始碼實現檔案大體可以分為如下幾個部分:

  1. message_compressor_*網路傳輸資料壓縮子模組
  2. service_entry_point*服務入口點子模組
  3. service_executor*服務執行子模組,即執行緒模型子模組
  4. service_state_machine*服務狀態機處理子模組
  5. Session*回話資訊子模組
  6. Ticket*資料分發子模組
  7. transport_layer*套接字處理及傳輸層模式管理子模組

通過上面的拆分,整個大的transport模組實現就被拆分成了7個小模組,這7個小的子模組各自負責對應功能實現,同時各個模組相互銜接,整體實現網路傳輸處理過程的整體實現,下面的章節將就這些子模組進行簡單功能說明。

1.6 從main入口開始大體走讀程式碼

前面5個步驟過後,我們已經熟悉了mongodb編譯除錯以及transport模組的各個子模組的相關程式碼檔案實現及大體子模組作用。至此,我們可以開始走讀程式碼了,mongos和mongod的程式碼入口分別在mongoSMain()和mongoDbMain(),從這兩個入口就可以一步一步瞭解mongodb服務層程式碼的整體實現。

注意: 走讀程式碼前期不要深入各種細節實現,大體瞭解程式碼實現即可,先大體弄明白程式碼中各個模組功能由那些子模組實現,千萬不要深究細節。

1.7 總結

本章節主要給出了數百萬級mongodb核心程式碼閱讀的一些建議,整個過程可以總結為如下幾點:

  1. 提前瞭解mongodb的作用及工作原理。
  2. 自己搭建叢集提前學習下mongodb叢集的常用運維操作,可以進一步幫助理解mongodb的功能特性,提升後期程式碼閱讀的效率。
  3. 自己下載原始碼編譯二進位制可執行檔案,同時學會使用日誌模組,通過加日誌列印的方式逐步開始除錯。
  4. 學習使用gdb程式碼除錯工具除錯執行緒的執行流程,這樣可以更進一步的促使快速學習程式碼處理流程,特別是一些複雜邏輯,可以大大提升走讀程式碼的效率。
  5. 正式走讀程式碼前,提前瞭解各個模組的程式碼目錄結構,把一個大模組拆分成各個小模組,先大體瀏覽各個模組的程式碼實現。
  6. 前期走讀程式碼千萬不要深入細節,捋清楚各個模組的大體功能作用後再開始一步一步的深入細節,瞭解深層次的內部實現。
  7. 從main()入口逐步開始走讀程式碼,結合log日誌列印和gdb除錯。
  8. 跳過整體流程中不熟悉的模組程式碼,只走讀本次想弄明白的模組程式碼實現。

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()中執行

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_具體功能包含如下三種如下:

  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: synchronous

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

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

1. 網路IO操作方式

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

2. 效能極致提升小細節

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


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

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

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

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

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

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

1. 執行緒池初始化

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

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

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

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

任務名 功能 執行時間
Task1 呼叫底層asio庫接收一個完整mongodb報文 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 執行緒池中所有執行緒從建立到被用來執行第一個任務的等待時間

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

  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 場景一、低併發場景+快請求測試

Sysbench併發執行緒數70測試結果如下圖所示(上圖為adaptive模式,下圖為Synchronousm執行緒模式):

Sysbench併發執行緒數500測試結果如下圖所示(上圖為adaptive模式,下圖為Synchronousm執行緒模式):

Sysbench併發執行緒數1000測試結果如下圖所示(上圖為adaptive模式,下圖為Synchronousm執行緒模式):

3.2 場景二、低併發場景+慢請求測試

Sysbench併發執行緒數30測試結果如下圖所示(上圖為adaptive模式,下圖為Synchronousm執行緒模式):

Sysbench併發執行緒數500測試結果如下圖所示(上圖為adaptive模式,下圖為Synchronousm執行緒模式):

Sysbench併發執行緒數1000測試結果如下圖所示(上圖為adaptive模式,下圖為Synchronousm執行緒模式):

3.3 場景三、高併發場景+快請求測試

Sysbench併發執行緒數5000測試結果如下圖所示(上圖為adaptive模式,下圖為Synchronousm執行緒模式):

Sysbench併發執行緒數10000測試結果如下圖所示(上圖為adaptive模式,下圖為Synchronousm執行緒模式):

測試中發現30000併發的時候synchronousm模式實際成功的連線數為24000,如下圖所示:

為了測試相同併發數的真實資料對比,因此把adaptive模式的測試併發執行緒數調整為24000測試,同時提前把adaptive做如下最低執行緒數調整:

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

兩種測試資料結果如下(左圖為adaptive模式,右圖為Synchronousm執行緒模式):

3.4 場景四、高併發場景+慢請求測試

Sysbench併發執行緒數5000測試結果如下圖所示(上圖為adaptive模式,下圖為Synchronousm執行緒模式):

Sysbench併發執行緒數10000測試結果如下圖所示(上圖為adaptive模式,下圖為Synchronousm執行緒模式):


Sysbench併發執行緒數20000測試結果如下圖所示(上圖為adaptive模式,下圖為Synchronousm執行緒模式):

3.5 測試總結

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

測試場景 執行緒模式 測試結果
70執行緒+快請求 Synchronous 總tps(包含異常請求):19.8W/s,錯誤請求總數:0,平均時延:0.35ms 95百分位時延:0.57ms,最大時延: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.6 不同執行緒模型總結

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

  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.7 adaptive動態執行緒模式線上調優實踐總結

前面3.6.2章節講了adaptive執行緒模型的工作原理,其中有8個引數供我們對執行緒池執行狀態進行調優。大體總結如下:

引數名 作用
adaptiveServiceExecutorReservedThreads 如果業務場景是針對類似整點推送、電商定期搶購等超大流量衝擊的場景,可以適當的調高該值,避免衝擊瞬間執行緒池不夠用引起的任務排隊、瞬間建立大量執行緒、時延過大的情況
adaptiveServiceExecutorRunTimeMillis 不建議調整
adaptiveServiceExecutorRunTimeJitterMillis 不建議調整
adaptiveServiceExecutorStuckThreadTimeoutMillis 可以適當調小該值,減少control控制執行緒休眠時間,從而可以更快的檢測到執行緒池中工作執行緒數是否夠用
adaptiveServiceExecutorMaxQueueLatencyMicros 不建議調整
adaptiveServiceExecutorIdlePctThreshold 如果流量是波浪形形式,例如上一秒tps=10萬/S,下一秒降為幾十,甚至跌0的情況,可以考慮調小該值,避免流量瞬間下降引起的執行緒瞬間批量消耗及流量上升後的大量執行緒建立
adaptiveServiceExecutorRecursionLimit 不建議調整

4. Asio網路庫全域性佇列鎖優化,效能進一步提升

前面的分析可以看出adaptive動態執行緒模型,為了獲取全域性任務佇列op_queue_上的任務,需要進行全域性鎖競爭,這實際上是整個執行緒池從佇列獲取任務執行最大的一個瓶頸。

優化思路: 我們可以通過優化佇列和鎖來提升整體效能,當前的佇列只有一個,我們可以把單個佇列調整為多個佇列,每個佇列一把鎖,任務入隊的時候雜湊到多個佇列,通過該優化,鎖競爭及排隊將會得到極大的改善。

優化前佇列架構:

優化後佇列架構:

如上圖,把一個全域性佇列拆分為多個佇列,任務入隊的時候按照hash雜湊到各自的佇列,工作執行緒獲取獲取任務的時候,同理通過hash的方式去對應的佇列獲取任務,通過這種方式減少鎖競爭,同時提升整體效能。

5. 網路傳輸模組原始碼詳細註釋

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

網路傳輸各個子模組及Asio庫原始碼詳細註釋詳見:

https://github.com/y123456yz/...

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

https://github.com/y123456yz/...

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

https://github.com/y123456yz/...

最後

歡迎加入OPPO網際網路資料庫團隊,一起參與公司千萬級峰值tps/萬億級資料量文件資料庫研發工作,想加入我們,請聯絡郵箱:yangyazhou#oppo.com

相關文章