[整理] Spark性能優(yōu)化指南

Spark的性能調(diào)優(yōu)實(shí)際上是由很多部分組成的缤骨,不是調(diào)節(jié)幾個(gè)參數(shù)就可以立竿見(jiàn)影提升作業(yè)性能的身笤。我們需要根據(jù)不同的業(yè)務(wù)場(chǎng)景以及數(shù)據(jù)情況擦盾,對(duì)Spark作業(yè)進(jìn)行綜合性的分析鳄袍,然后進(jìn)行多個(gè)方面的調(diào)節(jié)和優(yōu)化窗怒,才能獲得最佳性能映跟。

一、開(kāi)發(fā)調(diào)優(yōu)

主要包括扬虚,RDD lineage設(shè)計(jì)努隙,算子的合理使用,特殊操作的優(yōu)化等辜昵。

原則一:避免創(chuàng)建重復(fù)的RDD

通常的spark作業(yè)流程: 基于某個(gè)數(shù)據(jù)源創(chuàng)建初始RDD荸镊,接著執(zhí)行某個(gè)算子操作,然后得到一個(gè)新的RDD;循環(huán)往復(fù)直到計(jì)算出最終需要的結(jié)果堪置。在這個(gè)過(guò)程中躬存,多個(gè)RDD會(huì)通過(guò)不同的算子操作(比如map、reduce等)串起來(lái)舀锨,這個(gè)“RDD串”岭洲,就是RDD lineage,也就是“RDD的血緣關(guān)系鏈”坎匿。

這個(gè)就不舉例了盾剩,很常見(jiàn)的一個(gè)原則,大部分spark程序除了在輸入源類型不同的情況下替蔬,輸入源一般不會(huì)重復(fù)創(chuàng)建告私。

原則二:盡可能復(fù)用同一個(gè)RDD

對(duì)于類似多個(gè)RDD的數(shù)據(jù)有重疊或者包含的情況,我們應(yīng)該盡量復(fù)用一個(gè)RDD承桥,這樣可以盡可能地減少RDD的數(shù)量驻粟,從而盡可能減少算子執(zhí)行的次數(shù)。

這部分我要強(qiáng)調(diào)一下凶异,因?yàn)樽约汗ぷ鳟?dāng)中也遇到了這種情況蜀撑,很常見(jiàn)的一種調(diào)優(yōu)手段.

比如我有個(gè)key-value類型的RDD格式如下:
rdd1: (user,app_type,flux_size)
還有個(gè)同類型的RDD,但是格式如下:
rdd1: (user,flux_size)

很明顯唠帝,后者對(duì)于前者是包含關(guān)系屯掖,只是格式不同而已, 此時(shí)會(huì)因?yàn)閷?duì)rdd1執(zhí)行map算子來(lái)創(chuàng)建rdd2,而多執(zhí)行一次算子操作襟衰,進(jìn)而增加性能開(kāi)銷贴铜。

// 錯(cuò)誤的做法。

// 有一個(gè)<Long, String>格式的RDD,即rdd1绍坝。
// 接著由于業(yè)務(wù)需要徘意,對(duì)rdd1執(zhí)行了一個(gè)map操作,創(chuàng)建了一個(gè)rdd2轩褐,而rdd2中的數(shù)據(jù)僅僅是rdd1中的value值而已椎咧,也就是說(shuō),rdd2是rdd1的子集把介。
JavaPairRDD<String, String ,long> rdd1 = ...
JavaRDD<String, long> rdd2 = rdd1.map(...)

// 分別對(duì)rdd1和rdd2執(zhí)行了不同的算子操作勤讽。
rdd1.reduceByKey(...)
rdd2.map(...)

// 正確的做法。

// 上面這個(gè)case中拗踢,其實(shí)rdd1和rdd2的區(qū)別無(wú)非就是數(shù)據(jù)格式不同而已脚牍,rdd2的數(shù)據(jù)完全就是rdd1的子集而已,卻創(chuàng)建了兩個(gè)rdd巢墅,并對(duì)兩個(gè)rdd都執(zhí)行了一次算子操作诸狭。
// 此時(shí)會(huì)因?yàn)閷?duì)rdd1執(zhí)行map算子來(lái)創(chuàng)建rdd2,而多執(zhí)行一次算子操作君纫,進(jìn)而增加性能開(kāi)銷驯遇。

// 其實(shí)在這種情況下完全可以復(fù)用同一個(gè)RDD。
// 我們可以使用rdd1蓄髓,既做reduceByKey操作叉庐,也做map操作。
// 在進(jìn)行第二個(gè)map操作時(shí)双吆,只使用每個(gè)數(shù)據(jù)的tuple._1和tuple._3眨唬,也就是rdd1中的user和value值,即可好乐。
JavaPairRDD<String, String> rdd1 = ...
rdd1.reduceByKey(...)
rdd1.map(tuple._1,tuple_3,...)

// 第二種方式相較于第一種方式而言,很明顯減少了一次rdd2的計(jì)算開(kāi)銷瓦宜。
// 但是到這里為止蔚万,優(yōu)化還沒(méi)有結(jié)束,對(duì)rdd1我們還是執(zhí)行了兩次算子操作临庇,rdd1實(shí)際上還是會(huì)被計(jì)算兩次反璃。
// 因此還需要配合“原則三:對(duì)多次使用的RDD進(jìn)行持久化”進(jìn)行使用,才能保證一個(gè)RDD被多次使用時(shí)只被計(jì)算一次假夺。

原則三:對(duì)多次使用的RDD進(jìn)行持久化

當(dāng)你在Spark代碼中多次對(duì)一個(gè)RDD做了算子操作后淮蜈,恭喜腺毫,你已經(jīng)實(shí)現(xiàn)Spark作業(yè)第一步的優(yōu)化了涉枫,也就是盡可能復(fù)用RDD。此時(shí)就該在這個(gè)基礎(chǔ)之上稚虎,進(jìn)行第二步優(yōu)化了,也就是要保證對(duì)一個(gè)RDD執(zhí)行多次算子操作時(shí)裁眯,這個(gè)RDD本身僅僅被計(jì)算一次鹉梨。

Spark中對(duì)于一個(gè)RDD執(zhí)行多次算子的默認(rèn)原理是這樣的:每次你對(duì)一個(gè)RDD執(zhí)行一個(gè)算子操作時(shí),都會(huì)重新從源頭處計(jì)算一遍穿稳,計(jì)算出那個(gè)RDD來(lái)存皂,然后再對(duì)這個(gè)RDD執(zhí)行你的算子操作。這種方式的性能是很差的逢艘。

對(duì)多次使用的RDD進(jìn)行持久化的代碼示例

// 如果要對(duì)一個(gè)RDD進(jìn)行持久化旦袋,只要對(duì)這個(gè)RDD調(diào)用cache()和persist()即可。

