Spark 效能調優--開發階段

at_1發表於2021-09-09

開發階段調優

原則一:避免建立重複的RDD

一個Spark作業時,首先是基於某個資料來源建立一個初始的RDD,接著對這個RDD執行某個運算元操作,得到下一個RDD;以此類推,迴圈往復,直到計算出最終我們需要的結果。在這個過程中,多個RDD會透過不同的運算元操作(比如map、reduce等)串起來,就是RDD lineage,也就是“RDD的血緣關係鏈”

對於同一份資料,只應該建立一個RDD,不能建立多個RDD來代表同一份資料

val rdd1 = sc.textFile("hdfs://master_ip:9000/hello.txt")
rdd1.map(...)
val rdd2 = sc.textFile("hdfs://master_ip:9000/hello.txt")
rdd2.reduce(...)

val rdd1 = sc.textFile("hdfs://master_ip:9000/hello.txt")
rdd1.map(...)
rdd1.reduce(...)

這種寫法很明顯比上一種寫法要好多了,對於同一份資料只建立了一個RDD,然後對這一個RDD執行了多次運算元操作,但是由於rdd1被執行了兩次運算元操作,第二次執行reduce操作的時候,還會再次從源頭處重新計算一次rdd1的資料,因此還是會有重複計算的效能開銷



原則二:儘可能複用同一個RDD

除了要避免對一份完全相同的資料建立多個RDD之外,在對不同的資料執行運算元操作時還要儘可能地複用一個RDD。

比如說,有一個RDD的資料格式是key-value型別的,另一個是單value型別的,這兩個RDD的value資料是完全一樣的。那麼此時我們可以只使用key-value型別的那個RDD,因為其中已經包含了另一個的資料。對於類似這種多個RDD的資料有重疊或者包含的情況,我們應該儘量複用一個RDD,這樣可以儘可能地減少RDD的數量,從而儘可能減少運算元執行的次數

// 有一個<Long, String>格式的RDD,即rdd1。// 對rdd1執行了一個map操作,建立了一個rdd2,rdd2是rdd1的子集JavaPairRDD<Long, String> rdd1 = ...
JavaRDD<String> rdd2 = rdd1.map(...)// 分別對rdd1和rdd2執行了不同的運算元操作。rdd1.reduceByKey(...)
rdd2.map(...)

JavaPairRDD<Long, String> rdd1 = ...
rdd1.reduceByKey(...)
rdd1.map(tuple._2...)



原則三:對多次使用的RDD進行持久化

儘可能複用RDD,在這個基礎之上,進行第二步最佳化,保證對一個RDD執行多次運算元操作時,這個RDD本身僅僅被計算一次

對於一個RDD執行多次運算元的預設原理是這樣的:
每次你對一個RDD執行一個運算元操作時,都會重新從源頭處計算一遍,計算出那個RDD來,然後再對這個RDD執行你的運算元操作。這種方式的效能是很差的
對於這種情況,對多次使用的RDD進行持久化。Spark就會根據你的持久化策略,將RDD中的資料儲存到記憶體或者磁碟中。以後每次對這個RDD進行運算元操作時,都會直接從記憶體或磁碟中提取持久化的RDD資料,然後執行運算元

// 如果要對一個RDD進行持久化,只要對這個RDD呼叫cache()和persist()即可。// cache()方法表示:使用非序列化的方式將RDD中的資料全部嘗試持久化到記憶體中。// 此時再對rdd1執行兩次運算元操作時,只有在第一次執行map運算元時,才會將這個rdd1從源頭處計算一次。// 第二次執行reduce運算元時,就會直接從記憶體中提取資料進行計算,不會重複計算一個rdd。val rdd1 = sc.textFile("hdfs://master_ip:9000/hello.txt").cache()
rdd1.map(...)
rdd1.reduce(...)// persist()方法表示:手動選擇持久化級別,並使用指定的方式進行持久化。// 序列化的方式可以減少持久化的資料對記憶體/磁碟的佔用量,進而避免記憶體被持久化資料佔用過多,從而發生頻繁GC。val rdd1 = sc.textFile("hdfs://master_ip:9000/hello.txt").persist(StorageLevel.MEMORY_AND_DISK_SER)



原則四:儘量減少使用shuffle類運算元

Spark作業執行過程中,最消耗效能的地方就是shuffle過程,就是將分佈在叢集中多個節點上的同一個key,拉取到同一個節點上,進行聚合或join等操作

shuffle過程中,各個節點上的相同key都會先寫入本地磁碟檔案中,然後其他節點需要透過網路傳輸拉取各個節點上的磁碟檔案中的相同key,而且相同key都拉取到同一個節點進行聚合操作時,還有可能會因為一個節點上處理的key過多,導致記憶體不夠存放,進而溢寫到磁碟檔案中

