Spark - 第12章 弹性分布式数据集


        当结构化(高级)API无法解决遇到的业务或工程问题的时候,就需要使用Spark的低级API,特别是弹性分布式数据集(RDD)、SparkContext和分布式共享变量(例如累加器和广播变量)

什么是低级API?

        有两种低级API:一种用于处理分布式数据(RDD),零一种用于分发和处理分布式共享变量(广播变量和累加器)

何时使用低级API?

  • 当在高级API中找不到所需的功能时,例如要对集群中数据的物理放置进行非常严格的控制时
  • 当需要维护一些使用RDD编写的遗留代码库时
  • 当需要执行一些自定义共享变量操作时
            由于Spark所有工作负载都将编译成这些基本原语,因此学习低级API有助于我们更好的理解这些工具。当调用一个DataFrame的转换操作时,实际上等价于一组RDD的转换操作。当你开始调试越来越复杂的工作负载,这种对等价转换操作的理解可以帮你更容易完成任务。
            借助低级API去使用一些遗留代码,或者实现一些自定义分区程序,或在数据流水线的执行过程中更新和跟踪变量的值。为了防止在使用时弄巧成拙,这些工具提供了更细粒度的控制。

如何使用低级API?

        SparkContext是低级API函数库的入口,可以通过SparkSeesion来获取SparkContext,SparkSession是用于在Spark集群上执行计算的工作。

关于RDD

        无论是DataFrame还是Dataset,运行的所有的Spark代码都将编译成一个RDD。 简单来说,RDD是一个只读不可变的且已分块的记录集合,并可以被并行处理。RDD与DataFrame不同,DataFrame中每个记录即是一个结构化的数据行,各字段已知且schema已知,而RDD中的记录仅仅是程序员选择的Java、Scala或Python对象。
        正因为RDD中每个记录仅仅是一个Java或Python对象,因此能完全控制RDD,即能以任何格式在这些对象中存储任何内容。这使你具有很大的控制权,同时也带来一些潜在问题。比如,值之间的每个操作和交互都必须手动定义,也就是说,无论实现什么任务,都必须从底层开发。另外,因为Spark不像对结构化API那样清楚地理解记录的内部结构,所以往往需要用户自己写优化代码。比如,Spark的结构化API会自动以优化后的二进制压缩格式存储数据,而在使用低级API时,为了实现同样的空间效率和性能,你就需要在对象内部实现这种压缩格式,以及针对该格式进行计算的所有低级操作。同样,像重排过滤和聚合等这类SparkSQL中自动化的优化操作,也需要你手动实现。
        RDD API与Dataset类似,所不同的是RDD不在结构化数据引擎上进行存储或处理。然而,在RDD和Dataset之间来回转换的代价很小,因此可以同时使用两种API来取长补短。

RDD类型

        很多的RDD子类是DataFrame API优化物理执行计划时用到的内部表示。作为用户,一般只会创建两种类型的RDD:“通用”型RDD或提供附加函数的key-value RDD,例如基于key的聚合函数。这两种类型的RDD是最常使用的,两者都是表示对象的集合,但是key-value RDD支持特殊操作并支持按key的自定义数据分片。两者都只是表示对象的集合,但key-value RDD具有特殊的操作以及按key自定义分区的概念。
        每个RDD具有以下五个主要内部属性:

  • 数据分片(Partition)列表
  • 作用在每个数据分片的计算函数
  • 描述与其他RDD的依赖关系列表
  • (可选)为key-value RDD配置的Partition(分片方法,如hash分片)
  • (可选)优先位置列表,根据数据的本地特性,指定了每个Partition分片的处理位置偏好(例如,对于一个HDFS文件来说,这个列表就是每个文件块所在的节点)

        这些属性决定了Spark的所有调度和执行用户程序的能力,不同RDD都各自实现上述的每个属性,并允许你定义新的数据源。
        它支持两种类型(算子)操作:惰性执行的转换操作和立即执行的动作操作,都是以分布式的方式来处理数据。它们的工作方式与DataFrame和Dataset上的转换操作和动作操作相同,但是RDD中没有“行”的概念,RDD的单个记录只是原始的Java/Scala/Python对象,因此你必须手动处理这些数据,而不能使用结构化API中的函数库。对于Scala和Java而言,其性能基本是相同的,主要的性能开销花费在处理原始对象上。但对于Python来说,使用RDD会极大地影响性能。

何时使用RDD?

        一般来说,除非有非常非常明确的理由,否则不要手动创建RDD。它们是很低级的API,虽然它提供了大量的功能,但是缺少结构化API中许多可用的优化。在绝大多数情况下,DataFrame比RDD更高效、更稳定并且具有更强的表达能力。
        当你需要对数据的物理分布进行细粒度控制(自定义数据分区)时,可能才需要使用RDD。

Dataset和使用Case Class转换的RDD

        两者的不同之处在于,虽然它们可以执行相同的功能,但是Dataset可以利用结构化API提供的丰富功能和优化,无需选择是在JVM类型还是Spark类型上进行操作。你可以采用其中最简单或最灵活的方式,这样就有了两全其美之法。

