Spark2.2-persist checkpoint lineage解析

来源:互联网 发布:js打印当前时间 编辑:程序博客网 时间:2024/06/10 11:55

源码版本: 2.2

如有错误请指正

一、背景


    Spark中每一个RDD都记录它的血缘lineage,根据lineage,我们可以实现容错机制和数据重用。
    相比其他系统的细颗粒度的内存数据更新级别的备份或者LOG机制,RDD的Lineage记录的是粗颗粒度的特定数据Transformation操作(如filter、map、join等)行为。当这个RDD的部分分区数据丢失时,它可以通过Lineage获取足够的信息来重新运算和恢复丢失的数据分区。因为这种粗颗粒的数据模型,限制了Spark的运用场合,所以Spark并不适用于所有高性能要求的场景,但同时相比细颗粒度的数据模型,也带来了性能的提升。【from http://www.jianshu.com/p/99ebcc7c92d3】
    Spark在RDD复用方面有三种机制:cache、persist和checkpoint,这三种机制的使用场景不同。
    其中,cache是pesist的一种固定场景的实现,cache的缓存级别一定是内存,cache的实现方式也是通过调用persist完成的。
    在persist方法中,可以动态选择缓存级别,目前Spark提供了12中缓存级别,见下图 
  checkpoint是将RDD的内容以一个独立的Job写入到磁盘中,使用checkpoint前需要在SparkContext中设置一个checkpoint目录。在local模式下可以直接设置为机器文件目录,在非local模式下必须是HDFS目录(?)。如果RDD进行checkpoint操作,name它就将lineage中它的parent给切除了。
    cache和checkpoint的区别,参考Tathagata Das的回答
There is a significant difference between cache and checkpoint. Cache materializes the RDD and keeps it in memory and/or disk. But the lineage of RDD (that is, seq of operations that generated the RDD) will be remembered, so that if there are node failures and parts of the cached RDDs are lost, they can be regenerated. However, checkpoint saves the RDD to an HDFS file and actually forgets the lineage completely. This is allows long lineages to be truncated and the data to be saved reliably in HDFS (which is naturally fault tolerant by replication).
    cache物化RDD到内存或是磁盘中,同时会保留RDD的血统lineage,如果出现数据丢失,可以重新进行生成。但是checkpoint物化RDD到磁盘/HDFS后会完全切断RDD的lineage,同时checkpoint是多副本的可靠存储,通过副本实现容错可靠性。

二、Checkpoint

2.1 checkpoint写


    checkpoint的写入是通过driver program中对RDD/Dataset调用checkpoint方法进行执行的,checkpoint会触发一个Job的提交,但实际的写操作是在job执行完成之后进行的,因此强烈建议 RDD 缓存 在内存中。
    在对Dataset进行checkpoint操作时,Dataset首先会将它转换为一个checkpointed版本的Dataset,先转换为RDD,然后在RDD内部将它的checkpointData进行初始化为一个维护了自身RDD引用的ReliableRDDCheckpointData,注意,此时还不会开始往checkpoint路径写数据,checkpoint数据的写过程是通过向SparkContext提交一个Job执行的。
这里的操作仅仅只是对RDD做一个为之后的checkpoint做准备的标记,实际生成checkpoint文件时会去检查RDD中的checkpointData。 
    
    SparkContext中,RDD的action会触发job通过submitJob进行提交,submitJob方法最后一行会调用rdd对应的doCheckpoint方法,检查RDD是否需要生成checkpoint文件。
    RDD在执行doCheckpoint方法时会解析它自身的dependency,进行递归遍历。在前面一步中,提交任务之前SparkContext会先对需要进行checkpoint的RDD进行标记,并出示化出一个维护了RDD引用的ReliableRDDCheckpointData类作为RDD自身的checkpointData,当checkpointData不为空,就说明需要对这一个RDD进行checkpoint。
    实际checkpoint文件写入是通过checkpointData.get.checkpoint方法来实现的。 
    在checkpoinData中,通过doCheckpoint方法进行实际的文件写入,写入时会使用它所维护的RDD引用。
    抽象RDDCheckpointData类有下面两种不同的实现
    LocalRDDCheckpointData是在缓存层基础之上实现的,主要用于切断lineage来达到提高RDD重复计算效率的问题。
    在本例中,通过指定文件路径作为checkpoint文件路径,使用的是ReliableRDDCheckpointData类型,它将RDD的数据写入可靠存储中,当以local模式启动时可以写入到本机路径,当以集群模式启动时只能指定HDFS路径作为checkpoint文件路径。
    写入过程其实就是RDD的一个输出过程,同时,返回一个CheckpointRDD类型,在这个新类型的RDD中,就不再有依赖的父RDD,也就是,Spark批处理过程中checkpoint的主要作用是切断RDD lineage,以checkpoint的形式减少RDD重复计算的效果。这一点和Spark Streaming中的checkpoint有着很大不同,streaming中的checkpoint主要用来作为容错机制。同时,这也是checkpoint和cache/persist最大的不同。 

