Spark 基本概念
MapReduce 存在的缺陷
- 编写难度大
- 不能很好充分利用系统内存
- 一个作业多个MR任务嵌套不友好(每一个task都是jvm进程级别创建销毁开销都很大、每一次都要涉及磁盘或dfs或db和网络 的IO)(期望以pipeline 流水线的方式从头到尾)
- 只能离线处理
数据处理
读数据(read)–> 规整(ETL)–> 写(write)
将业务系统的数据经过抽取(Extract)、清洗转换(Transform)之后加载(Load)到数据仓库的过程.
Spark、Flink 都是批流一体(一站式解决 one stack to rule them all);架构越简单就越少一分风险。
- Batch: MR、Hive、Pig(几乎没用了)、RDD、DataFrame、DataSet
- Stream:Storm、SS(spark streaming)、SSS(结构化流)、Flink
- SQL :Impala、Hive、Spark SQL
- ML:Mathout 、MLlib
- Real time: Hbase、Cassandra
优先选择活跃度高的框架,看社区频率以及最后一次更新时间。
Spark官网: spark.apache.org
Lightning-fast unified analytics engine (超快的统一分析引擎)
- speed 快 :基于内存;基于多线程(区别与MRtask进程级别);pipeline 流水线的方式(DAG图)
- easy of use: 支持多语言 Java、scala、python、R、sql
- Generality :具有共性的(批流一体),SQL、ss
- runs everywhere: 能运行在hadoop(能读hdfs数据、能跑在yarn)、Mesos、K8S、standalone(spark集群)、获取其他云端。能对接各种数据源
(马铁)Martei 的博士论文最后孵化出了Spark。
Spark与Hadoop、MapReduce
spark 只是个计算引擎,不需要存储数据。只负责将去取出来的数据分析计算。
spark 基于 Hadoop(hdfs 存储、spark计算、yarn管理资源)。
Hadoop 与 Spark
作用:Distributed Storage + Compute Compute
计算过程中的存储:Disk / HDFS Disk / Memory
时间开销: 大 小
MapReduce 与 Spark
Spark即使全部走Disk也比MR快。
Spark 并不能替换 Hadoop,就不是一个概念范围;只能勉强可以说Spark能替换MapReduce。
RDD
Resilient Distributed Dataset --> 弹性分布式数据集
弹性:故障无感知,可以转移到其他机器
分布式:可以运行在任意节点上
不可变: rdd1 通过算子操作(如map) 得到的是 新的rdd2
可以被分区:集合的数据可以分区,每个分区一个task,也就是分区间多线程并行运行的
(有依赖关系的数据会被分在同一个分区)
序列化:spark也会涉及到 磁盘或网络IO,传输的对象一定要 extends Serializable
注解@triansient 加在属性上,表示这个属性不会序列化(不会将真实的值序列化到文件里,也读取不到)
分区
RDD 的容错是以分区为单位(故障隔离),某一分区出现故障,可以在当前分区找到上一次依赖重新计算即可
在RDD中: n个 partition == n个 task == n个 线程 == 输出文件的个数 n个
在MR中:inputspilt == mapTask == JVM 进程数
InitializingSpark
在linux终端使用
- 如果选择Pre-built 预编译版本,需要和已安装的Hadoop和scala版本匹配的spark包(从压缩包名字可以看出)
- 选择spark-3.2.0-bin-hadoop3.2 下载解压、环境变量;
bin 文件夹下有 spark-shell、spark-submit、spark-sql等命令
conf 中有配置文件的模板template
sbin 是与启动服务相关的(集群模式的,但生产一般都是使用yarn或k8s服务,不用standalone) - bin/spark-shell --master local[2] 进入交互式命令行(把IDEA写好的代码可以直接拿过来运行)
local[2] 以2个线程启动
[liqiang@Gargantua ~]$ spark-shell --master local[2]
Spark context available as 'sc' (master = local[2], app id = local-1642090369041).
Spark session available as 'spark'.
scala>
scala> val rdd = sc.parallelize(List(1, 2, 3, 4, 5))
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at <console>:23
scala> rdd.count()
res0: Long = 5
spark任务web 端访问,默认端口4040(如果再启一个就是4041):http:gargantua:4040
scala> sc.stop
# or
scala> [liqiang@Gargantua ~]$ 【Ctrl C 停止】
scal>:psate 然后粘贴整个scala代码去执行。 psate:拼接多行文件
在IDEA中使用
在idea 中也可以本地启动依赖里的spark-shell直接开发&运行,不需要连linux上spark-shell
引入依赖
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.12</artifactId>
<version>3.2.0</version>
</dependency>
创建 SparkContext
1. val sparkConf = new SparkConf().setAppName("Your App Name").setMaster("local[2]")
2. val sc = new SparkContext(sparkConf)
3. // sc 可以创建RDD: sc.makeRDD() 或 sc.parallelize()
4. val rdd = sc.parallelize(List(1, 2, 3, 4, 5))
5. val rdd = sc.makeRDD(List(1, 2, 3, 4, 5))
local[2] 代表指定并行度,也就是2个线程 == 2个分区 == 2个任务
sc.parallelize()中也可以重新指定并行度
sc.textFile() 中也可以指定分区度(好像范围不一样)
RDD 两大操作:transformations 和 actions
不可变:针对一个已有的RDD通过转换得到的是另一个RDD
lazy:不会立刻触发spark(不会提交作业),只有等到action触发
rdd.map…filter… 每一个transformations 都会记录,而不会每次都计算
transformations
所有的transformation算子都是lazy的,需要触发action时才能真正执行。
- map 作用在每个元素
- mapPartition 作用在每个分区,迭代器是在分区上
- filter
- flatmap = flatten + map
这几个算子底层都用 MapPatitionRDD来实现
使用MapPatitionRDD 来实现map:
(MapPatitionRDD 需要放在指定的包org.ahache.spark.rdd下,可以自己建一个同名的包。。。)
MapPatitionRDD中就是对迭代器使用scala的map等算子来实现。
同理使用scala的filter就实现spark的filter
- mapValues()
- flatMapValues()
- keys 就是获取所有的key
- values 获取所有value ==> map(_._2).collect
- keyby 把不是k,v 的元素(只有v),通过指定函数作为key得到k,v
reduceByKey 作用在k,v类型,将相同key的value两两计算
使用reduceByKey做wc:
rdd.flatMap(_.split(",")).map(_,1).reduceByKey(_+_)
- groupByKey 作用在k,v类型,将k相同的v合并到一个集合中,作为新的k,v
使用groupByKey 做wc:
rdd.flatMap(_.split(",")).map(_,1).groupByKey().map(_1,_2.sum)
rdd.flatMap(_.split(",")).map(_,1).groupByKey().mapValues(_.size)
- groupBy 不需作用k,v,通过指定条件分组,作为key,满足条件的原内容组成集合作为v
比如按奇偶数分组
使用groupBy 做wc:
rdd.flatMap(_.split(",")).groupBy(x => x).mapValues(_.size)
- sortBy 指定排序条件即可,默认升序。降序可以条件前加-号
- distanct 底层还是用的 reduceByKey
- union 就是简单的合起来,分区数等于union前分区数之和。
union没有经过shuffle,可以通过web页面看DAG图,判断是否经过shuffle (只有一个红框(stage)就是没有shuffle) - sample() 取样,随机抽取一个数,参数true控制取的数要不要放回去
zip() 拉链。(分区要一致,元素个数也要一致才能拉) - cogroup()
left.cogroup(right) 要求左右key的数据类型是相同,value类型可以不同
数据类型:
left:(String,Int)
right:(String,String)
cogroup: (String,(Iterable[Int],Iterable[String]))
// 把左右相同的key的value组成新的key,value
// 左边相同key的多个value组成第一个Iterable,右边相同key的多个value组成第二个Iterable;
// 左和右不匹配的用空CompactBuffer填充
- join
left.join(right) 内连接的效果
数据类型:
left:(String,String)
right:(String,String)
cogroup: (String,(String,String)
join 底层靠 cogroup:(将左右两个迭代器for打开组合)
left.cogroup(right).flatMap{
case (key,(l,r)) => {
val iter = for(v1 <- l; v2 <- r) yield (v1,v2)
iter.map((key, _))
}
}.collect
或
left.cogroup(right).flatMapValues{
case ((l,r)) => {
for(v1 <- l; v2 <- r) yield (v1,v2)
}
}.collect
或
left.cogroup(right).flatMapValues(x =>
for(v1 <- x._1.iterator; v2 <- x._2
iterator;) yield (v1,v2)
}).collect
left.leftOuterJoin(right) 左连接,用cogroup实现的话,就是当右边为空时放一个None值补位
- 交、补集
a.intersection(b) 取a,b的交集
a.subtract(b) 取在a单不在b的
actions
每个actions算子的底层都由 sc.runJob 触发作业。
collect
return 所有的 value 到窗口,适用结果集较小时,否则可能OOM。
(对相同大小的数据,mapPartition 比 map 发生OOM的可能性应该更大,分区超多的时候)
foreach
遍历输出到终端。但不一定是按顺序输出的,分区间并行,不确定哪个分区先结束。
take
take(2) 返回最先的2个元素(按原顺序,不作重新排序)
take(n) 如果需要被取出的数是在不同的分区,就会有触发多少个分区数(可能触发多个action)
.first ==> take(1)
takeOrdered(2) 返回最小的2个元素(排序后)
takeOrdered(2)(Ordering.by(x => x)) 柯里化指定排序条件
top(2) 返回最大的2个元素(排序后)底层就是takeOrdered(2) (orderf.reverse)
fold(0)(+) 相比于reduce多一个初始值
count、max、min、sum 、lookup 、countByKey
max 可以使用 reduce(if(x>y)x else y) 实现
lookup 找出指定key的元素
countByKey 也可以做wc:
rdd.map(_,1).countByKey().foreach
countByKey 的底层是 mapValues(x => 1).reduceByKey(_+_)
// : _* 可以将数组/集合 打散
mothod(1,2,3,4,5)
==>
mothod(1 to 5:_*)
mothod(Array(1,2,3,4,5):_*)
coalesce
一般 filter 就会配合coalesce,最常见的作用就是合并小文件。
如 coalesce(1) 合并分区
默认不会有shuffle(数据不需要重新分发),第二个参数是true是可以有shuffle;默认减少分区数,即只能往小于原来分区数合并,但是第二个参数是true时可以增大
repartition 会重新shuffle,底层就是调用coalesce(num,true) ,所以repartition 就算是减小分区数也会shuffle
RDD写入到 MySql
在 map 中,获取连接,执行sql,再关闭连接。开销很大
def map2MySql(rdd:RDD[String]) = {
rdd.map(
x => {
val conn:Connection = JDBCDriverUtil.getConnection()
val name = x
val sql = s"insert into student(name) values ('$name')"
val statement = conn.prepareStatement(sql)
statement.executeUpdate()
JDBCDriverUtil.close(conn)
}
)
}
可以在 mapPartition 中获取连接,在每个分区中map 执行sql
def mapPartitions2MySql(rdd:RDD[String]): RDD[String] = {
rdd.mapPartitions(
p => {
val conn:Connection = JDBCDriverUtil.getConnection
conn.setAutoCommit(false)
val sql = s"insert into student(name) values (?)"
val statement = conn.prepareStatement(sql)
p.foreach(x => {
statement.setString(1,x)
statement.executeUpdate()
})
conn.commit()
JDBCDriverUtil.close(conn)
p
}
)
}
注意 以上rdd 或还要触发action 才能真正执行。当然也可以直接使用foreachPartition
RDD持久化
RDD 持久化到内存/磁盘
由一个textFile读取到的数据放到一个RDD,后面由这个RDD经过多次action,每次action都是从头开始执行,即每个action都会再去执行textFile(说明从代码的角度将一个textFile的RDD定义为局部变量以便反复使用的“优化”在执行效率上是没有作用的)。如果需要反复对一个RDD使用,可以将这个RDD持久化到缓存。
把rdd持久化到个节点的内存中;当对RDD执行持久化操作时,每个节点都会将自己操作的RDD持久化到内存中,并且在之后对该RDD的反复使用中。
一般会加载大量的数据到RDD中,持久化的内存占用是比较高的。
persist()、cache()
可以把数据持久化到磁盘,也是lazy的,只会在下一次触发action时真正执行(说明接下来的一次还是不会从memory读取数据,要下下次memory中才有被cache的数据)。
cache()的数据可以在4040页面的storage种看到。
cache()后可以方便后续使用,cache() 也是调用的 persist(),cache是使用memory_only策略的persist()。
persist() 参数可选(有5个参数,组合成好多种策略等级… )
持久化策略等级
StorageLevel主构造器5个参数:
_useDisk:Boolean 是否使用磁盘
_useMemory:Boolean 是否使用内存
_useOffHeap:Boolean 是否使用OffHeah
_deserialized:Boolean 是否无需序列化
_repartition:Int=1 重新分区数
策略如何选择
默认情况下,性能最高的当然是MEMORY_ONLY(也就是cache),但前提是内存足够大,可以存放下整个RDD的所有数据。性能高是因为不进行(反)序列化操作,就避免了CPU开销;
如果RDD中数据比较多时,直接用这种持久化级别,会导致JVM的OOM内存溢出异常。生产直接用这种策略的场景还是有限的。
尝试使用MEMORY_ONLY_SER级别。将RDD数据序列化后再保存在内存中,此时每个partition仅仅是一个字节数组而已,大大降低了内存占用。
但理论上仍不能避免数据序列化后还是太大而导致OOM。
纯内存的级别都无法使用,那么建议使用MEMORY_AND_DISK_SER策略,会优先尽量尝试将数据缓存在内存中,内存缓存不下才会写入磁盘。
(不是MEMORY_AND_DISK策略。因为既然到了内存无法完全存放这一步,就说明RDD的数据量很大,做序列化几乎是必然的选择)
不建议使用 DISK_ONLY 和 后缀为_2 的级别:
DISK_ONLY因为完全基于磁盘文件进行数据的读写,会导致性能剧降低,有时还不如重新计算一次所有RDD。
_2的级别,必须将所有数据都复制一份副本并发送到其他节点上,数据复制以及网络传输会导致较大的性能开销,除非是要求作业的高可性,否则不建议使用。
sparkstreaming中socketTextStream默认是采用 MEMORY_AND_DISK_SER_2。
序列化
单纯持久化不会涉及这个数据的网络IO,是由这个节点自己取加载数据,然后缓存到自己节点的内存,所以不需强制要求序列化(序列化策略只是为了节省内存空间)。
而不像是广播变量需要从driver传输到个executor必然涉及网络IO,也就要求被广播的v必须实现序列化。
对序列化的选择其实就是对 Memory or CPU 开销的权衡:
- 序列化会导致CPU运算,但节省内存容量。假如机器CPU占用已经很高了,就不要选序列化了。
序列化默认Java序列化,效率低。
可以指定kryo序列化,但需要提前注册需要序列化的对象才能真正高效。
// 注册需要序列化的对象
sparkConf.registerKryoClasses(Array(classof[Info]))
// 切换到使用 kryo序列化
sparkConf.set("spark.serializer","org.apache.spark.serializer.KryoSerializer")
持久化的数据如何移除:旧分区drop时自动移除;手动:rdd.unpersist()。(不是lazy的)
如何处理小文件:在spark 里导入,再导出就好了。会自动合并小文件。
spark 官网术语解释
一定要熟读:https://spark.apache.org/docs/latest/cluster-overview.html 位置 spark.apache.org/docs/latest/index.html ==> Deploying ==> Overview
特别地,为了运行在一个集群上,SparkContext可以连接到多种类型的集群管理器(可以是Spark自己独立的集群管理器,Mesos, YARN或Kubernetes),这些管理器在应用程序之间分配资源。
一旦连接上,Spark将获取集群中节点上的executor,这些executor是为应用程序运行计算和存储数据的进程。
接下来,它将应用程序代码(由传递给SparkContext的JAR或Python文件定义)发送给executor。最后,SparkContext将任务发送给executor运行。
每个应用程序都有自己的executor进程,这些executor进程在整个应用程序期间保持运行,并在多个线程中运行任务。
这样做的好处是在调度端(每个driver调度自己的任务)和executor端(来自不同应用程序的任务运行在不同的jvm进程中)隔离应用程序。
但这也意味着,如果不将数据写入外部存储系统,就无法在不同的Spark应用程序(SparkContext的实例)之间共享数据。
Spark与底层集群管理器无关。只要它能够获取执行进程,并且这些进程之间相互通信,即使在一个支持其他应用程序的集群管理器(例如Mesos/YARN/Kubernetes)上运行它也是相对容易的。
driver必须在整个生命周期中侦听和接受来自它的executor的传入连接(例如,参见网络配置节中的spark.driver.port)。
因此driver必须从工作节点进行网络寻址。
因为驱动程序在集群上调度任务,所以它应该在靠近工作节点的地方运行,最好是在同一个局域网中。
如果您想要远程向集群发送请求,那么最好打开一个RPC给驱动程序,让它在附近提交操作,而不是在远离工作节点的地方运行驱动程序。
Driver program
一个应用程序Application由一个driver 和多个 executor。 driver是一个进程
Application Jar 不需要打入Hadoop和Spark(廋包就行) 。
胖包和瘦包
廋包就是平时打的包,只会打包自己的代码,而依赖的内容不会打入。
胖包除了打包自己的代码,还会将依赖的代码也直接打包到一起,方便在任何环境下直接运行。
修改pom文件就能快速同时打出胖廋包。
Cluster manager
控制管理spark作业提交到哪里运行
local // 本地运行
standalone // spark 集群,一般不用
yarn // 提交到yarn
k8s // 提交到k8s
mesos // 提交到mesos
spark-submit --master …
…的内容(提交到哪里运行)就是受Cluster manager管理,是一个请求资源的外部服务。如请求NM获得container
Worker node
如果是提交到 yarn,Worker node就是yarn的 NM
如果是standalone,Worker node 就是Spark的Worker
Executor
也是一个进程,让任务都在Executor中运行,同时要让计算数据缓存住。(run task & cache data)
每一个Application都有一些Executor,但各个Application间的Executor不会有关系(不跨Application)。
Task
是线程级 ==> 对标分区
Job
每一个action都会至少触发一个job,对标 action。
Stage
每一个job又会拆分出多个stage, stage之间一定由shuffle; 每遇到一个shuffle都会增加一个stage。
对比MR
在一个MR作业中:
1 Job == n Task(MapTask + ReduceTask) == n Jvm
在一个Spark Application中:
1 Application == 1 driver + n executor
1 Application == n Job(action)
1 Job == n stage
1 stage == n task (partition == 线程)
// 一个4040页面就对应一个Application,4041会是下一个Application
// 页面上:
// 这一个 Application,会有 Jobs,每一个Job都有Job Id(有一个action产生)。
// 一个Job点开,可能有多个stage(有shuffle的话),每个stage 有自己的 Stage Id。
// 列表中,每一个Stage Id有多个task线程。这些task 都是拿到 executor中运行。