// 正確的做法它改。
// cache()方法表示:使用非序列化的方式將RDD中的數(shù)據(jù)全部嘗試持久化到內(nèi)存中疤孕。
// 此時(shí)再對(duì)rdd1執(zhí)行兩次算子操作時(shí),只有在第一次執(zhí)行map算子時(shí)搔课,才會(huì)將這個(gè)rdd1從源頭處計(jì)算一次胰柑。
// 第二次執(zhí)行reduce算子時(shí),就會(huì)直接從內(nèi)存中提取數(shù)據(jù)進(jìn)行計(jì)算爬泥,不會(huì)重復(fù)計(jì)算一個(gè)rdd柬讨。
val rdd1 = sc.textFile("hdfs://192.168.0.1:9000/hello.txt").cache()
rdd1.map(...)
rdd1.reduce(...)

// persist()方法表示:手動(dòng)選擇持久化級(jí)別,并使用指定的方式進(jìn)行持久化袍啡。
// 比如說(shuō)踩官,StorageLevel.MEMORY_AND_DISK_SER表示,內(nèi)存充足時(shí)優(yōu)先持久化到內(nèi)存中境输,內(nèi)存不充足時(shí)持久化到磁盤文件中蔗牡。
// 而且其中的_SER后綴表示,使用序列化的方式來(lái)保存RDD數(shù)據(jù)嗅剖,此時(shí)RDD中的每個(gè)partition都會(huì)序列化成一個(gè)大的字節(jié)數(shù)組辩越,然后再持久化到內(nèi)存或磁盤中。
// 序列化的方式可以減少持久化的數(shù)據(jù)對(duì)內(nèi)存/磁盤的占用量信粮,進(jìn)而避免內(nèi)存被持久化數(shù)據(jù)占用過(guò)多黔攒,從而發(fā)生頻繁GC。
val rdd1 = sc.textFile("hdfs://192.168.0.1:9000/hello.txt").persist(StorageLevel.MEMORY_AND_DISK_SER)
rdd1.map(...)
rdd1.reduce(...)

對(duì)于persist()方法而言强缘,我們可以根據(jù)不同的業(yè)務(wù)場(chǎng)景選擇不同的持久化級(jí)別督惰。

** Spark的持久化級(jí)別 **

持久化級(jí)別 含義解釋
vMEMORY_ONLY 使用未序列化的Java對(duì)象格式,將數(shù)據(jù)保存在內(nèi)存中旅掂。如果內(nèi)存不夠存放所有的數(shù)據(jù)赏胚,則數(shù)據(jù)可能就不會(huì)進(jìn)行持久化。那么下次對(duì)這個(gè)RDD執(zhí)行算子操作時(shí)商虐,那些沒(méi)有被持久化的數(shù)據(jù)觉阅,需要從源頭處重新計(jì)算一遍崖疤。這是默認(rèn)的持久化策略,使用cache()方法時(shí)留拾,實(shí)際就是使用的這種持久化策略戳晌。
MEMORY_AND_DISK 使用未序列化的Java對(duì)象格式,優(yōu)先嘗試將數(shù)據(jù)保存在內(nèi)存中痴柔。如果內(nèi)存不夠存放所有的數(shù)據(jù)沦偎,會(huì)將數(shù)據(jù)寫入磁盤文件中,下次對(duì)這個(gè)RDD執(zhí)行算子時(shí)咳蔚,持久化在磁盤文件中的數(shù)據(jù)會(huì)被讀取出來(lái)使用豪嚎。
MEMORY_ONLY_SER 基本含義同MEMORY_ONLY。唯一的區(qū)別是谈火,會(huì)將RDD中的數(shù)據(jù)進(jìn)行序列化侈询,RDD的每個(gè)partition會(huì)被序列化成一個(gè)字節(jié)數(shù)組。這種方式更加節(jié)省內(nèi)存糯耍,從而可以避免持久化的數(shù)據(jù)占用過(guò)多內(nèi)存導(dǎo)致頻繁GC扔字。
MEMORY_AND_DISK_SER 基本含義同MEMORY_AND_DISK。唯一的區(qū)別是温技,會(huì)將RDD中的數(shù)據(jù)進(jìn)行序列化革为,RDD的每個(gè)partition會(huì)被序列化成一個(gè)字節(jié)數(shù)組。這種方式更加節(jié)省內(nèi)存舵鳞,從而可以避免持久化的數(shù)據(jù)占用過(guò)多內(nèi)存導(dǎo)致頻繁GC震檩。
DISK_ONLY 使用未序列化的Java對(duì)象格式,將數(shù)據(jù)全部寫入磁盤文件中蜓堕。
MEMORY_ONLY_2, MEMORY_AND_DISK_2, 等等. 對(duì)于上述任意一種持久化策略抛虏,如果加上后綴_2,代表的是將每個(gè)持久化的數(shù)據(jù)套才,都復(fù)制一份副本迂猴,并將副本保存到其他節(jié)點(diǎn)上。這種基于副本的持久化機(jī)制主要用于進(jìn)行容錯(cuò)背伴。假如某個(gè)節(jié)點(diǎn)掛掉错忱,節(jié)點(diǎn)的內(nèi)存或磁盤中的持久化數(shù)據(jù)丟失了,那么后續(xù)對(duì)RDD計(jì)算時(shí)還可以使用該數(shù)據(jù)在其他節(jié)點(diǎn)上的副本挂据。如果沒(méi)有副本的話,就只能將這些數(shù)據(jù)從源頭處重新計(jì)算一遍了儿普。

** 如何選擇一種最合適的持久化策略 **

默認(rèn)情況下崎逃,性能最高的當(dāng)然是MEMORY_ONLY,但前提是你的內(nèi)存必須足夠足夠大眉孩,可以綽綽有余地存放下整個(gè)RDD的所有數(shù)據(jù)个绍。因?yàn)椴贿M(jìn)行序列化與反序列化操作勒葱,就避免了這部分的性能開(kāi)銷;對(duì)這個(gè)RDD的后續(xù)算子操作巴柿,都是基于純內(nèi)存中的數(shù)據(jù)的操作凛虽,不需要從磁盤文件中讀取數(shù)據(jù),性能也很高广恢;而且不需要復(fù)制一份數(shù)據(jù)副本凯旋,并遠(yuǎn)程傳送到其他節(jié)點(diǎn)上。但是這里必須要注意的是钉迷,在實(shí)際的生產(chǎn)環(huán)境中至非,恐怕能夠直接用這種策略的場(chǎng)景還是有限的,如果RDD中數(shù)據(jù)比較多時(shí)(比如幾十億)糠聪,直接用這種持久化級(jí)別荒椭,會(huì)導(dǎo)致JVM的OOM內(nèi)存溢出異常。

如果使用MEMORY_ONLY級(jí)別時(shí)發(fā)生了內(nèi)存溢出舰蟆,那么建議嘗試使用MEMORY_ONLY_SER級(jí)別趣惠。該級(jí)別會(huì)將RDD數(shù)據(jù)序列化后再保存在內(nèi)存中,此時(shí)每個(gè)partition僅僅是一個(gè)字節(jié)數(shù)組而已身害,大大減少了對(duì)象數(shù)量味悄,并降低了內(nèi)存占用。這種級(jí)別比MEMORY_ONLY多出來(lái)的性能開(kāi)銷题造,主要就是序列化與反序列化的開(kāi)銷傍菇。但是后續(xù)算子可以基于純內(nèi)存進(jìn)行操作,因此性能總體還是比較高的界赔。此外丢习,可能發(fā)生的問(wèn)題同上,如果RDD中的數(shù)據(jù)量過(guò)多的話淮悼,還是可能會(huì)導(dǎo)致OOM內(nèi)存溢出的異常咐低。

