標籤:query zip 利用 tor 命中率 產生 shuff style 讀取資料
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 列儲存
傳統的關係儲存模型將一個元組的列連續儲存,即使只查詢一個列,也需要將整個元組讀取出來,可以發現,當查詢只有少量列時,效能非常低。
列儲存的思想是將元組垂直劃分為列族集合,每一個列族隔離儲存區 (Isolated Storage),列族可以退化為只僅包含一個列的平凡列族。當查詢少量列時,列儲存模型可以極大的減少磁碟 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 中的資料按Block Storage,每個Block Storage了最小值,最大值等輕量級索引,比如某個塊的最小值最大值分別是 100 和 120,這表明該塊中的任意一條資料都介於 100 到 120 之間,因此當我們執行 select column a from table where v>120 時,執行引擎可以跳過這個資料區塊,而不必將其解壓再進行資料過濾。相反,在行儲存中,必須將資料區塊完整的讀取到記憶體中,解壓,然後再進行資料過濾,導致不必要的磁碟讀取操作。
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++){ 30="" age="" val="">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++){ r="tuples.l_extprice*(1-tuple.l_discount)*(1+tuple.l_tax)" retult="" tuple="tuples(i)" val="">
近幾年,一些 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 語句中並沒有什麼複雜的計算。
SQL On Hadoop 設計的一個基本原則是:將計算任務移動到資料所在的節點而不是反過來