-
深入理解 RDD 的内在逻辑
-
能够使用 RDD 的算子
-
理解 RDD 算子的 Shuffle 和缓存
-
理解 RDD 整体的使用流程
-
理解 RDD 的调度原理
-
理解 Spark 中常见的分布式变量共享方式
1. 深入 RDD
-
深入理解 RDD 的内在逻辑, 以及 RDD 的内部属性(RDD 由什么组成)
1.1. 案例
-
给定一个网站的访问记录, 俗称 Access log
-
计算其中出现的独立 IP, 以及其访问的次数
val config = new SparkConf().setAppName("ip_ana").setMaster("local[6]")
val sc = new SparkContext(config)
val result = sc.textFile(“dataset/access_log_sample.txt”)
.map(item => (item.split(" ")(0), 1))
.filter(item => StringUtils.isNotBlank(item._1))
.reduceByKey((curr, agg) => curr + agg)
.sortBy(item => item._2, false)
.take(10)
result.foreach(item => println(item))
针对这个小案例, 我们问出互相关联但是又方向不同的五个问题
-
假设要针对整个网站的历史数据进行处理, 量有 1T, 如何处理?
-
如何放在集群中运行?
-
如果放在集群中的话, 可能要对整个计算任务进行分解, 如何分解?
-
移动数据不如移动计算是一个基础的优化, 如何做到?
-
在集群中运行, 需要很多节点之间配合, 出错的概率也更高, 出错了怎么办?
-
假如任务特别复杂, 流程特别长, 有很多 RDD 之间有依赖关系, 如何优化?
1.2. 再谈 RDD
-
理解 RDD 为什么会出现
-
理解 RDD 的主要特点
-
理解 RDD 的五大属性
1.2.1. RDD 为什么会出现?
-
在 RDD 出现之前, 当时 MapReduce 是比较主流的, 而 MapReduce 如何执行迭代计算的任务呢?
-
多个 MapReduce 任务之间没有基于内存的数据共享方式, 只能通过磁盘来进行共享
这种方式明显比较低效
RDD 如何解决迭代计算非常低效的问题呢?
-
在 Spark 中, 其实最终 Job3 从逻辑上的计算过程是:
Job3 = (Job1.map).filter
, 整个过程是共享内存的, 而不需要将中间结果存放在可靠的分布式文件系统中这种方式可以在保证容错的前提下, 提供更多的灵活, 更快的执行速度, RDD 在执行迭代型任务时候的表现可以通过下面代码体现
// 线性回归 val points = sc.textFile(...) .map(...) .persist(...) val w = randomValue for (i <- 1 to 10000) { val gradient = points.map(p => p.x * (1 / (1 + exp(-p.y * (w dot p.x))) - 1) * p.y) .reduce(_ + _) w -= gradient }
在这个例子中, 进行了大致 10000 次迭代, 如果在 MapReduce 中实现, 可能需要运行很多 Job, 每个 Job 之间都要通过 HDFS 共享结果, 熟快熟慢一窥便知
1.2.2. RDD 的特点
-
RDD 不仅是数据集, 也是编程模型
-
RDD 即是一种数据结构, 同时也提供了上层 API, 同时 RDD 的 API 和 Scala 中对集合运算的 API 非常类似, 同样也都是各种算子
RDD 的算子大致分为两类:
-
Transformation 转换操作, 例如
map
flatMap
filter
等 -
Action 动作操作, 例如
reduce
collect
show
等
执行 RDD 的时候, 在执行到转换操作的时候, 并不会立刻执行, 直到遇见了 Action 操作, 才会触发真正的执行, 这个特点叫做 惰性求值
RDD 可以分区
-
-
RDD 是一个分布式计算框架, 所以, 一定是要能够进行分区计算的, 只有分区了, 才能利用集群的并行计算能力
同时, RDD 不需要始终被具体化, 也就是说: RDD 中可以没有数据, 只要有足够的信息知道自己是从谁计算得来的就可以, 这是一种非常高效的容错方式
RDD 是只读的
-
RDD 是只读的, 不允许任何形式的修改. 虽说不能因为 RDD 和 HDFS 是只读的, 就认为分布式存储系统必须设计为只读的. 但是设计为只读的, 会显著降低问题的复杂度, 因为 RDD 需要可以容错, 可以惰性求值, 可以移动计算, 所以很难支持修改.
-
RDD2 中可能没有数据, 只是保留了依赖关系和计算函数, 那修改啥?
-
如果因为支持修改, 而必须保存数据的话, 怎么容错?
-
如果允许修改, 如何定位要修改的那一行? RDD 的转换是粗粒度的, 也就是说, RDD 并不感知具体每一行在哪.
RDD 是可以容错的
-
-
-
RDD 的容错有两种方式
-
-
保存 RDD 之间的依赖关系, 以及计算函数, 出现错误重新计算
-
直接将 RDD 的数据存放在外部存储系统, 出现错误直接读取, Checkpoint
-
-
1.2.3. 什么叫做弹性分布式数据集
-
分布式
-
RDD 支持分区, 可以运行在集群中
弹性
-
-
RDD 支持高效的容错
-
RDD 中的数据即可以缓存在内存中, 也可以缓存在磁盘中, 也可以缓存在外部存储中
数据集
-
-
-
RDD 可以不保存具体数据, 只保留创建自己的必备信息, 例如依赖和计算函数
-
RDD 也可以缓存起来, 相当于存储具体数据
-
2. RDD 的算子
-
理解 RDD 的算子分类, 以及其特性
-
理解常见算子的使用
-
分类
- 特点
2.1. Transformations 算子
Transformation function | 解释 |
---|---|
| |
|
|
| |
| RDD[T] ⇒ RDD[U] 和 map 类似, 但是针对整个分区的数据转换 |
| 和 mapPartitions 类似, 只是在函数中增加了分区的 Index |
| |
|
|
| |
| |
| (RDD[T], RDD[T]) ⇒ RDD[T] 差集, 可以设置分区数 |
| |
|
|
|
|
|
|
val combineRdd = rdd.combineByKey(
score => (score, 1),
(scoreCount: (Double, Int),newScore) => (scoreCount._1 + newScore, scoreCount._2 + 1),
(scoreCount1: (Double, Int), scoreCount2: (Double, Int)) =>
(scoreCount1._1 + scoreCount2._1, scoreCount1._2 + scoreCount2._2)
)
val meanRdd = combineRdd.map(score => (score._1, score._2._1 / score._2._2))
meanRdd.collect()
-
作用
-
-
对数据集按照 Key 进行聚合
调用
-
-
-
combineByKey(createCombiner, mergeValue, mergeCombiners, [partitioner], [mapSideCombiner], [serializer])
参数
-
-
-
createCombiner
将 Value 进行初步转换 -
mergeValue
在每个分区把上一步转换的结果聚合 -
mergeCombiners
在所有分区上把每个分区的聚合结果聚合 -
partitioner
可选, 分区函数 -
mapSideCombiner
可选, 是否在 Map 端 Combine -
serializer
序列化器
注意点
-
-
-
combineByKey
的要点就是三个函数的意义要理解 -
groupByKey
,reduceByKey
的底层都是combineByKey
-
aggregateByKey()
val rdd = sc.parallelize(Seq(("手机", 10.0), ("手机", 15.0), ("电脑", 20.0)))
val result = rdd.aggregateByKey(0.8)(
seqOp = (zero, price) => price * zero,
combOp = (curr, agg) => curr + agg
).collect()
println(result)
-
作用
-
-
聚合所有 Key 相同的 Value, 换句话说, 按照 Key 聚合 Value
调用
-
-
-
rdd.aggregateByKey(zeroValue)(seqOp, combOp)
参数
-
-
-
zeroValue
初始值 -
seqOp
转换每一个值的函数 -
comboOp
将转换过的值聚合的函数
-
注意点 * 为什么需要两个函数? aggregateByKey 运行将一个`RDD[(K, V)]聚合为`RDD[(K, U)]
, 如果要做到这件事的话, 就需要先对数据做一次转换, 将每条数据从`V`转为`U`, `seqOp`就是干这件事的 ** 当`seqOp`的事情结束以后, `comboOp`把其结果聚合
-
和 reduceByKey 的区别::
-
aggregateByKey 最终聚合结果的类型和传入的初始值类型保持一致
-
reduceByKey 在集合中选取第一个值作为初始值, 并且聚合过的数据类型不能改变
-
foldByKey(zeroValue)((V, V) ⇒ V)
sc.parallelize(Seq(("a", 1), ("a", 1), ("b", 1)))
.foldByKey(zeroValue = 10)( (curr, agg) => curr + agg )
.collect()
-
作用
-
-
和 ReduceByKey 是一样的, 都是按照 Key 做分组去求聚合, 但是 FoldByKey 的不同点在于可以指定初始值
调用
-
-
foldByKey(zeroValue)(func)
参数
-
-
zeroValue
初始值 -
func
seqOp 和 combOp 相同, 都是这个参数
注意点
-
-
-
FoldByKey 是 AggregateByKey 的简化版本, seqOp 和 combOp 是同一个函数
-
FoldByKey 指定的初始值作用于每一个 Value
-
join(other, numPartitions)
val rdd1 = sc.parallelize(Seq(("a", 1), ("a", 2), ("b", 1)))
val rdd2 = sc.parallelize(Seq(("a", 10), ("a", 11), ("a", 12)))
rdd1.join(rdd2).collect()
-
作用
-
-
将两个 RDD 按照相同的 Key 进行连接
调用
-
-
join(other, [partitioner or numPartitions])
参数
-
-
other
其它 RDD -
partitioner or numPartitions
可选, 可以通过传递分区函数或者分区数量来改变分区
注意点
-
-
-
Join 有点类似于 SQL 中的内连接, 只会再结果中包含能够连接到的 Key
-
Join 的结果是一个笛卡尔积形式, 例如`"a", 1), ("a", 2
和
"a", 10), ("a", 11的 Join 结果集是 `"a", 1, 10), ("a", 1, 11), ("a", 2, 10), ("a", 2, 11
-
cogroup(other, numPartitions)
val rdd1 = sc.parallelize(Seq(("a", 1), ("a", 2), ("a", 5), ("b", 2), ("b", 6), ("c", 3), ("d", 2)))
val rdd2 = sc.parallelize(Seq(("a", 10), ("b", 1), ("d", 3)))
val rdd3 = sc.parallelize(Seq(("b", 10), ("a", 1)))
val result1 = rdd1.cogroup(rdd2).collect()
val result2 = rdd1.cogroup(rdd2, rdd3).collect()
/*
执行结果:
Array(
(d,(CompactBuffer(2),CompactBuffer(3))),
(a,(CompactBuffer(1, 2, 5),CompactBuffer(10))),
(b,(CompactBuffer(2, 6),CompactBuffer(1))),
(c,(CompactBuffer(3),CompactBuffer()))
)
*/
println(result1)
/*
执行结果:
Array(
(d,(CompactBuffer(2),CompactBuffer(3),CompactBuffer())),
(a,(CompactBuffer(1, 2, 5),CompactBuffer(10),CompactBuffer(1))),
(b,(CompactBuffer(2, 6),CompactBuffer(1),Co…
*/
println(result2)
-
作用
-
-
多个 RDD 协同分组, 将多个 RDD 中 Key 相同的 Value 分组
调用
-
-
-
cogroup(rdd1, rdd2, rdd3, [partitioner or numPartitions])
参数
-
-
-
rdd…
最多可以传三个 RDD 进去, 加上调用者, 可以为四个 RDD 协同分组 -
partitioner or numPartitions
可选, 可以通过传递分区函数或者分区数来改变分区
注意点
-
-
-
对 RDD1, RDD2, RDD3 进行 cogroup, 结果中就一定会有三个 List, 如果没有 Value 则是空 List, 这一点类似于 SQL 的全连接, 返回所有结果, 即使没有关联上
-
CoGroup 是一个需要 Shuffled 的操作
-
cartesian(other)
(RDD[T], RDD[U]) ⇒ RDD[(T, U)] 生成两个 RDD 的笛卡尔积
sortBy(ascending, numPartitions)
val rdd1 = sc.parallelize(Seq(("a", 3), ("b", 2), ("c", 1)))
val sortByResult = rdd1.sortBy( item => item._2 ).collect()
val sortByKeyResult = rdd1.sortByKey().collect()
println(sortByResult)
println(sortByKeyResult)
-
作用
-
-
排序相关相关的算子有两个, 一个是`sortBy`, 另外一个是`sortByKey`
调用
-
-
sortBy(func, ascending, numPartitions)
参数
-
-
`func`通过这个函数返回要排序的字段
-
`ascending`是否升序
-
`numPartitions`分区数
注意点
-
-
-
普通的 RDD 没有`sortByKey`, 只有 Key-Value 的 RDD 才有
-
`sortBy`可以指定按照哪个字段来排序, `sortByKey`直接按照 Key 来排序
-
partitionBy(partitioner)
使用用传入的 partitioner 重新分区, 如果和当前分区函数相同, 则忽略操作
coalesce(numPartitions)
减少分区数
val rdd = sc.parallelize(Seq(("a", 3), ("b", 2), ("c", 1)))
val oldNum = rdd.partitions.length
val coalesceRdd = rdd.coalesce(4, shuffle = true)
val coalesceNum = coalesceRdd.partitions.length
val repartitionRdd = rdd.repartition(4)
val repartitionNum = repartitionRdd.partitions.length
print(oldNum, coalesceNum, repartitionNum)
-
作用
-
-
一般涉及到分区操作的算子常见的有两个,
repartitioin
和coalesce
, 两个算子都可以调大或者调小分区数量
调用
-
-
-
repartitioin(numPartitions)
-
coalesce(numPartitions, shuffle)
参数
-
-
-
numPartitions
新的分区数 -
shuffle
是否 shuffle, 如果新的分区数量比原分区数大, 必须 Shuffled, 否则重分区无效
注意点
-
-
-
repartition
和coalesce
的不同就在于coalesce
可以控制是否 Shuffle -
repartition
是一个 Shuffled 操作
-
repartition(numPartitions)
重新分区
repartitionAndSortWithinPartitions
重新分区的同时升序排序, 在partitioner中排序, 比先重分区再排序要效率高, 建议使用在需要分区后再排序的场景使用
2.2. Action 算子
Action function | 解释 |
---|---|
|
|
|
以数组的形式返回数据集中所有元素 |
|
返回元素个数 |
|
返回第一个元素 |
|
返回前 N 个元素 |
|
类似于 sample, 区别在这是一个Action, 直接返回结果 |
|
指定初始值和计算函数, 折叠聚合整个数据集 |
|
将结果存入 path 对应的文件中 |
|
将结果存入 path 对应的 Sequence 文件中 |
|
|
|
遍历每一个元素 |
-
应用
-
```scala val rdd = sc.parallelize(Seq(("手机", 10.0), ("手机", 15.0), ("电脑", 20.0))) // 结果: Array((手机,10.0), (手机,15.0), (电脑,20.0)) println(rdd.collect()) // 结果: Array((手机,10.0), (手机,15.0)) println(rdd.take(2)) // 结果: (手机,10.0) println(rdd.first()) ```
-
RDD 的算子大部分都会生成一些专用的 RDD
-
-
map
,flatMap
,filter
等算子会生成MapPartitionsRDD
-
coalesce
,repartition
等算子会生成CoalescedRDD
常见的 RDD 有两种类型
-
-
-
转换型的 RDD, Transformation
-
动作型的 RDD, Action
常见的 Transformation 类型的 RDD
-
-
-
map
-
flatMap
-
filter
-
groupBy
-
reduceByKey
常见的 Action 类型的 RDD
-
-
-
collect
-
countByKey
-
reduce
-
2.3. RDD 对不同类型数据的支持
-
理解 RDD 对 Key-Value 类型的数据是有专门支持的
-
理解 RDD 对数字类型也有专门的支持
-
一般情况下 RDD 要处理的数据有三类
- RDD 的算子设计对这三类不同的数据分别都有支持
- RDD 对键值对数据的额外支持
- RDD 对数字型数据的额外支持
3. RDD 的 Shuffle 和分区
-
RDD 的分区操作
-
Shuffle 的原理
-
分区的作用
-
RDD 使用分区来分布式并行处理数据, 并且要做到尽量少的在不同的 Executor 之间使用网络交换数据, 所以当使用 RDD 读取数据的时候, 会尽量的在物理上靠近数据源, 比如说在读取 Cassandra 或者 HDFS 中数据的时候, 会尽量的保持 RDD 的分区和数据源的分区数, 分区模式等一一对应
分区和 Shuffle 的关系
-
分区的主要作用是用来实现并行计算, 本质上和 Shuffle 没什么关系, 但是往往在进行数据处理的时候, 例如`reduceByKey`, `groupByKey`等聚合操作, 需要把 Key 相同的 Value 拉取到一起进行计算, 这个时候因为这些 Key 相同的 Value 可能会坐落于不同的分区, 于是理解分区才能理解 Shuffle 的根本原理
Spark 中的 Shuffle 操作的特点
-
-
只有
Key-Value
型的 RDD 才会有 Shuffle 操作, 例如RDD[(K, V)]
, 但是有一个特例, 就是repartition
算子可以对任何数据类型 Shuffle -
早期版本 Spark 的 Shuffle 算法是
Hash base shuffle
, 后来改为Sort base shuffle
, 更适合大吞吐量的场景
-
3.1. RDD 的分区操作
-
查看分区数
默认的分区数量是和 Cores 的数量有关的, 也可以通过如下三种方式修改或者重新指定分区数量
-
创建 RDD 时指定分区数
scala> rdd1.partitions.size
res1: Int = 6
scala> val rdd2 = sc.textFile(“hdfs:///dataset/wordcount.txt”, 6)
rdd2: org.apache.spark.rdd.RDD[String] = hdfs:///dataset/wordcount.txt MapPartitionsRDD[3] at textFile at <console>:24
scala> rdd2.partitions.size
res2: Int = 7
rdd1 是通过本地集合创建的, 创建的时候通过第二个参数指定了分区数量. rdd2 是通过读取 HDFS 中文件创建的, 同样通过第二个参数指定了分区数, 因为是从 HDFS 中读取文件, 所以最终的分区数是由 Hadoop 的 InputFormat 来指定的, 所以比指定的分区数大了一个.
scala> source.partitions.size
res0: Int = 6
scala> val noShuffleRdd = source.coalesce(numPartitions=8, shuffle=false)
noShuffleRdd: org.apache.spark.rdd.RDD[Int] = CoalescedRDD[1] at coalesce at <console>:26
scala> noShuffleRdd.toDebugString (1)
res1: String =
(6) CoalescedRDD[1] at coalesce at <console>:26 []
| ParallelCollectionRDD[0] at parallelize at <console>:24 []
scala> val noShuffleRdd = source.coalesce(numPartitions=8, shuffle=false)
noShuffleRdd: org.apache.spark.rdd.RDD[Int] = CoalescedRDD[1] at coalesce at <console>:26
scala> shuffleRdd.toDebugString (2)
res3: String =
(8) MapPartitionsRDD[5] at coalesce at <console>:26 []
| CoalescedRDD[4] at coalesce at <console>:26 []
| ShuffledRDD[3] at coalesce at <console>:26 []
±(6) MapPartitionsRDD[2] at coalesce at <console>:26 []
| ParallelCollectionRDD[0] at parallelize at <console>:24 []
scala> noShuffleRdd.partitions.size (3)
res4: Int = 6
scala> shuffleRdd.partitions.size
res5: Int = 8
1 | 如果 shuffle 参数指定为 false , 运行计划中确实没有 ShuffledRDD , 没有 shuffled 这个过程 |
2 | 如果 shuffle 参数指定为 true , 运行计划中有一个 ShuffledRDD , 有一个明确的显式的 shuffled 过程 |
3 | 如果 shuffle 参数指定为 false 却增加了分区数, 分区数并不会发生改变, 这是因为增加分区是一个宽依赖, 没有 shuffled 过程无法做到, 后续会详细解释宽依赖的概念 |
repartition
算子指定
scala> source.partitions.size
res7: Int = 6
scala> source.repartition(100).partitions.size (1)
res8: Int = 100
scala> source.repartition(1).partitions.size (2)
res9: Int = 1
1 | 增加分区有效 |
2 | 减少分区有效 |
repartition
算子无论是增加还是减少分区都是有效的, 因为本质上 repartition
会通过 shuffle
操作把数据分发给新的 RDD 的不同的分区, 只有 shuffle
操作才可能做到增大分区数, 默认情况下, 分区函数是 RoundRobin
, 如果希望改变分区函数, 也就是数据分布的方式, 可以通过自定义分区函数来实现
3.2. RDD 的 Shuffle 是什么
val sourceRdd = sc.textFile("hdfs://node01:9020/dataset/wordcount.txt")
val flattenCountRdd = sourceRdd.flatMap(_.split(" ")).map((_, 1))
val aggCountRdd = flattenCountRdd.reduceByKey(_ + _)
val result = aggCountRdd.collect
reduceByKey
这个算子本质上就是先按照 Key 分组, 后对每一组数据进行 reduce
, 所面临的挑战就是 Key 相同的所有数据可能分布在不同的 Partition 分区中, 甚至可能在不同的节点中, 但是它们必须被共同计算.
为了让来自相同 Key 的所有数据都在 reduceByKey
的同一个 reduce
中处理, 需要执行一个 all-to-all
的操作, 需要在不同的节点(不同的分区)之间拷贝数据, 必须跨分区聚集相同 Key 的所有数据, 这个过程叫做 Shuffle
.
3.3. RDD 的 Shuffle 原理
Spark 的 Shuffle 发展大致有两个阶段: Hash base shuffle
和 Sort base shuffle
-
Hash base shuffle
- Sort base shuffle
4. 缓存
-
缓存的意义
-
缓存相关的 API
-
缓存级别以及最佳实践
4.1. 缓存的意义
-
使用缓存的原因 - 多次使用 RDD
val interimRDD = sc.textFile(“dataset/access_log_sample.txt”)
.map(item => (item.split(" ")(0), 1))
.filter(item => StringUtils.isNotBlank(item._1))
.reduceByKey((curr, agg) => curr + agg) (1)
val resultLess = interimRDD.sortBy(item => item._2, ascending = true).first()
val resultMore = interimRDD.sortBy(item => item._2, ascending = false).first()
println(s"出现次数最少的 IP : r e s u l t L e s s < / s p a n > , 出 现 次 数 最 多 的 I P : < s p a n c l a s s = " h l j s − s u b s t " > resultLess</span>, 出现次数最多的 IP : <span class="hljs-subst"> resultLess</span>,出现次数最多的IP:<spanclass="hljs−subst">resultMore")
sc.stop()
1 | 这是一个 Shuffle 操作, Shuffle 操作会在集群内进行数据拷贝 |
在上述代码中, 多次使用到了 interimRDD
, 导致文件读取两次, 计算两次, 有没有什么办法增进上述代码的性能?
上述两个问题的解决方案其实都是 缓存
, 除此之外, 使用缓存的理由还有很多, 但是总结一句, 就是缓存能够帮助开发者在进行一些昂贵操作后, 将其结果保存下来, 以便下次使用无需再次执行, 缓存能够显著的提升性能.
所以, 缓存适合在一个 RDD 需要重复多次利用, 并且还不是特别大的情况下使用, 例如迭代计算等场景.
4.2. 缓存相关的 API
-
可以使用
cache
方法进行缓存
val interimRDD = sc.textFile(“dataset/access_log_sample.txt”)
.map(item => (item.split(" ")(0), 1))
.filter(item => StringUtils.isNotBlank(item._1))
.reduceByKey((curr, agg) => curr + agg)
.cache() (1)
val resultLess = interimRDD.sortBy(item => item._2, ascending = true).first()
val resultMore = interimRDD.sortBy(item => item._2, ascending = false).first()
println(s"出现次数最少的 IP : r e s u l t L e s s < / s p a n > , 出 现 次 数 最 多 的 I P : < s p a n c l a s s = " h l j s − s u b s t " > resultLess</span>, 出现次数最多的 IP : <span class="hljs-subst"> resultLess</span>,出现次数最多的IP:<spanclass="hljs−subst">resultMore")
sc.stop()
1 | 缓存 |
方法签名如下
cache(): this.type = persist()
cache 方法其实是 persist
方法的一个别名
val interimRDD = sc.textFile(“dataset/access_log_sample.txt”)
.map(item => (item.split(" ")(0), 1))
.filter(item => StringUtils.isNotBlank(item._1))
.reduceByKey((curr, agg) => curr + agg)
.persist(StorageLevel.MEMORY_ONLY) (1)
val resultLess = interimRDD.sortBy(item => item._2, ascending = true).first()
val resultMore = interimRDD.sortBy(item => item._2, ascending = false).first()
println(s"出现次数最少的 IP : $resultLess, 出现次数最多的 IP : $resultMore")
sc.stop()
1 | 缓存 |
方法签名如下
persist(): this.type
persist(newLevel: StorageLevel): this.type
persist
方法其实有两种形式, persist()
是 persist(newLevel: StorageLevel)
的一个别名, persist(newLevel: StorageLevel)
能够指定缓存的级别
val interimRDD = sc.textFile(“dataset/access_log_sample.txt”)
.map(item => (item.split(" ")(0), 1))
.filter(item => StringUtils.isNotBlank(item._1))
.reduceByKey((curr, agg) => curr + agg)
.persist()
interimRDD.unpersist() (1)
val resultLess = interimRDD.sortBy(item => item._2, ascending = true).first()
val resultMore = interimRDD.sortBy(item => item._2, ascending = false).first()
println(s"出现次数最少的 IP : r e s u l t L e s s < / s p a n > , 出 现 次 数 最 多 的 I P : < s p a n c l a s s = " h l j s − s u b s t " > resultLess</span>, 出现次数最多的 IP : <span class="hljs-subst"> resultLess</span>,出现次数最多的IP:<spanclass="hljs−subst">resultMore")
sc.stop()
1 | 清理缓存 |
根据缓存级别的不同, 缓存存储的位置也不同, 但是使用 unpersist
可以指定删除 RDD 对应的缓存信息, 并指定缓存级别为 NONE
4.3. 缓存级别
其实如何缓存是一个技术活, 有很多细节需要思考, 如下
-
是否使用磁盘缓存?
-
是否使用内存缓存?
-
是否使用堆外内存?
-
缓存前是否先序列化?
-
是否需要有副本?
如果要回答这些信息的话, 可以先查看一下 RDD 的缓存级别对象
val conf = new SparkConf().setMaster("local[6]").setAppName("debug_string")
val sc = new SparkContext(conf)
val interimRDD = sc.textFile(“dataset/access_log_sample.txt”)
.map(item => (item.split(" ")(0), 1))
.filter(item => StringUtils.isNotBlank(item._1))
.reduceByKey((curr, agg) => curr + agg)
.persist()
println(interimRDD.getStorageLevel)
sc.stop()
打印出来的对象是 StorageLevel
, 其中有如下几个构造参数
根据这几个参数的不同, StorageLevel
有如下几个枚举对象
缓存级别 | userDisk 是否使用磁盘 | useMemory 是否使用内存 | useOffHeap 是否使用堆外内存 | deserialized 是否以反序列化形式存储 | replication 副本数 |
---|---|---|---|---|---|
| false | false | false | false | 1 |
| true | false | false | false | 1 |
| true | false | false | false | 2 |
| false | true | false | true | 1 |
| false | true | false | true | 2 |
| false | true | false | false | 1 |
| false | true | false | false | 2 |
| true | true | false | true | 1 |
| true | true | false | true | 2 |
| true | true | false | false | 1 |
| true | true | false | false | 2 |
| true | true | true | false | 1 |
5. Checkpoint
-
Checkpoint 的作用
-
Checkpoint 的使用
5.1. Checkpoint 的作用
Checkpoint 的主要作用是斩断 RDD 的依赖链, 并且将数据存储在可靠的存储引擎中, 例如支持分布式存储和副本机制的 HDFS.
-
Checkpoint 的方式
- 什么是斩断依赖链
- Checkpoint 和 Cache 的区别
5.2. 使用 Checkpoint
val conf = new SparkConf().setMaster("local[6]").setAppName("debug_string")
val sc = new SparkContext(conf)
sc.setCheckpointDir("checkpoint") (1)
val interimRDD = sc.textFile(“dataset/access_log_sample.txt”)
.map(item => (item.split(" ")(0), 1))
.filter(item => StringUtils.isNotBlank(item._1))
.reduceByKey((curr, agg) => curr + agg)
interimRDD.checkpoint() (2)
interimRDD.collect().foreach(println(_))
sc.stop()
1 | 在使用 Checkpoint 之前需要先设置 Checkpoint 的存储路径, 而且如果任务在集群中运行的话, 这个路径必须是 HDFS 上的路径 |
2 | 开启 Checkpoint |
一个小细节
|
interimRDD.checkpoint()
interimRDD.collect().foreach(println(_))
1 | checkpoint 之前先 cache 一下, 准没错 |
应该在 checkpoint
之前先 cache
一下, 因为 checkpoint
会重新计算整个 RDD 的数据然后再存入 HDFS 等地方.
所以上述代码中如果 checkpoint
之前没有 cache
, 则整个流程会被计算两次, 一次是 checkpoint
, 另外一次是 collect
end