關於做者
前滴滴出行技術專家,現任OPPO 文檔數據庫 mongodb 負責人,負責 oppo 千萬級峯值 TPS/ 十萬億級數據量文檔數據庫 mongodb 研發和運維工做,一直專一於分佈式緩存、高性能服務端、數據庫、中間件等相關研發。後續持續分享《 MongoDB 內核源碼設計、性能優化、最佳運維實踐》, Github 帳號地址 : https://github.com/y123456yzlinux
序言
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 代碼,咱們在走讀代碼前,建議遵循以下準則。算法
1. 熟悉 mongodb 基本功能和使用方法
首先,咱們須要熟悉mongodb 的基本功能,明白 mongodb 是作什麼用的,用在什麼地方,這樣才能體現 mongodb 的真正價值。此外,咱們須要提早搭建一個 mongodb 集羣玩一玩,這樣也能夠進一步促使咱們瞭解 mongodb 內部的一些經常使用基本功能。千萬不要急於求成,若是連mongodb 是作什麼的都不知道,或者連 mongodb 的運維操做方法都沒玩過,直接讀取代碼會很是不適合,沒有目的的走讀代碼不利於分析整個代碼,同時閱讀代碼過程會很是痛苦。mongodb
2. 下載代碼編譯源碼
熟悉了mongodb 的基本功能,並搭建集羣簡單體驗後,咱們就能夠從 github 下載源碼,本身編譯源碼生成二進制文件,編譯文檔存放於docs/building.md 代碼目錄中,源碼編譯步驟以下 :shell
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模塊的具體目錄文件結構:
從上面的文件分佈內容,能夠清晰的看出,整個目錄中的源碼實現文件大致能夠分爲以下幾個部分:
- message_compressor_*網絡傳輸數據壓縮子模塊
- service_entry_point*服務入口點子模塊
- service_executor*服務運行子模塊,即線程模型子模塊
- service_state_machine*服務狀態機處理子模塊
- Session*回話信息子模塊
- Ticket*數據分發子模塊
- transport_layer*套接字處理及傳輸層模式管理子模塊
經過上面的拆分,整個大的transport模塊實現就被拆分紅了7個小模塊,這7個小的子模塊各自負責對應功能實現,同時各個模塊相互銜接,總體實現網絡傳輸處理過程的總體實現,下面的章節將就這些子模塊進行簡單功能說明。
6. 從main入口開始大致走讀代碼
前面5個步驟事後,咱們已經熟悉了mongodb編譯調試以及transport模塊的各個子模塊的相關代碼文件實現及大致子模塊做用。至此,咱們能夠開始走讀代碼了,mongos和mongod的代碼入口分別在mongoSMain()和mongoDbMain(),從這兩個入口就能夠一步一步瞭解mongodb服務層代碼的總體實現。
注意:走讀代碼前期不要深刻各類細節實現,大致瞭解代碼實現便可,先大致弄明白代碼中各個模塊功能由那些子模塊實現,千萬不要深究細節。
7. 總結
本章節主要給出了數百萬級mongodb內核代碼閱讀的一些建議,整個過程能夠總結爲以下幾點:
- 提早了解mongodb的做用及工做原理。
- 本身搭建集羣提早學習下mongodb集羣的經常使用運維操做,能夠進一步幫助理解mongodb的功能特性,提高後期代碼閱讀的效率。
- 本身下載源碼編譯二進制可執行文件,同時學會使用日誌模塊,經過加日誌打印的方式逐步開始調試。
- 學習使用gdb代碼調試工具調試線程的運行流程,這樣能夠更進一步的促使快速學習代碼處理流程,特別是一些複雜邏輯,能夠大大提高走讀代碼的效率。
- 正式走讀代碼前,提早了解各個模塊的代碼目錄結構,把一個大模塊拆分紅各個小模塊,先大致瀏覽各個模塊的代碼實現。
- 前期走讀代碼千萬不要深刻細節,捋清楚各個模塊的大致功能做用後再開始一步一步的深刻細節,瞭解深層次的內部實現。
- 從main()入口逐步開始走讀代碼,結合log日誌打印和gdb調試。
- 跳過總體流程中不熟悉的模塊代碼,只走讀本次想弄明白的模塊代碼實現。