Impala元數據緩存的生命週期

上一篇文章《Impala元數據簡介》介紹了Impala緩存的元數據(Metadata/Catalog)的具體內容,本文將介紹這些元數據緩存的生命週期,即它們是怎麼初始化的,怎麼加載的以及怎麼失效的。

以下是常見的元數據相關的問題,基本都跟元數據的生命週期有關:

  • 同樣的查詢,爲什麼第一次運行比後面幾次運行都要慢很多?
  • 在 Hive 中建了個新表,但在 Impala 中不可見,如何解決?
  • 在 Hive 中建了個新的函數,但在 Impala 中不可見,如何解決?
  • HUE中使用 Impala Editor 時,爲什麼有些 View 被顯示成了表?
  • Invalidate metadata 和 Refresh語句有什麼區別?各有什麼應用場景?

我們在最後再討論這些問題。另外本文儘量在每一部分給出相應的源碼入口(對應版本爲 Impala-3.3),感興趣的讀者可以鑽研更多細節,文中有不對之處也請大家多多指正。

1. 集羣啓動時的元數據加載

Impala是一個無狀態的系統,元數據都從外部系統獲取,啓動時Catalog Server、Impalad 和 Statestored 的內存都是空的。啓動後還無法提供服務,因爲要等Catalog Server從Hive中加載完所有的庫名、表名和函數名,通過Statestored傳播給所有Coordinator角色的Impalad,然後Impalad才能接收查詢。在這之前所有查詢都會出現如下報錯:

AnalysisException: This Impala daemon is not ready to accept user requests.
Status: Waiting for catalog update from the StateStore.

這個行爲在 Impala-2.11 (cdh5.14) 開始做了改動(IMPALA-4704):如果 Impalad 還沒準備好接收查詢,則不會打開 beeswax (21000) 和 hs2 (21050) 端口,客戶端的報錯變爲連接錯誤。

1.1 Catalog Server啓動時的元數據加載

Catalog Server 啓動時要初始化自己的Catalog對象,主要就是代碼裏的dbCache_:

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

Catalog Server 首先會從HMS(Hive MetaStore)中拉取所有的庫名,爲每個數據庫建立一個Db對象。

對於每一個數據庫,先加載裏面的函數(UDF/UDAF)。我們通常把函數建在default數據庫下,因此default數據庫的函數加載時間一般會比其它數據庫長。由於函數的元信息較小,Catalog Server會把它們都加載進來,它們包括:

  • 函數名
  • 各種重載的參數列表
  • binary文件(jar包或so文件)的hdfs路徑
  • 函數在binary文件中對應的符號(symbol)

另外 Catalog Server 還會把binary文件也下載到本地磁盤緩存下來。因此我們在生產環境中要控制binary文件的大小,避免從大的jar包或so文件中創建函數。
binary文件下載的路徑由啓動函數 --local_library_dir 控制,默認是 /tmp。

對於每一個數據庫,加載完函數後就是加載表名。Catalog Server直接使用 HMS 的 getAllTables API獲取該數據庫下所有表名字符串,然後爲它們一一建立 IncompleteTable 對象(上一篇文章有介紹),表的元數據都處於未加載的狀態。如果啓動參數裏 --load_catalog_in_background 設的是true(Cloudera Manager裏的默認值就是true,但Impala代碼裏的默認值是false),則還會把這些表名放入後臺加載隊列中,後臺有一個線程池會加載它們的元數據,後面我們再介紹這個線程池。生產環境中表的數目較多,而且還有不少大表,不建議將 --load_catalog_in_background 設置爲 true。

Catalog Server啓動時元數據加載的具體源碼可參見

  • https://github.com/apache/impala/blob/3.3.0/fe/src/main/java/org/apache/impala/service/JniCatalog.java#L134
  • https://github.com/apache/impala/blob/3.3.0/fe/src/main/java/org/apache/impala/catalog/CatalogServiceCatalog.java#L1443
  • https://github.com/apache/impala/blob/3.3.0/fe/src/main/java/org/apache/impala/catalog/CatalogServiceCatalog.java#L1354

1.2 Impalad啓動時的元數據加載

Impalad 啓動時緩存的元數據是空的,此時還不能提供服務。首先要從 statestored 獲取一份全量的元數據更新(即跟Catalogd同步),如果此時集羣剛啓動,則 statestored 裏也是空的,需要等 Catalogd 加載完後再更新給 statestored。statestored 是 C++ 寫的,內存裏緩存的是壓縮、序列化後的 Catalog。每一臺 Coordinator 角色的 Impalad 啓動時,都會從 statestored 獲取一份全量的 Catalog,然後纔會開始接收查詢。

