一篇文章掌握 Sql-On-Hadoop 核心技術

1. SQL On Hadoop 分類java

1.1 查詢延時分類node

AtScale 在 2016 年的一篇名爲 [15]The Business Intelligence for Hadoop Benchmark 的 SQL On Hadoop 性能測評報告中指出:受查詢數據量大小,查詢類型 (join 表個數,表大小,是否聚合),併發用戶量等因素影響,沒有一個 SQL On Hadoop 系統可以在全部場景下勝出。 好比 Impala 和 Presto 在併發場景下性能比較優越,Spark SQL 大表 Join 性能比較好。然而對於全部 SQL On Hadoop 而言,大表 Join 都比較慢。算法

在衆多的 SQL On Hadoop 系統中,有必要對其進行一個分類。通常而言,用戶更關心的是查詢時延,根據用戶提交查詢到結果返回的時間長短,將 SQL 查詢分爲以下三類:batch SQL,interactive SQL,operation SQL, 如圖 1。sql

圖 1 SQL On Hadoop 分類, 摘自文獻 [14]數據庫

  • Batch SQL,Batch SQL 的查詢時間一般在分鐘,小時級別,通常用於複雜的 ETL 處理,數據挖掘,高級分析。因爲 Batch SQL 的查詢延時比較高,所以支持查詢內 (Intra-query) 容錯是該類系統必須具有的屬性,查詢內容錯是指,當節點宕機或者查詢內部某個 Task 失敗時,系統必須可以從新提交該 task 而不是從新提交整個查詢來進行容錯。Batch SQL 中最典型的系統是 Hive。Spark SQL 也能夠歸類到該系統。數組

  • Interactive SQL,Interactive SQL 也叫作交互式 SQL 查詢,用戶一般在同一個表上反覆的執行不一樣的查詢,Interactive SQL 的查詢時間一般在毫秒級或者秒級之內,通常不超過度鍾級別。因爲該類系統主要追求低延遲,而不過度強調查詢內部容錯,因此當某個 task 失敗時,能夠從新提交該查詢以便進行容錯,由於從新提交一個 SQL 查詢的執行時間一般很短。Interactive SQL 在實現上一般採用 MPP 架構,而且將熱點數據緩存到內存中,好比 Presto,Impala,Drill,HAWQ。鑑於 Spark SQL 也具備很是高效的查詢速度,Spark SQL 也能夠歸類到 Interactive SQL 中。緩存

  • Operation SQL, 一般是單點查詢,延時要求小於 1 秒,該類系統主要是 HBase。性能優化

1.2 架構分類網絡

1.2.1 MPP 架構數據結構

MPP 架構的優勢是查詢速度快,一般在秒計甚至毫秒級之內就能夠返回查詢結果,這也是爲什麼不少強調低延遲的系統採用 MPP 架構的緣由。

下面重點看下 MPP 架構的缺點,MPP 架構最主要的缺點是不支持細粒度的容錯,集羣節點數量很難擴展到 100 個以上,若是集羣出現落後節點,那麼將影響整個系統的查詢性能,此外無論 MPP 節點數量的多少,併發查詢的數量一般只能達到 20 個左右。

容錯,MPP 架構的容錯特色是粗粒度容錯,不能處理落後節點 (Straggler node)。粗粒度容錯是指,某個 task 執行失敗將致使整個查詢失敗,而後系統從新提交整個查詢來獲取結果。這種容錯方式只適用於 Iterative SQL 這種低延遲的工做負載,而不適合 Batch SQL 場景,由於 Batch SQL 查詢時間一般在分鐘小時級別,從新提價一個查詢代價過高。

落後節點,當一個節點執行速度慢於其餘節點時,將致使整個系統的查詢性能降低。

擴展性:受落後節點的影響,MPP 架構很難擴展到 100 個節點以上。若是某個節點慢於其餘節點,那麼整個系統的查詢性能將受限於這個最慢的節點,而與集羣節點數量無關。須要注意的是,在大型集羣中落後節點是廣泛存在的,隨着集羣節點數量的增長,落後節點出現的機率也增長,[13] 針對磁盤故障機率的統計以下:

若是集羣包含 1000 個未使用一年的磁盤,那麼每一年將有大約 20 磁盤出現故障,平均每兩週就會出現一個故障。當磁盤使用超過一年後,每一年磁盤故障出現的機率將達到 8% 左右,平均每週將出現大約兩次故障。因爲這個緣由,MPP 架構很難擴展到 100 個節點以上,通常在 50 個節點左右。