如果純內(nèi)存的級(jí)別都無(wú)法使用,那么建議使用MEMORY_AND_DISK_SER策略袜腥,而不是MEMORY_AND_DISK策略见擦。因?yàn)榧热坏搅诉@一步,就說(shuō)明RDD的數(shù)據(jù)量很大羹令,內(nèi)存無(wú)法完全放下鲤屡。序列化后的數(shù)據(jù)比較少,可以節(jié)省內(nèi)存和磁盤的空間開(kāi)銷福侈。同時(shí)該策略會(huì)優(yōu)先盡量嘗試將數(shù)據(jù)緩存在內(nèi)存中酒来,內(nèi)存緩存不下才會(huì)寫入磁盤。

通常不建議使用DISK_ONLY和后綴為_(kāi)2的級(jí)別:因?yàn)橥耆诖疟P文件進(jìn)行數(shù)據(jù)的讀寫堰汉,會(huì)導(dǎo)致性能急劇降低,有時(shí)還不如重新計(jì)算一次所有RDD滴铅。后綴為_(kāi)2的級(jí)別,必須將所有數(shù)據(jù)都復(fù)制一份副本就乓,并發(fā)送到其他節(jié)點(diǎn)上,數(shù)據(jù)復(fù)制以及網(wǎng)絡(luò)傳輸會(huì)導(dǎo)致較大的性能開(kāi)銷盹兢,除非是要求作業(yè)的高可用性,否則不建議使用守伸。

原則四:盡量避免使用shuffle類算子

如果有可能的話,要盡量避免使用shuffle類算子见芹。因?yàn)镾park作業(yè)運(yùn)行過(guò)程中玄呛,最消耗性能的地方就是shuffle過(guò)程和二。** shuffle過(guò)程惯吕,簡(jiǎn)單來(lái)說(shuō)废登,就是將分布在集群中多個(gè)節(jié)點(diǎn)上的同一個(gè)key,拉取到同一個(gè)節(jié)點(diǎn)上甲锡,進(jìn)行聚合或join等操作缤沦。比如reduceByKey疚俱、join等算子缩多,都會(huì)觸發(fā)shuffle操作衬吆。**

shuffle過(guò)程中,各個(gè)節(jié)點(diǎn)上的相同key都會(huì)先寫入本地磁盤文件中姆泻,然后其他節(jié)點(diǎn)需要通過(guò)網(wǎng)絡(luò)傳輸拉取各個(gè)節(jié)點(diǎn)上的磁盤文件中的相同key拇勃。而且相同key都拉取到同一個(gè)節(jié)點(diǎn)進(jìn)行聚合操作時(shí)方咆,還有可能會(huì)因?yàn)橐粋€(gè)節(jié)點(diǎn)上處理的key過(guò)多瓣赂,導(dǎo)致內(nèi)存不夠存放煌集,進(jìn)而溢寫到磁盤文件中苫纤。因此在shuffle過(guò)程中卷拘,可能會(huì)發(fā)生大量的磁盤文件讀寫的IO操作恭金,以及數(shù)據(jù)的網(wǎng)絡(luò)傳輸操作横腿。磁盤IO和網(wǎng)絡(luò)數(shù)據(jù)傳輸也是shuffle性能較差的主要原因斤寂。

因此在我們的開(kāi)發(fā)過(guò)程中遍搞,能避免則盡可能避免使用reduceByKey溪猿、join、distinct措左、repartition等會(huì)進(jìn)行shuffle的算子怎披,盡量使用map類的非shuffle算子凉逛。這樣的話状飞,沒(méi)有shuffle操作或者僅有較少shuffle操作的Spark作業(yè)昔瞧,可以大大減少性能開(kāi)銷自晰。

** Broadcast與map進(jìn)行join代碼示例 **

// 傳統(tǒng)的join操作會(huì)導(dǎo)致shuffle操作。
// 因?yàn)閮蓚€(gè)RDD中混巧,相同的key都需要通過(guò)網(wǎng)絡(luò)拉取到一個(gè)節(jié)點(diǎn)上勤揩,由一個(gè)task進(jìn)行join操作陨亡。
val rdd3 = rdd1.join(rdd2)

// Broadcast+map的join操作负蠕,不會(huì)導(dǎo)致shuffle操作遮糖。
// 使用Broadcast將一個(gè)數(shù)據(jù)量較小的RDD作為廣播變量。
val rdd2Data = rdd2.collect()
val rdd2DataBroadcast = sc.broadcast(rdd2Data)

// 在rdd1.map算子中芭概,可以從rdd2DataBroadcast中谈山,獲取rdd2的所有數(shù)據(jù)。
// 然后進(jìn)行遍歷臊诊,如果發(fā)現(xiàn)rdd2中某條數(shù)據(jù)的key與rdd1的當(dāng)前數(shù)據(jù)的key是相同的抓艳,那么就判定可以進(jìn)行join玷或。
// 此時(shí)就可以根據(jù)自己需要的方式偏友,將rdd1當(dāng)前數(shù)據(jù)與rdd2中可以連接的數(shù)據(jù)位他,拼接在一起(String或Tuple)鹅髓。
val rdd3 = rdd1.map(rdd2DataBroadcast...)

// 注意窿冯,以上操作醒串,建議僅僅在rdd2的數(shù)據(jù)量比較少(比如幾百M(fèi)厦凤,或者一兩G)的情況下使用较鼓。
// 因?yàn)槊總€(gè)Executor的內(nèi)存中香椎,都會(huì)駐留一份rdd2的全量數(shù)據(jù)禽篱。

原則五:使用map-side預(yù)聚合的shuffle操作

如果因?yàn)闃I(yè)務(wù)需要玛界,一定要使用shuffle操作慎框,無(wú)法用map類的算子來(lái)替代笨枯,那么盡量使用可以map-side預(yù)聚合的算子馅精。

所謂的map-side預(yù)聚合洲敢,說(shuō)的是在每個(gè)節(jié)點(diǎn)本地對(duì)相同的key進(jìn)行一次聚合操作沦疾,類似于MapReduce中的本地combiner哮塞。map-side預(yù)聚合之后忆畅,每個(gè)節(jié)點(diǎn)本地就只會(huì)有一條相同的key家凯,因?yàn)槎鄺l相同的key都被聚合起來(lái)了绊诲。其他節(jié)點(diǎn)在拉取所有節(jié)點(diǎn)上的相同key時(shí)掂之,就會(huì)大大減少需要拉取的數(shù)據(jù)數(shù)量世舰,從而也就減少了磁盤IO以及網(wǎng)絡(luò)傳輸開(kāi)銷跟压。通常來(lái)說(shuō)茸塞,在可能的情況下翔横,建議使用reduceByKey或者aggregateByKey算子來(lái)替代掉groupByKey算子。 因?yàn)閞educeByKey和aggregateByKey算子都會(huì)使用用戶自定義的函數(shù)對(duì)每個(gè)節(jié)點(diǎn)本地的相同key進(jìn)行預(yù)聚合无切。而groupByKey算子是不會(huì)進(jìn)行預(yù)聚合的哆键,全量的數(shù)據(jù)會(huì)在集群的各個(gè)節(jié)點(diǎn)之間分發(fā)和傳輸籍嘹,性能相對(duì)來(lái)說(shuō)比較差。

