DRDS分佈式SQL引擎—執行計劃介紹

摘要: 本文着重介紹 DRDS 執行計劃中各個操作符的含義,以便用戶通過查詢計劃瞭解 SQL 執行流程,從而有針對性的調優 SQL。

DRDS分佈式SQL引擎 — 執行計劃介紹

前言

數據庫系統中,執行計劃是對 SQL 如何執行的形式化表示,往往由若干關係操作符構成,用戶可以通過對應的 EXPLAIN 命令查看,並通過執行計劃大致瞭解 SQL 的執行過程和執行方式,如全表掃描還是索引掃描,歸併連接還是哈希連接等。執行計劃可以爲用戶進行 SQL 調優提供重要依據。

DRDS 執行計劃

與多數數據庫系統類似,DRDS 在處理 SQL 時,會通過優化器生成執行計劃,該執行計劃由關係操作符構成一個樹形結構,反映 DRDS 如何執行 SQL 語句;不同的是,DRDS 本身不存儲數據,更側重考慮分佈式環境中的網絡 IO 開銷,將運算下推到各個分庫(如 RDS/MySQL)執行,從而提升 SQL 執行效率。用戶可通過 EXPLAIN 命令查看 SQL 的執行計劃。

本文着重介紹 DRDS 執行計劃中各個操作符的含義,以便用戶通過查詢計劃瞭解 SQL 執行流程,從而有針對性的調優 SQL。文中示例均基於如下表結構:

CREATE TABLE `sbtest1` (`id`  INT(10) UNSIGNED NOT NULL,`k`   INT(10) UNSIGNED NOT NULL DEFAULT '0',`c`   CHAR(120)        NOT NULL DEFAULT '',`pad` CHAR(60)         NOT NULL DEFAULT '',KEY `xid` (`id`),KEY `k_1` (`k`)) dbpartition BY HASH (`id`) tbpartition BY HASH (`id`) tbpartitions 4

先通過一個例子整體瞭解 DRDS 執行計劃的樹形結構。

mysql> explain select a.k, count(*) cnt from sbtest1 a, sbtest1 b where a.id = b.k and a.id > 1000 group by k having cnt > 1300 order by cnt limit 5, 10;+---------------------------------------------------------------------------------------------------------------------------------------------------+| LOGICAL PLAN                                                                                                                                      |+---------------------------------------------------------------------------------------------------------------------------------------------------+| TmpSort(sort="cnt ASC", offset=?2, fetch=?3)                                                                                                      ||   Filter(condition="cnt > ?1")                                                                                                                    ||     Aggregate(group="k", cnt="COUNT()")                                                                                                           ||       BKAJoin(id="id", k="k", c="c", pad="pad", id0="id0", k0="k0", c0="c0", pad0="pad0", condition="id = k", type="inner")                       ||         MergeSort(sort="k ASC")                                                                                                                   ||           LogicalView(tables="[0000-0031].sbtest1_[000-127]", shardCount=128, sql="SELECT * FROM `sbtest1` WHERE (`id` > ?) ORDER BY `k`")        ||         UnionAll(concurrent=true)                                                                                                                 ||           LogicalView(tables="[0000-0031].sbtest1_[000-127]", shardCount=128, sql="SELECT * FROM `sbtest1` WHERE ((`k` > ?) AND (`k` IN ('?')))") || HitCache:false                                                                                                                                    |+---------------------------------------------------------------------------------------------------------------------------------------------------+9 rows in set (0.01 sec)

如上,DRDS EXPLAIN 的結果總體分爲兩部分:執行計劃和其他信息。

  • 執行計劃

    執行計劃以縮進形式表示操作符之間的 "父-子" 關係。示例中,Filter 是 TmpSort 的子操作符,同時是 Aggregate 的父操作符。從真正執行的角度看,每個操作符均從其子操作符中獲取數據,經當前操作符處理,輸出給其父操作符。爲方便理解,將以上執行計劃轉換爲更加直觀的樹形結構:


  • 其他信息

    除執行計劃外,EXPLAIN 結果中還會有一些額外信息,目前僅有一項 `HitCache` 。需要說明的是,DRDS 會默認開啓 PlanCache 功能,`HitCache` 表示當前 SQL 是否命中 PlanCache。

    開啓 PlanCache 後,DRDS 會對 SQL 做參數化處理,參數化會將 SQL 中的大部分常量用 ? 替換,並構建一個參數列表。在執行計劃中的體現就是,LogicalView 的 sql 中會有 ? ,在部分操作符中會有類似 ?2 的字樣,這裏的 2 表示其在參數列表中的下標,後續會結合具體的例子進一步闡述。

