Intel李銳:Hive on Spark解析

Hive是基於Hadoop平臺的數據倉庫,最初由Facebook開發,在經過多年發展之後,已經成爲Hadoop事實上的SQL引擎標準。相較於其他諸如Impala、Shark(SparkSQL的前身)等引擎而言,Hive擁有更爲廣泛的用戶基礎以及對SQL語法更全面的支持。Hive最初的計算引擎爲MapReduce,受限於其自身的Map+Reduce計算模式,以及不夠充分的大內利用,MapReduce的性能難以得到提升。

Hortonworks於2013年提出將Tez作爲另一個計算引擎以提高Hive的性能。Spark則是最初由加州大學伯克利分校開發的分佈式計算引擎,藉助於其靈活的DAG執行模式、對內存的充分利用,以及RDD所能表達的豐富語義,Spark受到了Hadoop社區的廣泛關注。在成爲Apache頂級項目之後,Spark更是集成了流處理、圖計算、機器學習等功能,是業界公認最具潛力的下一代通用計算框架。鑑於此,Hive社區於2014年推出了Hive on Spark項目(HIVE-7292),將Spark作爲繼MapReduce和Tez之後Hive的第三個計算引擎。該項目由Cloudera、Intel和MapR等幾家公司共同開發,並受到了來自Hive和Spark兩個社區的共同關注。目前Hive on Spark的功能開發已基本完成,並於2015年1月初合併回trunk,預計會在Hive下一個版本中發佈。本文將介紹Hive on Spark的設計架構,包括如何在Spark上執行Hive查詢,以及如何藉助Spark來提高Hive的性能等。另外本文還將介紹Hive on Spark的進度和計劃,以及初步的性能測試數據。

背景

Hive on Spark是由Cloudera發起,由Intel、MapR等公司共同參與的開源項目,其目的是把Spark作爲Hive的一個計算引擎,將Hive的查詢作爲Spark的任務提交到Spark集羣上進行計算。通過該項目,可以提高Hive查詢的性能,同時爲已經部署了Hive或者Spark的用戶提供了更加靈活的選擇,從而進一步提高Hive和Spark的普及率。

在介紹Hive on Spark的具體設計之前,先簡單介紹一下Hive的工作原理,以便於大家理解如何把Spark作爲新的計算引擎供給Hive使用。