比如如下兩幅圖颂碘,就是典型的例子头岔,分別基于reduceByKey和groupByKey進(jìn)行單詞計(jì)數(shù)峡竣。其中第一張圖是groupByKey的原理圖适掰,可以看到攻谁,沒(méi)有進(jìn)行任何本地聚合時(shí)戚宦,所有數(shù)據(jù)都會(huì)在集群節(jié)點(diǎn)之間傳輸垦搬;第二張圖是reduceByKey的原理圖猴贰,可以看到米绕,每個(gè)節(jié)點(diǎn)本地的相同key數(shù)據(jù)栅干,都進(jìn)行了預(yù)聚合,然后才傳輸?shù)狡渌?jié)點(diǎn)上進(jìn)行全局聚合窿给。


groupByKey實(shí)現(xiàn)wordcount原理

reduceByKey實(shí)現(xiàn)wordcount原理

原則六:使用高性能的算子

除了shuffle相關(guān)的算子有優(yōu)化原則之外,其他的算子也都有著相應(yīng)的優(yōu)化原則允华。

  1. 使用reduceByKey/aggregateByKey替代groupByKey
    詳情見(jiàn)“原則五:使用map-side預(yù)聚合的shuffle操作”靴寂。

  2. 使用mapPartitions替代普通map
    mapPartitions類的算子,一次函數(shù)調(diào)用會(huì)處理一個(gè)partition所有的數(shù)據(jù)剖踊,而不是一次函數(shù)調(diào)用處理一條德澈,性能相對(duì)來(lái)說(shuō)會(huì)高一些梆造。但是有的時(shí)候屡穗,使用mapPartitions會(huì)出現(xiàn)OOM(內(nèi)存溢出)的問(wèn)題村砂。因?yàn)閱未魏瘮?shù)調(diào)用就要處理掉一個(gè)partition所有的數(shù)據(jù)础废,如果內(nèi)存不夠色迂,垃圾回收時(shí)是無(wú)法回收掉太多對(duì)象的图张,很可能出現(xiàn)OOM異常祸轮。所以使用這類操作時(shí)要慎重适袜!

  3. 使用foreachPartitions替代foreach
    原理類似于“使用mapPartitions替代map”售貌,也是一次函數(shù)調(diào)用處理一個(gè)partition的所有數(shù)據(jù)颂跨,而不是一次函數(shù)調(diào)用處理一條數(shù)據(jù)恒削。在實(shí)踐中發(fā)現(xiàn)躯砰,foreachPartitions類的算子琢歇,對(duì)性能的提升還是很有幫助的。比如在foreach函數(shù)中涌矢,將RDD中所有數(shù)據(jù)寫MySQL娜庇,那么如果是普通的foreach算子名秀,就會(huì)一條數(shù)據(jù)一條數(shù)據(jù)地寫,每次函數(shù)調(diào)用可能就會(huì)創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)連接汁掠,此時(shí)就勢(shì)必會(huì)頻繁地創(chuàng)建和銷毀數(shù)據(jù)庫(kù)連接考阱,性能是非常低下;但是如果用foreachPartitions算子一次性處理一個(gè)partition的數(shù)據(jù)吃既,那么對(duì)于每個(gè)partition跨细,只要?jiǎng)?chuàng)建一個(gè)數(shù)據(jù)庫(kù)連接即可态秧,然后執(zhí)行批量插入操作,此時(shí)性能是比較高的扼鞋。實(shí)踐中發(fā)現(xiàn)申鱼,對(duì)于1萬(wàn)條左右的數(shù)據(jù)量寫MySQL愤诱,性能可以提升30%以上。

  4. 使用filter之后進(jìn)行coalesce操作
    通常對(duì)一個(gè)RDD執(zhí)行filter算子過(guò)濾掉RDD中較多數(shù)據(jù)后(比如30%以上的數(shù)據(jù))淫半,建議使用coalesce算子,手動(dòng)減少RDD的partition數(shù)量匣砖,將RDD中的數(shù)據(jù)壓縮到更少的partition中去科吭。因?yàn)閒ilter之后,RDD的每個(gè)partition中都會(huì)有很多數(shù)據(jù)被過(guò)濾掉猴鲫,此時(shí)如果照常進(jìn)行后續(xù)的計(jì)算对人,其實(shí)每個(gè)task處理的partition中的數(shù)據(jù)量并不是很多,有一點(diǎn)資源浪費(fèi)拂共,而且此時(shí)處理的task越多牺弄,可能速度反而越慢。因此用coalesce減少partition數(shù)量宜狐,將RDD中的數(shù)據(jù)壓縮到更少的partition之后势告,只要使用更少的task即可處理完所有的partition。在某些場(chǎng)景下抚恒,對(duì)于性能的提升會(huì)有一定的幫助咱台。

  5. 使用repartitionAndSortWithinPartitions替代repartition與sort類操作
    repartitionAndSortWithinPartitions是Spark官網(wǎng)推薦的一個(gè)算子,官方建議俭驮,如果需要在repartition重分區(qū)之后回溺,還要進(jìn)行排序,建議直接使用repartitionAndSortWithinPartitions算子混萝。因?yàn)樵撍阕涌梢砸贿呥M(jìn)行重分區(qū)的shuffle操作馅而,一邊進(jìn)行排序。shuffle與sort兩個(gè)操作同時(shí)進(jìn)行譬圣,比先shuffle再sort來(lái)說(shuō),性能可能是要高的雄坪。

原則七:廣播大變量

有時(shí)在開(kāi)發(fā)過(guò)程中厘熟,會(huì)遇到需要在算子函數(shù)中使用外部變量的場(chǎng)景(尤其是大變量,比如100M以上的大集合)维哈,那么此時(shí)就應(yīng)該使用Spark的廣播(Broadcast)功能來(lái)提升性能绳姨。

在算子函數(shù)中使用到外部變量時(shí),默認(rèn)情況下阔挠,Spark會(huì)將該變量復(fù)制多個(gè)副本飘庄,通過(guò)網(wǎng)絡(luò)傳輸?shù)絫ask中,此時(shí)每個(gè)task都有一個(gè)變量副本购撼。如果變量本身比較大的話(比如100M跪削,甚至1G)谴仙,那么大量的變量副本在網(wǎng)絡(luò)中傳輸?shù)男阅荛_(kāi)銷,以及在各個(gè)節(jié)點(diǎn)的Executor中占用過(guò)多內(nèi)存導(dǎo)致的頻繁GC碾盐,都會(huì)極大地影響性能晃跺。

