Spark 是用于大数据处理的集群计算框架,没有以 MapReduce 作为执行引擎,而是使用了自己的分布式运行环境在集群上执行工作。
Spark 与 Hadoop 紧密集成,他可以在 YARN 上运行,并支持 Hadoop 文件格式及其存储后端(如 HDFS 等)。
Spark 将作业与作业之间产生的大规模的工作数据集存储在内存中,性能比 MapReduce 可高出一个数量级。
从 Spark 处理模型中获益最大的是迭代算法(对数据集重复应用某个函数,直至满足退出条件)和交互式分析(用户向数据集发送到一系列专用的探索性查询)。
Spark 软件栈
- Spark Core,实现 Spark 的基本功能,包括任务调度、内存管理、错误恢复、与存储系统交互等模块。以及对弹性分布式数据集(RDD)的 API 定义。
- Spark SQL,用来操作结构化数据的程序包,可使用 SQL 或 HQL 查询数据,支持多种数据源,比如 Hive 表、Parquet 以及 JSON 等。
- Spark Streaming,对实时数据进行流式计算。
- MLib,机器学习功能库。
- GraphX,操作图的程序库,可进行并行图计算。
- 集群管理器,支持 Hadoop YARN,Apache Mesos,以及 Spark 自带的独立调度器。
Spark 核心
- 每个 Spark 应用都由一个驱动器程序(driver program)来发起集群上的各种并行操作。
- 驱动器程序通过一个 SparkContext 对象访问 Spark,此对象代表一个对计算集群的连接。
- 驱动器程序一般要管理多个执行器(executor)节点。
单词词频统计
object WordCount {
def main(args: Array[String]) {
val inputFile = args(0)
val outputFile = args(1)
val conf = new SparkConf().setAppName("wordCount")
val sc = new SparkContext(conf)
val input = sc.textFile(inputFile)
val words = input.flatMap(line => line.split(" "))
val counts = words.map(word => (word, 1)).reduceByKey{case (x, y) => x + y}
counts.saveAsTextFile(outputFile)
}
}
构建与运行
sbt clean package
spark-submit --class WordCount target/scala-2.11/scala_demo_2.11-0.1.jar file/1.txt file/wordcounts
RDD 编程
RDD 是一个不可变的分布式对象集合,每个 RDD 被分为多个分区,分区运行在集群中的不同节点上。
两种创建 RDD 的方式:
- 读取一个外部数据集,
textFile
等。 - 在驱动程序里分发驱动器程序中的对象集合,
parallelize
。
RDD 支持的两种操作:
- 转化操作:由一个 RDD 生成一个新的 RDD,如 filter。
- 行动操作:对 RDD 计算出一个结果,如 count。
- 区别:定义新的 RDD 时,Spark 会惰性计算,只有在第一次运行一个行动操作时,才会真正的计算。
RDD 默认不进行持久化,如果想在多个行动操作中重用同一个 RDD,避免多次计算同一个 RDD,可以使用 RDD.persist()
让 Spark 把这个 RDD 缓存下来。
常用的转化操作
函数名 | 目的 | 实例 |
---|---|---|
map() | 将函数应用于每一个元素 | rdd.map(x => x + 1) |
flatMap() | 将函数应用与每一个元素,每个返回值是一个迭代器 | rdd.flatMap(x => x.to(3)) |
filter() | 满足条件 | |
distinct() | 去重 | |
union() | 包含两个 RDD 中的所有元素 | rdd.union(other) |
intersection() | 并 | rdd.intersection(other) |
subtract() | 存在于第一个,不存在于第二个 | 同上 |
cartesian() | 笛卡尔积 | 同上 |
常用的行动操作
函数名 | 目的 |
---|---|
collect() | 返回 RDD 中的所有元素 |
count() | 元素个数 |
countByValue() | 各个元素出现的次数 |
take(num) | 返回num个元素 |
top(num) | 返回最前面的num个元素 |
takerdered(num)(ordering) | 从RDD中按照提供的顺序返回最前面的num个元素 |
reduce(func) | 并行整合RDD中所有元数据,如sum |
fold(zero)(func) | 和reduce一样,但是需要提供初始值 |
aggregate(zeroValue)(seqOp, combOp) | 和reduce相似,通常返回不同类型的函数 |
foreach(func) | 对RDD中各个元素使用给定函数,没有结果返回 |
键值对操作
包含键值对的 RDD 称为 Pair RDD,Pair RDD 提供 reduceByKey() 方法,可以分别归纳每个键对应的数据;join() 可以把两个 RDD 中键相同的元素组合到一起,合并为一个 RDD。
可以用 map 将普通 RDD 转化为 Pair RDD
val pairs = linex.map(x => (x.split(" ")(0), x))
Pair RDD 的转化操作
函数 | 目的 |
---|---|
reduceByKey(func) | 合并具有相同键的值 |
groupByKey() | 对具有相同键的值进行分组 |
combineByKey(createCombiner, mergeValue, mergeCombiners, partitioner) | 使用不同的返回类型合并具有相同键的值 |
mapValues(func) | 对 pair RDD 中的每个值应用一个函数而不改变键 |
flatMapValues(func) | 对 pair RDD 中的每个值应用一个返回迭代器的函数,然后对返回的每个元素都生成一个对应原键的键值对记录。通常用于符号化 |
keys() | 返回一个仅包含键的 RDD |
values() | 返回一个仅包含值的 RDD |
sortByKey() | 返回一个根据键排序的 RDD |
对两个 pair RDD 的转化操作
函数 | 目的 |
---|---|
subtractByKey | 删掉 RDD 中键与 other RDD 中的键相同的元素 |
join | 对两个 RDD 进行内连接 |
rightOuterJoin | 右外连接,确保 RDD 中的键必须存在 |
leftOuterJoin | 左外连接,确保 other 中的键必须存在 |
cogroup | 将两个 RDD 中具有相同键的数据分组到一起 |
combineByKey 详解
combineByKey是Spark中一个比较核心的高级函数,其他一些高阶键值对函数底层都是用它实现的。诸如 groupByKey,reduceByKey等等
定义:
def combineByKey[C](
createCombiner: V => C,
mergeValue: (C, V) => C,
mergeCombiners: (C, C) => C,
partitioner: Partitioner,
mapSideCombine: Boolean = true,
serializer: Serializer = null)
参数:
createCombiner: V => C
,在各个分区中遇到一个新的元素时用来创建那个键对应的累加器的初始值mergeValue: (C, V) => C
,对已经遇到的键,用其将键累加器对应的当前值与这个新值进行合并mergeCombiners: (C, C) => C
,合并不同分区的同一键的累加器
流程示意图
实例代码
val initialScores = Array(("Fred", 88.0), ("Fred", 95.0), ("Fred", 91.0), ("Wilma", 93.0), ("Wilma", 95.0), ("Wilma", 98.0))
val d1 = sc.parallelize(initialScores)
type MVType = (Int, Double) //定义一个元组类型(科目计数器,分数)
d1.combineByKey(
score => (1, score),
(c1: MVType, newScore) => (c1._1 + 1, c1._2 + newScore),
(c1: MVType, c2: MVType) => (c1._1 + c2._1, c1._2 + c2._2)
).map { case (name, (num, socre)) => (name, socre / num) }.collect
参数含义的解释
-
score => (1, score)
,我们把分数作为参数,并返回了附加的元组类型。 以"Fred"为列,当前其分数为88.0 =>(1,88.0) 1表示当前科目的计数器,此时只有一个科目 -
(c1: MVType, newScore) => (c1._1 + 1, c1._2 + newScore)
,注意这里的c1就是createCombiner
初始化得到的(1,88.0)。在一个分区内,我们又碰到了"Fred"的一个新的分数91.0。当然我们要把之前的科目分数和当前的分数加起来即c1._2 + newScore
,然后把科目计算器加1即c1._1 + 1
-
(c1: MVType, c2: MVType) => (c1._1 + c2._1, c1._2 + c2._2)
,注意"Fred"可能是个学霸,他选修的科目可能过多而分散在不同的分区中。所有的分区都进行mergeValue
后,接下来就是对分区间进行合并了,分区间科目数和科目数相加分数和分数相加就得到了总分和总科目数
执行结果如下:
res1: Array[(String, Double)] = Array((Wilma,95.33333333333333), (Fred,91.33333333333333))
数据分区
对数据集在节点间的分区进行控制
Spark 提供的分区方式:
- 哈希分区
- 范围分区
如不知道数据集如何分区,对两个数据集进行 join 操作时,会将两个数据集所有元素的哈希值都求出来,将哈希值相同的记录通过网络传到同一台机器上,然后在这台机器上进行相同键记录的谅解操作。
当知道一方的分区方式后,可以将另一方对应的键的数据传输到知道分区的一方的键对应的分区,可节省很多网络通信时间。
对使用的RDD要进行持久化操作。
从分区中获益的操作
需要引入将数据根据键跨节点进行混洗的都会获益。
如:cogroup()、groupWith()、join()、leftOuterJoin()、rightOuterJoin()、groupByKey()、reduceByKey()、combineByKey()、loopup()
影响分区方式的操作
会设置分区的操作:cogroup(), groupWith(), join(), leftOuterJoin(), rightOuterJoin(), groupByKey(), reduceByKey(), combineByKey(), partitionBy(), sort(), mapValues()(如果父 RDD 有分区方式), flatMapValues()(如果父 RDD 有分区方式)
数据读取与保存
Spark 支持多种输入输出源,一部分原因是 Spark 基于 Hadoop 生态圈构建,可以通过 Hadoop MapReduce 所使用的 InputFormat 和 OutputFormat 接口访问数据,大部分常见的文件格式和存储系统都支持这个接口。
支持的文件格式: 文本文件、JSON、CSV、SequenceFiles、Protocol buffers、对象文件
支持的文件系统:本地/“常规”文件系统、Amazon S3、HDFS