併發,MPP 架構的併發查詢數量和集羣節點數量無關。MPP 是對稱結構,當執行一個查詢時,該查詢將被調度到集羣中的每個節點執行,這意味着一個包含 4 個節點的 MPP 集羣和一個包含 400 個節點的 MPP 集羣所支持的併發查詢數量是相同的,也就是說,併發查詢數量和集羣節點數量無關,通常而言,當併發查詢個數達到 20 左右時,整個系統的吞吐已經達到滿負荷狀態。

綜上所述,MPP 架構不適合大規模部署,若是須要大規模部署,能夠考慮 Spark Sql 這樣的系統。

1.2.2 非 MPP 架構

典型的非 MPP 架構有 Hive,Spark Sql。他們分別構建在 MR 和 Spark 之上,優勢是集羣節點數量能夠擴展到幾百甚至上千個,支持細粒度容錯。缺點是查詢速度可能不如 MPP 架構。

2. 運行引擎的設計

2.1. 優化器

目前 SQL On Hadoop 的查詢優化器主要有兩種:基於規則的 (Rule-Based Optimizer) 和基於代價的 (Cost-Based Optimizer CBO)。基於規則的優化器簡單,易於實現,經過內置的一組規則來決定如何執行查詢計劃,這裏不作介紹。

設計一個好的 CBO 優化器很是具備挑戰性,一個好的 CBO 依賴於詳細可靠的統計信息,好比每一個列的最大值,最小值,表大小,表分區信息,桶信息,然而在 SQL On Hadoop 中,一般缺少可靠的統計結果,代價估計代數,這使得在 SQL On Hadoop 中引入 CBO 很困難。儘管如此,鑑於 CBO 在運行能夠更加智能的進行查詢優化,仍然有愈來愈多的 SQL On Hadoop 開始支持 CBO,好比 Hive,Spark SQL(計劃中)。

CBO 主要用來優化 shuffle,join,如何儘量的避免 shuffle,提升 join 執行速度是 CBO 主要關注的問題,其中 Join 的實現方式和 Join 順序是重點考慮的。在 SQL On Hadoop 主要有四種 join 實現方式:shuffle hash join,broadcast join,Bucket join,cartesian join:

  • shuffle hash join,在 map 階段按照 join key 對兩個表執行 hash shuffle,這樣擁有相同 join key 的元組將 shuffle 到同一個節點,在 reduce 階段對錶進行 join。

  • broadcast join,當一個大表 join 一個小表時,而且小表能夠徹底放到內存中,此時能夠將小表廣播到大表所在的每個計算節點,而後執行 join。這種 join 方式叫作 broadcast join 或者 map join。Broadcast join 優勢是避免了 shuffle,提升 join 性能。

  • Bucket join, 假設表 A 和表 B 使用 bucket 分區策略存儲,而且表 A 和表 B 的 bucket 個數爲 n,此時能夠按照以下方式 join:bucket 1 of A join bucet 1 of B,......,bucket n of A join bucket n of B。

    Bucket join 優勢是能夠對兩個大表執行 join,而且不須要將數據放到內存中,在 Hive 和 Spark2.0 中都支持 Bucket join。

  • cartesian join,也叫作笛卡兒積 join,對兩個表執行笛卡兒積 join,結果集中元素的數量是兩個表大小的乘積。好比表 A 有 10 萬行,表 B 有 10 萬行,那麼笛卡兒積 join 以後的表大小將達到 100 萬條數據。所以除非到萬不得已,不然不會使用笛卡兒積 join。

表的 join 順序 (Join order) 主要有兩種:left-deep tree(下圖左),bushy tree(下圖右)。一個好的 CBO 應該可以根據 SQL 語句的特色,來自動選擇使用 Left-deep tree 仍是 bushy tree 執行 join。

  • Left-deep tree, 若是對 A,B,C,D 執行 join,那麼首先 A join B 獲得一個臨時表 AB 並 AB 物化到磁盤,而後 AB join C 獲得中間臨時表 ABC 並物化到磁盤,最後 ABC joinD 獲得最終結果。能夠發現,這種 join 順序很是簡單,缺點是隻能串行 join,而且因爲產生了大量的中間臨時表,所以不太適合 OLAP 中的星型和雪花模型。

  • bushy tree, 採用 bushy tree 方式,能夠並行執行 A join B 和 C joinD。而後將兩者的結果 AB 和 CD 進行 join 獲得最終結果。Bushy tree 優勢是能夠並行 join,而且可以很好的處理星型模型和雪花模型。

