OLAP系統解析:Apache Kylin和Baidu Palo哪家強?

做者 | 康凱森
編輯 | Debra
AI 前線導讀:Apache Kylin 和 Baidu Palo 都是優秀的開源 OLAP 系統,本文將全方位地對比 Kylin 和 Palo。Kylin 和 Palo 分別是 MOALP 和 ROLAP 的表明,對比這兩個系統的目的不是爲了說明哪一個系統更好,只是爲了明確每一個系統的設計思想和架構原理,讓你們能夠根據本身的實際需求去選擇合適的系統,也能夠進一步去思考咱們如何去設計出更優秀的 OLAP 系統。

本文對 Apache Kylin 的理解基於近兩年來在生產環境大規模地使用,運維和深度開發,我已向 Kylin 社區貢獻了 98 次 Commit,包含多項新功能和深度優化。本文對 Baidu Palo 的理解基於官方文檔和論文的閱讀,代碼的粗淺閱讀和較深刻地測試。

更多幹貨內容請關注微信公衆號「AI 前線」,(ID:ai-front)
  • 1 系統架構html

    • 1.1 What is Kylinjava

    • 1.2 What is Palogit

  • 2 數據模型github

    • 2.1 Kylin 的聚合模型算法

    • 2.2 Palo 的聚合模型sql

    • 2.3 Kylin Cuboid VS Palo RollUpapache

    • 2.4 Palo 的明細模型微信

  • 3 存儲引擎數據結構

  • 4 數據導入架構

  • 5 查詢

  • 6 精確去重

  • 7 元數據

  • 8 高性能

  • 9 高可用

  • 10 可維護性

    • 10.1 部署

    • 10.2 運維

    • 10.3 客服

  • 11 易用性

    • 11.1 查詢接入

    • 11.2 學習成本

    • 11.3 Schema Change

  • 12 功能

  • 13 社區和生態

  • 14 總結

  • 15 參考資料

注: 本文的對比 基於 Apache Kylin 2.0.0 和 Baidu Palo 0.8.0。

1 系統架構
1.1 What is Kylin

Kylin 的核心思想是預計算,利用空間換時間來 加速查詢模式固定的 OLAP 查詢。

Kylin 的理論基礎是 Cube 理論,每一種維度組合稱之爲 Cuboid,全部 Cuboid 的集合是 Cube。 其中由全部維度組成的 Cuboid 稱爲 Base Cuboid,圖中 (A,B,C,D) 即爲 Base Cuboid,全部的 Cuboid 均可以基於 Base Cuboid 計算出來。 在查詢時,Kylin 會自動選擇知足條件的最「小」Cuboid,好比下面的 SQL 就會對應 Cuboid(A,B):

select xx from table where A=xx group by B


Kylin-cube

下圖是 Kylin 數據流轉的示意圖,Kylin 自身的組件只有兩個:JobServer 和 QueryServer。 Kylin 的 JobServer 主要負責將數據源(Hive,Kafka)的數據經過計算引擎(MapReduce,Spark)生成 Cube 存儲到存儲引擎(HBase)中;QueryServer 主要負責 SQL 的解析,邏輯計劃的生成和優化,向 HBase 的多個 Region 發起請求,並對多個 Region 的結果進行彙總,生成最終的結果集。


kylin-data

下圖是 Kylin 可插拔的架構圖, 在架構設計上,Kylin 的數據源,構建 Cube 的 計算引擎,存儲引擎都是可插拔的。Kylin 的核心就是這套可插拔架構,Cube 數據模型和 Cuboid 的算法。


Kylin

1.2 What is Palo

Palo 是一個基於 MPP 的 OLAP 系統,主要整合了 Google Mesa(數據模型),Apache Impala(MPP Query Engine) 和 Apache ORCFile(存儲格式,編碼和壓縮) 的技術。


baidu-palo

Palo 的系統架構以下,Palo 主要分爲 FE 和 BE 兩個組件,FE 主要負責查詢的編譯,分發和元數據管理(基於內存,相似 HDFS NN);BE 主要負責查詢的執行和存儲系統。


baidu-palo

2 數據模型
2.1 Kylin 的聚合模型

Kylin 將表中的列分爲維度列和指標列。在數據導入和查詢時相同維度列中的指標會按照對應的聚合函數 (Sum, Count, Min, Max, 精確去重,近似去重,百分位數,TOPN) 進行聚合。

