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

y123456yzzyz發表於2020-10-23

關於作者

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

序言

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 熟悉 mongodb 基本功能和使用方法

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

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 庫透過靜態庫的方式進行編譯,而不是透過動態庫方式。

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

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

1. 日誌列印級別設定

啟動引數中verbose 設定日誌列印級別,日誌列印級別設定方法如下: Mongod -f ./mongo.conf -vvvv    

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

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

i) 新增宏定義 #define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kExecutor

ii) 使用 LOG(N) 或者 log() 來記錄想要輸出的日誌內容,其中 LOG(N) N 代表日誌列印級別, log() 對應的日誌全記錄到檔案。

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


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 功能需要在 scons 編譯程式碼的時候指定 gdbserver 選項 :scons --gdbserver=GDBSERVER -j 2

編譯出新的二進位制檔案後,就可以gdb 除錯了,如下圖所示,可以很方便的定位到某個函式之前的呼叫棧資訊,並進行單步、列印變數資訊等除錯:

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 個小的子模組各自負責對應功能實現,同時各個模組相互銜接,整體實現網路傳輸處理過程的整體實現,下面的章節將就這些子模組進行簡單功能說明。

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

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

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

7 總結

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

1.  提前瞭解mongodb 的作用及工作原理。

2.  自己搭建叢集提前學習下mongodb 叢集的常用運維操作,可以進一步幫助理解 mongodb 的功能特性,提升後期程式碼閱讀的效率。

3.  自己下載原始碼編譯二進位制可執行檔案,同時學會使用日誌模組,透過加日誌列印的方式逐步開始除錯。

4.  學習使用gdb 程式碼除錯工具除錯執行緒的執行流程,這樣可以更進一步的促使快速學習程式碼處理流程,特別是一些複雜邏輯,可以大大提升走讀程式碼的效率。

5.  正式走讀程式碼前,提前瞭解各個模組的程式碼目錄結構,把一個大模組拆分成各個小模組,先大體瀏覽各個模組的程式碼實現。

6.  前期走讀程式碼千萬不要深入細節,捋清楚各個模組的大體功能作用後再開始一步一步的深入細節,瞭解深層次的內部實現。

7.  main() 入口逐步開始走讀程式碼,結合 log 日誌列印和 gdb 除錯。

8.  跳過整體流程中不熟悉的模組程式碼,只走讀本次想弄明白的模組程式碼實現。


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

相關文章