基於Impala的高效能數倉實踐之執行引擎模組

語言: CN / TW / HK

導讀:

本系列文章將結合實際開發和使用經驗,聊聊可以從哪些方面對數倉查詢引擎進行優化

Impala是Cloudera開發和開源的數倉查詢引擎,以效能優秀著稱。除了Apache Impala開源專案,業界知名的Apache Doris和StarRocks、SelectDB專案也跟Impala有千絲萬縷的聯絡。筆者所在的網易數帆大資料團隊,是最早一批將其作為分析型數倉查詢引擎的團隊,目前正基於Impala打造有數高效能數倉引擎。

文章大致可以分為這幾個部分:首先會對簡單介紹下Impala的架構和元資料管理,以便後續內容展開;接著從執行引擎,儲存優化,物化檢視,資料快取和虛擬數倉等維度進行探討。本文為執行引擎篇。

1

Impala簡介

Impala叢集包含一個Catalog Server (Catalogd)、一個Statestore Server (Statestored) 和若干個Impala Daemon (Impalad)。Catalogd主要負責元資料的獲取和DDL的執行,Statestored主要負責訊息/元資料的廣播,Impalad主要負責查詢的接收和執行。

Impalad又可配置為coordinator only、 executor only 或coordinator and executor(預設)三種模式。Coordinator角色的Impalad負責查詢的接收、計劃生成、查詢的排程等,Executor角色的Impalad負責資料的讀取和計算。預設配置下每個Impalad既是Coordinator又是Executor。生產環境建議做好角色分離,即每個Impalad要麼是Coordinator要麼是Executor。

1.1 元資料管理

Impala的元資料快取在catalogd和各個Coordinator角色的Impalad中。Catalogd中的快取是最新的,各個Coordinator都快取的是Catalogd內元資料的一個複本。元資料由Catalogd向外部系統獲取,並通過Statestored 傳播給各個Coordinator。

以Hive表為例,Catalogd中的元資料分別從Hive Metastore(HMS)和HDFS NameNode(NN)獲取。從HMS獲取的資訊包括元資料資訊和統計資訊兩部分,元資料資訊指有哪些庫和表,表定義,列型別等,對應“show databases,show tables,show create table xxx,show ”等操作。統計資訊包括表的大小,行數,分割槽和各列的資訊等,對應“show table stats xx,show column stats xx”等操作。從NN獲取的是檔案粒度的資訊,包括檔案儲存位置,副本和檔案塊資訊等。

1.2 管理伺服器

管理伺服器是有數高效能數倉增加的Impala模組,提供叢集粒度的SQL檢視介面,持久化儲存歷史查詢資訊並展示,SQL審計,查詢錯誤和查詢效能分析,自動進行統計資訊計算等。

2

執行引擎(Execute Engine)

2.1 執行模型

在執行模型這塊,目前主要有動態程式碼生成(code generation或just in time/JIT)和向量化計算兩個流派,Impala主要是基於JIT進行效能優化,對於向量化引擎,Impala社群版目前並沒有相關規劃,有數高效能數倉團隊也有計劃對其進行向量化改造。

在具體實現上,Impala屬於改進版的火山模型,官方論文描述為

The execution model is the traditional Volcano‑style with Exchange operators. Processing is performed batch‑at‑a‑time: each GetNext() call operates over batches of rows, similar to

即在傳統的火山模型的基礎上加入Exchange操作符,用於進行不同執行節點的資料交換。 每次會獲取一批記錄而不是一條記錄。

不管是JIT還是向量化,其目的都是儘可能地減少執行引擎核心程式碼流程的呼叫次數並提高函式執行效率,這對於需要處理海量記錄時非常重要。Impala通過每次獲取一批記錄來減少呼叫次數,再利用JIT技術來生成針對特定型別資料的執行流程函式,提高每次呼叫的效率。

更進一步,Impala採用資料流水線(streaming pipelined)執行機制,充分利用計算資源進行併發執行。在Impala 4.0版本,完整支援了executor節點的多執行緒執行模型,進一步提高併發能力,壓榨計算資源。

