調優之前是將功能實現...然后算法優化,設計優化,再是spark調優!,需得一步一步來,不得直接越過,直接調優!
executor調優
對于exector的調優基于一個原則,那就是使用端口號界面看cpu的使用率.
在new SparkContext后,集群就會為在Worker上分配executor,如果executor對cpu使用率低的話,那就可以多增加幾個executor. 但是一套機器上的使用內存是不變的,executor越多,每個executor分配的內存就越少,從而會導致shuffle過程溢寫到磁盤上的文件過多,出現過多的數據spill over(溢出)甚至out of memory(內存不足)的情況.
每一次task只能處理一個partition的數據,這個值太小了會導致每片數據量太大,導致內存壓力,或者諸多executor的計算能力無法利用充分;但是如果太大了則會導致分片太多,執行效率降低。
調優方式1: 程序跑的過程中,發現Spark job有時候特別慢,查看cpu的利用率很低,可以嘗試減少每個executor占用cpu core的數量,增加并行executor的數量.配合增加切片,整體上提高cpu的利用率,從而提高性能.
調優方式2: 如果發現Spark job的時常發生內存溢出, 增加分片的數量,從而就可以減少每片數據的規模,能夠更多的task處理同樣的任務,減少executor的數量,這樣每個executor能夠分配到的內存就更大,相當于每個task的內存能夠更大的分配. 可能運行速度變慢了些,但是不會內存溢出了.
調優方式3: 如果數據量很少,有大量的小文件生成, 那就減少文件分片,使task整體數量減少,每個task能夠分到更大的內存.,處理更多的數據,就不會有大量的小文件生成.
頻繁GC或者OOM優化
頻繁GC(垃圾回收)或者OOM(內存溢出)在Driver端和Executor端分別有不同的處理方式
Driver端: 通常由于計算過大的結果集被回收到Driver端導致,需要調大Driver端的內存解決,或者進一步減少結果集的數量。
Executor端:
1. 以外部數據作為輸入的Stage,這類Stage中出現GC通常是因為在Map側進行map-side-combine時,由于group過多引起的。解決方法可以增加partition的數量(即task的數量)來減少每個task要處理的數據,來減少GC的可能性
2. 以shuffle作為輸入的Stage:這類Stage中出現GC的通常原因也是和shuffle有關,常見原因是某一個或多個group的數據過多,也就是所謂的數據傾斜,簡單的方法就是增加shuffle的數量.復雜點就在業務邏輯上調整,預先針對較大的group做單獨處理。
數據傾斜優化
注: 數據傾斜一般是代碼中的某些算子導致的
數據傾斜的物理本質
數據傾斜只會發生的shuffle階段,有時候會將各個節點相同的key拉取到某一個Task進行處理(比如join等操作),如果某個相同的key遠遠超過其他的key,那么這個Task所處理的數據量也就特別龐大,而spark作業的進度,也就是由最長的那個task決定的.
可能會誘發數據傾斜的算子
開發當中,不影響業務和效率的情況下,應當盡量避免以下算子:
distinct、groupByKey、reduceByKey、aggregateByKey、join、cogroup、repartition..
如何使用web ui看數據傾斜
通過Spark Web UI來查看當前stage各個task分配的數據量,進而確定是不是task的數據分配不均勻導致了數據傾斜
如圖上就是典型的數據傾斜, 有的task處理完數據,只要5s,共1000多個key,而有的task要處理完數據需要4分鐘,一萬多個key..
如何處理數據傾斜
1. 預處理數據源
有些數據源的數據,本身分布就是不均勻的,那就可以實現預處理,如比如數據源是hive的話,而hive本身的數據剛好不均勻,那就對Hive數據源進行ETL,再進行spark作業. 也就是講spark的shuffle操作提前到了Hive ETL中.
這種方法數據傾斜還是存在的,只不過發生在了spark處理之前
2. 增加stage中task的數量
一個stage中封裝了一組具有相同邏輯的task,默認是200個.我們可以通過設置spark.sql.shuffle.partitions來修改task的數量,比如增加為1000,此時,stage分配的內存也會增加,stage里面更多的的task能夠處理相同的key,這樣的操作能夠有效的緩解和減輕數據傾斜問題.
這種方法面對傾斜不是非常嚴重的情況,還是挺有用的,但是面對百萬,千萬key的話,就力不從心了
3.過濾一些導致傾斜的key
再某些場景下,某個key突然暴增,在對數據影響輕微的情況下,我們可以直接過濾這個key,讓他不參與計算.
這種方法完美規避數據傾斜,但是有可能是多個key導致的,那這個就不太好了.
4. 添加隨機前綴
對RDD執行reduceByKey等聚合類shuffle算子或者在Spark SQL中使用group by語句進行分組聚合時,比較適用這種方案
這個方法需要兩次聚合,第一次是局部聚合,先給每個key都打上一個隨機數,比如10以內的隨機數,此時原先一樣的key就變成不一樣的了,比如(hello, 1) (hello, 1) (hello, 1) (hello, 1),就會變成(1_hello, 1) (1_hello, 1) (2_hello, 1) (2_hello, 1)。接著對打上隨機數后的數據,執行reduceByKey等聚合操作,進行局部聚合,那么局部聚合結果,就會變成了(1_hello, 2) (2_hello, 2)。然后將各個key的前綴給去掉,就會變成(hello,2)(hello,2),再次進行全局聚合操作,就可以得到最終結果了,比如(hello, 4)。
將原本相同的key通過附加隨機前綴的方式,變成多個不同的key,就可以讓原本被一個task處理的數據分散到多個task上去做局部聚合,進而解決單個task處理數據量過多的問題。接著去除掉隨機前綴,再次進行全局聚合,就可以得到最終的結果.
這種方法很好,我在其他的博客上看到的,還沒實踐過,真心可以.不過僅限于聚合類
5.多種方案組合完成
就是將以上幾種方案一起組合起來使用,比如先預處理,在增加task,再增加前綴..當然,處理數據傾斜的方案肯定其他的,我也在不斷的學習!
程序開發調優
1. 避免創建重復的RDD
代碼如下: sc.textFile("hdfs://192.168.0.1:9000/hello.txt").map(...)
?????????????? ? sc.textFile("hdfs://192.168.0.1:9000/hello.txt").reduce(...)
對于同一份數據,我們只需要創建一份RDD來處理即可,如果對同一份數據創建了多個RDD,那就意味著spark作業會進行多次創建代表相同數據的RDD,并且對每一個RDD都重復計算,對spark作業的效率.內存.性能開銷都有明顯的影響
2. 盡可能復用同一個RDD
代碼如下: JavaPairRDD rdd1 = ...??????? JavaRDD rdd2 = rdd1.map(...)
???????????????? rdd1.reduceByKey(...)??????????? rdd1.map(tuple._2...)
針對不同的數據,但是不同的數據有重疊或者包含的關系,這樣的情景下,我們盡量復用同一個RDD,因為這樣可以盡可能地減少RDD的數量,從而盡可能減少算子執行的次數。
3. 對多次使用的RDD進行持久化
自動持久化: valrdd1= sc.textFile("hdfs://192.168.0.1:9000/666.txt").cache()
rdd1.map(...)???????? rdd1.reduce(...)???
//cache()使用非序列化的方式將RDD中的數據全部嘗試持久化到內存
手動持久化: valrdd1= sc.textFile("hdfs://192.168.0.1:9000/666.txt").persist(StorageLevel.MEMORY_AND_DISK_SER)
rdd1.map(...)??????? rdd1.reduce(...)
//prsist()使用指定的方式進行持久化,StorageLevel.MEMORY_AND_DISK_SER表示內存充足時有限持久化到內存中,不充足時持久化到磁盤中,后綴_SER表示使用序列化保存RDD數據
對多次使用的RDD進行持久化就是保證一個RDD執行多次算子操作時, 這個RDD本身僅僅被計算一次
對于多次使用的RDD進行持久化操作,Spark就會將數據保存到內存或磁盤中,下次使用此RDD時,就會從內存或磁盤中提取已持久化的rdd,而不需要將此RDD重新再計算一遍
4. 盡量避免使用shuffle算子
在上面開篇就提到過:在開發當中,不影響業務和效率的情況下,盡量避免使用shuffle類算子,這類算子包括distinct、groupByKey、reduceByKey、aggregateByKey、join、cogroup、repartition...
spark作業中最消耗性能的是shuffle,shuffle就是將key相同的數據,拉取到同一個task上進行join.reduceByKey等算子操作.這個過程中會有大量的磁盤IO,數據傳輸,內存消耗,對性能有很大的影響.
5. groupByKey算子替代
在要使用groupByKey算子的時候,盡量用reduceByKey或者aggregateByKey算子替代.因為調用groupByKey時候,按照相同的key進行分組,形成RDD[key,Iterable[value]]的形式,此時所有的鍵值對都將被重新洗牌,移動,對網絡數據傳輸造成理論上的最大影響.
而reduceByKey的話,他會先將本地的數據預聚合一次(通過reduceByKey的lambda函數,),就只有一條key了, 而進行shuffle操作,將所有相同的key拉取到同一個task上時,就可以減少極大的IO以及數據傳輸. 所以用reduceByKey或者aggregateByKey算子來替代掉groupByKey算子,這樣可以提前預聚合,從而提高性能.
6. 使用高性能的算子
像上面的reduceByKey或者aggregateByKey算子來替代掉groupByKey算子,這就是使用高性能算子,與此類似的還有
1) 使用mapPartitions替代普通map; mapPartitions算子,一次函數調用會處理一個partition的數據,而不是一次函數調用處理一條,性能也有大的提升. 但是一次處理一個partition的數據,就要一次處理一個partition的數據,如果內存不夠,就會出現OOM操作.
2) 使用foreachPartitions替代foreach; foreachPartitions也是一次調用處理一個partition的數據,而非foreach的一條,對性能也有大的提升,但是和上面一樣,也要注意內存的充足與否
3) filter之后進行coalesce操作; filter之后,RDD的每個partition中都會有很多數據被過濾掉,此時如果照常進行后續的計算,其實每個task處理的partition中的數據量并不是很多,有一點資源浪費,而且此時處理的task越多,可能速度反而越慢。因此用coalesce減少partition數量,將RDD中的數據壓縮到更少的partition之后,只要使用更少的task即可處理完所有的partition。這對spark的性能也有一定的穩定與幫助
4) repartitionAndSortWithinPartitions替代repartition與sort類操作; 如果需要在repartition重分區之后,還要進行排序, 建議直接使用repartitionAndSortWithinPartitions算子。因為該算子可以一邊進行重分區的shuffle操作,一邊進行排序。shuffle與sort兩個操作同時進行,比先shuffle再sort來說,性能也能優化不少.
7. 使用廣播大變量
代碼如下: val list1= ...
??????????????? rdd1.map(list1...)
??????????????? val list1= ...
??????????????? val list1 Broadcast=sc.broadcast(list1)
?????????????? rdd1.map(list1Broadcast...)
使用一般的外部變量,如果外部變量內存較大,會導致頻繁GC,這時候可以使用spark的廣播功能. 廣播后的變量,會保證每個Executor的內存中會有一份變量副本,此時task會共享它所在的Executor中的那個廣播變量. 從而減少網絡傳輸的性能開銷,并減少了副本的數量,減少對Executor內存的占用開銷,降低了GC的頻率。
8. 優化數據結構
Spark官方建議,在Spark編碼實現中,特別是對于算子函數中的代碼,盡量不要使用字符串.集合.對象三種數據結構.
盡量使用字符串代替對象,盡量用基本數據類型(Int,Long..)代替字符串,盡量舒勇數組代替集合,? 這樣可以盡可能減少內存占用,降低GC頻率,提高性能
運行資源調優
這一段我摘抄的...
前面的調優一直都提到了一個點,那就是內存的分配,資源的分配.不然會導致OOM,頻繁GC..? ?
1) num-executors
參數說明:該參數用于設置Spark作業總共要用多少個Executor進程來執行。Driver在向YARN集群管理器申請資源時,YARN集群管理器會盡可能按照你的設置來在集群的各個工作節點上,啟動相應數量的Executor進程。這個參數非常之重要,如果不設置的話,默認只會給你啟動少量的Executor進程,此時你的Spark作業的運行速度是非常慢的。
參數調優建議:每個Spark作業的運行一般設置50~100個左右的Executor進程比較合適,設置太少或太多的Executor進程都不好。設置的太少,無法充分利用集群資源;設置的太多的話,大部分隊列可能無法給予充分的資源。
2) executor-memory
參數說明:該參數用于設置每個Executor進程的內存。Executor內存的大小,很多時候直接決定了Spark作業的性能,而且跟常見的JVM OOM異常,也有直接的關聯。
參數調優建議:每個Executor進程的內存設置4G~8G較為合適。但是這只是一個參考值,具體的設置還是得根據不同部門的資源隊列來定??梢钥纯醋约簣F隊的資源隊列的最大內存限制是多少,num-executors乘以executor-memory,是不能超過隊列的最大內存量的。此外,如果你是跟團隊里其他人共享這個資源隊列,那么申請的內存量最好不要超過資源隊列最大總內存的1/3~1/2,避免你自己的Spark作業占用了隊列所有的資源,導致別的同學的作業無法運行。
3) executor-cores
參數說明:該參數用于設置每個Executor進程的CPU core數量。這個參數決定了每個Executor進程并行執行task線程的能力。因為每個CPU core同一時間只能執行一個task線程,因此每個Executor進程的CPU core數量越多,越能夠快速地執行完分配給自己的所有task線程。
參數調優建議:Executor的CPU core數量設置為2~4個較為合適。同樣得根據不同部門的資源隊列來定,可以看看自己的資源隊列的最大CPU core限制是多少,再依據設置的Executor數量,來決定每個Executor進程可以分配到幾個CPU core。同樣建議,如果是跟他人共享這個隊列,那么num-executors * executor-cores不要超過隊列總CPU core的1/3~1/2左右比較合適,也是避免影響其他同學的作業運行。
4) driver-memory
參數說明:該參數用于設置Driver進程的內存。
參數調優建議:Driver的內存通常來說不設置,或者設置1G左右應該就夠了。唯一需要注意的一點是,如果需要使用collect算子將RDD的數據全部拉取到Driver上進行處理,那么必須確保Driver的內存足夠大,否則會出現OOM內存溢出的問題。
5) spark.default.parallelism
參數說明:該參數用于設置每個stage的默認task數量。這個參數極為重要,如果不設置可能會直接影響你的Spark作業性能。
參數調優建議:Spark作業的默認task數量為500~1000個較為合適。很多同學常犯的一個錯誤就是不去設置這個參數,那么此時就會導致Spark自己根據底層HDFS的block數量來設置task的數量,默認是一個HDFS block對應一個task。通常來說,Spark默認設置的數量是偏少的(比如就幾十個task),如果task數量偏少的話,就會導致你前面設置好的Executor的參數都前功盡棄。試想一下,無論你的Executor進程有多少個,內存和CPU有多大,但是task只有1個或者10個,那么90%的Executor進程可能根本就沒有task執行,也就是白白浪費了資源!因此Spark官網建議的設置原則是,設置該參數為num-executors * executor-cores的2~3倍較為合適,比如Executor的總CPU core數量為300個,那么設置1000個task是可以的,此時可以充分地利用Spark集群的資源。
6) spark.storage.memoryFraction
參數說明:該參數用于設置RDD持久化數據在Executor內存中能占的比例,默認是0.6。也就是說,默認Executor 60%的內存,可以用來保存持久化的RDD數據。根據你選擇的不同的持久化策略,如果內存不夠時,可能數據就不會持久化,或者數據會寫入磁盤。
參數調優建議:如果Spark作業中,有較多的RDD持久化操作,該參數的值可以適當提高一些,保證持久化的數據能夠容納在內存中。避免內存不夠緩存所有的數據,導致數據只能寫入磁盤中,降低了性能。但是如果Spark作業中的shuffle類操作比較多,而持久化操作比較少,那么這個參數的值適當降低一些比較合適。此外,如果發現作業由于頻繁的gc導致運行緩慢(通過spark
web ui可以觀察到作業的gc耗時),意味著task執行用戶代碼的內存不夠用,那么同樣建議調低這個參數的值。
7) spark.shuffle.memoryFraction
參數說明:該參數用于設置shuffle過程中一個task拉取到上個stage的task的輸出后,進行聚合操作時能夠使用的Executor內存的比例,默認是0.2。也就是說,Executor默認只有20%的內存用來進行該操作。shuffle操作在進行聚合時,如果發現使用的內存超出了這個20%的限制,那么多余的數據就會溢寫到磁盤文件中去,此時就會極大地降低性能。
參數調優建議:如果Spark作業中的RDD持久化操作較少,shuffle操作較多時,建議降低持久化操作的內存占比,提高shuffle操作的內存占比比例,避免shuffle過程中數據過多時內存不夠用,必須溢寫到磁盤上,降低了性能。此外,如果發現作業由于頻繁的gc導致運行緩慢,意味著task執行用戶代碼的內存不夠用,那么同樣建議調低這個參數的值。