EXPLAIN 語法

EXPLAIN 用於查看 SQL 語句的執行計劃,語法如下:

EXPLAIN explainable_stmtexplainable_stmt: {SELECT statement| DELETE statement| INSERT statement| REPLACE statement| UPDATE statement}

操作符介紹

本小節詳細介紹 DRDS 執行計劃中各個操作符的含義。

LogicalView

LogicalView 是從底層數據源獲取數據的操作符。從數據庫的角度來看,使用 TableScan 命名更符合常規,但考慮到 DRDS 本身不存儲數據,而是通過 SQL 從底層數據源獲取,因此,該操作符中會記錄下推的 SQL 語句和數據源信息,這更像一個 "視圖"。該 "視圖" 中的 SQL,通過優化器的下推,可能包含多種操作,如投影、過濾、聚合、排序、連接和子查詢等。

以下通過示例說明 EXPLAIN 中 LogicalView 的輸出信息及其含義:

mysql> explain select * From sbtest1 where id > 1000;+-----------------------------------------------------------------------------------------------------------------------+| LOGICAL PLAN                                                                                                          |+-----------------------------------------------------------------------------------------------------------------------+| UnionAll(concurrent=true)                                                                                             ||   LogicalView(tables="[0000-0031].sbtest1_[000-127]", shardCount=128, sql="SELECT * FROM `sbtest1` WHERE (`id` > ?)") || HitCache:false                                                                                                        |+-----------------------------------------------------------------------------------------------------------------------+3 rows in set (0.00 sec)

LogicalView 的信息由三部分構成:

  • tables:底層數據源對應的表名,以 . 分割,其前是分庫對應的編號,其後是表名及其編號,對於連續的編號,會做簡寫,如 [000-127],表示表名編號從 000 到 127 的所有表。

  • shardCount:需要訪問的分表總數,該示例中會訪問從 000 到 127 共 128 張分表。

  • sql:下發至底層數據源的 SQL 模版。這裏顯示的並非真正下發的 SQL 語句,DRDS 在執行時會將表名替換爲物理表名;另外,SQL 中的常量 10 被 ? 替換,這是因爲 DRDS 默認開啓了 PlanCache 功能,對 SQL 做了參數化處理。

UnionAll

UnionAll 是 UNION ALL 對應的操作符,該操作符通常有多個輸入,表示將多個輸入的數據 UNION 在一起。以上示例中,LogicalView 之上的 UnionAll 表示將所有分表中的數據進行 UNION。

UnionAll 中的 concurrent 表示是否並行執行其子操作符,默認爲 true。

UnionDistinct

與 UnionAll 類似,UnionDistinct 是 UNION DISTINCT 對應的操作符。如下:

mysql> explain select * From sbtest1 where id > 1000 union distinct select * From sbtest1 where id < 200;+-------------------------------------------------------------------------------------------------------------------------+| LOGICAL PLAN                                                                                                            |+-------------------------------------------------------------------------------------------------------------------------+| UnionDistinct(concurrent=true)                                                                                          ||   UnionAll(concurrent=true)                                                                                             ||     LogicalView(tables="[0000-0031].sbtest1_[000-127]", shardCount=128, sql="SELECT * FROM `sbtest1` WHERE (`id` > ?)") ||   UnionAll(concurrent=true)                                                                                             ||     LogicalView(tables="[0000-0031].sbtest1_[000-127]", shardCount=128, sql="SELECT * FROM `sbtest1` WHERE (`id` < ?)") || HitCache:false                                                                                                          |+-------------------------------------------------------------------------------------------------------------------------+6 rows in set (0.02 sec)

MergeSort

