Spark盖中盖(一篇顶五篇)-2 RDD算子详解

前方高能减速慢行!

在上一篇RDD结构已经介绍完了。虽然RDD结构是spark设计思想最重要的组成,但是没有辅助的功能只有结构又不能独立使用。真正使RDD完成计算优化的,就是今天我们要讲到的spark RDD的另一个重要组成部分RDD算子。
一、RDD算子的定义
我给RDD算子的定义是:用来生成或处理RDD的方法叫做RDD算子。RDD算子就是一些方法,在Spark框架中起到运算符的作用。算子用来构建RDD及数据之间的关系。数据可以由算子转换成RDD,也可以由RDD产生新RDD,或者将RDD持久化到磁盘或内存。
从技术角度讲RDD算子可能比较枯燥,我们举个里生活学习中的例子来类比RDD算子的作用。
完成计算需要什么呢?
需要数据载体和运算方式。数据载体可以是数字,数组,集合,分区,矩阵等。一个普通的计算器,它的运算单位是数字,而运算符号是加减乘除,这样就可以得到结果并输出了。一个矩阵通过加减乘除也可以得到结果,但是结果跟计算器的加减乘除一样吗?非也!

矩阵相乘

AB矩阵运算规则

所以说加减乘除在不同的计算框架作用是不同的,而加减乘除这样的符号就是运算方式。在spark计算框架有自己的运算单位(RDD)和自己的运算符(RDD算子)。
是不是很抽象?下面来点具体的。
二、RDD算子的使用
Spark算子非常丰富,有几十个,开发者把算子组合使用,从一个基础的RDD计算出想要的结果。并且算子是优化Spark计算框架的主要依据。
我们以top算子举例,rdd.top(n)获取RDD的前n个排序后的结果。
例如计算:文件a的2倍与文件b的TOP 3结果。

算子的计算
  1. 窄依赖优化:如图中的RDD1,2,3在Stage3中被优化为RDD1到RDD3直接计算。是否可以直接计算是由算子的宽窄依赖决定,推荐使用数据流向区分宽窄依赖: partiton流向子RDD的多个partiton属于宽依赖,父RDD的partiton流向子RDD一个partiton或多个partiton流向一个子RDD的partiton属于窄依赖。上图中的RDD3和RDD4做top(3)操作,top是先排序后取出前3个值,排序过程属于宽依赖,spark计算过程是逆向的DAG(DAG和拓扑排序下一篇介绍),RDD5不能直接计算,必须等待依赖的RDD完成计算,我把这种算子叫做不可优化算子(计算流程不可优化,必须等待父RDD的完成),Action算子(后文讲解)都是不可优化算子,Transformation算子也有很多不可优化的算子(宽依赖算子),如:groupbykey,reducebykey,cogroup,join等。
  2. 数据量优化:上图中的a文件数据乘2,为什么前面有一个filter,假设filter过滤后的数据减少到三分之一,那么对后续RDD和shuffle的操作优化可想而知。而这只是提供一个思路,并不是说有的过滤都是高效的。
  3. 利用存储算子优化Lineage:RDD算子中除了save(输出结果)算子之外,还有几个比较特别的算子,用来保存中间结果的,如:persist,cache 和 checkpoint ,当RDD的数据保持不变并被复用多次的时候可以用它们临时保存计算结果。

1). cache和persist
修改当前RDD的存储方案StorageLevel,默认状态下与persist级别是一样的MEMORY_ONLY级别,保存到内存,内存不足选择磁盘。
def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)
def cache(): this.type = persist()
这2个方法都不会触发任务,只是修改了RDD的存储方案,当RDD被执行的时候按照方案存储到相应位置。而checkpoint会单独执行一个job,并把数据写入磁盘。
注:不要把RDD cache和Dataframe cache混淆。Dataframe cache将在spark sql中介绍。
2).checkpoint
检查RDD是否被物化或计算,一般在程序运行比较长或者计算量大的情况下,需要进行Checkpoint。这样可以避免在运行中出现异常导致RDD回溯代价过大的问题。Checkpoint会把数据写在本地磁盘上。Checkpoint的数据可以被同一session的多个job共用。

三、RDD算子之间的关系
算子从否触发job的角度划分,可以分为Transformation算子和Action算子,Transformation算子不会产生job,是惰性算子,只记录该算子产生的RDD及父RDD的partiton之间的关系,而Action算子将触发job,完成依赖关系的所有计算操作。
那么如果一个程序里有多个action算子怎么办?
顺序完成action操作,每个action算子产生一个job,上一job的结果转换成RDD,继续给后续的action使用。多数action返回结果都不是RDD,而transformation算子的返回结果都是RDD,但可能是多个RDD(如:randomSplit,将一个RDD切分成多个RDD)。

一张图了解所有RDD算子之间的关系

算子的关系图

上图划分为4个大块,从上到下我们顺序讲起:

  1. 图中的RDD dependency正是RDD结构中的private var deps: Seq[Dependency[_]],dependency类被两个类继承,NarrowDependency(窄依赖)和ShuffleDependency(宽依赖)。窄依赖又分onetoonedependency和rangedependency,这是窄依赖提供的2种抽样方式1对1数据抽样和平衡数据抽样,返回值都是一个partitonid的list集合。
  2. 第二层,是提供RDD底层计算的基本算法,继承了RDD,并实现了dependency的一种或多种依赖关系的计算逻辑,并互相调用实现更复杂的功能。
  3. 最下层是Spark API,利用RDD基本的计算实现RDD所有的算子,并调用多个底层RDD算子实现复杂的功能。
  4. 右边的泛型,是scala的一种类型,可以理解为类的泛型,泛指编译时被抽象的类型。Spark利用scala的这一特性把依赖关系抽象成一种泛型结构,并不需要真实的数据类型参与编译过程。编译的结构类由序列化和反序列化到集群的计算节点取数并计算。
    Transformation:转换算子,这类转换并不触发提交作业,完成作业中间过程处理。Transformation按照数据类型又分为两种,value数据类型算子和key-value数据类型算子。
    1) Value数据类型的Transformation算子
    Map,flatMap,mapPartitions,glom,union,cartesian,groupBy,filter,distinct,subtract,sample,takeSample
    2)Key-Value数据类型的Transfromation算子
    mapValues,combineByKey,reduceByKey,partitionBy,cogroup,join,leftOuterJoin和rightOuterJoin
    Action: 行动算子,这类算子会触发SparkContext提交Job作业。Action算子是用来整合和输出数据的,主要包括以下几种:
    Foreach,HDFS,saveAsTextFile,saveAsObjectFile, collect,collectAsMap,reduceByKeyLocally,lookup,count,top,reduce,fold,aggregate
    注:上述举例算子可能不全,随着spark的更新也会不断有新的算子加入其中。

推荐阅读更多精彩内容