鑑於 Spark 基於內存計算這一天性,如下集羣資源可能會形成 Spark 程序的瓶頸:java
CPU,帶寬和內存。一般狀況下,若是內存足夠的狀況下,瓶頸只可能出如今網絡帶apache
寬方面;但有時,你也須要作一些例如序列化優化來下降內存使用率。這份指導主要數組
集中於兩方面:數據序列化,這是充分提高網絡表現和下降內存消耗、內存優化的關緩存
鍵;咱們也會簡要闡述一些小技巧。服務器
數據序列化網絡
序列化在任何分佈式應用的運行中扮演了重要的角色。採用那些序列化慢的格式、或數據結構
者消費巨量字節時將會嚴重拖慢計算效率。一般狀況下,調整數據的序列化方式是你app
優化 Spark 程序時首先須要作的事。Spark 程序試圖在簡潔(循序你在代碼中使用任框架
何 Java 的數據類型)和效率之間取得一種平衡。Spark 提供了兩種序列化庫。分佈式
Java
serialization:默認狀況下,Spark 序列話一個對象時使用 Java 自帶的
ObjectOutputStream 框架,對於任何實現了 java.io.Serializable 接口的類都
有效。有也能夠經過繼承 java.io.Externalizable 來自定義你的序列化過程。
Java serialization 是靈活的,但一般至關緩慢而且致使不少類的序列化格式很
臃腫。
·
Kryo
serialization:
Spark 也可使用更快的序列化類庫 Kryo
library
(version 2)來序列化對象。相比 Java serialization,Kryo 具備更快和更加緊湊
(一般提供 10 倍於 Java 序列化的效率)的優點。但對於全部可序列化的類型
不是所有都支持,所以爲了更好的效率,你須要提早爲你的程序註冊這些類。
又能夠經過設置初始化時的 SparkConf 和調用
conf.set("spark.serializer","org.apache.spark.serializer.KryoSerializer")來切換到
Kryo 模式。這項配置不只會在工做節點進行數據混洗時用到 Kryo 序列化,並且在將
RDD 序列化到硬盤時也會使用到 kryo。Kryo 之因此沒有成爲默認設置是由於使用者
須要自行註冊一些類,可是咱們建議在一些網絡密集型應用中嘗試使用 kryo 序列化。
從 Spark2.0.0 開始,當對於簡單類型,簡單類型數組和字符串類型的 RDD 進行混洗
時,Spark 已經使用 Kryo 進行了內部整合。
Spark 已經自動對不少經常使用的核心 Scala 類(包含於 AllScalaRegistrar,位於
Twitter chill library)進行了 Kryo 序列化註冊。
要使自定義類應用 Kryo 註冊,你須要 registerKryoClasses 方法:
[java] view plain copy
1. SparkConf sparkConf = new SparkConf().setAppName("Kryo");
2. Class<?>[] classs={MyClass.class,YourClass.class};
3. sparkConf.registerKryoClasses(classs);
4. JavaSparkContext sc = new JavaSparkContext(sparkConf);
kryo 文檔描述了進階註冊選項,例如添加自定義序列化編碼。
若是你的對象很大,你可能須要增長 spark.kryoserializer.buffer。這個值須要足夠
大,以致於可以容納下被序列化的最大的對象。
若是你不註冊你的自定義類,Kryo 仍然會執行下去,但它將會存儲每一個對象的類全
名,這真是一種浪費。
內存優化
在優化內存使用率時主要有三方面可考慮的因素:你建立的對象佔用的內存量(你也
許想將整個數據據裝進內存),訪問這些數據的代價和累計回收的開銷(當你的對象
在內存中具備較高輪換率時)
默認狀況下,訪問 Java 對象是快速的。但一不留神就會消耗 2 到 5 倍的空間用於存儲
對象中的原始屬性變量,這主要是出於如下緣由:
·
每一個不一樣的 Java 對象擁有一個「對象頭」,這個「對象頭」佔用 16 字節,包
含指向所屬類型的指針信息。對於一個包含不多數據的對象(例如一個 Int 屬
性),這些「對象頭」信息佔用的內存空間可能比數據自己更大。
Java 的 String 對象包含將近 40 字節的開銷用於描述這些原始字符串數據(因
爲他們將其存儲與一個字符數組並保存了額外的信息,例如字符串長度),同
時每一個字符使用兩個字節存儲(UTF-16)。所以一個 10 個字符的 String 對
象,輕輕鬆鬆就能消耗 60 字節的空間。
·
常見的集合類,例如 HashMap 和 LinkedList,使用了鏈式結構,針對每一個實
體(Map.Entry)都對應一個包裝對象。這個對象不只包含了「頭信息」,而
且存儲了指向下一個對象的指針。
原始基本數據類型的集合對象在存儲每個基本類型時仍是用了包裝類對象,
例如 java.lang.Integer
這一部分將首先簡要概述一下 Spark 的內存管理,而後列舉一些特殊的策略,來幫助
你在優化你的應用時採起更高效的方式。咱們將着重描述如何肯定對象的內存佔用和
如何改變數據結構和序列化方式來下降內存佔用。而後,咱們會介紹如何優化 Spark
的緩存大小和 Java 垃圾回收。
內存管理概覽
Spark 的內存使用大體可劃分兩類:執行和存儲。執行存儲指的是計算(shuffles,
join,sorts 和 aggregations)時用的內存,而存儲內存指的是用於緩存和在集羣內
部傳播的數據。在 Spark 中,執行和存儲共享統一的區域(M)。當執行模塊沒有佔
用內存時,存儲模塊能夠獲取所有內存(統一區域),反之亦然。執行模塊能夠驅逐
存儲模塊,當且僅當所有內存使用落到某個設定的閾值時(R)。換句話說,R 從 M
劃分出一個亞區,這個亞區的緩存不可被驅逐。出於實現的複雜性的考慮,存儲模塊
沒法驅逐執行模塊。
這個設計確保了一些吸引人的特性。一、若是應用不使用緩存的話,計算模塊可使用
整個內存空間進行計算,排除沒必要要的硬盤溢寫。二、若是一個應用能夠經過 R 預約一
個最低的存儲空間用於緩存,那這些緩存對於驅逐是免疫的。三、這提供了可靠的開箱
即用的方式來應對不一樣的工做,即使你不是一個對內部存儲分配了如指掌的專家。
儘管 Spark 提供了兩個相關的配置項,但大部分用戶並不須要去調整它們,由於配置
項的默認值已經足夠應對大多數工做任務。
·
spark.memory.fraction:表明了上文中的 M,表示內存佔用(JVM
heap
space-300MB)比率(默認值 0.6)。剩餘的 40%的空間主要用來存儲數據
結構、內部元數據並預防由稀疏、大記錄引起 OOM。
spark.memory.storageFraction:表示上文提到的 R,表示從 M 中劃分出 R
大小的一個區域(默認值 0.5),這個被劃分出的區域中的緩存數據塊對於計
算模塊的驅逐是免疫的。
spark.memory.fraction 值的配置應當使得 JVM 中的堆內存與老代和永久代的空間相
協調。具體配置見下文 GC 優化調整細節。
判斷內存消耗
判斷一個數據集到底消耗多少內存的最佳方式是:將數據集加載到 RDD 並將其緩存下
來,而後去 Spark Web UI 查看「Storage」頁面。這個頁面將告訴你,你的 RDD 正
在申請多大的內存。
要預估某個指定對象的內存消耗時,請使用 SizeEstimator 的 estimate 方法,這是對
於哪些想試驗一下如何經過改變數據類型來消減內存和判斷某個廣播變量將在每一個執
行器申請多大內存的朋友來講是個好工具。
優化數據結構
下降內存消耗的首要方法就是避免使用添加額外開銷的 Java 特徵,例如基於指針的數
據結構和包裝對象。具體小貼士以下:
1. 使用對象數組和原始類型來構造你的數據結構,而不是使用標準的 Java 和
Scala 集合類(例如 HashMap)。fastutil 庫提供了針對原始類型的便捷的集
合類,這些類兼容 Java 標準庫。
2. 避免使用包含過多小對象和指針的嵌套結構。
3. 考慮使用數字和枚舉對象代替字符串做爲鍵值。
4. 若是你使用的隨機內存少於 32G,設置 JVM 的標誌-
XX:+UseCompressedOops 來使引用只佔用 4 字節而不是 8 字節。同窗你可
以在 spark-env.sh 中添加這個配置項哦
序列化 RDD 存儲
當你的對象太大以致於以上優化均被無視的狀況下,有一個副更簡單的藥能夠拯救你
的對象,那就是將它存儲爲序列化格式來下降內存使用。經過使用序列化級別來將
RDD 持久化,例如 MEMORY_ONLY_SER。隨後,Spark 將 RDD 的每一個分區存儲成
一個個字節數組。這粒藥丸只有一個反作用,那就是訪問這些序列化的數據是須要多
耗費些時間,由於在讀取前須要先反序列化這些數據。若是你以爲你的 Spark 程序需
要吃藥的話,咱們強烈建議你使用 Kryo 這一序列化格式來緩存你的數據,由於相比
Java 自帶的序列化方式,Kryo 可讓你的對象更瘦(這就是抽脂和整容流行的原
因)。
垃圾回收優化
當你的程序存儲的 RDD 須要頻繁輪換時,JVM 垃圾回收可能會出現問題。(當對一
個 RDD 僅讀取一次,而後在其上進行屢次操做時並不會帶來問題)當 Java 須要回收
老對象佔用的空間時,它將掃描你全部的對象來找到其中不被使用的。須要指出的一
點是,垃圾回收的消耗和你的 Java 對象個數成正比,所以你所應用的數據結構擁有的
對象越少越好(例如使用 int 數組代替 LinkedList)。一個更好的方法是使用序列化
格式來持久化你的對象,如上所述:一旦序列化後,每一個 RDD 將只對應一個對象(一
個字節數組)。因此當存在 GC 問題時,在嘗試其餘技巧前,你首先要作的是使用序
列化的緩存技術。
因爲工做節點上任務工做內存和 RDD 緩存之間的衝突也會致使 GC 問題。咱們將會討
論如何分配空間去存儲 RDD 緩存來緩解這個問題。
測算 GC 的影響
第一步是收集關於垃圾處理的頻率和 GC 消耗時間的統計數據。這個能夠經過添加如
下 Java 選項-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps 來實
現。
[java] view plain copy
1. ./bin/spark-submit --name "My app" --master local[4] --
conf spark.eventLog.enabled=false
2.
--conf "spark.executor.extraJavaOptions=-XX:+PrintGCDetails -
XX:+PrintGCTimeStamps" myApp.jar
下次 Spark 啓動時,每次 GC 的操做日誌將會打印出來。須要注意的是這些日誌將出
如今你的集羣工做節點上(在工做目錄的標準輸出文件裏),而不是你的驅動程序
裏。
GC 進階優化
爲了準備更深刻的垃圾回收優化,咱們先要理解一些關於 JVM 內存管理的基本知識:
·
Java Heap 空間被分紅了兩個區域 Young 和 Old。Young 代主要保存短生命
週期的對象,而 Old 代用於保存具備長生命週期的對象。
·
·
Young 代又被進一步劃分爲三個區[Eden,Survivor1,Survivor2]
簡述一下內存碎片整理步驟:當 Eden 滿了,一個小型的 GC 被觸發,Eden
和 Survivor1 中倖存的仍被使用的對象被複制到 Survivor2。Survivor1 和
Survivor2 區域進行交換。當一個對象生存的時間足夠長或者 Survivor2 滿
了,它被轉移到 Old 代。最終當 Old 空間快滿時,一個全面的 GC 被召喚。
GC 優化的目的是使 Spark 保證只有長生存週期的 RDDs 纔會被存儲在 Old
代,而且 Young 代設計爲知足存儲短生命週期的對象。
這將幫你避免全面 GC 去收集 Spark 運行期間產生的臨時對象。一些實用的小技巧如
下:
·
首先檢查 GC 日誌中是否有過於頻繁的 GC。若是在一個任務完成前,全量 GC
被喚醒了屢次,它意味着對於執行任務來講沒有分配足夠的內存。
若是有太多的小型垃圾收集但全量 GC 出現並很少,給 Eden 分配更多的內存
會頗有幫助。你能夠爲每一個任務設置爲一個高於其所需內存的值。假設 Eden
代的內存需求量爲 E,你將能夠設置 Young 代的內存爲-Xmn=4/3*E。(這一
設置一樣也會致使 survivor 區同時擴張)
·
在 GC 打印的日誌中,若是 OldGen 接近滿時,能夠經過下降
spark.memory.fraction 來減小用於緩存的空間。更好的方式是緩存更少的對
象而不是下降做業執行時間。一個可選的方案是減小 Young 代的規模。若是
你設置了「-Xmn」,能夠下降-Xmn。若是沒有設置,能夠嘗試改變 JVM 的
NewRatio 參數。不少 JVM 的 NewRation 默認值是 2,這意味着 Old 代申請
2/3 的堆空間。它的值應在足夠大以致能夠超過 spark.memory.fraction。
嘗試使用 G1GC 垃圾收集選項:-XX:+UseG1GC。當 GC 存在瓶頸時,採用這
一選項在某些狀況下能夠提高性能。當執行器的堆空間比較大時,提高 G1
region size(-XX:G1HeapRegionSize)是一種重要的選擇。
·
·
若是你的任務須要從 HDFS 系統讀取數據,能夠經過估計 HDFS 文件的大小來
預估任務所需的內存量。須要注意的是解壓後的塊大小是原大小的 2 到 3 倍。
所以咱們須要設置 3 到 4 倍的工做空間用於做業執行,例如 HDFS 的塊大小爲
128MB,咱們須要預估 Eden 的大小爲 4*3*128MB。
·
監控在新變化和設置生效後,GC 的頻率和耗費的事件。
咱們的經驗建議是,GC 優化的成效依賴與你的應用和可用內存的多少。網上也有許多
優化策略,可是須要更深的知識基礎,例如經過控制全量 GC 發生的頻率來降少總開
銷。
經過設置 spark.executor.extraJavaOptions 能夠實現對執行器中 GC 的優化調整。
其餘的考慮
並行度
若是合理分配每一個操做的並行度,將極大的發揮集羣的優點。Spark 按照文件的大小
自動設置 map 任務數來處理每一個文件(固然你能夠經過設置 SparkContext.textFile
的可選參數來控制並行程度),而且對於分佈式的 reduce 操做,例如 groupByKey
和 reduceByKey 他們使用父 RDD 的最大分區數來設置並行數。你也能夠經過傳遞第
二個參數來控制並行級別(參考 spark.PairRDDFunctions 文檔)或者設置啓動時的
並行級別來改變默認值(spark.default.parallelism),咱們建議你爲每一個 CPU 分配
2-3 個並行任務。
Reduce 任務的內存分配
有時,你會收到一個 OOM 的錯誤,由於你的 RDD 超出了內存的大小,這有多是
由於某個任務的工做集太大形成的,例如 groupByKey 這個 reduce 任務。Spark 的
混洗操做(例如 sortByKey,groupByKey,reduceByKey,join 等)在每一個子任務中構造
並維護了一個哈希表,來完成歸類操做,一般狀況下此表很大。最簡單的修復方法是
提高並行程度,以便使每一個任務的輸入足夠小。Spark 能夠高效的支持短如 200ms 的
任務,由於它能夠跨多任務重用執行器的 JVM,而且它的任務加載和啓動開銷很是
小,所以你能夠放心的增長並行度,甚至能夠設置比集羣核心總數更多的並行任務
數。
廣播大變量
使用 SparkContext 的廣播功能極大的減小每一個序列化人物的大小,而且下降集羣中
任務加載的開銷。若是你的任務使用任何來自驅動器的大對象(例如一個靜態查找
表),應該考慮將這個大對象加載到廣播變量中去。Spark 能夠打印序列化的任務的
大小,所以你能夠經過查看輸出來判斷你的做業是否太大了;一般狀況下,一個大於
20KB 的對象是值得放進廣播變量中來進行優化的。
數據存放位置
數據的存放位置對於 Spark 做業的執行效率具備重要影響。若是數據和操做它的代碼
在一塊兒的話,計算將是很是高效的。可是,若是代碼和數據是分離的,一方須要移動
到另外一方那裏。顯而易見的是移動代碼比移動數據高效的多,由於代碼的字節數源小
於數據。Spark 是基於這一常規原則來構建她的數據存放策略的。
數據本地化是使得數據和在它之上的操做距離更近。這裏列舉了一些存放數據的級
別,這些級別的劃分是基於數據存儲位置的,按照由近及遠的順序。
·
·
PROCESS_LOCAL:數據和運行的代碼同時在 JVM 中。這是最好的存儲方
式。
NODE_LOCAL:數據和代碼在同一個節點上。實例在同一個節點的 HDFS
中,或者在相同節點的另外一個執行器裏。這是比 PROCESS_LOCAL 稍慢的級
別,由於數據須要在進程間傳遞。
·
·
NO_PREF:從任何地方訪問數據都很快,沒有位置偏好。
RACK_LOCAL:數據和代碼存在與同一個機架的服務器中。數據在同一個機架
的不一樣服務中,所以須要依靠網絡來傳遞這一數據,通常只需通過一個交換
器。
·
ANY:數據存儲在同一網絡環境,但不在同一機架上。
Spark 程序理想狀態是調度全部的子任務都處於最佳的存儲策略之下,但這一般只是
理想。
(海量Spark, hadoop資源共享請點擊: 498835267)