源碼可參見:
https://github.com/apache/impala/blob/3.3.0/fe/src/main/java/org/apache/impala/service/Frontend.java#L1080

2. 集羣運行時的元數據加載

Impala裏的SQL語句可以簡單分爲查詢語句(Query)、DDL語句和DML語句三種。查詢語句指所有返回真實數據的語句,主要指以SELECT爲中心的語句。DDL(Data Definition Language)語句主要指元數據操作的各種語句,如 Create Table、Create View、Alter Table、Drop Table這些。DML(Data Manipulation Language)語句主要指數據修改相關的各種語句,如 Insert、Update、Delete等。DML語句可能也會修改元數據,比如 Insert 一個HDFS表時可能會創建新的 Partition。

所有這三種語句都可能觸發元數據的加載或刷新(reload),下面我們分異步和同步兩類討論。

2.1 SQL解析觸發的異步元數據加載(PrioritizedLoad)

對於數據庫裏的表,集羣剛啓動時Catalog Server只加載了表名,因此 Impalad 裏緩存的也只是這些字符串。當查詢來時,Impalad 會對查詢進行解析並生成執行計劃,這時就需要所查表的完整元數據了。比如展開 "select * " 時需要知道具體的字段名和類型;比如確定函數使用哪個重載時需要知道各參數的類型;再比如join順序的優化需要知道各表的大小和join字段的基數(Cardinality,即不同值的數目)等統計信息。

缺乏表的必要元數據(不包括統計信息),執行計劃就沒辦法生成。因此 Impalad 會向 Catalogd 發送一個 PrioritizedLoad 的 RPC,傳上缺少元數據的表名,讓 Catalogd 優先加載它們的元數據。Catalogd 的加載隊列裏可能有其它任務在排隊了,Impalad 請求的加載任務會放在隊列前面,優先加載。

Impalad 發送完 PrioritizedLoad 的 RPC 後,Catalogd 返回 ok 只是表示任務放進加載隊列裏了。Catalogd 加載完後會把元數據的更新發送給 Statestored,從而廣播給所有 Coordinator 角色的 Impalad(當然就包括了發起任務的那個)。在沒有收到元數據更新之前,查詢就一直處於 CREATED 的狀態,Last Event 是 “Query submitted”,如圖所示
CREATED 狀態
Impalad 在完成 PrioritizedLoad PRC 後就開始等待 Statestored 發來的 catalog 更新, 然後再次嘗試解析SQL語句。catalog 的更新不一定包含了所缺的所有表(取決於異步加載的執行情況),如果還發現有些表缺元數據,則會再向 Catalogd 發起 PrioritizedLoad RPC。如此循環下去。

最終加載的耗時會體現在Profile裏的Query Compilation這段:

Query Compilation: 3s938ms
   - Metadata load started: 1.158ms (1.158ms)
   - Metadata load finished. loaded-tables=1/4 load-requests=1 catalog-updates=3 storage-load-time=6ms: 3s886ms (3s885ms)
   - Analysis finished: 3s920ms (34.379ms)
   - Authorization finished (noop): 3s920ms (85.175us)
   - Value transfer graph computed: 3s921ms (467.540us)
   - Single node plan created: 3s931ms (9.774ms)
   - Runtime filters computed: 3s932ms (1.476ms)
   - Distributed plan created: 3s932ms (116.092us)
   - Planning finished: 3s938ms (6.271ms)

上例中 “Metadata load finished” 一段表示查詢觸發了元數據加載。其中 “loaded-tables=1/4” 表示查詢裏涉及4個表,本次加載了1個表(另外3個表在查詢編譯前已加載過元數據);“load-requests=1” 表示總共向 Catalogd 發送了1次 PrioritizeLoad 請求;“catalog-updates=3” 表示總共等了3輪的catalog廣播更新;“storage-load-time=6ms” 表示加載文件元數據花了 6ms。整個元數據加載的等待時間是 3s885ms.

另外,如果某個表剛被執行過 “Invalidate Metadata table_name”,或者全局的 “Invalidate Metadata” 剛被執行過,則跟集羣剛啓動時一樣,查詢所要的表的元數據也是空的,此時也會觸發 PrioritizedLoad RPC 讓 catalogd 去加載。等待元數據加載是比較耗時的,因此生產環境中儘量不要在大表上用 “Invalidate Metadata”。