因此對(duì)于上述情況,如果使用的外部變量比較大毫玖,建議使用Spark的廣播功能掀虎,對(duì)該變量進(jìn)行廣播。廣播后的變量付枫,會(huì)保證每個(gè)Executor的內(nèi)存中烹玉,只駐留一份變量副本,而Executor中的task執(zhí)行時(shí)共享該Executor中的那份變量副本阐滩。這樣的話二打,可以大大減少變量副本的數(shù)量狰右,從而減少網(wǎng)絡(luò)傳輸?shù)男阅荛_(kāi)銷抢野,并減少對(duì)Executor內(nèi)存的占用開(kāi)銷浮定,降低GC的頻率叔锐。

** 廣播大變量的代碼示例 **

// 以下代碼在算子函數(shù)中廊驼,使用了外部的變量很魂。
// 此時(shí)沒(méi)有做任何特殊操作样悟,每個(gè)task都會(huì)有一份list1的副本隙弛。
val list1 = ...
rdd1.map(list1...)

// 以下代碼將list1封裝成了Broadcast類型的廣播變量饱溢。
// 在算子函數(shù)中喧伞,使用廣播變量時(shí),首先會(huì)判斷當(dāng)前task所在Executor內(nèi)存中绩郎,是否有變量副本潘鲫。
// 如果有則直接使用;如果沒(méi)有則從Driver或者其他Executor節(jié)點(diǎn)上遠(yuǎn)程拉取一份放到本地Executor內(nèi)存中肋杖。
// 每個(gè)Executor內(nèi)存中溉仑,就只會(huì)駐留一份廣播變量副本。
val list1 = ...
val list1Broadcast = sc.broadcast(list1)
rdd1.map(list1Broadcast...)

原則八:使用Kryo優(yōu)化序列化性能

在Spark中状植,主要有三個(gè)地方涉及到了序列化:

  • 在算子函數(shù)中使用到外部變量時(shí)浊竟,該變量會(huì)被序列化后進(jìn)行網(wǎng)絡(luò)傳輸(見(jiàn)“原則七:廣播大變量”中的講解)。

  • 將自定義的類型作為RDD的泛型類型時(shí)(比如JavaRDD津畸,Student是自定義類型)振定,所有自定義類型對(duì)象,都會(huì)進(jìn)行序列化肉拓。因此這種情況下后频,也要求自定義的類必須實(shí)現(xiàn)Serializable接口。

  • 使用可序列化的持久化策略時(shí)(比如MEMORY_ONLY_SER)暖途,Spark會(huì)將RDD中的每個(gè)partition都序列化成一個(gè)大的字節(jié)數(shù)組卑惜。

對(duì)于這三種出現(xiàn)序列化的地方膏执,我們都可以通過(guò)使用Kryo序列化類庫(kù),來(lái)優(yōu)化序列化和反序列化的性能残揉。Spark默認(rèn)使用的是Java的序列化機(jī)制胧后,也就是ObjectOutputStream/ObjectInputStream API來(lái)進(jìn)行序列化和反序列化。但是Spark同時(shí)支持使用Kryo序列化庫(kù)抱环,Kryo序列化類庫(kù)的性能比Java序列化類庫(kù)的性能要高很多壳快。官方介紹,Kryo序列化機(jī)制比Java序列化機(jī)制镇草,性能高10倍左右眶痰。Spark之所以默認(rèn)沒(méi)有使用Kryo作為序列化類庫(kù),是因?yàn)镵ryo要求最好要注冊(cè)所有需要進(jìn)行序列化的自定義類型梯啤,因此對(duì)于開(kāi)發(fā)者來(lái)說(shuō)竖伯,這種方式比較麻煩。

以下是使用Kryo的代碼示例因宇,我們只要設(shè)置序列化類七婴,再注冊(cè)要序列化的自定義類型即可(比如算子函數(shù)中使用到的外部變量類型、作為RDD泛型類型的自定義類型等):

// 創(chuàng)建SparkConf對(duì)象察滑。
val conf = new SparkConf().setMaster(...).setAppName(...)
// 設(shè)置序列化器為KryoSerializer打厘。
conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
// 注冊(cè)要序列化的自定義類型。
conf.registerKryoClasses(Array(classOf[MyClass1], classOf[MyClass2]))

原則九:優(yōu)化數(shù)據(jù)結(jié)構(gòu)

Java中贺辰,有三種類型比較耗費(fèi)內(nèi)存:

  1. 對(duì)象户盯,每個(gè)Java對(duì)象都有對(duì)象頭、引用等額外的信息饲化,因此比較占用內(nèi)存空間莽鸭。
  2. 字符串,每個(gè)字符串內(nèi)部都有一個(gè)字符數(shù)組以及長(zhǎng)度等額外信息吃靠。
  3. 集合類型硫眨,比如HashMap、LinkedList等巢块,因?yàn)榧项愋蛢?nèi)部通常會(huì)使用一些內(nèi)部類來(lái)封裝集合元素礁阁,比如Map.Entry。

因此Spark官方建議夕冲,在Spark編碼實(shí)現(xiàn)中,特別是對(duì)于算子函數(shù)中的代碼裂逐,盡量不要使用上述三種數(shù)據(jù)結(jié)構(gòu)歹鱼,盡量使用字符串替代對(duì)象,使用原始類型(比如Int卜高、Long)替代字符串弥姻,使用數(shù)組替代集合類型南片,這樣盡可能地減少內(nèi)存占用,從而降低GC頻率庭敦,提升性能疼进。

但是在筆者的編碼實(shí)踐中發(fā)現(xiàn),要做到該原則其實(shí)并不容易秧廉。因?yàn)槲覀兺瑫r(shí)要考慮到代碼的可維護(hù)性伞广,如果一個(gè)代碼中,完全沒(méi)有任何對(duì)象抽象疼电,全部是字符串拼接的方式嚼锄,那么對(duì)于后續(xù)的代碼維護(hù)和修改,無(wú)疑是一場(chǎng)巨大的災(zāi)難蔽豺。同理区丑,如果所有操作都基于數(shù)組實(shí)現(xiàn),而不使用HashMap修陡、LinkedList等集合類型沧侥,那么對(duì)于我們的編碼難度以及代碼可維護(hù)性,也是一個(gè)極大的挑戰(zhàn)魄鸦。因此筆者建議宴杀,在可能以及合適的情況下,使用占用內(nèi)存較少的數(shù)據(jù)結(jié)構(gòu)号杏,但是前提是要保證代碼的可維護(hù)性婴氮。

資源調(diào)優(yōu)

調(diào)優(yōu)概述

在開(kāi)發(fā)完Spark作業(yè)之后,就該為作業(yè)配置合適的資源了盾致。Spark的資源參數(shù)主经,基本都可以在spark-submit命令中作為參數(shù)設(shè)置。很多Spark初學(xué)者庭惜,通常不知道該設(shè)置哪些必要的參數(shù)罩驻,以及如何設(shè)置這些參數(shù),最后就只能胡亂設(shè)置护赊,甚至壓根兒不設(shè)置惠遏。資源參數(shù)設(shè)置的不合理,可能會(huì)導(dǎo)致沒(méi)有充分利用集群資源骏啰,作業(yè)運(yùn)行會(huì)極其緩慢节吮;或者設(shè)置的資源過(guò)大,隊(duì)列沒(méi)有足夠的資源來(lái)提供判耕,進(jìn)而導(dǎo)致各種異常透绩。總之,無(wú)論是哪種情況帚豪,都會(huì)導(dǎo)致Spark作業(yè)的運(yùn)行效率低下碳竟,甚至根本無(wú)法運(yùn)行。因此我們必須對(duì)Spark作業(yè)的資源使用原理有一個(gè)清晰的認(rèn)識(shí)狸臣,并知道在Spark作業(yè)運(yùn)行過(guò)程中莹桅,有哪些資源參數(shù)是可以設(shè)置的,以及如何設(shè)置合適的參數(shù)值烛亦。