在Hive中, 一條SQL語句從用戶提交到計算並返回結果,大致流程如下圖所示(Hive 0.14中引入了基於開銷的優化器(Cost Based Optimizer,CBO),優化的流程會略有不同。


圖1:SQL語句執行流程

  • 語法分析階段,Hive利用Antlr將用戶提交的SQL語句解析成一棵抽象語法樹(Abstract Syntax Tree,AST)。
  • 生成邏輯計劃包括通過Metastore獲取相關的元數據,以及對AST進行語義分析。得到的邏輯計劃爲一棵由Hive操作符組成的樹,Hive操作符即Hive對錶數據的處理邏輯,比如對錶進行掃描的TableScanOperator,對錶做Group的GroupByOperator等。
  • 邏輯優化即對Operator Tree進行優化,與之後的物理優化的區別主要有兩點:一是在操作符級別進行調整;二是這些優化不針對特定的計算引擎。比如謂詞下推(Predicate Pushdown)就是一個邏輯優化:儘早的對底層數據進行過濾以減少後續需要處理的數據量,這對於不同的計算引擎都是有優化效果的。
  • 生成物理計劃即針對不同的引擎,將Operator Tree劃分爲若干個Task,並按照依賴關係生成一棵Task的樹(在生成物理計劃之前,各計算引擎還可以針對自身需求,對Operator Tree再進行一輪邏輯優化)。比如,對於MapReduce,一個GROUP BY+ORDER BY的查詢會被轉化成兩個MapReduce的Task,第一個進行Group,第二個進行排序。
  • 物理優化則是各計算引擎根據自身的特點,對Task Tree進行優化。比如對於MapReduce,Runtime Skew Join的優化就是在原始的Join Task之後加入一個Conditional Task來處理可能出現傾斜的數據。
  • 最後按照依賴關係,依次執行Task Tree中的各個Task,並將結果返回給用戶。每個Task按照不同的實現,會把任務提交到不同的計算引擎上執行。

總體設計

Hive on Spark總體的設計思路是,儘可能重用Hive邏輯層面的功能;從生成物理計劃開始,提供一整套針對Spark的實現,比如SparkCompiler、SparkTask等,這樣Hive的查詢就可以作爲Spark的任務來執行了。以下是幾點主要的設計原則。

  • 儘可能減少對Hive原有代碼的修改。這是和之前的Shark設計思路最大的不同。Shark對Hive的改動太大以至於無法被Hive社區接受,Hive on Spark儘可能少改動Hive的代碼,從而不影響Hive目前對MapReduce和Tez的支持。同時,Hive on Spark保證對現有的MapReduce和Tez模式在功能和性能方面不會有任何影響。
  • 對於選擇Spark的用戶,應使其能夠自動的獲取Hive現有的和未來新增的功能。
  • 儘可能降低維護成本,保持對Spark依賴的鬆耦合。

基於以上思路和原則,具體的一些設計架構如下。

新的計算引擎

Hive的用戶可以通過hive.execution.engine來設置計算引擎,目前該參數可選的值爲mr和tez。爲了實現Hive on Spark,我們將spark作爲該參數的第三個選項。要開啓Hive on Spark模式,用戶僅需將這個參數設置爲spark即可。

以Hive的表作爲RDD

Spark以分佈式可靠數據集(Resilient Distributed Dataset,RDD)作爲其數據抽象,因此我們需要將Hive的錶轉化爲RDD以便Spark處理。本質上,Hive的表和Spark的HadoopRDD都是HDFS上的一組文件,通過InputFormat和RecordReader讀取其中的數據,因此這個轉化是自然而然的。

使用Hive原語

這裏主要是指使用Hive的操作符對數據進行處理。Spark爲RDD提供了一系列的轉換(Transformation),其中有些轉換也是面向SQL的,如groupByKey、join等。但如果使用這些轉換(就如Shark所做的那樣),就意味着我們要重新實現一些Hive已有的功能;而且當Hive增加新的功能時,我們需要相應地修改Hive on Spark模式。有鑑於此,我們選擇將Hive的操作符包裝爲Function,然後應用到RDD上。這樣,我們只需要依賴較少的幾種RDD的轉換,而主要的計算邏輯仍由Hive提供。

由於使用了Hive的原語,因此我們需要顯式地調用一些Transformation來實現Shuffle的功能。下表中列舉了Hive on Spark使用的所有轉換。


對repartitionAndSortWithinPartitions 簡單說明一下,這個功能由SPARK-2978引入,目的是提供一種MapReduce風格的Shuffle。雖然sortByKey也提供了排序的功能,但某些情況下我們並不需要全局有序,另外其使用的Range Partitioner對於某些Hive的查詢並不適用。

物理執行計劃

通過SparkCompiler將Operator Tree轉換爲Task Tree,其中需要提交給Spark執行的任務即爲SparkTask。不同於MapReduce中Map+Reduce的兩階段執行模式,Spark採用DAG執行模式,因此一個SparkTask包含了一個表示RDD轉換的DAG,我們將這個DAG包裝爲SparkWork。執行SparkTask時,就根據SparkWork所表示的DAG計算出最終的RDD,然後通過RDD的foreachAsync來觸發運算。使用foreachAsync是因爲我們使用了Hive原語,因此不需要RDD返回結果;此外foreachAsync異步提交任務便於我們對任務進行監控。

SparkContext生命週期

SparkContext是用戶與Spark集羣進行交互的接口,Hive on Spark應該爲每個用戶的會話創建一個SparkContext。但是Spark目前的使用方式假設SparkContext的生命週期是Spark應用級別的,而且目前在同一個JVM中不能創建多個SparkContext(請參考SPARK-2243)。這明顯無法滿足HiveServer2的應用場景,因爲多個客戶端需要通過同一個HiveServer2來提供服務。鑑於此,我們需要在單獨的JVM中啓動SparkContext,並通過RPC與遠程的SparkContext進行通信。

任務監控與統計信息收集

Spark提供了SparkListener接口來監聽任務執行期間的各種事件,因此我們可以實現一個Listener來監控任務執行進度以及收集任務級別的統計信息(目前任務級別的統計由SparkListener採集,任務進度則由Spark提供的專門的API來監控)。另外Hive還提供了Operator級別的統計數據信息,比如讀取的行數等。在MapReduce模式下,這些信息通過Hadoop Counter收集。我們可以使用Spark提供的Accumulator來實現該功能。

測試

除了一般的單元測試以外,Hive還提供了Qfile Test,即運行一些事先定義的查詢,並根據結果判斷測試是否通過。Hive on Spark的Qfile Test應該儘可能接近真實的Spark部署環境。目前我們採用的是local-cluster的方式(該部署模式主要是Spark進行測試時使用,並不打算讓一般用戶使用),最終的目標是能夠搭建一個Spark on YARN的Mini Cluster來進行測試。

實現細節

這一部分我們詳細介紹幾個重要的實現細節。

SparkTask的生成和執行

我們通過一個例子來看一下一個簡單的兩表JOIN查詢如何被轉換爲SparkTask並被執行。下圖左半部分展示了這個查詢的Operator Tree,以及該Operator Tree如何被轉化成SparkTask;右半部分展示了該SparkTask執行時如何得到最終的RDD並通過foreachAsync提交Spark任務。


圖2:兩表join查詢到Spark任務的轉換

SparkCompiler遍歷Operator Tree,將其劃分爲不同的MapWork和ReduceWork。MapWork爲根節點,總是由TableScanOperator(Hive中對錶進行掃描的操作符)開始;後續的Work均爲ReduceWork。ReduceSinkOperator(Hive中進行Shuffle輸出的操作符)用來標記兩個Work之間的界線,出現ReduceSinkOperator表示當前Work到下一個Work之間的數據需要進行Shuffle。因此,當我們發現ReduceSinkOperator時,就會創建一個新的ReduceWork並作爲當前Work的子節點。包含了FileSinkOperator(Hive中將結果輸出到文件的操作符)的Work爲葉子節點。與MapReduce最大的不同在於,我們並不要求ReduceWork一定是葉子節點,即ReduceWork之後可以鏈接更多的ReduceWork,並在同一個SparkTask中執行。

從該圖可以看出,這個查詢的Operator Tree被轉化成了兩個MapWork和一個ReduceWork。在執行SparkTask時,首先根據MapWork來生成最底層的HadoopRDD,然後將各個MapWork和ReduceWork包裝成Function應用到RDD上。在有依賴的Work之間,需要顯式地調用Shuffle轉換,具體選用哪種Shuffle則要根據查詢的類型來確定。另外,由於這個例子涉及多表查詢,因此在Shuffle之前還要對RDD進行Union。經過這一系列轉換後,得到最終的RDD,並通過foreachAsync提交到Spark集羣上進行計算。

運行模式

Hive on Spark支持兩種運行模式:本地和遠程。當用戶把Spark Master URL設置爲local時,採用本地模式;其餘情況則採用遠程模式。本地模式下,SparkContext與客戶端運行在同一個JVM中;遠程模式下,SparkContext運行在一個獨立的JVM中。提供本地模式主要是爲了方便調試,一般用戶不應選擇該模式。因此我們這裏也主要介紹遠程模式(Remote SparkContext,RSC)。下圖展示了RSC的工作原理。


圖3:RSC工作原理

用戶的每個Session會創建一個SparkClient,SparkClient會啓動RemoteDriver進程,並由RemoteDriver創建SparkContext。SparkTask執行時,通過Session提交任務,任務的主體就是對應的SparkWork。SparkClient將任務提交給RemoteDriver,並返回一個SparkJobRef,通過該SparkJobRef,客戶端可以監控任務執行進度,進行錯誤處理,以及採集統計信息等。由於最終的RDD計算沒有返回結果,因此客戶端只需要監控執行進度而不需要處理返回值。RemoteDriver通過SparkListener收集任務級別的統計數據,通過Accumulator收集Operator級別的統計數據(Accumulator被包裝爲SparkCounter),並在任務結束時返回給SparkClient。

SparkClient與RemoteDriver之間通過基於Netty的RPC進行通信。除了提交任務,SparkClient還提供了諸如添加Jar包、獲取集羣信息等接口。如果客戶端需要使用更一般的SparkContext的功能,可以自定義一個任務並通過SparkClient發送到RemoteDriver上執行。

理論上來說,Hive on Spark對於Spark集羣的部署方式沒有特別的要求,除了local以外,RemoteDriver可以連接到任意的Spark集羣來執行任務。在我們的測試中,Hive on Spark在Standalone和Spark on YARN的集羣上都能正常工作(需要動態添加Jar包的查詢在yarn-cluster模式下還不能運行,請參考HIVE-9425)。

優化

我們再來看幾個針對Hive on Spark的優化。

Map Join

Map Join是Hive中一個很重要的優化,其原理是,如果參與Join的較小的表可以放入內存,就爲這些小表在內存中生成Hash Table,這樣較大的表只需要通過一個MapWork被掃描一次,然後與內存中的Hash Table進行Join就可以了,省去了Shuffle和ReduceWork的開銷。在MapReduce模式下,通過一個在客戶端本地執行的任務來爲小表生成Hash Table,並保存在本地文件系統上。後續的MapWork首先將Hash Table上傳至HDFS的Distributed Cache中,然後只要讀取大表和Distributed Cache中的數據進行Join就可以了。

Hive on Spark對於Map Join的實現與MapReduce不同。最初我們考慮使用Spark提供的廣播功能來把小表的Hash Table分發到各個計算節點上。使用廣播的優點是Spark採用了高效的廣播算法,其性能應該優於使用Distributed Cache。而使用廣播的缺點是會爲Driver和計算節點帶來很大的內存開銷。爲了使用廣播,Hash Table的數據需要先被傳送到Driver端,然後由Driver進行廣播;而且即使在廣播之後,Driver仍需要保留這部分數據,以便應對計算節點的錯誤。雖然支持Spill,但廣播數據仍會加劇Driver的內存壓力。此外,使用廣播相對的開發成本也較高,不利於對已有代碼的複用。

因此,Hive on Spark選擇了類似於Distributed Cache的方式來實現Map Join(請參考HIVE-7613),而且爲小表生成Hash Table的任務可以分佈式的執行,進一步減輕客戶端的壓力。下圖描述了Hive on Spark如何生成Map Join的任務。


 圖4:Hive on Spark Join優化 

不同於MapReduce,對於Hive on Spark而言,LocalWork只是爲了提供一些優化時的必要信息,並不會真正被執行。對於小表的掃描以獨立的SparkTask分佈式地執行,爲此,我們也實現了能夠分佈式運行的HashTableSinkOperator(Hive中輸出小表Hash Table的操作符),其主要原理是通過提高HDFS Replication Factor的方式,使得生成的HashTable能夠被每個節點在本地訪問。

雖然目前採取了類似Distributed Cache的這種實現方式,但如果在後期的測試中發現廣播的方式確實能夠帶來較大的性能提升,而且其引入的內存開銷可以被接受,我們也會考慮改用廣播來實現Map Join。

Table Cache

Spark的一個優勢就是可以充分利用內存,允許用戶顯式地把一個RDD保存到內存或者磁盤上,以便於在多次訪問時提高性能。另外,在目前的RDD轉換模式中,一個RDD的數據是無法同時被多個下游使用的(請參考SPARK-2688),當一個RDD需要通過不同的轉換得到不同的子節點時,就要被計算多次。這時,我們也應該使用Cache來避免重複計算。

 在Shark和SparkSQL中,都允許用戶顯式地把一張表Cache來提高對該表的查詢性能;對於Hive on Spark,我們也應該充分利用這一特性。

 一個應用場景是Multi Insert查詢,即同一個數據源經過運算後需要被插入到多個表中的情況。比如以下查詢。  


在這種情況下,對應的SparkWork中,一個MapWork/ReduceWork會有多個下游的Work,如果不進行Cache,那麼共享的數據源就會被計算多次。爲了避免這種情況,我們會將這些MapWork/ReduceWork複製成多個,每個對應一個下游的Work(請參考HIVE-8118),並對其共享的數據源進行Cache(由於IOContext的同步問題,該功能尚未完成,預計會在HIVE-9492中實現)。 

更爲一般的應用場景是一張表在查詢中被使用了多次的情況,Hive on Spark目前還不會針對這種查詢進行Cache,不過在後續的工作中會考慮採用自動的或者用戶指定的方式來優化這種查詢。

項目進度與計劃

Hive on Spark最初在Hive的Git倉庫中的spark分支下開發,到2014年12月底,已經完成功能開發,Hive已有的各種查詢基本都能支持。2015年1月初spark分支被合併回trunk,預計會在下一個版本中發佈(具體版本號待定)。

 對於已經搭建好Hadoop和Spark集羣的用戶,使用Hive on Spark是比較容易的,主要是引入Spark依賴和進行恰當的配置即可,具體步驟可以參考Hive on Spark Getting Started Wiki(https://cwiki.apache.org/confluence/display/Hive/Hive+on+Spark%3A+Getting+Started)。此外,Cloudera和Intel還在更早的時候提供了亞馬遜AWS虛擬機鏡像,以方便感興趣的用戶更加快捷的體驗Hive on Spark(請參考Hands-on Hive-on-Spark in the AWS Cloud,http://blog.cloudera.com/blog/2014/12/hands-on-hive-on-spark-in-the-aws-cloud/)。 

項目的下一階段工作重點主要在於Bug修復、性能優化,以及搭建基於YARN的Mini Cluster進行單元測試(請參考HIVE-9211)等。爲了保證效率,開發工作仍將在spark分支上進行,希望瞭解項目最新進展的用戶可以關注該分支。

初步性能測試

隨着項目開發接近尾聲,我們也已經開始對Hive on Spark進行初步的性能測試。測試集羣由10臺亞馬遜AWS虛擬機組成,Hadoop集羣由HDP 2.2搭建,測試數據爲320GB的TPC-DS數據集。目前的測試用例有6條,包含了自定義的查詢以及TPC-DS中的兩條查詢。由於Hive主要用於處理ETL查詢,因此我們在TPC-DS中選取用例時,選取的是較爲接近ETL查詢的用例(TPC-DS中的用例主要針對交互型查詢,Impala、SparkSQL等引擎更適合此類查詢)。爲了更有針對性,測試主要是對Hive on Spark和Hive on Tez進行性能對比,最新的一組測試數據如下圖所示(鑑於項目仍在開發中,該數據僅供參考)。 


圖5:Hive on Spark vs. Hive on Tez

圖中橫座標爲各個測試用例,縱座標爲所用時間,以秒爲單位。

總結

Hive on Spark由多家公司協作開發,從項目開始以來,受到了社區的廣泛關注,HIVE-7292更是有超過140位用戶訂閱,已經成爲Hive社區中關注度最高的項目之一。由於涉及到兩個開源項目,Hive社區和Spark社區的開發人員也進行了緊密的合作。在開發過程中發現的Spark的不足之處以及新的需求得到了積極的響應(請參考SPARK-3145)。通過將Spark作爲Hive的引擎,現有的用戶擁有了更加靈活的選擇。Hive與Spark兩個社區均將從中受益。

本文闡述了Hive on Spark的總體設計思想,並詳細介紹了幾個重要的實現細節。最後總結了項目進展情況,以及最新的性能數據。希望通過本文能爲用戶更好的理解和使用Hive on Spark帶來幫助。

關於作者:李銳,2013年取得復旦大學計算機應用技術專業碩士研究生學位,現任英特爾公司軟件工程師,Hive Committer。

發佈了51 篇原創文章 · 獲贊 13 · 訪問量 29萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章