關於 Impalad 發起 PrioritizedLoad 請求的源碼可參見:

  • https://github.com/apache/impala/blob/3.3.0/fe/src/main/java/org/apache/impala/service/Frontend.java#L1278
  • https://github.com/apache/impala/blob/3.3.0/fe/src/main/java/org/apache/impala/analysis/StmtMetadataLoader.java#L138

2.2 DDL/DML 執行觸發的同步元數據加載

DDL 或 DML 語句也需要解析,也需要各表的元數據。比如 Create Table A Like B 語句,就需要表B 的具體信息才能執行。這類元數據的獲取也是用 PrioritizedLoad,屬於上一類。這裏要介紹的是 DDL/DML 執行時觸發的元數據加載。Catalogd 裏維護了一個 Hive 的連接池,所有 DDL 語句和 DML 語句裏的 DDL 部分都是在 Catalogd 裏執行的。Catalogd 裏發生的同步元數據加載主要有兩種情況:

  • 如果執行過程中發現某個表的元數據是空的,則會立即對它進行加載。這種情況比較少見,因爲 DDL 語句在 Impalad 端解析時,已經加載完表的元數據了。到 Catalogd 端執行時卻發現表的元數據是空的,一般是因爲有併發的 Invalidate Metadata 在運行,纔會出現這種情況。
  • 執行完DDL後經常需要重新加載元數據,比如 Catalogd 調用 Hive 的 AlterTable API 後,會重新加載這個表的元數據。

總之這些元數據的加載請求都是阻塞式的同步加載,它們有的直接執行,有的會跟異步請求一起被調度執行,具體討論起來比較繁瑣,對實戰解決問題的幫助不大,所以我們略過。感興趣的讀者可以直接看代碼裏對 Table#load() 的各處調用:

public abstract class Table extends CatalogObjectImpl implements FeTable {
  ...
  public abstract void load(boolean reuseMetadata, IMetaStoreClient client,
      org.apache.hadoop.hive.metastore.api.Table msTbl, String reason)
      throws TableLoadingException;
}

源碼位置:
https://github.com/apache/impala/blob/3.3.0/fe/src/main/java/org/apache/impala/catalog/Table.java#L221

2.3 加載請求在 Catalogd 裏的調度

回顧一下異步加載請求,主要有兩個來源:

  • 每個 Coordinator 角色的 Impalad 都會向 Catalogd 發送 PrioritizedLoad 的異步加載請求,以獲取解析查詢所需要的元數據。
  • 如果集羣啓動參數設置了 --load_catalog_in_background=true,Catalogd 啓動時也會有一堆異步的加載請求(稱爲後臺請求)。

這些請求在 Catalogd 中統一用一個雙端隊列(Deque)來維護,後臺請求在隊列尾部入列,PrioritizedLoad 請求在隊列頭部入列,入列時都會去重以防重複加載。Catalogd 中有兩個線程池,一個用於執行元數據的加載,另一個用於調度元數據加載請求到這個線程池中,併發數都由 num_metadata_loading_threads 參數控制,默認爲16。調度請求的線程池(圖中的 SubmitterThreads)中的線程不斷從隊列頭部取出請求(Task),並提交到執行元數據加載的線程池(圖中的 TableLoadingPool)裏去執行。每個調度線程只有在當前的請求加載完後,纔會從隊列中取出下一個請求,因此每個時刻最多有16個異步請求被提交到 TableLoadingPool 裏去執行。
在這裏插入圖片描述
爲什麼需要兩個線程池呢?因爲有一些同步加載請求會直接放到執行的線程池裏,跳過調度這一步。兩個線程池的方式既保證了異步請求不會被餓死(Starvation),也保證了同步請求能儘量早地被執行。具體可參見 IMPALA-9140 裏的討論,其實是有改進空間的,這裏不多展開。

值得一提的是,曾經有這麼一個bug(IMPALA-4765):某個表的元數據加載時間很長,導致 Impalad 遲遲收不到它的元數據,因此對這個表發送了多輪的 PrioritizedLoad 請求,最終導致整個線程池裏的線程都在加載這個表,其它表的元數據就沒機會加載了(即出現 Starvation 現象)。此時整個集羣的表現就是大量查詢處於 CREATED 狀態,而且呈持續堆積的態勢。這個 bug 在數倉中存在大表時容易觸發, Impala-2.9 修復了這個問題(做了更精確的判重),對應的 CDH 版本是 5.12,建議還在使用老版本 CDH 的用戶儘量升級。

