共享变量
- 累加器
- 广播变量
累加器
提供了将工作节点中的值聚合到驱动器程序中的简单语法
例子:计算空行数
val sc = new SparkContext(...)
val file = sc.textFile("file.txt")
val blankLines = sc.accumulator(0) // 创建Accumulator[Int]并初始化为0
val callSigns = file.flatMap(line => {
if (line == "") {
blankLines += 1 // 累加器加1
}
line.split(" ")
})
callSigns.saveAsTextFile("output.txt")
println("Blank lines: " + blankLines.value)
总结起来就是:
- 通 过 在 驱动器中调用 SparkContext.accumulator(initialValue) 方法,创建出存有初始 值 的 累 加 器。 返 回 值 为 org.apache.spark.Accumulator[T] 对 象, 其 中 T 是 初 始 值initialValue 的类型。
- Spark 闭包里的执行器代码可以使用累加器的 += 方法(在 Java 中是 add)增加累加器的值。
- 驱动器程序可以调用累加器的 value 属性(在 Java 中使用 value() 或 setValue())来访问累加器的值。
累加器的注意事项:
如果一个任务执行了累加操作,然后出错了,Spark会重新执行该任务,就会导致多次累加。解决方法是
- 行动操作中使用的累加器,把累加操作放在 foreach() 这样的行动操作中。
- 在转化操作中的累加器就无法避免了。慎用,所以累加器大多用于调试目的。
用户可以自定义累加器
参考这篇文章:http://blog.csdn.net/asas1314/article/details/54571815
广播变量
介绍
当多个并行操作使用同一个变量或RDD时,但是 Spark 会为每个操作分别发送。这样就浪费了很多资源,如果可以把变量只分发到节点,每个操作通过读取节点上的变量进行操作就好了。广播变量实现的就是这样的功能。
广播变量其实就是类型为 spark.broadcast.Broadcast[T] 的一个对象,T只会被发送到各节点一次,使用的是一种高效的类似 BitTorrent 的通信机制。
广播变量是只读变量。
例子:
// 查询RDD contactCounts中的呼号的对应位置。将呼号前缀
// 读取为国家代码来进行查询
val signPrefixes = sc.broadcast(loadCallSignTable())
val countryContactCounts = contactCounts.map{case (sign, count) =>
val country = lookupInArray(sign, signPrefixes.value)
(country, count)
}.reduceByKey((x, y) => x + y)
countryContactCounts.saveAsTextFile(outputDir + "/countries.txt")
广播优化
广播一个比较大的值时,选择既快又好的序列化格式是很重要。
- Spark默认 使用Java 序列化库
- 可以使用 spark.serializer 属性选择另一个序列化库来优化序列化过程(如Kryo)
- 也可以使用自己的序列化方式(对 Java 对象使用 java.io.Externalizable 接口实现序列化,或使用 reduce() 方法为 Python 的 pickle 库定义自定义的序列化)
基于分区进行操作
基于分区对数据进行操作可以让我们避免为每个数据元素进行重复的配置工作诸如打开数据库连接或创建随机数生成器等操作,都是我们应当尽量避免为每个元素都配置一次的工作。Spark 提供基于分区的 map 和 foreach,让你的部分代码只对 RDD 的每个分区运行一次,这样可以帮助降低这些操作的代价。
通过使用基于分区的操作,可以在每个分区内共享一个数据库连接池,来避免建立太多连接,同时还可以重用 JSON 解析器
val contactsContactLists = validSigns.distinct().mapPartitions{
signs =>val mapper = createMapper()
val client = new HttpClient()
client.start()
// 创建http请求
signs.map {sign =>createExchangeForSign(sign)
// 获取响应
}.map{ case (sign, exchange) =>(sign, readExchangeCallLog(mapper, exchange))
}.filter(x => x._2 != null) // 删除空的呼叫日志
}
除了避免重复的配置工作,也可以使用 mapPartitions() 避免创建对象的开销。有时需要创建一个对象来将不同类型的数据聚合起来。回忆一下第 3 章中,当计算平均值时,一种方法
是将数值 RDD 转为二元组 RDD,以在归约过程中追踪所处理的元素个数。现在,可以为每个分区只创建一次二元组,而不用为每个元素都执行这个操作
在 Python 中不使用 mapPartitions() 求平均值
def combineCtrs(c1, c2):
return (c1[0] + c2[0], c1[1] + c2[1])
def basicAvg(nums):
"""计算平均值"""
nums.map(lambda num: (num, 1)).reduce(combineCtrs)
在 Python 中使用 mapPartitions() 求平均值
def partitionCtr(nums):
"""计算分区的sumCounter"""
sumCount = [0, 0]
for num in nums:
sumCount[0] += num
sumCount[1] += 1
return [sumCount]
def fastAvg(nums):
"""计算平均值"""
sumCount = nums.mapPartitions(partitionCtr).reduce(combineCtrs)
return sumCount[0] / float(sumCount[1])
与外部程序间的管道
查看书本或http://www.it165.net/os/html/201406/8619.html
数值RDD 的操作
Spark 的数值操作是通过流式算法实现的,允许以每次一个元素的方式构建出模型。这些统计数据都会在调用 stats() 时通过一次遍历数据计算出来,并以 StatsCounter 对象返
回。
如果你只想计算这些统计数据中的一个,也可以直接对 RDD 调用对应的方法,比如 rdd.mean() 或者 rdd.sum()。
// 现在要移除一些异常值,因为有些地点可能是误报的
// 首先要获取字符串RDD并将它转换为双精度浮点型
val distanceDouble = distance.map(string => string.toDouble)
val stats = distanceDoubles.stats()
val stddev = stats.stdev
val mean = stats.mean
val reasonableDistances = distanceDoubles.filter(x => math.abs(x-mean) < 3 * stddev)
println(reasonableDistance.collect().toList)
在这个应用中,我们使用了累加器、广播变量、基于分区处理、外部程序接口调用以及汇总统计。