一般常用两个shell,一个是spark-shell用来写scala或者java语法。另一个是pyspark,用来写python。提交python或者jar包常用spark-submit。spark-sql顾名思义。
利用Spark shell 很容易学习Spark API,同时也Spark shell也是强大的交互式数据分析工具。Spark shell既支持Scala(Scala版本的shell在Java虚拟机中运行,所以在这个shell中可以引用现有的Java库),也支持Python。
Spark最主要的抽象概念是个分布式集合,也叫作弹性分布式数据集(Resilient Distributed Dataset – RDD)。(shell中有时候执行完scala操作会有一行显示 res应该就是指RDD,res是RDD中第一个R的缩写)RDD可以由Hadoop InputFormats读取HDFS文件创建得来,或者从其他RDD转换得到。
他是一个可分区的数据集,需要注意的是RDD本身不包含数据,只是记录数据的位置,充当一个映射的作用,当然,它也可以持久化或者缓存方便使用。
下面我们就先利用Spark源代码目录下的README文件来新建一个RDD,因为读取文件生成新的RDD也是产生了一个RDD,所以该textFile属于transformation算子:
scala> val textFile = sc.textFile("README.md")
textFile: spark.RDD[String] = spark.MappedRDD@2ee9b6e3
RDD有两种算子,action算子(actions)返回结果,transformation算子(transformations)返回一个新RDD。需要注意的是,transformation操作进入shell时并不执行,只有action算子开始运行后,所有的操作才开始。(换而言之,如果之前的tramsformation操作读取了错误的文件或者没正确执行,只有在action的时候才会报错)
我们先来看一下action算子:
scala> textFile.count() // Number of items in this RDD
res0: Long = 126
scala> textFile.first() // First item in this RDD
res1: String = # Apache Spark
transformation算子。我们利用filter``这个transformation算子返回一个只包含原始文件子集的新RDD。
scala> val linesWithSpark = textFile.filter(line => line.contains("Spark"))
linesWithSpark: spark.RDD[String] = spark.FilteredRDD@7dd4af09
连起来
scala> textFile.filter(line => line.contains("Spark")).count() // How many lines contain "Spark"?
res3: Long = 15
RDD一个分区对应一个task。一般每个cpu有2-4个分区。
分区和分片指的是同一个东西。
如果数据源来自hdfs,一个block会对应一个分区
弹性:不是指数据集可以动态拓展,是指血缘的容错机制
分布式:RDD是多节点存储的。类似于hdfs切割文件为block,RDD分为多个partition存放在不同节点。
每一个RDD是在不同的分区上并行执行的。
至于后续遇到shuffle的操作,RDD的partition可以根据Hash再次进行划分(一般pairRDD是使用key做Hash再取余来划分partition)
持久化:以hdfs为例,持久化时每个partition会存成一个文件,小于block的大小(128M)时,就会存成一个文件。如果大于,就会存为多个文件。
例子
假设,第一次保存RDD时10个partition,每个partition有140M。那么该RDD保存在hdfs上就会有20个block,下一批次重新读取hdfs上的这些数据,RDD的partition个数就会变为20个。再后续有类似union的操作,导致partition增加,但是程序有没有repartition或者进过shuffle的重新分区,这样就导致这部分数据的partition无限增加,这样一直下去肯定是会出问题的。所以,类似这样的情景,再程序开发结束一定要审查需不需要重新分区。
正确的做法是,140M*10/128M划分为整数分区。在存储
共享变量是指可以在并行操作中共享的变量。
广播变量(BroadCast):一份只读的数据,封装好后送给各个节点,这样就不用一个数据搬来搬去。
累加器(Accumulator):用来跨界点执行 累加操作比如计数和求和
已经存在集合,通过SparkContext.parallelize()生成一个新的RDD。集合中数据将复制到新的RDD中。
val data = Array(1, 2, 3, 4, 5)
val distData = sc.parallelize(data)
spark支持hadoop支持的任何数据源。
如果是本地文件系统,那么所有的worker节点都得能用相同的路径访问到,或者挂载一个共享文件系统。
文件输入时都支持以下参数(目录 压缩文件 通配符)
textfile的可选参数是分区个数。默认spark会为每一个block创建一个分区。分区数必须大于等于block数
- SparkContext.wholeTextFiles 可以读取一个包含很多小文本文件的目录,并且以 (filename, content) 键值对的形式返回结果。这与textFile 不同,textFile只返回文件的内容,每行作为一个元素。
- 对于SequenceFiles,可以调用 SparkContext.sequenceFile[K, V],其中 K 和 V 分别是文件中key和value的类型。这些类型都应该是 Writable 接口的子类, 如:IntWritable and Text 等。另外,Spark 允许你为一些常用Writable指定原生类型,例如:sequenceFile[Int, String] 将自动读取 IntWritable 和 Text。
- 对于其他的Hadoop InputFormat,你可以用 SparkContext.hadoopRDD 方法,并传入任意的JobConf 对象和 InputFormat,以及key class、value class。这和设置Hadoop job的输入源是同样的方法。你还可以使用 SparkContext.newAPIHadoopRDD,该方法接收一个基于新版Hadoop MapReduce API (org.apache.hadoop.mapreduce)的InputFormat作为参数。
- RDD.saveAsObjectFile 和 SparkContext.objectFile 支持将RDD中元素以Java对象序列化方式不如Avro效率高,却为保存RDD提供了一种简便方式。
计算出文件中单词最多的行中有多少单词
scala> textFile.map(line => line.split(" ").size).reduce((a, b) => if (a > b) a else b)
首先,用一个map算子将每一行映射为一个整数,返回一个新RDD。然后,用reduce算子找出这个RDD中最大的单词数。map和reduce算组的参数都是scala 函数体(闭包),且函数体内可以使用任意的语言特性,或引用scala/java库。例如,我们可以调用其他函数。为了好理解,下面我们用Math.max作为例子:
scala> import java.lang.Math
import java.lang.Mathscala> textFile.map(line => line.split(" ").size).reduce((a, b) => Math.max(a, b))
res5: Int = 15
Hadoop上的MapReduce是大家耳熟能详的一种通用数据流模式。而Spark能够轻松地实现MapReduce流程:
scala> val wordCounts = textFile.flatMap(line => line.split(" ")).map(word => (word, 1)).reduceByKey((a, b) => a + b)
wordCounts: spark.RDD[(String, Int)] = spark.ShuffledAggregatedRDD@71f027b8
这个例子里,我使用了flatMap, map, and reduceByKey 这几个transformation算子,把每个单词及其在文件中出现的次数转成一个包含(String,int)键值对的RDD,计算出每个单词在文件中出现的次数:
scala> wordCounts.collect()
res6: Array[(String, Int)] = Array((means,1), (under,2), (this,3), (Because,1), (Python,2), (agree,1), (cluster.,1), ...)
分类
从功能上划分为A和T两类算子
从存储数据上来说分为三类
基础类型的普通算子,例如String key-value的数据处理bykey算子 针对数字类型处理的计算算子
特性
Transformation操作是惰性的,只会记录操作动作。当有结果要返回给Driver时才执行操作。通过DAGScheduler和TaskScheduler分发到集群中运行。这个特性叫惰性求值
之所以这么设计是因为,在action之前,就可以优化DAG或者各种提前的优化,最后在高效的执行。
每一个action运行的时候,与其关联的所有TransformationRDD都会重新计算。也可以用presist方法将数据持久化刀到内存或硬盘中,为了下次更快访问,可以把数据存储到集群中。
lineLengths.persist() 可以缓存在内存中
执行各个stage的任务,结果返回给Driver
reduce( (T, T) ⇒ U )
规约所有的值
val rdd = sc.parallelize(Seq(("手机", 10.0), ("手机", 15.0), ("电脑", 20.0)))
val result = rdd.reduce((curr, agg) => ("总价", curr._2 + agg._2))
println(result)
sc.parallelize(Seq(("a", 1), ("a", 1), ("b", 1))) .reduceByKey( (curr, agg) => curr + agg ) .collect()
输出(总价,45.0)
reduce后的lambda表达式意思是生成一个新RDD,其中第一个值是常量,第二个值是所有元素第二个值的累加。
- reduce 和 reduceByKey 是完全不同的, reduce 是一个 action, 并不是 Shuffled 操作
- 本质上 reduce 就是现在每个 partition 上求值, 最终把每个 partition 的结果再汇总
collect()
rdd1.collect()
用数组的形式返回数据集中所有元素
count
返回元素个数
countByKey()
得出整个数据集中key出现的次数,常用来解决数据倾斜,用来查看倾斜的key
val rdd = sc.parallelize(Seq(("手机", 10.0), ("手机", 15.0), ("电脑", 20.0)))
val result = rdd.countByKey()
println(result)
- 返回结果为
Map(key → count)- 常在解决数据倾斜问题时使用, 查看倾斜的 Key
first
返回第一个元素
take(N)
返回前N个元素
foreach( T ⇒ … )
遍历每一个元素
takeSample(withReplacement, fract)
类似于sample,但是这个是action直接返回结果
fold(zeroValue)( (T, T) ⇒ U )
指定初始值和聚合函数,折叠聚合整个数据集
saveAsTextFile(path)
保存文件到path路径
saveAsSequenceFile(path)
结果存入sequence文件
创建新的RDD,可以基于旧的RDD或者文件创建。
Map
把RDD中一对一数据转换为另一种形式
sc.parallelize(Seq(1, 2, 3))
.map( num => num * 10 )
.collect()
输出是10,20,30。这是num是指原来的本体,操作就是每个旧numX10得到新num
map(word => (word, 1)同理,原来是是一个单词,比如xcw,现在变成xcw,1(需要注意的是这里得到的应该是新的RDD)
flatMap
和Map类似,但是是一对多。
sc.parallelize(Seq("Hello lily", "Hello lucy", "Hello tim"))
.flatMap( line => line.split(" ") )
.collect()
输出是hello lily hello lucy hello tim 通过split分开
filter
过滤信息
sc.parallelize(Seq(1, 2, 3))
.filter( value => value >= 3 )
.collect()
输出是3
mapPartitions(List[T] ⇒ List[U])
RDD[T] ⇒ RDD[U] 和 map 类似, 但是针对整个分区的数据转换
mapPartitionsWithIndex
和 mapPartitions 类似, 只是在函数中增加了分区的 Index
mapvalues
和map类似,但是只对value操作
sc.parallelize(Seq(("a", 1), ("b", 2), ("c", 3)))
.mapValues( value => value * 10 )
.collect()
输出是 a,10 b,20 c,30
sample
从当前数据集中抽取一部分数据出来,这个算子常用来缩小数据集规模。
sc.parallelize(Seq(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
.sample(withReplacement = true, 0.6, 2)
.collect()
第一个参数是指被抽取的数是否放回原数组继续抽取,如果是true。生成的数据集可能是有数据重复。
第二个参数是抽取比例,此处为60%
第三个是第一次随机抽取的下标。一般使用默认值。
输出是1 3 3 5 5 10.一共六个数。
union
把两列数据拼起来
val rdd1 = sc.parallelize(Seq(1, 2, 3))
val rdd2 = sc.parallelize(Seq(4, 5, 6))
rdd1.union(rdd2)
.collect()
输出是12 3 4 5 6
join
不同RDD的相同key链接起来。
val rdd1 = sc.parallelize(Seq(("a", 1), ("a", 2), ("b", 1)))
val rdd2 = sc.parallelize(Seq(("a", 10), ("a", 11), ("a", 12)))
rdd1.join(rdd2).collect()
输出是 Array[(String, (Int, Int))] = Array((a,(1,10)), (a,(1,11)), (a,(1,12)), (a,(2,10)), (a,(2,11)), (a,(2,12)))
join(other, numPartitions) 第一个参数是要join的rdd,
partitioner or numPartitions可选, 可以通过传递分区函数或者分区数量来改变分区。join的结果是笛卡尔积。相当于rdd1的a key 会去匹配rdd2 的所有a key
intersection
用于求左侧和右侧集合都有的数据,就是交集。
val rdd1 = sc.parallelize(Seq(1, 2, 3, 4, 5))
val rdd2 = sc.parallelize(Seq(4, 5, 6, 7, 8))
rdd1.intersection(rdd2)
.collect()
输出是 4,5
subtract(other, numPartitions)
差集,可以设置分区数。
Distinct
原数组重复数据去重
sc.parallelize(Seq(1, 1, 2, 2, 3))
.distinct()
.collect()
输出是1,2,3
reduceByKey
先针对数据生成tuple(比如相同key的放一起)。然后对每个组执行reduce算子
sc.parallelize(Seq(("a", 1), ("a", 1), ("b", 1)))
.reduceByKey( (curr, agg) => curr + agg )
.collect()
输出是a 2 b 1。因为俩a,1是一个tuple。然后两者相加,a,2
ReduceByKey 只能作用于 Key-Value 型数据, Key-Value 型数据在当前语境中特指 Tuple2
ReduceByKey 是一个需要 Shuffled 的操作
和其它的 Shuffled 相比, ReduceByKey是高效的, 因为类似 MapReduce 的, 在 Map 端有一个 Cominer, 这样 I/O 的数据便会减少
groupByKey
根据key生成新的集合
sc.parallelize(Seq(("a", 1), ("a", 1), ("b", 1)))
.groupByKey()
.collect()
输出是两个集合,一个a,1,1 一个b,1
如果加一个a,2。则在输出a的集合里加个2
GroupByKey 算子的主要作用是按照 Key 分组, 和 ReduceByKey 有点类似, 但是 GroupByKey 并不求聚合, 只是列举 Key 对应的所有 Value
注意点
GroupByKey 是一个 Shuffled
GroupByKey 和 ReduceByKey 不同, 因为需要列举 Key 对应的所有数据, 所以无法在 Map 端做 Combine, 所以 GroupByKey 的性能并没有 ReduceByKey 好
combineByKey
把输入里的数据根据key,合并
var rdd = sc.makeRDD(Array(("A",2),("A",1),("A",3),("B",1),("B",2),("C",1)))
val collect: Array[(String, String)] = rdd.combineByKey((v: Int) => v + "_",(c: String, v: Int) => c + "@" + v,//同一分区内(c1: String, c2: String) => c1 + "$" + c2).collect
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rmUtEmBk-1669020639306)(SparkRDD.assets/image-20221107173521337.png)]
输出是
(B,1_$2_), (C,1_), (A,2_@1$3_)
首先需要知道,该lambda所有操作都是针对value,也就是参数列表里没有对key的操作。
第一个参数是初始化参数,根据分区(设置分区数为3,RDD.getpartitions可以看分区数,也就是共三个分区,每个分区两个元素),每个分区的不同key都会初始化一次。(比如某个分区有AAB三个key,则初始化第一个A和最后一个B,需要注意的是每个分区都不冲突,比如A在一分区初始化了,不影响A这个Key在第二个分区初始化)
第二个参数是分区间的操作,左边参数是相同key的元素,右边是其需要执行的函数。这里string和int是指相同key的value值,
不一样是因为第一步初始化时,每个分区的key都初始化一次。则每个分区的每个key至少有一个初始化后加了_,所以是string类型,int类型则是没初始化的元素。
以一个分区为例,这个分区所有key都初始化过了,则rdd算子干的事就是从上往下遍历。遇到一个初始化过的key就记住位置,下面有相同key时进行函数操作。可以理解为对第一个初始化过的key不停的累加后续key的值。
第三个参数是分区间的合并,所有分区内执行完第二部操作后还要继续合并。这里的输入参数都是string是因为上一步初始化后的key是string,他们累加其他key后也是string。所以这一步的输入值都是string。
aggregateByKey()
按照key聚合所有的value(底层调用的是combinebykey)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HpzrAQeJ-1669020639307)(SparkRDD.assets/image-20221107173546062.png)]
val rdd = sc.parallelize(Seq(("手机", 10.0), ("手机", 15.0), ("电脑", 20.0)))
val result = rdd.aggregateByKey(0.8)(
seqOp = (zero, price) => price * zero,
combOp = (curr, agg) => curr + agg
).collect()
println(result)
调用
rdd.aggregateByKey(zeroValue)(seqOp, combOp)
参数
zeroValue 初始值(后续可能用到的变量)
seqOp 转换每一个值的函数(第一个参数是初始值)
comboOp 将转换过的值聚合的函数
注意点 为什么需要两个函数? aggregateByKey 运行将一个 RDD[(K, V)] 聚合为 RDD[(K, U)], 如果要做到这件事的话, 就需要先对数据做一次转换, 将每条数据从 V 转为 U, seqOp 就是干这件事的.当 seqOp 的事情结束以后, comboOp 把其结果聚合
和 reduceByKey 的区别::
aggregateByKey 最终聚合结果的类型和传入的初始值类型保持一致(因为初始值的乘积操作会使数据类型改变)
reduceByKey 在集合中选取第一个值作为初始值, 并且聚合过的数据类型不能改变
foldByKey
和reduceBykey类似。根据相同key聚合value。但是这个可以带一个初始值。
sc.parallelize(Seq(("a", 1), ("a", 1), ("b", 1)))
.foldByKey(zeroValue = 10)( (curr, agg) => curr + agg )
.collect()
- FoldByKey 是 AggregateByKey 的简化版本, seqOp 和 combOp 是同一个函数
- FoldByKey 指定的初始值作用于每一个 Value
- 根据上述一二可知,curr+age的操作不仅是同key间的聚合操作,还是每个key和初始值间的操作。
cogroup(other, numPartitions)
将多个RDD中相同的key分组
val rdd1 = sc.parallelize(Seq(("a", 1), ("a", 2), ("a", 5), ("b", 2), ("b", 6), ("c", 3), ("d", 2)))
val rdd2 = sc.parallelize(Seq(("a", 10), ("b", 1), ("d", 3)))
val rdd3 = sc.parallelize(Seq(("b", 10), ("a", 1)))
val result1 = rdd1.cogroup(rdd2).collect()
val result2 = rdd1.cogroup(rdd2, rdd3).collect()
/*
执行结果:
Array(
(d,(CompactBuffer(2),CompactBuffer(3))),
(a,(CompactBuffer(1, 2, 5),CompactBuffer(10))),
(b,(CompactBuffer(2, 6),CompactBuffer(1))),
(c,(CompactBuffer(3),CompactBuffer()))
)
*/
println(result1)
/*
执行结果:
Array(
(d,(CompactBuffer(2),CompactBuffer(3),CompactBuffer())),
(a,(CompactBuffer(1, 2, 5),CompactBuffer(10),CompactBuffer(1))),
(b,(CompactBuffer(2, 6),CompactBuffer(1),Co...
*/
println(result2)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5IiSZyCB-1669020639307)(SparkRDD.assets/image-20221108101101354.png)]
作用
多个 RDD 协同分组, 将多个 RDD 中 Key 相同的 Value 分组
调用
cogroup(rdd1, rdd2, rdd3, [partitioner or numPartitions])
参数
rdd… 最多可以传三个 RDD 进去, 加上调用者, 可以为四个 RDD 协同分组
partitioner or numPartitions 可选, 可以通过传递分区函数或者分区数来改变分区
注意点
对 RDD1, RDD2, RDD3 进行 cogroup, 结果中就一定会有三个 List, 如果没有 Value 则是空 List, 这一点类似于 SQL 的全连接, 返回所有结果, 即使没有关联上
CoGroup 是一个需要 Shuffled 的操作
sortBy(ascending, numPartitions)
排序算法
val rdd1 = sc.parallelize(Seq(("a", 3), ("b", 2), ("c", 1)))
val sortByResult = rdd1.sortBy( item => item._2 ).collect()
val sortByKeyResult = rdd1.sortByKey().collect()
println(sortByResult) Array((c,1), (b,2), (a,3))
println(sortByKeyResult) Array((a,3), (b,2), (c,1))
作用
排序相关相关的算子有两个, 一个是 sortBy,其根据value排序(此处指定了排序第二个元素,也就是value)
另外一个是 sortByKey,根据key排序,默认升序
调用
sortBy(func, ascending, numPartitions)
参数
func 通过这个函数返回要排序的字段
ascending 是否升序
numPartitions 分区数
注意点
普通的 RDD 没有 sortByKey, 只有 Key-Value 的 RDD 才有
sortBy 可以指定按照哪个字段来排序, sortByKey 直接按照 Key 来排序
partitionBy(partitioner)
使用用传入的 partitioner 重新分区, 如果和当前分区函数相同, 则忽略操作
coalesce(numPartitions)
减少分区数
val rdd = sc.parallelize(Seq(("a", 3), ("b", 2), ("c", 1)))
val oldNum = rdd.partitions.length
val coalesceRdd = rdd.coalesce(4, shuffle = true)
val coalesceNum = coalesceRdd.partitions.length
val repartitionRdd = rdd.repartition(4)
val repartitionNum = repartitionRdd.partitions.length
print(oldNum, coalesceNum, repartitionNum)
作用
一般涉及到分区操作的算子常见的有两个, repartitioin 和 coalesce, 两个算子都可以调大或者调小分区数量
调用
repartitioin(numPartitions)
coalesce(numPartitions, shuffle)
参数
numPartitions 新的分区数
shuffle 是否 shuffle, 如果新的分区数量比原分区数大, 必须 Shuffled, 否则重分区无效
注意点
repartition 和 coalesce 的不同就在于 coalesce 可以控制是否 Shuffle
repartition 是一个 Shuffled 操作
repartitionAndSortWithinPartitions
重新分区的同时升序排序, 在
partitioner中排序, 比先重分区再排序要效率高, 建议使用在需要分区后再排序的场景使用
RDD算子会生成专用算子
map,flatmap,filter会生成MapPartitionsRDD
coalesce,repartition会生成VoalescedRDD
常见的 RDD 有两种类型
转换型的 RDD, Transformation
动作型的 RDD, Action
常见的 Transformation 类型的 RDD
map
flatMap
filter
groupBy
reduceByKey
常见的 Action 类型的 RDD
collect
countByKey
reduce
Spark同样支持把数据集拉到集群范围的内存缓存中。这对于需要重复访问的数据非常有用,比如:查询一些小而”热“(频繁访问)的数据集 或者 运行一些迭代算法(如 PageRank)。
xxx.cache()即可