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

y123456yzzyz 發表於 2020-12-04

關於作者

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

1. command 預設介面類核心程式碼實現及基本介面功能說明

每個命令都對應一個command 基類,該類中完成命令的一些基本介面功能初始化,核心介面實現如下 :

1. //命令模組基類基礎介面初始化實現  
2. class Command : public CommandInterface {  
3. public:  
4.     //獲取集合名collection  
5.     static std::string parseNsFullyQualified(...);  
6.     //獲取DB.COLLECTION  
7.     static NamespaceString parseNsCollectionRequired(...);  
8.     //map表結構  
9.     using CommandMap = StringMap<Command*>;  
10.     ......  
11.     //獲取命令名  
12.     const std::string& getName() const final {  
13.         return _name;  
14.     }  
15.     ......  
16.     //應答保留填充欄位長度  
17.     std::size_t reserveBytesForReply() const override {  
18.         return 0u;  
19.     }  
20.     //該命令是否只能在admin庫執行,預設不可以  
21.     bool adminOnly() const override {  
22.         return false;  
23.     }  
24.     //該命令是否需要許可權認證檢查?預設不需要  
25.     bool localHostOnlyIfNoAuth() override {  
26.         return false;  
27.     }  
28.     //該命令執行後是否進行command操作計數  
29.     bool shouldAffectCommandCounter() const override {  
30.         return true;  
31.     }  
32.     //該命令是否需要認證  
33.     bool requiresAuth() const override {  
34.         return true;  
35.     }  
36.     //help幫助資訊  
37.     void help(std::stringstream& help) const override;  
38.     //執行計劃資訊  
39.     Status explain(...) const override;  
40.     //日誌資訊相關  
41.     void redactForLogging(mutablebson::Document* cmdObj) override;  
42.     BSONObj getRedactedCopyForLogging(const BSONObj& cmdObj) override;  
43.     //該命令是否為maintenance模式,預設false  
44.     bool maintenanceMode() const override {  
45.         return false;  
46.     }  
47.     //maintenance是否支援,預設支援  
48.     bool maintenanceOk() const override {  
49.         return true;   
50.     }  
51.     //本地是否支援非本地ReadConcern,預設不支援  
52.     bool supportsNonLocalReadConcern(...) const override {  
53.         return false;  
54.     }  
55.     //是否允許AfterClusterTime,預設允許  
56.     bool allowsAfterClusterTime(const BSONObj& cmdObj) const override {  
57.         return true;  
58.     }  
59.     //3.6版本預設opCode=OP_MSG,所以對應邏輯操作op為LogicalOp::opCommand  
60.     LogicalOp getLogicalOp() const override {  
61.         return LogicalOp::opCommand;  
62.     }  
63.     //例如find就是kRead,update  delete insert就是kWrite,非讀寫操作就是kCommand  
64.     ReadWriteType getReadWriteType() const override {  
65.         return ReadWriteType::kCommand;  
66.     }  
67.     //該命令執行成功統計  
68.     void incrementCommandsExecuted() final {  
69.         _commandsExecuted.increment();  
70.     }  
71.   
72.     //該命令執行失敗統計  
73.     void incrementCommandsFailed() final {  
74.         _commandsFailed.increment();  
75.     }  
76.   
77.     //真正得命令執行  
78.     bool publicRun(OperationContext* opCtx, const OpMsgRequest& request, BSONObjBuilder& result);  
79.   
80.     //獲取支援的所有命令資訊  ListCommandsCmd獲取所有支援的命令 db.listCommands()  
81.     static const CommandMap& allCommands() {  
82.         return *_commands;  
83.     }  
84.   
85.     //沒用  
86.     static const CommandMap& allCommandsByBestName() {  
87.         return *_commandsByBestName;  
88.     }  
89.   
90.     //收到不支援命令的統計,例如mongo shell敲一個mongodb無法識別得命令,這裡就會統計出來  
91.     static Counter64 unknownCommands;  
92.     //根據命令字串名查詢對應命令  
93.     static Command* findCommand(StringData name);  
94.     //執行結果  
95.     static void appendCommandStatus(...);  
96.     //是否啟用了command test功能  
97.     static bool testCommandsEnabled;  
98.     //help幫助資訊  
99.     static bool isHelpRequest(const BSONElement& helpElem);  
100.     static const char kHelpFieldName[];  
101.     //認證檢查,檢查是否有執行該命令得許可權  
102.     static Status checkAuthorization(Command* c,  
103.                                      OperationContext* opCtx,  
104.                                      const OpMsgRequest& request);  
105.     ......  
106. private:  
107.     //新增地方見Command::Command(    
108.     //所有的command都在_commands中儲存  
109.     static CommandMap* _commands;  
110.     //暫時沒用  
111.     static CommandMap* _commandsByBestName;  
112.     //執行對應命令run介面  
113.     virtual bool enhancedRun(OperationContext* opCtx,  
114.                              const OpMsgRequest& request,  
115.                              BSONObjBuilder& result) = 0;  
116.     //db.serverStatus().metrics.commands命令檢視,本命令的執行統計,包括執行成功和執行失敗的  
117.     Counter64 _commandsExecuted;   
118.     Counter64 _commandsFailed;  
119.    //命令名,如"find" "insert" "update" "createIndexes" "deleteIndexes"  
120.     const std::string _name;  
121.   
122.     //每個命令執行是否成功通過MetricTree管理起來,也就是db.serverStatus().metrics.commands統計資訊  
123.     //通過MetricTree特殊二叉樹管理起來  
124.     ServerStatusMetricField<Counter64> _commandsExecutedMetric;  
125.     ServerStatusMetricField<Counter64> _commandsFailedMetric;  
};