動態程式碼生成原理及優化

JIT技術與靜態編譯技術相反,其是在具體的查詢執行之前才進行程式碼編譯,此時,查詢中需要處理的列型別,用到的運算元和函式都已經確定,可以為該查詢生成特定版本的處理函式。如下圖所示:

左側是通用的從檔案讀取記錄(tuple)並解析的行數,外層一個for迴圈用於對每一列進行處理,內層的switch用於判斷列的型別並呼叫特定的解析函式。如果我們已經知道該記錄由三列組成,型別分別為int,bool和int,那麼JIT技術就可以生成如圖右側的函式版本,不需要for迴圈,也不需要switch判斷,顯然,執行效率更高。

總的來說,Impala使用LLVM來進行JIT優化,生成對於某個具體查詢最優的函式實現。其優化項具體包括移除條件分支(Removing conditionals,如上所示)、移除記憶體載入和內聯虛擬函式呼叫等。

啟用動態程式碼生成時,在查詢執行前需要先動態生成其執行程式碼,因此有一定的時間消耗,對於小查詢,動態程式碼生成可能是有害的,生成程式碼的時間都有可能超過SQL執行時間。Impala提供了DISABLE_CODEGEN_ROWS_THRESHOLD引數,預設為50000,如果SQL需要處理的記錄數小於該值,則不會使用動態程式碼生成進行執行優化。Impala 4.0版本對JIT進行了進一步優化,採用非同步化改造來避免生成JIT程式碼對查詢效能的影響,當編譯未完成時使用原函式,完成後無縫切換成優化後的函式程式碼。

2.2 計算資源

Impala屬於SQL on Hadoop的一種,基於MPP(Massively Parallel Processing,即大規模並行處理)架構,正常情況下,查詢涉及的各種操作均在記憶體中完成的,因此,可用記憶體的多少及對其的利用效率,對Impala查詢效能有極大影響。同樣地,作為一個OLAP查詢引擎,可用的CPU資源對查詢效能也至關重要。Impala雖提供了少數CPU相關配置項,如num_threads_per_core 等,但對CPU使用的控制能力較差。本小節後續僅介紹記憶體資源相關,CPU計算後續另開一篇單獨介紹。

Impala資源池

Impala有比較豐富的資源使用限制方式,稱為準入控制。其中資源池(resource pool)是Impala進行併發控制的主要手段,可以決定某個查詢是否會被拒絕,或執行,或排隊。其主要有兩種控制方式,一種是手動設定最大併發數控制,超過閾值的請求會進行排隊,可以設定允許排隊的最大請求數和排隊時長,超過閾值的請求直接返回失敗;另一種是基於記憶體的併發控制,下面進行重點介紹。

基於記憶體的併發控制

Impala叢集支援通過fair-scheduler.xml設定多個資源池並規定其最大可用記憶體(maxResources),再通過llama-site.xml為每個資源池設定請求級別的記憶體限制,包括記憶體分配上下限max-query-mem-limit和min-query-mem-limit,及clamp-mem-limit-query-option。除了通過資源池相關配置控制請求的記憶體使用,還可以通過MEM_LIMIT請求選項設定記憶體限制。而clamp-mem-limit-query-option就是設定是否允許MEM_LIMIT設定的記憶體突破資源池記憶體配置的限制。

需要注意的是,max-query-mem-limit,min-query-mem-limit和MEM_LIMIT設定的是請求在每個executor節點允許申請的最大記憶體,請求申請的總記憶體還需要乘上執行該請求的executor節點個數。

若Impala通過預估發現查詢所需的記憶體資源超過叢集總記憶體資源,該查詢會被拒絕;若總資源滿足,但由於部分資源已被其他查詢佔用,則會將其放入請求佇列,待可用資源滿足查詢要求時再按查詢提交的先後順序排程執行。

若預估的記憶體資源超過了設定的max-query-mem-limit,則以max-query-mem-limit為準,若小於min-query-mem-limit,則以min-query-mem-limit為準。假設查詢請求設定了MEM_LIMIT,需先判斷clamp-mem-limit-query-option的值,若為true,則仍然受max-query-mem-limit,min-query-mem-limit約束。下面舉個例子進行說明:

假設一個Impala叢集有5個executor節點,叢集配置了一個最大可用記憶體為100GB的資源池。查詢請求的記憶體上下限為10GB和2GB,若clamp-mem-limit-query-option為true,Impala為某個查詢請求A預估的記憶體為14GB(或設定了MEM_LIMIT為14GB),則查詢A在每個executor最多隻能分配10GB記憶體。若clamp-mem-limit-query-option為false,查詢A最多可分配14GB記憶體。

假設clamp-mem-limit-query-option為true,則該資源池最多隻能同時執行2個查詢A這樣的請求(2 * 5 * 10GB)。

通過上面的例子可知Impala的准入控制會在每個executor為查詢請求預留所需的記憶體,因此,所預留的記憶體應該儘可能接近實際所需記憶體,預留過少會導致查詢失敗或中間結果溢位,預留過多會導致叢集資源沒有被充分利用。在記憶體資源管理的精確性方面,Impala還有較多需優化的點。

准入控制存在的問題

(1)叢集同步

Impala進行准入控制的載體是coordinator節點,由於一個叢集至少有2個及以上的coordinator節點,但准入控制是針對整個叢集的。Impala通過statestore的impala-request-queue topic機制在coordinator間週期性地同步每個coordinator上的查詢併發和記憶體使用情況。

Impala採用去中心化的設計來實現准入控制,而不是通過一箇中心節點來統一決策,雖然在效能和可用性上有優勢,但是這會導致coordinator獲取的其他coordinator資訊過舊的問題,尤其是在查詢併發度較高時,會導致准入控制模組做出錯誤的決策。

(2)記憶體預估精度

Impala需要基於統計資訊來評估查詢需要消耗多少記憶體,因為統計資訊裡面會記錄表的記錄數,列的型別和大小等。沒有統計資訊,就無法正確評估記憶體消耗,也就無法以較優的方式執行該查詢。(統計資訊相關的詳細描述見下一小節)

但就算是有統計資訊,仍有可能依然沒法正確估算需消耗的記憶體量。如下所示:

上圖第一張的"Mem Usage"和"Mem Estimate"分別表示查詢實際消耗和預估消耗的總記憶體,可見明顯差別。上圖下面兩張為通過compute incremental stats/compute stats前後通過explain看到的記憶體預估情況,可見每個節點均22MB(共67個executor節點),即該查詢記憶體預估不精確不是因為沒有統計資訊導致的。

資料溢位(spill to disk)

記憶體不夠怎麼辦?

如果因為叢集同步延遲或記憶體預估低於實際所需記憶體,導致查詢執行過程中消耗的記憶體超過准入控制的計算值,此時資料溢位功能可以派上用場。資料溢位是Impala一種兜底機制,避免因中間結果集過大導致記憶體不足,進而引起查詢失敗。當然,並不是所有情況的記憶體不足都會啟用資料溢位,能夠進行資料溢位的運算元主要包括group by,order by,join,distinct和union;

資料溢位機制的用處在於,能夠最大限度避免查詢失敗。OLAP場景由於SQL複雜度遠高於OLTP,耗時也明顯更長,查詢失敗的代價更大。其帶來的問題是因為需要將中間結果寫盤並讀取,SQL查詢效能會明顯下降,因此,應該通過查詢優化儘可能避免資料溢位。

關閉資料溢位

有兩種方法可以關閉資料溢位,均是通過query option來設定,分別是SCRATCH_LIMIT和DISABLE_UNSAFE_SPILLS。

SCRATCH_LIMIT用於設定溢位目錄的大小,當設為0時,即關閉了資料溢位特性。

DISABLE_UNSAFE_SPILLS更加智慧,用於禁止不安全的資料溢位。Impala認為下列情況屬於不安全的溢位:查詢中存在沒有統計資訊的表,或沒有為join設定hint,或對分割槽表進行insert ... select操作。

優化資源分配效率

(1)集中式准入控制

