Spark RDD分区知识总结

Spark RDD分区知识总结

1. RDD

1.1 RDD定义

RDD(Resilient Distributed Dataset)——弹性分布式数据集,是Spark中最基本的数据抽象

  • 不可变(只读)
  • 分区
  • 自动容错
  • 位置感知调度
  • 可伸缩

1.2 RDD属性

(1)一组分区(Partition),即数据集的基本组成单位。对于RDD来说,每个分区都会被一个计算任务处理,并决定并行计算的粒度。用户可以在创建RDD时指定RDD的分片个数,如果没有指定,那么就会采用默认值。默认值就是程序所分配到的CPU Core的数目。

(2)一个计算每个分区的函数。Spark中RDD的计算是以分区为单位的,每个RDD都会实现compute函数以达到这个目的。compute函数会对迭代器进行复合,不需要保存每次计算的结果。

(3)RDD之间的依赖关系。RDD的每次转换都会生成一个新的RDD,所以RDD之间就会形成类似于流水线一样的前后依赖关系。在部分分区数据丢失时,Spark可以通过这个依赖关系重新计算丢失的分区数据,而不是对RDD的所有分区进行重新计算。

(4)一个Partitioner,即RDD的分区函数。当前Spark中实现了两种类型的分片函数,一个是基于哈希的HashPartitioner,另外一个是基于范围的RangePartitioner。只有对于于key-value的RDD,才会有Partitioner,非key-value的RDD的Parititioner的值是None。Partitioner函数不但决定了RDD本身的分区数量,也决定了parent RDD Shuffle输出时的分区数量。

(5)一个列表,存储存取每个Partition的优先位置(preferred location)。对于一个HDFS文件来说,这个列表保存的就是每个Partition所在的块的位置。按照“移动数据不如移动计算”的理念,Spark在进行任务调度的时候,会尽可能地将计算任务分配到其所要处理数据块的存储位置。

1.3 RDD操作

1.3.1 RDD创建

创建RDD的两种方式

  • 已存在的Scala集合创建,sc.parallelize
  • 读取外部存储系统创建,例如sc.textFile

1.3.2 RDD操作

RDD操作分为两大类

  • 转换(transform)——从一个现有的数据集,创建一个新的数据集,例如map,filter,flatMap等
  • 执行(action)——在数据集上并行计算出一个值,返回给Driver端,例如reduce,collect,count等

1.3.3 RDD缓存

RDD.persist——标记RDD在下一次计算的时候进行持久化操作,以便在后续计算中重用

  • MEMORY_ONLY:默认选项,RDD的(分区)数据直接以Java对象的形式存储于JVM的内存中,如果内存空间不足,某些分区的数据将不会被缓存,需要在使用的时候重新计算。
  • MEMORY_AND_DISK:RDD的数据直接以Java对象的形式存储于JVM的内存中,如果内存空间不中,某些分区的数据会被存储至磁盘,使用的时候从磁盘读取。
  • MEMORY_ONLY_SER:RDD的数据(Java对象)序列化之后存储于JVM的内存中(一个分区的数据为内存中的一个字节数组),相比于MEMORY_ONLY能够有效节约内存空间(特别是使用一个快速序列化工具的情况下),但读取数据时需要更多的CPU开销;如果内存空间不足,处理方式与MEMORY_ONLY相同
  • MEMORY_AND_DISK_SER:相比于MEMORY_ONLY_SER,在内存空间不足的情况下,将序列化之后的数据存储于磁盘。
  • DISK_ONLY:仅仅使用磁盘存储RDD的数据(未经序列化)。
  • MEMORY_ONLY_2, MEMORY_AND_DISK_2等:以MEMORY_ONLY_2为例,MEMORY_ONLY_2相比于MEMORY_ONLY存储数据的方式是相同的,不同的是会将数据备份到集群中两个不同的节点,其余情况类似。
  • OFF_HEAP:与MEMORY_ONLY_SER类似,但是存储在非堆的内存中,需要开启非堆内存。

RDD.cache等价于RDD.persist(StorageLevel.MEMORY_ONLY)

1.3.4 RDD检查点

RDD.checkpoint()——设置RDD需要检查点机制

检查点机制是为了避免缓存丢失导致的重复计算开销,检查点计算在RDD计算之后重新建立一个job来完成,为了避免重复计算,推荐将需要写入检查点的RDD进行缓存。

1.4 RDD DAG