因此在shuffle過程中,可能會發生大量的磁碟檔案讀寫的IO操作,以及資料的網路傳輸操作,也是shuffle效能較差的主要原因。

// 傳統的join操作會導致shuffle操作。// 因為兩個RDD中,相同的key都需要透過網路拉取到一個節點上,由一個task進行join操作。val rdd3 = rdd1.join(rdd2)// Broadcast+map的join操作,不會導致shuffle操作// 使用Broadcast將一個資料量較小的RDD作為廣播變數。val rdd2Data = rdd2.collect()
val rdd2DataBroadcast = sc.broadcast(rdd2Data)// 在rdd1.map運算元中,可以從rdd2DataBroadcast中,獲取rdd2的所有資料// 然後進行遍歷,如果發現rdd2中某條資料的key與rdd1的當前資料的key是相同的,那麼就判定可以進行join。// 此時就可以根據自己需要的方式,將rdd1當前資料與rdd2中可以連線的資料,拼接在一起(String或Tuple)。val rdd3 = rdd1.map(rdd2DataBroadcast...)// 注意,以上操作,建議僅僅在rdd2的資料量比較少(比如幾百M,或者一兩G)的情況下使用。// 因為每個Executor的記憶體中,都會駐留一份rdd2的全量資料。



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

無法用map類的運算元來替代shuffle操作,那麼儘量使用可以map-side預聚合的運算元

每個節點本地對相同的key進行一次聚合操作,類似於MapReduce中的本地combiner。預聚合之後,每個節點本地就只會有一條相同的key,因為多條相同的key都被聚合起來了,其他節點在拉取所有節點上的相同key時,就會大大減少需要拉取的資料數量,從而也就減少了磁碟IO以及網路傳輸開銷

通常來說,在可能的情況下,建議使用reduceByKey或者aggregateByKey運算元來替代掉groupByKey運算元。因為reduceByKey和aggregateByKey運算元都會使用使用者自定義的函式對每個節點本地的相同key進行預聚合



原則六:使用高效能的運算元

使用 reduceByKey / aggregateByKey 替代 groupByKey

使用 mapPartitions 替代普通 map ,有的時候使用 mapPartitions 會出現OOM(記憶體溢位)的問題。因為單次函式呼叫就要處理掉一個 partition 所有的資料

使用 foreachPartitions 替代 foreach ,對於寫資料庫操作,能夠減少連線數

使用 filter 之後進行 coalesce 操作,對一個RDD執行 filter 運算元過濾掉RDD中較多資料後,建議使用 coalesce 運算元,手動減少RDD的partition數量,因為 filter
之後,RDD的每個partition中都會有很多資料被過濾掉,此時如果照常進行後續的計算,其實每個task處理的partition中的資料量並不是很多,有一點資源浪費

使用 repartitionAndSortWithinPartitions 替代 repartition 與 sort 類操作,官方建議,如果需要在 repartition 重分割槽之後,還要進行排序,建議直接使用
repartitionAndSortWithinPartitions 運算元。因為該運算元可以一邊進行重分割槽的shuffle操作,一邊進行排序



原則七:廣播大變數

有時在開發過程中,會遇到需要在運算元函式中使用外部變數的場景(尤其是大變數,比如100M以上的大集合),那麼此時就應該使用Spark的廣播(Broadcast)功能來提升效能

在運算元函式中使用到外部變數時,預設情況下,Spark會將該變數複製多個副本,透過網路傳輸到task中,此時每個task都有一個變數副本。如果變數本身比較大的話(比如100M,甚至1G),那麼大量的變數副本在網路中傳輸的效能開銷,以及在各個節點的Executor中佔用過多記憶體

如果使用的外部變數比較大,建議使用Spark的廣播功能,對該變數進行廣播。廣播後的變數,會保證每個Executor的記憶體中



原則八:使用Kryo最佳化序列化效能

在Spark中,主要有三個地方涉及到了序列化
- 在運算元函式中使用到外部變數時,該變數會被序列化後進行網路傳輸
- 自定義的型別作為RDD的泛型型別時
- 使用可序列化的持久化策略時

對於這三種出現序列化的地方,我們都可以透過使用Kryo序列化類庫,來最佳化序列化和反序列化的效能



原則九:最佳化資料結構

有三種型別比較耗費記憶體
- 物件,每個Java物件都有物件頭、引用等額外的資訊,因此比較佔用記憶體空間
- 字串,每個字串內部都有一個字元陣列以及長度等額外資訊
- 集合型別,比如HashMap、LinkedList等,因為集合型別內部通常會使用一些內部類來封裝集合元素,比如Map.Entry

運算元函式中的程式碼,儘量使用字串替代物件,使用原始型別(比如Int、Long)替代字串,使用陣列替代集合型別,這樣儘可能地減少記憶體佔用



作者:Alex90
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/1817/viewspace-2818658/,如需轉載,請註明出處,否則將追究法律責任。

相關文章