簡體   English   中英

在 Spark 作業之間共享存儲級別為 NONE 的 RDD

[英]Sharing RDDs with storage level NONE among Spark jobs

我有多個 Spark 作業,它們共享數據流圖的一部分,包括昂貴的洗牌操作。 如果我堅持使用 RDD,我會看到預期的巨大改進 (22x)。

然而,即使我將這些 RDD 的存儲級別保持為NONE ,僅通過在作業之間共享 RDD,我仍然看到高達 4 倍的改進。

為什么? 我假設 Sark 總是重新計算存儲級別為 NONE 的 RDD,並且這些 RDD 不會被驅逐/溢出。

我的 Spark 版本是 3.3.1。 顯示代碼很困難,因為代碼分布在更大系統中的多個文件中。 我基本上是在做以下事情:

  • 識別跨作業的重復(且昂貴)Spark 操作。 我通過維護自己的血統痕跡來做到這一點 [1]。
  • 在第一次執行這些 Spark 操作后,我將 RDD 句柄保存在本地緩存中,這是一個哈希映射 <lineage-trace, RDD>。
  • 下一次,當我得到相同的操作時,我只是簡單地重用緩存的 RDD。

如果我在第二步中保留 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 做了一些事情。 讓我們來看看這兩個操作。

注冊RDDForCleanup

registerRDDForCleanupContextCleaner類的一個方法 它看起來像這樣:

  /** 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

在我們的 RDD 上調用的第二個方法是persistRDD方法。 我不會詳細介紹(因為它在這里不太重要),但這種方法( Sparkcontext.scala的)基本上將此 RDD 添加到Map ,其中 SparkContext 跟蹤所有持久化的 RDD。

結論

我們可以在這項調查中進行更深入的研究,但這將變得不切實際。 我認為這種抽象級別足以理解調用rdd.persist(StorageLevel)實際上做了一些事情來使您的 RDD 不會過早地被垃圾收集!

希望這可以幫助:)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM