Spark 的两个共享变量:累加器(accumulator)与广播变量(broadcast variable)。
累加器用来对信息进行聚合,而广播变量用来高效分发较大的对象。
在Scala 中累加空行:
scala> val testrdd=sc.textFile("hdfs://localhost:9000/spark/sparktest.txt")
testrdd: org.apache.spark.rdd.RDD[String] = hdfs://localhost:9000/spark/sparktest.txt MapPartitionsRDD[2] at textFile at <console>:28
scala> testrdd.collect
res2: Array[String] = Array(hello,what's you name?, "", hello?, my name is wang., "", oh,glad to see you!, "", mee too!, "", but,i must leave now! bye!)
scala> val blankLines = sc.accumulator(0)
warning: there were two deprecation warnings; re-run with -deprecation for details
blankLines: org.apache.spark.Accumulator[Int] = 0
scala> val testrdd1=testrdd.flatMap(line=>{ if(line==""){ blankLines+=1} ;line.split(" ") })
testrdd1: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[3] at flatMap at <console>:32
scala> blankLines
res3: org.apache.spark.Accumulator[Int] = 0
scala> testrdd1.collect
res4: Array[String] = Array(hello,what's, you, name?, "", hello?, my, name, is, wang., "", oh,glad, to, see, you!, "", mee, too!, "", but,i, must, leave, now!, bye!)
scala> blankLines
res5: org.apache.spark.Accumulator[Int] = 4
scala> blankLines.value
res6: Int = 4
总结起来,累加器的用法如下所示。
• 通过在驱动器中调用SparkContext.accumulator(initialValue) 方法,创建出存有初始值的累加器。返回值为org.apache.spark.Accumulator[T] 对象,其中T 是初始值initialValue 的类型。
• Spark 闭包里的执行器代码可以使用累加器的+= 方法(在Java 中是add)增加累加器的值。
• 驱动器程序可以调用累加器的value 属性(在Java 中使用value() 或setValue())来访问累加器的值。
累加器与容错性
Spark 会自动重新执行失败的或较慢的任务来应对有错误的或者比较慢的机器。例如,如果对某分区执行map() 操作的节点失败了,Spark 会在另一个节点上重新运行该任务。即使该节点没有崩溃,而只是处理速度比别的节点慢很多,Spark 也可以抢占式地在另一个节点上启动一个“投机”(speculative)型的任务副本,如果该任务更早结束就可以直接获取结果。即使没有节点失败,Spark 有时也需要重新运行任务来获取缓存中被移除出内存的数据。因此最终结果就是同一个函数可能对同一个数据运行了多次,这取决于集群发生了什么。
这种情况下累加器要怎么处理呢?实际结果是,对于要在行动操作中使用的累加器,Spark只会把每个任务对各累加器的修改应用一次。因此,如果想要一个无论在失败还是重复计算时都绝对可靠的累加器,我们必须把它放在foreach() 这样的行动操作中。对于在RDD 转化操作中使用的累加器,就不能保证有这种情况了。转化操作中累加器可能会发生不止一次更新。举个例子,当一个被缓存下来但是没有经常使用的RDD 在第一次从LRU 缓存中被移除并又被重新用到时,这种非预期的多次更新就会发生。这会强制RDD 根据其谱系进行重算,而副作用就是这也会使得谱系中的转化操作里的累加器进行更新,并再次发送到驱动器中。在转化操作中,累加器通常只用于调试目的。
广播变量
Spark 的第二种共享变量类型是广播变量,它可以让程序高效地向所有工作节点发送一个较大的只读值,以供一个或多个Spark 操作使用。比如,如果你的应用需要向所有节点发送一个较大的只读查询表,甚至是机器学习算法中的一个很大的特征向量,广播变量用起来都很顺手。
scala> testrdd.collect
res7: Array[String] = Array(hello,what's you name?, "", hello?, my name is wang., "", oh,glad to see you!, "", mee too!, "", but,i must leave now! bye!)
scala> val str="you"
str: String = you
scala> val brodcast=sc.broadcast(str)
brodcast: org.apache.spark.broadcast.Broadcast[String] = Broadcast(5)
scala> testrdd.filter(line=>line.contains(brodcast.value)).collect
res10: Array[String] = Array(hello,what's you name?, oh,glad to see you!)
如以上示例所示,使用广播变量的过程很简单。
(1) 通过对一个类型T 的对象调用SparkContext.broadcast 创建出一个Broadcast[T] 对象。任何可序列化的类型都可以这么实现。
(2) 通过value 属性访问该对象的值
(3) 变量只会被发到各个节点一次,应作为只读值处理(修改这个值不会影响到别的节点)。
满足只读要求的最容易的使用方式是广播基本类型的值或者引用不可变对象。在这样的情况下,你没有办法修改广播变量的值,除了在驱动器程序的代码中可以修改。
基于分区进行操作
基于分区对数据进行操作可以让我们避免为每个数据元素进行重复的配置工作。诸如打开数据库连接或创建随机数生成器等操作,都是我们应当尽量避免为每个元素都配置一次的工作。Spark 提供基于分区的map 和foreach,让你的部分代码只对RDD 的每个分区运行一次,这样可以帮助降低这些操作的代价。
除了避免重复的配置工作,也可以使用mapPartitions() 避免创建对象的开销。
数值RDD的操作
Spark 的数值操作是通过流式算法实现的,允许以每次一个元素的方式构建出模型。这些统计数据都会在调用stats() 时通过一次遍历数据计算出来,并以StatsCounter 对象返回.
如果你只想计算这些统计数据中的一个,也可以直接对RDD 调用对应的方法,比如rdd.mean() 或者rdd.sum()。
用Scala 移除异常值
scala> val rdd00=sc.makeRDD(Array(80,95,60,86,30,99,78,101,31,54,79,85,3000))
rdd00: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[24] at makeRDD at <console>:28
scala> val stat=rdd00.stats
stat: org.apache.spark.util.StatCounter = (count: 13, mean: 298.307692, stdev: 780.232350, max: 3000.000000, min: 30.000000)
scala> val stdev=stat.stdev
stdev: Double = 780.2323504636674
scala> val meam=stat.mean
meam: Double = 298.3076923076923
scala> rdd00.mean
res22: Double = 298.3076923076923
scala> rdd00.filter(x=>math.abs(x-meam)<3*stdev).collect
res23: Array[Int] = Array(80, 95, 60, 86, 30, 99, 78, 101, 31, 54, 79, 85)