在存儲到 HBase 時,Cuboid+ 維度 會做爲 HBase 的 Rowkey, 指標會做爲 HBase 的 Value,通常全部指標會在 HBase 的一個列族,每列對應一個指標,但對於較大的去重指標會單獨拆分到第 2 個列族。


Kylin-model

2.2 Palo 的聚合模型

Palo 的聚合模型借鑑自 Mesa,但本質上和 Kylin 的聚合模型同樣,只不過 Palo 中將維度稱做 Key,指標稱做 Value。


palo-data-model

Palo 中比較獨特的聚合函數是 Replace 函數,這個聚合函數可以保證相同 Keys 的記錄只保留最新的 Value, 能夠藉助這個 Replace 函數來實現 點更新。通常 OLAP 系統的數據都是隻支持 Append 的,可是像電商中交易的退款,廣告點擊中的無效點擊處理,都須要去更新以前寫入的單條數據,在 Kylin 這種沒有 Relpace 函數的系統中咱們必須把包含對應更新記錄的整個 Segment 數據所有重刷,可是有了 Relpace 函數,咱們只須要再追加 1 條新的記錄便可。 可是 Palo 中的 Repalce 函數有個缺點:沒法支持預聚合, 就是說只要你的 SQL 中包含了 Repalce 函數,即便有其餘能夠已經預聚合的 Sum,Max 指標,也必須現場計算。

爲何 Palo 能夠支持點更新呢?