Spark作業(yè)基本運(yùn)行原理

Spark基本運(yùn)行原理

詳細(xì)原理見(jiàn)上圖诈泼。我們使用spark-submit提交一個(gè)Spark作業(yè)之后,這個(gè)作業(yè)就會(huì)啟動(dòng)一個(gè)對(duì)應(yīng)的Driver進(jìn)程此洲。根據(jù)你使用的部署模式(deploy-mode)不同厂汗,Driver進(jìn)程可能在本地啟動(dòng),也可能在集群中某個(gè)工作節(jié)點(diǎn)上啟動(dòng)呜师。Driver進(jìn)程本身會(huì)根據(jù)我們?cè)O(shè)置的參數(shù)娶桦,占有一定數(shù)量的內(nèi)存和CPU core。而Driver進(jìn)程要做的第一件事情汁汗,就是向集群管理器(可以是Spark Standalone集群衷畦,也可以是其他的資源管理集群,美團(tuán)?大眾點(diǎn)評(píng)使用的是YARN作為資源管理集群)申請(qǐng)運(yùn)行Spark作業(yè)需要使用的資源知牌,這里的資源指的就是Executor進(jìn)程祈争。YARN集群管理器會(huì)根據(jù)我們?yōu)镾park作業(yè)設(shè)置的資源參數(shù),在各個(gè)工作節(jié)點(diǎn)上角寸,啟動(dòng)一定數(shù)量的Executor進(jìn)程菩混,每個(gè)Executor進(jìn)程都占有一定數(shù)量的內(nèi)存和CPU core。

在申請(qǐng)到了作業(yè)執(zhí)行所需的資源之后扁藕,Driver進(jìn)程就會(huì)開(kāi)始調(diào)度和執(zhí)行我們編寫的作業(yè)代碼了沮峡。Driver進(jìn)程會(huì)將我們編寫的Spark作業(yè)代碼分拆為多個(gè)stage,每個(gè)stage執(zhí)行一部分代碼片段亿柑,并為每個(gè)stage創(chuàng)建一批task邢疙,然后將這些task分配到各個(gè)Executor進(jìn)程中執(zhí)行。task是最小的計(jì)算單元望薄,負(fù)責(zé)執(zhí)行一模一樣的計(jì)算邏輯(也就是我們自己編寫的某個(gè)代碼片段)疟游,只是每個(gè)task處理的數(shù)據(jù)不同而已。一個(gè)stage的所有task都執(zhí)行完畢之后痕支,會(huì)在各個(gè)節(jié)點(diǎn)本地的磁盤文件中寫入計(jì)算中間結(jié)果颁虐,然后Driver就會(huì)調(diào)度運(yùn)行下一個(gè)stage。下一個(gè)stage的task的輸入數(shù)據(jù)就是上一個(gè)stage輸出的中間結(jié)果卧须。如此循環(huán)往復(fù)另绩,直到將我們自己編寫的代碼邏輯全部執(zhí)行完瞬痘,并且計(jì)算完所有的數(shù)據(jù),得到我們想要的結(jié)果為止板熊。

Spark是根據(jù)shuffle類算子來(lái)進(jìn)行stage的劃分。如果我們的代碼中執(zhí)行了某個(gè)shuffle類算子(比如reduceByKey察绷、join等)干签,那么就會(huì)在該算子處,劃分出一個(gè)stage界限來(lái)拆撼∪堇停可以大致理解為,shuffle算子執(zhí)行之前的代碼會(huì)被劃分為一個(gè)stage闸度,shuffle算子執(zhí)行以及之后的代碼會(huì)被劃分為下一個(gè)stage竭贩。因此一個(gè)stage剛開(kāi)始執(zhí)行的時(shí)候,它的每個(gè)task可能都會(huì)從上一個(gè)stage的task所在的節(jié)點(diǎn)莺禁,去通過(guò)網(wǎng)絡(luò)傳輸拉取需要自己處理的所有key留量,然后對(duì)拉取到的所有相同的key使用我們自己編寫的算子函數(shù)執(zhí)行聚合操作(比如reduceByKey()算子接收的函數(shù))。這個(gè)過(guò)程就是shuffle哟冬。

當(dāng)我們?cè)诖a中執(zhí)行了cache/persist等持久化操作時(shí)楼熄,根據(jù)我們選擇的持久化級(jí)別的不同,每個(gè)task計(jì)算出來(lái)的數(shù)據(jù)也會(huì)保存到Executor進(jìn)程的內(nèi)存或者所在節(jié)點(diǎn)的磁盤文件中浩峡。

因此Executor的內(nèi)存主要分為三塊:第一塊是讓task執(zhí)行我們自己編寫的代碼時(shí)使用可岂,默認(rèn)是占Executor總內(nèi)存的20%;第二塊是讓task通過(guò)shuffle過(guò)程拉取了上一個(gè)stage的task的輸出后翰灾,進(jìn)行聚合等操作時(shí)使用,默認(rèn)也是占Executor總內(nèi)存的20%;第三塊是讓RDD持久化時(shí)使用香府,默認(rèn)占Executor總內(nèi)存的60%屎慢。

task的執(zhí)行速度是跟每個(gè)Executor進(jìn)程的CPU core數(shù)量有直接關(guān)系的。一個(gè)CPU core同一時(shí)間只能執(zhí)行一個(gè)線程萎馅。而每個(gè)Executor進(jìn)程上分配到的多個(gè)task双戳,都是以每個(gè)task一條線程的方式,多線程并發(fā)運(yùn)行的糜芳。如果CPU core數(shù)量比較充足飒货,而且分配到的task數(shù)量比較合理,那么通常來(lái)說(shuō)峭竣,可以比較快速和高效地執(zhí)行完這些task線程塘辅。

以上就是Spark作業(yè)的基本運(yùn)行原理的說(shuō)明,大家可以結(jié)合上圖來(lái)理解皆撩。理解作業(yè)基本原理扣墩,是我們進(jìn)行資源參數(shù)調(diào)優(yōu)的基本前提哲银。

資源參數(shù)調(diào)優(yōu)

了解完了Spark作業(yè)運(yùn)行的基本原理之后,對(duì)資源相關(guān)的參數(shù)就容易理解了呻惕。所謂的Spark資源參數(shù)調(diào)優(yōu)荆责,其實(shí)主要就是對(duì)Spark運(yùn)行過(guò)程中各個(gè)使用資源的地方,通過(guò)調(diào)節(jié)各種參數(shù)亚脆,來(lái)優(yōu)化資源使用的效率做院,從而提升Spark作業(yè)的執(zhí)行性能。以下參數(shù)就是Spark中主要的資源參數(shù)濒持,每個(gè)參數(shù)都對(duì)應(yīng)著作業(yè)運(yùn)行原理中的某個(gè)部分键耕,我們同時(shí)也給出了一個(gè)調(diào)優(yōu)的參考值。

