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 (超快的统一分析引擎)

  1. speed 快 :基于内存;基于多线程(区别与MRtask进程级别);pipeline 流水线的方式(DAG图)
  2. easy of use: 支持多语言 Java、scala、python、R、sql
  3. Generality :具有共性的(批流一体),SQL、ss
  4. 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中运行。