Kylin 中的 Segment 是不可變的,也就是說 HFile 一旦生成,就再也不發生任何變化。可是 Palo 中的 Segment 文件和 HBase 同樣,是能夠進行 Compaction 的,具體能夠參考 Google Mesa 論文解讀中的 Mesa 數據版本化管理(https://blog.bcmeng.com/post/google-mesa.html#mesa%E6%95%B0%E6%8D%AE%E7%89%88%E6%9C%AC%E5%8C%96%E7%AE%A1%E7%90%86)

Palo 的聚合模型相比 Kylin 有個缺點:就是一個 Column 只能有一個預聚合函數,沒法設置多個預聚合函數。 不過 Palo 能夠現場計算其餘的聚合函數。 Baidu Palo 的開發者 Review 時提到,針對這個問題,Palo 還有一種解法:因爲 Palo 支持多表導入的原子更新,因此 1 個 Column 須要多個聚合函數時,能夠在 Palo 中建多張表,同一份數據導入時,Palo 能夠同時原子更新多張 Palo 表,缺點是多張 Palo 表的查詢路由須要應用層來完成。

Palo 中和 Kylin 的 Cuboid 等價的概念是 RollUp 表,Cuboid 和 RollUp 表均可以認爲是一種 Materialized Views 或者 Index。 Palo 的 RollUp 表和 Kylin 的 Cuboid 同樣,** 在查詢時不須要顯示指定,系統內部會根據查詢條件進行路由。 以下圖所示:


Palo Rollup

Palo 中 RollUp 表的路由規則以下:

  1. 選擇包含全部查詢列的 RollUp 表

  2. 按照過濾和排序的 Column 篩選最符合的 RollUp 表

  3. 按照 Join 的 Column 篩選最符合的 RollUp 表

  4. 行數最小的

  5. 列數最小的

2.3 Kylin Cuboid VS Palo RollUp


Kylin cuboid vs palo rollup

2.4 Palo 的明細模型

因爲 Palo 的聚合模型存在下面的缺陷,Palo 引入了明細模型。

  • 必須區分維度列和指標列

  • 維度列不少時,Sort 的成本很高

  • Count 成本很高,須要讀取全部維度列(能夠參考 Kylin 的解決方法進行優化)

Palo 的明細模型不會有任何聚合,不區分維度列和指標列,可是在建表時須要指定 Sort Columns,數據導入時會根據 Sort Columns 進行排序,查詢時根據 Sort Column 過濾會比較高效。

以下圖所示,Sort Columns 是 Year 和 City。


Kylin-detail-model

這裏須要注意一點,Palo 中一張表只能有一種數據模型,即要麼是聚合模型,要麼是明細模型,並且 Roll Up 表的數據模型必須和 Base 表一致, 也就是說明細模型的 Base 表不能有聚合模型的 Roll Up 表。

3 存儲引擎

Kylin 存儲引擎 HBase:


如上圖所示,在 Kylin 中 1 個 Cube 能夠按照時間拆分爲多個 Segment,Segment 是 Kylin 中數據導入和刷新的最小單位。Kylin 中 1 個 Segment 對應 HBase 中一張 Table。 HBase 中的 Table 會按照 Range 分區拆分爲多個 Region, 每一個 Region 會按照大小拆分爲多個 HFile。

關於 HFile 的原理網上講述的文章已經不少了,我這裏簡單介紹下。首先 HFile 總體上能夠分爲元信息,Blcoks,Index3 部分,Blcoks 和 Index 均可以分爲 Data 和 Meta 兩部分。Block 是數據讀取的最小單位,Block 有多個 Key-Value 組成,一個 Key-Value 表明 HBase 中的一行記錄,Key-Value 由 Kylin-Len,Value-Len,Key-Bytes,Value-Bytes 4 部分組成。更詳細的信息你們能夠參考下圖 (下圖來源於互聯網,具體出處不詳):


HBase-HFile

Palo 存儲引擎:


如上圖所示,Palo 的 Table 支持二級分區,能夠先按照日期列進行一級分區,再按照指定列 Hash 分桶。具體來講,1 個 Table 能夠按照日期列分爲多個 Partition, 每一個 Partition 能夠包含多個 Tablet,Tablet 是數據移動、複製等操做的最小物理存儲單元, 各個 Tablet 之間的數據沒有交集,而且在物理上獨立存儲。Partition 能夠視爲邏輯上最小的管理單元,數據的導入與刪除,僅能針對一個 Partition 進行。1 個 Table 中 Tablet 的數量 = Partition num * Bucket num。Tablet 會按照必定大小(256M)拆分爲多個 Segment 文件,Segment 是列存的,可是會按行(1024)拆分爲多個 Rowblock。


palo segment file

下面咱們來看下 Palo Segment 文件的具體格式,Palo 文件格式主要參考了 Apache ORC。如上圖所示,Palo 文件主要由 Meta 和 Data 兩部分組成,Meta 主要包括文件自己的 Header,Segment Meta,Column Meta,和每一個 Column 數據流的元數據,每部分的具體內容你們看圖便可,比較詳細。 Data 部分主要包含每一列的 Index 和 Data,這裏的 Index 指每一列的 Min,Max 值和數據流 Stream 的 Position;Data 就是每一列具體的數據內容,Data 根據不一樣的數據類型會用不一樣的 Stream 來存儲,Present Stream 表明每一個 Value 是不是 Null,Data Stream 表明二進制數據流,Length Stream 表明非定長數據類型的長度。 下圖是 String 使用字典編碼和直接存儲的 Stream 例子。


Palo String encoding

下面咱們來看下 Palo 的前綴索引:


Palo index

本質上,Palo 的數據存儲是相似 SSTable(Sorted String Table)的數據結構。該結構是一種有序的數據結構,能夠按照指定的列有序存儲。在這種數據結構上,以排序列做爲條件進行查找,會很是的高效。而前綴索引,即在排序的基礎上,實現的一種根據給定前綴列,快速查詢數據的索引方式。前綴索引文件的格式如上圖所示,索引的 Key 是每一個 Rowblock 第一行記錄的 Sort Key 的前 36 個字節,Value 是 Rowblock 在 Segment 文件的偏移量。

有了前綴索引後,咱們查詢特定 Key 的過程就是兩次二分查找:

  1. 先加載 Index 文件,二分查找 Index 文件獲取包含特定 Key 的 Row blocks 的 Offest, 而後從 Sement Files 中獲取指定的 Rowblock;

  2. 在 Rowblocks 中二分查找特定的 Key

4 數據導入

Kylin 數據導入:


Kylin data loading

如上圖,Kylin 數據導入主要分爲建 Hive 大寬表 (這一步會處理 Join);維度列構建字典;逐層構建 Cuboid;Cuboid 轉爲 HFile;Load HFile To HBase; 元數據更新這幾步。

其中 Redistribute 大寬表這一步的做用是爲了將整個表的數據搞均勻,避免後續的步驟中有數據傾斜,Kylin 有配置能夠跳過這一步。

其中 Extract Distinct Columns 這一步的做用是獲取須要構建字典的維度列的 Distinct 值。假如一個 ID 維度列有 1,2,1,2,2,1,1,2 這 8 行,那麼通過這一步後 ID 列的值就只有 1,2 兩行,作這一步是爲了下一步對維度列構建字典時更快速。

其餘幾個步驟都比較好理解,我就再也不贅述。更詳細的信息能夠參考 Apache Kylin Cube 構建原理(https://blog.bcmeng.com/post/kylin-cube.html)

Palo 數據導入:


palo data loading

Palo 數據導入的兩個核心階段是 ETL 和 LOADING, ETL 階段主要完成如下工做:

  • 數據類型和格式的校驗

  • 根據 Teblet 拆分數據

  • 按照 Key 列進行排序, 對 Value 進行聚合

LOADING 階段主要完成如下工做:

  • 每一個 Tablet 對應的 BE 拉取排序好的數據

  • 進行數據的格式轉換,生成索引LOADING 完成後會進行元數據的更新。

5 查詢

Kylin 查詢:


Kylin query

如上圖,整個 Kylin 的查詢過程比較簡單,是個 Scatter-Gather 的模型。圖中圓形框的內容發生在 Kylin QueryServer 端,方形框的內容發生在 HBase 端。Kylin QueryServer 端收到 SQL 後,會先進行 SQL 的解析,而後生成和優化 Plan,再根據 Plan 生成和編譯代碼,以後會根據 Plan 生成 HBase 的 Scan 請求,若是可能,HBase 端除了 Scan 以外,還會進行過濾和聚合(基於 HBase 的 Coprocessor 實現),Kylin 會將 HBase 端返回的結果進行合併,交給 Calcite 以前生成好的代碼進行計算。

Palo 查詢:


palo-impala-query

Palo 的查詢引擎使用的是 Impala,是 MPP 架構。 Palo 的 FE 主要負責 SQL 的解析,語法分析,查詢計劃的生成和優化。查詢計劃的生成主要分爲兩步:

  1. 生成單節點查詢計劃 (上圖左下角)

  2. 將單節點的查詢計劃分佈式化,生成 PlanFragment(上圖右半部分)

第一步主要包括 Plan Tree 的生成,謂詞下推, Table Partitions pruning,Column projections,Cost-based 優化等;第二步 將單節點的查詢計劃分佈式化,分佈式化的目標是 最小化數據移動和最大化本地 Scan,分佈式化的方法是增長 ExchangeNode,執行計劃樹會以 ExchangeNode 爲邊界拆分爲 PlanFragment,1 個 PlanFragment 封裝了在一臺機器上對同一數據集的部分 PlanTree。如上圖所示:各個 Fragment 的數據流轉和最終的結果發送依賴:DataSink。

當 FE 生成好查詢計劃樹後,BE 對應的各類 Plan Node(Scan, Join, Union, Aggregation, Sort 等)執行本身負責的操做便可。

6 精確去重

Kylin 的精確去重:

Kylin 的精確去重是基於全局字典和 RoaringBitmap 實現的基於預計算的精確去重。具體能夠參考 Apache Kylin 精確去重和全局字典權威指南(https://blog.bcmeng.com/post/kylin-distinct-count-global-dict.html)

Palo 的精確去重:

Palo 的精確去重是現場精確去重,Palo 計算精確去重時會拆分爲兩步:

  1. 按照全部的 group by 字段和精確去重的字段進行聚合

  2. 按照全部的 group by 字段進行聚合

下面是個簡單的等價轉換的例子:



Palo 現場精確去重計算性能和 去重列的基數、去重指標個數、過濾後的數據大小成負相關;

7 元數據

Kylin 的元數據 :

Kylin 的元數據是利用 HBase 存儲的,能夠很好地橫向擴展。Kylin 每一個具體的元數據都是一個 Json 文件,HBase 的 Rowkey 是文件名,Value 是 Json 文件的內容。Kylin 的元數據表設置了 IN_MEMORY => 'true' 屬性, 元數據表會常駐 HBase RegionServer 的內存,因此元數據的查詢性能很好,通常在幾 ms 到幾十 ms。

Kylin 元數據利用 HBase 存儲的一個問題是,在 Kylin 可插拔架構下,即便咱們實現了另外一種存儲引擎,咱們也必須部署 HBase 來存儲元數據,因此 Kylin 要真正作到存儲引擎的可插拔,就必須實現一個獨立的元數據存儲。

Palo 的元數據:

Palo 的元數據是基於內存的,這樣作的好處是性能很好且不須要額外的系統依賴。 缺點是單機的內存是有限的,擴展能力受限,可是根據 Palo 開發者的反饋,因爲 Palo 自己的元數據很少,因此元數據自己佔用的內存不是不少,目前用大內存的物理機,應該能夠支撐數百臺機器的 OLAP 集羣。 此外,OLAP 系統和 HDFS 這種分佈式存儲系統不同,咱們部署多個集羣的運維成本和 1 個集羣區別不大。

關於 Palo 元數據的具體原理你們能夠參考 Palo 官方文檔 Palo 元數據設計文檔(https://github.com/baidu/palo/wiki/Metadata-Design)

8 高性能

Why Kylin Query Fast:


Kylin query

Kylin 查詢快的核心緣由就是預計算,如圖 (圖片出處 Apache kylin 2.0: from classic olap to real-time data warehouse https://www.slideshare.net/YangLi43/apache-kylin-20-from-classic-olap-to-realtime-data-warehouse),Kylin 現場查詢時不須要 Join,也幾乎不須要聚合,主要就是 Scan + Filter。

Why Palo Query Fast:

  1. In-Memory Metadata。 Palo 的元數據就在內存中,元數據訪問速度很快。

  2. 聚合模型能夠在數據導入時進行預聚合。

  3. 和 Kylin 同樣,也支持預計算的 RollUp Table。

  4. MPP 的查詢引擎。

  5. 向量化執行。相比 Kylin 中 Calcite 的代碼生成,向量化執行在處理高併發的低延遲查詢時性能更好,Kylin 的代碼生成自己可能會花費幾十 ms 甚至幾百 ms。

  6. 列式存儲 + 前綴索引。

9 高可用

Kylin 高可用:

Kylin JobServer 的高可用: Kylin 的 JobServer 是無狀態的,一臺 JobServer 掛掉後,其餘 JobServer 會很快接管正在 Running 的 Job。JobServer 的高可用是基於 Zookeeper 實現的,具體能夠參考 Apache Kylin Job 生成和調度詳解(https://blog.bcmeng.com/post/kylin-job.html)。

Kylin QueryServer 的高可用:Kylin 的 QueryServer 也是無狀態的,其高可用通常經過 Nginx 這類的負載均衡組件來實現。

Kylin Hadoop 依賴的高可用: 要單純保證 Kylin 自身組件的高可用並不困難,可是要保證 Kylin 總體數據導入和查詢的高可用是 十分困難的,由於必須同時保證 HBase,Hive,Hive Metastore,Spark,Mapreduce,HDFS,Yarn,Zookeeper,Kerberos 這些服務的高可用。

Palo 高可用:

Palo FE 的高可用: Palo FE 的高可用主要基於 BerkeleyDB java version 實現,BDB-JE 實現了類 Paxos 一致性協議算法。

Palo BE 的高可用:Palo 會保證每一個 Tablet 的多個副本分配到不一樣的 BE 上,因此一個 BE down 掉,不會影響查詢的可用性。

10 可維護性
10.1 部署

Kylin 部署: 若是徹底從零開始,你就須要部署 1 個 Hadoop 集羣和 HBase 集羣。 即便公司已經有了比較完整的 Hadoop 生態,在部署 Kylin 前,你也必須先部署 Hadoop 客戶端,HBase 客戶端,Hive 客戶端,Spark 客戶端。

Palo 部署: 直接啓動 FE 和 BE。

10.2 運維

Kylin 運維: 運維 Kylin 對 Admin 有較高的要求,首先必須瞭解 HBase,Hive,MapReduce,Spark,HDFS,Yarn 的原理;其次對 MapReduce Job 和 Spark Job 的問題排查和調優經驗要豐富;而後必須掌握對 Cube 複雜調優的方法;最後出現問題時排查的鏈路較長,複雜度較高。

Palo 運維: Palo 只須要理解和掌握系統自己便可。

10.3 客服

Kylin 客服: 須要向用戶講清 Hadoop 相關的一堆概念;須要教會用戶 Kylin Web 的使用;須要教會用戶如何進行 Cube 優化(沒有統一,簡潔的優化原則);須要教會用戶怎麼查看 MR 和 Spark 日誌;須要教會用戶怎麼查詢;

Palo 客服: 須要教會用戶聚合模型,明細模型,前綴索引,RollUp 表這些概念。

11 易用性
11.1 查詢接入

Kylin 查詢接入:Kylin 支持 Htpp,JDBC,ODBC 3 種查詢方式。

Palo 查詢接入: Palo 支持 Mysql 協議,現有的大量 Mysql 工具均可以直接使用,用戶的學習和遷移成本較低。

11.2 學習成本

Kylin 學習成本: 用戶要用好 Kylin,須要理解如下概念:

  • Cuboid

  • 彙集組

  • 強制維度

  • 聯合維度

  • 層次維度

  • 衍生維度

  • Extend Column

  • HBase RowKey 順序

此外,前面提到過,用戶還須要學會怎麼看 Mapreduce Job 和 Spark Job 日誌。

Palo 學習成本: 用戶須要理解聚合模型,明細模型,前綴索引,RollUp 表這些概念。

11.3 Schema Change

Schema 在線變動是一個十分重要的 feature,由於在實際業務中,Schema 的變動會十分頻繁。

Kylin Schema Change: Kylin 中用戶對 Cube Schema 的任何改變,都須要在 Staging 環境重刷全部數據,而後切到 Prod 環境。整個過程週期很長,資源浪費比較嚴重。

Palo Schema Change:Palo 支持 Online Schema Change。

所謂的 Schema 在線變動就是指 Scheme 的變動不會影響數據的正常導入和查詢,Palo 中的 Schema 在線變動有 3 種:

  • direct schema change:就是重刷全量數據,成本最高,和 kylin 的作法相似。當修改列的類型,稀疏索引中加一列時須要按照這種方法進行。

  • sorted schema change: 改變了列的排序方式,需對數據進行從新排序。例如刪除排序列中的一列, 字段重排序。

  • linked schema change: 無需轉換數據,直接完成。對於歷史數據不會重刷,新攝入的數據都按照新的 Schema 處理,對於舊數據,新加列的值直接用對應數據類型的默認值填充。例如加列操做。Druid 也支持這種作法。

12 功能


Apache kylin VS baidu palo

注: 關於 Kylin 的明細查詢,Kylin 自己只有聚合模型,可是也能夠 經過將全部列做爲維度列,只構建 Base Cuboid 來實現明細查詢, 缺點是效率比較低下。

注: 雖然 Palo 能夠同時支持高併發,低延遲的 OLAP 查詢和高吞吐的 Adhoc 查詢,但顯然這兩類查詢會相互影響。因此 Baidu 在實際應用中也是用兩個集羣分別知足 OLAP 查詢和 Adhoc 查詢需求。

13 社區和生態

Palo 社區剛剛起步,目前核心用戶只有 Baidu;Kylin 的社區和生態已經比較成熟,Kylin 是第一個徹底由中國開發者貢獻的 Apache 頂級開源項目,目前已經在多家大型公司的生產環境中使用。

14 總結

本文從多方面對比了 Apache Kylin 和 Baidu Palo,有理解錯誤的地方歡迎指正。本文更多的是對兩個系統架構和原理的客觀描述,主觀判斷較少。最近在調研了 Palo,ClickHouse,TiDB 以後,也一直在思考 OLAP 系統的發展趨勢是怎樣的,下一代更優秀的 OLAP 系統架構應該是怎樣的,一個系統是否能夠同時很好的支持 OLTP 和 OLAP,這些問題想清楚後我會再寫篇文章描述下,固然,你們有好的想法,也歡迎直接 Comment。

15 參考資料

1 Palo 文檔和源碼:https://github.com/baidu/palo

2 Kylin 源碼:https://github.com/apache/kylin

3 Apache kylin 2.0: from classic olap to real-time data warehouse 在 Kylin 高性能部分引用了第 4 頁 PPT 的截圖:https://www.slideshare.net/YangLi43/apache-kylin-20-from-classic-olap-to-realtime-data-warehouse

4 百度 MPP 數據倉庫 Palo 開源架構解讀與應用 在 Palo 查詢部分引用了第 31 頁 PPT 的截圖 https://myslide.cn/slides/6392

相關文章
相關標籤/搜索