圖 2left-deep tree 和 bushy tree, 摘自文獻 [16]

2.2. 查詢執行引擎

查詢執行引擎 (query execution engine) 是 SQL On Hadoop 的核心組件。查詢執行引擎的好壞對查詢性能的影響很是大。目前主要有兩種查詢執行:火山執行模型和向量化執行引擎。在後面的向量化執行引擎章節中有詳細的介紹。

3. 性能優化

從硬件資源角度將性能優化分爲 3 個部分:

  • 磁盤優化:數據本地化,減小中間結果的物化,數據壓縮,列存儲文件,分區,塊級索引

  • CPU 優化:向量化執行引擎,動態代碼生成,輕量級壓縮算法,任務啓動優化

  • 內存和 CPU 緩存:內存壓縮列存儲,堆外存儲,緩存敏感數據結構和算法

3.1 數據本地化

SQL On Hadoop 設計的一個基本原則是:將計算任務移動到數據所在的節點而不是反過來。這主要出於網絡優化的目的,由於數據分佈在不一樣的節點,若是移動數據那麼將會產生大量的低效的網絡數據傳輸。數據本地化通常分爲三種:節點局部性 (Node Locality), 機架局部性 (Rack Locality) 和全局局部性 (Global Locality)。節點局部性是指將計算任務分配到數據所在的節點上,此時無需任何數據傳輸,效率最佳。機架局部性是指將計算任務移動到數據所在的機架,雖然計算任務和數據分屬不一樣的計算節點,可是由於機架內部網絡傳輸速度明顯高於機架間網絡傳輸,因此機架局部性也是一種不錯的方式。其餘的狀況屬於全局局部性,此時須要跨機架進行網絡傳輸,會產生很是大的網絡傳輸開銷。

調度系統在進行任務調度時,應該儘量的保證節點局部性,而後是機架局部性,若是以上二者都不能知足,調度系統也會經過網絡傳輸將數據移動到計算任務所在的節點,雖然性能相對低效,但也比資源空置比較好。

爲了實現數據本地化調度,調度系統會結合延遲調度算法來進行任務調度。核心思想是優先將計算任務調度到數據所在的節點 i,若是節點 i 沒有足夠的計算資源,那麼等待幾秒鐘後若是節點 i 依然沒有計算資源可用,那麼就放棄數據本地化將該計算任務調度到其餘計算節點。

3.2 減小中間結果的物化

在一個追求低延遲的 SQL On Hadoop 系統中,儘量的減小中間結果的磁盤物化能夠極大的提升查詢性能。 以下圖,Hive 執行引擎採用 pull 獲取數據,其優勢是能夠進行細粒度的容錯,缺點是下游的 MapReduce 必須等待上游 MapReduce 徹底將數據寫入到磁盤後才能開始 pull 數據。Presto 採用 push 方式獲取數據,數據徹底以流的方式在不一樣 stage 之間進行傳輸,中間結果不須要物化到磁盤,從而使得 presto 具備很是高效的執行速度,缺點是不能支持細粒度的容錯。

圖 3push 和 pull

3.3 列存儲

傳統的關係存儲模型將一個元組的列連續存儲,即便只查詢一個列,也須要將整個元組讀取出來,能夠發現,當查詢只有少許列時,性能很是低。

列存儲的思想是將元組垂直劃分爲列族集合,每個列族獨立存儲,列族能夠退化爲只僅包含一個列的平凡列族。當查詢少許列時,列存儲模型能夠極大的減小磁盤 IO 操做,提升查詢性能。當查詢的列跨越多個列族時,須要將存儲在不一樣列族中列數據拼接成原始數據,因爲不一樣列族存儲在不一樣的 HDFS 節點上,致使大量的數據跨越網絡傳輸,從而下降查詢性能。所以在實際使用列族時,一般根據業務查詢特色,將頻繁訪問的列放在一個列族中。

在傳統的數據庫領域中,人們已經對列存儲進行了很是深入的研究,而且不少研究成果已經被應用到工業領域,其中包括輕量級壓縮算法,直接操做壓縮數據,延遲物化,向量化執行引擎。但是縱觀目前 SQL On Hadoop 系統,這些技術的應用仍然遠遠的落後於傳統數據庫,在最近的一些 SQL On Hadoop 中已經添加了向量化執行引擎,輕量級壓縮算法,可是諸如直接操做壓縮數據,延遲解壓等技術尚未被應用到 SQL on Hadop 系統。關於列存儲的更多內容能夠參見 [20]。

