提速 Spark SQL 2 倍,GLUTEN 向量化引擎原理剖析
來源:DataFunTalk
導讀 本文主題為 Gluten 向量化引擎,提速 Spark 兩倍效能。
內容包括以下三部分:
1. Why and What is Gluten?
2. Gluten 實現原理
3. 當前進展和後續工作
分享嘉賓|張智超 Kyligence 高階大資料架構師
編輯整理|張龍春 HW
出品社群|DataFun
Why and What is Gluten?
首先介紹一下為什麼發起 Gluten 這一專案,以及 Gluten 是一個什麼型別的專案。
在介紹 Gluten 之前,先探討一下當前 Spark 在計算效能上遇到的問題。
上圖是 Spark 各個版本在 TPCH 下的基礎運算元效能圖。這裡抽取的是三個主要的基礎運算元,HashAgg、HashJoin 和 TableScan。從 Spark 的 1.6 版本以來,逐步引入了諸如鎢絲計劃、向量化 Parquet Reader 等一系列最佳化之後,整體的計算效能有兩倍左右的提升。但同時我們也發現,在 3.0 版本以後,整體計算效能的提升有所減緩。那麼到底是什麼成為了 Spark 計算效能提升的瓶頸呢?這裡我們做了一些調研。
上面這張圖是 Spark 跑 TPCH 過程當中的效能監控指標。從圖上可以看到記憶體的使用率、網路卡 IO、磁碟 IO 等,這些指標時高時低,可以說明並不是瓶頸所在。最明顯的就是 CPU 利用率,在整個執行過程中,CPU 利用率基本上都高達 80%~90%。顯而易見 CPU 是 Spark 計算效能所面臨的較大瓶頸。
Spark 是基於 JVM 的,而 JVM 只能利用到一些比較基礎的 CPU 指令集。雖然有 JIT 的加持,但相比目前市面上很多的 Native 向量化計算引擎而言,效能還是有較大差距。因此考慮如何將具有高效能運算能力的 Native 向量引擎引用到 Spark 裡來,從而提升 Spark 的計算效能,突破 CPU 瓶頸。接下來從兩個方面來考慮如何整合向量化引擎。
一方面,Spark 經過多年發展,作為基礎的計算框架,不管是在穩定性還是可擴充套件性方面,以及生態建設都得到了業界廣泛認可。所以不考慮去改動基礎框架,而是保留它原有的架構,使用 Native 向量化計算引擎替換掉 Spark 原有基於 JVM 的 Task 計算模型,就可以把高效能運算能力帶給 Spark,突破 CPU 的瓶頸問題。
另一方面就是近幾年來 Native SQL 向量化計算引擎層出不窮,出現多種優秀框架。這些框架大致可以分為兩類,一類以產品方式釋出,比如 ClickHouse、DuckDB、MongoDB。ClickHouse 使用很廣泛,DuckDB 是單機下類似 SQLite 的向量化計算引擎。第二類是以 Library 方式釋出,比如 Meta 公司釋出的 Velox,其主旨也是想利用 Library 方式替換掉 Presto 基於 JVM 的計算模型,進而提升 Presto 計算能力。還有其他的,如 Arrow Computer Engine 也是以 Library 方式進行釋出。
以上提到的幾種 Native 向量化計算引擎,它們都有一些共同特點,比如都是使用 C++ 開發,就很容易利用 CPU 原生指令集的最佳化。另外它們都是基於列式資料格式。結合這兩點,這些引擎就很容易可以去做向量化處理,進而達到高效能運算。基於這兩點, Gluten 專案也就應運而生。它是一個基於 Spark 的向量化引擎中介軟體。會把 Spark SQL 整個執行過程當中的計算轉移到向量化引擎去執行,來獲得指令集的原生加速。
如上圖展示,整個框架仍然使用 Spark 原有的 Master/Worker 方式去執行。因為原生 Spark 是在 Task 上做具體的計算,所以這裡做了一些改動。在執行 Pipeline 的時候,會先做一個選擇,如果 Pipeline 裡的 Operator 或 Expression 是 Native 引擎支援的情況,就會交由 Gluten,然後透過 JNI 介面去呼叫 Native 向量化引擎做計算,從而提升效能。如果存在未支援的 Operator 或者 Expression 的情況,就會做 fallback,讓它回到 Spark 原生的 JVM 引擎去執行。
從上圖我們也可以看到,目前 Gluten 支援 ClickHouse、Velox、Apache Arrow Computer Engine 這幾個 Native 向量化引擎。這裡也體現了 Gluten 名字來源的意義,因為 Gluten 在拉丁語裡是粘合劑的意思,之所以取這個名稱,就是想要利用各種優秀的向量化執行引擎,來把它粘合到 Spark 的整個執行體系中。進而讓 Spark 提升計算效能,突破之前發現的 CPU 瓶頸問題。
02
Gluten 實現原理
接下來透過 Gluten 的元件架構、Plan 轉換等方面介紹 Gluten 的實現原理。
上圖是 Gluten 的整體設計。因為 Gluten 會沿用 Spark 原有的框架。當一個 SQL 進來,會透過 Spark 的 Catalyst 把 SQL 轉成 Spark 的物理計劃,然後物理計劃會傳遞給Gluten。Gluten 會以 Plugin 的方式整合到 Spark 中。
在 Physical Plan 交給 Gluten Plugin 的時候,會新增一些擴充套件的規則,然後把 Physical Plan 轉換成語言無關的 Substrait Plan。經過這個轉換後再交由下面的各種 Native 向量化引擎去執行計算。各自的向量化引擎會根據 Substrait Plan 構建自己的 Execute pipeline,然後讀取 Input 資料去做計算,計算完後都會以列式方式返回給 Spark。整個資料流轉過程是基於 Spark 原生 Columnar batch 和 Columnar vector 抽象,為 Native 向量化引擎做了一些具體的擴充套件和實現。
對於 ClickHouse,因為它內部有 block 概念,就會以 block 的方式去擴充套件 vector。對於 Arrow Computer Engine, 由於 Apache Arrow 本身就定義了記憶體資料格式,會把引擎計算出來的資料轉為 Arrow 格式,去實現一個叫 Arrow Columnar Vector 的格式表達 Arrow 型別的資料,從而讓 Spark 能夠去識別和讀取。目前 Gluten 支援的是 Velox、ClickHouse、Arrow Computer Engine 這三個主要的Native引擎。針對 GPU 方面,Intel 團隊之前做過一些調研和研究,計劃後續在 Gluten 發展壯大之後,會在 GPU 加速方面做一些擴充套件。
在整個流轉過程當中,Gluten 的 Plugin 層起到承上啟下的關係。Gluten Plugin 有哪些元件呢?
① 最為核心的就是 Plan 的 Conversion 元件,它把 Spark Physical Plan 透過 Extension rule inject 的方式轉成 Substrait Plan,然後再把 Substrait Plan 傳遞到底層的 Native Engine 執行。
② 第二就是 Memory 管理。我們知道 Native Engine 完全是脫離 JVM 的。如果不把Native Engine 的記憶體交給 Spark 統一來管理的話,就很可能出現記憶體溢位或者直接打爆整臺機器記憶體的情況,所以 Memory 管理也至關重要。
③ 第三就是 Shuffle,我們知道 Shuffle 是整個執行過程中比較重的 Operator,而且 Spark 原生的 Shuffle 是基於 Row 的。所以擴充套件出了一個叫 Columnar Shuffle Manager 的物件,支援整個 Shuffle 過程當中的列式資料。
④ 第四 Shim Layer 元件,熟悉 Spark 就應該知道,在 Spark 支援 Hive 時,也透過 Shim Layer 方式去支援多版本的 Hive。這裡的 Shim Layer 也是為了讓 Gluten 能支援多個版本 的Spark。因為 Spark 對外公開的介面,在版本之間變化不會那麼大,但內部介面變化還是比較大。所以如果要支援多版本 Spark 的話,就需要透過 Shim Layer 來適配多版本。
⑤ 第五個是 Fallback 元件,這是一個當前比較重要的元件。我們知道 Spark 經過這麼多年的發展,目前支援的 Operator 和 Expression 很多,而 Gluten 在發展初期,不可能把所有的 Operator 或 Expression 都支援。當遇到 Native Engine 不支援的情況時,會先透過 fallback 機制做驗證,驗證完之後如果不支援,就會回退到 Spark 原生 JVM 引擎去執行。
⑥ 第六個是 Metrics,它把 Native Engine 執行過程中的指標統計上報給 Gluten Plugin,然後再由 Plugin上報給 Spark 的 Metrics System 做展示或 API 呼叫。接下面針對這些元件逐一做簡單介紹。
2. 執行計劃
如上圖,整個 Gluten 的核心重點是執行計劃的傳遞。使用 Substrait 專案來作為 Spark Physical Plan 到 Native 向量化引擎傳遞的載體。Substrait 是與語言無關的,使用 proto 協議進行表達。在 Spark 經過 Catalyst 解析,規則最佳化後會輸出 Physical Plan,透過 Gluten 一系列 Extension Rules Inject 方式插入規則,這些規則主要是對 Spark Physical Plan 逐一轉換成 Substrait Plan。轉換過程會涉及到 fallback 機制校驗。轉換後的 pipeline 會盡可能構造在一個 whole stage 內,然後一次性地呼叫 Native Engine 執行。Gluten 使用 Java 方式去輸出 Substrait Plan 後,底層 Native 向量化引擎,比如 C++ 實現的引擎,會用 C++ 去解析這個 Substrait Plan,解析完後根據這個 Plan 去構造各自的 Execute pipeline,比如 scan、filter、aggregate 等,然後執行,執行完返回資料給 Spark。所以 Substrait 是一個比較重要的切入點。
3. Fallback
如上圖,描述的是 fallback 功能的實現, 實現 fallback 功能主要是因為 Spark 已經支援了十幾種 operator 和上百種 expression,Gluten 在初期不可能很快把這些 operator 及 expression 全部都支援。在整個 pipeline 的轉換過程中,會對每一個 Spark 的 Physical Plan 去做 Validate,Validate 成功說明裡麵包含的 operator 和 expression 都是支援使用 Native 向量化引擎執行。那麼就會盡可能打包成一個 Whole Stage 的 Transformer。這裡的 Whole Stage Transformer 可以對標 Spark 的 Whole Stage code gen。當校驗到某個 operator 或者 expression 失敗後,說明 Native Engine 是無法支援當前的 operator 或者 expression,就會觸發 fallback 機制。因為 Spark 的計算是基於行式的,而 Native 向量化引擎是基於列式的。因此就會在 fallback 這個 operator 前後插入行轉列、列轉行的運算元。Spark 執行完後行轉列,後繼交給 Native Engine 執行。當然要儘可能避免這種 fallback 操作,因為這兩次轉換對整個執行的過程的效能損耗是很大的。
這裡做過一些研究,嘗試在整個 Whole Stage 執行過程中,如果發現有一次回退,那麼後面的 operator 和 expression 即使 Native Engine 支援,也不再做轉換,減少頻繁的行列轉換。透過對每一個 Whole Stage 都做這樣的限制,儘可能減少 fallback,以及插入行轉列/列轉行的操作。當然最終的目標還是儘可能讓 Native Engine 支援 Spark 所有的 operator 和 expression,減少 fallback。
上面描述的 Plan 轉換可能會比較抽象。這裡舉個簡單例子,上面的圖是 TPCH Q1 做 Plan 轉換的過程。左邊是 Vanilla Spark 的 Plan,右邊是 Gluten 對標的 Plan。因為 DAG 圖比較大,所以直接用這種 Plan 的方式展示。可以看出 Vanilla Spark 在 scan 完之後,由於 Parquet Reader 是向量化讀取的列式資料,它就會先執行列轉行,然後給後面的 filter、project 進行行式計算。交給 Gluten 來執行後,我們可以看到 Scan 之後是列式資料。因為向量化執行引擎本身也是全部基於列式,這裡就不需要再做一次列轉行的操作。接下來就是執行和左邊一一對標的 Filter Execute Transformer,Project Execute Transformer 以及 Aggregate 等一系列操作,包括列式的 Shuffle Exchange,也都是列式執行。另外 Q1 會涉及到 order by,也就是會做 sort 操作。目前 sort 運算元還沒有支援,可以看到會插入列轉行運算元進去,把列式資料轉為行式。後面就全部由原生 Vanilla Spark 運算元 Shuffle Exchange,sort 去執行。
4. 列資料抽象
參照上圖,現在介紹整個 Gluten 的執行過程當中,涉及到的資料傳遞流程。在 Spark 裡有 Columnar Batch 和對應的 Columnar Vector 抽象。基於這個抽象把 Native Engine 各自對應的 Native 列式資料表達做具體實現。
這裡涉及兩種實現方式,第一種,我們知道 Apache Arrow 是想要實現一種與語言無關,規範的記憶體格式。如果使用 Arrow 這種格式,那麼在 Spark 這邊會有一個 Arrow Columnar Vector,對標的就是 Arrow 這種記憶體資料格式。這樣做的好處就是如果新接入一個 Native Engine,只要把資料 Export 成為 Arrow 格式,那麼 Spark 上層就可以很容易地接入,然後交給 Spark 來做讀取操作等。這種方式有一個不利的地方,因為每種 Native Engine 內部都會有各自的記憶體資料表達方式。為了統一接入,每一次都要做轉換成 Arrow 格式的操作,效能上會有所損耗。
還有一種方式,就如 ClickHouse backend 的自定義實現,因為 ClickHouse 內部是 block 格式,那麼基於 Spark Columnar Batch 和 Columnar Vector 去實現了 Block Columnar Vector,來代表了一個 block 資料儲存。透過這種方式在 Native Engine 引擎執行完後,返回的資料可以由這個 Block Columnar Vector 持有這塊資料,進而做後續的資料處理。
Velox 之前則採用的是方式一,因為效能的損耗,目前也在實現基於內部資料格式的 Velox Columnar Vector 的方式來做資料的傳遞。
5. Shuffle
對於 Spark 來講比較重的操作是 shuffle。參照上圖,在 Vanilla Spark 裡,使用的是行式 Shuffle。但是對於向量化引擎,內部資料流轉過程當中使用的是列式。因此基於 Spark Shuffle Manager 框架,擴充套件出 Columnar Shuffle Manager。在整個向量化引擎執行中,涉及到的 Shuffle 操作,都會透過這個 Columnar Shuffle Manager 去做列式資料 split,序列化,壓縮,最後輸出 Shuffle 資料到磁碟。而讀取 Shuffle 資料則交由 Spark 原有的框架進行操作,再傳遞給 Native Engine 作後續處理。目前的 Columnar Shuffle Manager 只實現了類似於 Spark 的 Bypass Sort Shuffle 的功能,所以當 Shuffle partition 過多的時候,它會生成過多的小檔案,所以效能會有問題。有做過一個調研,即直接利用 Spark 原生 Sort Shuffle Manager 的方案,只是在序列化和反序列化的時候直接使用了列式的方式輸出 Shuffle 資料。這麼做的優點是很容易利用 Spark 原生已經支援的 Sort Shuffle 的方式。在 Shuffle partition 達到幾百甚至上千的時候,可以去規避小檔案過多的問題。但是在實際測試過程中,也發現了一些問題,例如:在使用 Sort Shuffle Manager 時,整個 Shuffle 資料都會在記憶體裡複製一遍。因為 Native Engine 的資料在堆外,Spark Sort Shuffle Manager 在傳輸過程中,會把 Native 資料拉到 JVM 裡,再經由 Spark 的 Shuffle 框架做寫入或者讀取。這就會涉及一個 memory copy 問題,這個問題也是後面會重點解決的一個點。
6. 記憶體管理
上圖是記憶體管理機制的一個流程圖。Native Engine 完全脫離 JVM,為了保證整個執行過程中,不會讓 Native Engine 把整個記憶體吃完或者是出現 OOM 情況,則基於 Spark 原生的 Unified Memory Manager 機制,整合 Spark Off-heap Memory Manager 對 Native Engine 所用的記憶體做管理。Spark 的 task 執行時,會使用 Task Memory Manager 對每個 task 的記憶體做申請釋放管理。基於這套機制去實現了 Native Engine 的 memory consumer,每個 task 在呼叫執行 Native Engine 的時候,會透過這一套機制把 Native Engine 申請的記憶體上報到 Task Memory Manager,於是在 Spark 側就可以知道 Native Engine 使用了多少記憶體,Executor 的記憶體存量是否滿足要申請的記憶體大小。
但這裡有一點與 Spark 原生的 task memory 管理不同,就是 Native Engine 自身會有一個 memory pool,因為在 Native Engine 這一塊的記憶體的申請都是細化到 malloc 這個級別的,也就是在 Native Engine 執行過程中,new 一個物件或者是申請一個小的 byte 陣列,都會去呼叫這個介面來上報記憶體分配的情況,如果每次 malloc 就要上報的話,就會導致頻繁回撥介面,進而影響整體的效能。因此在 Native Engine 側都加了一個 memory pool。比如一次申請 8M 空間。那麼在 Native Engine 使用的記憶體沒達到 8M 時,是不會頻繁地透過 JNI 的方式呼叫 Spark 的 Memory Consumer 往 JVM 上報。而 ClickHouse backend 的實現有些不同,它則是如果申請的記憶體在小於 4M 情況下,並不會向 Spark 申請記憶體。
不管是以哪種方式,都是為了儘可能的規避頻繁透過JNI介面去回撥 Spark Memory Consumer 來上報記憶體,減少對效能的影響。另外 Spark 在記憶體管理的時候,當 Executor 記憶體不足時,會去呼叫 spill 方法讓 Memory Consumer 把自己管理的記憶體釋放掉一部分,或者把一部分記憶體吐出到磁碟上面。當前 spill 功能還未實現,目前全部資料都是 hold 在記憶體當中,後續計劃把這一塊補齊。
7. Debug 支援
上圖是 JVM 與 Native Engine 呼叫的關係圖。為了方便 Debug,在整個 Plan 轉換的過程當中,會以 Substrait Plan 的方式統一地下發給 Native Engine。在出現問題的情況下,可以把出現問題的 Substrait Plan 生成二進位制檔案。然後就可以把這個 Plan 交由 Native Engine 側做 UT 或者作為問題重現的輸入,達到直接在 Native 側做 Debug、profile 或者最佳化等操作。可以避免 Debug 時候同時要啟動 JVM 和 Native Engine 的麻煩。
03
當前進展和後續工作
下面介紹一下 Gluten 專案的進展情況,以及後續工作重點。
1. Gluten 當前的狀態
當前 TPCH 場景下,Velox 22 條 SQL 已經全部支援。對於 ClickHouse backend,因為 ClickHouse 不支援 not equal join,q21 目前暫時還不支援,其他的 21 條也都已經支援了。目前支援的 operator 包含 Scan、Filter、Project、HashAggregate。Join 支援的是 BroadcastHashJoin、ShuffledHashJoin。關於 Expression 的支援情況是:TPCH 裡,SQL 裡面涉及的 Expression 都已經支援。
2. 效能
在使用 Gluten+ Velox backend 或者 ClickHouse backend 後,到底能為 Spark 帶來多少效能的提升呢?上圖描述了 Intel 團隊,使用 Gluten + Velox backend,在 TPCH 1000 場景下做的一個測試。我們可以看到在有一些 SQL,比如 Q4 可以達到 3.6 倍的提升。整體也有兩倍提升,即整個 TPCH 跑一輪需要的耗時是 Vallina Spark 的一半。這是 6 月份出的資料,一些最新的資料會更新在 Gluten 的 Github 上。後面還做了一些最佳化,當前該圖中的效能已經有了進一步的提升。
而上圖是 Gluten + ClickHouse backend 的測試結果,同樣也是在 TPCH 1000 下的效能測試,最高的可以達到 3.5 倍的提升。各自跑一輪,整體時間的提升也是達到了兩倍多。目前針對 ClickHouse backend,TPCH 的 q21 先 skip 掉,只跑了剩下的 21 條,整體效能提升了兩倍。Gluten 團隊最近也在效能方面做了進一步最佳化,整體的效能還會有進一步的提升。
3. 工作計劃
後續的一個重點工作就是完成對 TPCDS 的 104 條 SQL 的支援。同時在 data type 方面,會支援 Float、Binary、Decimal、複雜型別等,後面也會再針對這些型別做更進一步的最佳化。
另外就是 local cache 的支援,如果 Gluten 執行在雲上,讀取的是物件儲存,則需要有 cache 層支援。但這個 cache 層,需要在各自 Native Engine 去做實現,因此需要各自 Native Engine 的 backend 把雲端的資料做 cache 管理。這一塊也都在計劃當中,應該在近期會有實質的進展。
最後是 Shuffle,Shuffle 一直以來都是重點,目前發現 Columnar Shuffle Manager 還存在著一些問題,包括 push based 的 Shuffle Manager 如何支援等,也是後面去調研支援的重點。
以上就是 Gluten 後續的工作重點。
04
回到開始第一張圖,在 Spark 發展到 3.2 時,效能提升趨緩。結合 Gluten,不管是 Velox backend 還是 ClickHouse backend,整體的效能提升達到了兩倍,甚至某一些場景下面可以達到三倍以上的效能提升。而且這是 Spark 效能整體的提升,並不是只針對某些個別 SQL 的調優。
上圖是目前 Gluten 專案參與團隊的主要人員名單。因為 Gluten 專案來自於 Intel 的開源軟體,所以 Intel 參與的人數最多,而且 Intel 在這一塊也深耕了多年。接著就是 Kyligence 的同事們。Kyligence 是從 Gluten 專案開始籌劃就一直參與開發的,目前重點 focus 在 ClickHouse backend,也包括整體的 Gluten 功能。接著是 Bigo 的幾位小夥伴們也是全職加入到了 Gluten 專案開發當中。
這裡還要特別感謝的一位是 Substrait 專案的 owner:Jack,因為 Substrait 專案在釋出初期,Gluten 專案就去使用了,在使用過程中得到了 Jack 的很多幫助和建議。
另外還要特別感謝的是 Velox 團隊,因為 Intel 團隊使用了 Velox 作為 backend 來開發。Velox 團隊為 Intel 團隊提供了很多支援,也特別感謝他們。
最後歡迎能有更多的小夥伴們加入到 Gluten 開源專案中,Github 地址:。也歡迎小夥伴們加入 Gluten 使用者群,一起交流談論~
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70027827/viewspace-2942554/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 向量化執行引擎框架 Gluten 宣佈正式開源,並亮相 Spark 技術峰會框架Spark
- 全文檢索引擎(三)---原理剖析索引
- MySQL Count(*)提速30倍MySql
- Gluten, the vectorized execution engine framework, is officially open sourced and unveiled at the Spark Technology SummitZedFrameworkSparkMIT
- spark基礎之spark sql執行原理和架構SparkSQL架構
- Spark SQL / Catalyst 內部原理 與 RBOSparkSQL
- 剖析Vue原理&實現雙向繫結MVVMVueMVVM
- Spark Driver Program剖析Spark
- Apache Spark原始碼剖析ApacheSpark原始碼
- 圖形加速可令Java提速10倍Java
- 儲存2N倍擴容原理
- 掌握4種SQL索引型別,剖析索引原理SQL索引型別
- 我是如何用2個Unix命令給SQL提速的SQL
- spark核心原始碼深度剖析Spark原始碼
- Quick BI 的模型設計與生成SQL原理剖析UI模型SQL
- Spark系列 - (3) Spark SQLSparkSQL
- Oracle 高效能SQL引擎剖析--SQL優化與調優機制詳解OracleSQL優化
- Spark2 Dataset之檢視與SQLSparkSQL
- Spark SQLSparkSQL
- 探究Presto SQL引擎(2)-淺析JoinRESTSQL
- Spark SQL:4.對Spark SQL的理解SparkSQL
- Memcached 原理剖析
- Eureka原理剖析
- AbstractQueuedSynchronizer原理剖析
- JVM原理剖析JVM
- KVC原理剖析
- 存算分離下寫效能提升10倍以上,EMR Spark引擎是如何做到的?Spark
- 我是如何使計算提速>150倍的
- 一次提速1000倍的delete操作delete
- Spark SQL 教程: 通過示例瞭解 Spark SQLSparkSQL
- Spark SQL 教程: 透過示例瞭解 Spark SQLSparkSQL
- spark 原始碼分析之十八 -- Spark儲存體系剖析Spark原始碼
- spark 原始碼分析之十五 -- Spark記憶體管理剖析Spark原始碼記憶體
- 向量化程式碼實踐與思考:如何藉助向量化技術給程式碼提速
- Spark_SQlSparkSQL
- 如何用WebAssembly為Web應用提速20倍(案例研究)Web
- [譯] 用 WebAssembly 提速 Web App 20 倍(例項學習)WebAPP
- 如何讓 Xcode 在讀寫上提速100倍?XCode