目录
DataFrame是spark1.3.0版本提出来的,spark1.6.0版本又引入了DateSet的。DataFrame、DataSet是基于RDD的,三者之间可以通过简单的API调用进行无缝切换。
RDD、DataFrame与DataSet区别
RDD
RDD弹性分布式数据集。与DataFrame,DataSet不同,RDD不支持Spark SQL。
DataFrame
在Spark中,DataFrame是一种以RDD为基础的分布式数据集,类似于传统数据库中的二维表格。与RDD和Dataset不同,DataFrame每一行的类型固定为Row,只有通过解析才能获取各个字段的值,每一列的值没法直接访问。DataFrame与Dataset均支持SparkSQL操作,比如select、groupby等
testDF.foreach{
line =>
val col1=line.getAs[String]("col1")
val col2=line.getAs[String]("col2")
}
DataFrame与RDD的主要区别:DataFrame带有schema元信息,即DataFrame所表示的二维表数据集的每一列都带有名称和类型。
DataSet
Dataset是一个由特定领域的对象组成强类型(typedrel)集合,可以使用函数(DSL)或关系运算(SQL)进行并行的转换操作。 每个Dataset 还有一个称为“DataFrame”的无类型(untypedrel)视图,它是[[Row]]的数据集。
RDD和Dataset
区别:
Dataset不使用Java序列化或Kryo,而是使用专用的Encoder编码器来序列化对象以便通过网络进行处理或传输。虽然Encoder编码器和标准序列化都负责将对象转换为字节,但Encoder编码器是动态生成的代码,并使用一种格式,允许Spark执行许多操作,如过滤,排序和散列,而无需将字节反序列化为对象
Dataset和DataFrame
区别:
Dataset是强类型typedrel的,会在编译的时候进行类型检测;而DataFrame是弱类型untypedrel的,在执行的时候进行类型检测;Dataset是通过Encoder进行序列化,支持动态的生成代码,直接在bytes的层面进行排序,过滤等的操作;而DataFrame是采用可选的java的标准序列化或是kyro进行序列化
联系:
DataFrame是Dataset中每一个元素为Row类型的特殊情况。DataFrame和Dataset实质上都是一个逻辑计划,并且是懒加载的,都包含着scahema信息,只有到数据要读取的时候,才会将逻辑计划进行分析和优化,并最终转化为RDD。
RDD、DataFrame与DataSet转化
DataFrame/Dataset转RDD:
val rdd1=testDF.rdd
val rdd2=testDS.rdd
RDD转DataFrame:
import spark.implicits._
val testDF = rdd.map {line=>
(line._1,line._2)
}.toDF("col1","col2")
一般用元组把一行的数据写在一起,然后在toDF中指定字段名
RDD转Dataset:
import spark.implicits._
case class Coltest(col1:String,col2:Int)extends Serializable //定义字段名和类型
val testDS = rdd.map {line=>
Coltest(line._1,line._2)
}.toDS
可以注意到,定义每一行的类型(case class)时,已经给出了字段名和类型,后面只要往case class里面添加值即可
Dataset转DataFrame:
//把case class封装成Row
import spark.implicits._
val testDF = testDS.toDF
DataFrame转Dataset:
import spark.implicits._
case class Coltest(col1:String,col2:Int)extends Serializable //定义字段名和类型
val testDS = testDF.as[Coltest]
这种方法就是在给出每一列的类型后,使用as方法,转成Dataset,这在数据类型是DataFrame又需要针对各个字段处理时极为方便
Spark常用算子比较
map与flatmap
- map算子会对每一条输入进行指定的操作,然后为每一条输入返回一个对象
- flatMap算子是将RDD里的每一个元素执行自定义函数f,这时这个元素的结果转换成iterator,最后将这些再拼接成一个新的RDD
mapPartition与map
map是对rdd中的每一个元素进行操作。mapPartitions则是对rdd中的每个分区的迭代器进行操作,这个分区的数据处理完后,原RDD中分区的数据才能释放,可能导致OOM。
当内存空间较大的时候建议使用mapPartition,以提高处理效率。如果在map过程中需要频繁创建额外的对象(例如将rdd中的数据通过jdbc写入数据库,map需要为每个元素创建一个链接而mapPartition为每个partition创建一个链接),则mapPartitions效率比map高的多。
reduce、reduceByKey与groupByKey
reduceByKey是transformation算子;reduce是action算子,聚合RDD中的所有元素。
reduceByKey用于对每个key对应的多个value进行merge操作,最重要的是它能够在本地先进行merge操作,并且merge操作可以通过函数自定义。groupByKey也是对每个key进行操作,但只生成一个sequence,groupByKey本身不能自定义操作函数。如果需要对sequence进行aggregation操作,那么选择reduceByKey更好。这是因为groupByKey不能自定义函数,需要先用groupByKey生成RDD,然后才能对此RDD通过map进行自定义函数操作。
reduceByKey默认在map端进行合并,即在shuffle前进行合并,如果合并了一些数据,那在shuffle时进行溢写则减少了磁盘IO,所以reduceByKey会快一些。当采用groupByKey时,由于它不接收函数,spark只能先将所有的键值对(key-value pair)都移动,这样的后果是集群节点之间的开销很大,导致传输延时。在对大数据进行复杂计算时,reduceByKey优于groupByKey。
join
join 对两个需要连接的 RDD操作。leftOutJoin(左外连接)和rightOutJoin(右外连接)相当于在join的基础上先判断一侧的RDD元素是否为空,如果为空,则填充为空。 如果不为空,则将数据进行连接运算,并返回结果。
补充:join时何时产生shuffle,何时不产生shuffle?
Spark中产生宽窄依赖的依据是shuffle,当发生shuffle时,会产生宽依赖,基本上shuffle算子都会产生宽依赖,但是join除外,在执行join算子之前如果先执行groupByKey,执行groupByKey之后,会把相同的key分到同一个分区,再执行join算子,join算子是把key相同的进行join,父RDD的分区只对应一个子RDD的分区,不一定会产生shuffle。
distinct
distinct算子将RDD中的元素进行去重操作,是宽依赖。
combineByKey
groupByKey/reduceByKey底层是由combineByKey实现的。
def combineByKey[C](
createCombiner: V => C,
mergeValue: (C, V) => C,
mergeCombiners: (C, C) => C,
partitioner: Partitioner,
mapSideCombine: Boolean = true,
serializer: Serializer = null)
createCombiner
当第一次遇到key时,调用这个函数,将key对应的V转换成C(初始化操作)mergeValue
不是第一次遇到key时,调用这个函数,将key对应的元素V合并到之前的元素C(createCombiner)上 (对这个分区中相同的key的进一步操作)- -
mergeCombiners
该函数将相同Key的C合并成一个C(这个操作在不同分区间进行)
除此之外,还包含三个默认参数:
partitioner
:分区函数,默认为HashPartitioner
mapSideCombine
:是否需要在Map端进行combine操作,类似于MapReduce中的combine,默认为true
serializer
:序列化,用于数据存储和传输
aggregateByKey
函数对PairRDD中相同Key的值进行聚合操作。因为aggregateByKey是对相同Key中的值进行聚合操作,所以aggregateByKey函数最终返回的类型还是Pair RDD,对应的结果是Key和聚合好的值;而aggregate函数直接是返回非RDD的结果,这点需要注意。
aggregateByKey()()存在两个参数列表,使用了函数柯里化:
zeroValue:表示分区内计算时的初始值,类型U要注意,最终返回的类型也必须是这个类型U
seqOp(U,Int):表示分区内计算规则,这个方法主要是做相同key在同一个partition的聚合操作,两个参数(U,int)第一个参数是开始初始值U,第二个类型是数据的value的类型,返回类型为定义的zeroValue的类型
combOp:表示分区间计算规则,根据key 对不同分区的数据进行一个聚合操作(也就是对seqOp的结果做合并操作),这个参数(U,U)这两个的类型都是seqOp返回类型
性能调优中有个方案, 使用 aggregateBykey 代替 groupbykey。因为aggregateByKey,使用map-side预聚合的shuffle操作, 相当于在Map端进行了聚合的操作,相当于MapReduce 中进行combiner
repartition和coalesce
repartition(numPartitions:Int)和coalesce(numPartitions:Int,shuffle:Boolean=false)
作用:对RDD的分区进行重新划分,repartition内部调用了coalesce,参数shuffle为true。coalesce只能减少分区,而repartition可以减少和增加。
对于coalesce:如果是生成一个窄依赖的结果,那么不会发生shuffle。比如:1000个分区被重新设置成10个分区,这样不会发生shuffle。即窄依赖:父RDD的多个分区对应一个子RDD分区。当把父RDD的分区数量增大时,比如RDD的分区是100,设置成1000,如果shuffle为false,并不会起作用。这时候就需要设置shuffle为true了,那么RDD将在shuffle之后返回一个1000个分区的RDD,数据分区方式默认是采用 hash partitioner。对于repartition:无论分区数是增加还是减少都会执行shuffle操作。
foreach与foreachPartition
foreach是对RDD中的每一个元素执行指定的函数,foreachPartition是对RDD中的每一个Partition执行指定的函数。
/**
* Applies a function f to all elements of this RDD.
*/
def foreach(f: T => Unit): Unit = withScope {
val cleanF = sc.clean(f)
sc.runJob(this, (iter: Iterator[T]) => iter.foreach(cleanF))
}
/**
* Applies a function f to each partition of this RDD.
*/
def foreachPartition(f: Iterator[T] => Unit): Unit = withScope {
val cleanF = sc.clean(f)
sc.runJob(this, (iter: Iterator[T]) => cleanF(iter))
}
可以看到方法通过clean操作(清理闭包,为序列化和网络传输做准备)进行了一层匿名函数的封装。针对foreach方法,是我们的方法被传入了迭代器的foreach(每个元素遍历执行一次函数),而对于foreachpartiton方法是迭代器被传入了我们的方法(每个分区执行一次函数。
假如我们的Function中有数据库,网络TCP等IO链接,文件流等等的创建关闭操作,采用foreachPatition方法,针对每个分区集合进行计算,更能提高我们的性能。
union与union all
union属于窄依赖,无shuffle。union all不去重,union会去重。union进行默认规则的排序,union all不进行排序。
collect
Spark的collect收集的数据在Driver JVM内存中