0. RDD基础
- RDD(Resilient Distributed Dataset):弹性分布式数据集
- Spark中的RDD其实是一个不可变的分布式对象集合,每个RDD都被分为多个分区,这些分区运行在集群上的不同节点上。
- 在Spark中,对数据的所有操作不外乎创建RDD、转化已有RDD以及调用RDD操作进行求值。而在这一切的背后,Spark会自动将RDD的数据分发到集群上,并将操作并行化执行。
- 创建RDD:
- 读取一个外部数据集
- 在驱动器程序里分发驱动器程序中的对象集合(e.g. list、set)
创建RDD后,支持两种类型的操作:转换操作(transformation)和行动操作(action)。其中,转换操作会由一个RDD生成一个新的RDD,例如,根据谓词匹配情况筛选数据就是一个常见的转换操作。而行动操作会对RDD计算出一个结果,并把结果返回到驱动器程序中,或把结果存储到外部存储系统中。
val lines = sc.textFile("Readme.MD") // 转换操作举例 val errorLines = lines.filter(line => line.contains("error") // 行动操作举例 val firstLine = lines.first()
转换操作和行动操作的区别在于Spark计算RDD的方式不同。虽然可以在任何时候定义新的RDD,但Spark只会惰性计算这些RDD。它们只有在第一次在一个行动操作中用到时,才会真正计算。
- 默认情况下,Spark的RDD会在你每次对它们进行行动操作时重新计算。如果想在多个行动操作中重用同一个RDD,可以使用RDD.persist()让Spark把这个RDD缓存下来。
- 总的来说,每个Spark程序或shell会话都按以下方式工作:
- 从外部数据创建出输入RDD。
- 使用诸如filter()这样的转换函数对RDD进行转化,以定义新的RDD。
- 告诉Spark对需要被重用的中间结果RDD执行persist()操作
- 使用行动操作(例如count()和first()等)来触发一次并行计算,Spark会对计算进行优化后执行。
1. 常见的转换操作和行动操作
1.1 基本RDD
- map(): 转换函数map()接收一个函数,把这个函数用于RDD的每个元素,将函数的返回结果作为结果RDD中对用的值。e.g. 1)可以把我们的URL集合中的每个URL对应的主机名抽取出来 , 或者简单到只对每个数字求平方值。
filter(): 接收一个函数,并将RDD中满足该函数的元素放入新的RDD中返回。
flatMap(): 与map()类似,提供给flatMap()的函数被分别应用到了输入RDD的每个元素上。不过flatMap()返回的不是一个元素,而是一个返回值序列的迭代器。e.g. 把输入的字符串切分为单词(一个集合)
举例
对一个数据为{1, 2, 3, 3}的RDD进行基本的RDD转换操作
函数名 目的 示例 结果 map() 将函数应用于RDD中的每个元素, 将返回值构成新的RDD rdd.map(x => x + 1) {2, 3, 4, 4} floatMap() 将函数应用于RDD中的每个元素, 将返回的 迭代器的所有内容构成新的RDD。通常用来 切分单词 rdd.floatMap(x => x.to(3)) {1, 2, 3, 2, 3, 3, 3} filter() 返回一个由通过传给filter()的函数的元素组成的RDD rdd.filter(x => x != 1) {2, 3, 3} distinct() 去重 rdd.distinct() {1, 2, 3} sample(withReplacement, fraction, [seed] 对RDD采样,以及是否替换 rdd.sample(false, 0.5) 非确定的
1.2 伪集合操作
- distinct(): RDD中最常缺失的集合属性是唯一性,因为常常有重复的元素。不过,distinct()操作的开销很大,因为它需要将所有数据通过网络进行混洗(shuffle), 以确保每个元素都只有一份。
- union(other): 返回一个包含两个RDD中所有元素的RDD。但与数学中的union()操作不同的是,如果输入的RDD中含有重复数据,Spark的union()操作也会包含这些重复数据.
- intersection(other): 只返回两个RDD中都有的元素。intersection()在运行时也会去掉所有重复的元素(单个RDD中的元素也会被移除掉)。虽然intersection与union的概念类似,但是intersection的性能却要差很多,因为它需要通过网络混洗数据来发现共有的元素。
- subtract(other): 返回一个只存在第一个RDD中而不在另外一个RDD中的所有元素组成的RDD。和intersection()一样,它也需要数据混洗。
cartesian(other): 计算两个RDD的笛卡尔积,返回所有可能的(a, b)对,其中a是源RDD中的元素,b则来自另外一个RDD.
举例
对数据分别为{1, 2, 3}和{3, 4, 5}的RDD进行针对两个RDD的转换操作函数名 目的 示例 结果 union() 生成一个包含两个RDD中的所有元素的RDD rdd.union(other) {1, 2, 3, 3, 4, 5} intersection() 求两个RDD共同元素的RDD rdd.intersection(other) {3} subtract() 移除一个RDD中的内容 rdd.subtract(other) {1, 2} certesian 与另一个RDD的笛卡儿积 rdd.cartesian(other) {(1, 3), (1, 4), …. (3, 5)}
1.3 行动操作
- reduce(): 接收一个函数作为参数,这个函数要操作两个相同元素类型的RDD数据并返回一个同样类型的新元素。
- fold(): 接收一个与reduce()接收的函数签名相同的函数,再加上一个”初始值”来作为每个分区第一次调用时的结果。你所提供的初始值应当是你提供的操作的单位元素;也就是说,使用你的函数对这个初始值进行多次计算不会改变结果(例如+对应的0,*对应的1,或拼接操作对应的空列表)。
aggregate(): fold()和reduce()都要求函数的返回值类型需要和我们所操作的RDD中的元素类型相同,而aggregate()则把我们从返回值类型必须与所操作的RDD类型相同的限制中解放出来。与fold()类似,使用aggregate()时,需要提供我们期待返回的类型的初始值。
举例:
函数名 目的 示例 结果 collect() 返回RDD中的所有元素 rdd.collect() {1, 2, 3, 4} count() RDD中的元素个数 rdd.count() 4 countByValue() 各元素在RDD中出现的次数 rdd.countByValue() {(1, 1), (2, 1), (3, 2)} take(num) 从RDD中返回num个元素 rdd.take(2) {1, 2} top(num) 从RDD中返回最前面的num个元素 rdd.top(2) {3, 3} takeOrdered(num)(ordering) 从RDD中按照提供的顺序返回最前面的num个元素 rdd.takeOrdered(2)(myOrdering) {3, 3} takeSample(withReplacement, num, [seed]) 从RDD中返回任意一些元素 rdd.takeSample(false, 1) 非确定的 reduce(func) 并行整合RDD中所有数据(例如sum) rdd.reduce((x, y) => x + y) 9 fold(zero)(func) 和reduce()一样,但是需要提供初始值 rdd.fold(0)((x, y) => x + y) 9 aggregate(zeroValue)(seqOp, combOp) 和reduce()相似,但是通常返回不同类型的函数 rdd.aggregate((0, 0)((x, y) =>
(x._1 + y, x._2 + 1),
(x, y) => (x._1 + y._1, x._2 + y._2)(9, 4) foreach(func) 对RDD中的每个元素使用给定的函数 rdd.foreach(func) 无
2. 持久化(缓存)
- 为了避免多次计算同一个RDD,可以让Spark对数据进行持久化。当我们让Spark持久化存储一个RDD时,计算出RDD的节点会分别保存它们所求出的分区数据。
如下表所示,出于不同的目的,可以为RDD选择不同的持久化级别。在Scala和Java中,默认情况下,persist()会把数据以序列化的形式缓存在JVM的堆空间中。当我们把数据写到磁盘或者堆外存储上时,也总是使用序列化的数据。
org.apache.spark.storage.StorageLevel中的持久化级别
级别 使用的空间 CPU时间 是否在内存中 是否在磁盘上 备注 MEMORY_ONLY 高 低 是 否 MEMORY_ONLY_SER 低 高 是 否 MEMORY_AND_DISK 高 中等 部分 部分 如果数据在内存中放不下,则溢写到磁盘上 MOMORY_AND_DISK_SER 低 高 部分 部分 如果内存在内存中放不下,则溢写到磁盘上。在内存中存放序列化后的数据 DISK_ONLY 低 高 否 是 如果要缓存的数据太多,内存中放不下,Spark会自动利用最近最少使用(LRU)的缓存策略把最老的分区从内存中移除。
- RDD中还有一个方法叫做unpersist(),调用该方法可以手动把持久化的RDD从缓存中移除。
3. 键值对操作
- Spark为包含键值对类型的RDD提供了一些专有的操作。这些RDD被称为pair RDD。pair RDD是很多程序的构成元素,因为它们提供了并行操作各个键或跨节点重新进行数据分组的操作接口。
3.1 创建Pair RDD
当需要把一个普通的RDD转为pair RDD时,可以调用map()函数来实现,传递的函数需要返回键值对。
val pairs = lines.map(x => (x.split(" ")(0), x))
3.2 Pair RDD的转化操作
Pair RDD可以使用所有标准RDD上的可用的转换操作。由于pair RDD中包含二元组,所以需要传递的函数应当操作二元组而不是独立的元素。
以键值对集合{(1, 2), (3, 4), (3, 6)}为例来说明Pair RDD的转化操作
函数名 目的 示例 结果 reduceByKey(func) 合并具有相同键的值 rdd.reduceByKey((x, y) => x + y) {(1, 2), (3, 10)} groupByKey() 对具有相同键的值进行分组 rdd.groupByKey() {(1, [2]), (3, [4, 6])} combineByKey(createCombiner, mergeValue, mergeCombiners, partitioner) 使用不同的返回类型合并具有相同值的键 mapValues(func) 对pair RDD中的每个值应用一个函数而不改变键 rdd.mapValue(x => x + 1) {(1, 3), (3, 5), (3, 7)} flatMapValues(func) 对pair RDD中的每个值应用一个返回迭代器的函数,然后对返回的每个元素都生成一个对应原键的键值对记录。通常用于符号化 rdd.flatMapValue(x = > x to 5) {(1, 2), (1, 3), (1, 4), (1, 5), (3, 4), (3, 5)} keys() 返回一个仅包含键的RDD rdd.keys {1, 3, 3} values() 返回一个仅包含值的RDD rdd.values {2, 4, 6} sortByKey() 返回一个根据键排序的RDD rdd.sortByKey() {(1, 2), (3, 4), (3, 6)} 针对两个pair RDD的转换操作, 以rdd = {(1, 2), (3, 4), (3, 6)} 和other = {(3, 9)} 来进行说明
函数名 目的 示例 结果 subtractByKey 删除RDD中键与other RDD中的键相同的元素 rdd.subtractByKey(other) {(1, 2)} join 对两个RDD进行内连接 rdd.join(other) {(3, (4, 9)), (3, (6, 9))} rightOuterJoin 对两个RDD进行连接操作,确保右边RDD的键必须存在(右外连接) rdd.rightOuterJoin(other) {(3, (Some(4), 9)), (3, (Some(6), 9))} leftOuterJoin 对两个RDD进行连接操作,确保左边RDD的键必须存在(左外连接) rdd.leftOuterJoin(other) {(1, (2, None)), (3, (4, Some(9))), (3, (6, Some(9)))} cogroup 将两个RDD中拥有相同键的数据分组到一起 rdd.cogroup(other) {(1, ([2], [])), (3, ([4, 6], [9]))} 若只想访问pair RDD的值部分,可以使用mapValues(func)函数,功能类似于map { case(x, y): (x, func(y)) }
聚合操作
- reduceByKey()与reduce()类似,它们都接收一个函数,并使用该函数对于值进行合并。reduceByKey()会为数据集中的每个键进行并行的归约操作,每个归约操作会将键值相同的值合并起来。因为数据集中可能有大量的键,所以reduceByKey()没有被实现为向用户返回一个值的行动操作。实际上,他会返回一个由各键和对应键规约出来的结果值组成的新的RDD.
- foldByKey()与fold()类似,它们都是用一个与RDD和合并函数中的数据结构和相同的零值作为初始值。与fold()一样,foldByKey()操作所使用的合并函数对零值与另一个元素进行合并,结果仍为该元素。
- combineByKey()是最为常见的基于键进行聚合的函数。大多数基于键聚合的函数都是用它实现的。由于combineByKey()会遍历分区中的所有元素,因此每个元素的键要么没有遇到过,要么就和之前的某个元素的键相同。
- 如果这是一个新的元素,combineByKey()会使用一个叫做createCombiner()的函数来创建那个键对应的累加器的初始值。需要注意的是,这一过程会在每个分区第一次出现各个键时发生,而不是在整个RDD中第一次出现一个键时发生。
- 如果这是一个在处理当前分区之前已经遇到的键,它会使用mergeValue()方法将该键的累加器对应的当前值与这个新的值进行合并。
- 由于每个分区都是独立处理的,因此对于同一个键可以有多个累加器。如果有两个或者更多的分区都有对应同一个键的累加器,就需要使用用户提供的mergeCombiners()方法将各个分区的结果进行合并。
combineByKey()有多个参数分别对应聚合操作的各个阶段,下面举例说明:
函数定义def combineByKey[C]( createCombiner: V => C, mergeValue: (C, V) => C, mergeCombiners: (C, C) => C): RDD[(K, C)] = self.withScope { combineByKeyWithClassTag(createCombiner, mergeValue, mergeCombiners)(null) }
举例
val result = input.combineByKey( (v) => (v, 1), // createCombiner (acc: (Int, Int), v) => (acc._1 + v, acc._2 + 1) //mergeValue (acc1: (Int, Int), acc2: (Int, Int)) => (acc1._1 + acc2._1, acc1._2 + acc2._2) // mergeCombiners ).map { case (key, value) => (key, value._1 / value._2.toFloat) } result.collectAsMap().map(println(_))
并行度调优
每个RDD都有固定数目的分区,分区数决定了在RDD上执行操作时的并行度。在执行聚合或分组操作时,可以要求Spark使用给定的分区数。Spark始终尝试根据集群的大小推断出一个有意义的默认值,但是有时候你可能要对并行度进行调优来获取更好的性能表现。数据分组
对于有键的数据,一个常见的用例是将数据根据键进行分组。- groupByKey(): 如果数据已经以预期的方法提取了键,groupByKey()就会使用RDD中的键来对数据进行分组。对于一个由类型K的键和类型V的值组成的RDD,所得到的结果RDD类型会是[K, Iterable[V]]。
- groupByKey()可以用于未成对的数据上,也可以根据除键相同以外的的条件进行分组。它可以接收一个函数,对源RDD中的每个元素使用该函数,将返回结果作为键再进行分组。
- 除了对单个RDD的数据进行分组,还可以使用一个叫做cogroup()的函数对多个共享同一个键的RDD进行分组。
连接
将有键的数据与另一组有键的数据一起使用是对键值对数据执行的最有用的操作之一。连接方式多种多样:右外连接、左外连接、交叉连接以及内连接。- 普通的join操作符表示内连接。只有在两个pair RDD中都存在的键才叫输出。当一个输入对应的某个键有多个值时,生成的pair RDD会包含来自两个输入RDD的每一组对应的记录。
- 有时,我们不希望结果中的键必须在两个RDD中都存在。leftOuterJoin(other)和rightOuterJoin(other)都会根据键连接两个RDD,但是允许结果中存在一个pair RDD中所缺失的键。在使用leftOuterJoin()产生的pair RDD中,源RDD的每一个键都有对应的记录。每个键相应的值是由一个源RDD中的值与一个包含第二个RDD的值的Option对象组成的二元组。
数据排序
- 如果键有已定义的顺序,就可以对这种键值对RDD排序。当把数据排好序后,后续对数据进行collect()或save()等操作都会得到有序的数据。
- sortByKey()函数接收一个叫ascending的参数,表示我们是否想要让结果按升序排序(默认值为true)。
Pair RDD的行动操作
以键值对集合{(1, 2), (3, 4), (3, 6)} 为例说明函数 描述 示例 结果 countByKey() 对每个键对应的元素分别计数 rdd.countByKey() {(1, 1), (3, 2)} collectAsMap() 将结果以映射表的形式返回,以便查询 rdd.collectAsMap() Map{(1, 2), (3, 6)} lookup(key) 返回给定键对应的所有值 rdd.lookup(3) [4, 6]