2.2 checkpoint读


    Spark 批处理中checkpoint的主要作用是切断RDD的lineage关系,以此来减少RDD根据lineage重复计算的目的。同时,checkpoint一般情况下会写入到可靠存储中,它的生命周期不会像cache一样受到SparkContext生命周期的影响。但也会造成,不同driver program想要在Spark batch中重用checkpoint时,不name方便。Spark batch中checkpoint使用的最佳场景还是在单个driver中对同一RDD的多个action操作进行检查点的记录。
    在Spark Job中对RDD的计算中,都会调用RDD.iterator对RDD中的每一行数据进行处理。在对RDD的数据进行迭代访问时,会先去检查改RDD的存储级别,当RDD的存储级别不为空,即可能对它进行了缓存时,会调用getOrCompute检查RDD是否被缓存起来了,否则就去检查RDD是否已存在checkpoint,如果没有checkpoint,再去根据RDD的lineage信息对它进行计算。
    在前面我们已经介绍过,checkpoint和cache一个本质的区别是,checkpoint会切断RDD的lineage信息,因此在computeOrReadCheckpoint中,如果RDD已经缓存了,那么就可以直接将这个RDD的第一个parent直接取出来进行后续的计算。 

三、Persist

3.1 persist写

    persist操作过程中,首先会在Dataset调用cacheManager来注册一个缓存的Dataset,注意,cache不会影响RDD的正常计算流程,即action操作才会触发job的提交和计算。所以在这里注册时是根据Dataset的逻辑执行计划来注册的。

    和RDD的cache默认使用内存不一样,Dataset的cache默认存储级别是DISK,因为重新计算底层表的内存列式数据代价较大。


    注意,在这一步,只是根据Dataset的逻辑执行计划在SparkSession中的ShareState中的CacheManager中进行了注册,实际上缓存的RDD都还没有进行计算。
    RDD的缓存在action操作触发计算时才会进行写入。
    主要流程和checkpoint一致,在RDD的iterator中, 
    如果发现RDD的存储级别已经被设置了,说明进行了缓存。如果在BlockManager中没有找到对应的文件,则先计算,再写入缓存中。
    主要写方法集中在BlockManager中的getOrElseUpdate方法调用的doPutIterator中,getOrElseUpdate其中一个参数为真正执行执行计算的函数
() => {  readCachedBlock = false computeOrReadCheckpoint(partition, context)}

    这个函数中直接调用了computeOrReadCheckpoint去对RDD进行计算,返回计算完成的结果

    很长的一个方法,我这里只说说几个比较重要的地方。这个方法顾名思义是用来缓存数据的,那么在执行缓存之前需要做一些准备工作。首先通过一个match确定是否要返回数据以及选择一个什么类型的BlockStore。对于BlockStore这里分了三种:memoryStore、tachyonStore、diskStore。第一种和第三种每什么疑问,至于tachyon是一个内存分布式文件系统,建立在ramdisk之上,和spark同宗同源,是spark兼容的存储系统之一,可以用来不同application之间共享rdd甚至与其他例如storm这样的框架共享内存数据,不过目前貌似还是在实验阶段,官网上有标注的。

确定了是否返回数据和BlockStore后,然后立马根据输入数据的组织结构把数据塞到BlockStore里面去,然后得到一个PutResult对象,如果配置的是会返回数据则在这里面还会带数据。随便挑一个看看,比如DiskStore的putBytes方法,可以看到这里是真的已经写进去了。附带一提,这里用的是nio哟。【http://blog.csdn.net/JoeYangY/article/details/40858067】

