Introduction(介绍)

本章介绍了之前章节没有涵盖的高级Spark编程特性。我们介绍两种类型的共享变量:用来聚合信息的累加器和能有效分配较大值的广播变量。基于对RDD现有的transformation(转换),我们针对构建成本高的任务引入批量操作,如查询数据库。为了扩展我们可使用工具的范围,我们介绍Spark与外部程序交互的方法,例如用R编写的脚本。

在本章中,我们将以无线电台的通话记录作为输入构造一个示例。这些日志至少包括联系电台的呼号。呼号由国家分配,并且每个国家有自己的呼号范围,所以我们可以查询相关国家。一些呼叫日志也包含物理地址和使用者,可以用来确定距离。Example6-1中有个样例日志条目。本书的样本repo包括一系列呼号,以查找呼叫记录并处理结果。

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的task中使用的特殊类型的变量。在例子中,我们使用Spark的共享变量来计算非致命错误条件并分配一个大的查找表。

当我们的task有很长的准备时间,比如创建数据库连接或随机数生成器,在多个数据项之间共享准备工作就很有用。使用远程调用查询数据库,我们将研究如何在每个分区上操作来复用准备工作。

除了Spark直接支持的语言,系统可以调用其他语言写的程序。这一章节会介绍如何使用Spark的与语言无关的pipe()方法通过标准输入输出来和其他程序交互。我们将使用pipe()方法访问R语言的库来计算业余无线电台联系人的距离。

最后,像使用键值对的工具一样,Spark有处理数值型数据的方法。我们会对业余无线电台日志计算出距离然后去除异常值,来演示这些处理数值的方法。

Accumulators(累加器)

我们通常把函数传递给Spark的时候,如map()函数或者按指定条件过滤的filter()函数,它们可以使用驱动程序中定义的变量,但是每个运行在集群上的task会得到一个新的变量副本,对这些副本的更新不会再回传给驱动程序。Spark的共享变量,累加器和广播变量放宽了对两种通信模式的限制:结果的汇总和广播。

第一个类型的共享变量是累加器,其对worker的节点的值进行累加并返回给驱动程序的语法比较简单。累加器最常用的一个用途就是统计job执行过程中的事件来进行调试。举例来说,我们加载那些想要检索的日志的呼号列表,但是我们对输入文件空白行的数目也感兴趣(可能我们并不想看到有效的输入中有这种空白行)。示例如下:

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<String> rdd = sc.textFile(args[1]); final Accumulator<Integer> blankLines = sc.accumulator(0);
JavaRDD<String> callSigns = rdd.flatMap(
new FlatMapFunction<String, String>() { public Iterable<String> call(Stringline) {
if (line.equals("")) {
blankLines.add(1);
}
return Arrays.asList(line.split(" "));
}}); callSigns.saveAsTextFile("output.txt")
System.out.println("Blank lines: "+ blankLines.value());

在例子中,我们创建了一个名叫blankLinesAccumulator[Int],然后当我们看到输入为空白行时为其加1。在对transformation(转换)求值之后,打印空白行总数。注意一点,我们只能在saveAsTextFile()action之后才能看到正确的值,因为之前的操作map()转换是惰性的,所以只有在惰性求值的map()saveAsTextFile强制求值后累加器的自增副作用才会生效。

当然,如果使用类似reduce的action可以对整个RDD的值累加并返回给驱动程序,但是有时我们需要更简单的方式对转换过程中RDD的值聚合,这些值与RDD本身的规模或粒度不同。在之前的例子中,我们在加载数据时累加器帮我们统计错误数量,不用再分别使用filter()reduce()

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

  • 我们在驱动程序中通过调用SparkContext.accumulator(initialValue)方法创建累加器,这会为累加器生成一个初始值。返回一个org.apache.spark.Accumulator[T]类型的对象,泛型T是初始值的类型。
  • Spark闭包中的工作代码可以通过+=方法为累加器增加值(或者Java中的add方法)。
  • 驱动程序能够调用累加器上的值属性来访问他的值(Java中value()setValue()方法)

请注意,worker节点上的task无法访问累加器的value()方法--从这些任务的角度来看,累加器是只写变量。这使得累加器可以高效地执行,而无需每次更新都进行通信。

当需要跟踪多个值,或者在并行程序中的多个位置需要增加相同的值时,此处显示的计数类型就特别方便(例如,你可能需要统计程序中调用JSON解析库的次数)。例如,我们的数据经常会损坏一定比例,或者结尾部分会失败一些次数。为了防止出现错误时产生垃圾输出,我们可以使用计数器来记录有效记录,并使用计数器记录无效记录。我们累加器的值只有在驱动程序中才能访问,所以这是我们进行检查的地点。

