Spark RDD

RDD最初是由伯克利大学的几个教授提出的,原始论文Resilient Distributed Datasets: A Fault-Tolerant Abstraction for In-Memory Cluster Computing可以在这里下载,非常值得认真研究。RDD(Resilient Distributed DataSet)是Spark最核心的抽象。它代表的是元素的集合,Spark的文档中这么描述RDD:

A Resilient Distributed Dataset (RDD), the basic abstraction in Spark. Represents an immutable, partitioned collection of elements that can be operated on in parallel.

RDD论文中对RDD的描述:

Resilient Distributed Datasets (RDDs) are a distributed memory abstraction that lets programmers perform in-memory computations on large clusters in a fault-tolerant manner.

可以把RDD理解为Scala集合的分布式版本,Scala集合位于单个JVM,而RDD通过分区分布在多个JVM,这些JVM可能跨越不同的物理机器节点

下图是RDD的一个示意图:

spark-rdd-partitioned-distributed.png-21.5kB

RDD的特点

RDD这种抽象是整个Spark的核心,具有如下特定:

  • immutable: 不可变(只读)
  • resilient: 弹性(容错)
  • distributed:分布式(数据存储在整个集群的各个机器上)
  • In-Memory:数据位于内存中(Spark提供了其他的持久化选项)
  • Lazy evaluated:有Action触发时才会执行真正的操作
  • Cacheable:内存不足以容纳数据时,可以缓存到磁盘
  • Typed:每个RDD都有类型,例如RDD[String],RDD[(int,String)]
  • patitioned:RDD的数据划分为多个Partition,分布在集群中,每个JVM(Executor)一个Partition。

不可变

RDD支持在其上做很多实用的数据操作,例如map,reduce等。但是这些操作不会修改原来的RDD,而是会产生一个新的RDD或者其他计算结果。一旦创建,RDD就不可修改,称为不可变的。类似于Java中的String,Google提供的基础库Guava中有很多不可变的集合类型,位于com.google.common.collection包下面。状态可变通常会引入复杂性,但是Spark RDD的不可变使得实现容错的方式比较直接。

弹性(容错)

Spark内置的容错和恢复机制使得RDD具备了弹性,当某些节点失败时,Spark可以重新计算得到RDD。在一般的分布式框架中,容错通常采用副本机制来实现,即把同一份数据在多台机器上存储副本,当其中的一些副本损坏时,可以从其他节点中恢复,HDFS中的数据容错就是采用这种机制。RDD则不一样,它通过记录创建RDD的一系列操作来达到容错,也就是记录RDD如果从原始数据演变成当前状态,不可变的特性提供了最基础的保障。当节点故障时,只需要重新计算位于该节点的数据(分区)。一个不太恰当的比喻,RDD记住的是最终答案是如何推倒出来的(记住的是RDD的lineage),而不是像HDFS把最终答案背下来。内部实现中,重新计算RDD通过compute方法进行:

compute(split: Partition , context : TaskContext): Iterator[T]

当请求一个RDD时,如果当前缓存中没有这个RDD,则调用compute计算。如果缓存中已有该RDD,则直接通过CacheManager获取。

分布式与分区

RDD三个词中另一核心词是分布式,一个集合的数据是分布在整个集群中的,更具体地说是执行上下文中,即Executor所在的JVM中。但这一点对用户来说是透明的,对用户而言,操作RDD和操作原生的list,map没有太大的不同。这里的透明不是绝对的,为了优化性能,我们可以控制数据集的分区,例如数据集分为几个partition,每个partition如何分布在集群中,也可以控制RDD分区的持久化机制。

一个RDD被分为若干个分区,分区是并行的基本单元,分区数量可以通过getNumPartitions:Int方法获取。可以在Spark管理界面4040端口查看到RDD的具体分区情况:

QQ截图20160726174526.jpg-135.1kB

rdd-detail0.jpg-243.1kB

可以通过repartition或者coalesce操作来控制分区数量,在创建RDD时也可以指定分区数量,例如:

val nums = sc.parallelize( 1 to 1000 , 10)