從Impala社群瞭解到,目前Impala在開發新的准入控制實現,預計後續會提供集中式的准入控制方案,詳見 Single Admission Controller per Cluster;

(2)降低准入資訊同步延時

雖可以通過statestore_update_frequency_ms縮短topic更新週期來緩解,但無法從根本上解決。除此之外,在Impala部署時,還應該控制coordinator的數量,對於50個節點以下的叢集,一般情況下配置2個coordinator實現高可用即可;

(3)豐富統計資訊型別

至於為什麼在有統計資訊情況下預估還是不夠精確,原因也很好理解,即統計資訊本身過於粗粒度,缺乏像直方圖這樣細粒度的資料統計。

基於歷史查詢的記憶體估算優化(HBO)

從前述的例子可知,有數的Impala版本通過管理伺服器儲存了Impala執行過的歷史查詢資訊,其中就包括了查詢的實際記憶體使用量。在BI場景,報表SQL會重複執行,往往一天一次或數次,完全可以將該SQL第一次執行的記憶體使用量作為後面幾次的記憶體預估值。進一步,可以提取同類SQL查詢模板,計算該模板下SQL的最大及平均記憶體使用量作為記憶體預估值。

此外,由於BI報表的SQL都來源於事先建立的資料模型,可以預先計算資料模型SQL的記憶體消耗,在執行該模型對應的報表SQL時,模型部分的記憶體消耗無需再次計算,直接代入即可。基於此,我們完成了方案設計和功能實現,下圖為一個查詢開啟HBO優化前後的記憶體估算值變化

下圖為將HBO用在某業務叢集上啟用前後的效果對比。

2.3 基於代價優化(CBO)

CBO與RBO(基於規則優化)都是傳統的執行計劃優化方式。CBO主要基於索引和統計資訊等元資料來選擇更優的執行計劃。傳統的商業數倉和OLTP系統有健全的索引系統,並且會自動計算表的統計資訊。因此,CBO往往能夠較充分發揮。但目前開源的分析型數倉查詢引擎(下稱OLAP),做得並不好,以Impala為例,自身並沒有索引系統,主要依賴底層的儲存系統,雖提供了統計資訊計算的命令,但不會自動進行統計資訊計算。在此,我們先介紹其對統計資訊的使用。

統計資訊的用途

在Impala中,統計資訊主要用於准入控制和確定Join方式等場景。

確定Join方式包括Join的先後順序和Join的方法,Join方式有shuffle和broadcast兩種。如下文描述:

Impala does not consider all possible join orderings, focusing instead on the subset of left deep join plans. This usually means joins are arranged in a long chain where the left input is preferred to be larger than the right input.

三個及以上的表進行Join時,一般選擇將結果集最小的Join先算掉,對於兩表Join,若是大表和小表,由於Impala使用Hash Join,採用大表左(probe table),小表在右(build table)的方式,將小表broadcast到大表分片所在的各個executor節點,若是大表跟大表,則採用shuffle的方式,兩表都會進行Hash分片,各個executor節點對兩表相同Hash值的分片進行Join。

如果SQL中的表缺失了統計資訊,如查詢所涉及的記錄數,所涉及的各列的大小等,則無法準確預估該SQL的記憶體消耗,導致准入控制模組出現誤判,生產環境中常會出現因executor節點可用記憶體不足導致查詢排隊的情況,但其實此時記憶體是夠的,這裡有多方面的原因,比如該查詢SQL所需記憶體預估值過大,或已經在執行的查詢的配額過大等。相反的,如果預估所需記憶體過小,則可能導致查詢在執行過程中因為executor節點無法分配所需記憶體而導致SQL執行失敗。

同樣的,如果沒有統計資訊,也就無法判斷兩表參與Join的記錄數和大小,出現大小表Join時大表被廣播的情況。在Impala中,兩表Join,沒有統計資訊的表會被放在右邊,所以,對大表做統計資訊計算顯得更加重要。

除此之外,統計資訊用於調優前文提到的動態程式碼生成。上文提到的 DISABLE_CODEGEN_ROWS_THRESHOLD 引數需要在有統計資訊的情況下使用,無統計資訊,意味著不知道需要處理多少條記錄,該引數也就無法生效。

