本文轉載自盤點SQL on Hadoop中用到的主要技術,我的以爲該文章對於諸如Impala這樣的MPP架構的SQL引擎和Runtime Framework架構的Hive/Spark SQL進行對比,感受總結的特別好,而且和本人最近的公司相近,學習轉載之。node
自hive出現以後,通過幾年的發展,SQL on Hadoop相關的系統已經百花齊放,速度愈來愈快,功能也愈來愈齊全。本文並非要去比較所謂「交互式查詢哪家強」,而是試圖梳理出一個統一的視角,來看看各家系統有哪些技術上相通之處。mysql
考慮到系統使用的普遍程度與成熟度,在具體舉例時通常會拿Hive和Impala爲例,固然在調研的過程當中也會涉及到一些其餘系統,如Spark SQL,Presto,TAJO等。而對於hawq這樣的商業產品和apache drill這樣成熟度還不是很高的開源方案就不作過多瞭解了。git
在SQL on Hadoop系統中,有兩種架構,一種是基於某個運行時框架來構建查詢引擎,典型案例是Hive;另外一種是仿照過去關係數據庫的MPP架構。前者現有運行時框架,而後套上sql層,後者則是從頭打造一個一體化的查詢引擎。有時咱們能聽到一種聲音,說後者的架構優於前者,至少在性能上。那麼是否果然如此?github
通常來講,對於SQL on Hadoop系統很重要的一個評價指標就是:快。後面提到的全部內容也大可能是爲了查詢速度更快。在Hive逐漸普及以後,就逐漸有了所謂交互式查詢的需求,由於不管是BI系統,仍是adhoc,都不能按照離線那種節奏玩。這時候不管是有實力的大公司(好比Facebook),仍是專業的供應商(好比Cloudera),都試圖去解決這個問題。短時間能夠靠商業方案或者關係數據庫去支撐一下,可是長遠的解決方案就是參考過去的MPP數據庫架構打造一個專門的系統,因而就有了Impala,Presto等等。從任務執行的角度說,這類引擎的任務執行其實跟DAG模型是相似的,當時也有Spark這個DAG模型的計算框架了,但這終究是別人家的孩子,並且往Spark上套sql又是Hive的那種玩法了。因而在Impala問世以後就強調本身「計算所有在內存中完成」,性能也是各類碾壓當時還只有MR做爲計算模型的Hive。那麼Hive所表明的「基於已有的計算模型」方式是否真的不行?算法
不能否認,按照這種方式去比較,那麼類MPP模式確實有不少優點:sql
固然MPP模式也有其劣勢,一個是擴展性不是很高,這在關係數據庫時代就已經有過結論;另外一個是容錯性差,對於Impala來講一旦運行過程當中出點問題,整個查詢就掛了。數據庫
可是,通過不斷的發展,Hive也能跑在DAG框架上了,不只有Tez,還有Spark。上面提到的一些劣勢,其實大都也能夠在計算模型中解決,只不過考慮到計算模型的通用性和自己的設計目標,不會去專門知足(因此若是從這個角度分類,Impala屬於「專用系統」,Spark則屬於「通用系統」)。在最近Cloudera作的benchmark中,雖然Impala仍然一路領先,可是基於Spark的Spark SQL徹底不遜色於Presto,基於Tez的Hive也不算不好,至少在多用戶併發模式下能超過Presto,足見MPP模式並非絕對佔上風的。因此這種架構上的區別在我看來並非制勝的關鍵,至少不是惟一的因素,真正要作到快速查詢,各個方面的細節都要有所把握。後面說的都是這些細節。express
無論是上面提到的那種架構,一個SQL on Hadoop系統通常都會有一些通用的核心組件,這些組件根據設計者的考慮放在不一樣的節點角色中,在物理上節點都按照master/worker的方式去作,若是master壓力太大,一些原本適合放在master上的組件能夠放到一個輔助master上。apache
從SQL到執行計劃,大體分爲5步。編程
下面分別舉兩個例子,直觀的認識下sql、邏輯計劃、物理計劃之間的關係,具體解釋各個operator的話會比較細碎,就不展開了。
select count(1) from status_updates where ds = '2009-08-01'
引用自美團技術團隊,其中SubPlan就是物理計劃的一個計算單元
select c1.rank, count(*) from dim.city c1 join dim.city c2 on c1.id = c2.id where c1.id > 10 group by c1.rank limit 10;
關於執行計劃的優化,雖然不必定是整個編譯流程中最難的部分,但倒是最有看點的部分,並且目前還在不斷髮展中。Spark系之因此放棄Shark另起爐竈作Spark SQL,很大一部分緣由是想本身作優化策略,避免受Hive的限制,爲此還專門獨立出優化器組件Catalyst(固然Spark SQL目前仍是很是新,其將來發展給人很多想象空間)。總之這部分工做能夠不斷的創新,優化器越智能,越傻瓜化,用戶就越能解放出來解決業務問題。
早期在Hive中只有一些簡單的規則優化,好比謂詞下推(把過濾條件儘量的放在table scan以後就完成),操做合併(連續的filter用and合併成一個operator,連續的projection也能夠合併)。後來逐漸增長了一些略複雜的規則,好比相同key的join + group by合併爲1個MR,還有star schema join。在Hive 0.12引入的相關性優化(correlation optimizer)算是規則優化的一個高峯,他可以減小數據的重複掃描,具體來講,若是查詢的兩個部分用到了相同的數據,而且各自作group by / join的時候用到了相同的key,這個時候因爲數據源和shuffle的key是同樣的,因此能夠把原來須要兩個job分別處理的地方合成一個job處理。
好比下面這個sql:
SELECT sum(l_extendedprice) / 7.0 as avg_yearly FROM (SELECT l_partkey, l_quantity, l_extendedprice FROM lineitem JOIN part ON (p_partkey=l_partkey) WHERE p_brand='Brand#35' AND p_container = 'MED PKG')touter JOIN (SELECT l_partkey as lp, 0.2 * avg(l_quantity) as lq FROM lineitem GROUP BY l_partkey) tinner ON (touter.l_partkey = tinnter.lp) WHERE touter.l_quantity < tinner.lq
這個查詢中兩次出現lineitem表,group by和兩處join用的都是l_partkey,因此原本兩個子查詢和一個join用到三個job,如今只須要用到一個job就能夠完成。
可是,基於規則的優化(RBO)不能解決全部問題。在關係數據庫中早有另外一種優化方式,也就是基於代價的優化CBO。CBO經過收集表的數據信息(好比字段的基數,數據分佈直方圖等等)來對一些問題做出解答,其中最主要的問題就是肯定多表join的順序。CBO經過搜索join順序的全部解空間(表太多的狀況下能夠用有限深度的貪婪算法),而且算出對應的代價,能夠找到最好的順序。這些都已經在關係數據庫中獲得了實踐。
目前Hive已經啓動專門的項目,也就是Apache Optiq來作這個事情,而其餘系統也沒有作的很好的CBO,因此這塊內容還有很大的進步空間。
即便有了高效的執行計劃,若是在運行過程自己效率較低,那麼再好的執行計劃也會大打折扣。這裏主要關注CPU和IO方面的執行效率。
在具體的計算執行過程當中,低效的cpu會致使系統的瓶頸落在CPU上,致使IO沒法充分利用。在一項針對Impala和Hive的對比時發現,Hive在某些簡單查詢上(TPC-H Query 1)也比Impala慢主要是由於Hive運行時徹底處於CPU bound的狀態中,磁盤IO只有20%,而Impala的IO至少在85%。
在SQL on Hadoop中出現CPU bound的主要緣由有如下幾種:
針對上面的問題,目前大多數系統中已經加入瞭如下兩個解決辦法中至少一個。
一個方法是動態代碼生成,也就是不使用解釋性的統一代碼。好比a + 2 * b這個表達式就會生成對應的執行語言的代碼,並且能夠直接用primitive type,而不是用固定的解釋性代碼。具體實現來講,JVM系的如Spark SQL,Presto能夠用反射,C++系的Impala則使用了llvm生成中間碼。對於判斷數據類型形成的分支判斷,動態代碼的效果能夠消除這些類型判斷,還能夠展開循環,能夠對比下面這段代碼,左邊是解釋性代碼,右邊是動態生成代碼。
另外一個方法是vectorization(向量化),基本思路是放棄每次處理一行的模式,改用每次處理一小批數據(好比1k行),固然前提條件是使用列存儲格式。這樣一來,這一小批連續的數據能夠放進cache裏面,cpu不只減小了branch instruction,甚至能夠用SIMD加快處理速度。具體的實現參考下面的代碼,對一個long型的字段增長一個常量。經過把數據表示成數組,過濾條件也用selVec裝進數組,造成了很緊湊的循環:
add(int vecNum, long[] result, long[] col1, int[] col2, int[] selVec) { if (selVec == null) for (int i = 0; i < vecNum; i++) result[i] = col1[i] + col2[i]; else for (int i = 0; i < vecNum; i++) { int selIdx = selVec[i]; result[selIdx] = col1[selIdx] + col2[selIdx]; } }
因爲SQL on Hadoop存儲數據都是在HDFS上,因此IO層的優化其實大多數都是HDFS的事情,各大查詢引擎則提出需求去進行推進。要作到高效IO,一方面要低延遲,屏蔽沒必要要的消耗;另外一方面要高吞吐,充分利用每一塊磁盤。目前與這方面有關的特性有:
對於分析類型的workload來講,最好的存儲格式天然是列存儲,這已經在關係數據庫時代獲得了證實。目前hadoop生態中有兩大列存儲格式,一個是由Hortonworks和Microsoft開發的ORCFile,另外一個是由Cloudera和Twitter開發的Parquet。
ORCFile顧名思義,是在RCFile的基礎之上改造的。RCFile雖然號稱列存儲,可是隻是「按列存儲」而已,將數據先劃分紅row group,而後row group內部按照列進行存儲。這其中沒有列存儲的一些關鍵特性,而這些特性在之前的列式數據庫中(好比我之前用過的Infobright)早已用到。好在ORCFile已經彌補了這些特性,包括:
ORCFile的結構以下圖,數據先按照默認256M分爲row group,也叫strip。每一個strip配一個index,存放每一個數據單元(默認10000行)的min/max值用於過濾;數據按照上面提到的編碼方式序列化成stream,而後再進行snappy或gz壓縮。footer提供讀取stream的位置信息,以及更多的統計值如sum/count等。尾部的file footer和post script提供全局信息,如每一個strip的行數,各列數據類型,壓縮參數等。
Parquet的設計原理跟ORC相似,不過它有兩個特色:
對嵌套格式作列存儲的難點在於,存儲時須要標記某個數據對應於哪個存儲結構,或者說是哪條記錄,因此須要用數據清楚的進行標記。 在Dremel中提出用definition level和repetition level來進行標記。definition level指的是,這條記錄在嵌套結構中所處於第幾層,而repetition level指的是,這條記錄相對上一條記錄,在第幾層重複。好比下圖是一個二級嵌套數組。圖中的e跟f在都屬於第二層的重複記錄(同一個level2),因此f的r值爲2,而c跟d則是不一樣的level2,但屬於同一個level1,因此d的r值爲1。對於頂層而言(新的一個嵌套結構),r值就爲0。
可是僅僅這樣還不夠。上圖說明了r值的做用,可是尚未說明d值的做用,由於按照字面解釋,d值對於每個字段都是能夠根據schema獲得的,那爲何還要從行記錄級別標記?這是由於記錄中會插入一些null值,這些null值表明着他們「能夠存在」可是由於是repeated或者是optional因此沒有值的狀況,null值是用來佔位的(或者說是「想象」出來的),因此他們的值須要單獨計算。null的d值就是說這個結構往上追溯到哪一層(不包括平級)就不是null(不是想象)了。在dremel paper中有完整的例子,例子中country的第一個null在code = en所在的結構裏面,那麼language不是null(不考慮code,他跟country平級),他就是第二層;又好比country的第二個null在url = http://B 所在的結構裏面,那麼name不是null(不考慮url,由於他跟原本就是null的language平級),因此就是第一層。
經過這種方式,就對一個樹狀的嵌套格式完成了存儲。在讀取的時候能夠經過構造一個狀態機進行遍歷。
有意思的是,雖然parquet支持嵌套格式,可是Impala尚未來得及像Hive那樣增長array,map,struct等複雜格式,固然這項功能已經被列入roadmap了,相信不久就會出現。
在最近咱們作的Impala2.0測試中,順便測試了存儲格式的影響。parquet相比sequencefile在壓縮比上達到1:5,查詢性能也相差5-10倍,足見列存儲一項就給查詢引擎帶來的提高。
對於一個MR Job,reduce task的數量一直是須要人爲估算的一個麻煩事,基於MR的Hive也只是根據數據源大小粗略的作估計,不考慮具體的Job邏輯。可是在以後的框架中考慮到了這個狀況,增長了運行時調整資源分配的功能。Tez中引入了vertex manager,能夠根據運行時收集到的數據智能的判斷reduce動做須要的task。相似的功能在TAJO中也有提到,叫progressive query optimization,並且TAJO不只能作到動態調整task數量,還能動態調整join順序。
在Hadoop已經進入2.x的時代,全部想要獲得普遍應用的SQL on Hadoop系統勢必要能與YARN進行集成。雖然這是一個有利於資源合理利用的好事,可是因爲加入了YARN這一層,卻給系統的性能帶來了必定的障礙,由於啓動AppMaster和申請container也會佔用很多時間,尤爲是前者,並且container的供應若是時斷時續,那麼會極大的影響時效性。在Tez和Impala中對這些問題給出了相應的解決辦法:
到這裏爲止,已經從上到下順了一遍各個層面用到的技術,固然SQL on Hadoop自己就至關複雜,涉及到方方面面,時間精力有限不可能一一去琢磨。好比其餘一些具備技術複雜度的功能有:
儘管如今相關係統已經不少,也通過了幾年的發展,可是目前各家系統仍然在不斷的進行完善,好比:
畢竟相比已經比較成熟的關係數據庫,分佈式環境下須要解決的問題更多,將來必定還會出現不少精彩的技術實踐,讓咱們在海量數據中更快更方便的查到想要的數據。