用一个统一的数据抽象对象,来实现分布式框架中的计算功能
这个数据对象就是rdd
RDD
定义
-
弹性分布式数据集,spark中最基本的数据抽象
-
代表一个不可变、可分区、元素可并行计算的集合
-
Resilient:RDD中的数据可存储在内存或者硬盘中
-
Distributed: 数据是分布式的,可用于分布式计算
-
Dataset: 数据集合,用于存放数据
特性
- RDD是有分区的
- 分区是RDD最小的存储单位
- 分区是物理概念
- 多个物理的分区组成了一个抽象的RDD
- 可以用glom() API查看分区
- 计算方法会作用到每一个分区上
- RDD之间是有相互依赖的关系的
- 每个新产生的RDD都需要依赖于之前的RDD
- RDD之间是迭代计算的,会形成一个依赖链条
- KV型RDD可以有分区器 (可选)
- KV型RDD,RDD中的数据是二元组,例如
("hadoop",3)
,"hadoop"
是key,3是value - 默认会有一个hash型的分区器,将相同key值的RDD分到同一个分区
- 该特性是可选的,不是所有的RDD都是KV型的,例如像list的RDD
- KV型RDD,RDD中的数据是二元组,例如
- RDD的分区数据读取会尽可能靠近数据源 (可选)
- 尽可能保证数据是本地读取而不是网络读取,从而减少通讯时间
- 尽可能确保,不是100%保证的,优先要保证并行计算能力
RDD编程
对于sparkRDD编程,其入口一定是sparkContext,sparkContext的作用就是创建出第一个RDD
创建RDD主要两种方式:
- 通过将本地对象转化成分布式的RDD
- 读取外部的文件
RDD算子
本质就是RDD上的api,本地对象(例如list)的叫做方法,RDD的叫做算子
RDD的算子分成两类,Transformation算子(转换算子),Action算子
Transformation算子
只要返回值依然是rdd的,就是trans算子
特性: 这类算子是lazy的,如果没有action算子的话,他永远不会计算,只是在构建计划
常见的transformation算子
-
map算子
传入一个函数对象,将RDD中的数据按照函数中的逻辑一条条进行处理,返回一个新的rdd -
flatmap算子
先对rdd执行map操作,再执行解除嵌套
解除嵌套就是将多维的对象转换成一维的 -
reduceByKey
针对KV型RDD,按照key进行分组,在按照聚合逻辑完成组内数据的聚合操作
传入两个参数,返回一个值 -
mapValues
针对二元元组,内部的value进行map操作,只对value进行操作
例如
rdd = sc.paralleize([('a',1),('b',11))
rdd.mapValues(lambda x : x *10).collect()
输出的结果为[('a',10),('b',110)]
- groupBy
将RDD数据进行分组
action算子
只要返回值不是rdd的就是action算子
只有有了action算子,数据才开始真正被处理
常见的action算子
-
collect
这个要十分注意,因为RDD是分布式的,可以计算的数据量非常大,collect会将分布式的rdd计算结果收集到driver端,如果不注意的话可能会造成driver端的内存溢出 -
foreach
该算子也是遍历rdd中的每一个元素,对其进行func中的操作,但是要注意和map不同的是,该算子没有返回值
该算子中的操作都是在executor端进行的,map后进行collect的操作,是将元素计算处理后再收集到driver端,由driver端进行处理。因此该算子能够降低driver端的压力。同时因为是在executor中执行,多个executor之间是并行的,能够有效提高部分逻辑的计算效率,避免了和driver的交互和统一处理
reduceByKey 和 groupByKey的区别
- reduceByKey的性能远大于groupByKey
groupByKey是先进行分组,分完组后再进行自逻辑的聚合,分组的过程中会发生多次io
而reduceByKey是先进行预分组,再进行分组,分组完成后再进行最终的聚合。
groupBykey的每一次分组都是一次shuffle,而reduceByKey减少了shuffle的次数
对于分区操作,尽量不要尝试去增加分区数,不然可能会破坏内存迭代的管道
RDD的持久化
为了最大化利用资源,旧的rdd在进行转化成新的rdd之后就没有必要再存在了,为后续的计算节省内存空间
但是如果基于rdd1生成了rdd2和rdd3,在rdd1转化成rdd2时,rdd1就已经不存在了,生成rdd3时需要重新再生成rdd1,如果该rdd是较为靠后的rdd,需要重新遍历一遍依赖链,从最开始重新开始生成。这样会造成效率的降低,需要将rdd进行持久化
cache 缓存
可以通过调用.cache()
的api,将rdd持久化
通过调用.persist()
设置将rdd持久化的位置,推荐的持久化位置MEMORY_AND_DISK
,对于内存较小的集群,推荐将其在硬盘上持久化DISK_ONLY
最后请将缓存主动清理掉,避免占用内存,使用.unpersist()
特性
-
调用
.cache()
后,会在每台服务器的内存或者硬盘上都创建一个副本,属于分散存储 -
缓存是不安全的,如果缓存丢失,会重新根据迭代链进行计算并缓存
因此对于rdd来说,一定会保留其血缘关系(迭代链),确保rdd能够再次被计算
checkpoint
checkpoint只支持使用硬盘进行存储,他被认为是安全的,在设计上认为是不会丢失的。因此不保留rdd的血缘关系
checkpoint集中收集每个分区的数据进行存储,属于集中存储,将分区中的文件收集保存到hdfs上(hdfs是一种安全的存储)
在使用是,首先要在sparkconf中调用声明cp的保存路径
sc.setCheckpointDir("hdfs://node1:8020/....")
# 如果是local可以保存在本地,其余需要保存在hdfs上
# 对于某个需要多次调用的rdd,直接调用checkpoint算子就可以了
rdd.checkpoint()
checkpoint和cache的对比
- cp不管分区多少,风险是一致的,因为都要收集起来集中存储。而cache分区越多,风险越高,因为是分散存储的,一旦一个分区挂了就都丢失了
- cp支持写入hdfs,高可靠的,而cache不行
- cp不支持写入内存,但是cache可以,因此cache的性能比cp稍微好一点
- cp被认为是安全的,因此不保留血缘关系,而cache需要保留
- cache一般适合于轻量化的,对于较为复杂庞大的rdd最好使用cp
广播变量
当本地存在某些变量需要去和rdd进行交互的时候,会被分发到每个rdd所在的线程中使用。但是对于同一个executor中的线程而言,他们同属于一个进程,他们之间的资源是共享的,每个线程发送一个数据实际上造成了内存的浪费。
代码
# 1. 将本地变量标记成广播变量
broadcast = sc.broadcast(local_list)
# 2. 使用广播变量的时候,直接从broadcast对象中取出value,
local_val_value = broadcast.value
在使用的时候,spark分发的不再是本地变量了,而是给每个executor发送一个broadcast对象
能够有效减少网络io
但是如果本地集合对象很大的时候,driver端可能放不下,那还是需要将其封装到rdd中
累加器
对于分布式的executor,如果给一个全局变量,希望各分区中实现累加,实际上各分区内实现累加,最终到driver上的内容与rdd中executor中的数据无关,导致全局变量的异常
需要把这个累加的全局变量转换成spark的累加器类型,实现在分布式的场景下的累加
global_accum = sc.accumulator(0)
方法内的参数为初始值
-
如果涉及到多个rdd,例如rdd1->rdd2->rdd3,rdd2->rdd4
在生成rdd3的时候rdd2就已经不存在了,生成rdd4的时候会重新遍历生成链,再次计算rdd2 -
如果rdd2中调用了累加器,此时会再次激活累加器,再做一次计算。最终导致结果异常。为了避免这个问题,要将多次使用到的rdd持久化
DAG
action算子会将之前的一串rdd依赖链进行执行,每次执行action都会创建一个DAG
一个action会产生一个job,一个job就会产生一个DAG
宽窄依赖
- 窄依赖: 父RDD的一个分区,全部将数据发送给子RDD的一个分区
- 宽依赖: 父RDD的一个分区,将数据发送给子RDD的多个分区(也就是产生了不同分区之间的数据交互)
宽依赖有个别名叫做shuffle
窄依赖
- 父RDD的子RDD只有一个分区
- 子RDD分区可以有多个父RDD分区
宽依赖
父RDD有多个子RDD分区
一个job(DAG)中的stage的划分标准就是看是否产生了宽依赖,在同一个stage内部,必定都是窄依赖
stage的排序是从action算子开始从后往前计数
内存迭代计算
对于一个stage中,一个分区中的计算迭代都是在一个内存计算管道(pipeline)中独立运行,独立作为一个线程存在,作为一个task
多个分区中的pipeline为多个线程,多个线程并行计算
只有出现宽依赖的时候才会出现不同executor之间的io操作
对于spark优先保证并行计算,再去保证pipeline
为避免破坏pipeline,spark尽量不要去改动分区,遵循全局的并行度要求
spark并行度
同一时间内有多少task在同时运行
有多少并行度,就会创建多少个分区
设置并行度的时候可以通过代码或者配置文件来设置,其中有一个优先级从高到底为
- 代码
conf.set("spark.default.parallelism","100")
- 客户端提交参数
--conf "spark.default.parallelism=100"
- 配置文件
spark.default.parallelism 100
- 默认参数(1,或者基于读取文件的分片数量)
推荐设置成cpu核心的2到10倍,这个核心数与单个服务器的核心数无关,只看集群的总cpu核数
如果设置并行度和cpu核心数一致,如果某个task先执行完了,就会导致某个cpu核心空闲。因此,设置更大的并行度,保证一段时间内所有cpu都在工作,一个task执行完了之后会有后续的task补上
spark的任务调度
driver负责任务调度,包括
- 产生逻辑DAG
- 产生分区DAG
- 划分task
- 将task分配给executor并监控其工作
对于一个spark程序而言,其工作调度流程为
- 构建driver
- 构建sparkContext,作为执行环境的入口对象
- 基于DAG scheduler,构建逻辑task分配
- 基于task scheduler,将逻辑task分配给executor,并监控
- worker被task schedur监控,听从其指令,并定期汇报
其中只有5是由worker在工作,其余都是由driver管理
对于一个服务器来说,一般就开启一个executor,线程之间的资源是不共享的,交互的时候还是要从网络进行io,虽然快点,但是没什么意义
DAG调度器
处理逻辑DAG,得到逻辑上的task划分
task调度器
基于DAG调度器划分的task,规划这些task应该在哪些物理的executor上执行,并监控其运行