创建RDD

在DataFrame,Dataset和RDD之间进行交互操作

        只要调用DataFrame或Dataset的rdd方法,就可以将它们转换成RDD。并且可以使用相同的方法从RDD中创建DataFrame或Dataset,操作方法就是调用RDD的toDF方法。

从本地集合中创建RDD

        要从集合中创建RDD,需要调用SparkContext(在SparkSession中)中的parallelize方法,该方法会将位于单个节点的数据集合转换成一个并行集合。在创建该并行集合时,还可以显式指定该并行集合的分片数量。另一个功能是,可以命名此RDD以方便将其展示在Spark UI上。

从数据源创建

        尽管从数据源或文本文件都能创建RDD,但通常最好使用数据源API来创建。RDD中没有像DataFrame中“Data Source API”这样的概念,使用RDD中主要是用来定义它们的依赖结构和数据分片方案。

操作RDD

        操作RDD的方式与操作DataFrame的方式非常相似,核心区别在于RDD操作的是原始Java或Scala对象而非Spark类型,以及还缺乏一些可帮助你简化计算的“辅助”方法或函数。你必须自己定义每个过滤器filter、映射函数map functions、聚合aggregation,以及其他想被作为函数使用的任何操作。

转换操作

        大多数情况下,转换操作与结构化API中的转换操作在功能上相同。就像使用DataFrame和Dataset一样,需要在RDD上指定转换操作来创建一个新的RDD,这也将导致新的RDD依赖于原有的RDD。

distinct

        在RDD上调用distinct方法用于删除RDD中的重复项。

filter

        过滤器filter等同于创建一个类似SQL中的where字句。
        可以通过RDD中的记录来查看谓词函数匹配,该匹配的函数被用来作为过滤函数时,只需要返回一个布尔类型值,而其输入应该是用户给定的行。和Dataset API一样,它返回的是本地数据类型。这是因为我们从不将数据强制转换为Row类型,也不在收集数据后做数据转换操作。

map

        指定一个函数,将给定数据集中的记录一条一条地输入该函数处理以得到你期望的结果。

flatMap

        flatMap是对上面的map函数的一个简单扩展。在映射操作中,有时每个当前行会映射为多行,例如,将一组单词由flatMap映射为一组字符。由于每个单词由多个字符,因此需要使用flatMap来展开它。flatMap映射要求输出是一个可扩展的迭代器。

排序

        要对RDD进行排序,必须使用sortBy方法,同其他RDD操作一样,可以通过一个指定函数从RDD对象中读取值,然后对该值进行排序。

随机分割

        通过randomSplit方法可以将一个RDD随即切分成若干个RDD,这些RDD组成一个RDD的Array返回。randomSplit方法有两个参数:一个是包含权重的Array(randomSplit方法根据weight权重值进行切分,权重越高划分得到的元素较多的几率就越大。数组的长度即为划分成RDD的数量,注意,weight数组内weight权重值的加和应为1),第二个随机数种子(seed,可选参数)。返回一个可以被操作的RDD数组。

动作操作

        就像在DataFrame和Dataset中的动作操作一样,KDD的动作操作用来触发具体的转换操作。动作操作或者将数据收集到驱动器,或者将其写到外部数据源。

reduce

        reduce方法指定一个函数将RDD中的任何类型的值“规约”为一个值。

count

        count方法用于获取数据集中的元素个数。

countApprox

        它是count方法的一个近似方法,用于返回大概的计数结果,但它必须在指定时间内执行完成(当超过指定时间时,返回一个不准确的近似结果)。
        confidence(置信度)表示近似结果的误差区间包括真实值的概率,也就是说,如果以0.9的置信度重复调用countApprox方法,则表示在90%的情况下的近似计数结果中为真实的计数值。置信度取值必须在[0,1]范围内,否则会抛出异常。

countApproxDistinct

        countApproxDistinct函数用来计算去重后的值的大约个数。它有两种实现,都是基于streamlib实现的这篇文章的方法“HyperLogLog in Practice Algorithmic Engineering of a State-of-the-Art Cardinality Estimation Algorithm”。
        在第一种函数实现中,传入函数的参数是相似精度。值与值之间的相似度达到该参数指定值,则将其看作是一样的值。相似精度值越小,代表值与值之间越相似,计数结果可能越大。该值要求必须大于0.000017。
        另一个函数实现中,则可以基于两个参数来指定相似精度:一个是用于“常规”数据,另一个是用于稀疏数据。 这两个参数分别是p和sp,其中p表示准确率(precision),sp表示稀疏准确率(sparse precision)。相似精度约为1.054/sqrt(2p)。设定非零值sp(sp>p)能减少内存消耗,并在处理低相似性数据时提高准确性。p和sp均为整数。

countByValue

        此方法用于对给定RDD中的值的个数进行计数,它需要将结果集加载到驱动器的内存中来实现计数。因为整个结果map映射集合都会加载到驱动器的内存中,所以只有当结果映射集预计很小时才使用此方法。因此,只有在总行数较少或不同Key数量较少的情况下,才适合使用此方法。