Catalogd內元數據加載請求的調度源碼:

  • https://github.com/apache/impala/blob/3.3.0/fe/src/main/java/org/apache/impala/catalog/TableLoadingMgr.java
  • https://github.com/apache/impala/blob/3.3.0/fe/src/main/java/org/apache/impala/catalog/CatalogServiceCatalog.java#L1701

3. 元數據的清除 —— Invalidate Metadata

Impala的表級元數據只有兩種狀態,即加載和未加載。如果已加載,則爲具體的實現(HdfsTable、KuduTable、View等):
在這裏插入圖片描述
前面已經介紹了元數據從啓動時的未加載轉爲已加載狀態的各種機制,正常情況下,元數據已加載的表不會自動回到 IncompleteTable 的狀態。如果在 Impala 中對某個表執行了 DDL/DML,Impala會對應地更改元數據緩存,以讓其保持最新狀態(還是已加載狀態)。如果是外部系統(如Hive、Spark)對某個表做了更改,則Impala緩存的變成了過時的元數據,會導致查詢失敗或查漏數據。Impala 提供了 Invalidate Metadata 和 Refresh 兩個語句來解決這種情況,語法如下:

  • INVALIDATE METADATA [[db_name.]table_name]
  • REFRESH [db_name.]table_name [PARTITION (key_col1=val1 [, key_col2=val2…])]

INVALIDATE METADATA 直接把元數據重置回未加載狀態,從而下次使用時加載的就是最新的元數據。如果未指定表名,則清空的是全體元數據(包括所有db、函數的元數據),相當於重啓了Catalog Server。如果指定了表名,則只是把該表的元數據刪除,然後在 HMS 中查詢該表是否存在,存在則爲其生成一個 IncompleteTable 對象。實踐中重新加載元數據的做法會讓查詢延時產生較大的抖動,因此大部分場景推薦增量的更新方式。

REFRESH 語句讓 Impala 增量更新指定表的元數據(表名是必須指定的),還可以細化到 partition 級別。REFRESH 語句以 mtime(最近更改時間)爲依據對元數據進行增量更新,所有 mtime 未變的 partition 都被認爲元數據未改變而跳過更新。

INVALIDATE METADATA 和 REFRESH 語句的實現細節較多(特別是加 sync_ddl=true 的情況),我們在後續文章中會專門討論。關於 INVALIDATE METADATA 和 REFRESH 語句的細節可參考源碼:

  • https://github.com/apache/impala/blob/3.3.0/fe/src/main/java/org/apache/impala/catalog/CatalogServiceCatalog.java#L1443
  • https://github.com/apache/impala/blob/3.3.0/fe/src/main/java/org/apache/impala/catalog/CatalogServiceCatalog.java#L1354
  • https://github.com/apache/impala/blob/3.3.0/fe/src/main/java/org/apache/impala/catalog/CatalogServiceCatalog.java#L2039
  • https://github.com/apache/impala/blob/3.3.0/fe/src/main/java/org/apache/impala/service/CatalogOpExecutor.java#L3663

4. FAQ

下面我們集中討論一下文章開頭列出的問題。

4.1 同樣的查詢,爲什麼第一次運行比後面幾次運行都要慢很多?

第一次運行時表的元數據未加載,Impalad在編譯查詢時向Catalogd發送PrioritizedLoad請求,等待Catalogd加載需要額外的時間。而第二次第三次再運行查詢時,這部分的時間就不需要了,因此會更快。

另外如果表的元數據被 INVALIDATE METADATA 清空,查詢的編譯也會因爲元數據加載而突然變慢。如果遇到全局的(即不加表名的)INVALIDATE METADATA,則集羣裏所有新提交的查詢都會突然變慢。

4.2 在Hive中建了個新表,但在Impala中不可見,如何解決?

執行 INVALIDATE METADATA table_name 讓 Impala 感知到新表的存在。

類似的,如果在 Hive 中建了個新庫,也只有通過 INVALIDATE METADATA db_name.table_name 才能讓 Impala 感知到這個新庫的存在。由於 INVALIDATE METADATA 目前不支持只指定庫名(IMPALA-1763),如果這個數據庫裏沒有任何表,那就只能用不帶表名的 INVALIDATE METADATA 了…… 這種場景還是建議直接在 Impala 裏建庫,或者在 Hive 中再建個表來對它執行 INVALIDATE METADATA table_name。

4.3 在Hive中建了個新的函數,但在Impala中不可見,如何解決?

乍一看好像只能用不帶表名的 INVALIDATE METADATA,因爲沒有函數級別的 INVALIDATE METADATA。但其實有個語句專門爲這個場景而生:REFRESH FUNCTIONS db_name,用它就可以了。