MergeSort,歸併排序操作符,通常有多個子操作符。DRDS 中實現了兩種排序:基於有序數據的歸併排序和對無序數據的內存排序。如下:

mysql> explain select *from sbtest1 where id > 1000 order by id limit 5,10;+---------------------------------------------------------------------------------------------------------------------------------------------------+| LOGICAL PLAN                                                                                                                                      |+---------------------------------------------------------------------------------------------------------------------------------------------------+| MergeSort(sort="id ASC", offset=?1, fetch=?2)                                                                                                     ||   LogicalView(tables="[0000-0031].sbtest1_[000-127]", shardCount=128, sql="SELECT * FROM `sbtest1` WHERE (`id` > ?) ORDER BY `id` LIMIT (? + ?)") || HitCache:false                                                                                                                                    |+---------------------------------------------------------------------------------------------------------------------------------------------------+3 rows in set (0.00 sec)

MergeSort 操作符包含三部分內容:

  • sort:表示排序字段以及排列順序,id ASC 表示按照 id 字段遞增排序,DESC 表示遞減排序。

  • offset:表示獲取結果集時的偏移量,同樣由於對 SQL 做了參數化,示例中的 offst 表示爲 ?1 ,其中 ? 表示這是一個動態參數,其後的數字對應參數列表的下標。示例中 SQL 對應的參數爲 [1000, 5, 10],因此,?1 實際對應的值爲 5

  • fetch:表示最多返回的數據行數。與 offset 類似,同樣是參數化的表示,實際對應的值爲 10

Aggregate

Aggregate 是聚合操作符,通常包含兩部分內容:Group By 字段和聚合函數。如下:

mysql> explain select k, count(*) from sbtest1 where id > 1000 group by k;+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+| LOGICAL PLAN                                                                                                                                                                |+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+| Aggregate(group="k", count(*)="SUM(count(*))")                                                                                                                              ||   MergeSort(sort="k ASC")                                                                                                                                                   ||     LogicalView(tables="[0000-0031].sbtest1_[000-127]", shardCount=128, sql="SELECT `k`, COUNT(*) AS `count(*)` FROM `sbtest1` WHERE (`id` > ?) GROUP BY `k` ORDER BY `k`") || HitCache:true                                                                                                                                                               |+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+4 rows in set (0.00 sec)

Aggregate 包含兩部分內容:

  • group:表示 GROUP BY 字段,示例中爲 k 。

  • 聚合函數:= 前爲聚合函數對應的輸出列名,其後爲對應的計算方法。示例中 count(*)="SUM(count(*))" ,第一個 count(*) 對應輸出的列名,隨後的 SUM(count(*)) 表示對其輸入數據中的 count(*) 列進行 SUM 運算得到最終的 count(*)

由此可見,DRDS 將聚合操作分爲兩部分,首先將聚合操作下推至底層數據源做局部聚合,最終在 DRDS 層面對局部聚合的結果做全局聚合。另外,DRDS 的最終聚合是基於排序做的,因此,會在優化器階段爲其添加一個 Sort 子操作符,而 Sort 操作符又進一步通過下推 Sort 轉換爲 MergeSort

再來看一個 AVG 聚合函數的例子,如下:

mysql> explain select k, avg(id) avg_id from sbtest1 where id > 1000 group by k;+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+| LOGICAL PLAN|+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+| Project(k="k", avg_id="sum_pushed_sum / sum_pushed_count")||   Aggregate(group="k", sum_pushed_sum="SUM(pushed_sum)", sum_pushed_count="SUM(pushed_count)")||     MergeSort(sort="k ASC")||       LogicalView(tables="[0000-0031].sbtest1_[000-127]", shardCount=128, sql="SELECT `k`, SUM(`id`) AS `pushed_sum`, COUNT(`id`) AS `pushed_count` FROM `sbtest1` WHERE (`id` > ?) GROUP BY `k` ORDER BY `k`")|| HitCache:false|+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+5 rows in set (0.01 sec)

DRDS 會將 AVG 聚合函數轉換爲 SUM / COUNT,再分別根據 SUM 和 COUNT 的下推規則,將其轉換爲局部聚合和全局聚合。用戶可自行嘗試瞭解其他聚合函數的執行計劃。

