Learning Spark中文版--第六章--Spark高级编程(2)
Working on a Per-Partition Basis
(基于分区的操作)
以每个分区为基础处理数据使我们可以避免为每个数据项重做配置工作。如打开数据库连接或者创建随机数生成器这样的操作,我们希望避免为每个元素重做配置工作。Spark有分区版本的map
和foreach
,通过让RDD的每个分区只运行一次代码,可帮助降低这些操作的成本。
回到我们的呼号例子中,有一个无线电台呼号的在线数据库,我们可以查询联系日志的公共列表。通过使用基于分区的操作,我们可以分享数据库的连接池来避免为多个连接配置连接参数,并且可以复用JSON解析器。如Example6-10到6-12所示,我们使用mapPartitions()
函数,这个函数会给我们一个输入RDD每个分区中元素的迭代器,并且期望我们的返回是一个结果的迭代器。
Example 6-10. Shared connection pool in Python
def processCallSigns(signs):
"""Lookup call signs using a connection pool"""
# Create a connection pool
http = urllib3.PoolManager()
# the URL associated with each call sign record
urls = map(lambda x: "http://73s.com/qsos/%s.json" % x, signs)
# create the requests (non-blocking)
requests = map(lambda x: (x, http.request('GET', x)), urls)
# fetch the results
result = map(lambda x: (x[0], json.loads(x[1].data)), requests)
# remove any empty results and return
return filter(lambda x: x[1] is not None, result)
def fetchCallSigns(input):
"""Fetch call signs"""
return input.mapPartitions(lambda callSigns : processCallSigns(callSigns))
contactsContactList = fetchCallSigns(validSigns)
Example 6-11. Shared connection pool and JSON parser in Scala
val contactsContactLists = validSigns.distinct().mapPartitions{
signs =>
val mapper = createMapper()
val client = new HttpClient()
client.start()
// create http request
signs.map {sign =>
createExchangeForSign(sign)
// fetch responses
}.map{ case (sign, exchange) =>
(sign, readExchangeCallLog(mapper, exchange))
}.filter(x => x._2 != null) // Remove empty CallLogs
}
Example 6-12. Shared connection pool and JSON parser in Java
// Use mapPartitions to reuse setup work.
JavaPairRDD<String, CallLog[]> contactsContactLists =
validCallSigns.mapPartitionsToPair(
new PairFlatMapFunction<Iterator<String>, String, CallLog[]>() {
public Iterable<Tuple2<String, CallLog[]>> call(Iterator<String> input) {
// List for our results.
ArrayList<Tuple2<String, CallLog[]>> callsignLogs = new ArrayList<>();
ArrayList<Tuple2<String, ContentExchange>> requests = new ArrayList<>();
ObjectMapper mapper = createMapper();
HttpClient client = new HttpClient();
try {
client.start();
while (input.hasNext()) {
requests.add(createRequestForSign(input.next(), client));
}
for (Tuple2<String, ContentExchange> signExchange : requests) {
callsignLogs.add(fetchResultFromRequest(mapper, signExchange));
}
} catch (Exception e) {
}
return callsignLogs;
}});
System.out.println(StringUtils.join(contactsContactLists.collect(), ","));
当使用基于分区的操作时,Spark会给函数一个分区中元素的迭代器。我们的返回结果也是一个迭代器。除了mapPartitions()
之外,Spark还有很多其他分区操作,如表6-1中所示:
函数名 | 所调用内容 | 返回类型 | RDD[T]函数签名 |
---|---|---|---|
mapPartitions() |
分区元素的迭代器 | 返回元素的迭代器 | f: (Iterator[T]) ->Iterator[U] |
mapPartitionsWithIndex() |
分区数量和分区元素的迭代器 | 返回元素的迭代器 | f: (Int, Iterator[T]) -> Iterator[U] |
foreachPartition() |
元素迭代器 | 无 | f: (Iterator[T]) -> Unit |
除了可以避免重复的配置工作,我们有时还可以使用mapPartitions() 来避免创建对象的开销。有时我们需要创建一个对象来聚合不同类型的结果。回想一下第三章我们计算平均数的时候,当时把数值RDD转换成了元组RDD,这样便于跟踪在归约步骤中元素处理的数量从而计算平均值。除了这种对每个元素都处理的方式,我们可以为每个分区创建一次元组,示例如下: |
Example 6-13. Average without mapPartitions() in Python
def combineCtrs(c1, c2):
return (c1[0] + c2[0], c1[1] + c2[1])
def basicAvg(nums):
"""Compute the average"""
nums.map(lambda num: (num, 1)).reduce(combineCtrs)
Example 6-14. Average with mapPartitions() in Python
def partitionCtr(nums):
"""Compute sumCounter for partition"""
sumCount = [0, 0]
for num in nums:
sumCount[0] += num
sumCount[1] += 1
return [sumCount]
def fastAvg(nums):
"""Compute the avg"""
sumCount = nums.mapPartitions(partitionCtr).reduce(combineCtrs)
return sumCount[0] / float(sumCount[1])
Piping to External Programs(连接外部程序)
Spark提供三种开箱即用的语言支持,你似乎在编写Spark程序时不会遇到语言上的障碍。但是,如果Scala,Java和Python都不是你想要的,那也没关系,SPark提供了一个使用其他语言(如R脚本)传输数据给程序的机制,。
Spark在RDD上提供了一个pipe()
方法。这个方法可以让我们使用其他语言编写job,但前提是这种语言支持Unix标准流的读入和写出。使用pipe()
,可以编写一个RDD转换,它从标准输入读取每个RDD元素作为String,接着按照需要操作该String,然后将结果作为String写入标准输出。提供的接口和编程模型是受到了一些限制,但是有时后你需要做的就是像写在map()
或filter()
中使用的本地代码函数一样。
你可能会想把RDD的内容传输给外部程序或者脚本,因为你可能已经构建并测试了一个复杂的软件,而且你想在Spark中重用他。有很多数据科学家有R语言写的代码,我们可以通过pipe()
函数与R程序进行交互。
在Example6-15中,我们使用R语言的库来计算所有联系人的距离。RDD中的每个元素通过换行符作为分割写出,并且程序输出的每一行都是结果RDD中的字符串元素。为了使R程序转换输入数据简便一些,我们把数据转换成mylat, mylon, theirlat, theirlon
格式。我们以逗号作为分隔符。
Example 6-15. R distance program
#!/usr/bin/env Rscript
library("Imap")
f <- file("stdin")
open(f)
while(length(line <- readLines(f,n=1)) > 0) {
# process line
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())
}
如果把上述代码写入一个名为./src/R/finddistance.R
的可执行文件,那么使用方法大体如下:
$ ./src/R/finddistance.R
37.75889318222431,-122.42683635321838,37.7614213,-122.4240097
349.2602
coffee
NA
ctrl-d
目前为止很不错,我们学会了从stdin
读取的每一行转换成stdout
输出的方法了。现在我们需要让每个工作节点都能使用finddistance.R
并且能让shell脚本转换我们的RDD。完成这两点其实在Spark中很简单,示例如下:
Example 6-16. Driver program using pipe() to call finddistance.R in Python
# Compute the distance of each call using an external R program
distScript = "./src/R/finddistance.R"
distScriptName = "finddistance.R"
sc.addFile(distScript)
def hasDistInfo(call):
"""Verify that a call has the fields required to compute the distance"""
#验证计算距离的必要字段
requiredFields = ["mylat", "mylong", "contactlat", "contactlong"]
return all(map(lambda f: call[f], requiredFields))
def formatCall(call):
"""Format a call so that it can be parsed by our R program"""
#格式话数据便于R程序解析
return "{0},{1},{2},{3}".format(
call["mylat"], call["mylong"],
call["contactlat"], call["contactlong"])
pipeInputs = contactsContactList.values().flatMap(
lambda calls: map(formatCall, filter(hasDistInfo, calls)))
distances = pipeInputs.pipe(SparkFiles.get(distScriptName))
print distances.collect()
Example 6-17. Driver program using pipe() to call finddistance.R in Scala
// Compute the distance of each call using an external R program
// adds our script to a list of files for each node to download with this job
//通过外部的R程序计算每个呼叫的距离
//把脚本添加到这个job中每个节点都要下载的文件列表
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)
Example 6-18. Driver program using pipe() to call finddistance.R in Java
// Compute the distance of each call using an external R program
// adds our script to a list of files for each node to download with this job
String distScript = "./src/R/finddistance.R";
String distScriptName = "finddistance.R";
sc.addFile(distScript);
JavaRDD<String> pipeInputs = contactsContactLists.values()
.map(new VerifyCallLogs()).flatMap(
new FlatMapFunction<CallLog[], String>() {
public Iterable<String> call(CallLog[] calls) {
ArrayList<String> latLons = new ArrayList<String>();
for (CallLog call: calls) {
latLons.add(call.mylat + "," + call.mylong +
"," + call.contactlat + "," + call.contactlong);
}
return latLons;
}
});
JavaRDD<String> distances = pipeInputs.pipe(SparkFiles.get(distScriptName));
System.out.println(StringUtils.join(distances.collect(), ","));
使用SparkContext.addFile(path)
方法,我们可以构建一个在单个job中每个工作节点都要下载的文件列表。这些文件可以来自驱动程序的本地文件系统(如例子中那样),或者来自HDFS或其他Hadoop支持的文件系统,也可以是一个HTTP,HTTPS
或FTP
的URI。当一个action在一个job中运行,每个节点都会开始下载这些文件。这些文件可以在工作节点的SparkFiles.getRootDirectory
路径或者SparkFiles.get(filename)
中找到。当然,这只是确定pipe()
方法能够在每个工作节点上找到脚本文件的方法之一。你可以使用其他的远程复制工具来将脚本文件放在每个节点可知位置上。
使用
SparkContext.addFile(path)
添加的所有文件都存放在同一个目录中,因此使用唯一名称非常重要。
一旦确定了脚本是可以使用的,RDD上的pipe()
方法可以很简单地把RDD中的元素传输给脚本。也许更智能的findDistance版本会接受SEPARATOR作为命令行参数。在那种情况下,以下任何一个都可以完成这项工作,尽管第一个是首选:
rdd.pipe(Seq(SparkFiles.get("finddistance.R"), ","))
rdd.pipe(SparkFiles.get("finddistance.R") + " ,")
第一种方式,我们把命令的调用当做具有位置的元素序列来传递(命令本身在零偏移的位置上);第二种方式,我们把它作为一个单独的命令字符串传递,然后Spark会分解成位置参数 。
我们也可以在需要的情况下明确设置pipe()
的环境变量。直接把环境变量的map作为pipe()
的第二个参数,Spark会去设置这些在值。
你现在应该至少理解了如何使用pipe()
来利用外部命令处理RDD的元素,还有如何将命令脚本分发到集群中工作节点能找到的位置。
Numeric RDD Operations(数值RDD的操作)
Spark提供了几种包含数值数据RDD的描述性统计操作。除此之外,更复杂的统计和机器学习方法之外,我们将在后面的第11章中介绍这些方法。
Spark的数值操作是通过流式算法实现的,它允许一次构建我们模型的一个元素。描述性统计对数据一次性计算出结果并且调用stats()
会返回一个StatsCounter
对象。表6-2介绍了StatsCounter
对象的方法。
Table 6-2. Summary statistics available fromStatsCounter
方法 | 作用 |
---|---|
count() |
元素数量 |
mean() |
元素平均值 |
sum() |
总值 |
max() |
最大值 |
min() |
最小值 |
variance() |
方差 |
sampleVariance() |
样本方差 |
stdev() |
标准差 |
sampleStdev() |
样本标准差 |
如果您只想计算其中一个统计数据,那么也可以直接在RDD上调用相应的方法,例如rdd.mean()
或rdd.sum()
。
在例6-19到6-21中,我们将使用汇总统计来从我们的数据中删除一些异常值。因为我们可能会遍历相同的RDD两次(一次是汇总统计,第二次是删除异常值),所以将RDD缓存起来。回到我们的呼叫日志的例子上,我们可以从呼叫日志上删除距离太远的联系地点。
Example 6-19. Removing outliers in Python
# Convert our RDD of strings to numeric data so we can compute stats and
# remove the outliers.
distanceNumerics = distances.map(lambda string: float(string))
stats = distanceNumerics.stats()
stddev = std.stdev()
mean = stats.mean()
reasonableDistances = distanceNumerics.filter(
lambda x: math.fabs(x - mean) < 3 * stddev)
print reasonableDistances.collect()
Example 6-20. Removing outliers in Scala
// Now we can go ahead and remove outliers since those may have misreported locations
// first we need to take our RDD of strings and turn it into doubles.
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)
Example 6-21. Removing outliers in Java
// First we need to convert our RDD of String to a DoubleRDD so we can
// access the stats function
JavaDoubleRDD distanceDoubles = distances.mapToDouble(new DoubleFunction<String>() {
public double call(String value) {
return Double.parseDouble(value);
}});
final StatCounter stats = distanceDoubles.stats();
final Double stddev = stats.stdev();
final Double mean = stats.mean();
JavaDoubleRDD reasonableDistances =
distanceDoubles.filter(new Function<Double, Boolean>() {
public Boolean call(Double x) {
return (Math.abs(x-mean) < 3 * stddev);}});
System.out.println(StringUtils.join(reasonableDistance.collect(), ","));
配合累加器和广播变量,分区处理,外部程序接口和汇总统计,我们最终完成了示例程序。
完整的代码在 src/python/ChapterSixExample.py,src/main/scala/com/oreilly/learningsparkexamples/scala/ChapterSixExample.scala,和src/main/java/com/oreilly/learningsparkexamples/java/ChapterSixExample.java这几个文件之中。
Conclusion(总结)
这一章节,我们介绍了一些Spark的高级编程特性,你可以使用这些特性来提高你程序的效率或可读性。后续章节将介绍部署和调整Spark应用程序,以及用于SQL,流媒体以及机器学习的内置库。我们也将开始看到更复杂和更完整的示例应用程序,它们利用了迄今为止描述的大部分功能,并且也会对你自己使用Spark的方式有所指导或启发。
Learning Spark中文版--第六章--Spark高级编程(2)的更多相关文章
- Learning Spark中文版--第六章--Spark高级编程(1)
Introduction(介绍) 本章介绍了之前章节没有涵盖的高级Spark编程特性.我们介绍两种类型的共享变量:用来聚合信息的累加器和能有效分配较大值的广播变量.基于对RDD现有的transform ...
- 简学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,或者调 ...
随机推荐
- Java之父 James Gosling 发表博文 《Too Soon》纪念乔布斯。
几个礼拜前,我们还在讨论乔布斯的辞职.虽然我们都知道这意味着什么,但是我没有想到一切来的如此之快.已经有很多关于这件事情的文章了,特别是"经济学人"的这篇文章. 乔布斯是一个很独特 ...
- ssl 原理和建立连接过程
ssl ("Secure Sockets Layer")加密原理 和https的关系https = http + ssl ssl 位置: SSL握手 证书主要作用是在SSL握手中, ...
- 基于Dapr的 Azure 容器应用
微软在 Ignite 2021 大会上发布了预览版的Azure Container Apps,这是一个完全托管的无服务器容器运行时间,用于大规模构建和运行现代应用程序.从2021 年 11 月 2 日 ...
- 你说说RPC的一个请求的流程是怎么样的?
前言 面试的时候经常被问到RPC相关的问题,例如:你说说RPC实现原理.让你实现一个RPC框架应该考虑哪些地方.RPC框架基础上发起一个请求是怎样一个流程等等.所以这次我就总结一波RPC的相关知识点, ...
- Spring Cloud Gateway 整合阿里 Sentinel网关限流实战!
大家好,我是不才陈某~ 这是<Spring Cloud 进阶>第八篇文章,往期文章如下: 五十五张图告诉你微服务的灵魂摆渡者Nacos究竟有多强? openFeign夺命连环9问,这谁受得 ...
- 【Go语言学习笔记】函数做参数和闭包
函数做参数 在Go语言中,函数也是一种数据类型,我们可以通过type来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型.类似于重写(同名覆盖). 回调函数:函数有一个参数是函数类型,这个 ...
- 问题 Q: 最大的数
题目描述 小明和小红在打赌说自己数学学的好,于是小花就给他们出题了,考考他们谁NB,题目是这样的给你N个数 在这n个数之间添加N-1个*或+,使结果最大,但不可以打乱原顺序,请得出这个结果 如 1 3 ...
- Unity——有限状态机FSM修改
FSM状态机改 一.前言 FSM状态机初版 之前写过一版有限状态机,后来发现很多问题: 前一个版本是记录了当前的状态,切换状态时,要等下一帧状态机Update的时候才会调动上个状态的退出,总会有一帧的 ...
- Jetpack架构组件学习(1)——LifeCycle的使用
原文地址:Jetpack架构组件学习(1)--LifeCycle的使用 | Stars-One的杂货小窝 要看本系列其他文章,可访问此链接Jetpack架构学习 | Stars-One的杂货小窝 最近 ...
- CentOS编译openjdk
编译openjdk 1. 下载openjdk源码 openjdk的官网是OpenJDK (java.net) 在网站左侧就能看到它的源码位置的链接 从图上可以看到,它的源码在两个位置有托管,Mercu ...