本文來自OPPO互聯網基礎技術團隊,轉載請註名做者。同時歡迎關注咱們的公衆號:OPPO_tech,與你分享OPPO前沿互聯網技術及活動。
開源mongodb代碼規模數百萬行,本篇文章內容主要分析mongodb網絡傳輸模塊內部實現及其性能調優方法,學習網絡IO處理流程,體驗不一樣工做線程模型性能極致設計原理。另一個目的就是引導你們快速進行百萬級別規模源碼閱讀,作到不一樣大工程源碼」觸類旁通」快速閱讀的目的。react
此外,mognodb網絡工做線程模型設計很是好,不只很是值得數據庫相關研發人員學習,中間件、分佈式、高併發、服務端等相關研發人員也能夠借鑑,極力推薦你們學習。linux
Mongodb內核源碼由第三方庫third_party和mongodb服務層源碼組成,其中mongodb服務層代碼在不一樣模塊實現中依賴不一樣的third_party庫,第三方庫是mongodb服務層代碼實現的基礎(例如:網絡底層IO實現依賴asio-master庫, 底層存儲依賴wiredtiger存儲引擎庫),其中第三方庫也會依賴部分其餘庫(例如:wiredtiger庫依賴snappy算法庫,asio-master依賴boost庫)。c++
雖然Mongodb內核源碼數百萬行,工程量巨大,可是mongodb服務層代碼實現層次很是清晰,代碼目錄結構、類命名、函數命名、文件名命名都很是一目瞭然,充分體現了10gen團隊的專業精神。git
說明:mongodb內核除第三方庫third_party外的代碼,這裏統稱爲mongodb服務層代碼。github
本文以mongodb服務層transport實現爲例來講明如何快速閱讀整個mongodb代碼,咱們在走讀代碼前,建議遵循以下準則:算法
首先,咱們須要熟悉mongodb的基本功能,明白mongodb是作什麼用的,用在什麼地方,這樣才能體現mongodb的真正價值。此外,咱們須要提早搭建一個mongodb集羣玩一玩,這樣也能夠進一步促使咱們瞭解mongodb內部的一些經常使用基本功能。千萬不要急於求成,若是連mongodb是作什麼的都不知道,或者連mongodb的運維操做方法都沒玩過,直接讀取代碼會很是不適合,沒有目的的走讀代碼不利於分析整個代碼,同時閱讀代碼過程會很是痛苦。mongodb
熟悉了mongodb的基本功能,並搭建集羣簡單體驗後,咱們就能夠從github下載源碼,本身編譯源碼生成二進制文件,編譯文檔存放於docs/building.md 代碼目錄中,源碼編譯步驟以下:shell
在正在編譯代碼並運行的過程當中,發現如下兩個問題:數據庫
1)編譯出的二進制文件佔用空間很大,以下圖所示:segmentfault
從上圖能夠看出,經過strip處理工具處理後,二進制文件大小已經和官方二進制包大小同樣了。
2)在一些低版本操做系統運行的時候出錯,找不到對應stdlib庫,以下圖所示:
如上圖所示,當編譯出的二進制文件拷貝到線上運行後,發現沒法運行,提示libstdc庫找不到。緣由是咱們編譯代碼時候依賴的stdc庫版本比其餘操做系統上面的stdc庫版本更高,形成了不兼容。
解決辦法: 編譯的時候編譯腳本中帶上-static-libstdc++,把stdc庫經過靜態庫的方式進行編譯,而不是經過動態庫方式。
因爲前期咱們對代碼總體實現不熟悉,不知道各個接口的調用流程,這時候就能夠經過加日誌打印進行調試。Mongodb的日誌模塊設計的比較完善,從日誌中能夠很明確的看出由那個功能模塊打印日誌,同時日誌模塊有多種打印級別。
1)日誌打印級別設置
啓動參數中verbose設置日誌打印級別,日誌打印級別設置方法以下:
Mongod -f ./mongo.conf -vvvv
這裏的v越多,代表日誌打印級別設置的越低,也就會打印更多的日誌。一個v表示只會輸出LOG(1)日誌,-vv表示LOG(1) LOG(2)都會寫日誌。
2)如何在.cpp文件中使用日誌模塊記錄日誌
若是須要在一個新的.cpp文件中使用日誌模塊打印日誌,須要進行以下步驟操做:
例如: LogComponent::kExecutor表明executor模塊相關的日誌,參考log_component.cpp日誌模塊文件實現,對應到日誌文件內容以下:
Gdb是linux系統環境下優秀的代碼調試工具,支持設置斷點、單步調試、打印變量信息、獲取函數調用棧信息等功能。gdb工具能夠綁定某個線程進行線程級調試,因爲mongodb是多線程環境,所以在用gdb調試前,咱們須要肯定調試的線程號,mongod進程包含的線程號及其對應線程名查看方法以下:
注意: 在調試mongod工做線程處理流程的時候,不要選擇adaptive動態線程池模式,由於線程可能由於流量低引發工做線程不飽和而被銷燬,從而形成調試過程由於線程銷燬而中斷,synchronous線程模式是一個連接一個線程,只要咱們不關閉這個連接,線程就會一直存在,不會影響咱們理解mongodb服務層代碼實現邏輯。 synchronous線程模式調試的時候能夠經過mongo shell連接mongod服務端端口來模擬一個連接,所以調試過程相對比較可控。
在對工做線程調試的時候,發現gdb沒法查找到mongod進程的符號表,沒法進行各類gdb功能調試,以下圖所示:
上述gdb沒法attach到指定線程調試的緣由是沒法加載二進制文件符號表,這是由於編譯的時候沒有加上-g選項引發,mongodb經過SConstruct腳原本進行scons編譯,要啓編譯出新的二進制文件後,就能夠gdb調試了,以下圖所示,能夠很方便的定位到某個函數以前的調用棧信息,並進行單步、打印變量信息等調試:
在進行代碼閱讀前還有很重要的一步就是熟悉代碼目錄及文件命名實現,mongodb服務層代碼目錄結構及文件命名都有很嚴格的規範。下面以truansport網絡傳輸模塊爲例,transport模塊的具體目錄文件結構:
從上面的文件分佈內容,能夠清晰的看出,整個目錄中的源碼實現文件大致能夠分爲以下幾個部分:
經過上面的拆分,整個大的transport模塊實現就被拆分紅了7個小模塊,這7個小的子模塊各自負責對應功能實現,同時各個模塊相互銜接,總體實現網絡傳輸處理過程的總體實現,下面的章節將就這些子模塊進行簡單功能說明。
前面5個步驟事後,咱們已經熟悉了mongodb編譯調試以及transport模塊的各個子模塊的相關代碼文件實現及大致子模塊做用。至此,咱們能夠開始走讀代碼了,mongos和mongod的代碼入口分別在mongoSMain()和mongoDbMain(),從這兩個入口就能夠一步一步瞭解mongodb服務層代碼的總體實現。
注意: 走讀代碼前期不要深刻各類細節實現,大致瞭解代碼實現便可,先大致弄明白代碼中各個模塊功能由那些子模塊實現,千萬不要深究細節。
本章節主要給出了數百萬級mongodb內核代碼閱讀的一些建議,整個過程能夠總結爲以下幾點:
從1.5章節中,咱們把transport功能模塊細化拆分紅了網絡傳輸數據壓縮子模塊、服務入口子模塊、線程模型子模塊、狀態機處理子模塊、session會話信息子模塊、數據分發子模塊、套接字處理和傳輸管理子模塊,總共七個子模塊。
實際上mongodb服務層代碼的底層網絡IO實現依賴asio庫完成,所以transport功能模塊應該是7+1個子模塊構成,也就是服務層代碼實現由8個子模塊支持。
Asio是一個優秀網絡庫,依賴於boost庫的部分實現,支持linux、windos、unix等多平臺,mongodb基於asio庫來實現網絡IO及定時器處理。asio庫因爲爲了支持多平臺,在代碼實現中用了不少C++的模板,同時用了不少C++的新語法特性,所以總體代碼可讀性相比mongodb服務層代碼差不少。
服務端網絡IO異步處理流程大致以下:
服務端網絡IO同步方式處理流程和異步流程大同小異,少了epoll註冊和epoll事件通知過程,直接同步調用accept()、recv()、send()進行IO處理。
同步IO處理方式相對比較簡單,下面僅分析和mongodb服務層transport模塊結合比較緊密的asio異步IO實現原理。
Mongodb服務層用到的Asio庫功能中最重要的幾個結構有io_context、scheduler、epoll_reactor。Asio把網絡IO處理任務、狀態機調度任務作爲2種不一樣操做,分別由兩個繼承自operation的類結構管理,每種類型的操做也就是一個任務task。io_context、scheduler、epoll_reactor最重要的功能就是管理和調度這些task有序而且高效的運行。
io_context 上下文類是mongodb服務層和asio網絡庫交互的樞紐,是mongodb服務層和asio庫進行operation任務交互的入口。該類負責mongodb相關任務的入隊、出隊,並與scheduler調度處理類配合實現各類任務的高效率運行。Mongodb服務層在實現的時候,accept新鏈接任務使用_acceptorIOContext這個IO上下文成員實現,數據分發及其相應回調處理由_workerIOContext上下文成員實現。
該類的幾個核心接口功能以下表所示:
Io_context類成員/函數名 | 功能 | 備註說明 |
---|---|---|
impl_type& impl_; | Mongodb對應的type類型爲scheduler | 經過該成員來調用scheduler調度類的接口 |
io_context::run() | 負責accept對應異步回調處理 | 1.mongodb中該接口只針對accept對應IO異步處理 2.調用scheduler::run()進行accept異步讀操做 |
io_context::stop() | 中止IO調度處理 | 調用scheduler::stop()接口 |
io_context::run_one_until() | 1. 從全局隊列上獲取一個任務執行 2. 若是全局隊列爲空,則調用epoll_wait()獲取網絡IO事件處理 | 調用schedule::wait_one() |
io_context::post() | 任務入隊到全局隊列 | 調用scheduler::post_immediate_completion() |
io_context::dispatch() | 1.若是調用該接口的線程已經運行過全局隊列中的任務,則直接繼續由本線程運行該入隊的任務 2.若是不知足條件1條件,則直接入隊到全局隊列,等待調度執行 | 若是條件1知足,則直接由本線程執行 若是條件1不知足,則調用scheduler::do_dispatch () |
總結:
上一節的io_context上下文中提到mongodb操做的io上下文最終都會調用scheduler的幾個核心接口,io_context只是起銜接mongodb和asio庫的連接橋樑。scheduler類主要工做在於完成任務調度,該類和mongodb相關的幾個主要成員變量及接口以下表:
scheduler類主要成員/接口 | 功能 | 備註說明 |
---|---|---|
mutable mutex mutex_; | 互斥鎖,全局隊列訪問保護 | 多線程從全局隊列獲取任務的時候加鎖保護 |
op_queue<operation> op_queue_; | 全局任務隊列,全局任務和網絡事件相關任務都添加到該隊列 | 3.1.1中的5種類型的任務都入隊到了該全局隊列 |
bool stopped_; | 線程是否可調度標識 | 爲true後,將再也不處理epoll相關事件,參考scheduler::do_run_one |
event wakeup_event_; | 喚醒等待鎖得線程 | 實際event由信號量封裝 |
task_operation task_operation_; | 特殊的operation | 在鏈表中沒進行一次epoll獲取到IO任務加入全局隊列後,都會緊接着添加一個特殊operation |
reactor* task_; | 也就是epoll_reactor | 藉助epoll實現網絡事件異步處理 |
atomic_count outstanding_work_; | 套接字描述符個數 | accept獲取到的連接數fd個數+1(定時器fd) |
scheduler::run() | 循環處理epoll獲取到的accept事件信息 | 循環調用scheduler::do_run_one()接口 |
scheduler::do_dispatch() | 任務入隊 | 任務入隊到全局隊列op_queue_ |
scheduler::do_wait_one() | 任務出隊執行 | 若是隊列爲空則獲取epoll事件集對應的網絡IO任務放入全局op_queue_隊列 |
scheduler::restart() | 從新啓用調度 | 實際上就是修改stopped_標識爲false |
scheduler::stop_all_threads() | 中止調度 | 實際上就是修改stopped_標識爲true |
從前面的分析能夠看出,一個任務對應一個operation類結構,asio異步實現中schduler調度的任務分爲IO處理任務(accept處理、讀io處理、寫io處理、網絡IO處理回調處理)和全局狀態機任務,總共2種任務小類。
此外,asio還有一種特殊的operation,該Operastion什麼也不作,只是一個特殊標記。網絡IO處理任務、狀態機處理任務、特殊任務這三類任務分別對應三個類結構,分別是:reactor_op、completion_handler、task_operation_,這三個類都會繼承基類operation。
1. operation基類實現
operation基類實際上就是scheduler_operation類,經過typedef scheduler_operation operation指定,是其餘三個任務的父類,其主要實現接口以下:
operation類主要成員/接口 | 功能 | 備註說明 |
---|---|---|
unsigned int task_result_ | Epoll_wait獲取到的事件位圖信息記錄到該結構中 | 在descriptor_state::do_complete中取出位圖上的事件信息作底層IO讀寫處理 |
func_type func_; | 須要執行的任務 | |
scheduler_operation::complete() | 執行func_() | 任務的內容在func()中運行 |
2. completion_handler狀態機任務
當mongodb經過listener線程接受到一個新連接後,會生成一個狀態機調度任務,而後入隊到全局隊列op_queue_,worker線程從全局隊列獲取到該任務後調度執行,從而進入狀態機調度流程,在該流程中會觸發epoll相關得網絡IO註冊及異步IO處理。一個全局狀態機任務對應一個completion_handler類,該類主要成員及接口說明以下表所示:
completion_handler類主要成員/接口 | 功能 | 備註說明 |
---|---|---|
Handler handler_; | 全局狀態機任務函數 | 這個handler就至關於一個任務,其實是一個函數 |
completion_handler(Handler& h) | 構造初始化 | 啓用該任務,等待調度 |
completion_handler::do_complete() | 執行handler_回調 | 任務的內容在handler_()中運行 |
completion_handler狀態機任務類實現過程比較簡單,就是初始化和運行兩個接口。全局任務入隊的時候有兩種方式,一種是io_context::dispatch方式,另外一種是io_context::post。從前面章節對這兩個接口的代碼分析能夠看出,任務直接入隊到全局隊列op_queue_中,而後工做線程經過scheduler::do_wait_one從隊列獲取該任務執行。
注意: 狀態機任務入隊由Listener線程(新連接到來的初始狀態機任務)和工做線程(狀態轉換任務)共同完成,任務出隊調度執行由mongodb工做線程執行,狀態機具體任務內容在後面《狀態機實現》章節實現。
3. 網絡IO事件處理任務
網絡IO事件對應的Opration任務最終由reactor_op類實現,該類主要成員及接口以下:
reactor_op類主要成員/接口 | 功能 | 備註說明 |
---|---|---|
asio::error_code ec_; | 全局狀態機任務函數 | 這個handler就至關於一個任務,其實是一個函數 |
std::size_t bytes_transferred_; | 讀取或者發送的數據字節數 | Epoll_wait返回後獲取到對應的讀寫事件,而後進行數據分發操做 |
enum status; | 底層數據讀寫狀態 | 標識讀寫數據的狀態 |
perform_func_type perform_func_; | 底層IO操做的函數指針 | perform()中運行 |
status perform(); | 運行perform_func_函數 | perform實際上就是數據讀寫的底層實現 |
reactor_op(perform_func_type perform_func, func_type complete_func) | 類初始化 | 這裏有兩個func: 1. 底層數據讀寫實現的接口,也就是perform_func 2. 讀取或者發送一個完整mongodb報文的回調接口,也就是complete_func |
從reactor_op類能夠看出,該類的主要兩個函數成員:perform_func_和complete_func。其中perform_func_函數主要負責異步網絡IO底層處理,complete_func用於獲取到一個新連接、接收或者發送一個完整mongodb報文後的後續回調處理邏輯。
perform_func_具體功能包含以下三種以下:
針對上面的三個網絡IO處理功能,ASIO在實現的時候,分別經過三個不一樣的類(reactive_socket_accept_op_base、reactive_socket_recv_op_base、reactive_socket_send_op_base)實現,這三個類都繼承父類reactor_op。
這三個類的功能總結以下表所示:
類名 | 功能 | 說明 |
---|---|---|
reactive_socket_accept_op_base | 1. Accept()系統調用獲取新fd 2. 獲取到一個新fd後的mongodb層邏輯回調處理 | Accept()系統調用由perform_func()函數處理 獲取到新連接後的邏輯回調由complete_func執行 |
reactive_socket_recv_op_base | 1. 讀取一個完整mongodb報文讀取 2. 讀取完整報文後的mongodb服務層邏輯回調處理 | 從一個連接上讀取一個完整mongodb報文讀取由perform_func()函數處理 讀取完整報文後的mongodb服務層邏輯回調處理由complete_func執行 |
reactive_socket_send_op_base | 1. 發送一個完整的mongodb報文 2. 發送完一個完整mongodb報文後的mongodb服務層邏輯回調處理 | Accept()系統調用由perform_func()函數處理 獲取到新連接後的邏輯回調由complete_func執行 |
總結: asio在實現的時候,把accept處理、數據讀、數據寫分開處理,都繼承自公共基類reactor_op,該類由兩個操做組成:底層IO操做和回調處理。其中,asio的底層IO操做最終由epoll_reactor類實現,回調操做最終由mongodb服務層指定,底層IO操做的回調映射表以下:
底層IO操做類型 | Mongodb服務層回調 | 說明 |
---|---|---|
Accept(reactive_socket_accept_op_base) | ServiceEntryPointImpl::startSession,回調中進入狀態機任務流程 | Listener線程獲取到一個新連接後mongodb的回調處理 |
Recv(reactive_socket_recv_op_base) | ServiceStateMachine::_sourceCallback,回調中進入狀態機任務流程 | 接收一個完整mongodb報文的回調處理 |
Send(reactive_socket_send_op_base) | ServiceStateMachine::_sinkCallback,回調中進入狀態機任務流程 | 發送一個完整mongodb報文的回調處理 |
說明: 網絡IO事件處理任務實際上在狀態機任務內運行,也就是狀態機任務中調用asio庫進行底層IO事件運行處理。
4. 特殊任務task_operation
前面提到,ASIO庫中還包含一種特殊的task_operation任務,asio經過epoll_wait獲取到一批IO事件後,會添加到op_queue_全局隊列,工做線程從隊列取出任務有序執行。每次經過epoll_wait獲取到IO事件信息後,除了添加這些讀寫事件對應的底層IO處理任務到全局隊列外,每次還會額外生成一個特殊task_operation任務添加到隊列中。
爲什麼引入一個特殊任務的Opration?
工做線程變量全局op_queue_隊列取出任務執行,若是從隊列頭部取出的是特殊Op操做,就會立馬觸發獲取epoll網絡事件信息,避免底層網絡IO任務長時間不被處理引發的"飢餓"狀態,保證狀態機任務和底層IO任務都能」平衡」運行。
asio庫底層處理實際上由epoll_reactor類實現,該類主要負責epoll相關異步IO實現處理,鑑於篇幅epoll reactor相關實現將在後續《mongodb內核源碼實現及調優系列》相關章節詳細分析。
網絡傳輸數據壓縮子模塊主要用於減小網絡帶寬佔用,經過CPU來換取IO消耗,也就是以更多CPU消耗來減小網絡IO壓力。
鑑於篇幅,該模塊的詳細源碼實現過程將在《mongodb內核源碼實現及調優系列》相關章節分享。
transport_layer套接字處理及傳輸層管理子模塊功能主要以下:
鑑於篇幅,該模塊的詳細源碼實現過程將在《mongodb內核源碼實現及調優系列》相關章節詳細分析。
Session會話模塊功能主要以下:
鑑於篇幅,該模塊的詳細源碼實現過程將在《mongodb內核源碼實現及調優系列》相關章節詳細分析。
Ticket數據分發子模塊主要功能以下:
鑑於篇幅,該模塊的詳細源碼實現過程將在《mongodb內核源碼實現及調優系列》相關章節詳細分析。
service_state_machine狀態機處理模塊主要功能以下:
鑑於篇幅,該模塊的詳細源碼實現過程將在《mongodb內核源碼實現及調優系列》相關章節詳細分析。
service_entry_point服務入口點子模塊主要負責以下功能:
鑑於篇幅,該模塊的詳細源碼實現過程將在《mongodb內核源碼實現及調優系列》相關章節詳細分析。
線程模型設計在數據庫性能指標中起着很是重要的做用,所以本文將重點分析mongodb服務層線程模型設計,體驗mongodb如何經過優秀的工做線程模型來達到多種業務場景下的性能極致表現。
service_executor線程子模塊,在代碼實現中,把線程模型分爲兩種:synchronous線程模式和adaptive線程模型。Mongodb啓動的時候經過配置參數net.serviceExecutor來肯定採用那種線程模式運行mongo實例,配置方式以下:
net: //同步線程模式配置
serviceExecutor: synchronous
或者 //動態線程池模式配置
net:
serviceExecutor: synchronous
synchronous同步線程模型,listener線程每接收到一個連接就會建立一個線程,該連接上的全部數據讀寫及內部請求處理流程將一直由本線程負責,整個線程的生命週期就是這個連接的生命週期。
1. 網絡IO操做方式
synchronous同步線程模型實現過程比較簡單,線程循循環以同步IO操做方式從fd讀取數據,而後處理數據,最後返回客戶端對應得數據。同步線程模型方式針對某個連接的系統調用以下圖所示(mongo shell創建連接後show dbs):
2. 性能極致提高小細節
雖然synchronous線程模型比較簡單,可是mongodb服務層再實現的時候針對細節作了極致的優化,主要體如今以下代碼實現細節上面:
具體實現中,mongodb線程每處理16次用戶請求,就讓線程空閒一下子。同時,當總的工做線程數大於cpu核數後,每次都作讓出一次CPU調度。經過這兩種方式,在性能測試中能夠提高5%的性能,雖然提高性能很少,可是充分體現了mongodb在性能優化提高方面所作的努力。
3. 同步線程模型監控統計
能夠經過以下命令獲取同步線程模型方式獲取當前mongodb中的連接數信息:
該監控中主要由兩個字段組成:passthrough表明同步線程模式,threadsRunning表示當前進程的工做線程數。
adaptive動態線程池模型,內核實現的時候會根據當前系統的訪問負載動態的調整線程數。當線程CPU工做比較頻繁的時候,控制線程增長工做線程數;當線程CPU比較空閒後,本線程就會自動消耗退出。下面一塊兒體驗adaptive線程模式下,mongodb是如何作到性能極致設計的。
1. 線程池初始化
Mongodb默認初始化後,線程池線程數默認等於CPU核心數/2,主要實現以下:
從代碼實現能夠看出,線程池中最低線程數能夠經過adaptiveServiceExecutorReservedThreads配置,若是沒有配置則默認設置爲CPU/2。
2. 工做線程運行時間相關的幾個統計
3.6狀態機調度模塊中提到,一個完整的客戶端請求處理能夠轉換爲2個任務:經過asio庫接收一個完整mongodb報文、接收到報文後的後續全部處理(含報文解析、認證、引擎層處理、發送數據給客戶端等)。假設這兩個任務對應的任務名、運行時間分別以下表所示:
任務名 | 功能 | 運行時間 |
---|---|---|
Task1 | 調用底層asio庫接收一個完整mongodb報文 | T1 |
Task2 | 接收到報文後的後續全部處理(含報文解析、認證、引擎層處理、發送數據給客戶端等) | T2 |
客戶端一次完整請求過程當中,mongodb內部處理過程=task1 + task2,整個請求過程當中mongodb內部消耗的時間T1+T2。
實際上若是fd上沒有數據請求,則工做線程就會等待數據,等待數據的過程就至關於空閒時間,咱們把這個時間定義爲T3。因而一個工做線程總運行時間=內部任務處理時間+空閒等待時間,也就是線程總時間=T1+T2+T3,只是T3是無用等待時間。
3. 單個工做線程如何判斷本身處於」空閒」狀態
步驟2中提到,線程運行總時間=T1 + T2 +T3,其中T3是無用等待時間。若是T3的無用等待時間佔比很大,則說明線程比較空閒。
Mongodb工做線程每次運行完一次task任務後,都會判斷本線程的有效運行時間佔比,有效運行時間佔比=(T1+T2)/(T1+T2+T3),若是有效運行時間佔比小於某個閥值,則該線程自動退出銷燬,該閥值由adaptiveServiceExecutorIdlePctThreshold參數指定。該參數在線調整方式:
db.adminCommand( { setParameter: 1, adaptiveServiceExecutorIdlePctThreshold: 50} )
4. 如何判斷線程池中工做線程「太忙」
Mongodb服務層有個專門的控制線程用於判斷線程池中工做線程的壓力狀況,以此來決定是否在線程池中建立新的工做線程來提高性能。
控制線程每過必定時間循環檢查線程池中的線程壓力狀態,實現原理就是簡單的實時記錄線程池中的線程當前運行狀況,爲如下兩類計數:總線程數_threadsRunning、當前正在運行task任務的線程數_threadsInUse。若是_threadsRunning=_threadsRunning,說明全部工做線程當前都在處理task任務,這時候已經沒有多餘線程去asio庫中的全局任務隊列op_queue_中取任務執行了,這時候隊列中的任務就不會獲得及時的執行,就會成爲響應客戶端請求的瓶頸點。
5. 如何判斷線程池中全部線程比較「空閒」
control控制線程會在收集線程池中全部工做線程的有效運行時間佔比,若是佔比小於指定配置的閥值,則表明整個線程池空閒。
前面已經說明一個線程的有效時間佔比爲:(T1+T2)/(T1+T2+T3),那麼全部線程池中的線程總的有效時間佔比計算方式以下:
全部線程的總有效時間TT1 = (線程池中工做線程1的有效時間T1+T2) + (線程池中工做線程2的有效時間T1+T2) + ..... + (線程池中工做線程n的有效時間T1+T2)
全部線程總運行時間TT2 = (線程池中工做線程1的有效時間T1+T2+T3) + (線程池中工做線程2的有效時間T1+T2+T3) + ..... + (線程池中工做線程n的有效時間T1+T2+T3)
線程池中全部線程的總有效工做時間佔比 = TT1/TT2
6. control控制線程如何動態增長線程池中線程數
Mongodb在啓動初始化的時候,會建立一個線程名爲」worker-controller」的控制線程,該線程主要工做就是判斷線程池中是否有充足的工做線程來處理asio庫中全局隊列op_queue_中的task任務,若是發現線程池比較忙,沒有足夠的線程來處理隊列中的任務,則在線程池中動態增長線程來避免task任務在隊列上排隊等待。
control控制線程循環主體主要壓力判斷控制流程以下:
while { #等待工做線程喚醒條件變量,最長等待stuckThreadTimeout _scheduleCondition.wait_for(stuckThreadTimeout) #獲取線程池中全部線程最近一次運行任務的總有效時間TT1 Executing = _getThreadTimerTotal(ThreadTimer::Executing); #獲取線程池中全部線程最近一次運行任務的總運行時間TT2 Running = _getThreadTimerTotal(ThreadTimer::Running); #線程池中全部線程的總有效工做時間佔比 = TT1/TT2 utilizationPct = Executing / Running; #表明control線程過久沒有進行線程池壓力檢查了 if(本次循環到該行代碼的時間 > stuckThreadTimeout閥值) { #說明過久沒作壓力檢查,形成工做線程不夠用了 if(_threadsInUse == _threadsRunning) { #批量建立一批工做線程 for(; i < reservedThreads; i++) #建立工做線程 _startWorkerThread(); } #control線程繼續下一次循環壓力檢查 continue; } #若是當前線程池中總線程數小於最小線程數配置 #則建立一批線程,保證最少工做線程數達到要求 if (threadsRunning < reservedThreads) { while (_threadsRunning < reservedThreads) { _startWorkerThread(); } } #檢查上一次循環到本次循環這段時間範圍內線程池中線程的工做壓力 #若是壓力不大,則說明無需增長工做線程數,則繼續下一次循環 if (utilizationPct < idlePctThreshold) { continue; } #若是發現已經有線程建立起來了,可是這些線程尚未運行任務 #這說明當前可用線程數可能足夠了,咱們休息sleep_for會兒在判斷一下 #該循環最多持續stuckThreadTimeout時間 do { stdx::this_thread::sleep_for(); } while ((_threadsPending.load() > 0) && (sinceLastControlRound.sinceStart() < stuckThreadTimeout) #若是tasksQueued隊列中的任務數大於工做線程數,說明任務在排隊了 #該擴容線程池中線程了 if (_isStarved()) { _startWorkerThread(); } }
7. 實時serviceExecutorTaskStats線程模型統計信息
本文分析的mongodb版本爲3.6.1,其network.serviceExecutorTaskStats網絡線程模型相關統計經過db.serverStatus().network.serviceExecutorTaskStats能夠查看,以下圖所示:
上圖的幾個信息功能能夠分類爲三大類,說明以下:
大類類名 | 字段名 | 功能 |
無 | executor | Adaptive,說明是動態線程池模式 |
線程統計 | threadsInUse | 當前正在運行task任務的線程數 |
threadsRunning | 當前運行的線程數 | |
threadsPending | 當前建立起來,可是尚未執行過task任務的線程數 | |
隊列統計 | totalExecuted | 線程池運行成功的任務總數 |
tasksQueued | 入隊到全局隊列的任務數 | |
deferredTasksQueued | 等待接收網絡IO數據來讀取一個完整mongodb報文的任務數 | |
時間統計 | totalTimeRunningMicros | 全部工做線程運行總時間(含等待網絡IO的時間T1 + 讀一個mongodb報文任務的時間T2 + 一個請求後續處理的時間T3) |
totalTimeExecutingMicros | 也就是T2+T3,mongodb內部響應一個完整mongodb耗費的時間 | |
totalTimeQueuedMicros | 線程池中全部線程從建立到被用來執行第一個任務的等待時間 |
上表中各個字段的都有各自的意義,咱們須要注意這些參數的如下狀況:
上面三個大類中的整體反映趨勢都是同樣的,任何一個差值越大就說明越空閒。
在後續mongodb最新版本中,去掉了部分重複統計的字段,同時也增長了如下字段,以下圖所示:
新版本增長的幾個統計項實際上和3.6.1大同小異,只是把狀態機任務按照不通類型進行了更加詳細的統計。新版本中,更重要的一個功能就是control線程在發現線程池壓力過大的時候建立新線程的觸發狀況也進行了統計,這樣咱們就能夠更加直觀的查看動態建立的線程是由於什麼緣由建立的。
8. Mongodb-3.6早期版本control線程動態調整動態增長線程缺陷1例
從步驟6中能夠看出,control控制線程建立工做線程的第一個條件爲:若是該線程超過stuckThreadTimeout閥值都沒有作線程壓力控制檢查,而且線程池中線程數所有在處理任務隊列中的任務,這種狀況control線程一次性會建立reservedThreads個線程。reservedThreads由adaptiveServiceExecutorReservedThreads配置,若是沒有配置,則採用初始值CPU/2。
那麼問題來了,若是我提早經過命令行配置了這個值,而且這個值配置的很是大,例如一百萬,這裏豈不是要建立一百萬個線程,這樣會形成操做系統負載升高,更容易引發耗盡系統pid信息,這會引發嚴重的系統級問題。
不過,不用擔憂,最新版本的mongodb代碼,內核代碼已經作了限制,這種狀況下建立的線程數變爲了1,也就是這種狀況只建立一個線程。
9. adaptive線程模型實時參數
動態線程模設計的時候,mongodb設計者考慮到了不通應用場景的狀況,所以在覈心關鍵點增長了實時在線參數調整設置,主要包含以下7種參數,以下表所示:
參數名 | 做用 |
---|---|
adaptiveServiceExecutorReservedThreads | 默認線程池最少線程數 |
adaptiveServiceExecutorRunTimeMillis | 工做線程從全局隊列中獲取任務執行,若是隊列中沒有任務則須要等待,該配置就是限制等待時間的最大值 |
adaptiveServiceExecutorRunTimeJitterMillis | 若是配置爲0,則任務入隊從隊列獲取任務等待時間則不須要添加一個隨機數 |
adaptiveServiceExecutorStuckThreadTimeoutMillis | 保證control線程一次while循環操做(循環體裏面判斷是否須要增長線程池中線程,若是發現線程池壓力大,則增長線程)的時間爲該配置的值 |
adaptiveServiceExecutorMaxQueueLatencyMicros | 若是control線程一次循環的時間不到adaptiveServiceExecutorStuckThreadTimeoutMillis,則do {} while(),直到保證本次while循環達到須要的時間值。 {}中就是簡單的sleep,sleep的值就是本配置項的值。 |
adaptiveServiceExecutorIdlePctThreshold | 單個線程循環從全局隊列獲取task任務執行,同時在每次循環中會判斷該本工做線程的有效運行時間佔比,若是佔比小於該配置值,則本線程自動退出銷燬。 |
adaptiveServiceExecutorRecursionLimit | 因爲adaptive採用異步IO操做,所以可能存在線程同時處理多個請求的狀況,這時候咱們就須要限制這個遞歸深度,若是深度過大,容易引發部分請求慢的狀況。 |
命令行實時參數調整方法以下,以adaptiveServiceExecutorReservedThreads爲例,其餘參數調整方法相似:
db.adminCommand( { setParameter: 1, adaptiveServiceExecutorReservedThreads: xx} )
Mongodb服務層的adaptive動態線程模型設計代碼實現很是優秀,有不少實現細節針對不一樣應用場景作了極致優化,鑑於篇幅,該模塊的詳細源碼實現過程將在《mongodb內核源碼實現及調優系列》相關章節詳細分析。
前面對線程模型進行了分析,下面針對Synchronous和adaptive兩種模型設計進行不一樣場景和不一樣緯度的測試,總結兩種模型各類的使用場景,並根據測試結果結合前面的理論分析得出不一樣場景下那種線程模型更合適。
測試緯度主要包括:併發數、請求快慢。本文的壓力測試工具採用sysbench實現,如下是這幾種緯度的名稱定義:
併發數: 也就是sysbench啓動的線程數,默認一個線程對應一個連接
請求快慢: 快請求就是請求返回比較快,sysbench的lua測試腳本經過read同一條數據模擬快請求(走存儲引擎緩存),內部處理時延小於1ms。 慢請求也經過sysbench測試,測試腳本作range操做,單次操做時延幾十ms。
sysbench慢操做測試原理: 首先寫20000萬數據到庫中,而後經過range操做測試,range操做比較慢,慢操做啓動方式:
./sysbench --mongo-write-concern=1 --mongo-url="mongodb://xxx" --mongo-database-name=sbtest11 --oltp_table_size=600 --rand-type=pareto --report-interval=2 --max-requests=0 --max-time=200 --test=./tests/mongodb/ranges_ro.lua --oltp_range_size=2000 --num-threads=xx run
測試硬件資源,容器一臺,配置以下:
Sysbench併發線程數70測試結果以下圖所示(上圖爲adaptive模式,下圖爲Synchronousm線程模式):
Sysbench併發線程數500測試結果以下圖所示(上圖爲adaptive模式,下圖爲Synchronousm線程模式):
Sysbench併發線程數1000測試結果以下圖所示(上圖爲adaptive模式,下圖爲Synchronousm線程模式):
Sysbench併發線程數30測試結果以下圖所示(上圖爲adaptive模式,下圖爲Synchronousm線程模式):
Sysbench併發線程數500測試結果以下圖所示(上圖爲adaptive模式,下圖爲Synchronousm線程模式):
Sysbench併發線程數1000測試結果以下圖所示(上圖爲adaptive模式,下圖爲Synchronousm線程模式):
Sysbench併發線程數5000測試結果以下圖所示(上圖爲adaptive模式,下圖爲Synchronousm線程模式):
Sysbench併發線程數10000測試結果以下圖所示(上圖爲adaptive模式,下圖爲Synchronousm線程模式):
測試中發現30000併發的時候synchronousm模式實際成功的鏈接數爲24000,以下圖所示:
爲了測試相同併發數的真實數據對比,所以把adaptive模式的測試併發線程數調整爲24000測試,同時提早把adaptive作以下最低線程數調整:
db.adminCommand( { setParameter: 1, adaptiveServiceExecutorReservedThreads: 120} )
兩種測試數據結果以下(左圖爲adaptive模式,右圖爲Synchronousm線程模式):
Sysbench併發線程數5000測試結果以下圖所示(上圖爲adaptive模式,下圖爲Synchronousm線程模式):
Sysbench併發線程數10000測試結果以下圖所示(上圖爲adaptive模式,下圖爲Synchronousm線程模式):
Sysbench併發線程數20000測試結果以下圖所示(上圖爲adaptive模式,下圖爲Synchronousm線程模式):
上面的測試數據,彙總以下表:
測試場景 | 線程模式 | 測試結果 |
70線程+快請求 | Synchronous | 總tps(包含異常請求):19.8W/s,錯誤請求總數:0,平均時延:0.35ms 95百分位時延:0.57ms,最大時延:51ms |
Adaptive | 總tps(包含異常請求):18.1W/s,錯誤請求總數:0,平均時延:0.38ms 95百分位時延:0.6ms,最大時延:41ms | |
500線程+快請求 | Synchronous | 總tps(包含異常請求):19.5W/s,錯誤請求總數:0,平均時延:2.53ms 95百分位時延:5.39ms,最大時延:4033ms |
Adaptive | 總tps(包含異常請求):18.2W/s,錯誤請求總數:0,平均時延:2.7ms 95百分位時延:3.77ms,最大時延:1049ms | |
1000線程+快請求 | Synchronous | 總tps(包含異常請求):18.4W/s,錯誤請求總數:4448/s,有效請求tps:17.9W/s,平均時延:5.41ms , 95百分位時延:20.58ms,最大時延:16595ms |
Adaptive | 總tps(包含異常請求):18.8W/s,錯誤請求總數:5000/s,有效請求tps:18.3W/s, 平均時延:5.28ms , 95百分位時延:17.6ms,最大時延:4087ms | |
5000線程+快請求 | Synchronous | 總tps(包含異常請求):18.2W/s,錯誤請求總數:7000/s,有效請求tps:17.5W/s,平均時延:27.3ms , 95百分位時延:44.1ms,最大時延:5043ms |
Adaptive | 總tps(包含異常請求):18.2W/s,錯誤請求總數:37000/s,有效請求tps:14.5W/s,平均時延:27.4ms , 95百分位時延:108ms,最大時延:22226ms | |
30000線程+快請求 | Synchronous | 總tps(包含異常請求):21W/s,錯誤請求總數:140000/s,有效請求tps:6W/s,平均時延:139ms ,95百分位時延:805ms,最大時延:53775ms |
Adaptive | 總tps(包含異常請求):10W/s,錯誤請求總數:80/s,有效請求tps:10W/s,平均時延:195ms, 95百分位時延:985ms,最大時延:17030ms | |
30線程+慢請求 | Synchronous | 總tps(包含異常請求):850/s,錯誤請求總數:0,平均時延:35ms 95百分位時延:45ms,最大時延:92ms |
Adaptive | 總tps(包含異常請求):674/s,錯誤請求總數:0,平均時延:44ms 95百分位時延:52ms,最大時延:132ms | |
500線程+慢請求 | Synchronous | 總tps(包含異常請求):765/s,錯誤請求總數:0,平均時延:652ms 95百分位時延:853ms,最大時延:2334ms |
Adaptive | 總tps(包含異常請求):783/s,錯誤請求總數:0,平均時延:637ms 95百分位時延:696ms,最大時延:1847ms | |
1000線程+慢請求 | Synchronous | 總tps(包含異常請求):2840/s,錯誤請求總數:2140/s,有效請求tps:700/s,平均時延:351ms 95百分位時延:1602ms,最大時延:6977ms |
Adaptive | 總tps(包含異常請求):3604/s,錯誤請求總數:2839/s,有效請求tps:800/s, 平均時延:277ms 95百分位時延:1335ms,最大時延:6615ms | |
5000線程+慢請求 | Synchronous | 總tps(包含異常請求):4535/s,錯誤請求總數:4000/s,有效請求tps:500/s,平均時延:1092ms 95百分位時延:8878ms,最大時延:25279ms |
Adaptive | 總tps(包含異常請求):4952/s,錯誤請求總數:4236/s,有效請求tps:700/s,平均時延:998ms 95百分位時延:7025ms,最大時延:16923ms | |
10000線程+慢請求 | Synchronous | 總tps(包含異常請求):4720/s,錯誤請求總數:4240/s,有效請求tps:500/s,平均時延:2075ms 95百分位時延:19539ms,最大時延:63247ms |
Adaptive | 總tps(包含異常請求):8890/s,錯誤請求總數:8230/s,有效請求tps:650/s,平均時延:1101ms 95百分位時延:14226ms,最大時延:40895ms | |
20000線程+慢請求 | Synchronous | 總tps(包含異常請求):7950/s,錯誤請求總數:7500/s,有效請求tps:450/s,平均時延:2413ms 95百分位時延:17812ms,最大時延:142752ms |
Adaptive | 總tps(包含異常請求):8800/s,錯誤請求總數:8130/s,有效請求tps:700/s,平均時延:2173ms 95百分位時延:27675ms,最大時延:57886ms | |
根據測試數據及其前面理論章節的分析,能夠得出不一樣業務場景結論:
爲何併發越高,adaptive動態線程模型性能比Synchronous會更好,而併發低的時候反而更差,緣由以下:
前面3.6.2章節講了adaptive線程模型的工做原理,其中有8個參數供咱們對線程池運行狀態進行調優。大致總結以下:
參數名 | 做用 |
---|---|
adaptiveServiceExecutorReservedThreads | 若是業務場景是針對相似整點推送、電商按期搶購等超大流量衝擊的場景,能夠適當的調高該值,避免衝擊瞬間線程池不夠用引發的任務排隊、瞬間建立大量線程、時延過大的狀況 |
adaptiveServiceExecutorRunTimeMillis | 不建議調整 |
adaptiveServiceExecutorRunTimeJitterMillis | 不建議調整 |
adaptiveServiceExecutorStuckThreadTimeoutMillis | 能夠適當調小該值,減小control控制線程休眠時間,從而能夠更快的檢測到線程池中工做線程數是否夠用 |
adaptiveServiceExecutorMaxQueueLatencyMicros | 不建議調整 |
adaptiveServiceExecutorIdlePctThreshold | 若是流量是波浪形形式,例如上一秒tps=10萬/S,下一秒降爲幾十,甚至跌0的狀況,能夠考慮調小該值,避免流量瞬間降低引發的線程瞬間批量消耗及流量上升後的大量線程建立 |
adaptiveServiceExecutorRecursionLimit | 不建議調整 |
前面的分析能夠看出adaptive動態線程模型,爲了獲取全局任務隊列op_queue_上的任務,須要進行全局鎖競爭,這其實是整個線程池從隊列獲取任務運行最大的一個瓶頸。
優化思路: 咱們能夠經過優化隊列和鎖來提高總體性能,當前的隊列只有一個,咱們能夠把單個隊列調整爲多個隊列,每一個隊列一把鎖,任務入隊的時候散列到多個隊列,經過該優化,鎖競爭及排隊將會獲得極大的改善。
優化前隊列架構:
優化後隊列架構:
如上圖,把一個全局隊列拆分爲多個隊列,任務入隊的時候按照hash散列到各自的隊列,工做線程獲取獲取任務的時候,同理經過hash的方式去對應的隊列獲取任務,經過這種方式減小鎖競爭,同時提高總體性能。
鑑於篇幅,transport模塊的詳細源碼實現過程將在《mongodb內核源碼實現及調優系列》相關章節詳細分析。
網絡傳輸各個子模塊及Asio庫源碼詳細註釋詳見:
https://github.com/y123456yz/...
本文mongodb對應的sysbench代碼目錄(該工具來自Percona,本文只是簡單作了改動):
https://github.com/y123456yz/...
Sysbench-mongodb對應的lua腳本目錄:
https://github.com/y123456yz/...
歡迎加入OPPO互聯網數據庫團隊,一塊兒參與公司千萬級峯值tps/萬億級數據量文檔數據庫研發工做,想加入咱們,請聯繫郵箱:yangyazhou#oppo.com