注意:DRDS 會將 DISTINCT 操作轉換爲 GROUP 操作,如下:

mysql> explain select distinct k from sbtest1 where id > 1000;+-----------------------------------------------------------------------------------------------------------------------------------------------------+| LOGICAL PLAN                                                                                                                                        |+-----------------------------------------------------------------------------------------------------------------------------------------------------+| Aggregate(group="k")                                                                                                                                ||   MergeSort(sort="k ASC")                                                                                                                           ||     LogicalView(tables="[0000-0031].sbtest1_[000-127]", shardCount=128, sql="SELECT `k` FROM `sbtest1` WHERE (`id` > ?) GROUP BY `k` ORDER BY `k`") || HitCache:false                                                                                                                                      |+-----------------------------------------------------------------------------------------------------------------------------------------------------+4 rows in set (0.02 sec)

TmpSort

TmpSort,表示在內存中對數據進行排序。與 MergeSort 的區別在於,MergeSort 可以有多個子操作符,且每個子操作符返回的數據都已經排序。TmpSort 僅有一個子操作符。

TmpSort 對應的查詢計劃信息與 MergeSort 一致,請參考 MergeSort。

Project

Project 表示投影操作,即從輸入數據中選擇部分列輸出,或者對某些列進行轉換(通過函數或者表達式計算)後輸出,當然,也可以包含常量。以上 AVG 的示例中,最頂層就是一個 Project,其輸出 k 和 sum_pushed_sum / sum_pushed_count ,後者對應的列名爲 avg_id 。

mysql> explain select '你好, DRDS', 1 / 2, CURTIME();+-------------------------------------------------------------------------------------+| LOGICAL PLAN                                                                        |+-------------------------------------------------------------------------------------+| Project(你好, DRDS="_UTF-16'你好, DRDS'", 1 / 2="1 / 2", CURTIME()="CURTIME()") ||                                                                                     || HitCache:false                                                                      |+-------------------------------------------------------------------------------------+3 rows in set (0.00 sec)

可見,Project 的計劃中包括每列的列名及其對應的列、值、函數或者表達式。

Filter

Filter 表示過濾操作,其中包含一些過濾條件。該操作符對輸入數據進行過濾,若滿足條件,則輸出,否則丟棄。如下是一個較複雜的例子,包含了以上介紹的大部分操作符。

mysql> explain select k, avg(id) avg_id from sbtest1 where id > 1000 group by k having avg_id > 1300;+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+| LOGICAL PLAN  |+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+| Filter(condition="avg_id > ?1")  ||   Project(k="k", avg_id="sum_pushed_sum / sum_pushed_count")  ||     Aggregate(group="k", sum_pushed_sum="SUM(pushed_sum)", sum_pushed_count="SUM(pushed_count)")  ||       MergeSort(sort="k ASC")  ||         LogicalView(tables="[0000-0031].sbtest1_[000-127]", shardCount=128, sql="SELECT `k`, SUM(`id`) AS `pushed_sum`, COUNT(`id`) AS `pushed_count` FROM `sbtest1` WHERE (`id` > ?) GROUP BY `k` ORDER BY `k`") || HitCache:false  |+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+6 rows in set (0.01 sec)

在以上 AVG 示例的 SQL 基礎上添加了having avg_id > 1300 ,執行計劃最上層添加了一個 Filter 操作符,用於過濾所有滿足 avg_id > 1300 的數據。

有讀者可能會問,WHERE 中的條件爲什麼沒有對應的 Filter 操作符呢?在 DRDS 優化器的某個階段,WHERE 條件的 Filter 操作符的確是存在的,只是最終將其下推到了 LogiacalView 中,因此可以在 LogicalView 的 sql 中看到 id > 1000 。

NlJoin

NlJoin,表示 NestLoop Join 操作符,即使用 NestLoop 方法進行兩表 Join。DRDS 中實現了兩種 JOIN 策略:NlJoin 和 BKAJoin,後者表示 Batched Key Access Join,批量鍵值查詢,會從左表取一批數據,構建一個 IN 條件拼接在訪問右表的 SQL 中,從右表一次獲取一批數據。