列存儲壓縮

列存儲壓縮算法具備以下特色:

壓縮比列存儲模型具備很是高的壓縮比,一般能夠達到 10:1,而行存儲壓縮比一般只有 4:1。如圖 4:

圖 4 重量級壓縮算法

輕量級壓縮算法 (Leight-Weight Compression)輕量級壓縮算法是 CPU 友好的。行存儲模型只能使用 zip,lzo,snappy 等重量級壓縮算法,這些算法最大的缺點是壓縮和解壓縮速度比較慢,一般每秒只能解壓至多幾百兆數據。相反,列存儲模型不只可使用重量級壓縮算法,還可使用一些很是輕量級的壓縮算法,好比 Run-length encode,Bit Vector。輕量級壓縮算法不只具備較好的壓縮比,並且還具備很是高的壓縮和解壓速度。目前在 ORC File 和 Parquet 存儲中,已經支持 Bit packing,Run-length enode,Dictionary encode 等輕量級壓縮算法。

直接操做壓縮數據 (Operating Directly on Compressed Data)當使用輕量級壓縮算法時,可能無需解壓便可直接獲取計算結果。例如:Run Length Encode 算法將連續重複的字符壓縮爲字符個數和字符,好比 aaaaaabbccccaaaa 將被壓縮爲 6a2b4c4a,其中 6a 表示有連續 6 個字符 a。如今假設一個某列包含上述壓縮的字符串,當執行 select count(*) from table where columnA=’a’時,不須要解壓 6a2b4c4a,就可以知道 a 的個數是 10。

須要注意的是,因爲行存儲只能使用重量級壓縮算法,因此直接操做壓縮數據不能被應用到行存儲。

延遲解壓parquet 中的數據按塊存儲,每一個塊存儲了最小值,最大值等輕量級索引,好比某個塊的最小值最大值分別是 100 和 120,這代表該塊中的任意一條數據都介於 100 到 120 之間,所以當咱們執行 select column a from table where v>120 時,執行引擎能夠跳過這個數據塊,而沒必要將其解壓再進行數據過濾。相反,在行存儲中,必須將數據塊完整的讀取到內存中,解壓,而後再進行數據過濾,致使沒必要要的磁盤讀取操做。

3.4 塊級索引

傳統數據庫使用索引來優化查詢性能,然而受限於 HDFS block 的放置策略,使用索引來優化 SQL On Hadoop 不是一件容易的事情。目前大部分 SQL On Hadoop 系統都不支持全局索引,取而代之使用的是塊級索引,好比 Hive Index,ORC File,Parquet。塊級索引的思想是在每個數據塊中添加一些諸如最大值,最小值的輕量級索引,當 SQL 引擎掃描 HDFS 文件時,能夠跳過不符合條件的 Block,從而減小磁盤 IO 提升查詢性能。以下圖,在 ORC File 中,每個 Stripe 都包含一個 Index Data,Index Data 中存儲了列的最大值,最小值。當執行引擎執行 filter 這種查詢時,只須要讀取 Index Data 就行,若是符合條件就讀取 Row Data,不然能夠直接跳過 Row Data 的讀取,從而減小磁盤 IO,提升查詢性能。

圖 3-3 ORC Storage

最大值,最小值這樣的統計索引主要用於優化範圍查詢性能,對於單點查詢一般可使用布隆過濾器做爲索引,布隆過濾器能夠在數據量很是大的狀況下快速的查詢數據。

3.5 分區

MPP 數據庫根據分區策略將一個表水平或者垂直切分爲一個子表集合,不一樣的子表存儲在不一樣的節點,這樣能夠並行的處理不一樣的子表。典型的分區策略有哈希,範圍。

SQL On Hadoop 中也存在表分區的概念,一個表分區存儲在一個 HDFS 文件目錄下,文件目錄以列名 = 列值方式存儲。好比咱們在 Hive 中執行以下 SQL:

CREATE TABLE test_table(id string,name int) PARTITION BY(ds string)。

當向 test_table 中插入以下元組時:

(id=‘10010’,name=‘sql on hadoop’,ds=‘2017-05-31’)(id=‘10010’,name=‘sql on hadoop’,ds=‘2017-05-32’)