继续我们之前的例子,我们只有在大部分输入有效的情况下才验证呼号并输出数据。国际电信联盟在第19条中规定了无线电呼号的格式,从中我们构造了一个正则表达式来验证一致性,如Example6-5所示。

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)

Accumulators and Fault Tolerance(累加器和容错性)

Spark通过重新执行失败的或缓慢的task来自动应对机器失败或缓慢的情况。举例来说没如果节点运行一个分区的map()操作崩溃了,Spark会返回给另一个节点,即使节点没有崩溃,但是比其他节点慢得多,Spark也可以抢先在另一个节点上启动task的“speculative(推测)”副本,并在结束时取得结果。即使没有节点失败,Spark也可能必须重新运行一个task来重建内存不足的缓存值。因此,在相同的数据上,相同的函数可能会运行多次,这取决于集群上运行的情况。

所以是如何与累加器交互的呢?最终结果是,对于action中使用的累加器,Spark仅将每个task的更新应用于每个累加器一次。因此,如果我们在失败或求值多次的情况下仍想要一个可靠的绝对值计数器,一定要把它放进类似于foreach()的action中。

对于在transformation中而不是action中使用的累加器,这种保证就不存在了。一个累加器因transformation导致的更新可能会不止一次。当已经缓存过但不怎么使用的RDD从LRU缓存中驱逐出来并且紧接着又需要使用该RDD的情况下可能会导致无意识地多次更新。这迫使RDD根据其依赖关系重新求值,并且无意识的副作用会对该依赖关系中transformation的累加器调用更新,然后再发回给驱动程序。在转换中的累加器,应该只在调试时使用。

虽然未来版本的Spark可能会更改此行为来仅对更新进行一次计数,但当前版本(1.2.0)确实具有多重更新行为,因此建议仅在进行调试时使用转换中的累加器。

Custom Accumulators(定制累加器)

目前为止我们已经看到了一种Spark内置累加器类型使用方式:整数的加法(Accumulator[Int])。Spark支持很多累加器类型,如Double,Long和Float。除此之外,Spark还有一个自定义累加器类型和累加器操作的API(如,寻找累加值中的最大值)。自定义累加器需要继承AccumulatorParam,Spark API文档中有详细的介绍。除了数值类型可以相加,只要提供的操作是满足交换律和结合律,我们就可以用对任意操作使用加法。举例来讲,除了使用加法来追踪总数,我们还可以追踪所见元素的最大值。

如果操作op满足--对于所有的值a,b;都有 a op b = b op a,那么op满足交换律。

如果操作op满足--对于所有的值a,b,c;都有 (a op b) op c = a op (b op c),那么op满足结合律。

举例来说,summax是满足交换律和结合律的操作,所以可以用于Spark的累加器。

Broadcast Variables(广播变量)

Spark第二个共享变量类型是广播变量,它允许程序高效地发送只读的大型值给所有的工作节点,以用于一个或多个Spark操作。例如,如果你的应用程序需要将大型只读查找表发送到所有节点,或者是机器学习算法中的大型特征向量,则它们就派上用场了。

回忆一下,Spark会自动把闭包中引用的变量发送给所有节点。尽管这很方便,但是可能会导致效率低下,因为(1)默认的任务启动机制对task的大小进行了优化;(2)事实上,你可能会在多个并行操作中使用相同的变量,但Spark会为每个操作单独发送它。作为一个例子,假设我们想编写一个Spark程序,该程序根据数组中的前缀匹配国家呼号查找国家/地区。这对于无线电呼号很有用,因为每个国家都有自己的前缀,但前缀长度不统一。如果我们在Spark中想当然地写这个代码,代码可能看起来像Example6-6。

Example 6-6. Country lookup in Python

# Look up the locations of the call signs on the
# RDD contactCounts. We load a list of call sign
# prefixes to country code to support this lookup.
#在contactCounts RDD上查找呼号的位置
#我们加在一个国家呼号前缀的列表来帮助查询
signPrefixes = loadCallSignTable() def processSignCount(sign_count, signPrefixes):
country = lookupCountry(sign_count[0], signPrefixes)
count = sign_count[1]
return (country, count) countryContactCounts = (contactCounts
.map(processSignCount)
.reduceByKey((lambda x, y: x+ y)))

这个程序可以运行,但如果我们有一个大型的表(保存了IP地址而不是呼号的表),那么signPrefixes可以很轻易达到几百万字节,这样的话,把数组从master发送到每个task的代价就非常高了。除此之外,如果我们之后使用相同的signPrefixes对象(可能我们一会会在file2.txt上运行),这又会再次发送给每个节点。

