1,Spark引數調優

平凡的神燈發表於2021-09-07

Spark調優

一、程式碼規範

  • 調優順序:spark任務的調優順序依次是程式碼規範、資源引數(並行度)、資料傾斜、shuffle調優、業務層面

1.1 避免建立重複RDD

  • 對於新手,或者一些較為複雜的spark任務,可能會忘記之前對於某一份資料已經建立過一個RDD,而重複建立,造成不必要的計算;

1.2 儘量複用同一個RDD

  • 下游需要使用key-value型別和key型別的兩個RDD,這兩個RDD的資料完全相同,只是格式不同,那麼就只需要建立key-value這一個RDD就行,而使用key型別的RDD直接複用key-value型別的RDD就行了;因此,對於需要使用資料相同,格式不同的資料來源時,最好複用欄位較多的RDD;

1.3 多次使用的RDD要持久化

  • 當一個RDD被使用了多次,比如上面的複用同一個RDD,那麼這個RDD就要做持久化,否則這個RDD就會被計算多次;例如,a = rdd1.map(); b = rdd1.map(); 那麼就需要對 rdd1做持久化rdd1.persist(),否則rdd1就會被計算兩次;

1.4 使用高效能運算元

  • 用reduceByKey替代groupByKey求聚合:前者是map-side預聚合運算元,會在map端預聚合,類似於Combiner;
  • 用combineByKey代替groupByKey求topN:前者可以自定義分割槽內合併和分割槽間合併的計算邏輯,也是預聚合;
  • mapPartition替代map:一次呼叫處理一個分割槽的資料,對於需要在map中建立很多重複物件的場景,最好使用mapPartition,同時注意OOM問題;
  • foreachPartition替代foreach:道理同mapPartition一樣;在需要將rdd的資料寫入MySQL時,後者是一條一條資料插入,並且每條資料都會建立一次資料庫連線;而前者則是一個分割槽操作一次,效能有很高的提升;

1.5 好習慣

  • 廣播大變數:當需要在運算元中使用大變數(1g以內)時,最好將大變數廣播到Executor中,例如:rdd1.filter(x=>slant.contains(x)),如果slant在20M~1G之間,就可以將slant廣播;

  • filter後coalesce:由於filter後,各個分割槽中的資料不再均衡,使用coalesce再平衡一下分割槽資料;

  • 優化資料結構:對於運算元中的資料結構,能用陣列就不要用集合型別,最好使用字串代替物件,用基本型別代替字串;

  • 使用Kryo序列化:spark中的三個場景會涉及到序列化,運算元中使用外部變數、將自定義物件作為RDD中的型別、可序列化的持久化策略(如MEMORY_ONLY_SER),使用kryo的效能會高很多;使用Kryo序列化時,最好註冊所有的自定義類;conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer") conf.registerKryoClasses(Array(classOf[MyClass1], classOf[MyClass2]));

  • persist後unpersist:unpersist是立即釋放快取,對複用的RDD使用persist快取後,需要使用行動運算元提交job後,才會真正的快取,然後再使用unpersist釋放快取;所以當persist快取的RDD不會再使用時,最好是手動unpersist釋放快取;

二、引數調優

資源引數


1.1 --num-executors 100

  • 引數解釋:任務可以申請的Excutor最大數量,並不是一次性分配100個Excutor;Excutor數量會在任務的執行過程中動態調整,有 job處於pending狀態則申請Excutor,一個Excutor空閒時間過長則將其移除;Excutor的數量決定了任務的並行度;

  • 申請Excutor:當有任務處於pending狀態(積壓)超過一定時間,就認為資源不足,需要申請Excutor;

    何時申請:當pending積壓的任務超過spark.dynamicAllocation.schedulerBacklogTimeout(1秒)就申請
    申請多少:申請數量 = 正在執行和pending的任務數量 * spark.dynamicAllocation.executorAllocationRatio(1)/ 並行度
    
  • 移除Excutor:

    spark.dynamicAllocation.enabled(false)決定是否使用資源動態分配;必須開啟外部shuffle;
    spark.dynamicAllocation.executorIdleTimeout (60s)空閒60s就會被回收(並且沒有快取);
    
  • 決定任務的並行度:executor的數量就是工作節點的數量,直接決定了任務的並行度;準確的說是由executor*core決定的;這只是物理上提供的最大並行度,而任務實際的並行度還是由程式中設定的並行度決定,也就是RDD的分割槽數;

1.2 --executor-memory 5g

  • 引數解釋:每個executor的記憶體大小;對於spark調優和OOM異常,通常都是對executor的記憶體做調整,spark記憶體模型也是指executor的記憶體分配,所以executor的記憶體管理是非常重要的;
  • 記憶體分配:該引數是總的記憶體分配,而在任務執行中,會根據spark記憶體模型對這個總記憶體再次細分;在實際生產中,通常需要根據程式中使用的快取記憶體和計算記憶體,來劃分不同的比例,從而合理的利用記憶體,避免OOM,提高效能;

