spark
Spark是什么?
Apache Spark 是一个快速的, 多用途的集群计算系统, 相对于 Hadoop MapReduce 将中间结果保存在磁盘中, Spark 使用了内存保存中间结果, 能在数据尚未写入硬盘时在内存中进行运算.
Spark的特点(优点)
速度快
- Spark 的在内存时的运行速度是 Hadoop MapReduce 的100倍
- 基于硬盘的运算速度大概是 Hadoop MapReduce 的10倍
- Spark 实现了一种叫做 RDDs 的 DAG 执行引擎, 其数据缓存在内存中可以进行迭代处理
易用
df = spark.read.json("logs.json")
df.where("age > 21") \
.select("name.first") \
.show()
- Spark 支持 Java, Scala, Python, R, SQL 等多种语言的API.
- Spark 支持超过80个高级运算符使得用户非常轻易的构建并行计算程序
- Spark 可以使用基于 Scala, Python, R, SQL的 Shell 交互式查询.
通用
- Spark 提供一个完整的技术栈, 包括 SQL执行, Dataset命令式API, 机器学习库MLlib, 图计算框架GraphX, 流计算SparkStreaming
- 用户可以在同一个应用中同时使用这些工具, 这一点是划时代的
兼容
- Spark 可以运行在 Hadoop Yarn, Apache Mesos, Kubernets, Spark Standalone等集群中
- Spark 可以访问 HBase, HDFS, Hive, Cassandra 在内的多种数据库
Spark组件
Spark 最核心的功能是 RDDs, RDDs 存在于 spark-core
这个包内, 这个包也是 Spark 最核心的包.
同时 Spark 在 spark-core
的上层提供了很多工具, 以便于适应不用类型的计算.
Spark-Core 和 弹性分布式数据集(RDDs)
Spark-Core 是整个 Spark 的基础, 提供了分布式任务调度和基本的 I/O 功能Spark 的基础的程序抽象是弹性分布式数据集(RDDs), 是一个可以并行操作, 有容错的数据集合RDDs 可以通过引用外部存储系统的数据集创建(如HDFS, HBase), 或者通过现有的 RDDs 转换得到RDDs 抽象提供了 Java, Scala, Python 等语言的APIRDDs 简化了编程复杂性, 操作 RDDs 类似通过 Scala 或者 Java8 的 Streaming 操作本地数据集合
Spark SQL
Spark SQL 在
spark-core
基础之上带出了一个名为 DataSet 和 DataFrame 的数据抽象化的概念Spark SQL 提供了在 Dataset 和 DataFrame 之上执行 SQL 的能力Spark SQL 提供了 DSL, 可以通过 Scala, Java, Python 等语言操作 DataSet 和 DataFrame它还支持使用 JDBC/ODBC 服务器操作 SQL 语言Spark Streaming
Spark Streaming 充分利用
spark-core
的快速调度能力来运行流分析它截取小批量的数据并可以对之运行 RDD Transformation它提供了在同一个程序中同时使用流分析和批量分析的能力MLlib
MLlib 是 Spark 上分布式机器学习的框架. Spark分布式内存的架构 比 Hadoop磁盘式 的 Apache Mahout 快上 10 倍, 扩展性也非常优良MLlib 可以使用许多常见的机器学习和统计算法, 简化大规模机器学习汇总统计, 相关性, 分层抽样, 假设检定, 随即数据生成支持向量机, 回归, 线性回归, 逻辑回归, 决策树, 朴素贝叶斯协同过滤, ALSK-meansSVD奇异值分解, PCA主成分分析TF-IDF, Word2Vec, StandardScalerSGD随机梯度下降, L-BFGS
GraphX
GraphX 是分布式图计算框架, 提供了一组可以表达图计算的 API, GraphX 还对这种抽象化提供了优化运行
Spark 集群结构
Spark 自身是没有集群管理工具的, 但是如果想要管理数以千计台机器的集群, 没有一个集群管理工具还不太现实, 所以 Spark 可以借助外部的集群工具来进行管理
整个流程就是使用 Spark 的 Client 提交任务, 找到集群管理工具申请资源, 后将计算任务分发到集群中运行
Driver
该进程调用 Spark 程序的 main 方法, 并且启动 SparkContext
Cluster Manager
该进程负责和外部集群工具打交道, 申请或释放集群资源
Worker
该进程是一个守护进程, 负责启动和管理 Executor
Executor
该进程是一个JVM虚拟机, 负责运行 Spark Task
运行一个 Spark 程序大致经历如下几个步骤
- 启动 Drive, 创建 SparkContext
- Client 提交程序给 Drive, Drive 向 Cluster Manager 申请集群资源
- 资源申请完毕, 在 Worker 中启动 Executor
- Driver 将程序转化为 Tasks, 分发给 Executor 执行
Driver 和 Worker 什么时候被启动?
- Standalone 集群中, 分为两个角色: Master 和 Slave, 而 Slave 就是 Worker, 所以在 Standalone 集群中, 启动之初就会创建固定数量的 Worker
- Driver 的启动分为两种模式: Client 和 Cluster. 在 Client 模式下, Driver 运行在 Client 端, 在 Client 启动的时候被启动. 在 Cluster 模式下, Driver 运行在某个 Worker 中, 随着应用的提交而启动
- 在 Yarn 集群模式下, 也依然分为 Client 模式和 Cluster 模式, 较新的版本中已经逐渐在废弃 Client 模式了, 所以上图所示为 Cluster 模式
- 如果要在 Yarn 中运行 Spark 程序, 首先会和 RM 交互, 开启 ApplicationMaster, 其中运行了 Driver, Driver创建基础环境后, 会由 RM 提供对应的容器, 运行 Executor, Executor会反向向 Driver 反向注册自己, 并申请 Tasks 执行
- 在后续的 Spark 任务调度部分, 会更详细介绍
总结
Master
负责总控, 调度, 管理和协调 Worker, 保留资源状况等Slave
对应 Worker 节点, 用于启动 Executor 执行 Tasks, 定期向 Master汇报Driver
运行在 Client 或者 Slave(Worker) 中, 默认运行在 Slave(Worker) 中
地址 | 解释 |
---|---|
local[N] |
使用 N 条 Worker 线程在本地运行 |
spark://host:port |
在 Spark standalone 中运行, 指定 Spark 集群的 Master 地址, 端口默认为 7077 |
mesos://host:port |
在 Apache Mesos 中运行, 指定 Mesos 的地址 |
yarn |
在 Yarn 中运行, Yarn 的地址由环境变量 HADOOP_CONF_DIR 来指定 |
spark-submit 命令
spark-submit [options] <app jar> <app options>
app jar
程序 Jar 包app options
程序 Main 方法传入的参数options
提交应用的参数, 可以有如下选项
同 Spark shell 的 Master, 可以是spark, yarn, mesos, kubernetes等 URL --deploy-mode
Driver 运行位置, 可选 Client 和 Cluster, 分别对应运行在本地和集群(Worker)中 --class
Jar 中的 Class, 程序入口 --jars
依赖 Jar 包的位置 --driver-memory
Driver 程序运行所需要的内存, 默认 512M --executor-memory
Executor 的内存大小, 默认 1G " style="margin: 20px 0px 10px; padding: 0px; cursor: text; position: relative; font-family: Helvetica, "Hiragino Sans GB", 微软雅黑, "Microsoft YaHei UI", SimSun, SimHei, arial, sans-serif;">
参数
解释
--master <url>
同 Spark shell 的 Master, 可以是spark, yarn, mesos, kubernetes等 URL
--deploy-mode <client or cluster>
Driver 运行位置, 可选 Client 和 Cluster, 分别对应运行在本地和集群(Worker)中
--class <class full name>
Jar 中的 Class, 程序入口
--jars <dependencies path>
依赖 Jar 包的位置
--driver-memory <memory size>
Driver 程序运行所需要的内存, 默认 512M
--executor-memory <memory size>
Executor 的内存大小, 默认 1G
参数 | 解释 |
---|---|
--master <url> |
同 Spark shell 的 Master, 可以是spark, yarn, mesos, kubernetes等 URL |
--deploy-mode <client or cluster> |
Driver 运行位置, 可选 Client 和 Cluster, 分别对应运行在本地和集群(Worker)中 |
--class <class full name> |
Jar 中的 Class, 程序入口 |
--jars <dependencies path> |
依赖 Jar 包的位置 |
--driver-memory <memory size> |
Driver 程序运行所需要的内存, 默认 512M |
--executor-memory <memory size> |
Executor 的内存大小, 默认 1G |
弹性分布式数据集(RDDs)
分布式
RDD 支持分区, 可以运行在集群中
弹性
- RDD 支持高效的容错
- RDD 中的数据即可以缓存在内存中, 也可以缓存在磁盘中, 也可以缓存在外部存储中
- Task如果失败会自动进行特定次数的重试
RDD的计算任务如果运行失败,会自动进行任务的重新计算,默认次数是4次 - Stage如果失败会自动进行特定次数的重试
如果Job的某个Stage阶段计算失败,框架也会自动进行任务的重新计算,默认次数也是4次。 - Checkpoint和Persist可主动或被动触发
RDD可以通过Persist持久化将RDD缓存到内存或者磁盘,当再次用到该RDD时直接读取就行。也可以将RDD进行检查点,检查点会将数据存储在HDFS中,该RDD的所有父RDD依赖都会被移除。 - 数据分片[partition]的高度弹性
可以根据业务的特征,动态调整数据分片的个数,提升整体的应用执行效率。
数据集
- RDD 可以不保存具体数据, 只保留创建自己的必备信息, 例如依赖和计算函数
RDD 也可以缓存起来, 相当于存储具体数据
RDD 是混合型的编程模型, 可以支持迭代计算, 关系查询, MapReduce, 流计算
RDD 是只读的
RDD 是只读的, 不允许任何形式的修改. 虽说不能因为 RDD 和 HDFS 是只读的, 就认为分布式存储系统必须设计为只读的. 但是设计为只读的, 会显著降低问题的复杂度, 因为 RDD 需要可以容错, 可以惰性求值, 可以移动计算, 所以很难支持修改.
- RDD2 中可能没有数据, 只是保留了依赖关系和计算函数, 那修改啥?
- 如果因为支持修改, 而必须保存数据的话, 怎么容错?
- 如果允许修改, 如何定位要修改的那一行? RDD 的转换是粗粒度的, 也就是说, RDD 并不感知具体每一行在哪.
RDD 可以包含多个分区.RDD 作为数据结构, 本质上是一个只读的分区记录集合. 一个 RDD 可以包含多个分区, 每个分区就是一个 DataSet 片段.
RDD 之间可以相互依赖, 如果 RDD 的每个分区最多只能被一个子 RDD 的一个分区使用,则称之为窄依赖, 若被多个子 RDD 的分区依赖,则称之为宽依赖. 不同的操作依据其特性, 可能会产生不同的依赖. 例如 map 操作会产生窄依赖, 而 join 操作则产生宽依赖.
RDD 是可以容错的
- RDD 的容错有两种方式
保存 RDD 之间的依赖关系, 以及计算函数, 出现错误重新计算直接将 RDD 的数据存放在外部存储系统, 出现错误直接读取, Checkpoint
问题1:在集群中运行的前提?
- 必须可以分解为多个并发计算的部分
- 每部分可以在不同处理器上执行
- 需要一个共享内存的机制
- 需要一个总体上的协作机制来调度
问题2:如果放在集群中的话, 可能要对整个计算任务进行分解, 如何分解?
概述
对于 HDFS 中的文件, 是分为不同的 Block 的在进行计算的时候, 就可以按照 Block 来划分, 每一个 Block 对应一个不同的计算单元
扩展
RDD
并没有真实的存放数据, 数据是从 HDFS 中读取的, 在计算的过程中读取即可RDD
至少是需要可以 分片 的, 因为HDFS中的文件就是分片的,RDD
分片的意义在于表示对源数据集每个分片的计算,RDD
可以分片也意味着 可以并行计算
问题3:移动数据不如移动计算是一个基础的优化, 如何做到?
每一个计算单元需要记录其存储单元的位置, 尽量调度过去
问题4:在集群中运行, 需要很多节点之间配合, 出错的概率也更高, 出错了怎么办?
- 备份机制
- 重新计算(前提,要记录依赖关系)
问题5:假如任务特别复杂, 流程特别长, 有很多 RDD 之间有依赖关系, 如何优化?
记录数据集的状态
- 缓存
- Checkpoint
总结: RDD 的五大属性
Partition List
分片列表, 记录 RDD 的分片, 可以在创建 RDD 的时候指定分区数目, 也可以通过算子来生成新的 RDD 从而改变分区数目Compute Function
为了实现容错, 需要记录 RDD 之间转换所执行的计算函数RDD Dependencies
RDD 之间的依赖关系, 要在 RDD 中记录其上级 RDD 是谁, 从而实现容错和计算Partitioner
为了执行 Shuffled 操作, 必须要有一个函数用来计算数据应该发往哪个分区Preferred Location
优先位置, 为了实现数据本地性操作, 从而移动计算而不是移动存储, 需要记录每个 RDD 分区最好应该放置在什么位置
API代码:
需求
- 给定一个网站的访问记录, 俗称 Access log
- 计算其中出现的独立 IP, 以及其访问的次数
@Test
def tets1(): Unit = {
//添加本地文件
val tuples: Array[(String, Int)] = sc.textFile("dataset/access_log_sample.txt")
//切割\取第一个\计数1
.map(x => (x.split(" ")(0), 1))
//判断是否为空(以后常用)
.filter(li => StringUtils.isNotBlank(li._1))
//统计
.reduceByKey((c, a) => c + a)
//排序,第二个参数决定是否降序
.sortBy(i => i._2, false)
//取TOP值
.take(10)
//遍历打印
tuples.foreach(println(_))
mapPartitions(List[T] ⇒ List[U])
RDD[T] ⇒ RDD[U] 和 map 类似, 但是针对整个分区的数据转换
/**
* mapPartitions 和 map 算子是一样的, 只不过 map 是针对每一条数据进行转换, mapPartitions 针对一整个分区的数据进行转换
* 所以:
* 1. map 的 func 参数是单条数据, mapPartitions 的 func 参数是一个集合(一个分区整个所有的数据)
* 2. map 的 func 返回值也是单条数据, mapPartitions 的 func 返回值是一个集合
*/
@Test
def mapPartitions(): Unit = {
// 1. 数据生成
// 2. 算子使用
// 3. 获取结果
sc.parallelize(Seq(1, 2, 3, 4, 5, 6), 2)
.mapPartitions(iter => {
iter.foreach(item => println(item))
iter
})
.collect()
}
//5
//3
//1
//4
//6
//2
mapPartitionsWithIndex
和 mapPartitions 类似, 只是在函数中增加了分区的 Index
/**
* mapPartitionsWithIndex 和 mapPartitions 的区别是 func 中多了一个参数, 是分区号
*/
@Test
def mapPartitionsWithIndex(): Unit = {
sc.parallelize(Seq(1, 2, 3, 4, 5, 6), 2)
.mapPartitionsWithIndex( (index, iter) => {
println("index: " + index)
iter.foreach(item => println(item))
iter
} )
.collect()
}
//index:2
//index:0
//index:1
//1
//5
//2
//3
//4
//6
reduceByKey((V, V) ⇒ V, numPartition)
@Test
def reduceByKey(): Unit ={
sc.parallelize(Seq(("a",2),("b",3),("a",1)))
.reduceByKey(_ + _).collect().foreach(println(_))
}
//(a,3)
//(b,3)
注意点
- ReduceByKey 只能作用于 Key-Value 型数据, Key-Value 型数据在当前语境中特指 Tuple2
- ReduceByKey 是一个需要 Shuffled 的操作
- 和其它的 Shuffled 相比, ReduceByKey是高效的, 因为类似 MapReduce 的, 在 Map 端有一个 Cominer, 这样 I/O 的数据便会减少
groupByKey()
@Test
def groupByKey(): Unit ={
sc.parallelize(Seq(("aa",2),("bb",3),("aa",2)))
.groupByKey().collect().foreach(println(_))
}
//(bb,CompactBuffer(3))
//(aa,CompactBuffer(2, 2))
作用
GroupByKey 算子的主要作用是按照 Key 分组, 和 ReduceByKey 有点类似, 但是 GroupByKey 并不求聚合, 只是列举 Key 对应的所有 Value
注意点
GroupByKey 是一个 ShuffledGroupByKey 和 ReduceByKey 不同, 因为需要列举 Key 对应的所有数据, 所以无法在 Map 端做 Combine, 所以 GroupByKey 的性能并没有 ReduceByKey 好
combineByKey()
@Test
def conbineByKey(): Unit ={
val rdd = sc.parallelize(Seq(
("zhangsan", 99.0),("zhangsan", 96.0),
("lisi", 97.0),("lisi", 98.0),
("zhangsan", 97.0),("wangwu",76.0),
("lisi",56.0),("zhangsan",60.0))
)
val unit: RDD[(String, (Double, Int))] = rdd.combineByKey(
//参数一:针对每一个分区中每一个key的第一个值
createCombiner = (curr: Double) => (curr, 1),
//(value,1),
//参数二:针对每个分区中合并相同的key的数据
mergeValue = (curr: (Double, Int), nextValue: Double) => (curr._1 + nextValue, curr._2 + 1),
//(x:(Int,Int),y:(Int,Int))=>(x._1+y,x._2+1),
//参数三:针对所有分区中第二个函数的结果进行合并
mergeCombiners = (curr: (Double, Int), agg: (Double, Int)) => (curr._1 + agg._1, curr._2 + agg._2)
//(x:(Int,Int),y:(Int,Int))=>(x._1+y._1,x._2+y._2).map(x=>(x._1,x._2._1/x._2._2)).foreach(println(_))
)
val unit1: RDD[(String, Double)] = unit.map(x=>(x._1,x._2._1/x._2._2))
unit1.collect().foreach(println(_))
}
作用
对数据集按照 Key 进行聚合
调用
combineByKey(createCombiner, mergeValue, mergeCombiners, [partitioner], [mapSideCombiner], [serializer])
参数
createCombiner
将 Value 进行初步转换mergeValue
在每个分区把上一步转换的结果聚合mergeCombiners
在所有分区上把每个分区的聚合结果聚合partitioner
可选, 分区函数mapSideCombiner
可选, 是否在 Map 端 Combineserializer
序列化器注意点
combineByKey
的要点就是三个函数的意义要理解groupByKey
,reduceByKey
的底层都是combineByKey
aggregateByKey()
@Test
def aggregateByKey(): Unit ={
val rdd = sc.parallelize(Seq(("手机", 10.0), ("手机", 15.0), ("电脑", 20.0)))
//rdd.aggregateByKey(zeroValue)(seqOp, combOp)
//zeroValue:指定初始值
//seqOp:作用与每一个元素,根据初始值,进行计算
//combOp:将seqOp处理过的结果进行聚合
rdd.aggregateByKey(0.8)((zeroValue,item)=> item *zeroValue,(curr,agg)=>curr + agg)
.collect().foreach(println(_))
}
//(电脑,16.0)
//(手机,20.0)
作用
聚合所有 Key 相同的 Value, 换句话说, 按照 Key 聚合 Value
调用
rdd.aggregateByKey(zeroValue)(seqOp, combOp)
参数
zeroValue
初始值seqOp
转换每一个值的函数comboOp
将转换过的值聚合的函数
注意点 * 为什么需要两个函数? aggregateByKey 运行将一个RDD[(K, V)]
聚合为RDD[(K, U)]
, 如果要做到这件事的话, 就需要先对数据做一次转换, 将每条数据从V
转为U
, seqOp
就是干这件事的 ** 当seqOp
的事情结束以后, comboOp
把其结果聚合
- 和 reduceByKey 的区别::
- aggregateByKey 最终聚合结果的类型和传入的初始值类型保持一致
- reduceByKey 在集合中选取第一个值作为初始值, 并且聚合过的数据类型不能改变
foldByKey(zeroValue)((V, V) ⇒ V)
/**
* foldByKey 和 Spark 中的 reduceByKey 的区别是可以指定初始值
* foldByKey 和 Scala 中的 foldLeft 或者 foldRight 区别是, 这个初始值作用于每一个数据
*/
@Test
def foldByKey(): Unit ={
sc.parallelize(Seq(("a", 1), ("a", 1), ("b", 1)))
.foldByKey(zeroValue = 10)((curr,agg)=>curr+agg)
.collect().foreach(println(_))
}
//(a,22)
//(b,11)
作用
和 ReduceByKey 是一样的, 都是按照 Key 做分组去求聚合, 但是 FoldByKey 的不同点在于可以指定初始值
调用
foldByKey(zeroValue)(func)
参数
zeroValue
初始值func
seqOp 和 combOp 相同, 都是这个参数注意点
FoldByKey 是 AggregateByKey 的简化版本, seqOp 和 combOp 是同一个函数FoldByKey 指定的初始值作用于每一个 Value
join(other, numPartitions)
@Test
def join(): Unit ={
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().foreach(println(_))
}
//(a,(1,10))
//(a,(1,11))
//(a,(1,12))
//(a,(2,10))
//(a,(2,11))
//(a,(2,12))
作用
将两个 RDD 按照相同的 Key 进行连接
调用
join(other, [partitioner or numPartitions])
参数
other
其它 RDDpartitioner or numPartitions
可选, 可以通过传递分区函数或者分区数量来改变分区注意点
Join 有点类似于 SQL 中的内连接, 只会再结果中包含能够连接到的 KeyJoin 的结果是一个笛卡尔积形式, 例如
"a", 1), ("a", 2
和"a", 10), ("a", 11
的 Join 结果集是"a", 1, 10), ("a", 1, 11), ("a", 2, 10), ("a", 2, 11
sortBy(ascending, numPartitions)
@Test
def sortBy(): Unit ={
val rdd1 = sc.parallelize(Seq(("a", 3), ("b", 2), ("c", 1)))
rdd1.sortBy(_._1).collect().foreach(println(_))
println("=============================")
rdd1.sortBy(_._2).collect().foreach(println(_))
println("=============================")
rdd1.sortBy(_._1,false).collect().foreach(println(_))
}
//(a,3)
//(b,2)
//(c,1)
//=============================
//(c,1)
//(b,2)
//(a,3)
//=============================
//(c,1)
//(b,2)
//(a,3)
作用
- 排序相关相关的算子有两个, 一个是
sortBy
, 另外一个是sortByKey
调用
sortBy(func, ascending, numPartitions)
参数
func
通过这个函数返回要排序的字段ascending
是否升序numPartitions
分区数
注意点
- 普通的 RDD 没有
sortByKey
, 只有 Key-Value 的 RDD 才有 sortBy
可以指定按照哪个字段来排序,sortByKey
直接按照 Key 来排序
partitionBy(partitioner)
使用用传入的 partitioner 重新分区, 如果和当前分区函数相同, 则忽略操作
repartition(numPartitions)
重新分区
/**
* repartition 进行重分区的时候, 默认是 Shuffle 的
* coalesce 进行重分区的时候, 默认是不 Shuffle 的, coalesce 默认不能增大分区数
*/
@Test
def partitioning(): Unit = {
val rdd = sc.parallelize(Seq(1, 2, 3, 4, 5), 2)
// repartition
// println(rdd.repartition(5).partitions.size)
// println(rdd.repartition(1).partitions.size)
// coalesce
println(rdd.coalesce(5, shuffle = true).partitions.size)
}