我们可以把signPrefixes改成一个广播变量。广播变量就是一个spark.broadcast.Broadcast[T]类型的对象,对T类型的值进行了包装。我们可以在task上通过调用Broadcast对象的value来访问广播变量的值。这个值只会向节点发送一次,使用高效地,类似比特流的传输机制。

使用广播变量,我们之前的例子变成了下面的样子:

Example 6-7. Country lookup with Broadcast values in Python

# Look up the locations of the call signs on the
# RDD contactCounts. We load a list of call sign
# prefixes to country code to support this lookup.
signPrefixes = sc.broadcast(loadCallSignTable()) def processSignCount(sign_count, signPrefixes):
country = lookupCountry(sign_count[0], signPrefixes.value)
count = sign_count[1]
return (country, count) countryContactCounts = (contactCounts
.map(processSignCount)
.reduceByKey((lambda x, y: x+ y))) countryContactCounts.saveAsTextFile(outputDir + "/countries.txt") Example 6-8. Country lookup with Broadcast values in Scala // Look up the countries for each call sign for the
// contactCounts RDD. We load an array of call sign
// prefixes to country code to support this lookup.
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") Example 6-9. Country lookup with Broadcast values in Java
// Read in the call sign table
// Look up the countries for each call sign in the
// contactCounts RDD
final Broadcast<String[]> signPrefixes = sc.broadcast(loadCallSignTable());
JavaPairRDD<String, Integer> countryContactCounts = contactCounts.mapToPair(
new PairFunction<Tuple2<String, Integer>, String, Integer> (){
public Tuple2<String, Integer> call(Tuple2<String, Integer> callSignCount) {
String sign = callSignCount._1();
String country = lookupCountry(sign, callSignInfo.value());
return new Tuple2(country, callSignCount._2());
}}).reduceByKey(new SumInts());
countryContactCounts.saveAsTextFile(outputDir + "/countries.txt");

如例所示,使用广播变量非常简单:

  • 1.通过调用SparkContext.broadcast创建一个Broadcast[T]对象,T类型对象必须是可序列化的。
  • 2.使用vvalue属性(或者Java中使用value()方法)访问它的值。
  • 3.变量只会发送到每个节点一次,并且应该当做只读值对待(更新值不会传播到其他节点)。

满足只读要求的最简单方法是广播原始值或对不可变对象的引用。这种情况,你就无法改变广播变量的值了,除了在驱动程序代码中。但是,有时广播变量是一个可变对象会更方便。如果你这样做了,变量的只读性只能由你来保证了。就像我们对呼号前缀的数组做的那样,必须确保在工作节点运行的代码不去做类似于val theArray = broadcastArray.value; theArray(0) = newValue的操作。当在工作节点上运行的时候,这几行代码会将当前节点的本地数组副本的第一个元素设置为newValue;它不会改变其他工作节点的broadcastArray.value的内容。

Optimizing Broadcasts(广播变量优化)

当我们广播一个大型值的时候,选择紧凑快速的数据序列化格式是很重要的,因为如果对值序列化或在网络上发送该序列化值需要很长时间,那么网络发送该序列化值会成为程序的瓶颈。特别是,Java Serialization(Spark Spark Scala和Java API中使用的默认序列化库)对于除基本类型数组之外的任何内容都可能非常低效。你可以通过spark.serializer属性选择不同的序列化库(第八章会介绍如何使用Kryo,一个非常高效的序列化库),或者通过为你的数据类型实现你自己的序列化规范(例如使用java.io.Externalizable接口为Java序列化,或使用reduce()方法为Python的pickle库定义自定义序列化)。

