累加器
//scala中累加空行
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")
//只有在执行saveAsTextFile才能看到计数,因为操作是惰性的
println("Blank lines: " + blankLines.value)
累加器用法:
1、在驱动器中调用SparkContext.accumulator(initialValue)方法,创建初始累加器,返回org.apache.spark.Accumulator[T]对象,其中T是初始值initialValue的类型。
2、Spark闭包里执行器代码可以使用了累加器的+=方法增加累加器的值。
3、驱动器可以调用累加器的valuye属性来访问累加器的值
Ps:对于工作节点上的任务,累加器是一个只写变量。工作节点上的任务不能访问累加器的值,从而使累加器实现更加高效,不需要对每次操作进行复杂的通信。
累加器与容错性
Spark会重新执行失败或较慢的任务来应对错误或者比较慢的机器。
对于要在行动操作中使用的累加器,Spark只会把每个任务对各累加器的修改应用一次。因此需要放入foreach()中。在RDD转化操作中可能会发生不只一次更新。
自定义累加器
Spark支持Double、Long、Float类型累加器,以及自动以累加器和聚合操作的API,自定义累加器需要AccumulatorParam
广播变量
广播变量可以高效地向所有工作节点发送一个较大的只读值,以供一个或多个Spark操作操作,例如机器学习算法中的一个很大的特征向量。
Spark会自动把闭包中所有引用到的变量发送到工作节点上,这点很方便但很低效:1、默认的任务发射机制是专门为小任务优化的。2、如果多个并行操作同时操作一个变量,那么Spark会为每个操作分别发送。
在scala中使用广播变量查询国家
// 查询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")
满足只读要求的最容易的使用方式是广播基本类型的值或者引用不可变对象。这种情况下,只有驱动器代码可以修改值。
但有时传一个可变对象可能更为方便与高效,如果传可变变量,需要自己维护只读的条件。
广播优化
当广播值比较大时,需要选择更好的序列化格式。Scala和java API默认使用的序列化库为Java序列化库,因此它对于除基本类型的数组意外的任何对象都比较抵消。这时,你可以使用spark.serializer属性选择另一个序列化库来优化序列化过程。同时也可以使用自己的数据类型实现自己的序列化方式。
基于分区进行操作
//在 Scala 中使用共享连接池与 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) // 删除空的呼叫日志
}
函数名 | 输入 | 返回 | 对于RDD[T]的函数签名 |
---|---|---|---|
mapPartitions() | 该分区中的元素的迭代器 | 返回的元素的迭代器 | f:(Iterator[T])=>Iterator[U] |
mapPartitionsWithIndex | 分区序号,以及每个分区中的元素的迭代器 | 返回的元素的迭代器 | f(Int,Iterator[T])=>Iterator[U] |
foreachPartitions() | 元素迭代器 | 无 | f:(Iterator[T]=>Unit) |
上述函数除了避免重复的配置工作,也可以使用mapPartiotions()。
与外部程序间的管道
Spark允许将数据通过管道传给用其他语言写的程序,如R语言脚本。
pipe()方法:Spark的pipe()方法可以使用任何一种语言实现Spark作业中的逻辑部分,只要它能读写Unix标准流。
// R 语言的距离程序
#!/usr/bin/env Rscript
library("Imap")
f <- file("stdin")
open(f)
while(length(line <- readLines(f,n=1)) > 0) {
# 处理行
contents <- Map(as.numeric, strsplit(line, ","))
mydist <- gdist(contents[[1]][1], contents[[1]][2],
contents[[1]][3], contents[[1]][4],
units="m", a=6378137.0, b=6356752.3142, verbose = FALSE)
write(mydist, stdout())
}
// 使用一个R语言外部程序计算每次呼叫的距离
// 将脚本添加到各个节点需要在本次作业中下载的文件的列表中
val distScript = "./src/R/finddistance.R"
val distScriptName = "finddistance.R"
sc.addFile(distScript)
val distances = contactsContactLists.values.flatMap(x => x.map(y =>
s"$y.contactlay,$y.contactlong,$y.mylat,$y.mylong")).pipe(Seq(
SparkFiles.get(distScriptName)))
println(distances.collect().toList
可以通过SparkContext.addFile(path)欧吉安一个文件列表,让每个工作节点在spark作业中下载列表中文件,可通过驱动器的本地文件系统、HDFS、HTTP、HTTPS、FTP等方式下载。
然后通过SparkFiles.getRootDirectory找到他们。也可以使用SparkFiles.get(Filename)直接定位文件。
另外,所有通过SparkContext.addFile(path)添加的文件都存储在同一个目录中,所以有必要使用唯一的名字。
推荐使用SparkFilese.get()函数。
数值RDD的操作
Spark的数值操作是通过流式算法实现的看,允许以每次一个元素的方式构建出模型。这些统计数据会在调用stats()时通过一次遍历数据计算出来,并以StatusCounter对象返回。
StatsCounter可用方法
方法 | 含义 |
---|---|
count() | RDD中的元素个数 |
mean() | 元素的平均值 |
sum() | 总和 |
min() | 最小值 |
max() | 最大值 |
variance() | 元素的方差 |
sampleVariance() | 从采样中计算出的方差 |
stdev() | 标准差 |
sampleStdev() | 采样的标准差 |
//用 Scala 移除异常值
// 现在要移除一些异常值,因为有些地点可能是误报的
// 首先要获取字符串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)