sparkRDD编程常用函数方法

RDD创建

RDD:弹性分布式数据集。

在 Spark 中,对数据的所有操作不外乎创建 RDD、转化已有 RDD 以及调用 RDD 操作进行求值。而在这一切背后,Spark 会自动将 RDD 中的数据分发到集群上,并将操作并行化执行。

可以使用两种方法创建 RDD:

  • 读取一个外部数据集,

    • val lines = sc.textFile("/path/to/README.md")

  • 在驱动器程序里分发驱动器程序中的对象集合(比如 list 和 set)。

    • val lines = sc.parallelize(List("pandas", "i like pandas"))

    • 不常用,因为需要把整个数据集先放在一台机器的内存中。


RDD操作

创建完成后,RDD支持两种类型的操作:

  • 转化操作(transformation):由一个RDD生成一个新的RDD,比如 map() 和 filter()。Spark 会使用谱系 图(lineage graph)来记录这些不同 RDD 之间的依赖关系。

    val inputRDD = sc.textFile("log.txt")
    val errorsRDD = inputRDD.filter(line => line.contains("error"))

  • 行动操作(action):对RDD计算出一个结果,并把结果返回到驱动器程序中或存到外部存储系统中,比如 count() 和 first()。


    # 使用行动操作对错误进行计数
    println("Input had " + badLinesRDD.count() + " concerning lines")
    println("Here are 10 examples:")
    badLinesRDD.take(10).foreach(println)

区分一个函数是转化操作还是行动操作,可以看返回值类型:转化操作返回的是 RDD,而行动操作返回的是其他的数据类型。

区别两种操作的原因是,Spark是惰性计算RDD的,只有第一次在一个action中用到时,才会真正计算。默认情况下,Spark的RDD会在每次action时重新计算,需要重用同一个RDD,可以用RDD.persist()缓存。默认不进行持久化是为了节省存储空间。

总结,Spark的工作方式:

  1. 从外部数据创建出输入 RDD。

  2. 使用诸如 filter() 这样的转化操作对 RDD 进行转化,以定义新的 RDD。

  3. 告诉 Spark 对需要被重用的中间结果 RDD 执行 persist() 操作。

  4. 使用行动操作来触发一次并行计算,Spark 会对计算进行优化后再执行。


向Spark传递函数

在 Scala 中,我们可以把定义的内联函数、方法的引用或静态方法传递给 Spark,就像 Scala 的其他函数式 API 一样。

我们还要考虑其他一些细节,比如所传递的函数及其引用的数据需要是可序列化的(实现了 Java 的 Serializable 接口)。

除此以外,传递一个对象的方法或者字段时,会包含对整个对象的引用(可能很大)。我们可以把需要的字段放到一个局部变量中,来避免传递包含该字段的整个对象,

例子:


class SearchFunctions(val query: String) {
 def isMatch(s: String): Boolean = {
   s.contains(query)
}
 def getMatchesFunctionReference(rdd: RDD[String]): RDD[String] = {
 // 问题:"isMatch"表示"this.isMatch",因此我们要传递整个"this"
    rdd.map(isMatch)
  }
 def getMatchesFieldReference(rdd: RDD[String]): RDD[String] = { // 问题:"query"表示"this.query",因此我们要传递整个"this" rdd.map(x => x.split(query))
}
 def getMatchesNoReference(rdd: RDD[String]): RDD[String] = {
// 安全:只把我们需要的字段拿出来放入局部变量中 val query_ = this.query
 rdd.map(x => x.split(query_))
}
}

如果在 Scala 中出现了 NotSerializableException,通常问题就在于我们传递了一个不可序列 化的类中的函数或字段。记住,传递局部可序列化变量或顶级对象中的函数始终是安全的。


常见的转化操作

  • map():接收一个函数,把这个函数用于 RDD 中的每个元素,将函数的返回结果作为结果RDD 中对应元素的值。

  • flatMap():对每个输入元素生成多个输出元素。不过返回的不是一个元素,而是一个返回值序列的迭代器。

  • filter():接收一个函数,并将 RDD 中满足该函数的 元素放入新的 RDD 中返回。

  • sample(withRe placement, fra ction, [seed]):对 RDD 采样,以及是否替换

尽管 RDD 本身不是严格意义上的集合,但它也支持许多数学上的集合操作,比如合并和相交操作。

RDD 中最常缺失的集合属性是元素的唯一性,因为常常有重复的元素。如果只 要唯一的元素,我们可以使用 RDD.distinct() 转化操作来生成一个只包含不同元素的新 RDD,不过开销很大。