command 作為預設介面類,主要完成一些命令基本介面初始化操作及預設配置設定,該類最基本的介面主要如下:

reserveBytesForReply

ReserveBytesForReply() 介面主要完成該命令應答填充欄位長度,預設值為 0 。對應命令可以在具體命令類中修改。

adminOnly

該命令是否只能在admin 庫操作,預設為 false 。也可以在對應命令繼承類中修改,例如 "moveChunk" 命令則在 MoveChunkCommand 繼承類中設定為 true ,也就是該命令只能在 admin 庫操作。

localHostOnlyIfNoAuth

該命令是否支援在例項所在本機不認證操作,預設值false 。對應命令可以在具體繼承類中修改。

shouldAffectCommandCounter

該命令是否需要command 統計,也就是 mongostat 中的 command 統計計數是否需要使能。預設值 true ,也就是該命令會進行 command 計數統計。對應命令可以在具體繼承類中修改。

requiresAuth

該命令是否需要認證,預設為true 。對應命令可以在具體繼承類中修改。

allowsAfterClusterTime

該命令是否支援AfterClusterTime ,預設為 true 。對應命令可以在具體繼承類中修改。

getLogicalOp

該命令是否為邏輯opCommand 命令。 3.6 版本預設 opCode=OP_MSG, 所以對應邏輯操作 op LogicalOp::opCommand

getReadWriteType

如果為讀命令則type 對應 kRead ,寫命令 type 對應 kWrite ,其他讀寫以外的命令對應 kCommand

incrementCommandsExecuted

該命令執行成功統計,通過db.serverStatus().metrics.commands 獲取該命令統計。

_commandsFailed

該命令執行失敗統計,通過db.serverStatus().metrics.commands 獲取該命令統計。

以上列舉除了command 基類的幾個核心功能預設值資訊,如果繼承類中沒有修改這些介面值,則該命令對應功能就是這些預設值。

說明:各種不同命令如果不適用command 基類的預設介面,則可以在繼承類中修改對應介面值即可更改對應功能。

命令除了上面提到的基本功能是否支援外,command 類還有其他幾個核心介面功能。例如,該命令是否認證成功、是否有操作許可權、允許對應 run 命令等。 command 類函式介面功能總結如下表所示:

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

2.  命令run

