Learning Spark笔记12-累加器

6高级Spark编程


介绍


我们介绍两种共享变量:累加器聚合信息,传播变量分发大值。基于我们现有的RDD的转换,我们为有更高成本的任务(如查询数据库)使用批处理操作。在本章中,我们构建了一个使用ham无线电操作员的呼叫日志作为输入的示例。这些日志至少包括联系无线电台的呼号,呼号按国家进行划分,每个国家都有自己范围的呼号,所以我们可以根据呼号查找国家。一些呼叫日志也包括操作员的物理位置,我们可以根据物理位置计算距离。


Example 6-1. Sample call log entry in JSON, with some fields removed
{"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的语言无关的pipe()方法通过标准输入和输出与其他程序进行交互。我们将使用pipe()方法来访问一个R库,用于计算一个ham无线电操作员的联系人的距离。


最后,类似于使用键值对的工具,Spark有处理数字数据的方法。我们通过从使用我们的无线电通话记录计算的距离中去除异常值来演示这些方法。




6.1累加器


通常我们将函数传给Spark,像是map()函数或filter()函数,我们可以使用在驱动程序的外面的定义的变量,但是在集群上运行的每个任务都会得到一个每个变量的副本,从这些副本更新不会传播到驱动上。Spark的共享变量、累加器和传播变量,放宽了两种类型通信模式的限制:结果的聚合和传播。




我们第一个共享变量的类型是累加器,提供了简单的将值聚合的语法从工作节点到驱动程序。累加器最常用的用法是为了调试目的在作业执行期间对事件进行计数。例如,我们从日志文件中加载了所有的呼叫的列表,但是我们也对文件中有多少空行感兴趣。


Example 6-2. Accumulator empty line count in Python


file = sc.textFile(inputFile)
# Create Accumulator[Int] initialized to 0
blankLines = sc.accumulator(0)
def extractCallSigns(line):
global blankLines # Make the global variable accessible
if (line == ""):
blankLines += 1
  return line.split(" ")
callSigns = file.flatMap(extractCallSigns)
callSigns.saveAsTextFile(outputDir + "/callsigns")
print "Blank lines: %d" % blankLines.value


Example 6-3. Accumulator empty line count in Scala


val sc = new SparkContext(...)
val file = sc.textFile("file.txt")
val blankLines = sc.accumulator(0) // Create an Accumulator[Int] initialized to 0
val callSigns = file.flatMap(line => {
if (line == "") {
blankLines += 1 // Add to the accumulator
  }
line.split(" ")
})
callSigns.saveAsTextFile("output.txt")
println("Blank lines: " + blankLines.value)


Example 6-4. Accumulator empty line count in Java
JavaRDD rdd = sc.textFile(args[1]);
final Accumulator blankLines = sc.accumulator(0);
JavaRDD callSigns = rdd.flatMap(
 new FlatMapFunction() { public Iterable call(String line) {
 if (line.equals("")) {
 blankLines.add(1);
 }
 return Arrays.asList(line.split(" "));
 }});
callSigns.saveAsTextFile("output.txt")
System.out.println("Blank lines: "+ blankLines.value());


我们创建了一个累加器叫做blanklines,当在输入中有一个空行的时候我们就加1,最后输出这个值。在运行saveAsTextFile()动作之后,我们会看到正确的结果,因为转换已经发生了,map()是延迟加载的,所以累加器的副作用递增只有在当map()转换被强制saveAsTextFile()动作发生时。


当然,可以使用像reduce()这样的动作将整个RDD的值聚合返回到驱动程序。但是有时我们需要一种简单的方法聚合值,在前面的例子中,当我们加载数据时累加器可以对错误计数,不需要使用filter()或reduce()。


总结一下,累加器的工作如下:


1.我们在驱动中创建它,通过SparkContext.accumulator(initialValue)方法,返回的是org.apache.spark.Accumulator[T]类型的对象,T是initialValue类型


2.Spark闭包中的工作代码可以通过+=方法添加到累加器中


3.驱动程序可以在累加器上调用值属性来访问它的值(在Java中的方法是value()和setValue())


注意在工作节点上的任务是不能访问累加器的value()方法的,从这些任务的角度来看,累加器只能写入。这样可以有效的实现累加器,而不需要传达每一次更新。


当多个值跟踪,或者在分布式程序中相同值在多个地方增加(例如,在整个你的程序中可能会计数调用JSON解析库)时,计数变得更加便利。当出现太多错误时,我们为了避免垃圾输出,我们可以使用一个计数器记录有效的记录,一个计数器记录无效的记录。我们累加器的值只有在驱动程序中可以用,所以我们可以在驱动程序中检查它。


下面的例子,只有在大部分输入有效的时候,我们可以验证呼号然后输出。ham无线电通过标准格式已由国际电信联盟第19条指定


Example 6-5. Accumulator error count in Python
# Create Accumulators for validating call signs
validSignCount = sc.accumulator(0)
invalidSignCount = sc.accumulator(0)
def validateSign(sign):
global validSignCount, invalidSignCount
if re.match(r"\A\d?[a-zA-Z]{1,2}\d{1,4}[a-zA-Z]{1,3}\Z", sign):
validSignCount += 1
  return True
  else:
  invalidSignCount += 1
  return False
 # Count the number of times we contacted each call sign
validSigns = callSigns.filter(validateSign)
contactCount = validSigns.map(lambda sign: (sign, 1)).reduceByKey(lambda (x, y): x + y)
# Force evaluation so the counters are populated
contactCount.count()
if invalidSignCount.value < 0.1 * validSignCount.value:
contactCount.saveAsTextFile(outputDir + "/contactCount")
else:
print "Too many errors: %d in %d" % (invalidSignCount.value, validSignCount.value)




累加器和容错


Spark通过重新执行失效的,慢的任务来处理错误的或慢的机器。例如,节点在一个分区上执行map()操作时崩溃了,Spark会在另一个节点重新运行它;如果节点没有崩溃它只是比其他节点慢,Spark会预先发起一个任务的副本(a “speculative” copy of the task),当执行完成后获得它的结果。即使没有节点失败,Spark可能需要重新运行任务才能重建缓存的值,该值是超出内存的。因此,根据集群上发生的情况,相同的功能可以同时运行多次。


这种情况下累加器是如何交互的?最后的结果是在动作中使用累加器,Spark将每个任务的更新应用到每个累加器一次。因此,如果我们需要一个绝对可靠值计数器,无论失败或多次评估,我们必须把它放到一个动作中,例如foreach()。


在RDD转换中使用累加器就没有这个保证了。累加器在转换中更新可能会发生多次。不常用的RDD从LRU缓存中移出后,随后又需要用它的时候,可能会有多次更新。这迫使RDD从谱系重新计算,意外的影响是在转换的过程中更新累加器将会重新发送给driver。因此,在转换的过程中,累加器应仅用于调试。


在未来的Spark版本中这种更新只进行一次,在当前版本(1.2.0)还是会多次更新,所以累加器在转换中仅仅推荐用来调试。


自定义累加器


现在我们看到了如何使用Spark内置的累加器:整型累加器。除了这些累加器也提供了Double、Long和Float类型。此外,Spark也提供API来定义自定义的累加器类型和自定义的聚合操作(例如,找到累积值得最大值,而不是添加他们)。自定义的累加器需要继承AccumulatorParam。除了添加数值之外,我们可以使用任何操作,只要操作是可交换的和可关联的。例如,我们可以跟踪到目前为止所看到的最大值,而不是追加总计。

你可能感兴趣的:(Spark)