spark2原理分析-RDD的caching和persistence原理分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zg_hover/article/details/83241435

概述

本文分析RDD的caching和persistence的原理。并对其代码实现进行分析。

persist and cache

基本概念

Spark的一个重要特性是:能够跨操作把数据保存到内存中,这个过程称为persist,或称为caching。当persist一个RDD时,每个spark节点都会把它计算的任何分区保存到内存中,当在基于这些数据进行其他操作时进行复用。这样使得将来进行的操作更快(通常是数10倍)。Caching是算法迭代(iterative algorithms)和快速交互式(fast interactive)使用的关键工具。

您可以使用persist()或cache()方法来标记要保留的RDD。在第一次操作计算完成后,它将保留在该计算节点的内存中。
Spark的缓存具有容错性:若任何RDD的分区丢失,它将使用最初创建它的转换(transformations)自动重新计算。

此外,每次persist RDD可以使用不同的存储级别进行存储,例如,您可以将数据集保存在磁盘上,将其保留在内存中,但作为序列化Java对象(以节省空间),跨节点复制它。

通过将StorageLevel对象(Scala,Java,Python)传递给persist()来设置这些存储级别。cache()方法是使用默认存储级别的简写,即StorageLevel.MEMORY_ONLY(在内存中存储反序列化的对象)。完整的存储级别是:

Storage Level 说明
MEMORY_ONLY 将RDD存储为JVM中的反序列化Java对象。如果RDD不保存在内存,则某些分区将不会被缓存,并且每次需要时都会重新计算。这是默认级别。
MEMORY_AND_DISK 将RDD存储为JVM中的反序列化Java对象。若RDD不适合保存在内存中,则可以保存在磁盘中,需要时可以从磁盘读取。
MEMORY_ONLY_SER(java and Scala) 将RDD存储为序列化Java对象(每个分区一个字节数组)。这通常比反序列化对象更节省空间,特别是在使用快速序列化器时,但读取CPU密集程度更高。
MEMORY_AND_DISK_SER(java and Scala) 与MEMORY_ONLY_SER类似,但将不适合内存的分区溢出到磁盘,而不是每次需要时即时重新计算它们。
DISK_ONLY 仅将RDD分区存储在磁盘上。
MEMORY_ONLY_2 与上面的级别相同,但复制两个群集节点上的每个分区。
OFF_HEAP(测试中) 与MEMORY_ONLY_SER类似,但将数据存储在堆外内存中。这需要启用堆外内存。

Spark会在shuffe操作时自动缓存(persist)一些数据,即使用户不调用persist函数。这样做是为了避免在shuffle期间节点出现故障时重新计算整个输入。若用户需要重用结果RDD,任然建议在得到RDD结果时调用persist函数。

如何选择存储级别(Storage Level)

Spark的存储级别目的是:在提供内存使用和CPU效率之间的选择的权衡。我们建议您通过以下流程选择一个:

  • 若RDD适合使用默认存储级别(MEMORY_ONLY),就尽量使用。这是使CPU效率最高的选项,允许RDD上的操作尽可能快地执行。
  • 若使用默认存储级别不太合适,可以尝试使用MEMORY_ONLY_SER并选择一个快速序列化库,以使对象更加节省空间,但仍然可以快速访问。(Java和Scala)
  • 若计算数据集的函数不是很消耗资源,或则这些函数过滤了数据集中大量的数据,不要把数据保存在磁盘。另外,重新计算分区可能与从磁盘读取分区一样快。
  • 如果要快速的进行故障恢复,请使用复制的存储级别( replicated storage levels)(例如,如果使用Spark来处理来自Web应用程序的请求)。所有存储级别都会通过重新计算丢失的数据提供完全容错,但复制的存储级别允许您继续在RDD上运行任务,而无需等待重新计算丢失的分区。

Persist and Cache原理分析

Persisting一个RDD意味着物理化RDD(通常通过将其保存在执行器(executor)的内存中),以便在当前作业期间重用。Spark会记住一个RDD的血缘(RDD Lineage)。这样,若RDD的其中一个persisting分区丢失,就可以继续spark任务来重新计算它。作业结束后,persist函数接受一个StorageLevel参数,该参数指定应如何存储RDD。

