RDD持久化 --- Spark調優
問題:重複建立RDD
通常來說,開發一個Spark作業時,首先是建立一個初始的RDD;接著對這個RDD執行某個運算元操作,然後得到下一個RDD;以此類推,迴圈往復,直到計算出最終我們需要的結果。在這個過程中,多個RDD會通過不同的運算元操作(比如map、reduce等)串起來,這個“RDD串”,就是RDD lineage,也就是“RDD的血緣關係鏈”。
我們在開發過程中要注意:對於同一份資料,只應該建立一個RDD,不能建立多個RDD來代表同一份資料。
在開發RDD lineage極其冗長的Spark作業時,可能會忘了自己之前對於某一份資料已經建立過一個RDD了,從而導致對於同一份資料,建立了多個RDD。這就意味著,我們的Spark作業會進行多次重複計算來建立多個代表相同資料的RDD,進而增加了作業的效能開銷。
解決:對多次使用的RDD進行持久化
Spark中對於一個RDD執行多次運算元的預設原理是這樣的:每次你對一個RDD執行一個運算元操作時,都會重新從源頭處計算一遍,計算出那個RDD來,然後再對這個RDD執行你的運算元操作。這種方式的效能是很差的。
因此對於這種情況,我們的建議是:對多次使用的RDD進行持久化。此時Spark就會根據你的持久化策略,將RDD中的資料儲存到記憶體或者磁碟中。以後每次對這個RDD進行運算元操作時,都會直接從記憶體或磁碟中提取持久化的RDD資料,然後執行運算元,而不會從源頭處重新計算一遍這個RDD,再執行運算元操作。
RDD持久化落地實現:
如果要對一個RDD進行持久化,只要對這個RDD呼叫 cache() 和 persist() 方法即可。
cache()方法表示:使用非序列化的方式將RDD中的資料全部嘗試持久化到記憶體中。
// 此時再對rdd1執行兩次運算元操作時,只有在第一次執行map運算元時,才會將這個rdd1從源頭處計算一次。
// 第二次執行reduce運算元時,就會直接從記憶體中提取資料進行計算,不會重複計算一個rdd。
persist()方法表示:手動選擇持久化級別,並使用指定的方式進行持久化。
// 比如說,StorageLevel.MEMORY_AND_DISK_SER表示,記憶體充足時優先持久化到記憶體中,記憶體不充足時持久化到磁碟檔案中。
// 而且其中的_SER字尾表示,使用序列化的方式來儲存RDD資料,此時RDD中的每個partition都會序列化成一個大的位元組陣列,然後再持久化到記憶體或磁碟中。
// 序列化的方式可以減少持久化的資料對記憶體/磁碟的佔用量,進而避免記憶體被持久化資料佔用過多,從而發生頻繁GC。
對於persist()方法而言,我們可以根據不同的業務場景選擇不同的持久化級別。
持久化級別 | 含義 |
---|---|
MEMORY_ONLY | 使用未序列化的Java物件格式,將資料儲存在記憶體中。如果記憶體不夠存放所有的資料,則資料可能就不會進行持久化。那麼下次對這個RDD執行運算元操作時,那些沒有被持久化的資料,需要從源頭處重新計算一遍。這是預設的持久化策略,使用cache()方法時,實際就是使用的這種持久化策略。 |
MEMORY_AND_DISK | 使用未序列化的Java物件格式,優先嚐試將資料儲存在記憶體中。如果記憶體不夠存放所有的資料,會將資料寫入磁碟檔案中,下次對這個RDD執行運算元時,持久化在磁碟檔案中的資料會被讀取出來使用。 |
MEMORY_ONLY_SER基本 | 含義同MEMORY_ONLY。唯一的區別是,會將RDD中的資料進行序列化,RDD的每個partition會被序列化成一個位元組陣列。這種方式更加節省記憶體,從而可以避免持久化的資料佔用過多記憶體導致頻繁GC。 |
MEMORY_AND_DISK_SER | 基本含義同MEMORY_AND_DISK。唯一的區別是,會將RDD中的資料進行序列化,RDD的每個partition會被序列化成一個位元組陣列。這種方式更加節省記憶體,從而可以避免持久化的資料佔用過多記憶體導致頻繁GC。 |
DISK_ONLY | 使用未序列化的Java物件格式,將資料全部寫入磁碟檔案中。 |
MEMORY_ONLY_2, MEMORY_AND_DISK_2, 等等. | 對於上述任意一種持久化策略,如果加上字尾_2,代表的是將每個持久化的資料,都複製一份副本,並將副本儲存到其他節點上。這種基於副本的持久化機制主要用於進行容錯。假如某個節點掛掉,節點的記憶體或磁碟中的持久化資料丟失了,那麼後續對RDD計算時還可以使用該資料在其他節點上的副本。如果沒有副本的話,就只能將這些資料從源頭處重新計算一遍了。 |
如何選擇一種最合適的持久化策略
-
預設情況下,效能最高的當然是MEMORY_ONLY,但前提是你的記憶體必須足夠足夠大,可以綽綽有餘地存放下整個RDD的所有資料。因為不進行序列化與反序列化操作,就避免了這部分的效能開銷;對這個RDD的後續運算元操作,都是基於純記憶體中的資料的操作,不需要從磁碟檔案中讀取資料,效能也很高;而且不需要複製一份資料副本,並遠端傳送到其他節點上。但是這裡必須要注意的是,在實際的生產環境中,恐怕能夠直接用這種策略的場景還是有限的,如果RDD中資料比較多時(比如幾十億),直接用這種持久化級別,會導致JVM的OOM記憶體溢位異常。
-
如果使用MEMORY_ONLY級別時發生了記憶體溢位,那麼建議嘗試使用MEMORY_ONLY_SER級別。該級別會將RDD資料序列化後再儲存在記憶體中,此時每個partition僅僅是一個位元組陣列而已,大大減少了物件數量,並降低了記憶體佔用。這種級別比MEMORY_ONLY多出來的效能開銷,主要就是序列化與反序列化的開銷。但是後續運算元可以基於純記憶體進行操作,因此效能總體還是比較高的。此外,可能發生的問題同上,如果RDD中的資料量過多的話,還是可能會導致OOM記憶體溢位的異常。
-
如果純記憶體的級別都無法使用,那麼建議使用MEMORY_AND_DISK_SER策略,而不是MEMORY_AND_DISK策略。因為既然到了這一步,就說明RDD的資料量很大,記憶體無法完全放下。序列化後的資料比較少,可以節省記憶體和磁碟的空間開銷。同時該策略會優先儘量嘗試將資料快取在記憶體中,記憶體快取不下才會寫入磁碟。
-
通常不建議使用DISK_ONLY和字尾為_2的級別:因為完全基於磁碟檔案進行資料的讀寫,會導致效能急劇降低,有時還不如重新計算一次所有RDD。字尾為_2的級別,必須將所有資料都複製一份副本,併發送到其他節點上,資料複製以及網路傳輸會導致較大的效能開銷,除非是要求作業的高可用性,否則不建議使用。