統計資訊計算

Impala為什麼不像MySQL等資料庫一樣自動計算和更新表統計資訊呢?個人認為,主要是不好做,MySQL自動進行統計資訊更新的方式是監測表中的記錄,如果更新的記錄數超過設定的閾值,則自動觸發更新。當Impala對接Hive表時,往往僅用於查詢而不是資料產出,資料產出由Spark或Hive負責,因此也就無法自動感知表中資料的變化。

如果能夠及時感知Hive表的資料變化情況,那麼就有辦法驅動統計資訊更新。Impala可以通過訂閱有數大資料開發及管理平臺的資料產出日誌,感知Hive表的資料變化。具體的統計資訊計算由Impala管理伺服器執行。Impala 3.4版本下,表和列的統計資訊欄位如下所示。

[localhost:21000] > show table stats t1;
Query: show table stats t1
+-------+--------+------+--------+
| #Rows | #Files | Size | Format |
+-------+--------+------+--------+
| -1 | 1 | 33B | TEXT |
+-------+--------+------+--------+
Returned 1 row(s) in 0.02s
[localhost:21000] > show column stats t1;
Query: show column stats t1
+--------+--------+------------------+--------+----------+----------+
| Column | Type | #Distinct Values | #Nulls | Max Size | Avg Size |
+--------+--------+------------------+--------+----------+----------+
| id | INT | -1 | -1 | 4 | 4 |
| s | STRING | -1 | -1 | -1 | -1 |
+--------+--------+------------------+--------+----------+----------+
Returned 2 row(s) in 1.71s

進行統計資訊計算的“compute stats”命令本質是通過兩條SQL分別獲取表/分割槽和列粒度的資訊:即為上述兩個查詢結果中的為“-1”的欄位進行賦值,如下所示:

兩個SQL均需在全表掃描的基礎上進行聚合操作。對於大表,這需要消耗可觀的計算資源,而且,若表中的列個數非常多,則統計資訊的儲存空間也是需要考慮的因素(需要持久化到HMS元資料庫中,並快取在catalogd和impalad)。對於分割槽表,一般使用“compute incremental stats”每次僅計算一個分割槽,但有時仍會因為統計資訊過大而出錯,這是由於超過了增量統計資訊計算的“inc_stats_size_limit_bytes”引數設定值導致:

org.apache.impala.common.AnalysisException: Incremental stats size estimate exceeds 200.00MB.

參考Impala的文件,統計資訊計算可以進行如下優化:

  • 對於分割槽表,僅對頻繁查詢的分割槽計算統計資訊,並定期刪除舊分割槽統計資訊;

  • 對於寬表,僅對頻繁查詢的列計算統計資訊;

  • 對於記錄數過多的表,啟用統計資訊高階特性:推斷和取樣(Extrapolation and Sampling)

從效能優化角度,統計資訊顯然是越精細越好。但在OLAP這種大資料量場景下,越精細意味著越龐大的統計資訊計算和儲存開銷,使用何種粒度的統計資訊是個需要權衡的問題。

2.4 查詢的分散式執行

在大資料場景下,單靠單臺伺服器執行分析型查詢操作顯然過於單薄,所以分析型數倉一般基於MPP(Massively Parallel Processing,即大規模並行處理)架構,Impala就是基於MPP,可以將一個查詢分為多個片段分散式執行。在Impala上,分散式執行又可分為節點間和節點內。

為了能夠在分散式執行的同時,能夠對不同業務或不同型別的SQL進行隔離,避免相互影響,有數的Impala版本進一步引入了虛擬數倉概念,能夠有效的進行資源隔離,同時有兼顧資源的有效利用。對於虛擬數倉,我們會在後續單獨寫一篇文章進行介紹。

節點間並行

在“Impala簡介”小節提到,Impala有多個executor節點,在確定執行計劃時,Impala會充分考慮併發執行該查詢,儘可能將需要掃描的資料分成range分發到各executor節點上執行,並響應對資料進行查詢所需的運算元進行分散式計算。