1.3 --executor-cores 4

  • 引數解釋:每個executor的核數;是每個executor內部的並行度,即一個executor中可同時執行的task數量;
  • 並行度:core的數量決定了一個executor同時執行的task數量,如果task數量越多,則意味著佔用的executor記憶體也越多;所以,在executor記憶體固定的情況下,可以通過增加executor數量,減少core數量,使任務總並行度不變的前提下,降低OOM風險;如果任務需要廣播大變數,可以增大core數,使更多的task共用廣播變數;

1.4 --driver-memory

  • 引數解釋:driver端的記憶體大小;如果要collect大量資料到driver端,或者要廣播大變數時,就需要調大driver端的記憶體;一般給個3G、4G就夠了;

記憶體引數


spark.storage.memoryFraction、spark.shuffle.memoryFraction(spark1.6之前靜態記憶體管理)

  • 引數解釋:在spark1.6之前,使用的是靜態記憶體管理,而這兩個引數就是用來決定快取記憶體和執行記憶體大小的;在spark1.6及之後,採用的是統一記憶體管理(也叫動態記憶體管理),這兩個引數就廢棄了(但也可以讓它生效)

spark.memory.fraction(spark1.6及之後,統一記憶體管理)

  • 引數解釋:spark1.6及之後採用的是統一記憶體管理,也叫動態記憶體管理,顧名思義,就是快取記憶體和執行記憶體統一管理,並且是動態的;首先解釋“統一”:spark.memory.fraction是堆內記憶體中用於執行、shuffle、快取的記憶體比例;這個值越低,則執行時溢位到磁碟更頻繁、同時快取被逐出記憶體也更頻繁;一般使用預設值就好了,spark2.2預設是0.6,那麼剩下的0.4就是用於儲存使用者的資料結構(比如map運算元中定義的中間資料)以及spark內部的後設資料;

spark.memory.storageFraction

  • 引數解釋:儲存記憶體不會被逐出記憶體的總量,這個是基於spark.memory.fraction的佔比;這個值越高,則執行、shuffle的記憶體就越少,從而溢寫到磁碟就越頻繁;一般使用預設值就好了,spark2.2預設是0.5;

spark.kryoserializer.buffer.max

  • 引數解釋:kryo序列化時使用的快取大小;如果collect大量資料到driver端,可能會拋buffer limit exceeded異常,這個時候就要調大該引數;預設是64m,掛了就設定為1024m;如果序列化的一個物件很大,那麼就需要增大改引數的值spark.kryoserializer.buffer(預設64k);

dfs.client.block.write.locateFollowingBlock.retries

  • 引數解釋:寫入塊後嘗試關閉的次數;Unable to close file because the last block does not have enough number of replicas異常的原因;2.7.4已修復;預設是5,掛了就設定為6;

spark.driver.maxResultSize

  • 引數解釋:一次collect到driver端的最大記憶體大小,Total size of serialized results of 16 tasks (1048.5 MB) is bigger than spark.driver.maxResultSize (1024.0 MB)異常時需要調大該值;預設1g,掛了就設定為2g,0表示不限制;

shuffle引數


spark.shuffle.file.buffer

  • 引數解釋:shuffle write時,會先寫到BufferedOutputStream緩衝區中,然後再溢寫到磁碟;該引數就是快取區大小,預設32k,建議設定為64k;

spark.shuffle.spill.batchSize

  • 引數解釋:shuffle在spill溢寫過程中需要將資料序列化和反序列化,這個是一個批次處理的條數;預設是10000,可以調大該值,2萬5萬都可以;

spark.shuffle.io.maxRetries

  • 引數解釋:shuffle read拉取資料時,由於網路異常或者gc導致拉取失敗,會自動重試,改引數就是配置重試次數,在資料量達到十億、百億級別的時候,最好調大該引數以增加穩定性;預設是3次,建議設定為10到20;

spark.shuffle.io.retryWait

  • 引數解釋:該引數是 spark.shuffle.io.maxRetries的重試間隔,預設是5s,建議設定為20s;

spark.reducer.maxSizeInFlight

  • 引數解釋:shuffle read拉取資料時的快取區大小,也就是一次拉取的資料大小;注意是從5個節點拉取48M的資料,而不是從一個節點獲取48M;預設48m,建議設定為96m;
  • 原理解釋:從遠端節點拉取資料時,是並行的從傳送5個請求,每個請求拉取的最大長度是 48M / 5,但是拉取時都是以block為最小單位的,所以實際獲取的有可能會大於這個值;