4.4 HUE 中使用 Impala Editor 時,爲什麼有些 View 被顯示成了表?

元數據未加載時,Impala 只知道庫名和表名,因此不知道一個表是否是View。因此在返回給HUE的元數據中,凡是元數據未加載的表統一都當作表來返回。解決辦法是在 HUE 中執行 DESCRIBE table_name 觸發這個表元數據的加載,然後再點擊 “Clear Cache” 模式的 Refresh 讓HUE重新從Impala獲取元數據。

相關源碼可參考:

  • https://github.com/apache/impala/blob/3.3.0/fe/src/main/java/org/apache/impala/service/Frontend.java#L1562
  • https://github.com/apache/impala/blob/3.3.0/fe/src/main/java/org/apache/impala/service/MetadataOp.java#L515
  • https://github.com/apache/impala/blob/3.3.0/fe/src/main/java/org/apache/impala/service/MetadataOp.java#L328-L331

4.5 Invalidate metadata 和 Refresh語句有什麼區別?各有什麼應用場景?

Invalidate metadata 用來清空(重置)元數據,執行完後元數據處於未加載狀態。Refresh 用來增量更新元數據,執行完後元數據處於已加載狀態。

大部分情況我們推薦用 REFRESH 語句來解決元數據過時的問題,只有以下兩種情況需要使用 INVALIDATE METADATA:

  • Hive 中創建的新表在 Impala 中找不到,使用 REFRESH 語句會報錯。
  • HDFS Rebalance 挪動了文件的 block 位置,此時 partition、文件的 mtime 都不變,REFRESH 語句只看 mtime,因此感知不到 block 位置過時了,並不會更新 block 位置。這種情況的後果是查詢分片(PlanFragment)會被調度到錯誤的 Impalad 去執行,導致查詢性能變差(Impala以爲是本地讀,其實變成了遠程讀)。

5. 總結

Impala 通過在 Server 級別緩存元數據來加速查詢的編譯,不同的查詢共用同一份元數據緩存。由於元數據總量很大(相當於HMS+NameNode的元數據),Impala在啓動時並沒有全部加載,只加載了所有數據庫和UDF的元數據以及各表的表名

表級元數據只有未加載和已加載兩種狀態,初次使用時 Impalad 會發送 PrioritizedLoad 請求讓Catalogd 進行異步加載,Catalogd 中執行的 DDL/DML 也可能發起同步加載的請求。Catalogd 中用線程數爲 --num_metadata_loading_threads(默認值爲16)的線程池來執行加載請求,調高這個參數能讓 Catalogd 同時加載更多的表,但也會增加對 HMS 和 NameNode 的壓力。關於 HdfsTable 的元數據加載還有一些相關參數,我們在下一篇文章中再一併介紹。

Catalogd 加載完元數據後通過 Statestored 廣播給所有 Coordinator 角色的 Impalad,從而讓 Impalad 能繼續查詢的編譯。在這之前 Impalad 對該查詢的編譯會一直處於等待狀態,並按需或定時重發 PrioritizedLoad 請求。

表級元數據只有通過 Invalidate Metadata 才能重置回未加載的狀態,但對查詢的編譯延時有影響,一般情況下建議使用 Refresh。

6. Future Work

表級元數據的二元化狀態(加載 vs 未加載)帶來了很多問題,比如上述 FAQ 中提到的把 View 當成 Table 返回給 HUE 的問題。其實應該引進更細粒度的狀態來減少元數據加載引起的等待時間,比如執行 DESCRIBE table_name 時,只需要獲取 HMS 中的元數據就夠了,不需要再從 NameNode 加載所有文件的元數據。社區在 2018 年發起了 “Fetch-on-demand metadata” 項目(IMPALA-7127,也叫 LocalCatalog 或 catalog-v2,我們在後續文章會專門介紹),目前解決了 Impalad 端元數據粒度的問題,能做到只向 Catalogd 獲取查詢需要的元數據,也解決了 Impalad 端元數據緩存沒有上限的問題。然而 Catalogd 端的元數據問題還沒有解決,主要因爲 DDL/DML 引入的複雜性,無法照搬 Impalad 端的解決方案,這塊目前還在進行當中,可以關注以下 JIRA:

  • IMPALA-3127: 把元數據的粒度做到 partition 級別(目前是表級別)
  • IMPALA-8937: Catalog Server 端細粒度的元數據加載

下一篇文章將介紹表級元數據的加載細節,敬請期待!

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