countByValueApprox

        它和countByValue函数的功能是一样的,只是它返回的是一个近似值。该函数必须在指定的timeout(第一个参数)时间内返回结果(如果超过timeout时间时,则会返回一个未完成的结果)
        置信度表示返回结果集在误差范围内包含真实值的概率,也就是说,如果以0.9的置信度重复调用countByValueApprox函数,则表示期望有90%的概率在结果中包含真实的计数值。置信度取值必须在[0,1]范围内,否则将抛出异常。

first

        first方法返回数据集中的第一个值。

max和min

        max和min方法则分别返回最大值和最小值。

take

        take和它的派生方法的功能是从RDD中读取一定数量的值。具体执行流程是:首先扫描一个数据分区,然后根据该分区的实际返回结果的数量来预估还需要再读取多少个分区。
        这个函数有很多变体,例如takeOrdered,takeSample和top函数。takeSample函数用于从RDD中获取指定大小的随机样本,并可以指定withReplacement(采样过程是否允许替换)、返回样本数量和随机数种子这3个参数。按默认排序返回前几个值时,top函数与takeOrdered函数返回值的排序顺序相反。

保存文件

        保存文件指将RDD写入纯文本文件。对于RDD,不能以常规的方式读取它并“保存”到数据源中。而是必须遍历分区才能将每个分区的内容保存到某个外部数据库,这是一种低级方法,它展现了在更高级的API中所执行的基础操作。Spark会把RDD中每个分区都读取出来,并写到指定位置中。

saveAsTextFile

        要将RDD保存到文本文件中,只需指定文件路径和压缩编码器(可选参数)即可。要设置一个压缩编码器,则必须从Hadoop中导入适当的编码器。在org.apache.hadoop.io.compress库中可找到这些编解码器。

SequenceFiles

        Saprk最初是由Hadoop生态系统发展而来的,因此它与各种Hadoop工具紧密集成。sequenceFile是一种由二进制键值对(key-value)组成的文件,它也是Hadoop MapReduce作业中常见的输入/输出格式。
        Spark中可以使用saveAsObjectFile方法或者显示写出键值对的方式将RDD写入sequenceFile格式文件中。

Hadoop文件

        你可以将RDD保存为多种不同的Hadoop文件格式,并可指定类、输出格式、Hadoop配置和压缩方式。

缓存

        缓存RDD的原理与缓存DataFrame和Dataset相同,你可以缓存或持久化RDD。默认情况下,仅对内存中的数据进行缓存和持久化。
        可以通过org.spache.spark.stprage.StorageLevel来指定单例对象的任何存储级别,存储级别包括内存(memory only)、仅磁盘(disk only)、堆外内存(off heap)。可以对存储级别进行查询。

检查点

        DataFrame API中没有检查点(checkpointing)这个概念。检查点是将RDD保存到磁盘上的操作,以便将来对此RDD的引用能直接访问磁盘上的那些中间结果,而不需要从其源头重新计算RDD。它与缓存类似,只是它不存储在内存中,只存储在磁盘上。这在执行迭代计算时很有用。
        当引用此RDD时,它将从检查点直接获得而非从源数据重新计算而来,这是个很有用的优化。

通过pipe方法调用系统命令操作RDD

        pipe方法可能是Spark最有趣的方法之一,通过pipe方法,可以利用流水线技术调用外部进程来生成RDD。将每个数据分区交给指定的外部进程来计算得到结果RDD,每个输入分区的所有元素被当作另一个外部进程的标准输入,输入元素由换行符分隔。最终结果由该外部进程的标准输出生成,标准输出的每一行产生输出分区的一个元素。空分区也会调用一个外部进程。

mapPartitions

        Spark在实际执行代码时是基于每个分区运行的。实际上在集群中我们也是每次处理一个分区,而不是具体的一行。这意味着我们能按照每个分区进行操作,处理单元为整个分区。在RDD的整个子数据集上执行某些操作很有用,你可以将属于某类的值收集到一个分区中,或分组到一个分区上,然后对整个分组进行操作。
        RDD上的map函数实际上是基于MapPartitionsRDD来实现的,map只是mapPartitions基于行操作的一个别名,mapPartitions函数每次处理一个数据分区(可以通过迭代器来遍历该数据分区)

foreachPartiotion

        mapPartitions函数需要返回值才能正常执行,但foreachPartition函数不需要。foreachPartition函数仅用于迭代所有的数据分区,与mapPartitions的不用在于它没有返回值,这使得它非常适合像写入数据库这样的操作(不需要返回计算结果)。实际上,许多数据源连接器也是基于此函数实现的。如果需要创建自己的文本文件源,可以基于一个随机ID指定输出到临时目录中来实现。

glom

        glom是一个有趣的函数,它用于将数据集中的每个分区都转换为数组。当需要将数据收集到驱动器并想为每个分区创建一个数组时,就很适合用glom函数实现。但是,这可能会导致严重的稳定性问题,因为当有很大的分区或大量分区时,该函数很容易导致驱动器崩溃。

小结

        本章介绍了RDD API的基础知识,包含单个RDD的操作。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值