Impala元數據簡介

Impala元數據簡介

背景

Impala是一個高性能的OLAP查詢引擎,與其它SQL-on-Hadoop的ROLAP解決方案如Presto、SparkSQL 等不同的是,Impala對元數據(Metadata/Catalog)做了緩存,因此在做查詢計劃生成時不再依賴外部系統(如Hive、HDFS、Kudu),能做到毫秒級別的生成時間。另外緩存元數據也能極大減少對底層系統Master節點(Hive Metastore、HDFS NameNode、Kudu Master)的負載。

然而事情總有兩面性,元數據緩存給Impala的系統設計引入了極大的複雜性。一方面在功能上爲了維護緩存的正確性,引入了兩個Impala特有的SQL語句:Invalidate Metadata 和 Refresh,另外還引入了query option SYNC_DDL。這些都讓用戶參與了緩存的維護。另一方面在架構設計上,有許多元數據相關的複雜設計,比如元數據的增量傳播、緩存一致性的維護等。

在數倉規模較大時,Impala的元數據設計暴露出了許多問題,比如大表的元數據加載和刷新時間特別長、元數據的廣播會被DDL阻塞導致廣播延遲很大、元數據緩存導致節點Full GC或OOM等。因此Impala元數據設計也一直在演化之中,最新進展主要集中在Fetch-on-demand coordinator(又稱local catalog mode、catalog-v2等)的設計,詳見IMPALA-7127。

本文旨在介紹Impala元數據設計的基本概念,更多元數據相關的複雜設計將在後續文章中介紹。

Impala Server簡介

Impala集羣包含一個 Catalog Server (Catalogd)、一個 Statestore Server (Statestored) 和若干個 Impala Daemon (Impalad)。Catalogd 主要負責元數據的獲取和DDL的執行,Statestored主要負責消息/元數據的廣播,Impalad主要負責查詢的接收和執行。
Impala Servers
Impalad 又可配置爲 coordinator only、 executor only 或 coordinator and executor(默認)三種模式。Coordinator角色的Impalad負責查詢的接收、計劃生成、查詢的調度等,Executor角色的Impalad負責數據的讀取和計算。默認配置下每個Impalad既是Coordinator又是Executor。生產環境建議做好角色分離,即每個Impalad要麼是Coordinator要麼是Executor,且可以以1:50的比例配置。更多細節可參考官方文檔[1].

Impala元數據的構成

Impala的元數據緩存在catalogd和各個Coordinator角色的Impalad中。Catalogd中的緩存是最新的,各個Coordinator都緩存的是Catalogd內元數據的一個複本。如下圖所示,元數據由Catalogd向外部系統獲取,並通過 Statestored 傳播給各個 Coordinator
Metadata In Impala Servers
元數據緩存主要由Java代碼實現,主體代碼在FE中。另有一些C++實現的代碼,主要處理FE跟BE的交互,以及元數據的廣播。代碼中把 Catalogd 和 Coordinator (Impalad) 中相同的元數據管理邏輯抽出來放在了 Catalog.java 中,Catalogd 裏的實現是 CatalogServiceCatalog.java,Coordinator 裏的實現是 ImpaladCatalog.java.

Catalog是一個層級結構,第一層是 db name 到 db 的映射,每二層是每個db下的 function map和 table map:

Catalog
|-- dbCache_ = Map<String, Db>
    |-- functions_ = Map<String, List<Function>>
    |-- tableCache_ = CatalogObjectCache<Table>

functions_ Map 裏的 value 是 Function 列表,主要表示同名函數的不同重載。tableCache_ 由 CatalogObjectCache 來維護。CatalogObjectCache 封裝了一個 ConcurrentHashMap,另加了版本管理的邏輯,比如避免低版本的更新覆蓋高版本的緩存、追蹤所有緩存的版本號等。這些版本管理邏輯在Impalad中尤其重要。我們在後續的文章中會詳細介紹。

Table 在代碼裏有五個具體的子類:HdfsTable、KuduTable、HBaseTable、View、IncompleteTable、DataSourceTable。前4個都比較直白,解釋下最後兩個:

  • IncompleteTable 表示未加載元數據的表或視圖(View)。Catalogd 啓動時,爲了減少啓動時間,只加載了所有表的表名,每個表用IncompleteTable來表示。如果執行了INVALIDATE METADATA,則表的元數據也會被清空,其表現就是回置成了IncompleteTable。IncompleteTable可能代表一個視圖,但這在元數據未加載時是無法確定的。因此在HUE等可視化界面中使用Impala時,常常會看到一個View是用Table的圖標表示的,但一旦有被使用過,就又變回成了View的圖標。
  • DataSourceTable 屬於external data source的實現,這塊沒有任何文檔提及,因爲一直處於實驗狀態。其初衷是提供一個Java接口來自定義外部數據源,只需要實現 prepare、open、getNext、close 這幾個接口。具體可參考代碼裏的 EchoDataSource 和 AllTypesDataSource。

接下來我們重點介紹下前三個的元數據構成。

HdfsTable

HdfsTable 代表一張底層存儲爲 HDFS 的 Hive 表。無分區表的元數據比較簡單,少了各個分區對應的元數據。這裏以分區表爲例,其元數據如圖所示:
HdfsTable UML
其中 msTable 和 msPartition 表示 HMS API 裏返回的對象:

org.apache.hadoop.hive.metastore.api.Table
org.apache.hadoop.hive.metastore.api.Partition

HdfsPartition 代表一個分區的元數據,其一大部分內容是 HDFS 文件和塊的信息。圖中的 FileDescriptor 和 BlockDescriptor,就是從 HDFS API 裏返回的 FileStatus 和 BlockLocation 對象抽取數據後生成的。爲了節省空間,實際緩存的並不是上圖展示的 FileDescriptor 和 FileBlock。IMPALA-4029 引入了 FlatBuffer 來壓縮 FileDescriptor 和 FileBlock。FlatBuffer 的好處是不需要像 protobuf 或 thrift 一樣做序列化和反序列化,但卻可以直接訪問對象裏的內容,同時帶來了一定的壓縮比。更多關於 FlatBuffer 參見文末文檔 [2].

HdfsPartition 的另一大部分內容是統計信息,緩存的是deflate算法壓縮後的數據,具體詳見:PartitionStatsUtil#partStatsFromCompressedBytes()。解壓之後是一個 TPartitionStats 對象,主要包含了各列在該partition裏的統計信息,每列的統計信息用一個 TIntermediateColumnStats 表示:

struct TIntermediateColumnStats {
  1: optional binary intermediate_ndv // NDV HLL 計算的中間結果
  2: optional bool is_ndv_encoded     // HLL中間結果是否有用 RLE 壓縮
  3: optional i64 num_nulls           // 該列在該分區的 NULL 數目
  4: optional i32 max_width           // 該列在該分區的最大長度
  5: optional double avg_width        // 該列在該分區的平均長度
  6: optional i64 num_rows            // 該分區行數,用於聚集HLL中間結果
}

關於 Impala 裏 ndv() 的實現,可參考 be/src/exprs/aggregate-functions-ir.cc 中的 HllInit()、HllUpdate()、HllMerge()、HllFinalEstimate() 的邏輯。ndv 的中間結果用一個string表示,長度爲 1024。在傳輸時一般會用 RLE (Run Length Encoding) 壓縮。

Impala的統計信息受限於Hive(因爲要保存在Hive Metastore中),目前並沒有統計數值類型列的最大最小、平均值等信息。這塊有個古老的 JIRA: IMPALA-2416,目前還沒有進展。

一個HDFS分區表的元數據在各種壓縮後,在內存中的大小約爲

分區數*2048 + 分區數*列數*400 + 文件數*500 + 塊數目*150

實際應用中要降低大表的元數據大小,就需要在分區數、列數、文件數、塊數目上尋求優化的空間。其中 2048、400、500、150 這些數都是各對象壓縮大小的估計值,“分區數 * 列數 * 200” 指的是增量統計信息的大小,如果表的統計信息是非增量的,即一直用 Compute Stats 來統計,則不需要這部分。實際應用中很少直接對大表做 Compute Stats,因爲執行時間可能很長,一般都是使用 Compute Incremental Stats,因此這部分的內存佔用不可忽略。

KuduTable