spark.reducer.maxReqsInFlight

  • 引數解釋:shuffle read時,一個task的一個批次同時傳送的請求數量;預設是 Int的最大值;
  • 原理解釋:構造遠端請求時,單個請求大小限制是 48M / 5,而在一次拉取遠端block資料時,是按批次拉取,一個批次的大小限制是 48M,所以理想情況下一個批次會傳送5個請求;但如果block的分佈不均勻,導致一個請求的請求大小遠小於 48M / 5 (例如1M),而一個批次的大小限制是48M,所以這個批次就會傳送48個請求;當節點數較多時,一個task的一個批次可能會傳送非常多的請求,導致某些節點的入站連線數過多,從而導致失敗;

spark.reducer.maxReqSizeShuffleToMem

  • 引數解釋:shuffle read時,從遠端拉取block如果大於這個值就會強行落盤,預設是Long的最大值,建議小於2G,一般設為200M,spark2.2開始生效;(spark2.3開始換成了這個引數spark.maxRemoteBlockSizeFetchToMem);shuffle read這個部分的引數在spark的版本更新中變化較大,所以在優化時一定要根據叢集的spark版本設定對應的引數;
  • 原理解釋:一次拉取請求中,如果要拉取的資料比較大,記憶體放不下,就直接落盤;對於資料傾斜比較嚴重的任務,有可能一個block非常大,而沒有足夠的記憶體存放時就會OOM,所以最好限制該引數的大小;還有一個原因就是 netty的最大限制是2G,所以大於2G肯定會報錯;spark2.4該引數的預設值是:Int的最大值-512 (2G,減512用來儲存後設資料);spark3.0的最大值也是2G,並且給了預設值200M;

spark.reducer.maxBlocksInFlightPerAddress

  • 引數解釋:shuffle read時,一個節點同時被拉取的最大block數,如果太多可能會導致executor服務或nodemanager崩潰;預設Int的最大值;(spark2.2.1開始支援);

  • 原理解釋:shuffle read時每個task都會從shuffle write所在的節點拉取自己的block資料,如果一個shuffle write的executor執行了9個task,就會write9個data檔案;如果shuffle read有1000核,那麼同時執行1000個task,每個task要到shuffle write所在的executor獲取9個block,極端情況下一個shuffle write的executor會被請求9000次;當節點數非常多時,一個shuffle write的executor會同時被很多節點拉取block,從而導致失敗;

檔案相關


spark.sql.files.maxPartitionBytes

  • 引數解釋:sparksql讀取檔案時,每個分割槽的最大檔案大小,這個引數決定了讀檔案時的並行度;預設128M;例如一個300M的text檔案,按128M劃分為3個切片,所以SparkSQL讀取時最少有3個分割槽;
  • 原理解釋:sparksql讀取檔案的並行度=max(spark預設並行度,切片數量(檔案大小/ 該引數));這裡要注意壓縮檔案是否可分割;但是要注意,對於parquet格式,一個切片對應一個row group;

spark.sql.parquet.compression.codec

  • 引數解釋:parquet格式的壓縮方式,預設是snappy,可以選擇gzip、lzo、或者uncompressed不壓縮;

spark.io.compression.codec

  • 引數解釋:spark中rdd分割槽、廣播變數、shuffle輸出的壓縮格式,spark2.2預設是lz4;

spark.serializer

  • 引數解釋:spark序列化的實現,這裡的序列化是針對shuffle、廣播和rdd cache的序列化方式;預設使用java的序列化方式org.apache.spark.serializer.JavaSerializer效能比較低,所以一般都使用org.apache.spark.serializer.KryoSerializer ,使用Kryo序列化時最好註冊十分需要空間的型別,可以節省很多空間;spark task的序列化由引數spark.closure.serializer配置,目前只支援JavaSerializer;

spark.sql.hive.convertMetastoreParquet

  • 引數解釋:是否採用spark自己的Serde來解析Parquet檔案;Spark SQL為了更好的效能,在讀取hive metastore建立的parquet檔案時,會採用自己Parquet Serde,而不是採用hive的Parquet Serde來序列化和反序列化,這在處理null值和decimal精度時會有問題;預設為true,設為false即可(會採用與hive相同的Serde);

spark.sql.parquet.writeLegacyFormat

  • 引數解釋:是否使用遺留的(hive的方式)format來寫Parquet檔案;由於decimal精度問題,hive讀取spark建立的Parquet檔案會報錯;所以這裡的spark採用與hive相同的writeFormat來寫Parquet檔案,這樣hive在讀取時就不會報錯;並且上下游表的精度最好一致,例如a表的欄位精度為decimal(10,2),b表也最好是decimal(10,2);
  • 原理解釋:在hive中decimal型別是固定的用int32來表示,而標準的parquet規範約定,根據精度的不同會採用int32和int64來儲存,而spark就是採用的標準的parquet格式;所以對於精度不同decimal的,底層的儲存型別有變化;所以使用spark儲存的parquet檔案,在使用hive讀取時報錯;將spark.sql.parquet.writeLegacyFormat(預設false)配置設為true,即採用與hive相同的format類來讀寫parquet檔案;

參考文章

相關文章