Spark高级特性 (难)
-
闭包
scala/* * 编写一个高阶函数,在这个函数要有一个变量,返回一个函数,通过这个变量完成一个计算 * */ @Test def test(): Unit = { // val f: Int => Double = closure() // val area = f(5) // println(area) // 在这能否访问到 factor,不能,因为factor所在作用域是closure()方法,test()方法和closure()方法作用域是平级的,所有不能直接访问 // 不能访问,说明 factor 在一个单独的作用域中 // 在拿到 f 的时候, 可以通过 f 间接的访问到 closure() 作用域中的内容 // 说明 f 携带了一个作用域 // 如果一个函数携带了一个外包的作用域,这种函数我们称之为闭包 val f = closure() f(5) // 闭包的本质是什么? // f 就是闭包,闭包的本质就是一个函数 // 在 Scala 中,函数就是一个特殊的类型,FunctionX // 闭包也是一个 FunctionX 类型的对象 // 所以闭包是一个对象 } /* * 返回一个新的函数 * */ def closure(): Int => Double = { val factor = 3.14 val areaFunction = (r: Int) => math.pow(r, 2) * factor // 计算圆的面积 areaFunction }
通过 closure 返回的函数 f 就是一个闭包, 其函数内部的作用域并不是 test 函数的作用域, 这种连带作用域一起打包的方式, 我们称之为闭包, 在 Scala 中
Scala 中的闭包本质上就是一个对象, 是 FunctionX 的实例
-
Spark中的闭包
分发闭包
scalasc.textFile("./dataset/access_log_sample.txt") .flatMap(item => item.split(" ")) .collect() // item => item.split(" ") 是一个函数,代表一个Task,这个Task会被分发到不同的Executor中
上述这段代码中,flatMp中传入的是另外一个函数,传入的这个函数就是一个闭包,这个闭包会被序列化运行在不同的Executor中
scalaclass MyClass { val field = "Hello" def doStuff(rdd: RDD[String]): RDD[String] = { rdd.map(x => field + x) } } /* * x => field + x 引用MyClass对象中的一个成员变量,说明它可以访问MyClass这个类的作用域, * 所以这个函数也是一个闭包,封闭的是MyClass这个作用域。 * x => field + x */
这段代码中的闭包就有了一个依赖, 依赖于外部的一个类, 因为传递给算子的函数最终要在 Executor 中运行, 所以需要 序列化 MyClass 发给每一个 Executor, 从而在 Executor 访问 MyClass 对象的属性
总结
- 闭包就是一个封闭的作用域, 也是一个对象
- Spark 算子所接受的函数, 本质上是一个闭包, 因为其需要封闭作用域, 并且序列化自身和依赖, 分发到不同的节点中运行
-
累加器
-
一个小问题
scalavar count = 0 val conf = new SparkConf().setAppName("ip_ana").setMaster("local[6]") val sc = new SparkContext(conf) sc.parallelize(Seq(1, 2, 3, 4, 5)) .foreach(count += _) println(count)
上面这段代码是一个非常错误的使用, 请不要仿照, 这段代码只是为了证明一些事情
先明确两件事,
var count = 0
是在 Driver 中定义的,foreach(count += _)
这个算子以及传递进去的闭包运行在 Executor 中这段代码整体想做的事情是累加一个变量, 但是这段代码的写法却做不到这件事, 原因也很简单, 因为具体的算子是闭包, 被分发给不同的节点运行, 所以这个闭包中累加的并不是 Driver 中的这个变量
-
全局累加器 (只能对数值型数据累加)
Accumulators(累加器) 是一个只支持
added
(添加) 的分布式变量, 可以在分布式环境下保持一致性, 并且能够做到高效的并发.原生 Spark 支持数值型的累加器, 可以用于实现计数或者求和, 开发者也可以使用自定义累加器以实现更高级的需求
scalaval conf = new SparkConf().setAppName("ip_ana").setMaster("local[6]") val sc = new SparkContext(conf) val counter = sc.longAccumulator("counter") sc.parallelize(Seq(1, 2, 3, 4, 5)) .foreach(counter.add(_)) // 运行结果: 15 println(counter.value)
注意点:
- Accumulator 是支持并发并行的, 在任何地方都可以通过
add
来修改数值, 无论是 Driver 还是 Executor - 只能在 Driver 中才能调用
value
来获取数值
在 WebUI 中关于 Job 部分也可以看到 Accumulator 的信息, 以及其运行的情况
累加器还有两个小特性, 第一, 累加器能保证在 Spark 任务出现问题被重启的时候不会出现重复计算. 第二, 累加器只有在 Action 执行的时候才会被触发.
scalaval config = new SparkConf().setAppName("ip_ana").setMaster("local[6]") val sc = new SparkContext(config) val counter = sc.longAccumulator("counter") sc.parallelize(Seq(1, 2, 3, 4, 5)) .map(counter.add(_)) // 这个地方不是 Action, 而是一个 Transformation // 运行结果是 0 println(counter.value)
- Accumulator 是支持并发并行的, 在任何地方都可以通过
-
自定义累加器
开发者可以通过自定义累加器来实现更多类型的累加器, 累加器的作用远远不只是累加, 比如可以实现一个累加器, 用于向里面添加一些运行信息
scala/** * RDD -> (1,2,3,4,5) ---> Set(1,2,3,4,5),将原先的数,累加到一个集合中 */ @Test def acc(): Unit = { val conf = new SparkConf().setMaster("local[6]").setAppName("acc") val sc = new SparkContext(conf) val numAcc = new NumAccumulator() // 注册给Spark sc.register(numAcc, "num") sc.parallelize(Seq("1", "2", "3")) .foreach(item => numAcc.add(item)) println(numAcc.value) sc.stop() } } class NumAccumulator extends AccumulatorV2[String, Set[String]] { private val nums: mutable.Set[String] = mutable.Set() // 定义类型是可变Set,否则后面的newAccumulator.nums ++= this.nums,++=会报错 /** * 告诉 Spark 框架,这个累加器对象是否是空的 */ override def isZero: Boolean = { nums.isEmpty } /** * 提供给 Spark 框架一个拷贝的累加器 */ override def copy(): AccumulatorV2[String, Set[String]] = { val newAccumulator = new NumAccumulator() nums.synchronized { newAccumulator.nums ++= this.nums } newAccumulator } /** * 帮助 Spark 框架,清理累加器的内容 */ override def reset(): Unit = { nums.clear() } /** * 外部传入要累加的内容,在这个方法中进行累加 */ override def add(v: String): Unit = { nums += v } /** * 累加器在进行累加的时候,可能每个分布式节点都有一个实例 * 在最后 Driver 进行一次合并,把所有的实例的内容合并起来,会调用这个 merge 方法进行合并 */ override def merge(other: AccumulatorV2[String, Set[String]]): Unit = { nums ++= other.value } /** * 提供给外部累加结果 * 为什么一定不可变的,因为外部有可能再进行修改,如果是可变的集合,其外部的修改会影响内部的值 */ override def value: Set[String] = { nums.toSet // 不可变 }
-
-
广播变量
-
广播变量的作用
广播变量允许开发者将一个
Read-Only
的变量缓存到集群中每个节点中, 而不是传递给每一个 Task 一个副本.- 集群中每个节点, 指的是一个机器
- 每一个 Task, 一个 Task 是一个 Stage 中的最小处理单元, 一个 Executor 中可以有多个 Stage, 每个 Stage 有多个 Task
所以在需要跨多个 Stage 的多个 Task 中使用相同数据的情况下, 广播特别的有用
只需要2个map,我们可以用广播
-
广播变量的API
方法名 描述 id 唯一标识 value 广播变量的值 unpersist 在 Executor 中异步的删除缓存副本 destroy 销毁所有此广播变量所关联的数据和元数据 toString 字符串表示 -
使用广播变量的一般套路
可以通过如下方式创建广播变量
scalaval b = sc.broadcast(1)
如果 Log 级别为 DEBUG 的时候, 会打印如下信息
scalaDEBUG BlockManager: Put block broadcast_0 locally took 430 ms DEBUG BlockManager: Putting block broadcast_0 without replication took 431 ms DEBUG BlockManager: Told master about block broadcast_0_piece0 DEBUG BlockManager: Put block broadcast_0_piece0 locally took 4 ms DEBUG BlockManager: Putting block broadcast_0_piece0 without replication took 4 ms
创建后可以使用
value
获取数据scalab.value
获取数据的时候会打印如下信息
scalaDEBUG BlockManager: Getting local block broadcast_0 DEBUG BlockManager: Level for block broadcast_0 is StorageLevel(disk, memory, deserialized, 1 replicas)
广播变量使用完了以后, 可以使用
unpersist
删除数据scalab.unpersist
删除数据以后, 可以使用
destroy
销毁变量, 释放内存空间scalab.destroy
销毁以后, 会打印如下信息
scalaDEBUG BlockManager: Removing broadcast 0 DEBUG BlockManager: Removing block broadcast_0_piece0 DEBUG BlockManager: Told master about block broadcast_0_piece0 DEBUG BlockManager: Removing block broadcast_0
-
使用
value
方法的注意方法签名
value: T
在
value
方法内部会确保使用获取数据的时候, 变量必须是可用状态, 所以必须在变量被destroy
之前使用value
方法, 如果使用value
时变量已经失效, 则会报出以下错误scalaorg.apache.spark.SparkException: Attempted to use Broadcast(0) after it was destroyed (destroy at <console>:27) at org.apache.spark.broadcast.Broadcast.assertValid(Broadcast.scala:144) at org.apache.spark.broadcast.Broadcast.value(Broadcast.scala:69) ... 48 elided
-
使用
destory
方法的注意点方法签名
destroy(): Unit
destroy
方法会移除广播变量, 彻底销毁掉, 但是如果你试图多次destroy
广播变量, 则会报出以下错误scalaorg.apache.spark.SparkException: Attempted to use Broadcast(0) after it was destroyed (destroy at <console>:27) at org.apache.spark.broadcast.Broadcast.assertValid(Broadcast.scala:144) at org.apache.spark.broadcast.Broadcast.destroy(Broadcast.scala:107) at org.apache.spark.broadcast.Broadcast.destroy(Broadcast.scala:98) ... 48 elided
-
使用code
scala/** * 资源占用比较大, 有十个对应的 value */ @Test def bc1():Unit = { // 数据,假装这个数据很大,大概一百兆 val v = Map("Spark" -> "http://spark.apache.cn", "Scala" -> "http://www.scala-lang.org") val conf = new SparkConf().setMaster("local[6]").setAppName("bc") val sc = new SparkContext(conf) // 将其中的 Spark 和 Scala 转为对应的网址 val r = sc.parallelize(Seq("Spark","Scala")) val result = r.map(item => v(item)).collect() result.foreach(println(_)) } /** * 使用广播, 大幅度减少 value 的复制 */ @Test def bc2():Unit = { // 数据,假装这个数据很大,大概一百兆 val v = Map("Spark" -> "http://spark.apache.cn", "Scala" -> "http://www.scala-lang.org") val conf = new SparkConf().setMaster("local[6]").setAppName("bc") val sc = new SparkContext(conf) // 创建广播 val bc = sc.broadcast(v) // 将其中的 Spark 和 Scala 转为对应的网址 val r = sc.parallelize(Seq("Spark","Scala")) // 在算子中使用广播变量代替直接引用集合, 只会复制和executor一样的数量 // 在使用广播之前, 复制 map 了 task 数量份 // 在使用广播以后, 复制次数和 executor 数量一致 val result = r.map(item => bc.value(item)).collect() result.foreach(println(_)) }
-