num-executors

  • 參數(shù)說(shuō)明:該參數(shù)用于設(shè)置Spark作業(yè)總共要用多少個(gè)Executor進(jìn)程來(lái)執(zhí)行柑营。Driver在向YARN集群管理器申請(qǐng)資源時(shí)屈雄,YARN集群管理器會(huì)盡可能按照你的設(shè)置來(lái)在集群的各個(gè)工作節(jié)點(diǎn)上,啟動(dòng)相應(yīng)數(shù)量的Executor進(jìn)程官套。這個(gè)參數(shù)非常之重要酒奶,如果不設(shè)置的話,默認(rèn)只會(huì)給你啟動(dòng)少量的Executor進(jìn)程奶赔,此時(shí)你的Spark作業(yè)的運(yùn)行速度是非常慢的讥蟆。
  • 參數(shù)調(diào)優(yōu)建議:** 每個(gè)Spark作業(yè)的運(yùn)行一般設(shè)置50~100個(gè)左右的Executor進(jìn)程比較合適,設(shè)置太少或太多的Executor進(jìn)程都不好纺阔。設(shè)置的太少瘸彤,無(wú)法充分利用集群資源;設(shè)置的太多的話笛钝,大部分隊(duì)列可能無(wú)法給予充分的資源质况。**

executor-memory

  • 參數(shù)說(shuō)明:該參數(shù)用于設(shè)置每個(gè)Executor進(jìn)程的內(nèi)存。Executor內(nèi)存的大小玻靡,很多時(shí)候直接決定了Spark作業(yè)的性能结榄,而且跟常見(jiàn)的JVM OOM異常,也有直接的關(guān)聯(lián)囤捻。
  • 參數(shù)調(diào)優(yōu)建議:** 每個(gè)Executor進(jìn)程的內(nèi)存設(shè)置4G8G較為合適臼朗。但是這只是一個(gè)參考值,具體的設(shè)置還是得根據(jù)不同部門的資源隊(duì)列來(lái)定蝎土∈友疲可以看看自己團(tuán)隊(duì)的資源隊(duì)列的最大內(nèi)存限制是多少,num-executors乘以executor-memory誊涯,是不能超過(guò)隊(duì)列的最大內(nèi)存量的挡毅。此外,如果你是跟團(tuán)隊(duì)里其他人共享這個(gè)資源隊(duì)列暴构,那么申請(qǐng)的內(nèi)存量最好不要超過(guò)資源隊(duì)列最大總內(nèi)存的1/31/2跪呈,避免你自己的Spark作業(yè)占用了隊(duì)列所有的資源段磨,導(dǎo)致別的同學(xué)的作業(yè)無(wú)法運(yùn)行。**

executor-cores

  • 參數(shù)說(shuō)明:該參數(shù)用于設(shè)置每個(gè)Executor進(jìn)程的CPU core數(shù)量耗绿。這個(gè)參數(shù)決定了每個(gè)Executor進(jìn)程并行執(zhí)行task線程的能力苹支。因?yàn)槊總€(gè)CPU core同一時(shí)間只能執(zhí)行一個(gè)task線程,因此每個(gè)Executor進(jìn)程的CPU core數(shù)量越多误阻,越能夠快速地執(zhí)行完分配給自己的所有task線程沐序。
  • 參數(shù)調(diào)優(yōu)建議:** Executor的CPU core數(shù)量設(shè)置為2~4個(gè)較為合適。同樣得根據(jù)不同部門的資源隊(duì)列來(lái)定堕绩,可以看看自己的資源隊(duì)列的最大CPU core限制是多少,再依據(jù)設(shè)置的Executor數(shù)量邑时,來(lái)決定每個(gè)Executor進(jìn)程可以分配到幾個(gè)CPU core奴紧。同樣建議,如果是跟他人共享這個(gè)隊(duì)列晶丘,那么num-executors * executor-cores不要超過(guò)隊(duì)列總CPU core1/3~1/2左右比較合適黍氮,也是避免影響其他同學(xué)的作業(yè)運(yùn)行。**

driver-memory

  • 參數(shù)說(shuō)明:該參數(shù)用于設(shè)置Driver進(jìn)程的內(nèi)存浅浮。
  • 參數(shù)調(diào)優(yōu)建議:Driver的內(nèi)存通常來(lái)說(shuō)不設(shè)置沫浆,或者設(shè)置1G左右應(yīng)該就夠了。唯一需要注意的一點(diǎn)是滚秩,如果需要使用collect算子將RDD的數(shù)據(jù)全部拉取到Driver上進(jìn)行處理专执,那么必須確保Driver的內(nèi)存足夠大,否則會(huì)出現(xiàn)OOM內(nèi)存溢出的問(wèn)題郁油。

spark.default.parallelism

  • 參數(shù)說(shuō)明:該參數(shù)用于設(shè)置每個(gè)stage的默認(rèn)task數(shù)量本股。這個(gè)參數(shù)極為重要,如果不設(shè)置可能會(huì)直接影響你的Spark作業(yè)性能桐腌。
  • 參數(shù)調(diào)優(yōu)建議:Spark作業(yè)的默認(rèn)task數(shù)量為500~1000個(gè)較為合適拄显。很多同學(xué)常犯的一個(gè)錯(cuò)誤就是不去設(shè)置這個(gè)參數(shù),那么此時(shí)就會(huì)導(dǎo)致Spark自己根據(jù)底層HDFS的block數(shù)量來(lái)設(shè)置task的數(shù)量案站,默認(rèn)是一個(gè)HDFS block對(duì)應(yīng)一個(gè)task躬审。通常來(lái)說(shuō),Spark默認(rèn)設(shè)置的數(shù)量是偏少的(比如就幾十個(gè)task)蟆盐,如果task數(shù)量偏少的話承边,就會(huì)導(dǎo)致你前面設(shè)置好的Executor的參數(shù)都前功盡棄。試想一下石挂,無(wú)論你的Executor進(jìn)程有多少個(gè)炒刁,內(nèi)存和CPU有多大,但是task只有1個(gè)或者10個(gè)誊稚,那么90%的Executor進(jìn)程可能根本就沒(méi)有task執(zhí)行翔始,也就是白白浪費(fèi)了資源罗心!因此Spark官網(wǎng)建議的設(shè)置原則是,設(shè)置該參數(shù)為num-executors * executor-cores的2~3倍較為合適城瞎,比如Executor的總CPU core數(shù)量為300個(gè)渤闷,那么設(shè)置1000個(gè)task是可以的,此時(shí)可以充分地利用Spark集群的資源脖镀。

