RDD 编程
弹性分布式数据集(Resilient Distributed Dataset,简称 RDD),每个RDD被分为多个分区,这些分区运行在多个不同节点上。
RDD创建
RDD创建有两种方式:
1、读取外部数据来创建RDD,如
lines = sc.textFile("README.md")
2、通过parallelize()对一个集合进行并行化。
RDD操作
RDD操作包括:转化操作(transformation)和行动操作(action)。
一、转化操作
1、转化操作本质:
转化操作会由一个RDD生成另一个新的RDD。
>>> pythonLines = lines.filter(lambda line: "python" in line)
2、惰性计算
转化操作会惰性计算RDD,意思是只有第一次在一个行动操作中用到该转化操作时才会真正计算。像first()操作,只会找到符合条件的第一个文件并读取,而不是读取所有文件。
sc.textFile也是惰性的,只有在需要时才会读取。
3、数据持久化:RDD.persist()
默认情况,RDD在每次行动操作时进行重新计算,如果多个行动中使用同一个RDD,可以使用数据持久化RDD.persist()将数据在内存中缓存下来,也可以保存在磁盘中。
4、谱系图(lineage graph)
spark使用谱系图记录不同RDD之间的依赖关系,spark需要用这些信息计算每个RDD,也可以在持久化RDD丢失部分数据时进行恢复。
二、行动操作
行动操作会把最终求得的结果返回到驱动器,或者写入外部存储。
>>> pythonLines.first()
u'## Interactive Python Shell'
三、向Spark传递函数
1、在 Scala 中,可以把定义的内联函数、方法的引用或静态方法传递给Spark。
2、所传递的函数及其引用的数据需要是可序列化的(实现了 Java 的 Serializable 接口)。
3、与Python类似,传递一个对象的方法或者字段时,会包含对整个对象的引用。可以把需要的字段放到一个局部变量中,来避免传递包含该字段的整个对象
常见的转化操作和行动操作
一、针对各个元素的转化操作
1、map()
map()接收一个函数,把这个函数应用于RDD中的每个操作,将函数返回结果作为RDD中对应的元素值。
val input = sc.parallelize(List(1, 2, 3, 4))
val result = input.map(x => x * x)
println(result.collect().mkString(","))
2、filter()
filter()接收一个函数,将RDD中满足该函数的元素放入新的RDD中。
val lines = sc.parallelize(List("hello world", "hi"))
val words = lines.flatMap(line => line.split(" "))
words.first() // 返回"hello
函数 | 作用 |
---|---|
map() | 将函数应用于每个元素,返回一个新的RDD |
flatMap() | 将函数应用于每个元素,并将迭代器内所有内容构成一个RDD,通常用来分词 |
filter() | 返回一个由通过filter()函数的元素组成的RDD |
sample() | 采样,以及是否替换 |
二、伪集合操作
RDD不是严格意义上的集合,但它支持很多数学上的集合操作。
函数 | 作用 |
---|---|
RDD.distinct() | 生成只包含不同元素的新RDD |
RDD.union(rdd) | 返回包含两个RDD所有元素的RDD |
RDD.intersection(rdd) | 返回两个RDD共有元素,性能比union差 |
RDD.cartesian(rdd) | 返回两个RDD的笛卡尔乘积 |
RDD.subtract(rdd) | 返回只存在于第一个RDD中的所有元素 |
rdd.sample(withReplacement,fraction,[seed]) | 对RDD采样,以及是否替换 |
三、行动操作
1、reduce操作
接收一个函数,该函数(如累加)操作两个RDD元素,并返回一个同样类型的新元素。
val sum = rdd.reduce((x, y) => x + y)
2、fold()操作
fold()与reduce()类似,接收与reduce()接收的函数签名相同的函数,再加上一个“初始值”来作为每个分区第一次调用时的结果。初始值应是所提供操作的==单位元素==。
返回的类型与操作输入相同。
也就是说,使用这个函数对这个初始值进行多次计算,不会改变结果。
当我们需要不同类型的返回结果时,需要使用map操作惊奇转化为需要的类型。
3、aggregate()
aggregate()能够返回与输入类型不同的结果,函数需要我们提供返回值的类型。
val result = input.aggregate((0, 0))(
(acc, value) => (acc._1 + value, acc._2 + 1),
(acc1, acc2) => (acc1._1 + acc2._1, acc1._2 + acc2._2))
val avg = result._1 / result._2.toDouble
4、collect()
collect()操作将会返回整个RDD的内容,要求单台机器的内存能够保存下所有内容。
5、take(n)
返回RDD中的n个元素,并尝试只访问尽量少的分区,因此会得到一个不均衡的集合
6、top()
如果数据定义了顺序,top()从RDD中获取前几个元素,top()会使用数据的默认顺序,但也可以使用自己的比较函数。
7、takeSample(withPeplacement,num,seed)
从数据中获取一个采样,并制定是否替换
函数 | 作用 |
---|---|
collect() | 返回RDD所有元素 |
count() | 统计RDD中元素个数 |
countByValue() | 各元素在RDD中出现的次数 |
take(n) | 从RDD中返回n个元素 |
takeOrdered(n) | 从RDD中按照提供的顺序返回前n个元素 |
takeSample() | 从RDD中返回一些元素,以及是否替换 |
reduce() | 并行整合RDD中所有数据 |
fold(zero)(func) | 与reduce一样,但要提供初始值 |
aggregate(zeroValue)(seqOp,combOp) | 和reduce()相似,但返回不同类型的值 |
foreach() | 对RDD中每个元素使用给定函数 |
四、在不同RDD类型间的转换
有些函数只能用于特定类型的RDD,比如mean()和variance()只能用于在数值RDD上,而join()只能用于键值对RDD上。
在scala中,将RDD转化为特定函数的RDD是由隐式转换自动处理的,需要==引入import org.apache.spark.SparkContext. 来==使用这些转换。
持久化(缓存)
persist()
scala两次执行
val result = input.map(x => x*x)
println(result.count())
println(result.collect().mkString(","))
为了避免多次计算同一个RDD,可以让Spark对数据进行持久化。如果一个有持久化数据的节点发生故障,Spark会在需要用到缓存的数据时重算丢失的数据分区。如果希望节点故障的情况不会拖累我们的执行速度,也可以把数据备份到多个节点上。
在 Scala和 Java中,默认情况下persist()会把数据以序列化的形式缓存在JVM的堆空间中。在Python中,我们会始终序列化要持久化存储的数据。
持久化级别默认是以序列化后的对象存储在JVM堆空间中。
当我们把数据写到磁盘或堆外存储上时,也总是使用序列化后的数据。
级别 | 使用的空间 | CPU时间 | 是否在内存中 | 是否在磁盘上 | 备注 |
---|---|---|---|---|---|
MEMORY_ONLY | 高 | 低 | 是 | 否 | |
MEMORY_ONLY_SER | 低 | 高 | 是 | 否 | |
MEMORY_AND_DISK | 高 | 中等 | 部分 | 部分 | 如果内存中放不下,则溢写到磁盘上 |
MEMORY_AND_DISK_SER | 低 | 高 | 部分 | 部分 | 溢出到磁盘上,序列化后的数据 |
DISK_ONLY | 低 | 高 | 否 | 是 |
如果要缓存的数据太多, 内存中放不下, Spark 会自动利用最近最少使用(LRU)的缓存策略把最老的分区从内存中移除。 对于仅把数据存放在内存中的缓存级别,下一次要用到已经被移除的分区时,这些分区就需要重新计算。
但是对于使用内存与磁盘的缓存级别的分区来说,被移除的分区都会写入磁盘。
缓存不必要的数据会导致有用的数据被移出内存,带来更多重算的时间开销。
RDD还有一个方法叫作 unpersist(),调用该方法可以手动把持久化的 RDD 从缓存中移除。