Learning Spark中文版--第六章--Spark高级编程(1)的更多相关文章

  1. Learning Spark中文版--第六章--Spark高级编程(2)

    Working on a Per-Partition Basis(基于分区的操作) 以每个分区为基础处理数据使我们可以避免为每个数据项重做配置工作.如打开数据库连接或者创建随机数生成器这样的操作,我们 ...

  2. 简学Python第六章__class面向对象编程与异常处理

    Python第六章__class面向对象编程与异常处理 欢迎加入Linux_Python学习群  群号:478616847 目录: 面向对象的程序设计 类和对象 封装 继承与派生 多态与多态性 特性p ...

  3. Learning Spark中文版--第三章--RDD编程(2)

    Common Transformations and Actions   本章中,我们浏览了Spark中大多数常见的transformation(转换)和action(开工).在包含特定数据类型的RD ...

  4. Learning Spark中文版--第五章--加载保存数据(2)

    SequenceFiles(序列文件)   SequenceFile是Hadoop的一种由键值对小文件组成的流行的格式.SequenceFIle有同步标记,Spark可以寻找标记点,然后与记录边界重新 ...

  5. Spark入门(六)--Spark的combineByKey、sortBykey

    spark的combineByKey combineByKey的特点 combineByKey的强大之处,在于提供了三个函数操作来操作一个函数.第一个函数,是对元数据处理,从而获得一个键值对.第二个函 ...

  6. Learning Spark中文版--第五章--加载保存数据(1)

      开发工程师和数据科学家都会受益于本章的部分内容.工程师可能希望探索更多的输出格式,看看有没有一些适合他们下游用户的格式.数据科学家可能会更关注他们已经使用的数据格式. Motivation   我 ...

  7. Learning Spark中文版--第四章--使用键值对(1)

      本章介绍了如何使用键值对RDD,Spark中很多操作都基于此数据类型.键值对RDD通常在聚合操作中使用,而且我们经常做一些初始的ETL(extract(提取),transform(转换)和load ...

  8. Learning Spark中文版--第四章--使用键值对(2)

    Actions Available on Pair RDDs (键值对RDD可用的action)   和transformation(转换)一样,键值对RDD也可以使用基础RDD上的action(开工 ...

  9. Learning Spark中文版--第三章--RDD编程(1)

       本章介绍了Spark用于数据处理的核心抽象概念,具有弹性的分布式数据集(RDD).一个RDD仅仅是一个分布式的元素集合.在Spark中,所有工作都表示为创建新的RDDs.转换现有的RDD,或者调 ...

随机推荐

  1. 『学了就忘』Linux基础 — 16、Linux系统与Windows系统的不同

    目录 1.Linux严格区分大小写 2.Linux一切皆文件 3.Linux不靠扩展名区分文件类型 4.Linux中所有的存储设备都必须在挂载之后才能使用 5.Windows下的程序不能直接在Linu ...

  2. Spring事务的介绍,以及基于注解@Transactional的声明式事务

    前言 事务是一个非常重要的知识点,前面的文章已经有介绍了关于SpringAOP代理的实现过程:事务管理也是AOP的一个重要的功能. 事务的基本介绍 数据库事务特性: 原子性 一致性 隔离性 持久性 事 ...

  3. robot framework 常用关键字介绍

    1.log 打印所有内容 log hello word 2.定义变量 ${a} Set variable 92 log ${a}   3.连接对象 ${a} Catenate hello word l ...

  4. Vue 之 Mixins (混入)的使用

    是什么 混入 (mixins): 是一种分发 Vue 组件中可复用功能的非常灵活的方式.混入对象可以包含任意组件选项.当组件使用混入对象时,所有混入对象的选项将被合并到组件本身,也就是说父组件调用混入 ...

  5. super和this

    super注意点: 1.super调用父类的构造方法,必须在构造方法的第一个 2.super必须只能出现在子类的方法或者构造方法中 3.super和this不能同时调用构造方法 this: 代表的对象 ...

  6. 限制q-error,防止产生次优计划

    原文:<Preventing bad plans by bounding the impact of cardinality estimation errors> 摘要 文章定义了一个衡量 ...

  7. Pycharm下载安装详细教程

    目录 1.Pycharm 简介 2.Pycharm下载 3.环境变量的配置 4.Pycharm的使用 1.Pycharm 简介 PyCharm是一种Python IDE(Integrated Deve ...

  8. 🏆【Alibaba中间件技术系列】「RocketMQ技术专题」小白专区之领略一下RocketMQ基础之最!

    应一些小伙伴们的私信,希望可以介绍一下RocketMQ的基础,那么我们现在就从0开始,进入RocketMQ的基础学习及概念介绍,为学习和使用RocketMQ打好基础! RocketMQ的定位 Rock ...

  9. 剖析虚幻渲染体系(12)- 移动端专题Part 3(渲染优化)

    目录 12.6 移动端渲染优化 12.6.1 渲染管线优化 12.6.1.1 使用新特性 12.6.1.2 管线优化 12.6.1.3 带宽优化 12.6.2 资源优化 12.6.2.1 纹理优化 1 ...

  10. opencv 视频处理相关

    包含视频格式知识(编解码和封装格式):如何获取视频信息及视频编解码格式:opencv读取及保存视频,及opencv fourcc编码格式 一.基础知识 视频的编解码格式和封装格式 参考如山似水 视频编 ...