結合《命令處理模組原始碼實現一》和本章節對command 處理流程可以得出, runCommandImpl 介面通過如下呼叫流程最終執行特定命令的run 介面,這裡以 insert 寫入和讀取流程為例, mongod 例項寫入呼叫過程如下圖所示 :


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

最終,mongod mongos 例項呼叫相關命令得 run 介面完成具體的 command 命令處理操作。 mongos mongod(shardServer) mongod(configServer) 相關常用的操作命令 ( 以最基本的讀寫命令為例 ) 入口及功能說明總結如下表所示:

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

3. command 模組統計資訊

mongodb command 命令處理模組相關統計包含三類:單個命令統計、彙總型統計、讀寫時延統計。其中,單個命令統計針對所有接受到的命令名字串進行統計,彙總型統計則是把同一型別的命令總結為一個整體統計 ( 例如 commands 統計 )

3.1 單個命令統計

mongodb 會收集所有操作命令執行結果,如果本次命令執行成功,則該命令成功統計自增加 1 ,同理如果該命令執行過程失敗,則失敗統計自增加 1 ,這些統一歸類為 單個命令統計資訊

單個命令統計由command 類的 _commandsExecuted _commandsFailed 實現命令執行成功統計和失敗統計,相關核心程式碼實現如下:

1. //該命令執行成功統計  
2. void incrementCommandsExecuted() final {  
3.     _commandsExecuted.increment();  
4. }  
5.   
6. //該命令執行失敗統計  
7. void incrementCommandsFailed() final {  
8.     _commandsFailed.increment();  
9. }  
 
1. //命令入口  
2. void execCommandDatabase(...)  
3. {  
4.     ......  
5.     //該命令執行次數統計  db.serverStatus().metrics.commands可以獲取統計資訊  
6.     command->incrementCommandsExecuted();  
7.     ......  
8.     //真正的命令執行在這裡面  
9.     retval = runCommandImpl(opCtx, command, request, replyBuilder, startOperationTime);  
10.   
11.     //該命令失敗次數統計  
12.     if (!retval) {  
13.         command->incrementCommandsFailed();  
14.     }  
15.     ......  
}


mongodb 預設會統計每個客戶端發往服務端的命令,即使是無法識別的命令也會統計,命令統計可以通過 db.serverStatus().metrics.commands 獲取,如下圖所示:

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

3.2 彙總型 commands 命令統計

從前面的單個命令統計可以看出,單個命令會記錄所有傳送給mongodb 的命令資訊。 mongodb 支援的命令百餘個,由於命令眾多,因此 mongodb 為了更加直觀明瞭的獲取統計資訊,除了提供單個命令統計外,還對外提供彙總型命令統計。

彙總型命令統計可以通過db.serverStatus().opcounters 命令獲取, mongostat 中的增刪改查等資訊也來自於該統計,如下圖:

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

從上圖可以看出,整個mongostat 監控統計可以歸類為小表:

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

insert delete update find 分別對應增刪改查四個命令操作, getMore 對應批量遊標操作命令。這五個命令,對應命令執行的時候統計資訊自增,核心程式碼實現如下:

3.2.1 insert 操作統計

insert 操作統計在代理 mongos 和分片儲存節點 mongod 都會統計,兩種角色的 insert 統計核心程式碼如下:

1.  代理mongos insert 統計核心程式碼實現

1.    bool insertBatchAndHandleErrors(...) {  
2.     ......  
3.     //一次性一條一條插入,上面的固定集合是一次性插入  
4.     for (auto it = batch.begin(); it != batch.end(); ++it) {  
5.         //insert操作計數  
6.         globalOpCounters.gotInsert();   
7.     }  
8.     ......  
9. }

2.  分片儲存節點mongod insert 統計核心程式碼實現

1. //mongod代理insert統計核心流程  
2. bool ClusterWriteCmd::enhancedRun(...) {  
3.     ......  
4.     if (_writeType == BatchedCommandRequest::BatchType_Insert) {  
5.         //insert計數  
6.         for (size_t i = 0; i < numAttempts; ++i) {  
7.             globalOpCounters.gotInsert();  
8.         }  
9.     }  
10.     ......  
11. }