mysql> explain select a.* from sbtest1 a, sbtest1 b where a.id = b.k and a.id > 1000;+----------------------------------------------------------------------------------------------------------------------------+| LOGICAL PLAN                                                                                                               |+----------------------------------------------------------------------------------------------------------------------------+| Project(id="id", k="k", c="c", pad="pad")                                                                                  ||   NlJoin(id="id", k="k", c="c", pad="pad", k0="k0", condition="id = k", type="inner")                                      ||     UnionAll(concurrent=true)                                                                                              ||       LogicalView(tables="[0000-0031].sbtest1_[000-127]", shardCount=128, sql="SELECT * FROM `sbtest1` WHERE (`id` > ?)")  ||     UnionAll(concurrent=true)                                                                                              ||       LogicalView(tables="[0000-0031].sbtest1_[000-127]", shardCount=128, sql="SELECT `k` FROM `sbtest1` WHERE (`k` > ?)") || HitCache:false                                                                                                             |+----------------------------------------------------------------------------------------------------------------------------+7 rows in set (0.03 sec)

NlJOIN 的計劃包括三部分內容:

  • 輸出列信息:輸出的列名,示例中的 JOIN 會輸出 5 列 id="id", k="k", c="c", pad="pad", k0="k0" 。

  • contition:連接條件,示例中連接條件爲 id = k 。

  • type:連接類型,示例中是 INNER JOIN,因此其連接類型爲 inner 。

BKAJoin

BKAJoin,Batched Key Access Join,表示通過批量鍵值查詢的方式進行 JOIN,即從左表取一批數據,構建一個 IN 條件拼接在訪問右表的 SQL 中,從右表一次獲取一批數據進行 JOIN。

mysql> explain select a.* from sbtest1 a, sbtest1 b where a.id = b.k order by a.id;+-------------------------------------------------------------------------------------------------------------------------------+| LOGICAL PLAN                                                                                                                  |+-------------------------------------------------------------------------------------------------------------------------------+| Project(id="id", k="k", c="c", pad="pad")                                                                                     ||   BKAJoin(id="id", k="k", c="c", pad="pad", id0="id0", k0="k0", c0="c0", pad0="pad0", condition="id = k", type="inner")       ||     MergeSort(sort="id ASC")                                                                                                  ||       LogicalView(tables="[0000-0031].sbtest1_[000-127]", shardCount=128, sql="SELECT * FROM `sbtest1` ORDER BY `id`")        ||     UnionAll(concurrent=true)                                                                                                 ||       LogicalView(tables="[0000-0031].sbtest1_[000-127]", shardCount=128, sql="SELECT * FROM `sbtest1` WHERE (`k` IN ('?'))") || HitCache:false                                                                                                                |+-------------------------------------------------------------------------------------------------------------------------------+7 rows in set (0.01 sec)

BKAJoin 的計劃內容與 NlJoin 相同,這兩個操作符命名不同,旨在告知執行器以何種方法執行 JOIN 操作。另外,以上執行計劃中右表的 LogicalView 中 k IN ('?') 是優化器構建出來的對右表的IN查詢模板。

LogicalModifyView

如上文介紹,LogicalView 表示從底層數據源獲取數據的操作符,與之對應的,LogicalModifyView 表示對底層數據源的修改操作符,其中也會記錄一個 SQL 語句,該 SQL 可能是 INSERT、UPDATE 或者 DELETE。

mysql> explain update sbtest1 set c='Hello, DRDS' where id > 1000;+--------------------------------------------------------------------------------------------------------------------------------+| LOGICAL PLAN                                                                                                                   |+--------------------------------------------------------------------------------------------------------------------------------+| LogicalModifyView(tables="[0000-0031].sbtest1_[000-127]", shardCount=128, sql="UPDATE `sbtest1` SET `c` = ? WHERE (`id` > ?)") || HitCache:false                                                                                                                 |+--------------------------------------------------------------------------------------------------------------------------------+2 rows in set (0.03 sec)mysql> explain delete from sbtest1 where id > 1000;+-------------------------------------------------------------------------------------------------------------------------+| LOGICAL PLAN                                                                                                            |+-------------------------------------------------------------------------------------------------------------------------+| LogicalModifyView(tables="[0000-0031].sbtest1_[000-127]", shardCount=128, sql="DELETE FROM `sbtest1` WHERE (`id` > ?)") || HitCache:false                                                                                                          |+-------------------------------------------------------------------------------------------------------------------------+2 rows in set (0.03 sec)