常见的集合操作:

  • distinct():去重,distinct操作的开销很大,因为它需要将所有数据通过网络进行 混洗(shuffle),以确保每个元素都只有一份。

  • union(other):它会返回一个包含两个 RDD 中所有元素的 RDD。

  • intersection(other) 方法,只返回两个 RDD 中都有的元素。性能比union要差很多,因为它需要去重。

  • subtract(other) :移除。接收一个 RDD 作为参数,返回 一个由只存在于第一个 RDD 中而不存在于第二个 RDD 中的所有元素组成的 RDD。和 intersection() 一样,它也需要数据混洗。

  • cartesian(other) :计算两个 RDD 的笛卡儿积。转化操作会返回所有可能的 (a, b) 对,其中 a 是源 RDD 中的元素,而 b 则来自另一个 RDD。求大规模 RDD 的笛卡儿积开销巨大。


常见的行动操作

  • reduce():接收一个函数作为参数,这个函数要操作两个相同元素类型的 RDD 数据,并返回一个同样类型的新元素。

  • fold():接收一个与 reduce() 接收的函数签名相同的函数,再加上一个 “初始值”来作为每个分区第一次调用时的结果。你所提供的初始值应当是你提供的操作 的单位元素;也就是说,使用你的函数对这个初始值进行多次计算不会改变结果(例如 +对应的 0,* 对应的 1,或拼接操作对应的空列表)。

  • 注意:fold() 和 reduce() 都要求函数的返回值类型需要和我们所操作的 RDD 中的元素类型相同。例如,在计算平均值时,需要记录遍历过程中的计数以及元素的数量,这就需要我们返回一 个二元组。可以先对数据使用 map() 操作,来把元素转为该元素和 1 的二元组,也就是我 们所希望的返回类型。这样 reduce() 就可以以二元组的形式进行归约了。

  • aggregate() :返回值类型可以与所操作的 RDD 类型不同。需要提供我们期待返回的类型的初始值。然后通过一个函数把 RDD 中的元素合并起来放入累加器。考虑到每个节点是在本地进行累加的,最终,还需要提供第二个函数来将累加器两两合并。
    # 计算 RDD 的平均值
    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

  • collect():它会将整个 RDD 的内容返回,要求所有数据都必须能一同放入单台机器的内存中。一般在单元测试中使用。

  • take(n) :返回 RDD 中的 n 个元素,并且尝试只访问尽量少的分区,因此该操作会得到一个不均衡的集合。需要注意的是,这些操作返回元素的顺序与你预期的可能不一样。

  • top() :从 RDD 中获取前几个元素。top() 会使用数据 的默认顺序,但我们也可以提供自己的比较函数。

  • takeSample(withReplacement, num, seed) :从数据中获取一个采样,并指定是否替换。

  • foreach():对 RDD 中的每个元素进行操作,不把 RDD 发回本地,不把任何结果返回到驱动器程序中。

  • count(): 用来返回元素的个数。

  • countByValue():返回一个从各值到值对应的计数的映射表。


RDD类型转换

有些函数只能用于特定类型的 RDD,比如 mean() 和 variance() 只能用在数值 RDD 上, 而 join() 只能用在键值对 RDD 上。

在 Scala 中,将 RDD 转为有特定函数的 RDD(比如在 RDD[Double] 上进行数值操作)是 由隐式转换来自动处理的。我们需要加上 import org.apache.spark. SparkContext._ 来使用这些隐式转换。可以在 SparkContext 对象的 Scala 文档中查看所列出的隐式转换。这些隐式转换可以隐式地将一个 RDD 转为各种封装类,比如 DoubleRDDFunctions(数值数据的 RDD)和 PairRDDFunctions(键值对 RDD),这样我们就有了诸如 mean() 和variance() 之类的额外的函数。


持久化(缓存)

Spark RDD 是惰性求值的,而有时我们希望能多次使用同一个 RDD。

默认情况下 persist() 会把数据以序列化的形式缓存在 JVM 的堆空间中。

unpersist()方法可以手动把持久化的 RDD 从缓存中移除。

可以为 RDD 选择不同的持久化级别:

如有必要,可以通过在存储级别的末尾加上“_2”来把持久化数据存为两份

例子:
    import org.apache.spark.storage.StorageLevel
    val result = input.map(x => x * x)

    # persist()调用本身不会触发强制求值。
    result.persist(StorageLevel.DISK_ONLY)
    println(result.count())
    println(result.collect().mkString(","))


如果要缓存的数据太多,内存中放不下,Spark 会自动利用最近最少使用(LRU)的缓存策略把最老的分区从内存中移除。对于仅把数据存放在内存中的缓存级别,下一次要用到 已经被移除的分区时,这些分区就需要重新计算。

但是对于使用内存与磁盘的缓存级别的分区来说,被移除的分区都会写入磁盘。不论哪一种情况,都不必担心作业因为缓存太多数据而被打断。不过,缓存不必要的数据会导致有用的数据被移出内存,带来更多重算的时间开销。


猜你喜欢

转载自blog.csdn.net/weixin_38405636/article/details/80677024