因此,在資料量足夠大的情況下,增加executor節點數可以提升查詢效能。若executor所在伺服器的計算資源充足,可以考慮同一臺伺服器上部署多個executor節點。

節點內並行

Impala還可以通過MT_DOP引數配置查詢在executor節點內的執行併發執行緒數。對於統計資訊計算產生的SQL,Impala自動將MT_DOP設定為4以提升計算效能。相比節點間並行,節點內並行通過query option設定,更加靈活可控。在Impala 3.4及之前版本,MT_DOP不夠完善,無法支援分散式Join等操作,從Impala 4.0開始,MT_DOP已支援絕大部分運算元。我們在TPCH和TPCDS場景下的測試資料表明,將MT_DOP設定為16的效能明顯好於不設定或將其設定為1時的效能,絕對效能有數倍提升。

合理配置並行數

顯然,查詢的執行並行度不是越高越好,需要考慮Impala叢集的查詢併發數以及executor節點的計算資源可用量。一般建議executor節點所在伺服器的計算資源和網路資源的利用率應該小於80%。

對於節點內並行,需要考慮impala profile輸出對效能的影響,在配置高MT_DOP時,應啟用精簡模式的profile-v2(gen_experimental_profile=true),防止profile過大。

2.5 查詢重試和改寫

查詢重試

查詢出錯的原因有很多,比如執行該查詢的任意一個executor不可用(宕機或網路隔離等),或因排隊過久導致執行超時,或因元資料過舊導致執行出錯等。

在Impala 4.0版本,引入了查詢透明重試的特性,該特性會判斷引起查詢出錯的原因,目前支援對因executor不可用而出錯的查詢進行自動重試,無需使用者/客戶端參與。

元資料過舊重試

因元資料過舊導致執行出錯是Impala特有的錯誤場景,最典型的錯誤形如:“Failed to open HDFS file .....”。有數的Impala版本還支援對該類錯誤進行透明重試,coordinator節點通過匹配錯誤關鍵字識別錯誤型別。在重試前會解析HDFS檔案路徑獲取庫名和表名,並獲取當前該表的元資料版本,重試時若元資料版本未變化,這會將對應的表元資料失效掉,重新載入元資料。

對元資料錯誤進行查詢重試,是一種把錯誤內部化的一種優化方式。元資料過舊是由於Impala出於效能考慮對其進行了快取,對使用者來說元資料快取應該是黑盒的,因快取過舊導致的錯誤,不應該直接暴露給使用者,應該在系統設計時消化掉。

SQL改寫

常規改寫

Impala提供了表示式級別的改寫優化,改寫規則主要包括常量摺疊、通用表示式提取和,全部規則如下所示:

List<ExprRewriteRule> rules = new ArrayList<>();
// BetweenPredicates must be rewritten to be executable. Other non-essential
// expr rewrites can be disabled via a query option. When rewrites are enabled
// BetweenPredicates should be rewritten first to help trigger other rules.
rules.add(BetweenToCompoundRule.INSTANCE);
//between轉大小比較
// Binary predicates must be rewritten to a canonical form for both Kudu predicate
// pushdown and Parquet row group pruning based on min/max statistics.
rules.add(NormalizeBinaryPredicatesRule.INSTANCE);
//規範化二元謂語,如“5 + 3 = id"改為"id = 5 + 3"
if (queryCtx.getClient_request().getQuery_options().enable_expr_rewrites) {
rules.add(FoldConstantsRule.INSTANCE);
//常量摺疊,如"1 + 1"改為"2"
rules.add(NormalizeExprsRule.INSTANCE);
//規範化表示式,如"id = 0 OR false"改為"FALSE OR id = 0"
rules.add(ExtractCommonConjunctRule.INSTANCE);
//通用表示式提取,如"(int_col < 10 and bigint_col < 10) or " + "(string_col = '10' and int_col < 10)"改為"int_col < 10 AND ((bigint_col < 10) OR (string_col = '10'))"
// Relies on FoldConstantsRule and NormalizeExprsRule.
rules.add(SimplifyConditionalsRule.INSTANCE);
//簡化條件判斷,場景覆蓋較廣,包括if、case等等,如"if(true, id, id+1)"改為"id"
rules.add(EqualityDisjunctsToInRule.INSTANCE);
//or轉in,如"int_col = 1 or int_col = 2"改為"int_col IN (1, 2)"
rules.add(NormalizeCountStarRule.INSTANCE);
//count(常量)轉為count(*),如"count(1)"改為"count(*)"
rules.add(SimplifyDistinctFromRule.INSTANCE);
//簡化條件判斷(is distinct, <, >, <=>等)語句,如"if(bool_col <=> bool_col, 1, 2)"改為"1"
rules.add(SimplifyCastStringToTimestamp.INSTANCE);
//簡化將字串轉為時間戳,如"cast(unix_timestamp(date_string_col) as timestamp)"改為"CAST(date_string_col AS TIMESTAMP)"
}
exprRewriter_ = new ExprRewriter(rules);
}

