mongodb核心原始碼實現、效能調優、最佳運維實踐系列-command命令處理模組原始碼實現一

y123456yzzyz發表於2020-11-12

關於作者

前滴滴出行技術專家,現任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. }

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/,如需轉載,請註明出處,否則將追究法律責任。

相關文章