HDFS 中將建立以下目錄:

/user/hive/warehouse/test_table/ds=2017-05-31/user/hive/warehouse/test_table/ds=2017-05-32

當執行 SELECT * FROM test_table WHERE ds=’2017-05-31’時,只須要掃描 ds=2017-05-31目錄便可,這樣能夠跳過大量無關數據的掃描,從而加快數據查詢速度。

目前大部分 SQL On Hadoop 都支持分區功能,好比 Hive,Presto,Impala,Spark SQL。

3.6 壓縮

通常狀況下,壓縮 HDFS 中的文件能夠極大的提升查詢性能。壓縮可以減小數據所佔用的存儲空間,減小磁盤 IO 的讀寫,提升數據處理速度,此外,壓縮還可以減小網絡傳輸量,提升網絡傳輸速度。在 SQL On Hadoop 中,壓縮主要應用在 HDFS 中的數據源,shuffle 數據,最終計算結果。

若是應用程序是 io-bound 的,那麼壓縮數據能夠提升數據處理速度,由於壓縮後的數據變小了,因此能夠增長數據讀寫速度。須要主要的是,壓縮算法並非壓縮比越高越好,壓縮率越高的算法壓縮和解壓縮速度就越慢,用戶須要在 cpu 和 io 之間取得一個良好的平衡。例如 gzip2 擁有很是高的壓縮比,可是其壓縮和解壓縮速度卻很是慢,甚至可能超過數據未壓縮時的讀寫時間,所以沒有 SQL On Hadooop 系統使用 gzip2 算法,目前在 SQL On Hadoop 系統中比較流行的壓縮算法主要有:Snappy,Lzo,Glib。

若是應用程序是 cpu-bound 的,那麼選擇一個能夠 splittable 的壓縮算法是很重要的,若是一個文件是 splittabe 的,那麼這個文件能夠被切分爲多個能夠並行讀取的數據塊,這樣 MR 或者 Spark 在讀取文件時,會爲每個數據塊分配一個 task 來讀取數據,從而提升數據查詢速度。

3.7 向量化執行引擎

查詢執行引擎 (query execution engine) 是數據庫中的一個核心組件,用於將查詢計劃轉換爲物理計劃,並對其求值返回結果。查詢執行引擎對數據庫系統性能影響很大,目前主要的執行引擎有以下四類:Volcano-style,Block-oriented processing,Column-at-a-time,Vectored iterator model。下面分別介紹這四種執行引擎。

Volcano-style, 最先的查詢執行引擎是 Volcano-style execution engine(火山執行引擎,火山模型),也叫作迭代模型 (iterator model),或者 one-tuple-at-a-time。在這種模型中,查詢計劃是一個由 operator 組成的 tree 或者 DAG,其中每個 operator 包含三個函數:open,next,close。Open 用於申請資源,好比分配內存,打開文件,close 用於釋放資源,next 方法遞歸的調用子 operator 的 next 方法生成一個元組。圖 1 描述了 select id,name,age from people where age >30 的火山模型的查詢計劃,該查詢計劃包含 User,Project,Select,Scan 四個 operator,每一個 operator 的 next 方法遞歸調用子節點的 next,一直遞歸調用到葉子節點 Scan operato,Scan Operator 的 next 從文件中返回一個元組。

圖 3-4 火山模型 摘自文獻 [2,page 39]

火山模型的主要缺點是昂貴的解釋開銷 (interpretation overhead) 和低下的 CPU Cache 命中率。首先,火山模型的 next 方法一般實現爲一個虛函數,在編譯器中,虛函數調用須要查找虛函數表, 而且虛函數調用是一個非直接跳轉 (indirect jump), 會致使一次錯誤的 CPU 分支預測 (brance misprediction), 一次錯誤的分支預測須要十幾個週期的開銷。火山模型爲了返回一個元組,須要調用屢次 next 方法,致使昂貴的函數調用開銷。[] 研究代表,在採用火山執行模型的 MySQL 中執行 TPC-H Q1 查詢,僅有 10% 的時間用於真正的查詢計算,其他的 90% 時間都浪費在解釋開銷 (interpretation overhead)。其次,next 方法一次只返回一個元組,元組一般採用行存儲,如圖 3-5 Row Format,若是順序訪問第一列 1,2,3,那麼每次訪問都將致使 CPU Cache 命中失敗 (假設該行不能徹底放入 CPU Cache 中)。若是採用 Column Format,那麼只有在訪問第一個值時纔出現緩存命中失敗,後續訪問 2 和 3 時都將緩存命中成功, 從而極大的提升查詢性能。

