[英]Sharing RDDs with storage level NONE among Spark jobs
我有多個 Spark 作業,它們共享數據流圖的一部分,包括昂貴的洗牌操作。 如果我堅持使用 RDD,我會看到預期的巨大改進 (22x)。
然而,即使我將這些 RDD 的存儲級別保持為NONE
,僅通過在作業之間共享 RDD,我仍然看到高達 4 倍的改進。
為什么? 我假設 Sark 總是重新計算存儲級別為 NONE 的 RDD,並且這些 RDD 不會被驅逐/溢出。
我的 Spark 版本是 3.3.1。 顯示代碼很困難,因為代碼分布在更大系統中的多個文件中。 我基本上是在做以下事情:
如果我在第二步中保留 RDD(通過調用rdd.persist(StorageLevel.MEMORY_AND_DISK)
,我會看到巨大的改進。但即使我只是重復使用相同的 RDD(存儲級別NONE
),我仍然看到改進。
[1] LIMA:機器學習系統中的細粒度沿襲追蹤和重用。 Arnab Phani、本傑明·拉斯、馬蒂亞斯·伯姆。 SIGMOD 2021
如果我們看一下rdd.persist(StorageLevel)
方法的源代碼,我們會看到以下內容:
def persist(newLevel: StorageLevel): this.type = {
if (isLocallyCheckpointed) {
// This means the user previously called localCheckpoint(), which should have already
// marked this RDD for persisting. Here we should override the old storage level with
// one that is explicitly requested by the user (after adapting it to use disk).
persist(LocalRDDCheckpointData.transformStorageLevel(newLevel), allowOverride = true)
} else {
persist(newLevel, allowOverride = false)
}
}
這樣就可以調用帶有額外輸入參數的persist
方法。 它看起來像這樣:
/**
* Mark this RDD for persisting using the specified level.
*
* @param newLevel the target storage level
* @param allowOverride whether to override any existing level with the new one
*/
private def persist(newLevel: StorageLevel, allowOverride: Boolean): this.type = {
// TODO: Handle changes of StorageLevel
if (storageLevel != StorageLevel.NONE && newLevel != storageLevel && !allowOverride) {
throw SparkCoreErrors.cannotChangeStorageLevelError()
}
// If this is the first time this RDD is marked for persisting, register it
// with the SparkContext for cleanups and accounting. Do this only once.
if (storageLevel == StorageLevel.NONE) {
sc.cleaner.foreach(_.registerRDDForCleanup(this))
sc.persistRDD(this)
}
storageLevel = newLevel
this
}
在那里,我們看到了一些有趣的東西。 如果當前的storageLevel
(不是新的)== StorageLevel.NONE
,我們將在這個 RDD 上registerRDDForCleanup
RDDForCleanup 和persistRDD
。
現在, storageLevel
的默認值為StorageLevel.NONE
。 這意味着您的案例(調用persist
on unpersisted RDD)屬於此類。
所以我們發現調用rdd.persist(StorageLevel.NONE)
實際上對你的 RDD 做了一些事情。 讓我們來看看這兩個操作。
registerRDDForCleanup
是ContextCleaner
類的一個方法。 它看起來像這樣:
/** Register an RDD for cleanup when it is garbage collected. */
def registerRDDForCleanup(rdd: RDD[_]): Unit = {
registerForCleanup(rdd, CleanRDD(rdd.id))
}
// some other code between here that I removed for this explanation
/** Register an object for cleanup. */
private def registerForCleanup(objectForCleanup: AnyRef, task: CleanupTask): Unit = {
referenceBuffer.add(new CleanupTaskWeakReference(task, objectForCleanup, referenceQueue))
}
所以這個方法實際上添加了一個清理任務(與這個 RDD 相關聯)到一些叫做referenceBuffer
的緩沖區。 看起來像這樣:
/**
* A buffer to ensure that `CleanupTaskWeakReference`s are not garbage collected as long as they
* have not been handled by the reference queue.
*/
private val referenceBuffer =
Collections.newSetFromMap[CleanupTaskWeakReference](new ConcurrentHashMap)
因此,正如代碼注釋所說,此referenceBuffer
是一個緩沖區,可確保任務不會過早地被垃圾收集。 因此,您的 RDD 收集的垃圾更少,從而提高了您的性能!
在我們的 RDD 上調用的第二個方法是persistRDD
方法。 我不會詳細介紹(因為它在這里不太重要),但這種方法( Sparkcontext.scala的)基本上將此 RDD 添加到Map
,其中 SparkContext 跟蹤所有持久化的 RDD。
我們可以在這項調查中進行更深入的研究,但這將變得不切實際。 我認為這種抽象級別足以理解調用rdd.persist(StorageLevel)
實際上做了一些事情來使您的 RDD 不會過早地被垃圾收集!
希望這可以幫助:)
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.