spark核心-RDD编程指导

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,(AccumulatorV2API自查一下)

同样的情况还发生在打印的时候,例如: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中的ab两个参数分别是两个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.unionrdd2;

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])//根据keyRDD中的元素排序,但是首先这个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。

洗牌后每个分区的元素集是确定的,但是分区中的每个元素并不是有序的,想要有序,可以使用mapPartitionsrepartitionAndSortWithinPartitionssortBy

shuffle操作也是一系列map(映射)操作和reduce(归纳)操作。(注意,这里的说的mapreduce并不是直接依赖于spark的map,reduce,只是一个同样的概念。映射,归纳。)

shuffle操作会消耗大量的内存,因为:

一、单个map任务的结果会保存在内存中,等着被reduce

二、spark使用内存数据结构来组织管理每一行记录。如果内存不够,会产生额外的银盘存储开销和垃圾回收开销。

spark还会保存很多中间RDD数据集在硬盘上,直到可能不再使用为止。保存的临时目录在spark.local.dir这个属性配置。

RDD PersistenceRDD的持久化)

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,参见上文,理解封闭区的说明解释。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值