1.4.1 RDD依赖关系

  • 窄依赖——每一个parent RDD的partition最多被子RDD的一个partition使用
  • 宽依赖——多个子RDD的partition会依赖同一个parent RDD的partition

Spark RDD分区知识总结_第1张图片

1.4.2 DAG stage划分

用户提交的计算任务是一个由RDD构成的DAG,根据执行过程中是否发生shuffle操作,将DAG划分为不同的执行阶段(stage)。一个stage由一组完全独立的计算任务(task)组成,每个task对应一个partition,并行执行相同的处理逻辑;不同的stage不能并行计算,因为后面的stage需要前面stage执行shuffle操作的结果。

Spark RDD分区知识总结_第2张图片

2. 分片

RDD.getNumPartitions()——获取RDD分区数

2.1 默认分区数

相关参数

  • sc.defaultParallelism

    • spark.default.parallelism配置
    • 未配置spark.default.parallelism时,num-executors * executor-cores
  • sc.defaultMinPartitions = min(sc.defaultParallelism, 2)

  • HDFS文件分片数 = 文件大小 / HDFS block size(默认为128M或者256M)

RDD分片数

  • sc.parallelize读取scala集合生成RDD,未指定分区数,RDD分区数=sc.defaultParallism
  • sc.textFile读取本地文件生成RDD,未指定分区数,RDD分区数=max(本地文件分区数,sc.defaultMinPartitions)
  • sc.textFile读取HDFS文件生成RDD,未指定分区数,RDD分区数=max(HDFS文件分区数,sc.defaultMinPartitions)

2.2 重新分区

RDD.coalesce(numPartitions, shuffle)

  • 这个方法返回一个新的RDD,它被简化为numPartitions分区。这导致了一个窄依赖关系,例如,从1000个分区到100个分区,将不会有一个shuffle,而是100个新分区中的每一个都会使用10个当前分区。

  • 当numPartitions = 1时,计算发生在1个节点上,导致并行度下降,无法充分利用分布式环境的优势。为了避免这种情况,可以传递shuffle = true。这将添加一个shuffle步骤,但意味着当前的上游分区将并行执行(无论当前分区是什么)。

RDD.repartition(numPartitions) = RDD.coalesce(numPartitions, shuffle=True)

  • 返回一个新的RDD,该RDD恰好具有numPartitions分区。repartition这个方法可以增加或减少此RDD中的并行度,在内部,这使用shuffle来重新分配数据。
  • 如果要减少RDD中的分区数量,请考虑使用“coalesce”,这样可以避免执行shuffle。

2.3 分区器

HashPartitioner

  • Spark默认的分区器
  • key求取hash值,再对hash值对分区数partitions 取余数,如果余数<0,那么就取“余数+partitions”,作为该row对应的分区编号
  • 如果大部分key是相同的话将会导致,各partition之间存在数据倾斜的问题,极端情况下,RDD的所有row被分配到了同一个partition中
  • groupByKey为RDD生成HashPartitioner

RangePartitioner

  • 将一定范围内的数映射到某一个分区内
  • RangePartitioner分区则尽量保证每个分区中数据量的均匀,而且分区与分区之间是有序的,也就是说一个分区中的元素肯定都是比另一个分区内的元素小或者大;但是分区内的元素是不能保证顺序的。
  • reduceByKey为RDD生成RangePartitioner

自定义分区器

  • Spark允许用户通过自定义分区器灵活控制RDD分区方式
  • 继承 org.apache.spark.Partitioner
  • 实现以下方法
    • numPartitions: Int: 总分区数
    • getPartition(key: Any): Int: 返回给定键对应的分区编号(0 <= 且 <= numPartitions-1)
    • equals(): Java 判断相等性的标准方法
    • hashCode() : 当你的算法依赖于 Java 的 hashCode() 方法时,这个方法有可能会 键值对操作返回负数,需要十分谨慎,确保 getPartition() 永远返回一个非负数。

2.4 分区器继承

  • map对键操作,则子RDD不再继承父RDD的分区器,但是分区数会继承
  • mapValues只对value操作,则子RDD会继承父RDD的分区器及分区数
  • 对于两个或多个RDD的操作,生成的新的RDD,其分区方式,取决于父RDD的分区方式。如果两个父RDD都设置过分区方式,则会选择第一个父RDD的分区方式

你可能感兴趣的:(Spark,大数据,Hadoop)