spark.storage.memoryFraction

  • 參數(shù)說(shuō)明:該參數(shù)用于設(shè)置RDD持久化數(shù)據(jù)在Executor內(nèi)存中能占的比例飒箭,默認(rèn)是0.6。也就是說(shuō)蜒灰,默認(rèn)Executor 60%的內(nèi)存弦蹂,可以用來(lái)保存持久化的RDD數(shù)據(jù)。根據(jù)你選擇的不同的持久化策略强窖,如果內(nèi)存不夠時(shí)凸椿,可能數(shù)據(jù)就不會(huì)持久化,或者數(shù)據(jù)會(huì)寫入磁盤翅溺。
  • 參數(shù)調(diào)優(yōu)建議:如果Spark作業(yè)中脑漫,有較多的RDD持久化操作,該參數(shù)的值可以適當(dāng)提高一些咙崎,保證持久化的數(shù)據(jù)能夠容納在內(nèi)存中优幸。避免內(nèi)存不夠緩存所有的數(shù)據(jù),導(dǎo)致數(shù)據(jù)只能寫入磁盤中褪猛,降低了性能网杆。但是如果Spark作業(yè)中的shuffle類操作比較多,而持久化操作比較少伊滋,那么這個(gè)參數(shù)的值適當(dāng)降低一些比較合適跛璧。此外,如果發(fā)現(xiàn)作業(yè)由于頻繁的gc導(dǎo)致運(yùn)行緩慢(通過(guò)spark web ui可以觀察到作業(yè)的gc耗時(shí))新啼,意味著task執(zhí)行用戶代碼的內(nèi)存不夠用追城,那么同樣建議調(diào)低這個(gè)參數(shù)的值。

spark.shuffle.memoryFraction

  • 參數(shù)說(shuō)明:該參數(shù)用于設(shè)置shuffle過(guò)程中一個(gè)task拉取到上個(gè)stage的task的輸出后燥撞,進(jìn)行聚合操作時(shí)能夠使用的Executor內(nèi)存的比例座柱,默認(rèn)是0.2。也就是說(shuō)物舒,Executor默認(rèn)只有20%的內(nèi)存用來(lái)進(jìn)行該操作色洞。shuffle操作在進(jìn)行聚合時(shí),如果發(fā)現(xiàn)使用的內(nèi)存超出了這個(gè)20%的限制冠胯,那么多余的數(shù)據(jù)就會(huì)溢寫到磁盤文件中去火诸,此時(shí)就會(huì)極大地降低性能。
  • 參數(shù)調(diào)優(yōu)建議:如果Spark作業(yè)中的RDD持久化操作較少荠察,shuffle操作較多時(shí)置蜀,建議降低持久化操作的內(nèi)存占比奈搜,提高shuffle操作的內(nèi)存占比比例,避免shuffle過(guò)程中數(shù)據(jù)過(guò)多時(shí)內(nèi)存不夠用盯荤,必須溢寫到磁盤上馋吗,降低了性能。此外秋秤,如果發(fā)現(xiàn)作業(yè)由于頻繁的gc導(dǎo)致運(yùn)行緩慢宏粤,意味著task執(zhí)行用戶代碼的內(nèi)存不夠用,那么同樣建議調(diào)低這個(gè)參數(shù)的值灼卢。

資源參數(shù)的調(diào)優(yōu)绍哎,沒(méi)有一個(gè)固定的值,需要同學(xué)們根據(jù)自己的實(shí)際情況(包括Spark作業(yè)中的shuffle操作數(shù)量鞋真、RDD持久化操作數(shù)量以及spark web ui中顯示的作業(yè)gc情況)崇堰,同時(shí)參考本篇文章中給出的原理以及調(diào)優(yōu)建議,合理地設(shè)置上述參數(shù)灿巧。

** 資源參數(shù)參考示例**
以下是一份spark-submit命令的示例,大家可以參考一下揽涮,并根據(jù)自己的實(shí)際情況進(jìn)行調(diào)節(jié):

./bin/spark-submit \
  --master yarn-cluster \
  --num-executors 100 \
  --executor-memory 6G \
  --executor-cores 4 \
  --driver-memory 1G \
  --conf spark.default.parallelism=1000 \
  --conf spark.storage.memoryFraction=0.5 \
  --conf spark.shuffle.memoryFraction=0.3 \

寫在最后的話

根據(jù)實(shí)踐經(jīng)驗(yàn)來(lái)看抠藕,大部分Spark作業(yè)經(jīng)過(guò)本次基礎(chǔ)篇所講解的開(kāi)發(fā)調(diào)優(yōu)與資源調(diào)優(yōu)之后,一般都能以較高的性能運(yùn)行了蒋困,足以滿足我們的需求盾似。但是在不同的生產(chǎn)環(huán)境和項(xiàng)目背景下,可能會(huì)遇到其他更加棘手的問(wèn)題(比如各種數(shù)據(jù)傾斜)雪标,也可能會(huì)遇到更高的性能要求零院。為了應(yīng)對(duì)這些挑戰(zhàn),需要使用更高級(jí)的技巧來(lái)處理這類問(wèn)題村刨。在后續(xù)的《Spark性能優(yōu)化指南——高級(jí)篇》中告抄,我們會(huì)詳細(xì)講解數(shù)據(jù)傾斜調(diào)優(yōu)以及Shuffle調(diào)優(yōu)。

整理自:
美團(tuán)點(diǎn)評(píng)技術(shù)團(tuán)隊(duì)-Spark性能優(yōu)化指南——基礎(chǔ)篇
美團(tuán)點(diǎn)評(píng)技術(shù)團(tuán)隊(duì)-Spark性能優(yōu)化指南——高級(jí)篇
其中結(jié)合自己的工作經(jīng)歷做了些修改.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末嵌牺,一起剝皮案震驚了整個(gè)濱河市打洼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌逆粹,老刑警劉巖募疮,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異僻弹,居然都是意外死亡阿浓,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門蹋绽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)芭毙,“玉大人筋蓖,你說(shuō)我怎么就攤上這事「宥祝” “怎么了歧斟?”我有些...
    開(kāi)封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)渐逃。 經(jīng)常有香客問(wèn)我秃流,道長(zhǎng),這世上最難降的妖魔是什么设哗? 我笑而不...
    開(kāi)封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任唱捣,我火速辦了婚禮,結(jié)果婚禮上网梢,老公的妹妹穿的比我還像新娘震缭。我一直安慰自己,他們只是感情好战虏,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布拣宰。 她就那樣靜靜地躺著,像睡著了一般烦感。 火紅的嫁衣襯著肌膚如雪巡社。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天手趣,我揣著相機(jī)與錄音晌该,去河邊找鬼。 笑死绿渣,一個(gè)胖子當(dāng)著我的面吹牛朝群,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播中符,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼姜胖,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了淀散?” 一聲冷哼從身側(cè)響起谭期,我...
    開(kāi)封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎吧凉,沒(méi)想到半個(gè)月后隧出,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡阀捅,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年胀瞪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡凄诞,死狀恐怖圆雁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情帆谍,我是刑警寧澤伪朽,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站汛蝙,受9級(jí)特大地震影響烈涮,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜窖剑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一坚洽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧西土,春花似錦讶舰、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至肋乍,卻和暖如春鹅颊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背住拭。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工挪略, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留历帚,地道東北人滔岳。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像挽牢,于是被迫代替她去往敵國(guó)和親谱煤。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容