關於作者
前滴滴出行技術專家,現任OPPO文檔數據庫mongodb負責人,負責oppo千萬級峯值TPS/十萬億級數據量文檔數據庫mongodb內核研發及運維工作,一直專注於分佈式緩存、高性能服務端、數據庫、中間件等相關研發。後續持續分享《MongoDB內核源碼設計、性能優化、最佳運維實踐》,Github賬號地址:https://github.com/y123456yz
背景
<<transport_layer網絡傳輸層模塊源碼實現>>中分享了mongodb內核底層網絡IO處理相關實現,包括套接字初始化、一個完整mongodb報文的讀取、獲取到DB數據發送給客戶端等。Mongodb支持多種增、刪、改、查、聚合處理、cluster處理等操作,每個操作在內核實現中對應一個command,每個command有不同的功能,mongodb內核如何進行command源碼處理將是本文分析的重點
此外,mongodb提供了mongostat工具來監控當前集羣的各種操作統計。Mongostat監控統計如下圖所示:
其中,insert、delete、update、query這四項統計比較好理解,分別對應增、刪、改、查。但是,comand、getmore不是很好理解,command代表什麼統計?getMore代表什麼統計?,這兩項相對比較難理解。
1. Command命令處理模塊回顧
《Mongodb command命令處理模塊源碼實現一》中我們分析了一個客戶端請求到來後,mognodb服務端大體處理流程如下:
- 從message中解析初報文頭部,從而確定一個完整的mongodb報文
- 從body中解析初OpCode操作碼信息,3.6版本默認OpCode操作碼爲OP_MSG
- 根據解析初的OP_MSG操作碼,構造對應OpMsg類,真實命令請求以bson數據格式保存在該類成員body中。
- 從body中解析出command命令字符串信息(如“insert”、“update”等)。
- 從全局_commands map表中查找是否支持該命令,如果支持則執行該命令處理,如果不支持則直接報錯提示。
- 最終找到對應command命令後,執行command的功能run接口。
Mongodb內核支持的command命令信息保存在一個全局map表_commands中,從命令請求bson中解析出command命令字符串後,就是從該全局map表查找,如果找到該命令則說明mongodb支持該命令,找不到則說明不支持,整個過程歸納爲下圖所示:
從OpMsg類中解析出命令名字符串後(例如:”insert”、”delete”等),從全局map表_commands查找,找到則執行對應命令。如果找不到,說明不支持該命令操作,進行異常提示處理。
Mongodb不同實例支持那些command命令完全取決於全局map表_commands,下面繼續分析該全局map來源。
2. Command命令處理模塊源碼目錄結構
mongodb集羣中通常包含3種節點實例角色:mongos、mongod(ShardServer)、mongod(ConfigServer)。這3種實例校色功能如下:
- Mongos:代理,從shardServer獲取路由信息,轉發客戶端請求到shard。
- mongod(ShardServer):數據存儲節點,所有客戶端數據記錄到shard中。
- mongod(ConfigServer):記錄數據路由信息以及一些元數據。
Mongos代理進程名唯一,也就是”mongos”,代理mongos支持的命令信息比較好確認。但是ShardServer和ConfigServer的進程名都是”mongod”,如何區分各自支持那些命令呢?
configServer實際上是一種特殊的shardServer,它擁有shard數據分片的功能外,還擁有特殊的元數據管理功能,例如記錄chunk元數據信息、mongos信息、分片操作日誌信息等。因此,configServer除了支持shardServer的命令外,還會支持更多的特有命令。
mongos代理支持的命令信息全部在src/mongo/s/commands目錄中實現,源碼文件如下:
mongod(shardServer)支持的命令信息全部在src/mongo/db/commands目錄中實現,源碼文件如下:
mongod(configServer)幾乎支持所有shardServer支持的命令(說明:也有個別別特例,如”mapreduce.shardedfinish”),還支持特有的一些命令,這些特意命令在src/mongo/db/s/config目錄中實現,源碼文件如下:
從上面的不同實例支持命令的源碼目錄文件可以看出,mongodb內核源碼設計之優秀,從目錄結構即可一眼確定不同實例角色支持的各自不同命令信息,代碼可讀性非常好。目錄結構可以總結爲下表:
configServer和shardServer各自支持的命令範圍類似於下圖包含與被包含的關係,小橢圓代表shardServer,大圓代表configServer:
3. command模塊類繼承關係
第2章節代碼目錄結構可以看出,絕大部分命令功能由對應源碼文件實現,例如find_cmd.cpp源碼文件進行find”命令處理。此外,也有部分源碼文件,一個文件對應多個命令實現,例如write_commands.cpp源碼文件,同時負責”insert”、”update”、”delete”增刪改處理。
由於命令衆多,瞭解了代碼目錄結構後,在進行核心代碼分析前,我們先了解一下command類的各種繼承關係。不同命令有不同功能,也就需要不同的實現,但是所有命令也會有一些共同的接口特性,例如該命令是否需要認證、是否支持從節點操作、是否支持WriteConcern操作等。
不同command命令有相同的共性,也會有各自不同的獨有特性。所以,mongodb在源碼實現中充分考慮了這些問題,抽象出一些共有的特性接口由基類實現,command用於的一些獨有的特性,則在繼承類中實現。command命令處理模塊相關核心源碼類主要繼承關係圖如下:
如上圖,command命令處理模塊相關實現類按照父子繼承關係可以包含四層,每層功能說明如下:
- CommandInterface類:虛擬接口類,只定義虛擬接口,不做具體實現。
- Command類:完成一些基本功能檢查,例如是否支持從節點操作、是否需要認證、是否支持WriteConcern、獲取命令名、是否只能在admin庫操作等。
- BasicCommand類:認證相關接口實現、定義虛擬run接口。
- 具體命令類:每個命令都有一個相應的類定義,都是在該層實現,真正的命令run接口實現在該層完成。
4. command命令註冊核心代碼實現
前面分析提到,當解析到對應命令字符串(如:”insert”、”update”等)後,從全局map表中_commands查找,找到說明支持該命令,找不到則不支持。全局_commands表中保存了實例支持的command命令信息,不同命令需要提前註冊到該map表中,註冊方式有兩種:
- 每個命令定義一個對應全局類變量
- new()一個該命令類信息
類註冊過程源碼實現由command類初始化構造接口完成,註冊過程核心代碼如下所示:
1.//命令註冊,所有註冊的命令最終全部保存到_commands全局map表中
2.//name和oldName實際上是同一個command,只是可能因爲歷史原因,命令名改名了
3.Command::Command(StringData name, StringData oldName)
4. //命令名字符串
5. : _name(name.toString()),
6. //對應命令執行統計,total代表總的,failed代表執行失敗的次數
7. _commandsExecutedMetric("commands." + _name + ".total", &_commandsExecuted),
8. _commandsFailedMetric("commands." + _name + ".failed", &_commandsFailed) {
9. //如果_commands map表還沒有生成,則new一個
10. if (_commands == 0)
11. _commands = new CommandMap();
12. ......
13. //把name命令對應的command添加到map表中
14. Command*& c = (*_commands)[name];
15. if (c)
16. log() << "warning: 2 commands with name: " << _name;
17. c = this;
18. ......
19.
20. //大部分命令name和oldName是一樣的,所以在數組中只會記錄一個
21. //如果改名過,則name和oldName就不一樣,這時候都需要註冊到map表,對應同一個command
22. if (!oldName.empty()) //也就是name和oldName兩個命令對應的是同一個this類
23. (*_commands)[oldName.toString()] = this;
24.}
command初始化構造函數中有兩個入參,分表代表當前命令名和老舊命令名稱,這樣設計是爲了兼容處理。
4.1 command註冊方式一
超過99%的command命令通過定義一個全局類變量來完成註冊,本文以shardServer實例的”insert”、”update”、”delete”、“find”爲例,這幾個命令註冊方式如下:
1.//insert命令初始化
2.class CmdInsert : public WriteCommand { //
3.public:
4. //insert命令初始化構造
5. CmdInsert() : WriteCommand("insert") {}
6. ......
7. //認證檢查
8. Status checkAuthForRequest(...) final {
9. ......
10. }
11.
12. //真正的Insert插入文檔會走這裏面
13. void runImpl(...);
14. }
15.} cmdInsert; //直接定義一個cmdInsert全局變量
16.
17.//update命令初始化
18.class CmdUpdate: public WriteCommand { //
19.public:
20. //update命令初始化構造
21. CmdUpdate() : WriteCommand("update") {}
22. ......
23. //認證檢查
24. Status checkAuthForRequest(...) final {
25. ......
26. }
1. //查詢計劃執行過程
2. Status explain(...) const override {
3. ......
4. }
27. //真正的update插入文檔會走這裏面
28. void runImpl(...);
29. }
30.} cmdUpdate; //直接定義一個cmdUpdate全局變量
31.
32.//delete命令初始化
33.class CmdDelete: public WriteCommand { //
34.public:
35. //delete命令初始化構造
36. CmdDelete() : WriteCommand("delete") {}
37. ......
38. //認證檢查
39. Status checkAuthForRequest(...) final {
40. ......
41. }
5. //查詢計劃執行過程
6. Status explain(...) const override {
7. ......
8. }
42.
43. //真正的delete插入文檔會走這裏面
44. void runImpl(...);
45. }
46.} cmdDelete; //直接定義一個cmdDelete全局變量
“find”命令也是通過定義一個全局FindCmd類變量來完成該命令的註冊過程,註冊過程代碼如下:
9.//find命令實現類
10.class FindCmd : public BasicCommand {
11.public:
12. //初始化構造
13. FindCmd() : BasicCommand("find") {}
14. ......
15.
16. //查詢計劃執行過程
17. Status explain(...) const override {
18. ......
19. }
20.} findCmd; //直接定義一個findCmd全局變量
上面的類除了可以確定shardServer讀寫命令的註冊方式外,還可以看出讀寫命令實現過程中,類繼承關係稍微有點區別。主要體現在:FindCmd (查)命令類直接繼承BasicCommand 命令類,而CmdInsert(增) 、CmdDelete(刪)、CmdUpdate(改)這三個寫相關的命令,則通過繼承WriteCommand 來中轉一次,WriteCommand 實現WriteCommand 共性接口,而三個子類則實現自己特有的功能。
shardServer實例,增、刪、改、查四個級別命令的繼承關係圖可以總結爲下圖所示:
4.2 command註冊方式二
除了直接定義一個全局命令類變量外,mongodb內核命令註冊實現的時候,部分命令註冊通過new一個命令類實現,例如planCache執行計劃對應的幾個命令就是通過該方式實現,代碼實現如下:
1.//執行計劃相關的幾個command註冊過程,通過new實現
2.MONGO_INITIALIZER_WITH_PREREQUISITES(SetupPlanCacheCommands, MONGO_NO_PREREQUISITES)
3.(InitializerContext* context) {
4. //執行計劃相關的幾個命令註冊
5. new PlanCacheListQueryShapes();
6. new PlanCacheClear();
7. new PlanCacheListPlans();
8. return Status::OK();
9.}
10.
11.//test命令相關的幾個command註冊過程,也是通過new實現
12.MONGO_INITIALIZER(RegisterEmptyCappedCmd)(InitializerContext* context) {
13. //必須使能testCommandsEnabled,該命令纔有效
14. if (Command::testCommandsEnabled) {
15. new CapTrunc();
16. new CmdSleep();
17. new EmptyCapped();
18. new GodInsert();
19. }
20. return Status::OK();
21.}
至此,mongodb內核command命令註冊過程就分析完畢,如果想新註冊一個新的命令,可以模仿這個流程實現即可。
5. mongos、mongod(shardServer)、mongod(configServer)命名規範
mongodb不同校色得二進制實例支持的命令有所差異,分別由不同的代碼文件實現對應命令功能。mongodb內核設計非常優秀,通過文件名即可確定對應的命令,以及該命令歸屬於那個角色實例。這裏回顧一下前面提到的不同校色實例對應的命令代碼目錄實現:
- mongos代理:代碼目錄src/mongo/s/commands
- mongod(shardServer):代碼目錄src/mongo/db/commands
- mongod(configServer):代碼目錄src/mongo/db/s/config
除了代碼目錄有明確的區別外,代碼文件名及命令類名也各不相同。但是,命令類名和文件名也有特定的命名規範,有一定的命名規律,下面還是以mongod(含shardServer和configServer)和mongos代理爲例,來說明最常用的增、刪、改、查command命令對應的源碼文件命名和命令類命名。
提前梳理好各個校色實例的命名規範,對我們理解整個代碼具有事半功倍的效果,同時也可以方便我們快速找到任何一個命令的代碼文件及其對應命令的核心代碼實現,具有”舉一反三”的效果。
5.1 mongos、mongod(含shardServer和configServer)命名規範
mongod實例的寫操作命令(增、刪、改)由write_commands.cpp文件實現,該文件中的CmdInsert、CmdDelete、CmdUpdate類分別對應具體的增、刪、改命令操作。讀操作命令由find_cmd.cpp文件實現,對應命令類爲FindCmd
除了mongod實例,mongos作爲代理轉發節點,同樣支持增、刪、改操作。mongodb內核實現的時候,如果集羣部署是sharding集羣模式,則需要mongos代理,客戶端訪問入口爲代理。正是因爲代理模式爲sharding分片集羣模式,所以mongos支持的命令在源文件命名和命令類命名的時候,做了特殊標記。相比mongod實例,所有mongos支持的命令相關原文件和類實現基本上都增加”cluster”特殊標記。
以增、刪、改、查、isMaster、getMore、findAndModify爲例,mongos和mongod(含shardServer和configServer)支持的命令列表總結如下:
從上面的命名文件和命令類名可以看出,大多數mongos代理相關命令會增加”cluster”標記(但是也有部分個例,例如findAndModify對應類命就沒帶改標記)。
此外,也有部分mongos和mongod實例命令不滿足上面的命名規範,例如"dropIndexes"、"createIndexes"、"reIndex"、"create"、"renameCollection"等命令,各自命名規則如下:
如上,絕大多數mongos命令源碼文件和命令實現類命名相比mongod實例,都帶有”cluster”標識,但是還是有部分命令命名不準尋該規則。如果想知道某個命令的源碼實現文件,可以在前面提到的三個實例中搜索相應字符串即可定位到。注意:搜索的時候需要帶上雙引號。
5.2 mongod(configServer)特有命令命名規則
和mongos命名規則類似,configServer支持的獨有命令源碼文件命名規則相比shardServer增加了”configsvr”特性,從源碼文件名即可明顯的看出是configServer獨有的命令。
此外,命令對應類命命名也帶有”ConfigSvr”特性,例如class ConfigSvrAddShardCommand{}、class ConfigSvrMoveChunkCommand{}等,命名規則和mongos代理支持的command命名規則類似。
5.3 命名規則總結
上面的命名規則可以總結爲如下圖解信息:
7. 命令run
結合《命令處理模塊源碼實現一》和本章節對command處理流程可以得出,runCommandImpl接口通過如下調用流程最終執行特定命令的run接口,這裏以insert寫入和讀取流程爲例,mongod實例寫入調用過程如下圖所示:
最終,mongod和mongos實例調用相關命令得run接口完成具體的command命令處理操作。mongos、mongod(shardServer)、mongod(configServer)相關常用的操作命令(以最基本的讀寫命令爲例)入口及功能說明總結如下表所示: