Learning Spark阅读笔记4

Spark编程进阶

两种类型的共享变量:

累加器:用来对信息进行聚合。
广播变量:用来高效分发较大的对象。

数据介绍:

使用业余无线电操作者的呼叫日志作为输入,构建出一个完整的示例应用。这些日志中至少包含联系过的站点的呼号。呼号是由国家分配的,每个国家都有自己的呼号号段。有一些呼叫日志也包含操作者的地理位置,用来帮助确定距离。

示例:

{"address":"address here", "band":"40m","callsign":"KK6JLK","city":"SUNNYVALE",
"contactlat":"37.384733","contactlong":"-122.032164",
"county":"Santa Clara","dxcc":"291","fullname":"MATTHEW McPherrin",
"id":57779,"mode":"FM","mylat":"37.751952821","mylong":"-122.4208688735",...}

我们使用Spark共享变量来对非严重错误的情况进行计数,以及分发一张巨大的查询表。

累加器

通常在向Spark传递函数时,可以使用驱动器程序中定义的变量,但是集群中运行的每个任务都会得到这些变量的一份新的副本,更新这些副本的值也不会影响驱动器中的对应变量。Spark的两个共享变量,累加器与广播变量,分别为结果聚合与广播这两种常见的通信模式突破了这一限制。

累加器提供了将工作节点的值聚合到驱动器程序中的简单语法。累加器的一个常见用途是在调试时对作业执行过程中的事件进行计数。例如想知道输入文件中有多少空行。

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
    }
    line.split(" ")
})
callSigns.saveAsTextFile("output.txt")
println("Blank lines: " + blankLines.value)

注意:只有在运行saveAsTextFile()行动操作后才能看到正确的计数。在工作节点上的任务不能呢个访问累加器的值。从这些任务的角度来看,累加器是一个只写变量。在这种模式下,累加器的实现可以更加高效,不需要对每次更新操作进行复杂的通信。累加器的值只有在驱动程序中可以访问,所以检查也应当在驱动器程序中完成。

累加器与容错性

Spark会自动重新执行失败的或较慢的任务来应对有错误的或者比较慢的机器。因此最终结果就是同一个函数可能对同一个数据运行了多次,这种情况下累加器要怎么处理呢?

实际结果是,对于要在行动操作中使用的累加器,Spark只会把每个任务对各累加器的修改应用一次。因此,如果想要一个无论在失败还是重复计算时都绝对可靠的累加器,我们必须把它放在foreach()这样的行动操作中。

对于在RDD转化操作中使用的累加器,就不能保证有这种情况了,可能将来的Spark可能会把这一行为改成只更新一次累加器的结果,但当前版本(1.2.0)确实会进行多次更新,因此转化操作中的累加器最好只在调试时使用。

自定义累加器

Spark也引入了自定义累加器和聚合操作的API。自定义累加器需要扩展AccumulatorParam,只要该操作同时满足交换律和结合律,就可以使用任意操作来代替数值上的加法。

广播变量

它可以让程序高效地向所有工作节点发送一个较大的只读值,以供一个或多个Spark操作使用。

Spark会自动把闭包中所有引用到的变量发送到工作节点上,虽然这很方便,但也很低效:

  1. 默认的任务发射机制是专门为小任务进行优化的。
  2. 事实上可能会在多个并行操作中使用同一个变量,但是Spark会为每个操作分别发送。

广播变量其实就是类型为spark.broadcast.Broadcast[T]的一个对象,其中存放着类型为T的值,可以在任务中通过对Broadcast对象调用value来获取该对象的值。这个值只会被发送到各节点一次,使用的是一种高效的类似BitTorrent的通信机制。

举个例子,使用广播变量查询国家:

// 查询RDD contactCounts中的呼号的对应位置,将呼号前缀
// 读取为国家代码来进行查询
val fignPrefixes = 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")
  1. 用过对一个类型T的对象调用SparkContext.broadcast创建出一个Broadcast[T]对象。任何可序列化的类型都可以这么实现。
  2. 通过value属性访问该对象的值。
  3. 变量只会被发到各个节点一次,应作为只读值处理(修改这个值不会影响到别的节点)。

当广播一个比较大的值时,选择即快又好的序列化格式是很重要的,因为如果序列化对象的时间很长或者传送花费的时间太久,这段时间很容易成为性能瓶颈。

基于分区进行操作

基于分区对数据进行操作可以让我们避免为每个数据元素进行重复的配置工作。Spark提供基于分区的map和foreach,让你的部分代码只对RDD的每个分区运行一次,这样可以帮助降低这些操作的代价。

示例:有一个在线的业余电台呼号数据库,可以用这个数据库查询日志中记录过的联系人呼号列表。通过使用基于分区的操作,可以在每个分区内共享一个数据库连接池,来避免建立太多的连接,同时还可以重用JSON解析器。使用mapPartitions函数获得输入RDD的每个分区中的元素迭代器,而需要返回的是执行结果的序列的迭代器。

val contactsContactLists = validSigns.distinct().mapPartitions{
  sign =>
  val mapper = createMapper()
  val client = new HttpClient()
  client.start()
  signs.map {sign =>
    createExchangeForSign(sign)
  }.map{ case (sign, exchage) =>
    (sign, readExchangeCallLog(mapper, exchange))
  }.filter(x => x._2 != null)
}

当基于分区操作RDD时,Spark会为函数提供该分区中的元素的迭代器。返回值方面,也返回一个迭代其。除mapPartitions()外,还有一些别的基于分区的操作符:

数值RDD的操作

Spark对包含数值数据的RDD提供了一些描述性的统计操作。Spark的数值操作是通过流式算法实现的,允许以每次一个元素的方式构建出模型。这些统计数据都会在调用stats()时通过一次遍历数据计算出来,病以StatsCounter对象返回。

StatsCounter中可用的汇总统计数据

回到呼叫日志的示例中,来看看如何从呼叫日志中移除距离过远的联系点。

// 现在要移除一些异常值,因为有些地点可能是误报的
// 首先要获取字符串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)
posted @ 2016-12-01 14:06  传奇魔法师  阅读(201)  评论(0编辑  收藏  举报