第二个参数指定该RDD的分区数量为10. 在分区内部,数据是串行处理的。保存为外部文件时,也是分区的,类似result-part-000。RDD分布式的特点为并行计算提供了基础。


RDD类型

下面是一些常见的RDD类型:

  • ParallelCollectionRDD: Scala集合并行化而来的RDD
  • CoGroupedRDD
  • HadoopRDD:读取来自HDFS的数据
  • MapPartitionsRDD:map,filter,flatMap等操作返回的RDD
  • CoalescedRDD:调用repartition或coalesce等操作的结果
  • ShuffledRDD: Shuffle后的结果,例如repartition等操作
  • PipedRDD:piping元素构成的RDD
  • PairRDD:key-value 对,例如groupByKey之后的结果
  • DoubleRDD: Double类型
  • SequenceFileRDD: HDFS的SequenceFile创建而来的RDD

转换与动作

RDD通常在下面两种类型中使用:

  • 迭代式算法:机器学习或者图计算中的算法
  • 交互式分析:即席查询(ad-hoc queries)等数据挖掘工具

Spark提供了2种类型的操作:转换和动作。转换操作在已有的RDD上执行数据操作,产生新的RDD,例如filter,map,union等。动作则在RDD上触发计算,计算通常是为了返回结果给调用方或者写入到稳定的存储介质中,例如count,saveAsTextFile。一般可以根据返回类型来判断一个操作是转换还是动作。

理解转换最重要的一点是其惰性操作(lazy operation),转换操作只有在调用Action的时候才会真正发生。当Action被触发时,Spark检查RDD的lineage,并使用这些信息构建一个操作流图(graph of operations),执行这些计算流程得到最终的RDD。lineage的中文翻译是血统、世系,也就是说记住RDD的祖宗十八代,记录其如何从最原始的数据集中演变成当前状态,感受一下这张图:

image_1aoicp44fdl014q9156i1j9eb939.png-115kB

能够根据lineage重新计算RDD,是基于RDD的不可变特性,如果RDD是可变的,不同时间点的值是不一样的,那将无从还原。相比于采用副本实现的容错机制,RDD的容错其实是以不可变为代价的,在HDFS中,即使文件在集群中有多个副本,但是一旦追加新的内容,HDFS会自动帮助我们实现副本同步,也就是说数据是可变的,虽然大多数HDFS操作都是追加类型。计算RDD的整个过程通常使用有向无环图(DAG)来表示。例如,下图和对应的代码展示的是一个过滤日志的操作:

val lines = sc.textFile("hdfs://...")
val errors = lines.filter(_.startWith("ERROR"))
val hdfsErros = errors.filter(_.contains("HDFS"))
val times = hdfsErros.map(_.split("\t")(3)

Transformation是作用在RDD上的懒操作,产生一个或者多个RDD:

transformation: RDD => RDD
transformation: RDD => Seq[RDD]

可以把转换理解为一个函数,接受一个RDD输入,输出一个或者多个RDD。有2中类型的转换操作:

  • narrow transformation:
    结果RDD中的Partition只能自己父RDD的一个Partition,例如map,filter。 这种narrow transformation可以组织为一个stage,称为pipeling。
  • wide transformation:
    结果RDD的Partition来自父多个RDD计算的结果,例如groupByKey,reduceByKey等操作。wide transformation有时候也称为shuffle transformation。为了使得key相同的元组都到同一个分区中(被同一个Task处理),Spark必须执行RDD shuffle,shuffle在集群中传输数据,其结果是一个新的Stage和一系列新的分区。

定义RDD的几个方面

一个RDD由5个主要的方面来定义:
1)List of Parent RDDs: RDD依赖的父级RDD
RDD的依赖可以通过dependencies方法获取:

nums.dependencies.map(_.rdd).foreach(println)

依赖又分为shuffle dependency和narrow dependency

2)组成RDD的partition数组
RDD的partitions返回该RDD的分区。

3)作用于分区上计算函数
各种转换及Action,能够在RDD上进行何种操作,很大程度上决定了这种RDD是否好用。

4)定义如何分区的Partitioner
Partitioner决定RDD如何分区:

val partitiner: Option[Partitioner]

常见的有HashPartitioner等。