圖 3-6 行存儲和列存儲

Block-oriented processing,Block-oriented processing 模型是對火山模型的一個改進,該模型一次 next 調用返回一批元組, 元組個數在 100-1000 不等,next 內部使用一個循環來處理這批元組。在圖 1 的火山模型中,Select operator next 方法能夠以下實現:

def next():Array[Tuple]={ // 調用子節點的 next 方法,返回一個元組向量,該向量包含 1024 個元組 val tuples=child.next() val result=new ArrayBuffer[Tuple] for(i=0;i<tuples.length;i++){ val age=tuples(i).age // 篩選年齡大於 30 的人 If(age>30) result.append(tuples(i)) } result// 返回結果}

Block-oriented processing 模型的優勢是一次 next 返回多個元組,減小了解釋開銷,同時也被證實增長了 CPU Cache 的命中率,當 CPU 訪問元組中的某個列時會將該元組加載到 CPU Cache(若是該元組大小小於 CPU Cache 緩存行的大小), 訪問後繼的列將直接從 CPU Cache 中獲取,從而具備較高的 CPU Cache 命中率,然而若是之訪問一個列或者少數幾個列時 CPU 命中率仍然不理想。該模型最大的一個缺點是不能充分利用現代編譯器技術,好比在上面的循環中,很難使用 SIMD 指令處理數據。

Column-at-a-time 模型,向量化執行的最先歷史能夠追朔到 MonetDB[], 在 MonetDB 提出了一個叫作 Column-at-a-time 的查詢執行模型,該模型中每一次 next 調用返回一個或者多個列,每一個列以數組形式返回。該模型優勢是具備很是高的查詢效率,缺點是一個列數據須要被物化到內存甚至磁盤,致使很高的內存佔用和 io 開銷,同時數據不能放到 CPU Cache 中,致使較低的 CPU Cache 命中率。

Vectored iterator model,VectorWise提出了 Vectored iterator model 模型,該模型是對 Column-at-a-time 的改進,next 調用不是返回完整的一個列,而是返回一個能夠放到 CPU Cache 的向量。該模型避免了 Column-at-a-tim CPU Cache 命中率低的缺點。Vectored iterator model 最大的優勢是可使用運行時編譯器 (JIT) 動態的生成更適合現代處理器的指令,好比 JIT 能夠生成 SIMD 指令來處理向量。考慮 TPC-H Q1 查詢:SELECT l_extprice*(1-l_discount)*(1+l_tax) FROM lineitem。該 SQL 查詢的執行計劃以下:

其中 Project operator 的 next 方法能夠以下實現 (scala 僞代碼):

def next():Array[Tuple]={ val tuples=child.next() var result=new ArrayBuffer[Int] for(i=0;i<tuples.length;i++){ val tuple=tuples(i) val r=tuples.l_extprice*(1-tuple.l_discount)*(1+tuple.l_tax) result.append(r) } retult}

近幾年,一些 SQL On Hadoop 系統引入了向量化執行引擎,好比 Hive,Impala,Presto,Spark 等,儘管其實現細節不一樣,但核心思想是一致的:儘量的在一次 next 方法調用返回多條數據,而後使用動態代碼生成技術來優化循環,表達式計算從而減小解釋開銷,提升 CPU Cache 命中率,減小分支預測。

Impala 中的向量化執行引擎本質上屬於 Block-oriented processing,imapla 的每次 next 調用返回一批元組,這種模型仍然具備較低的 CPU Cache 命中率,同時也很難使用 SIMD 等指令進行優化,爲了緩解這個問題,Impala 使用動態代碼生成技術,對於大循環,表達式計算等進行使用動態代碼生成來進行優化。

在 Spark2.0 中,實現了基於 Parquet 的向量化執行引擎 [12],該執行引擎屬於 Vectored iterator model,引擎在調用 next 方法時以列存儲格式返回一批元組,可使用循環來處理該批元組。此外爲了更充分的利用現代 CPU 特性,Spark 還支持整階段代碼生成技術,核心思想是將多個 operator 編譯到一個方法中,從而減小解釋開銷。

3.8 動態代碼生成

動態代碼生成通常和向量化執行引擎結合使用,由於向量執行引擎的 next 方法內部可使用 for 循環來處理元組向量或者列向量,使用動態代碼生成技術能夠在運行時對 next 方法生成更高效的執行代碼。研究證實向量化執行引擎和動態代碼生成能夠減小解釋開銷 (interpretation overhead), 見文獻 [18],主要影響如下三個方面:

  • Select, 當 select 語句中包含複雜的表達式計算時,好比 avg,sum,count,select 的計算性能主要受 CPU Cache 和 SIMD 指令影響。當數據不能放到 CPU Cache 時,CPU 大部分時間都在等待數據從內存加載到 CPU Cache,所以當 CPU 執行計算所需的數據在 CPU Cache 中時能夠極大的提升計算性能。一條 SIMD 指令能夠同時計算多個數據,所以使用 SIMD 指令執行表達式計算能夠提升計算性能。

  • where,與 Select 語句不一樣的是 Where 語句通常不須要複雜的計算,影響 where 性能更多的是分支預測。若是 CPU 分支預測錯誤,那麼以前的 CPU 流水線將全被清洗,一次 CPU 分支預測錯誤可能至少浪費十幾個指令週期的開銷。經過使用動態代碼生成技術,JIT 編譯器可以自動的生成分支預測友好的指令。

  • Hash,hash 算法影響 equal-join,group 的查詢性能,hash 算法的 CPU Cache 命中率很低。[18] 描述了一種緩存友好的 hash 算法,能夠顯著的提升 hash 計算性能。

動態代碼生成有兩種:C++ 系和 java 系。其中 C++ 系能夠直接生成本機可執行二進制代碼,而且可以生成高效的 SIMD 指令,例如 Impala 使用 C++ 實現查詢執行引擎,同時使用 LLVM 編譯器動態的生成本機可執行二進制代碼,LLVM 能夠生成 SIMD 指令對錶達式執行計算。Java 系利用反射機制動態的生成 java 字節碼,通常而言,不能充分利用 SIMD 指令進行優化,Spark 使用反射機制動態的生成 java 字節碼,一般很難直接利用 SIMD 進行表達式優化。此外在 Spark2.0 中所提供的整階段代碼生成 (Whole-Stage Code Generation) 技術也是動態代碼生成技術將多個 Operator 編譯成一個方法進行優化。

須要注意的是,動態代碼生成技術並不老是萬能藥,在下圖中,impala 的動態代碼生成技術並無提升 TPC-DS Q42,Q52,Q55 的查詢速度,主要緣由這些 SQL 語句的 SELECT 語句中並無什麼複雜的計算。

3.9 堆外存儲

使用 JVM 實現的查詢執行引擎依賴於 GC 回收內存,每一次 Full GC 會暫停全部工做線程,一次 GC 一般在分鐘級別以上,致使全部 SQL 查詢計算中止,從而嚴重的影響查詢性能而且可能會致使一些很是奇怪的異常出現,好比網絡超時,shuffle 獲取數據數據失敗。爲了減小 GC 對程序性能的影響,許多 SQL On Hadoop 使用堆外存儲 (off heap) 來存儲數據。

堆外存儲所需的內存由操做系統管理而不是 Java GC,java.nio 提供了一些用於讀寫堆外存儲的類,能夠在堆外存儲中存儲 Int,Double 這種基元類型,也能夠存儲 map,struct 這種複合對象,當存儲複合對象時須要將複合對象序列化存儲到堆外存儲,在讀取時也需進行反序列化。由於序列化 / 反序列化會消耗大量的 CPU 計算,所以在使用堆外存儲時須要在 GC 和 cpu 之間進行一個合理的平衡。

3.10 內存壓縮列存儲

在內存中緩存熱點數據是提升查詢性能的一個基本優化手段。在內存中緩存熱點數據須要考慮至少考慮三個問題: 第一,如何減小數據的內存佔用,第二,如何提升 CPU Cache 命中率,第三,若是使用 JVM 系統,還要考慮如何減小 GC 次數和 GC 時間。這裏須要重點關注的是如何提升 CPU Cache 的命中率。

這三個問題能夠經過使用內存壓縮列存儲來解決:

  • 減小內存佔用,在內存列存儲中,若是列元素類型是基元類型 (Int,Double,Long 等),那麼每個列存儲爲一個數組,若是列元素是 Map,Struct 這種負責對象,能夠將其序列化爲一個字節數據進行存儲。數組能夠被壓縮存儲,須要注意的是,在選擇壓縮算法時,通常不會選擇重量級壓縮算法,雖然重量級壓縮算法具備較高的壓縮率,可是它在壓縮和解壓縮時很是慢,這將嚴重的影響查詢性能。在內存壓縮列存儲中,輕量級壓縮算法具備更高執行效率,這是由於輕量級壓縮算法在進行壓縮和解壓時幾乎不須要太多的 CPU 計算。在 Spark SQL 的內存壓縮列存儲中 [10],使用的就是 Run length encode,dictionary encode 等輕量級壓縮算法。

  • 提升 CPU Cache 命中率,內存列存儲具備較好的 CPU Cache 命中率,由於列數據連續存儲,因此當 CPU 訪問數組中某個元素時能夠將該元素臨近的數據一塊兒加載到 CPU Cache 緩存行中,這樣 CPU 訪問該元素的下一個元素時就不須要訪問內存了,從而提升 CPU Cache 命中率,提升查詢計算性能。

  • 減小 GC 時間,最後,內存列存儲對於 JVM 系統也是友好的。首先,JVM 中每一個對象都包含一個對象頭,這個對象頭的開銷一般須要 12 個字節,若是咱們將 Int 按行存儲,那麼每一個 Int 都將至少浪費 12 個字節的存儲空間佔用。相反,若是將 Int 存儲爲一個數組,那麼每一個 Int 只須要 4 個字節,能夠減小 3 倍的存儲空間佔用。內存列存儲還能夠減小 GC 時間,GC 時間主要和對象數量呈正相關,經過採用內存列存儲,每一個列做爲一個數組對象存儲,能夠極大的減小對象數量,減小 GC 時間。

3.11 緩存敏感算法

自從 CPU Cache 出現以來,人們對於緩存敏感算法的研究就從未中止。所謂的緩存敏感算法,就是編寫 CPU Cache 命中率高的算法。在這個領域已經有了大量的研究,好比磁盤索引 B-tree 的緩存敏感實現,內存索引 T-tree 的緩存敏感實現,鏈表,哈希表等等。

緩存敏感算法一般比較複雜,而且不易理解,所以將全部算法都設計成緩存敏感的是不明智的,事實上大部分 SQL 計算主要爲排序,聚合,join,只需對這些算法進行優化便可。在 Spark SQL 實現了緩存敏感的 Sort 算法,該算法應用在基於 sort 的 shuffle,排序和 join,優化後的 Sort 性能至少提升了 3 倍。

4. 其餘

目前在 SQL On Hadoop 領域中存在種類繁雜的開源軟件,儘管其具體的實現細節和應用場景不一樣,可是仍然有一些共同的技術被普遍採用:列存儲,向量化執行引擎,緩存熱點數據,內存壓縮列存儲等。

因爲設計決策,架構的不一樣,不一樣 SQL On Hadoop 仍然有許多不一樣的地方:

  • 統一資源管理,一個支持統一資源調度的 SQL On Hadoop 系統很是具備研究價值,由於在一個大型複雜的分佈式集羣中,不可能只有一種計算框架擁有數據,更多的是多種工做負載不一樣的計算框架同時部署在同一集羣,好比 Spark,MR,Hive,SparkSql,Impala,爲了不不一樣計算框架之間的資源競爭,須要使用統一的資源調度框架進行資源管理,使用統一資源管理能夠避免計算框架申請過多的資源致使集羣,操做系統等出現不穩定狀態,Yarn 和 Mesos 是兩個最流行的開源資源管理框架。Impala,SparkSql 等都支持 Yarn 進行統一資源調度,presto 目前不支持 yarn。

  • 容錯粒度,Impala,Presto,drill 這些採用 MPP 架構的系統不支持細粒度的容錯。Spark Sql,Hive 這些系統經過借鑑底層系統 MR 和 Spark 的容錯機制,也實現了細粒度的容錯。

  • JVM, 大部分 SQL On Hadoop 都採用 JVM 語言來實現,部分系統採用非 Jvm,好比 Impala 使用 C++ 實現查詢執行引擎。

最後,全部的 SQL On Hadoop 都應該儘量的追求快速,易使用。查詢速度越快,就越能適應更多的場景。支持 ANSI SQL 而不是其餘方言能夠減小用戶學習曲線,避免用戶陷入到過多的語言特性中。

相關文章
相關標籤/搜索