從上面的程式碼可知,大部分的改寫規則需要通過ENABLE_EXPR_REWRITES 這個query option開啟。

定製改寫

有數的Impala版本在上述基礎上,結合BI工具和業務屬性進行鍼對性的優化,有助於提升BI查詢效能。其中一項優化是簡化時間比較表示式。舉例如下:

SELECT TO_DATE(CAST(`t1`.`dt` AS TIMESTAMP)) `d0`,`t2`.`itemname` `d1`,
COUNT(DISTINCT `t1`.`user_id`) `m0`
FROM `music_dws`.`dws_log_music_xxx_aggr_di` `t1`
WHERE (((TO_DATE(CAST(`t1`.`dt` AS TIMESTAMP)) >= CAST('2021-01-01' AS TIMESTAMP)) AND (TO_DATE(CAST(`t1`.`dt` AS TIMESTAMP)) < CAST('2022-01-01' AS TIMESTAMP)))

已知t1表為按天分割槽,dt為分割槽欄位,結構為'yyyy-mm-dd',那麼在此條件下,可以將dt欄位與時間字串進行比較,去掉CAST AS TIMESTAMP和TO_DATE操作,上述SQL可改寫為:

SELECT `t1`.`dt` `d0`,
`t2`.`itemname` `d1`,
COUNT(DISTINCT `t1`.`user_id`) `m0`
FROM `music_dws`.`dws_log_music_xxx_aggr_di` `t1`
WHERE (`t1`.`dt` >= '2021-01-01') AND (`t1`.`dt` < '2022-01-01')

高階改寫

從上面Impala原生支援的改寫規則可以看出,其支援的改寫規則都比較初級,實現上是將SQL拆解為SelectList、FromClause、WhereClause、GroupByExpr和OrderByExpr等片段後,對各片段進行改寫。並沒有對SQL進行整體的,跨片段的改寫。

有數的Impala版本還可進一步對SQL整體進行改寫優化,其中最為重要的是基於物化檢視的SQL透明改寫,我們會在後續單獨寫一篇文章進行介紹。除了物化檢視改寫,還有其他一些優化手段,如左連線(left join)消除等。

左連線消除

一般來說,BI軟體基於某個數倉模型(寬表,星型,雪花型等)建立報告,其中包括一張或多張報表,舉一個網易雲音樂使用有數BI報表模型為例,SQL形如:

SELECT `t1`.`os`, other select list
FROM `music_impala`.`left_join_table1` `t1`
LEFT JOIN `music_impala`.`left_join_table2` `t2` ON ((`t1`.`is_new` = `t2`.`is_new`) AND (`t1`.`anchor_id` = `t2`.`anchor_id`) AND (`t1`.`app_ver` = `t2`.`app_ver`) AND (`t1`.`os` = `t2`.`os`) AND (TO_DATE(CAST(`t1`.`dt` AS TIMESTAMP)) = `t2`.`report_date`))
LEFT JOIN `music_impala`.`left_join_table3` `t3` ON ((`t1`.`is_new` = `t3`.`is_new`) AND (`t1`.`anchor_id` = `t3`.`anchor_id`) AND (`t1`.`app_ver` = `t3`.`app_ver`) AND (`t1`.`os` = `t3`.`os`) AND (TO_DATE(CAST(`t1`.`dt` AS TIMESTAMP)) = `t3`.`report_date`))
LEFT JOIN `music_iplay`.`left_join_table4` `t4` ON ((`t1`.`dt` = `t4`.`dt`) AND (`t1`.`anchor_id` = CAST(`t4`.`anchor_id` AS VARCHAR(255))))

可以認為,該模型是將下面 這4個數倉表通過左連線打寬成一個邏輯大寬表。

music_impala.left_join_table1、
music_impala.left_join_table2、
music_impala.left_join_table3
music_impala.left_join_table4`

下面是產生的一個報表的列表篩選器元件產生的SQL。

SELECT `t1`.`os` `d0`
FROM `music_impala`.`left_join_table1` `t1`
LEFT JOIN `music_impala`.`left_join_table2` `t2` ON ((`t1`.`is_new` = `t2`.`is_new`) AND (`t1`.`anchor_id` = `t2`.`anchor_id`) AND (`t1`.`app_ver` = `t2`.`app_ver`) AND (`t1`.`os` = `t2`.`os`) AND (TO_DATE(CAST(`t1`.`dt` AS TIMESTAMP)) = `t2`.`report_date`))
LEFT JOIN `music_impala`.`left_join_table3` `t3` ON ((`t1`.`is_new` = `t3`.`is_new`) AND (`t1`.`anchor_id` = `t3`.`anchor_id`) AND (`t1`.`app_ver` = `t3`.`app_ver`) AND (`t1`.`os` = `t3`.`os`) AND (TO_DATE(CAST(`t1`.`dt` AS TIMESTAMP)) = `t3`.`report_date`))
LEFT JOIN `music_iplay`.`left_join_table4` `t4` ON ((`t1`.`dt` = `t4`.`dt`) AND (`t1`.`anchor_id` = CAST(`t4`.`anchor_id` AS VARCHAR(255))))
GROUP BY `t1`.`os`
LIMIT 20000

對於BI軟體來說,基於模型產生該SQL非常合理。但考慮到模型是邏輯的大寬表,在Impala層面,可以對SQL進行改寫以優化查詢效能。

該篩選器用於在報告中對music_impala.left_join_table1的os欄位進行選擇,且模型中各表Join的條件(ON和WHERE)均沒有對os欄位進行過濾性操作。在這種情況下,如果在select list中沒有對os欄位進一步做SUM/AVG/COUNT等聚合操作(可以是MIN/MAX/DISTINCT等聚合操作),那麼可以去掉left join運算元,改寫成如下形式:

SELECT `t1`.`os` `d0`
FROM `music_impala`.`left_join_table1` `t1`
GROUP BY `t1`.`os`
LIMIT 20000

3

小結

本文簡單說明了Impala的系統架構和元資料管理,介紹了我們內部版本引入的集中式管理伺服器。重點介紹了在分析型數倉技術中執行引擎這塊的主要技術點和常見優化方法,並結合Impala展開進行了分析,包括動態程式碼生成、基於准入控制的資源管理、基於統計資訊的代價計算、查詢並行執行、SQL優化和錯誤重試等。

下一篇我們會重點分析由雲原生數倉Snowflake引入的虛擬數倉特性。

作者簡介

榮廷,網易杭研資料庫開發專家。10年資料庫和儲存開發經驗,2013年起一直從事資料庫核心和資料庫雲服務相關工作,現為杭研資料庫核心團隊負責人;專注於資料庫核心技術和分散式系統架構,樂於挑戰和解決疑難問題;負責網易MySQL分支InnoSQL的開發和優化工作,大幅提升了線上業務的MySQL資料庫服務質量;主導並推動MGR、MyRocks等新方案在考拉海購、雲音樂、傳媒等業務場景大規模使用;累計申請10+技術發明專利(已授權8個),《MySQL 核心:InnoDB 儲存引擎 卷1》作者之一。