Spark提供了许多不同的存储级别作为常量,但每个存储级别(storage level)都是基于如何存储RDD的五个属性创建的:useDisk,useMemory,useOfHeap,deserialized和replication。在存储级别调用toString将显示它包含的选项。在Spark的文档中有一个关于persist的现有存储选项的列表,如上一节所示。

在StorageLevel类中对以上的各种Storagelevel进行了封装。通过该对象,来判断是否要将RDD写入到外部存储,是否需要进行序列化,是否需要再多个节点中复制RDD的副本。

useDisk

包含DISK的存储级别标志(例如MEMORY_AND_DISK)启用此功能。

默认情况下,如果分区数据不适合在内存,它们将被写出到磁盘,并且在使用persist RDD时需要重新计算。

持久化到磁盘可以确保避免重新计算那些额外的大分区。但是,从磁盘读取数据是时间密集的操作,因此如果重新计算的成本特别高,则对磁盘的持久性才显得更加重要。

如果您希望RDD不保存在内存,则允许写入磁盘可能会有所帮助。但是,如果重新计算分区的成本不高(它们是简单的映射并且不减小数据的大小),重新计算某些分区而不是从磁盘读取实际上可能会更快。

useMemory

若设置了该变量,RDD会直接写入到内存中,否则会写入磁盘。

若设置了DISK_ONLY则该选项被设置为false。缓存的大多数速度优势来自于将RDD保留在内存中,因此如果重用是为了重复计算的快速访问,那么选择在内存中存储分区的存储选项可能是个好主意。 然而,在某些情况下,仅disk-only persistence是有意义的,例如,当计算比在本地磁盘中读取更昂贵或者网络文件系统特别慢时(例如对于某些对象存储)。

useOfHeap

若设置了该选项,则RDD会保存在执行器外部的存储系统中。

可以通过存储选项off_heap来启用该属性。如果内存是一个严重的问题,或者群集有噪声并且有分区被写入磁盘,则此选项可能很有用。

deserialized

若设置了该选项,RDD会被当成java的序列化对象进行保存。

通过该选项可以让保存的RDD更节约空间。

如果您的RDD太大而无法在内存中保留,请首先尝试使用MEMORY_ONLY_SER选项对其进行序列化。这将使RDD快速访问,但会减少存储它所需的内存。

replication

replication(副本数)变量是一个整数,用于控制要存储在群集中的持久数据的副本数。

默认情况下,此值设置为1; 但是,以_2结尾的序列化选项(如DISK_ONLY_2)会跨两个节点复制每个分区。 使用此选项可确保更快的容错能力。 但是,请注意,复制持久性会导致无需复制的持久性空间和速度成本增加一倍。 复制通常仅在嘈杂群集或错误连接的实例中是必需的,其中异常可能发生故障。 如果您在发生故障时没有时间重新计算,例如在提供实时Web应用程序时,它也可能很有用。

persist 和 cache 实现分析

persist()函数的实现

在类RDD中,对RDD进行缓存的函数定义如下:

/**
   * Persist this RDD with the default storage level (`MEMORY_ONLY`).
   */
  def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)
  /**
   * Persist this RDD with the default storage level (`MEMORY_ONLY`).
   */
  def cache(): this.type = persist()

可以看到,若直接调用persist()或cache()函数,默认的StorageLevel是MEMORY_ONLY。也就是默认是保存在内存中。

有的可以指定缓存RDD的StorageLevel,函数定义如下:

  def persist(newLevel: StorageLevel): this.type 

最终各个函数都会调用下面的函数来实现其功能:

  private def persist(newLevel: StorageLevel, allowOverride: Boolean): this.type 

该函数会把RDD的storageLevel变量设置为新的level,如下:

    storageLevel = newLevel

unpersist

该函数的实现如下:

  def unpersist(blocking: Boolean = true): this.type = {
    logInfo("Removing RDD " + id + " from persistence list")
    sc.unpersistRDD(id, blocking)
    storageLevel = StorageLevel.NONE
    this
  }

该函数会调用SparkContext的unpersistRDD函数,从所有节点的内存和磁盘上删除指定RDD的数据。并把该RDD的StorageLevel设置为NONE。

总结

本文介绍了Spark的persist/cache的原理,并对其实现进行了分析。

猜你喜欢

转载自blog.csdn.net/zg_hover/article/details/83241435