mongodb核心原始碼實現、效能調優、最佳運維實踐系列-command命令處理模組原始碼實現一
關於作者
前滴滴出行技術專家,現任OPPO 文件資料庫 mongodb 負責人,負責 oppo 千萬級峰值 TPS/ 十萬億級資料量文件資料庫 mongodb 核心研發及運維工作,一直專注於分散式快取、高效能服務端、資料庫、中介軟體等相關研發。後續持續分享《 MongoDB 核心原始碼設計、效能優化、最佳運維實踐》, Github 賬號地址 : https://github.com/y123456yz
1. 背景
<<transport_layer 網路傳輸層模組原始碼實現 >> 中分享了 mongodb 核心底層網路 IO 處理相關實現,包括套接字初始化、一個完整 mongodb 報文的讀取、獲取到 DB 資料傳送給客戶端等。 Mongodb 支援多種增、刪、改、查、聚合處理、 cluster 處理等操作,每個操作在核心實現中對應一個 command ,每個 command 有不同的功能, mongodb 核心如何進行 command 原始碼處理將是本文分析的重點
此外,mongodb 提供了 mongostat 工具來監控當前叢集的各種操作統計。 Mongostat 監控統計如下圖所示:
其中,insert 、 delete 、 update 、 query 這四項統計比較好理解,分別對應增、刪、改、查。但是, comand 、 getmore 不是很好理解, command 代表什麼統計? getMore 代表什麼統計?,這兩項相對比較難理解。
此外,通過本文字分析,我們將搞明白這六項統計的具體含義,同時弄清這六項統計由那些操作進行計數。
Command 命令處理模組分為: mongos 操作命令、 mongod 操作命令、 mongodb 叢集內部命令,具體定義如下 :
① mongos 操作命令,客戶端可以通過 mongos 訪問叢集相關的命令。
② mongod 操作命令:客戶端可以通過 mongod 複製集和 cfg server 訪問叢集的相關命令。
③ mongodb 叢集內部命令: mongos 、 mongod 、 mongo-cfg 叢集例項之間互動的命令。
Command 命令處理模組核心程式碼實現如下:
《command 命令處理模組原始碼實現》相關文章重點分析命令處理模組核心程式碼實現,也就是上面截圖中的命令處理原始碼檔案實現。
2. <<transport_layer 網路傳輸層模組原始碼實現 >> 銜接回顧
<<transport_layer 網路傳輸層模組原始碼實現三 >> 一文中,我們對 service_state_machine 狀態機排程子模組進行了分析,該模組中的 dealTask 任務進行 mongodb 內部業務邏輯處理,其核心實現如下:
1. //dealTask處理 2. void ServiceStateMachine::_processMessage(ThreadGuard guard) { 3. ...... 4. //command處理、DB訪問後的資料通過dbresponse返回 5. DbResponse dbresponse = _sep->handleRequest(opCtx.get(), _inMessage); 6. ...... 7. }
上面的_sep 對應 mongod 或者 mongos 例項的服務入口實現,該 _seq 成員分別在如下程式碼中初始化為 ServiceEntryPointMongod 和 ServiceEntryPointMongod 類實現。 SSM 狀態機的 _seq 成員初始化賦值核心程式碼實現如下:
1. //mongos例項啟動初始化 2. static ExitCode runMongosServer() { 3. ...... 4. //mongos例項對應sep為ServiceEntryPointMongos 5. auto sep = stdx::make_unique<ServiceEntryPointMongos>(getGlobalServiceContext()); 6. getGlobalServiceContext()->setServiceEntryPoint(std::move(sep)); 7. ...... 8. } 9. 10. //mongod例項啟動初始化 11. ExitCode _initAndListen(int listenPort) { 12. ...... 13. //mongod例項對應sep為ServiceEntryPointMongod 14. serviceContext->setServiceEntryPoint( 15. stdx::make_unique<ServiceEntryPointMongod>(serviceContext)); 16. ...... 17. } 18. 19. //SSM狀態機初始化 20. ServiceStateMachine::ServiceStateMachine(...) 21. : _state{State::Created}, 22. //mongod和mongos例項的服務入口通過這裡賦值給_seq成員變數 23. _sep{svcContext->getServiceEntryPoint()}, 24. ...... }
通過上面的幾個核心介面實現,把mongos 和 mongod 兩個例項的服務入口與狀態機 SSM( ServiceStateMachine ) 聯絡起來,最終和下面的 command 命令處理模組關聯。
dealTask 進行一次 mongodb 請求的內部邏輯處理,該處理由 _sep->handleRequest( ) 介面實現。由於 mongos 和 mongod 服務入口分別由 ServiceEntryPointMongos 和 ServiceEntryPointMongo d 兩個類實現,因此 dealTask 也就演變為如下介面處理:
① mongos 例項: ServiceEntryPointMongos :: handleRequest (...)
② Mongod 例項 : : ServiceEntryPointMongo d:: handleRequest (...)
這兩個介面入參都是OperationContext 和 Message ,分別對應操作上下文、請求原始資料內容。下文會分析 Message 解析實現、 OperationContext 服務上下文實現將在後續章節分析。
Mongod 和 mongos 例項服務入口類都繼承自網路傳輸模組中的 ServiceEntryPointImpl 類,如下圖所示:
Tips: mongos 和 mongod 服務入口類為何要繼承網路傳輸模組服務入口類?
原因是一個請求對應一個連結session ,該 session 對應的請求又和 SSM 狀態機唯一對應。所有客戶端請求對應的 SSM 狀態機資訊全部儲存再 ServiceEntryPointImpl._sessions 成員中,而 command 命令處理模組為 SSM 狀態機任務中的 dealTask 任務,通過該繼承關係, ServiceEntryPointMongod 和 ServiceEntryPointMongos 子類也就可以和狀態機及任務處理關聯起來,同時也可以獲取當前請求對應的 session 連結資訊。
3. Mongodb 協議解析
在《transport_layer 網路傳輸層模組原始碼實現二》中的資料收發子模組完成了一個完整 mongodb 報文的接收,一個 mongodb 報文由 Header 頭部 +opCode 包體組成,如下圖所示:
上圖中各個欄位說明如下表:
opCode 取值比較多,早期版本中 OP_INSERT 、 OP_DELETE 、 OP_UPDATE 、 OP_QUERY 分別針對增刪改查請求, Mongodb 從 3.6 版本開始預設使用 OP_MSG 操作作為預設 opCode ,是一種可擴充套件的訊息格式,旨在包含其他操作碼的功能,新版本讀寫請求協議都對應該操作碼。本文以 OP_MSG 操作碼對應協議為例進行分析,其他操作碼協議分析過程類似, OP_MSG 請求協議格式如下:
1. OP_MSG { 2. //mongodb報文頭部 3. MsgHeader header; 4. //點陣圖,用於標識報文是否需要校驗 是否需要應答等 5. uint32 flagBits; // message flags 6. //報文內容,例如find write等命令內容通過bson格式存在於該結構中 7. Sections[] sections; // data sections 8. //報文CRC校驗 9. optional<uint32> checksum; // optional CRC-32C checksum }
OP_MSG 各個欄位說明如下表:
一個完整OP_MSG 請求格式如下:
除了通用頭部header 外,客戶端命令請求實際上都儲存於 sections 欄位中,該欄位存放的是請求的原始 bson 格式資料。 BSON 是由 10gen 開發的一個資料格式,目前主要用於 MongoDB 中,是 MongoDB 的資料儲存格式。 BSON 基於 JSON 格式,選擇 JSON 進行改造的原因主要是 JSON 的通用性及 JSON 的 schemaless 的特性。 BSON 相比JSON 具有以下特性 :
① Lightweight ( 更輕量級 )
② Traversable ( 易操作 )
③ Efficient ( 高效效能 )
本文重點不是分析bson 協議格式, bson 協議實現細節將在後續章節分享。 bson 協議更多設計細節詳見: http://bsonspec.org/
總結:一個完整mongodb 報文由 header+body 組成,其中 header 長度固定為 16 位元組, body 長度等於 messageLength -16 。 Header 部分協議解析由 message.cpp 和 message.h 兩原始碼檔案實現, body 部分對應的 OP_MSG 類請求解析由 op_msg.cpp 和 op_msg.h 兩原始碼檔案實現。
3. mongodb 報文通用頭部解析及封裝原始碼實現
Header 頭部解析由 src/mongo/util/net 目錄下 message.cpp和message.h 兩檔案完成,該類主要完成通用header 頭部和 body 部分的解析、封裝。因此報文頭部核心程式碼分為以下兩類:
① 報文頭部內容解析及封裝(MSGHEADER 名稱空間實現 )
② 頭部和body 內容解析及封裝 (MsgData 名稱空間實現 )
3.1 mongodb 報文頭部解析及封裝核心程式碼實現
mongodb 報文頭部解析由 namespace MSGHEADER {...} 實現,該類主要成員及介面實現如下:
1. namespace MSGHEADER { 2. //header頭部各個欄位資訊 3. struct Layout { 4. //整個message長度,包括header長度和body長度 5. int32_t messageLength; 6. //requestID 該請求id資訊 7. int32_t requestID; 8. //getResponseToMsgId解析 9. int32_t responseTo; 10. //操作型別:OP_UPDATE、OP_INSERT、OP_QUERY、OP_DELETE、OP_MSG等 11. int32_t opCode; 12. }; 13. 14. //ConstView實現header頭部資料解析 15. class ConstView { 16. public: 17. ...... 18. //初始化構造 19. ConstView(const char* data) : _data(data) {} 20. //獲取_data地址 21. const char* view2ptr() const { 22. return data().view(); 23. } 24. //TransportLayerASIO::ASIOSourceTicket::_headerCallback呼叫 25. //解析header頭部的messageLength欄位 26. int32_t getMessageLength() const { 27. return data().read<LittleEndian<int32_t>>(offsetof(Layout, messageLength)); 28. } 29. //解析header頭部的requestID欄位 30. int32_t getRequestMsgId() const { 31. return data().read<LittleEndian<int32_t>>(offsetof(Layout, requestID)); 32. } 33. //解析header頭部的getResponseToMsgId欄位 34. int32_t getResponseToMsgId() const { 35. return data().read<LittleEndian<int32_t>>(offsetof(Layout, responseTo)); 36. } 37. //解析header頭部的opCode欄位 38. int32_t getOpCode() const { 39. return data().read<LittleEndian<int32_t>>(offsetof(Layout, opCode)); 40. } 41. 42. protected: 43. //mongodb報文資料起始地址 44. const view_type& data() const { 45. return _data; 46. } 47. private: 48. //資料部分 49. view_type _data; 50. }; 51. 52. //View填充header頭部資料 53. class View : public ConstView { 54. public: 55. ...... 56. //構造初始化 57. View(char* data) : ConstView(data) {} 58. //header起始地址 59. char* view2ptr() { 60. return data().view(); 61. } 62. //以下四個介面進行header填充 63. //填充header頭部messageLength欄位 64. void setMessageLength(int32_t value) { 65. data().write(tagLittleEndian(value), offsetof(Layout, messageLength)); 66. } 67. //填充header頭部requestID欄位 68. void setRequestMsgId(int32_t value) { 69. data().write(tagLittleEndian(value), offsetof(Layout, requestID)); 70. } 71. //填充header頭部responseTo欄位 72. void setResponseToMsgId(int32_t value) { 73. data().write(tagLittleEndian(value), offsetof(Layout, responseTo)); 74. } 75. //填充header頭部opCode欄位 76. void setOpCode(int32_t value) { 77. data().write(tagLittleEndian(value), offsetof(Layout, opCode)); 78. } 79. private: 80. //指向header起始地址 81. view_type data() const { 82. return const_cast<char*>(ConstView::view2ptr()); 83. } 84. }; 85. }
從上面的header 頭部解析、填充的實現類可以看出, header 頭部解析由 MSGHEADER::ConstView 實現; header 頭部填充由 MSGHEADER::View 完成。實際上程式碼實現上,通過 offsetof 來進行移位,從而快速定位到頭部對應欄位。
3.2 mongodb 報文頭部 +body 解析封裝核心程式碼實現
Namespace MSGHEADER{...} 名稱空間只負責 header 頭部的處理, namespace MsgData{...} 名稱空間相對 MSGHEADER 名稱空間更加完善,除了處理頭部解析封裝外,還負責 body 資料起始地址維護、 body 資料封裝、資料長度檢查等。 MsgData 名稱空間核心程式碼實現如下:
1. namespace MsgData { 2. struct Layout { 3. //資料填充組成:header部分 4. MSGHEADER::Layout header; 5. //資料填充組成: body部分,body先用data佔位置 6. char data[4]; 7. }; 8. 9. //解析header欄位資訊及body其實地址資訊 10. class ConstView { 11. public: 12. //初始化構造 13. ConstView(const char* storage) : _storage(storage) {} 14. //獲取資料起始地址 15. const char* view2ptr() const { 16. return storage().view(); 17. } 18. 19. //以下四個介面間接執行前面的MSGHEADER中的頭部欄位解析 20. //填充header頭部messageLength欄位 21. int32_t getLen() const { 22. return header().getMessageLength(); 23. } 24. //填充header頭部requestID欄位 25. int32_t getId() const { 26. return header().getRequestMsgId(); 27. } 28. //填充header頭部responseTo欄位 29. int32_t getResponseToMsgId() const { 30. return header().getResponseToMsgId(); 31. } 32. //獲取網路資料包文中的opCode欄位 33. NetworkOp getNetworkOp() const { 34. return NetworkOp(header().getOpCode()); 35. } 36. //指向body起始地址 37. const char* data() const { 38. return storage().view(offsetof(Layout, data)); 39. } 40. //messageLength長度檢查,opcode檢查 41. bool valid() const { 42. if (getLen() <= 0 || getLen() > (4 * BSONObjMaxInternalSize)) 43. return false; 44. if (getNetworkOp() < 0 || getNetworkOp() > 30000) 45. return false; 46. return true; 47. } 48. ...... 49. protected: 50. //獲取_storage 51. const ConstDataView& storage() const { 52. return _storage; 53. } 54. //指向header起始地址 55. MSGHEADER::ConstView header() const { 56. return storage().view(offsetof(Layout, header)); 57. } 58. private: 59. //mongodb報文儲存在這裡 60. ConstDataView _storage; 61. }; 62. 63. //填充資料,包括Header和body 64. class View : public ConstView { 65. public: 66. //構造初始化 67. View(char* storage) : ConstView(storage) {} 68. ...... 69. //獲取報文起始地址 70. char* view2ptr() { 71. return storage().view(); 72. } 73. 74. //以下四個介面間接執行前面的MSGHEADER中的頭部欄位構造 75. //以下四個介面完成msg header賦值 76. //填充header頭部messageLength欄位 77. void setLen(int value) { 78. return header().setMessageLength(value); 79. } 80. //填充header頭部messageLength欄位 81. void setId(int32_t value) { 82. return header().setRequestMsgId(value); 83. } 84. //填充header頭部messageLength欄位 85. void setResponseToMsgId(int32_t value) { 86. return header().setResponseToMsgId(value); 87. } 88. //填充header頭部messageLength欄位 89. void setOperation(int value) { 90. return header().setOpCode(value); 91. } 92. 93. using ConstView::data; 94. //指向data 95. char* data() { 96. return storage().view(offsetof(Layout, data)); 97. } 98. private: 99. //也就是報文起始地址 100. DataView storage() const { 101. return const_cast<char*>(ConstView::view2ptr()); 102. } 103. //指向header頭部 104. MSGHEADER::View header() const { 105. return storage().view(offsetof(Layout, header)); 106. } 107. }; 108. 109. ...... 110. //Value為前面的Layout,減4是因為有4位元組填充data,所以這個就是header長度 111. const int MsgDataHeaderSize = sizeof(Value) - 4; 112. 113. //除去頭部後的資料部分長度 114. inline int ConstView::dataLen() const { 115. return getLen() - MsgDataHeaderSize; 116. } 117. } // namespace MsgData
和MSGHEADER 名稱空間相比, MsgData 這個 namespace 名稱空間介面實現和前面的 MSGHEADER 名稱空間實現大同小異。 MsgData 不僅僅處理 header 頭部的解析組裝,還負責 body 部分資料頭部指標指向、頭部長度檢查、 opCode 檢查、資料填充等。其中, MsgData 名稱空間中 header 頭部的解析構造底層依賴 MSGHEADER 實現。
3.3 Message/DbMessage 核心程式碼實現
在《transport_layer 網路傳輸層模組原始碼實現二》中,從底層 ASIO 庫接收到的 mongodb 報文是存放在 Message 結構中儲存,最終存放在ServiceStateMachine._inMessage 成員中。
在前面第2 章我們知道 mongod 和 mongso 例項的服務入口介面 handleRequest (...) 中都帶有 Message 入參,也就是接收到的 Message 資料通過該介面處理。Message 類主要介面實現如下:
1. //DbMessage._msg成員為該型別 2. class Message { 3. public: 4. //message初始化 5. explicit Message(SharedBuffer data) : _buf(std::move(data)) {} 6. //頭部header資料 7. MsgData::View header() const { 8. verify(!empty()); 9. return _buf.get(); 10. } 11. //獲取網路資料包文中的op欄位 12. NetworkOp operation() const { 13. return header().getNetworkOp(); 14. } 15. //_buf釋放為空 16. bool empty() const { 17. return !_buf; 18. } 19. //獲取報文總長度messageLength 20. int size() const { 21. if (_buf) { 22. return MsgData::ConstView(_buf.get()).getLen(); 23. } 24. return 0; 25. } 26. //body長度 27. int dataSize() const { 28. return size() - sizeof(MSGHEADER::Value); 29. } 30. //buf重置 31. void reset() { 32. _buf = {}; 33. } 34. // use to set first buffer if empty 35. //_buf直接使用buf空間 36. void setData(SharedBuffer buf) { 37. verify(empty()); 38. _buf = std::move(buf); 39. } 40. //把msgtxt拷貝到_buf中 41. void setData(int operation, const char* msgtxt) { 42. setData(operation, msgtxt, strlen(msgtxt) + 1); 43. } 44. //根據operation和msgdata構造一個完整mongodb報文 45. void setData(int operation, const char* msgdata, size_t len) { 46. verify(empty()); 47. size_t dataLen = len + sizeof(MsgData::Value) - 4; 48. _buf = SharedBuffer::allocate(dataLen); 49. MsgData::View d = _buf.get(); 50. if (len) 51. memcpy(d.data(), msgdata, len); 52. d.setLen(dataLen); 53. d.setOperation(operation); 54. } 55. ...... 56. //獲取_buf對應指標 57. const char* buf() const { 58. return _buf.get(); 59. } 60. 61. private: 62. //存放接收資料的buf 63. SharedBuffer _buf; 64. };
Message 是操作 mongodb 收發報文最直接的實現類,該類主要完成一個完整 mongodb 報文封裝。有關 mongodb 報文頭後面的 body 更多的解析實現在 DbMessage 類中完成, DbMessage 類包含 Message 類成員 _msg 。實際上, Message 報文資訊在 handleRequest (...) 例項服務入口中賦值給 DbMessage._msg ,報文後續的 body 處理繼續由 DbMessage 類相關介面完成處理。 DbMessage 和 Message 類關係如下 :
1. class DbMessage { 2. ...... 3. //包含Message成員變數 4. const Message& _msg; 5. //mongodb報文起始地址 6. const char* _nsStart; 7. //報文結束地址 8. const char* _theEnd; 9. } 10. 11. DbMessage::DbMessage(const Message& msg) : _msg(msg), 12. _nsStart(NULL), _mark(NULL), _nsLen(0) { 13. //一個mongodb報文(header+body)資料的結束地址 14. _theEnd = _msg.singleData().data() + _msg.singleData().dataLen(); 15. //報文起始地址 [_nextjsobj, _theEnd ]之間的資料就是一個完整mongodb報文 16. _nextjsobj = _msg.singleData().data(); 17. ...... 18. }
DbMessage . _msg 成員為 DbMessage 型別, DbMessage 的 _nsStart 和 _theEnd 成員分別記錄完整mongodb 報文的起始地址和結束地址,通過這兩個指標就可以獲取一個完整 mongodb 報文的全部內容,包括 header 和 body 。
注意: DbMessage 是早期mongodb 版本 (version<3.6) 中用於報文 body 解析封裝的類,這些類針對 opCode=[dbUpdate, dbDelete] 這個區間的操作。在 mongodb 新版本 (version>=3.6) 中, body 解析及封裝由 op_msg.h 和 op_msg.cpp 程式碼檔案中的 clase OpMsgRequest{} 完成處理。
3.4 OpMsg 報文解析封裝核心程式碼實現
Mongodb 從 3.6 版本開始預設使用 OP_MSG 操作作為預設 opCode ,是一種可擴充套件的訊息格式,旨在包含其他操作碼的功能,新版本讀寫請求協議都對應該操作碼。 OP_MSG 對應 mongodb 報文 body 解析封裝處理由 OpMsg 類相關介面完成, OpMsg::parse(Message) 從 Message 中解析出報文 body 內容,其核心程式碼實現如下:
1. struct OpMsg { 2. ...... 3. //msg解析賦值見OpMsg::parse 4. //各種命令(insert update find等)都存放在該body中 5. BSONObj body; 6. //sequences用法暫時沒看懂,感覺沒什麼用?先跳過 7. std::vector<DocumentSequence> sequences; //賦值見OpMsg::parse 8. } 1. //從message中解析出OpMsg資訊 2. OpMsg OpMsg::parse(const Message& message) try { 3. //message不能為空,並且opCode必須為dbMsg 4. invariant(!message.empty()); 5. invariant(message.operation() == dbMsg); 6. //獲取flagBits 7. const uint32_t flags = OpMsg::flags(message); 8. //flagBits有效性檢查,bit 0-15中只能對第0和第1位操作 9. uassert(ErrorCodes::IllegalOpMsgFlag, 10. str::stream() << "Message contains illegal flags value: Ob" 11. << std::bitset<32>(flags).to_string(), 12. !containsUnknownRequiredFlags(flags)); 13. 14. //校驗碼預設4位元組 15. constexpr int kCrc32Size = 4; 16. //判斷該mongo報文body內容是否啟用了校驗功能 17. const bool haveChecksum = flags & kChecksumPresent; 18. //如果有啟用校驗功能,則報文末尾4位元組為校驗碼 19. const int checksumSize = haveChecksum ? kCrc32Size : 0; 20. //sections欄位內容 21. BufReader sectionsBuf(message.singleData().data() + sizeof(flags), 22. message.dataSize() - sizeof(flags) - checksumSize); 23. 24. //預設先設定位false 25. bool haveBody = false; 26. OpMsg msg; 27. //解析sections對應命令請求資料 28. while (!sectionsBuf.atEof()) { 29. //BufReader::read讀取kind內容,一個位元組 30. const auto sectionKind = sectionsBuf.read<Section>(); 31. //kind為0對應命令請求body內容,內容通過bson報錯 32. switch (sectionKind) { 33. //sections第一個位元組是0說明是body 34. case Section::kBody: { 35. //預設只能有一個body 36. uassert(40430, "Multiple body sections in message", !haveBody); 37. haveBody = true; 38. //命令請求的bson資訊儲存在這裡 39. msg.body = sectionsBuf.read<Validated<BSONObj>>(); 40. break; 41. } 42. 43. //DocSequence暫時沒看明白,用到的地方很少,跳過,後續等 44. //該系列文章主流功能分析完成後,從頭再回首分析 45. case Section::kDocSequence: { 46. ...... 47. } 48. } 49. } 50. //OP_MSG必須有body內容 51. uassert(40587, "OP_MSG messages must have a body", haveBody); 52. //body和sequence去重判斷 53. for (const auto& docSeq : msg.sequences) { 54. ...... 55. } 56. return msg; 57. }
OpMsg 類被 OpMsgRequest 類繼承, OpMsgRequest 類中核心介面就是解析出 OpMsg.body 中的庫資訊和表資訊, OpMsgRequest 類程式碼實現如下:
1. //協議解析得時候會用到,見runCommands 2. struct OpMsgRequest : public OpMsg { 3. ...... 4. //構造初始化 5. explicit OpMsgRequest(OpMsg&& generic) : OpMsg(std::move(generic)) {} 6. //opMsgRequestFromAnyProtocol->OpMsgRequest::parse 7. //從message中解析出OpMsg所需成員資訊 8. static OpMsgRequest parse(const Message& message) { 9. //OpMsg::parse 10. return OpMsgRequest(OpMsg::parse(message)); 11. } 12. //根據db body extraFields填充OpMsgRequest 13. static OpMsgRequest fromDBAndBody(... { 14. OpMsgRequest request; 15. request.body = ([&] { 16. //填充request.body 17. ...... 18. }()); 19. return request; 20. } 21. //從body中獲取db name 22. StringData getDatabase() const { 23. if (auto elem = body["$db"]) 24. return elem.checkAndGetStringData(); 25. uasserted(40571, "OP_MSG requests require a $db argument"); 26. } 27. //find insert 等命令資訊 body中的第一個elem就是command 名 28. StringData getCommandName() const { 29. return body.firstElementFieldName(); 30. } 31. };
OpMsgRequest 通過 OpMsg::parse(message) 解析出OpMsg 資訊,從而獲取到 body 內容, GetCommandName() 介面和 getDatabase() 則分別從 body 中獲取庫 DB 資訊、命令名資訊。通過該類相關介面,命令名 (find 、 write 、 update 等 ) 和 DB 庫都獲取到了。
OpMsg 模組除了 OP_MSG 相關報文解析外,還負責 OP_MSG 報文組裝填充,該模組介面功能大全如下表:
4. Mongod 例項服務入口核心程式碼實現
Mongod 例項服務入口類 ServiceEntryPointMongod 繼承 ServiceEntryPointImpl 類, mongod 例項的報文解析處理、命令解析、命令執行都由該類負責處理。 ServiceEntryPointMongod 核心介面可以細分為: opCode 解析及回撥處理、命令解析及查詢、命令執行三個子模組。
4.1 opCode 解析及回撥處理
OpCode 操作碼解析及其回撥處理由 ServiceEntryPointMongod::handleRequest (...) 介面實現,核心程式碼實現如下 :
1. //mongod服務對於客戶端請求的處理 2. //通過狀態機SSM模組的如下介面呼叫:ServiceStateMachine::_processMessage 3. DbResponse ServiceEntryPointMongod::handleRequest(OperationContext* opCtx, const Message& m) { 4. //獲取opCode,3.6版本對應客戶端預設使用OP_MSG 5. NetworkOp op = m.operation(); 6. ...... 7. //根據message構造DbMessage 8. DbMessage dbmsg(m); 9. //根據操作上下文獲取對應的client 10. Client& c = *opCtx->getClient(); 11. ...... 12. //獲取庫.表資訊,注意只有dbUpdate<opCode<dbDelete的opCode請求才通過dbmsg直接獲取庫和表資訊 13. const char* ns = dbmsg.messageShouldHaveNs() ? dbmsg.getns() : NULL; 14. const NamespaceString nsString = ns ? NamespaceString(ns) : NamespaceString(); 15. .... 16. //CurOp::debug 初始化opDebug,慢日誌相關記錄 17. OpDebug& debug = currentOp.debug(); 18. //慢日誌閥值 19. long long logThresholdMs = serverGlobalParams.slowMS; 20. //時mongodb將記錄這次慢操作,1為只記錄慢操作,即操作時間大於了設定的配置,2表示記錄所有操作 21. bool shouldLogOpDebug = shouldLog(logger::LogSeverity::Debug(1)); 22. DbResponse dbresponse; 23. if (op == dbMsg || op == dbCommand || (op == dbQuery && isCommand)) { 24. //新版本op=dbMsg,因此走這裡 25. //從DB獲取資料,獲取到的資料通過dbresponse返回 26. dbresponse = runCommands(opCtx, m); 27. } else if (op == dbQuery) { 28. ...... 29. //早期mongodb版本查詢走這裡 30. dbresponse = receivedQuery(opCtx, nsString, c, m); 31. } else if (op == dbGetMore) { 32. //早期mongodb版本查詢走這裡 33. dbresponse = receivedGetMore(opCtx, m, currentOp, &shouldLogOpDebug); 34. } else { 35. ...... 36. //早期版本增 刪 改走這裡處理 37. if (op == dbInsert) { 38. receivedInsert(opCtx, nsString, m); //插入操作入口 新版本CmdInsert::runImpl 39. } else if (op == dbUpdate) { 40. receivedUpdate(opCtx, nsString, m); //更新操作入口 41. } else if (op == dbDelete) { 42. receivedDelete(opCtx, nsString, m); //刪除操作入口 43. } 44. } 45. //獲取runCommands執行時間,也就是內部處理時間 46. debug.executionTimeMicros = durationCount<Microseconds>(currentOp.elapsedTimeExcludingPauses()); 47. ...... 48. //慢日誌記錄 49. if (shouldLogOpDebug || (shouldSample && debug.executionTimeMicros > logThresholdMs * 1000LL)) { 50. Locker::LockerInfo lockerInfo; 51. //OperationContext::lockState LockerImpl<>::getLockerInfo 52. opCtx->lockState()->getLockerInfo(&lockerInfo); 53. 54. //OpDebug::report 記錄慢日誌到日誌檔案 55. log() << debug.report(&c, currentOp, lockerInfo.stats); 56. } 57. //各種統計資訊 58. recordCurOpMetrics(opCtx); 59. }
Mongod 的 handleRequest() 介面主要完成以下工作:
① 從Message 中獲取 OpCode ,早期版本每個命令又對應取值,例如增刪改查早期版本分別對應: dbInsert 、 dbDelete 、 dbUpdate 、 dbQuery ; Mongodb 3.6 開始,預設請求對應 OpCode 都是 OP_MSG ,本文預設只分析 OpCode=OP_MSG 相關的處理。
② 獲取本操作對應的Client 客戶端資訊。
③ 如果是早期版本,通過Message 構造 DbMessage ,同時解析出庫 . 表資訊。
④ 根據不同OpCode 執行對應回撥操作, OP_MSG 對應操作為 runCommands(...) ,獲取的資料通過 dbresponse 返回。
⑤ 獲取到db 層返回的資料後,進行慢日誌判斷,如果 db 層資料訪問超過閥值,記錄慢日誌。
⑥ 設定debug 的各種統計資訊。
4.2 命令解析及查詢
從上面的分析可以看出,介面最後呼叫runCommands(...) ,該介面核心程式碼實現如下所示:
1. //message解析出對應command執行 2. DbResponse runCommands(OperationContext* opCtx, const Message& message) { 3. //獲取message對應的ReplyBuilder,3.6預設對應OpMsgReplyBuilder 4. //應答資料通過該類構造 5. auto replyBuilder = rpc::makeReplyBuilder(rpc::protocolForMessage(message)); 6. [&] { 7. OpMsgRequest request; 8. try { // Parse. 9. //協議解析 根據message獲取對應OpMsgRequest 10. request = rpc::opMsgRequestFromAnyProtocol(message); 11. } 12. } 13. try { // Execute. 14. //opCtx初始化 15. curOpCommandSetup(opCtx, request); 16. //command初始化為Null 17. Command* c = nullptr; 18. //OpMsgRequest::getCommandName查詢 19. if (!(c = Command::findCommand(request.getCommandName()))) { 20. //沒有找到相應的command的後續異常處理 21. ...... 22. } 23. //執行command命令,獲取到的資料通過replyBuilder.get()返回 24. execCommandDatabase(opCtx, c, request, replyBuilder.get()); 25. } 26. //OpMsgReplyBuilder::done對資料進行序列化操作 27. auto response = replyBuilder->done(); 28. //responseLength賦值 29. CurOp::get(opCtx)->debug().responseLength = response.header().dataLen(); 30. // 返回 31. return DbResponse{std::move(response)}; 32. }
RunCommands(...) 介面從 message 中解析出 OpMsg 資訊,然後獲取該 OpMsg 對應的 command 命令資訊,最後執行該命令對應的後續處理操作。主要功能說明如下:
① 獲取該OpCode 對應 replyBuilder , OP_MSG 操作對應 builder 為 OpMsgReplyBuilder 。
② 根據message 解析出 OpMsgRequest 資料, OpMsgRequest 來中包含了真正的命令請求 bson 資訊。
③ opCtx 初始化操作。
④ 通過request.getCommandName() 返回命令資訊 ( 如“ find ”、“ update ”等字串 ) 。
⑤ 通過Command::findCommand(command name) 從 CommandMap 這個 map 表中查詢是否支援該 command 命令。如果沒找到說明不支援,如果找到說明支援。
⑥ 呼叫execCommandDatabase(...) 執行該命令,並獲取命令的執行結果。
⑦ 根據command 執行結果構造 response 並返回
4.3 命令執行
1. void execCommandDatabase(...) { 2. ...... 3. //獲取dbname 4. const auto dbname = request.getDatabase().toString(); 5. ...... 6. //mab表存放從bson中解析出的elem資訊 7. StringMap<int> topLevelFields; 8. //body elem解析 9. for (auto&& element : request.body) { 10. //獲取bson中的elem資訊 11. StringData fieldName = element.fieldNameStringData(); 12. //如果elem資訊重複,則異常處理 13. ...... 14. } 15. //如果是help命令,則給出help提示 16. if (Command::isHelpRequest(helpField)) { 17. //給出help提示 18. Command::generateHelpResponse(opCtx, replyBuilder, *command); 19. return; 20. } 21. //許可權認證檢查,檢查該命令執行許可權 22. uassertStatusOK(Command::checkAuthorization(command, opCtx, request)); 23. ...... 24. 25. //該命令執行次數統計 db.serverStatus().metrics.commands可以獲取統計資訊 26. command->incrementCommandsExecuted(); 27. //真正的命令執行在這裡面 28. retval = runCommandImpl(opCtx, command, request, replyBuilder, startOperationTime); 29. //該命令執行失敗次數統計 30. if (!retval) { 31. command->incrementCommandsFailed(); 32. } 33. ...... 34. }
execCommandDatabase(...) 最終呼叫RunCommandImpl(...) 進行對應命令的真正處理,該介面核心程式碼實現如下:
1. bool runCommandImpl(...) { 2. //獲取命令請求內容body 3. BSONObj cmd = request.body; 4. //獲取請求中的DB庫資訊 5. const std::string db = request.getDatabase().toString(); 6. //ReadConcern檢查 7. Status rcStatus = waitForReadConcern( 8. opCtx, repl::ReadConcernArgs::get(opCtx), command->allowsAfterClusterTime(cmd)); 9. //ReadConcern檢查不通過,直接異常提示處理 10. if (!rcStatus.isOK()) { 11. //異常處理 12. return; 13. } 14. if (!command->supportsWriteConcern(cmd)) { 15. //命令不支援WriteConcern,但是對應的請求中卻帶有WriteConcern配置,直接報錯不支援 16. if (commandSpecifiesWriteConcern(cmd)) { 17. //異常處理"Command does not support writeConcern" 18. ...... 19. return result; 20. } 21. //呼叫Command::publicRun執行不同命令操作 22. result = command->publicRun(opCtx, request, inPlaceReplyBob); 23. } 24. //提取WriteConcernOptions資訊 25. auto wcResult = extractWriteConcern(opCtx, cmd, db); 26. //提取異常,直接異常處理 27. if (!wcResult.isOK()) { 28. //異常處理 29. ...... 30. return result; 31. } 32. ...... 33. //執行對應的命令Command::publicRun,執行不同命令操作 34. result = command->publicRun(opCtx, request, inPlaceReplyBob); 35. ...... 36. }
RunCommandImpl(...) 介面最終呼叫該介面入參的 command ,執行 command->publicRun (...) 介面,也就是命令模組的公共 publicRun 。
4.4 總結
Mongod 服務入口首先從 message 中解析出 opCode 操作碼, 3.6 版本對應客戶端預設操作碼為 OP_MSQ ,解析出該操作對應 OpMsgRequest 資訊。然後從 message 原始資料中解析出 command 命令字串後,繼續通過全域性 Map 表種查詢是否支援該命令操作,如果支援則執行該命令;如果不支援,直接異常列印,同時返回。
5. Mongos 例項服務入口核心程式碼實現
mongos 服務入口核心程式碼實現過程和 mongod 服務入口程式碼實現流程幾乎相同, mongos 例項 message 解析、 OP_MSG 操作碼處理、 command 命令查詢等流程和上一章節 mongod 例項處理過程類似,本章節不在詳細分析。 Mongos 例項服務入口處理呼叫流程如下:
ServiceEntryPointMongos::handleRequest(...)->Strategy::clientCommand(...)-->runCommand(...)->execCommandClient(...)
最後的介面核心程式碼實現如下:
1. void runCommand(...) { 2. ...... 3. //獲取請求命令name 4. auto const commandName = request.getCommandName(); 5. //從全域性map表中查詢 6. auto const command = Command::findCommand(commandName); 7. //沒有對應的command存在,拋異常說明不支援該命令 8. if (!command) { 9. ...... 10. return; 11. } 12. ...... 13. //執行命令 14. execCommandClient(opCtx, command, request, builder); 15. ...... 16. } 17. 18. void execCommandClient(...) 19. { 20. ...... 21. //認證檢查,是否有操作該command命令的許可權,沒有則異常提示 22. Status status = Command::checkAuthorization(c, opCtx, request); 23. if (!status.isOK()) { 24. Command::appendCommandStatus(result, status); 25. return; 26. } 27. //該命令的執行次數自增,代理上面也是要計數的 28. c->incrementCommandsExecuted(); 29. //如果需要command統計,則加1 30. if (c->shouldAffectCommandCounter()) { 31. globalOpCounters.gotCommand(); 32. } 33. ...... 34. //有部分命令不支援writeconcern配置,報錯 35. bool supportsWriteConcern = c->supportsWriteConcern(request.body); 36. //不支援writeconcern又帶有該引數的請求,直接異常處理"Command does not support writeConcern" 37. if (!supportsWriteConcern && !wcResult.getValue().usedDefault) { 38. ...... 39. return; 40. } 41. //執行本命令對應的公共publicRun介面,Command::publicRun 42. ok = c->publicRun(opCtx, request, result); 43. ...... 44. }
l Tips: mongos 和 mongod 例項服務入口核心程式碼實現的一點小區別
① Mongod 例項 opCode 操作碼解析、 OpMsg 解析、 command 查詢及對應命令呼叫處理都由 class ServiceEntryPointMongod{...} 類一起完成。
② mongos 例項則把 opCode 操作碼解析交由 class ServiceEntryPointMongos{...} 類實現, OpMsg 解析、 command 查詢及對應命令呼叫處理放到了 clase Strategy{...} 類來處理。
6. 總結
Mongodb 報文解析及組裝流程總結
① 一個完整mongodb 報文由通用報文 header 頭部 +body 部分組成。
② Body 部分內容,根據報文頭部的 opCode 來決定不同的body 內容。
③ 3.6 版本對應客戶端請求 opCode 預設為 OP_MSG ,該操作碼對應 body 部分由 flagBits + sections + checksum 組成,其中 sections 中存放的是真正的命令請求資訊,已 bson 資料格式儲存。
④ Header 頭部和 body 報文體封裝及解析過程由 class Message {...} 類實現
⑤ Body 中對應 command 命令名、庫名、表名的解析在 mongodb(version<3.6) 低版本協議中由 class DbMessage {...} 類實現
⑥ Body 中對應 command 命令名、庫名、表名的解析在 mongodb(version<3.6) 低版本協議中由 struct OpMsgRequest{...} 結構和 struct OpMsg {...} 類實現
Mongos 和 mongod 例項的服務入口處理流程大同小異,整體處理流程如下:
① 從message 解析出 opCode 操作碼,根據不同操作碼執行對應操作碼回撥。
② 根據message 解析出 OpMsg request 資訊, mongodb 報文的命令資訊就儲存在該 body 中,該 body 已 bson 格式儲存。
③ 從body 中解析出 command 命令字串資訊 ( 如“ insert ”、“ update ”等 ) 。
④ 從全域性_commands map 表中查詢是否支援該命令,如果支援則執行該命令處理,如果不支援則直接報錯提示。
⑤ 最終找到對應command 命令後,執行 command 的功能 run 介面。
圖形化總結如下:
說明: 第3 章的協議解析及封裝過程實際上應該算是網路處理模組範疇,本文為了分析 command 命令處理模組方便,把該部分實現歸納到了命令處理模組,這樣方便理解。
Tips: 下期繼續分享不同command 命令執行細節。
7. 遺留問題
第1 章節中的統計資訊,將在 command 模組核心程式碼分析完畢後揭曉答案,《 mongodb command 命令處理模組原始碼實現二》中繼續分析,敬請關注。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69984922/viewspace-2732975/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- mongodb核心原始碼實現、效能調優、最佳運維實踐系列-command命令處理模組原始碼實現三MongoDB原始碼運維
- mongodb核心原始碼實現、效能調優、最佳運維實踐系列-command命令處理模組原始碼實現二MongoDB原始碼運維
- mongodb核心原始碼實現、效能調優、最佳運維實踐系列-mongodb網路傳輸層模組原始碼實現三MongoDB原始碼運維
- mongodb核心原始碼實現、效能調優、最佳運維實踐系列-網路傳輸層模組原始碼實現四MongoDB原始碼運維
- mongodb核心原始碼實現、效能調優、最佳運維實踐系列-網路傳輸層模組原始碼實現二MongoDB原始碼運維
- mongodb核心原始碼實現、效能調優、最佳運維實踐系列-網路傳輸層模組原始碼實現三MongoDB原始碼運維
- mongodb核心原始碼實現及效能優化系列:Mongodb write寫(增、刪、改)模組原始碼實現MongoDB原始碼優化
- mongodb原始碼實現、調優、最佳實踐系列-數百萬行mongodb核心原始碼閱讀經驗分享MongoDB原始碼
- mongodb核心原始碼實現及效能最佳化:transport_layer網路傳輸層模組原始碼實現二MongoDB原始碼
- mongodb核心原始碼實現及效能最佳化系列:Mongodb特定場景效能數十倍提升最佳化實踐MongoDB原始碼
- 萬字長文 | MongoDB絡傳輸處理原始碼實現及效能調優MongoDB原始碼
- mongodb網路傳輸處理原始碼實現及效能調優-體驗核心效能極致設計MongoDB原始碼
- mongodb核心原始碼實現、效能調優系列-為何要對開源mongodb資料庫核心做二次開發MongoDB原始碼資料庫
- Mongodb write寫(增、刪、改)模組原始碼實現MongoDB原始碼
- mongodb核心transport_layer網路傳輸層模組原始碼實現三MongoDB原始碼
- mongodb核心transport_layer 網路傳輸層模組原始碼實現四MongoDB原始碼
- mongodb核心原始碼實現及效能最佳化:常用高併發執行緒模型設計及mongodb執行緒模型最佳化實踐MongoDB原始碼執行緒模型
- 70行實現Promise核心原始碼Promise原始碼
- FPGA排序模組與verilog實現【含原始碼!!!】FPGA排序原始碼
- QT Widgets模組原始碼解析與實踐QT原始碼
- Vue原始碼探究-核心類的實現Vue原始碼
- 最佳實踐 | 原始碼升級gcc原始碼GC
- Promise原始碼實現Promise原始碼
- 影片直播系統原始碼,非同步處理實現程式碼分析原始碼非同步
- 讀書APP原始碼,搜尋欄模糊處理實現APP原始碼
- 多合一收款二維碼原理及實現(原始碼)原始碼
- [Redis原始碼閱讀]實現一個redis命令--nonzerodecrRedis原始碼
- Axios 原始碼解讀 —— 原始碼實現篇iOS原始碼
- 婚戀app原始碼開發,如何實現介面效能優化?APP原始碼優化
- webpack Hmr 原始碼實現Web原始碼
- HashMap原始碼實現分析HashMap原始碼
- 仿Express原始碼實現(-)Express原始碼
- Redis核心原理與實踐--事務實踐與原始碼分析Redis原始碼
- 手動實現一個promise(原始碼)Promise原始碼
- 實現語音社交原始碼介面效能優化,從索引入手原始碼優化索引
- Dubbo 實現原理與原始碼解析系列 —— 精品合集原始碼
- Spark 原始碼系列(七)Spark on yarn 具體實現Spark原始碼Yarn
- 原始碼|ThreadLocal的實現原理原始碼thread