本文結構採用巨集觀著眼,微觀入手,從整體到細節的方式剖析 Hive SQL 底層原理。第一節先介紹 Hive 底層的整體執行流程,然後第二節介紹執行流程中的 SQL 編譯成 MapReduce 的過程,第三節剖析 SQL 編譯成 MapReduce 的具體實現原理。
Hive
Hive是什麼?Hive 是資料倉儲工具,再具體點就是一個 SQL 解析引擎,因為它即不負責儲存資料,也不負責計算資料,只負責解析 SQL,記錄後設資料。
Hive直接訪問儲存在 HDFS 中或者 HBase 中的檔案,通過 MapReduce、Spark 或 Tez 執行查詢。
我們今天來聊的就是 Hive 底層是怎樣將我們寫的 SQL 轉化為 MapReduce 等計算引擎可識別的程式。瞭解 Hive SQL 的底層編譯過程有利於我們優化Hive SQL,提升我們對Hive的掌控力,同時有能力去定製一些需要的功能。
Hive 底層執行架構
我們先來看下 Hive 的底層執行架構圖, Hive 的主要元件與 Hadoop 互動的過程:
在 Hive 這一側,總共有五個元件:
UI:使用者介面。可看作我們提交SQL語句的命令列介面。
DRIVER:驅動程式。接收查詢的元件。該元件實現了會話控制程式碼的概念。
COMPILER:編譯器。負責將 SQL 轉化為平臺可執行的執行計劃。對不同的查詢塊和查詢表示式進行語義分析,並最終藉助表和從 metastore 查詢的分割槽後設資料來生成執行計劃。
METASTORE:後設資料庫。儲存 Hive 中各種表和分割槽的所有結構資訊。
EXECUTION ENGINE:執行引擎。負責提交 COMPILER 階段編譯好的執行計劃到不同的平臺上。
上圖的基本流程是:
步驟1:UI 呼叫 DRIVER 的介面;
步驟2:DRIVER 為查詢建立會話控制程式碼,並將查詢傳送到 COMPILER(編譯器)生成執行計劃;
步驟3和4:編譯器從後設資料儲存中獲取本次查詢所需要的後設資料,該後設資料用於對查詢樹中的表示式進行型別檢查,以及基於查詢謂詞修建分割槽;
步驟5:編譯器生成的計劃是分階段的DAG,每個階段要麼是 map/reduce 作業,要麼是一個後設資料或者HDFS上的操作。將生成的計劃發給 DRIVER。
如果是 map/reduce 作業,該計劃包括 map operator trees 和一個 reduce operator tree,執行引擎將會把這些作業傳送給 MapReduce :
步驟6、6.1、6.2和6.3:執行引擎將這些階段提交給適當的元件。在每個 task(mapper/reducer) 中,從HDFS檔案中讀取與表或中間輸出相關聯的資料,並通過相關運算元樹傳遞這些資料。最終這些資料通過序列化器寫入到一個臨時HDFS檔案中(如果不需要 reduce 階段,則在 map 中操作)。臨時檔案用於向計劃中後面的 map/reduce 階段提供資料。
步驟7、8和9:最終的臨時檔案將移動到表的位置,確保不讀取髒資料(檔案重新命名在HDFS中是原子操作)。對於使用者的查詢,臨時檔案的內容由執行引擎直接從HDFS讀取,然後通過Driver傳送到UI。
Hive SQL 編譯成 MapReduce 過程
編譯 SQL 的任務是在上節中介紹的 COMPILER(編譯器元件)中完成的。Hive將SQL轉化為MapReduce任務,整個編譯過程分為六個階段:
詞法、語法解析: Antlr 定義 SQL 的語法規則,完成 SQL 詞法,語法解析,將 SQL 轉化為抽象語法樹 AST Tree;
Antlr是一種語言識別的工具,可以用來構造領域語言。使用Antlr構造特定的語言只需要編寫一個語法檔案,定義詞法和語法替換規則即可,Antlr完成了詞法分析、語法分析、語義分析、中間程式碼生成的過程。
語義解析: 遍歷 AST Tree,抽象出查詢的基本組成單元 QueryBlock;
生成邏輯執行計劃: 遍歷 QueryBlock,翻譯為執行操作樹 OperatorTree;
優化邏輯執行計劃: 邏輯層優化器進行 OperatorTree 變換,合併 Operator,達到減少 MapReduce Job,減少資料傳輸及 shuffle 資料量;
生成物理執行計劃: 遍歷 OperatorTree,翻譯為 MapReduce 任務;
優化物理執行計劃: 物理層優化器進行 MapReduce 任務的變換,生成最終的執行計劃。
下面對這六個階段詳細解析:
為便於理解,我們拿一個簡單的查詢語句進行展示,對5月23號的地區維表進行查詢:
select * from dim.dim_region where dt = '2021-05-23';
階段一:詞法、語法解析
根據Antlr定義的sql語法規則,將相關sql進行詞法、語法解析,轉化為抽象語法樹AST Tree:
ABSTRACT SYNTAX TREE:
TOK_QUERY
TOK_FROM
TOK_TABREF
TOK_TABNAME
dim
dim_region
TOK_INSERT
TOK_DESTINATION
TOK_DIR
TOK_TMP_FILE
TOK_SELECT
TOK_SELEXPR
TOK_ALLCOLREF
TOK_WHERE
=
TOK_TABLE_OR_COL
dt
'2021-05-23'
階段二:語義解析
遍歷AST Tree,抽象出查詢的基本組成單元QueryBlock:
AST Tree生成後由於其複雜度依舊較高,不便於翻譯為mapreduce程式,需要進行進一步抽象和結構化,形成QueryBlock。
QueryBlock是一條SQL最基本的組成單元,包括三個部分:輸入源,計算過程,輸出。簡單來講一個QueryBlock就是一個子查詢。
QueryBlock的生成過程為一個遞迴過程,先序遍歷 AST Tree ,遇到不同的 Token 節點(理解為特殊標記),儲存到相應的屬性中。
階段三:生成邏輯執行計劃
遍歷QueryBlock,翻譯為執行操作樹OperatorTree:
Hive最終生成的MapReduce任務,Map階段和Reduce階段均由OperatorTree組成。
基本的操作符包括:
TableScanOperator SelectOperator FilterOperator JoinOperator GroupByOperator ReduceSinkOperator`
Operator在Map Reduce階段之間的資料傳遞都是一個流式的過程。每一個Operator對一行資料完成操作後之後將資料傳遞給childOperator計算。
由於Join/GroupBy/OrderBy均需要在Reduce階段完成,所以在生成相應操作的Operator之前都會先生成一個ReduceSinkOperator,將欄位組合並序列化為Reduce Key/value, Partition Key。
階段四:優化邏輯執行計劃
Hive中的邏輯查詢優化可以大致分為以下幾類:
投影修剪 推導傳遞謂詞 謂詞下推 將Select-Select,Filter-Filter合併為單個操作 多路 Join 查詢重寫以適應某些列值的Join傾斜
階段五:生成物理執行計劃
生成物理執行計劃即是將邏輯執行計劃生成的OperatorTree轉化為MapReduce Job的過程,主要分為下面幾個階段:
對輸出表生成MoveTask 從OperatorTree的其中一個根節點向下深度優先遍歷 ReduceSinkOperator標示Map/Reduce的界限,多個Job間的界限 遍歷其他根節點,遇過碰到JoinOperator合併MapReduceTask 生成StatTask更新後設資料 剪斷Map與Reduce間的Operator的關係
階段六:優化物理執行計劃
Hive中的物理優化可以大致分為以下幾類:
分割槽修剪(Partition Pruning) 基於分割槽和桶的掃描修剪(Scan pruning) 如果查詢基於抽樣,則掃描修剪 在某些情況下,在 map 端應用 Group By 在 mapper 上執行 Join 優化 Union,使Union只在 map 端執行 在多路 Join 中,根據使用者提示決定最後流哪個表 刪除不必要的 ReduceSinkOperators 對於帶有Limit子句的查詢,減少需要為該表掃描的檔案數 對於帶有Limit子句的查詢,通過限制 ReduceSinkOperator 生成的內容來限制來自 mapper 的輸出 減少使用者提交的SQL查詢所需的Tez作業數量 如果是簡單的提取查詢,避免使用MapReduce作業 對於帶有聚合的簡單獲取查詢,執行不帶 MapReduce 任務的聚合 重寫 Group By 查詢使用索引表代替原來的表 當表掃描之上的謂詞是相等謂詞且謂詞中的列具有索引時,使用索引掃描
經過以上六個階段,SQL 就被解析對映成了叢集上的 MapReduce 任務。
SQL編譯成MapReduce具體原理
在階段五-生成物理執行計劃,即遍歷 OperatorTree,翻譯為 MapReduce 任務,這個過程具體是怎麼轉化的呢
我們接下來舉幾個常用 SQL 語句轉化為 MapReduce 的具體步驟:
Join的實現原理
以下面這個SQL為例,講解 join 的實現:
select u.name, o.orderid from order o join user u on o.uid = u.uid;
在map的輸出value中為不同表的資料打上tag標記,在reduce階段根據tag判斷資料來源。MapReduce的過程如下:
Group By的實現原理
以下面這個SQL為例,講解 group by 的實現:
select rank, isonline, count(*) from city group by rank, isonline;
將GroupBy的欄位組合為map的輸出key值,利用MapReduce的排序,在reduce階段儲存LastKey區分不同的key。MapReduce的過程如下:
Distinct的實現原理
以下面這個SQL為例,講解 distinct 的實現:
select dealid, count(distinct uid) num from order group by dealid;
當只有一個distinct欄位時,如果不考慮Map階段的Hash GroupBy,只需要將GroupBy欄位和Distinct欄位組合為map輸出key,利用mapreduce的排序,同時將GroupBy欄位作為reduce的key,在reduce階段儲存LastKey即可完成去重: