您好,登錄后才能下訂單哦!
cdn.xitu.io/2018/11/21/1673560dca70a6b7?w=1433&h=534&f=jpeg&s=309760">?
?
spark sql 可以說是 spark 中的精華部分了,我感覺整體復雜度是 spark streaming 的 5 倍以上,現在 spark 官方主推 structed streaming, spark streaming ?維護的也不積極了, 我們基于 spark 來構建大數據計算任務,重心也要向 DataSet 轉移,原來基于 RDD 寫的代碼遷移過來,好處是非常大的,尤其是在性能方面,有質的提升, ?spark sql 中的各種內嵌的性能優化是比人裸寫 RDD 遵守各種所謂的最佳實踐更靠譜的,尤其對新手來講, 比如有些最佳實踐講到先 filter 操作再 map 操作,這種 spark sql 中會自動進行謂詞下推,比如盡量避免使用 shuffle 操作,spark sql 中如果你開啟了相關的配置,會自動使用 broadcast join 來廣播小表,把 shuffle join 轉化為 map join 等等,真的能讓我們省很多心。?
?
spark sql 的代碼復雜度是問題的本質復雜度帶來的,spark sql 中的?Catalyst 框架大部分邏輯是在一個 Tree 類型的數據結構上做各種折騰,基于 scala 來實現還是很優雅的,scala 的偏函數和強大的 Case 正則匹配,讓整個代碼看起來還是清晰的, 這篇文章簡單的描述下 spark sql 中的一些機制和概念。
?
SparkSession 是我們編寫 spark 應用代碼的入口,啟動一個 spark-shell 會提供給你一個創建 SparkSession, 這個對象是整個 spark 應用的起始點,我們來看下 sparkSession 的一些重要的變量和方法:
?
?
?
上面提到的 sessionState 是一個很關鍵的東西,維護了當前 session 使用的所有的狀態數據,有以下各種需要維護的東西:
?
?
?
spark sql 內部使用 dataFrame 和 Dataset 來表示一個數據集合,然后你可以在這個數據集合上應用各種統計函數和算子,有人可能對 ?DataFrame 和 Dataset 分不太清,其實?DataFrame 就是一種類型為 Row 的 DataSet,
?
type?DataFrame?=?Dataset[Row]
?
這里說的 Row 類型在 Spark sql 對外暴露的 API 層面來說的, 然而 DataSet 并不要求輸入類型為 Row,也可以是一種強類型的數據,DataSet 底層處理的數據類型為 Catalyst 內部?InternalRow?或者 UnsafeRow?類型, 背后有一個?Encoder 進行隱式轉換,把你輸入的數據轉換為內部的?InternalRow,那么這樣推論,DataFrame 就對應 RowEncoder。
?
在?Dataset 上進行?transformations 操作就會生成一個元素為 LogicalPlan 類型的樹形結構, 我們來舉個例子,假如我有一張學生表,一張分數表,需求是統計所有大于 11 歲的學生的總分。
?
?
?
?
這個?queryExecution 就是整個執行計劃的執行引擎, 里面有執行過程中,各個中間過程變量,整個執行流程如下
?
?
?
那么我們上面例子中的 sql 語句經過 Parser 解析后就會變成一個抽象語法樹,對應解析后的邏輯計劃 AST 為
?
?
?
形象一點用圖來表示
?
?
?
我們可以看到過濾條件變為了 Filter 節點,這個節點是 UnaryNode 類型, 也就是只有一個孩子,兩個表中的數據變為了?UnresolvedRelation 節點,這個節點是?LeafNode 類型, 顧名思義,葉子節點, JOIN 操作就表位了 Join 節點, 這個是一個?BinaryNode 節點,有兩個孩子。
?
上面說的這些節點都是 LogicalPlan 類型的, 可以理解為進行各種操作的 Operator, spark sql 對應各種操作定義了各種 Operator。
?
?
?
這些 operator 組成的抽象語法樹就是整個 Catatyst 優化的基礎,Catatyst 優化器會在這個樹上面進行各種折騰,把樹上面的節點挪來挪去來進行優化。
?
現在經過 Parser 有了抽象語法樹,但是并不知道 score,sum 這些東西是啥,所以就需要 analyer 來定位,?analyzer 會把 AST 上所有 Unresolved 的東西都轉變為 resolved 狀態,sparksql 有很多resolve 規則,都很好理解,例如 ResolverRelations 就是解析表(列)的基本類型等信息,ResolveFuncions 就是解析出來函數的基本信息,比如例子中的sum 函數,ResolveReferences 可能不太好理解,我們在 sql 語句中使用的字段比如 Select name 中的 name 對應一個變量, 這個變量在解析表的時候就作為一個變量(Attribute 類型)存在了,那么 Select 對應的 Project 節點中對應的相同的變量就變成了一個引用,他們有相同的 ID,所以經過 ResolveReferences 處理后,就變成了?AttributeReference 類型???,保證在最后真正加載數據的時候他們被賦予相同的值,就跟我們寫代碼的時候定義一個變量一樣,這些 Rule 就反復作用在節點上,指定樹節點趨于穩定,當然優化的次數多了會浪費性能,所以有的 rule??作用 Once, 有的 rule 作用 FixedPoint, 這都是要取舍的。好了,?不說廢話,我們做個小實驗。
?
?
?
我們使用?ResolverRelations 對我們的 AST 進行解析,解析后可以看到原來的?UnresolvedRelation 變成了 LocalRelation,這個表示一個本地內存中的表,這個表是我們使用 createOrReplaceTempView 的時候注冊在 catalog 中的,這個 relove 操作無非就是在 catalog 中查表,找出這個表的 schema,?而且解析出來相應的字段,把外層用戶定義的 各個 StructField 轉變為 AttibuteReference,使用 ID 進行了標記。
?
?
?
我們再使用?ResolveReferences 來搞一下,你會發現上層節點中的相同的字段都變成了擁有相同 ID 的引用,他們的類型都是?AttibuteReference。最終所有的 rule 都應用后,整個 AST 就變為了?
?
?
?
下面重點來了,要進行邏輯優化了,我們看下邏輯優化有哪些:
?
?
?
sparksql 中的邏輯優化種類繁多,spark sql 中的?Catalyst 框架大部分邏輯是在一個 Tree 類型的數據結構上做各種折騰,基于 scala 來實現還是很優雅的,scala 的偏函數 和 強大的 Case 正則匹配,讓整個代碼看起來還是清晰的,廢話少說,我們來搞個小實驗。
?
?
?
看到了沒,把我的 (100 + 10) 換成了 110。
?
?
?
使用?PushPredicateThroughJoin 把一個單單對 stu 表做過濾的 Filter 給下推到 Join 之前了,會少加載很多數據,性能得到了優化,我們來看下最終的樣子。
?
?
?
至少用了?ColumnPruning,PushPredicateThroughJoin,ConstantFolding,RemoveRedundantAliases 邏輯優化手段,現在我的小樹變成了:
?
?
做完邏輯優化,畢竟只是抽象的邏輯層,還需要先轉換為物理執行計劃,將邏輯上可行的執行計劃變為 Spark 可以真正執行的計劃。
?
?
?
spark sql 把邏輯節點轉換為了相應的物理節點,?比如 Join 算子,Spark 根據不同場景為該算子制定了不同的算法策略,有BroadcastHashJoin、ShuffleHashJoin 以及 SortMergeJoin 等, 當然這里面有很多優化的點,spark 在轉換的時候會根據一些統計數據來智能選擇,這就涉及到基于代價的優化,這也是很大的一塊,后面可以開一篇文章單講, 我們例子中的由于數據量小于 10M, 自動就轉為了?BroadcastHashJoin,眼尖的同學可以看到好像多了一些節點,我們來解釋下,?BroadcastExchange 節點繼承 Exchage 類,用來在節點間交換數據,這里的BroadcastExchange 就是會把 LocalTableScan出來的數據 broadcast 到每個 executor 節點,用來做 map-side join。最后的 Aggregate 操作被分為了兩步,第一步先進行并行聚合,然后對聚合后的結果,再進行 Final 聚合,這個就類似域名 map-reduce? 里面的 combine 和最后的 reduce, 中間加上了一個?Exchange hashpartitioning, 這個是為了保證相同的 key shuffle 到相同的分區,當前物理計劃的 Child 輸出數據的 Distribution 達不到要求的時候需要進行Shuffle,這個是在最后的?EnsureRequirement 階段插入的交換數據節點,在數據庫領域里面,有那么一句話,叫得 join 者得天下,我們重點講一些 spark sql 在 join 操作的時候做的一些取舍。
?
Join 操作基本上能上會把兩張 Join 的表分為大表和小表,大表作為流式遍歷表,小表作為查找表,然后對大表中的每一條記錄,根據 Key 來取查找表中取相同 Key 的記錄。
?
spark 支持所有類型的 Join:
?
?
?
spark sql 中 join 操作根據各種條件選擇不同的 join 策略,分為?BroadcastHashJoin,?SortMergeJoin, ShuffleHashJoin。
?
可以看到這個最終執行的時候分分成了兩個 stage, 把小表 broeadcastExechage 到了大表上做?BroadcastHashJoin, 沒有進化 shuffle 操作,然后最后一步聚合的時候,先在 map 段進行了一次 HashAggregate sum 函數, 然后 Exchage 操作根據 name 把相同 key 的數據 shuffle 到同一個分區,然后做最終的 HashAggregate sum?操作,這里有個 WholeStageCodegen 比較奇怪,這個是干啥的呢,因為我們在執行 Filter ,Project 這些 operator 的時候,這些 operator 內部包含很多? Expression, 比如?SELECT sum(v),name, 這里的 sum 和 v 都是 Expression,這里面的 v 屬于 Attribute 變量表達式,表達式也是樹形數據結構,sum(v) ?就是 sum 節點和 sum 的子節點 v 組成的一個樹形結構,這些表達式都是可以求值和生成代碼的,表達式最基本的功能就是求值,對輸入的 Row 進行計算 , Expression 需要實現?def eval(input: InternalRow = null): Any?函數來實現它的功能。
?
表達式是對 Row 進行加工,輸出的可以是任意類型,但是 Project 和 Filter 這些 Plan 輸出的類型是?def output: Seq[Attribute], 這個就是代表一組變量,比如我們例子中的?Filter (age >= 11) 這個plan, 里面的?age>11 就是一個表達式,這個 > 表達式依賴兩個子節點, 一個Literal常量表達式求值出來就是 11, 另外一個是?Attribute 變量表達式 age, 這個變量在 analyze 階段轉變為了?AttributeReference 類型,但是它是Unevaluable,為了獲取屬性在輸入 Row 中對應的值, 還得根據 schema 關聯綁定一下這個變量在一行數據的 index, 生成 BoundReference,然后?BoundReference 這種表達式在 eval 的時候就可以根據 index 來獲取 Row 中的值。??age>11 這個表達式最終輸出類型為 boolean 類型,但是 Filter 這個 Plan 輸出類型是?Seq[Attribute] 類型。
?
可以想象到,數據在一個一個的 plan 中流轉,然后每個 plan 里面表達式都會對數據進行處理,就相當于經過了一個個小函數的調用處理,這里面就有大量的函數調用開銷,那么我們是不是可以把這些小函數內聯一下,當成一個大函數,WholeStageCodegen 就是干這事的。
?
?
可以看到最終執行計劃每個節點前面有個 * 號,說明整段代碼生成被啟用,在我們的例子中,Filter, Project,BroadcastHashJoin,Project,HashAggregate 這一段都啟用了整段代碼生成,級聯為了兩個大函數,有興趣可以使用 a.queryExecution.debug.codegen 看下生成后的代碼長什么樣子。然而 Exchange 算子并沒有實現整段代碼生成,因為它需要通過網絡發送數據。
?
我今天的分享就到這里,其實 spark sql 里面有很多有意思的東西,但是因為問題的本質復雜度,導致需要高度抽象才能把這一切理順,這樣就給代碼閱讀者帶來了理解困難, 但是你如果真正看進去了,就會有很多收獲。如果對本文有任何見解,歡迎在文末留言說出你的想法。??
**牛人說**
?
「牛人說」專欄致力于技術人思想的發現,其中包括技術實踐、技術干貨、技術見解、成長心得,還有一切值得被發現的內容。我們希望集合最優秀的技術人,挖掘獨到、犀利、具有時代感的聲音。
?
投稿郵箱:marketing@qiniu.com
br/>??
**牛人說**
?
「牛人說」專欄致力于技術人思想的發現,其中包括技術實踐、技術干貨、技術見解、成長心得,還有一切值得被發現的內容。我們希望集合最優秀的技術人,挖掘獨到、犀利、具有時代感的聲音。
?
投稿郵箱:marketing@qiniu.com
?
?
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。