5)位置偏好:分区更倾向于放在那些节点上
位置偏好(locality pregrence or placement preference)是指RDD分区在HDFS中Block的存储位置,可以通过下面的方法获取:

getPreferredLocations(split: Partition): Seq[String]

RDD位于SparkContext中,一般一个SparkContext对应一个Spark应用。RDD有一个特定的名字和ID。不同SparkContext之间的RDD不能共享,即RDD不能在不同的应用之间共享,只能在同一个应用(SparkContext)内部的作业之间共享。

RDD的id创建时被分配,可以通过id属性来访问,名字可以通过name方法,并且name是可以被修改的:

val nums = sc.parallelize(1 to 200)
nums.id
nums.name
nums.name = "meaningful name"

在Spark Shell中打印出来的是RDD的Debug信息,可以通过toDebugString获取:

nums.toDebugString

Filter操作

filter转换用于过滤RDD元素。filter方法在RDD类定义,其签名如下:

class RDD[T] {
  // 其他方法
  def filter( f : (T) => Boolean: RDD[T]
}

RDD类定义了一个泛型参数T,类似于Java中的RDD<T>。事实上Spark提供的Java API就是这样的。filter的参数接受的是一个函数,这个函数接受类型T的元素,返回一个布尔值。filter方法最终返回的是另一个同类型的RDD。
Scala中,函数的声明使用的是关键字def,类似于声明不变量的val和声明可变量的var。可以这样来读这个方法:

声明一个叫做filter的函数,接受一个函数类型的参数,返回一个同类型的RDD。

filter返回的RDD类型与原始RDD是一样的,它只是移除了不满足条件(通过传入的函数来判断)的元素,这里的移除是逻辑上的概念,而不是真正移除RDD元素,因为RDD是不可变的。然后使用剩下的元素构建一个新的RDD,原来的RDD保持不变(也不可以变)。

在参数的定义中,我们看到是个函数,名称叫f。参数类型是T,返回Boolean。T表名该参数类型与RDD定义的泛型T是一样的,例如RDD是Int类型(RDD[Int]),则参数类型就为Int,filter返回的RDD元素类型也是Int。例如:

val nums = sc.parallelize( 1 to 10)
val numsEven = nums.filter( num => num % 2 ==0)
numsEven.foreach(println(_))

SparkContext的parallelize方法把集合并行化,即把集合分布到集群中。在Spark Shell中,我们可以看到nums的类型是ParallelCollectionRDD:

nums: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[3] at parallelize at <console>:21

过滤之后的numsEven是MapPartitionsRDD:

numsEven: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[4] at filter at <console>:23

这是因为filter操作会在整个集群中并发运行,即执行的是Map任务(Spark中有Map Shuffle和Result两类Task)。
foreach这个Action针对每个元素执行println操作:

2 
4 
6 
8 
10

Predicate Function

filter函数接受的参数是一个predicate函数,predicate是断言的意思:

That which is affirmed or denied

在函数式编程中,断言函数定义为返回true或者false的函数,断言函数基于给定的原书是否满足某些条件来返回结果。

函数在Scala中是一等公民,可以被存储在变量中,可以作为其他函数的输入参数,或者作为函数的返回值。如果一个函数接收一个函数类型的参数,或者返回一个函数,就称为高阶函数(higher order function)。

上述中,我们传给filter的是一个匿名函数,但是我们也可以先定义:

def isEven(num Int) = { num % 2 == 0 }

REPL提示我们:

isEven: (num:Int) Boolean

我们也可以把函数定义存储在一个变量中:

val isEvenInVal = (num:Int) => num % 2 == 0

然后传递给filter:

val numsEven = nums.filter(isEvenInVal)
<think>好的,用户想了解Spark RDD的使用和概念。首先我需要回顾一下RDD的基本定义和核心特性。根据引用[3],RDD是弹性分布式数据集的缩写,是Spark对不可变分布式数据集的抽象。这意味着RDD代表一个可以并行操作的元素集合,具有容错性。 接下来,我需要解释RDD的五大特性,包括分区、依赖关系、计算函数、分区器以及优先位置。这些都是RDD的核心概念,用户需要清楚这些特性如何支持容错和高效计算。 然后要讲RDD的创建方式。常见的有从外部数据源加载,比如HDFS或本地文件系统,或者通过转换已有的RDD生成新的RDD。需要举一些例子,比如parallelize方法或者textFile方法。 关于RDD的操作,分为转换(Transformations)和动作(Actions)。转换是惰性的,只有触发动作时才会执行。这里可以举一些常用的转换和动作的例子,如map、filter、reduceByKey,以及collect、count等。同时要注意区分宽依赖和窄依赖,宽依赖会触发shuffle,影响性能。 容错机制方面,RDD通过血统(Lineage)来恢复丢失的分区。每个RDD记录了自己是如何从其他RDD转换而来的,这样在节点失败时可以根据这些信息重新计算丢失的数据,而不需要复制多份数据。 另外,用户可能对RDDSpark生态系统中的位置感兴趣。比如,DStream是Spark Streaming的基础,由一系列RDD组成,如引用[1]和[2]提到的。而结构化流(Structured Streaming)则是基于DataFrame,优化更多,但底层可能还是利用了RDD的特性。 需要提到RDD的适用场景,比如需要低延迟操作、精细控制数据分区,或者使用非结构化数据时。同时也要指出当需要更高级优化时,DataFrame或Dataset可能更合适,如引用[5]提到的转换器和预测器的例子,虽然那更多是ML库的内容,但能说明生态系统的多样性。 后,生成的相关问题需要覆盖RDD与其他组件的比较,比如DStream和DataFrame,以及优化方法、容错机制和适用场景。这样用户如果有进一步的问题,可以继续深入。</think>### Spark RDD 核心概念与使用详解 #### 一、RDD 基础定义 **弹性分布式数据集(Resilient Distributed Dataset, RDD)** 是 Spark 的底层核心抽象,代表一个**不可变、分区的数据集合**,支持并行操作[^3]。其核心特性可总结为五大要素: - **分区列表**:数据被划分为多个分区(Partition),每个分区在集群节点上分布式存储 - **依赖关系**:记录父 RDD 到子 RDD 的转换关系(窄依赖/宽依赖) - **计算函数**:用于从父 RDD 计算当前分区的函数 - **分区器**:定义数据分布规则(如 HashPartitioner) - **优先位置列表**:标识数据分片的佳计算位置(数据本地性优化) #### 二、RDD 创建方式 1. **从外部数据源加载**: ```python # 从本地文件创建 rdd = sc.textFile("hdfs://path/to/file.txt") # 从集合创建 rdd = sc.parallelize([1,2,3,4,5]) ``` 2. **通过转换操作生成**: ```python mapped_rdd = rdd.map(lambda x: x*2) filtered_rdd = rdd.filter(lambda x: x>3) ``` #### 三、核心操作类型 | 操作类型 | 特点 | 示例 | |---------|------|------| | **Transformations**(转换) | 惰性执行,生成新 RDD | `map()`, `filter()`, `groupByKey()` | | **Actions**(动作) | 触发实际计算 | `collect()`, `count()`, `saveAsTextFile()` | | **宽依赖操作** | 涉及数据重分布(Shuffle) | `join()`, `reduceByKey()` | #### 四、容错机制 通过**血统(Lineage)**实现容错: $$ \text{RDD}_n = f(\text{RDD}_{n-1}) = f_{n} \circ f_{n-1} \circ \cdots \circ f_1(\text{原始数据}) $$ 当某个分区丢失时,Spark 可根据这个依赖链重新计算该分区[^3]。 #### 五、应用模式示例 ```python # 词频统计经典案例 lines = sc.textFile("hdfs://...") words = lines.flatMap(lambda line: line.split(" ")) pairs = words.map(lambda word: (word, 1)) counts = pairs.reduceByKey(lambda a,b: a+b) counts.saveAsTextFile("hdfs://output") ``` #### 六、与流处理的关联 在 Spark Streaming 中,DStream 本质上是**时间维度上的 RDD 序列**: $$ \text{DStream} = \{ \text{RDD}_t \mid t \in \text{时间窗口} \} $$ 每个微批处理实际上是对 RDD 的操作[^1][^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值