rdd-programming-guide
官网地址:http://spark.apache.org/docs/latest/rdd-programming-guide.html
本文是根据官网原文翻译简化,是个人在学习过程中消化所得,感觉可以作为初识spark的一个指导文档,也是spark的核心东西。
Linking with Spark(准备使用spark)
spark是基于scala的语言环境的,spark2.4是默认被构建基于scala2.11来使用的,当然基于其他的scala版本来使用也可以。只是如果要在scala上写应用的话,最好还是采用兼容的版本。
开始写一个spark应用之前,需要先引入spark的相关依赖。
groupId = org.apache.spark
artifactId = spark-core_2.11
version = 2.4.0
如果spark的计算资源来源于hdfs,那么还要引入hadoop-client的相关依赖。
groupId = org.apache.hadoop
artifactId = hadoop-client
version = <your-hdfs-version>
最后,如果用scala来编写应用的话在代码中就要引入:
import org.apache.spark.SparkContext import org.apache.spark.SparkConf
如果使用java来编写的应用的话,就要在代码中引入:
import org.apache.spark.api.java.JavaSparkContext; import org.apache.spark.api.java.JavaRDD; import org.apache.spark.SparkConf;
Initializing Spark(初始化spark)
一个spark程序做的第一件事,就是去创建一个SparkConText对象,要创建一个SparkConText对象还需要一个包含应用信息的SparkConf对象。
scala:
val conf = new SparkConf().setAppName(appName).setMaster(master)
new SparkContext(conf)
java:
SparkConf conf = new SparkConf().setAppName(appName).setMaster(master);
JavaSparkContext sc = new JavaSparkContext(conf);
new SparkConf的时候,一般都至少要传入appname。集群模式中setMaster是设置yarn集群的URL,并且一般通过spark-submit --master来设置,而不是设置在这里,local模式的话,就传入“local”就可以了。
Using the Shell(使用spark-shell交互式启动spark)
如果使用spark-shell来启动,就默认创建了一个SparkContext(sc)和一个SparkSession(spark),如下图所示:
直接启动spark-shell的话,默认master=local ;appname=app
或者通过—master --jars --package 等参数,传入master,依赖jars或者包。
Resilient Distributed Datasets (RDDs)
弹性分布式数据集(就是一个可以用于分布式计算的集合)
弹性分布式数据集RDDs;SPARK就是围绕着这个概念进行的。只有RDD数据才可以进行并发操作。普通的集合或者文档就不行。
有两种方式可以创建一个RDD
1,根据已有集合或者数组。paralleize()
2,根据文件,通过textFile导入进来就是RDD: sc.textFile("/root/wordcount.txt")
JavaRDD<String> distFile = sc.textFile("data.txt");
这种方式,会将文本中每一行作为一个元素。
这个在java和scala中都一样。(其实所有的scala方法都有对应的spark-javaAPI,所以后文可能不再写java中的方法)
如果textFile的路径是文件系统的话,要注意让worker节点也要读得到,所以一般都是hdfs文件系统地址hdfs://
。
textFile不光可以读入文本文档,还可以读入目录,甚至是压缩文件。
text还可以传入第二个参数,分区数,不传的话,默认按照每128M分一个区。
Spark’s Java API还支持其他得数据格式。
JavaSparkContext.wholeTextFiles可以读入一个目录,然后返回类似list<Map<filename,content>>的结构。textfile返回的是类似list<line>。注意这里说的是类似,因为实际返回的都是RDD。
JavaRDD.saveAsObjectFile and JavaSparkContext.objectFile,可以很方便(虽然并不怎么高效)将RDD保存为序列化文件,以及从序列化文件中读取RDD。
RDD Operations(可以对RDD做哪些操作?)
前面我们已经知道怎么获取RDD了。那么怎么对RDD进行操作,以便得到我们想要的结果呢?
RDD有两种操作,转换操作(transformations),和计算操作(actions)(中文名我随便取的别介意)。转换操作就是把已有的RDD变成一个新的RDD。map(映射)就是一种转换操作。计算操作就是基于RDD和计算方法,返回一个计算结果。reduce就是一个计算操作。
所有的转换操作都是懒加载的,并不立即执行。只有在需要计算的时候才会执行。为什么这样呢?因为我们通常并不需要转换的结果,只需要计算后的结果。如果我们想要转换后的RDD怎么办呢?那么就对转换后的RDD执行.persist()。这样在第一次执行之后,这个RDD也会被保留在内存中。
一个计算文本字符总长度的示例:
var txt=sc.textFile("/root/wordcount.txt")
var len=txt.map(s => s.length()).reduce((a, b) => a + b)
.reduce的时候,才分布式执行。
map是映射,reduce是递归。两者的用法请去自行了解。上面的参数是简单的匿名函数,将RDD中的每一个元素映射成s的长度,然后将RDD的每个元素累加起来。当然也可以传入更复杂的函数。
Passing Functions to Spark(传入自定义函数)
上文的map和reduce的参数,是要对RDD处理的函数,传入形式有两种,一种是传入匿名函数,另一种是传入外部定义的函数:
object MyFunctions {
def func1(s: String): String = { ... }
}
myRdd.map(MyFunctions.func1)
scala中可以直接传入一个方法,java中就只能传入一个实现了Function接口的类。
JavaRDD<Integer> lineLengths = lines.map(new Function<String, Integer>() { public Integer call(String s) { return s.length(); } });
例如要计算一个txt中有多少个单词,那么function就应该是
def func1(s:String):int={
words=s.split(“ ”);
retrun words.length;
}
Understanding closures(理解封闭区)
在使用spark中最难理解的地方在于变量或者函数的生命周期和使用区间。RDD的操作中。如果不小心在变量的有效区间外去修改变量就会引起一系列问题(疑惑)。
var counter = 0
var rdd = sc.parallelize(data)
// Wrong: Don't do this!!
rdd.foreach(x => counter += x)
println("Counter value: " + counter)
这段代码,在单机模式中,如果只在一个executor中运行可能没有问题,但是如果在分布式运行环境中,封闭区内的变量会被复制到每一个executor中。每一节点上的counter都是一个副本,每一节点上的counter都是从0开始,显然不是我们想要的结果。
如果我们要在分布式中对同一个变量进行变更怎么办呢?那么使用Accumulator。
Accumulators通常用于实现计数器,以及求和。spark本地支持数值类型的Accumulators。上面的例子就可以改编使用如下:
val accum = sc.longAccumulator("My Accumulator") //初始化之后默认为0
sc.parallelize(Array(1, 2, 3, 4)).foreach(x => accum.add(x));
除了spark已经提供的,还可以自定义类,通过继承AccumulatorV2,(AccumulatorV2的API自查一下)
同样的情况还发生在打印的时候,例如:rdd.foreach(println)
,
在分布式环境下,每一个executor都会输出到自己的stdout(标准输出),而不是输出到提交任务的stdout中。如果非要打印的话,可以使用collect().这个方法会把RDD所有数据都收集到一个机器上。或者你只需要打印一部分数据,就使用take(n)只收集一部分数据到一台机器上。原文如下:
collect()
fetches the entire RDD to a single machine; if you only need to print a few elements of the RDD, a safer approach is to use the take()
: rdd.take(100).foreach(println)
.
Working with Key-Value Pairs(键值对RDD)
RDDs数据集支持很多对象类型,但是有一些特殊的操作,需要键值对类型(tuple)的RDD数据才可以使用。常见的 “shuffle”操作,例如根据key来累加或者分组的操作。
在scala中这些操作是一些定义好的函数,可以直接调用。例如计算一个文本中的单词各自出现了多少次
var txt=sc.textFile("/root/wordcount.txt")//读取文本,按行生成RDD
var rddi=txt.flatMap(s=>s.split(" ")) //把每一行,再拆分成每一个单词一行的RDD
rddi.map(s => (s,1) ).reduceByKey((a,b)=>a+b) //map将每一行的String单词变成tuple(String,int)类型。然后使用reduceByKey,根据key值分组,然后对v值操作。reduceByKey中的a,b两个参数分别是两个tuple中的v。
Transformations(一些通用的转换方法)
(明细参考API文档)下面是scala中的API,java中的类似。
map//通过传入的函数,映射RDD中的每一个元素,然后返回映射后的新RDD
filter(func)//通过传入的函数,判断RDD的每一个元素,过滤掉返回为false的元素。返回新的RDD
flatMap(func)//将RDD中的每一个元素,映射成0个或者多个元素。例如上面例子中txt.flatMap(s=>s.split(" "))
mapPartitions(func)//
mapPartitionsWithIndex(func)//
sample(withReplacement, fraction, seed)
union(otherDataset)//合并两个RDD rdd1.union(rdd2);
intersection(otherDataset)//返回两个RDD的交集RDD
distinct([numPartitions]) //对RDD去重操作。
groupByKey([numPartitions])//根据key分组, returns a dataset of (K, Iterable<V>) pairs.。
reduceByKey(func, [numPartitions])//根据key来做递归操作。
aggregateByKey(zeroValue)(seqOp, combOp, [numPartitions])//根据key值来做汇总操作
sortByKey([ascending], [numPartitions])//根据key给RDD中的元素排序,但是首先这个key得实现了Ordered
join(otherDataset, [numPartitions])//连接操作,跟数据库连接操作差不到。两个RDD之间通过key连接再一起,eturns a dataset of (K, (V, W)) pairs,跟数据库一样,还有左外连接leftOuterJoin,右外连接rightOuterJoin
,全外连接fullOuterJoin
cogroup(otherDataset, [numPartitions])
cartesian(otherDataset)
pipe(command, [envVars])//将rdd的所有元素作为shell命令的标准输入,然后返回标准输出生成的RDD
coalesce(numPartitions) //减少RDD中的分区至coalesce个
repartition(numPartitions)//将RDD中的元素,重新随机shuffle(洗牌)生成新的分区数。
…
Actions(一些通用的操作方法)
reduce(func)//根据传入的函数,对RDD中的元素做归纳,主要是能够再分布式环境中做并发计算。
collect()//返回RDD的所有元素,通常用于过滤或者其他返回更小的子集操作之后。
count()//返回RDD中元素的个数
first()//返回RDD数据集的第一个元素
take(n)//返回RDD的前N个元素,形成的数组
takeSample(withReplacement, num, [seed])//返回一个数据集的n个元素的随机样本数组。
takeOrdered(n, [ordering]) //返回排序后的前n个元素,可以传入自定义的比较器
saveAsTextFile(path)//将RDD保存为给定路径下的text文件(或者文件集),每个元素就是一行
saveAsSequenceFile(path)// 将RDD保存为hadoop的sequenceFile
saveAsObjectFile(path)//将RDD保存为序列化文件。SparkContext.objectFile()
.可以读取文件成RDD对象。
countByKey()//根据RDD的每个元素的key,计算每个key的总数量。
foreach(func)//对RDD中的每一个元素执行func操作,如果是修改这个元素,这个元素最好是Accumulators。不然可能会出错,去产看上面的封闭区的解释。
Shuffle operations(转移/洗牌操作)
shuffle是spark的数据重分发机制,所以可以跨区分组,通常涉及executors甚至机器之间的数据复制,所以shuffle是一个比较耗时的操作。
例如reduceByKey(func)根据key把RDD归纳起来生成一个新的RDD。然而这些右相同key的值,可能并不在一个分区甚至一个机器上。所以要执行reduceByKey就要库跨越所有分区把数据集合到一起,这就是shuffle。
洗牌后每个分区的元素集是确定的,但是分区中的每个元素并不是有序的,想要有序,可以使用mapPartitions,repartitionAndSortWithinPartitions,sortBy
shuffle操作也是一系列map(映射)操作和reduce(归纳)操作。(注意,这里的说的mapreduce并不是直接依赖于spark的map,reduce,只是一个同样的概念。映射,归纳。)
shuffle操作会消耗大量的内存,因为:
一、单个map任务的结果会保存在内存中,等着被reduce
二、spark使用内存数据结构来组织管理每一行记录。如果内存不够,会产生额外的银盘存储开销和垃圾回收开销。
spark还会保存很多中间RDD数据集在硬盘上,直到可能不再使用为止。保存的临时目录在spark.local.dir
这个属性配置。
RDD Persistence(RDD的持久化)
spark一个重要的能力就是在跨操作的时候,持久化或者缓存内存中的数据集RDD,每一个节点都会保存它计算的任何一部分,并在之后的操作中重用。让接下来的操作可以更快。缓存是迭代算法和更快交互的关键工具。
使用persist() or cache()区主动的缓存一个RDD,在第一次计算后被持久化或者缓存下来。如果丢失了,会重新计算出来。
持久化有多个级别:persist()的时候可以传入级别。
StorageLevel.MEMORY_ONLY
将RDD保存为非序列化的java对象,保存在jvm中,如果内存不够,一些分区就不能被缓存,会在需要的时候被重新快速的计算。(也是默认级别)
MEMORY_AND_DISK:将RDD保存为非序列化的java对象,保存在jvm中,如果内存不够,一些分区就被保存在硬盘上。
MEMORY_ONLY_SER :将RDD保存为序列化java对象,通常更节省空间,只是读的时候更耗CPU,溢出部分重新计算(Java and Scala)
MEMORY_AND_DISK_SER(Java and Scala):将RDD保存为序列化java对象,通常更节省空间,只是读的时候更耗CPU,溢出部分保存在硬盘中
DISK_ONLY:将RDD保存在硬盘上
MEMORY_ONLY_2, MEMORY_AND_DISK_2, etc:跟上面类似:只是持久化的时候会保存两份在两个集群节点上
OFF_HEAP (experimental):
注意:使用python的话,则总是被序列化的。虽然有些时候spark也会自动区持久化RDD,但是我们逻辑上确定需要的话,最好还是自己手动persist一下。
持久化级别的选择:
1,内存够的话,可以使用默认级别,因为最快,
2,不够的话,使用MEMORY_ONLY_SER
可以节省内存空间,只是序列化和反序列会小号一点时间,但是也算快了
3,如果重计算比较快的话,就不要保存在硬盘上,只有重计算复杂耗时的时候,才保存到硬盘中。
4,所有的级别都支持重计算以完全容错能力,但是如果有备份的话,在故障恢复的时候,会更快。
spark会自动监视并删除老的不要的RDD,如果你想手动删除的话,就使用RDD.unpersist()
Shared Variables(共享变量)
尽管跨任务的读写共享变量并不高效,但是spark还是为两种常见使用模式提供了两种有限类型的共享变量。broadcast variables and accumulators广播变量和累加器。
广播变量允许开发者在每一个机器上保存一个只读变量的缓存,而不是在任务间传递变量副本。广播变量以高效的方式为每一个节点提供一个大输入集合的副本。spark使用广播算法减少交互时间。
spark在每一个stage(任务阶段)会自动生成多个任务阶段都要使用的广播变量(通过序列化后缓存的方式)。
所以手动创建广播变量的方式仅仅被用于多个阶段需要一样的数据,或者需要以非序列化形式缓存广播变量的时候。
scala> val broadcastVar = sc.broadcast(Array(1, 2, 3))//将v= Array(1, 2, 3)创建为广播变量。
broadcastVar: org.apache.spark.broadcast.Broadcast[Array[Int]] = Broadcast(0)
scala> broadcastVar.value//通过.value的方式,获取广播变量v
累加器accumulators,参见上文,理解封闭区的说明解释。