3.2 persist读

    persist读操作其实在写操作是耦合的,执行写操作之前都会先执行一次读操作,确认没有才会执行写。读操作同样也在RDD进行iterator迭代时调用的getOrCompute方法中

    RDD根据它自身的id和分区partition的下标构造一个blockid,向blockManager请求,如果获取到请求结果,则直接返回,如果没有获取到,则会先对RDD进行计算,写入BlockManager,同时返回。

    但是为什么在persist写操作的时候是根据Dataset的logicalPlan构造一个CacheData放入SparkSession间接维护的CacheManager中,而读的时候却是请求的BlockManager呢?

    因为CacheManager的作用并不是为了管理cache流程,CacheManager存储Dataset的logicalPlan,是为了支持SQLContext中的数据缓存功能,支持根据Dataset或是logicalPlan进行缓存数据的搜索。实际上缓存数据的存储还是由BlockManager进行管理的。

四、Lineage

   Spark的RDD对象中记录了它的血统Lineage,RDD的Lineage记录的是粗颗粒度的特定数据Transformation操作(如filter、map、join等)行为。

4.1 Lineage切入点:toDebugString

   在本地debug时,Spark提供了一种查看RDD Lineage的操作toDebugString。

    Spark在生成RDD的具体血缘关系时,对RDD的依赖进行迭代遍历,遍历时通过提供的几个方法就可以窥探出RDD Lineage的大概结构。

  1. debugSelf
  2. debugChildren
  3. firstDebugString
  4. shuffleDebugString
  5. debugString

4.2 RDD的Dependency

    关于dependency,RDD中维护了两个变量,一个是构造函数参数deps

    另一个是private变量dependencies_

    从本质上来说,实际对RDD的Lineage有用的数据结构是Dependency。Dependency抽象类中维护了对应被依赖的RDD的引用,同时根据依赖特性,分为两大类NarrowDependency和ShuffleDependency,NarrowDependency又细分为三种类型OneToOneDependency、PruneDependency和RangeDependency,分别根据依赖特性实现了自己的getParent以及一些其他辅助方法。

    这两个变量其实都是指向RDD的依赖关系,RDD提供了从deps赋值到dependencies_的转换函数。这个转换函数会将RDD的构造参数deps赋值给private成员变量dependencies_,为什么要增加一道转换操作而不直接使用呢,从下面这个函数里看出,是为了做checkpoint的lineage切断。当RDD做过checkpoint后,它的checkpointRDD成员变量会存放一个由它自身数据构造的一个CheckpointRDD,此时再去获取这个RDD的dependency时,RDD会用这个CheckpointRDD去new一个新的OneToOneDependency,也就是它自身的依赖只有它自己的Checkpoint RDD了,以此来达到切断Lineage的作用。所有对RDD依赖关系访问的操作都是直接调用dependencies方法,而不是调用RDD自身的dependency成员变量。

    针对上一个问题,可能大家会产生疑问,针对CheckPoint切断Lineage的目的是什么?我们可以首先看一下Lineage信息在RDD计算中的作用。

    首先,每一个RDD在进行数据计算时,都调用了RDD的iterator来对数据进行迭代访问。在进行迭代访问时,假设RDD没有进行cache和checkpoint,那么RDD就会使用到它的Lineage进行计算。

    实际的计算过程在RDD的compute方法中,针对不同类型的RDD实现类,有不同的实现方法。例如,针对从数据源转化而来的RDD或是Checkpoint,它们的compute方法可能是读取文件,而其他类型的RDD中的compute可能会在数据上执行一些函数。

     在这里,我们抽取集中比较典型的RDD进行分析,跟踪Lineage信息在计算过程中的真正作用。

  1. MapPartitionsRDD

    MapPartitionsRDD这个会将它的函数作用于它的父RDD中的每一个partition。 在它的compute中,我们可以看到,它在apply函数f时首先会调用firstParent[T].iterator(split, context),即先去触发它的parent的计算。


      获取一个RDD的firstParent的方法如下,获取自身的Dependency的头节点,即parent,获取该Dependency的RDD提交计算。

    这样,RDD在计算过程中就可以根据它的Lineage去恢复整个计算流程了。

4.3 Lineage小结

    Spark的数据Lineage记录了数据集之间的依赖以及操作流程,其中,数据集的依赖关系实例化存放在每一个基础数据对象RDD中,每个RDD维护它自己的计算方式。这两者结合起来实现了完整的Data Lineage。

    





原创粉丝点击