3.2.2 query 操作統計

1.  代理mongos query 統計核心程式碼實現:

1.     bool ClusterFindCmd::run(...) {  
2.     //find操作統計,也就是query統計  
3.     globalOpCounters.gotQuery();  
4.     ......  
5. }

2.  分片儲存節點mongod query 統計核心程式碼實現:

1. bool FindCmd::run(...) {  
2.     //find操作統計,也就是query統計  
3.     globalOpCounters.gotQuery();  
4.     ......  
5. }

3.2.3 update 操作統計

1.  代理mongos update 統計核心程式碼實現 :

1. bool ClusterWriteCmd::enhancedRun(...) {  
1.     //update操作統計
2.     globalOpCounters.gotUpdate();  
3.     ......  
4. }

2.  分片儲存節點mongod update 統計核心程式碼實現 :

1. //mongod代理update統計核心流程  
2. bool ClusterWriteCmd::enhancedRun(...) {  
3.     ......  
4.     if (_writeType == BatchedCommandRequest::BatchType_Update) {  
5.         //insert計數  
6.     for (size_t i = 0; i < numAttempts; ++i) {  
7.          globalOpCounters.gotUpdate();  
8.        }  
9.     }  
10.     ......  
11. }

3.2.4 delete 操作統計

1.  代理mongos delete 統計核心程式碼實現 :

1. //mongos代理update統計核心流程  
2. bool ClusterWriteCmd::enhancedRun(...) {  
3.     ......  
4.     if (_writeType == BatchedCommandRequest::BatchType_Update) {  
5.         //insert計數  
6.     for (size_t i = 0; i < numAttempts; ++i) {  
7.          globalOpCounters.gotUpdate();  
8.        }  
9.     }  
10.     ......  
11. }

2.  分片儲存節點mongod delete 統計核心程式碼實現 :

1. static SingleWriteResult performSingleDeleteOp(...) {  
2.     ......  
3.     //分片mongod例項delete操作統計  
4.     globalOpCounters.gotDelete();  
5.     ......  
6. }

3.2.5 getMore 操作統計

1.  代理mongos getMore 統計核心程式碼實現 :

1. //mongos代理getMore統計核心流程  
2. bool ClusterGetMoreCmd::enhancedRun(...) {  
3.     //代理getMore統計  
4.     globalOpCounters.gotGetMore();  
5.     ......  
6. }

2.  分片儲存節點mongod getMore 統計核心程式碼實現 :

1.  //mongod分片儲存節點getMore統計  

2.  bool ClusterGetMoreCmd::enhancedRun(...) {  

3.      //儲存節點mongod getMore統計  

4.      globalOpCounters.gotGetMore();  

5.      ......  

6.  }  

3.2.6 command 操作統計

前面五種操作統計都很好理解,commands 統計由那些命令操作組成,本節將重點分析 commands 如何實現統計。 commands 統計核心程式碼實現如下:

1.  代理mongos commands 統計核心程式碼實現 :

1. //mongos代理commands統計  
2. void execCommandDatabase(...) {  
3.     ......  
4.     if (command->shouldAffectCommandCounter()) {  
5.         OpCounters* opCounters = &globalOpCounters;  
6.         opCounters->gotCommand();  
7.     }  
8.     ......  
9. }

2.  儲存節點mongod commands 統計核心程式碼實現:

1. void execCommandDatabase(...) {  
2.     ......  
3.     //是否進行command統計  
4.     if (command->shouldAffectCommandCounter()) {  
5.         OpCounters* opCounters = &globalOpCounters;  
6.         //commands計數自增
7.         opCounters->gotCommand();  
8.     }  
9.     ......  
10. }

從上面的程式碼可以看出,只有對應命令類中 shouldAffectCommandCounter () true 的命令才會進行 commands 計數。前面章節中我們提到,所有命令都有一個對應類實現相應功能,所有命令實現類都繼承一個功能 class command {} 類,該類對 shouldAffectCommandCounter () 介面進行初始化。程式碼實現如下:

1. class Command : {  
2.     ......  
3.     //該命令是否進行command操作計數,預設需要。如果不需要進行command統計,可在命令繼承類中置為false  
4.     bool shouldAffectCommandCounter() const override {  
5.         return true;  
6.     }  
7.     ......  
8. }

該介面預設為true ,如果對應命令不需要進行 commands 計數統計,則需要在對應命令實現類中把該介面置為 false 。通過分析程式碼,可以看出,只有以下命令子類把 shouldAffectCommandCounter () 介面設定為 false ,搜尋結果如下 :

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

分析程式碼可以得出如下結論:

1)  mongos 代理中的 clase Cluster_find_cmd { } 類和 class Cluster_getmore_cmd {} 類的 shouldAffectCommandCounter () 介面置為 false ,這兩個類分別對應代理的“ find ”和“ getMore ”命令操作,也就說明 mongos 代理 de 這兩個命令操作不會統計到 commands 中。

2)  mongod 分片儲存節點的 clase Find_cmd{} class Getmore_cmd {} class Write_commands{} 三個類中把 shouldAffectCommandCounter () 介面置為 false 。這三個類分別對應 mongod 儲存例項的如下幾個命令:“ find ”、“ getMore ”、“ insert ”、“ update ”、“ delete ”五個命令。

  mongos mongod 例項 commands 統計資訊總結如下:

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

3.3 慢日誌、時延統計

每次客戶端請求執行實踐如果超過了log level 配置的最大慢日誌時間,則會把該操作詳細資訊記錄下來,同時把本操作執行時間新增到對應的讀或者寫計數及時延統計中。命令處理模組中,時延相關統計包括以下兩種統計:

①  慢日誌統計

②  讀寫計數及時延統計

3.3.1 慢日誌統計

當啟用了慢日誌記錄功能後,mongod 會把執行時間超過指定閥值的慢日誌記錄下來。慢日誌預設記錄到服務日誌檔案 ( systemLog.path 配置項設定) ,同時會記錄日誌到 ”system.profile” 集合中。慢日誌核心程式碼實現如下:

1. DbResponse ServiceEntryPointMongod::handleRequest(...) {  
2.     ......  
3.     //記錄開始時間  
4.     //獲取當前操作對應curop  
5.     CurOp& currentOp = *CurOp::get(opCtx);  
6.   
7.     ......  
8.     //執行請求對應命令  
9.     runCommands(opCtx, m);  
10.     ......  
11.   
12.     //記錄結束時間  
13.     currentOp.ensureStarted();  
14.     currentOp.done();   
15.     //獲取開始和結束時間差,也就是命令執行時間  
16.     debug.executionTimeMicros = durationCount<Microseconds>  
17.                  (currentOp.elapsedTimeExcludingPauses());  
18.     //記錄超過閥值的慢日誌到日誌檔案  
19.     if (shouldLogOpDebug || (shouldSample && debug.executionTimeMicros > logThresholdMs * 1000LL)) {  
20.         ......  
21.     //記錄慢日誌到日誌檔案  
22.         log() << debug.report(&c, currentOp, lockerInfo.stats);   
23.     }  
24.     //記錄慢日誌到system.profile集合   
25.     if (currentOp.shouldDBProfile(shouldSample)) {  
26.         ......  
27.         //記錄慢日誌到system.profile集合中  
28.         profile(opCtx, op);  
29.     }  
30.     ......  
31. }


3.3.2 讀寫操作計數及時延統計

根據請求command 命令型別 ( 包含讀命令、寫命令、 command 命令 ) ,以及命令執行時間,可以計算出不同型別命令的讀寫執行時間,從而計算出叢集的讀時延、寫時延、 command 時延。 mongodb 所有命令可以歸納為讀、寫、 command 三類,核心程式碼如下:

1. //獲取操作型別  
2. Command::ReadWriteType CurOp::getReadWriteType() const {  
3.     ......  
4.     switch (_logicalOp) {  
5.     //getmore find歸納為讀  
6.         case LogicalOp::opGetMore:  
7.         case LogicalOp::opQuery:  
8.             return Command::ReadWriteType::kRead;  
9.     //增刪改統一歸納為寫  
10.         case LogicalOp::opUpdate:  
11.         case LogicalOp::opInsert:  
12.         case LogicalOp::opDelete:  
13.             return Command::ReadWriteType::kWrite;  
14.     //增刪改以外的歸納為command  
15.         default:  
16.             return Command::ReadWriteType::kCommand;  
17.     }  
18. }

從上面的程式碼可以看出,讀、寫、command 分別對應以下命令:

(read) :包含getMore find

(write) :包含insert delete update

command :讀和寫以外的所有命令。

命令執行完計算出命令執行時間後,mongod 例項會記錄下這個時延,累加到歷史統計 OperationLatencyHistogram 中,讀、寫、 command 操作計數及時延統計分別記錄到 _reads _writes _commands 三個變數成員中。該統計核心程式碼實現如下:

1. //Top::_incrementHistogram呼叫  
2. //操作和時延計數操作  
3. void OperationLatencyHistogram::increment(uint64_t latency, Command::ReadWriteType type) {  
4.     int bucket = _getBucket(latency);  
5.     switch (type) {  
6.         //讀時延累加,讀計數自增  
7.         case Command::ReadWriteType::kRead:  
8.             _incrementData(latency, bucket, &_reads);  
9.             break;  
10.         //寫時延累加,寫計數自增    
11.         case Command::ReadWriteType::kWrite:  
12.             _incrementData(latency, bucket, &_writes);  
13.             break;  
14.         //command時延累加,command計數自增    
15.         case Command::ReadWriteType::kCommand:  
16.             _incrementData(latency, bucket, &_commands);  
17.             break;  
18.         default:  
19.             MONGO_UNREACHABLE;  
20.     }  
21. }

命令請求執行過程及其對應的讀寫請求操作計數、時延累加銜接程式碼實現如下:

1. DbResponse ServiceEntryPointMongod::handleRequest(...) {  
2.     ......  
3.     //記錄開始時間  
4.     //獲取當前操作對應curop  
5.     CurOp& currentOp = *CurOp::get(opCtx);  
6.   
7.     ......  
8.     //執行請求對應命令  
9.     runCommands(opCtx, m);  
10.     ......  
11.   
12.     //記錄結束時間  
13.     currentOp.ensureStarted();  
14.     currentOp.done();   
15.     //獲取開始和結束時間差,也就是命令執行時間  
16.     debug.executionTimeMicros = durationCount<Microseconds>  
17.                  (currentOp.elapsedTimeExcludingPauses());  
18.     ......  
19.     //記錄慢日誌到system.profile集合   
20.     //mongod讀寫的時間延遲統計  db.serverStatus().opLatencies獲取   
21.     Top::get(opCtx->getServiceContext())  
22.         .incrementGlobalLatencyStats(   //讀寫統計
23.             opCtx,  
24.              //時延             
25.             durationCount<Microseconds>(currentOp.elapsedTimeExcludingPauses()),  
26.             currentOp.getReadWriteType());  //讀寫型別
27.     ......  
28. }

mongod 例項讀、寫、 command 操作計數及其各自時延統計可以通過 db.serverStatus() 介面獲取,使用者可用取樣來計算對應的 tps 和平均時延資訊。獲取操作統計和時延統計的命令如下 :

db.serverStatus().opLatencies

4.  問題回顧

    Mongodb command 命令處理模組原始碼實現一》一文中提到的 commands 統計資訊到這裡就可以得到答案了,如下表所示:

  mongos mongod 例項 commands 統計資訊總結如下:

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

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

mongos 代理 mongostat 統計可以彙總為下圖所示:

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

mongod 代理 mongostat 統計可以彙總為下圖所示:

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

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

相關文章