HdfsTable 代表一張底層存儲爲 Kudu 的 Hive 表。Impala 緩存的 Kudu 元數據特別有限:

  • msTable: HMS API 返回的 Table 對象,主要是 Hive 中的元數據
  • TableStats: HMS 中存的統計信息,主要是各列統計信息和整張表的行數等
  • kuduTableName: Kudu 存儲中的實際表名,該名字可以跟 Hive 中的表名不同。
  • kuduMasters: Kudu 集羣的 master 列表
  • primaryKeyColumnNames: Kudu 表的主鍵列
  • partitions: Kudu 表的分區信息
  • kuduSchema: Kudu API 返回的 Schema 信息

關於分區信息,只緩存了分區的列是哪些,以及 hash 分區的分區數,並沒有緩存 Range 分區的各個 Range 是什麼,因此在用 SHOW CREATE TABLE 語句時,看到的 range partition 信息只包含了列名。比如下面這個例子,“Partition by range(id)” 部分的各個 range 被省略了:

Query: show create table functional_kudu.dimtbl
+-------------------------------------------------------------------------------------------------------------------------------------------+
| result                                                                                                                                    |
+-------------------------------------------------------------------------------------------------------------------------------------------+
| CREATE TABLE functional_kudu.dimtbl (                                                                                                     |
|   id BIGINT NOT NULL ENCODING AUTO_ENCODING COMPRESSION DEFAULT_COMPRESSION,                                                              |
|   name STRING NULL ENCODING AUTO_ENCODING COMPRESSION DEFAULT_COMPRESSION,                                                                |
|   zip INT NULL ENCODING AUTO_ENCODING COMPRESSION DEFAULT_COMPRESSION,                                                                    |
|   PRIMARY KEY (id)                                                                                                                        |
| )                                                                                                                                         |
| PARTITION BY RANGE (id) (...)                                                                                                             |
| STORED AS KUDU                                                                                                                            |
| TBLPROPERTIES ('STATS_GENERATED'='TASK', 'impala.lastComputeStatsTime'='1573922577', 'kudu.master_addresses'='localhost', 'numRows'='10') |
+-------------------------------------------------------------------------------------------------------------------------------------------+

如果需要查看具體有哪些 range 分區,還是需要用 SHOW RANGE PARTITIONS 語句,Impala 會從 Kudu 中獲取結果來返回,然而還是不會緩存這些 range 信息。

Query: show range partitions functional_kudu.dimtbl
+-----------------------+
| RANGE (id)            |
+-----------------------+
| VALUES < 1004         |
| 1004 <= VALUES < 1008 |
| VALUES >= 1008        |
+-----------------------+
Fetched 3 row(s) in 0.07s

這塊個人覺得還有很多工作可做,比如把 range 分區的分界點緩存下來後,可以用來優化 Insert 語句,提升批量導入 Kudu 的性能(IMPALA-7751)。另外關於更細節的信息如每個 kudu tablet 的複本位置,kudu tserver 地址等都是沒有緩存的,利用這些信息實際也能做很多優化,歡迎大家一起來參與開發!

HBaseTable

Impala 對 HBase 的支持始於對 Hive 的兼容(Hive 可以讀 HBase 的數據),但目前已經處於維護狀態,社區不再在這方面投入精力。一方面是 Kudu 更適合替代 HBase 來做 OLAP,另一方面是 Impala 也不適合太高併發的 DML 操作。

HBaseTable 代表底層存儲爲 HBase 的 Hive 表,緩存了 HMS 中的 Table 定義和表的大小(行數)這些基本的統計信息,另外也緩存了底層 HBase 表的所有列族名。

總結

Impala 緩存了外部系統(Hive、HDFS、Kudu等)的元數據,主要目的是讓查詢計劃生成階段不再需要跟外部系統交互。生成查詢計劃需要哪些元數據,哪些元數據就會被緩存下來:

  • Table: Schema(表名、字段名、字段類型、分區字段等)、各列統計信息
    • HdfsTable: 分區目錄、文件路徑、文件分塊及複本位置、各分區的增量統計信息等
    • KuduTable: 分區列及分區類型(Hash、Range)
    • HBaseTable: 各列族名
    • View: 具體的查詢語句

參考文檔

  1. https://impala.apache.org/docs/build/html/topics/impala_dedicated_coordinator.html
  2. https://google.github.io/flatbuffers/index.html
發佈了17 篇原創文章 · 獲贊 7 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章