Learning Spark中文版--第六章--Spark高级编程(1)
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());
在例子中,我们创建了一个名叫blankLines
的Accumulator[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满足结合律。
举例来说,sum
和max
是满足交换律和结合律的操作,所以可以用于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)的更多相关文章
- Learning Spark中文版--第六章--Spark高级编程(2)
Working on a Per-Partition Basis(基于分区的操作) 以每个分区为基础处理数据使我们可以避免为每个数据项重做配置工作.如打开数据库连接或者创建随机数生成器这样的操作,我们 ...
- 简学Python第六章__class面向对象编程与异常处理
Python第六章__class面向对象编程与异常处理 欢迎加入Linux_Python学习群 群号:478616847 目录: 面向对象的程序设计 类和对象 封装 继承与派生 多态与多态性 特性p ...
- Learning Spark中文版--第三章--RDD编程(2)
Common Transformations and Actions 本章中,我们浏览了Spark中大多数常见的transformation(转换)和action(开工).在包含特定数据类型的RD ...
- Learning Spark中文版--第五章--加载保存数据(2)
SequenceFiles(序列文件) SequenceFile是Hadoop的一种由键值对小文件组成的流行的格式.SequenceFIle有同步标记,Spark可以寻找标记点,然后与记录边界重新 ...
- Spark入门(六)--Spark的combineByKey、sortBykey
spark的combineByKey combineByKey的特点 combineByKey的强大之处,在于提供了三个函数操作来操作一个函数.第一个函数,是对元数据处理,从而获得一个键值对.第二个函 ...
- Learning Spark中文版--第五章--加载保存数据(1)
开发工程师和数据科学家都会受益于本章的部分内容.工程师可能希望探索更多的输出格式,看看有没有一些适合他们下游用户的格式.数据科学家可能会更关注他们已经使用的数据格式. Motivation 我 ...
- Learning Spark中文版--第四章--使用键值对(1)
本章介绍了如何使用键值对RDD,Spark中很多操作都基于此数据类型.键值对RDD通常在聚合操作中使用,而且我们经常做一些初始的ETL(extract(提取),transform(转换)和load ...
- Learning Spark中文版--第四章--使用键值对(2)
Actions Available on Pair RDDs (键值对RDD可用的action) 和transformation(转换)一样,键值对RDD也可以使用基础RDD上的action(开工 ...
- Learning Spark中文版--第三章--RDD编程(1)
本章介绍了Spark用于数据处理的核心抽象概念,具有弹性的分布式数据集(RDD).一个RDD仅仅是一个分布式的元素集合.在Spark中,所有工作都表示为创建新的RDDs.转换现有的RDD,或者调 ...
随机推荐
- 整数中1出现的次数 牛客网 剑指Offer
整数中1出现的次数 牛客网 剑指Offer 题目描述 求出113的整数中1出现的次数,并算出1001300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1.10.11.12.13因此 ...
- hdu 2571 命运(水DP)
题意: M*N的grid,每个格上有一个整数. 小明从左上角(1,1)打算走到右下角(M,N). 每次可以向下走一格,或向右走一格,或向右走到当前所在列的倍数的列的位置上.即:若当前位置是(i,j), ...
- 计算机网络漫谈之IP与子网掩码
通过之前的介绍,我们现在已有的概念是任何一台计算机如果需要接入互联网,都会分配到一个IP地址.这个地址分成两个部分,前一部分代表网络,后一部分代表主机.比如,IP地址172.16.254.1,这是一个 ...
- etcd原理详解代码剖析
1 架构 从etcd的架构图中我们可以看到,etcd主要分为四个部分. HTTP Server: 用于处理用户发送的API请求以及其它etcd节点的同步与心跳信息请求. Store:用于处理etcd支 ...
- Pytorch中stack()方法的理解
Torch.stack() 1. 概念 在一个新的维度上连接一个张量序列 2. 参数 tensors (sequence)需要连接的张量序列 dim (int)在第dim个维度上连接 注意输入的张量s ...
- Centos6.8 yum报错及修复YumRepo Error: All mirror URLs are not using ftp, http[s] or file. Eg. Invalid
问题 使用yum安装软件时报错 YumRepo Error: All mirror URLs are not using ftp, http[s] or file. Eg. Invalid relea ...
- 计算机网络tcp
tcp/ip协议 什么是这个协议:计算机与网络设备之间通信的时候,两者需要使用相同的语言,如何侦察到对方,如何传输,谁先传输,都需要规定有一系列的协议,而tcp/ip协议则是这样的一种 tcp/ip的 ...
- GO语言数据结构之链表
链表是一种物理存储单元上非连续.非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的.链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成.每个结点包括两个部分: ...
- 程序员PS技能(四):程序员创建PSD文件、展示简单PSD设计流程,上传PSD至蓝湖,并下载Demo切图
前言 本篇是程序员仿照ui设计创建psd且切图五个按钮效果上传至蓝湖,本篇篇幅较长,整体完成一个目标,没有分篇幅了. 前提条件 已经安装了PS,已经在PS上安装了蓝湖插件,并且曾经已经上传 ...
- 菜鸡的Java笔记 Eclipse 的使用
Eclipse 的使用 1. Eclipse 简介 2. Eclipse 中的JDT 的使用 3. Eclipse 中的使用 junit 测试 Eclipse (中文翻 ...