LogicalModifyView 查詢計劃的內容與 LogicalView 類似,包括下發的物理分表,分表數以及 SQL 模版。同樣,由於開啓了 PlanCache,對 SQL 做了參數化處理,SQL 模版中的常量會用 ? 替換。

PhyTableOperation

PhyTableOperation 表示對某個物理分表執行一個操作。該操作符目前僅用於 INSERT INTO ... VALUES ...。

mysql> explain insert into sbtest1 values(1, 1, '1', '1'),(2, 2, '2', '2');+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+| LOGICAL PLAN                                                                                                                                                                                                 |+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+| PhyTableOperation(tables="SYSBENCH_CORONADB_1526954857179TGMMSYSBENCH_CORONADB_VGOC_0000_RDS.[sbtest1_001]", sql="INSERT INTO ? (`id`, `k`, `c`, `pad`) VALUES(?, ?, ?, ?)", params="`sbtest1_001`,1,1,1,1") || PhyTableOperation(tables="SYSBENCH_CORONADB_1526954857179TGMMSYSBENCH_CORONADB_VGOC_0000_RDS.[sbtest1_002]", sql="INSERT INTO ? (`id`, `k`, `c`, `pad`) VALUES(?, ?, ?, ?)", params="`sbtest1_002`,2,2,2,2") ||                                                                                                                                                                                                              || HitCache:false                                                                                                                                                                                               |+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+4 rows in set (0.00 sec)

示例中,INSERT 插入兩行數據,每行數據對應一個 PhyTableOperation 操作符,PhyTableOperation 操作符的內容包括三部分:

  • tables:物理表名,僅有唯一一個物理表名。

  • sql:SQL 模版,該 SQL 模版中表名和常量均被參數化,用 ? 替換,對應的參數在隨後的 params 中給出。

  • params:SQL 模版對應的參數,包括表名和常量。

其他信息

HitCache

DRDS 會默認開啓 PlanCache 功能,HitCache 用於告知用戶當前查詢是否命中 PlanCache。如下,第一次運行 HitCache 爲 false,第二次運行爲 true。

mysql> explain select * From sbtest1 where id > 1000;+-----------------------------------------------------------------------------------------------------------------------+| LOGICAL PLAN                                                                                                          |+-----------------------------------------------------------------------------------------------------------------------+| UnionAll(concurrent=true)                                                                                             ||   LogicalView(tables="[0000-0031].sbtest1_[000-127]", shardCount=128, sql="SELECT * FROM `sbtest1` WHERE (`id` > ?)") || HitCache:false                                                                                                        |+-----------------------------------------------------------------------------------------------------------------------+3 rows in set (0.01 sec)mysql> explain select * From sbtest1 where id > 1000;+-----------------------------------------------------------------------------------------------------------------------+| LOGICAL PLAN                                                                                                          |+-----------------------------------------------------------------------------------------------------------------------+| UnionAll(concurrent=true)                                                                                             ||   LogicalView(tables="[0000-0031].sbtest1_[000-127]", shardCount=128, sql="SELECT * FROM `sbtest1` WHERE (`id` > ?)") || HitCache:true                                                                                                         |+-----------------------------------------------------------------------------------------------------------------------+3 rows in set (0.00 sec)

小結

以上介紹了 DRDS 5.3 的 EXPLAIN 命令,以及執行計劃中每個操作符的含義,相信可以爲用戶調優 SQL 提供極大得便利。

DRDS 5.3 已經在阿里雲正式上線,除全新設計的執行計劃外,性能也有大幅提升,並支持原生事務、Outline 和 Plan Cache 等功能。後續支持複雜查詢的只讀實例、回收站、基於事務的廣播表寫入等功能也將相繼上線,敬請期待。

 原文鏈接


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章