文章目录
RDD 详解
为什么需要 RDD
分布式计算需要什么条件?
分区控制:将数据分区,分别处理
shuffle控制:实现不同设备间的关联数据通信
数据存储\序列化\发送
数据计算API
这些功能我们不能通过Python内置的本地集合对象实现(那不成自己写框架了嘛,费事)
因此我们需要有一个统一的数据抽象对象,来实现上述分布式计算所需的功能,这个就是 RDD
什么是 RDD
弹性分布式数据集(Resilient Distributed Dataset),Spark中最基本的数据抽象,代表一个不可变、可分区、所含元素可并行计算的集合。
dataset:数据集合,存数据的
distributed:分布式存储,RDD的数据是跨机器存储的(跨进程)
resilient:弹性,数据可以存在内存或硬盘中,且可以动态扩容或缩容
RDD 五大特性
RDD数据结构有五个特性,其中前三个每个RDD都具备,后两个特征可能有但不确定
-
有分区
分区是RDD存储的最小单位。RDD是逻辑概念,而分区是物理概念,一个RDD由多个分区构成
-
RDD的 方法作用在每个分区 上
元素变大10倍的操作会作用在三个分区上
-
RDD之间有单向依赖关系
RDD总是由一个RDD迭代到另一个RDD,对一个RDD的每次运算都会生成新的RDD
RDD1->RDD2->RDD3->RDD4,构建成为DAG(有向无环图)
当有分区数据丢失的时候,Spark会通过依赖关系重新计算,算出丢失的数据,而不是对RDD所有的分区进行重新计算
-
KV型RDD有分区器
KV型RDD:存储的数据是二元元组的RDD,如(“hadoop”, 3)
KV型RDD在分区时默认按Hash规则分区,key的哈希值相同的元组划分到同一个分区
也可以手动设置分区器(rdd.partitionBy的方法来设置)
-
RDD分区规划会尽量靠近 数据 所在地
RDD在规划时,分区会尽量规划到存储数据的服务器上
这样就可以走本地存储,避免网络通信
移动数据不如移动计算
WordCount分析(RDD视角)
总结
如何理解RDD?
RDD是一个抽象的概念,
存储上来说,是一组分布式分区的集合,分区的数量和存储位置是弹性的(可动态扩容,可内存可硬盘)
计算上来说,RDD是分布式计算的载体,计算会作用到RDD中的每个分区上
RDD的五大特点?
分区、计算雨露均沾、不同阶段的RDD单向依赖、KV型RDD有分区器、分区靠近数据
RDD 编程入门
程序执行入口SparkContext
无论何种语言,Spark RDD 编程的程序入口对象就是SparkContext,基于它才能执行后续的API调用和计算
本质上来说,SparkContext的主要功能就是创建第一个RDD出来
RDD的创建
-
并行化创建
将本地集合转为分布式RDDrdd = sparkcontext.parallelize(集合对象,分区数) # 集合对象,如list等
-
读取文件创建
读取 本地 或 HDFS 数据创建RDD对象rdd = sparkcontext.textfile(文件路径,分区数) # 分区数在Spark允许的范围内才有效 rdd = sparkcontext.wholeTextfile(文件夹路径,分区数) 适用于读取一堆小文件 collect后结果为一个list,其中元素为二元元组,key为文件路径,value为文件内容
RDD 算子
算子:分布式集合对象上的API,即作用在分布式对象上的方法,为了区分本地对象上的API(方法)
分类:转换算子和动作算子
- 转换算子(Transformation)
return RDD 的算子,这类算子是 懒加载 的(如果没有action算子,转换算子是不工作的) - 动作算子(Action)
返回值不是RDD的算子
转换算子相当于构建计划,动作算子为开关,只有动作算子出现计划才会被执行
转换算子
算子 | 参数 | 功能 | 语法 | 注 |
---|---|---|---|---|
map | 函数名 | 将RDD的数据一条条处理(根据接收的处理函数),将处理函数的结果封装到新的RDD中返回 | rdd.map(func) | 参数函数的参数类型不限,返回值类型不限 |
flatMap | 函数名 | 对RDD进行map操作,然后进行解除嵌套 | rdd.flatMap(func) | 参数函数的要求同map |
reduceByKey | 函数名 | 针对KV型RDD,先按照key分组,然后根据参数函数完成组内数据 (value)聚合 | 参数函数接收两个参数,返回一个值,两个参数和返回值的类型要相同;参数函数只管聚合,不管分组;value两两叠加式聚合,如对1,2,3,4相加,计算顺序是1+2,3+3,6+4 | |
groupBy | 函数名 | 将RDD的数据进行分组 | 分组依据是将RDD中的每条数据输入参数函数得到返回值,将返回值相同的对应数据分到一组;每个组是一个二元组,key为参数函数返回值,value是同组数据的迭代器 | |
filter | 函数名 | 过滤出想要的数据 | 参数函数的返回值需为bool,true的保留,false的丢弃 | |
distinct | 去重分区数量 | 对RDD数据去重,返回新RDD | 算子参数一般缺省;RDD数据类型任意,数字字符都行 | |
union | rdd | 合并2个RDD为1个RDD返回 | rdd.union(other_rdd) | 只合并,不去重,合并时不管数据类型 |
join | rdd | 对两个RDD执行join操作(可实现SQL的内/外连接) | rdd.join(other_rdd) rdd.leftOuterJoin(other_rdd) | join算子只能作用于二元元组的RDD,默认按照key进行关联,若要通过value关联需要用map调换kv的位置 |
intersection | rdd | 求两个RDD的交集,返回新RDD | rdd.intersection(other_rdd) | |
glom | - | 将RDD数据按照分区加上嵌套 | rdd.glom() | |
groupByKey | - | 针对KV型RDD,自动按照key将value分组 | 分组后的数据迭代器中只保留value | |
sortBy | 函数名; ascending(bool); numPartitions(int) | 对RDD数据进行排序 | rdd.sortBy(func, ascending=False, numPartitions=3) | 参数函数的作用是指定排序的依据列;ascending升序降序;numPartitions表示排序的分区数,要保证数据全局有序要设置为1 |
sortByKey | keyfunc; ascending; numPartitions | 针对KV型RDD,按key进行排序 | rdd.sortByKey(ascending=True, numPartitions=None, keyfunc=< function>) | keyfunc在排序前对key进行处理 |
解除嵌套:[ [1, 2], [3, 4] ] -->[1,2,3,4],对分区内的字符串split后collect会得到嵌套结果
flatMap若只想解嵌套不Map,可以传一个lambda x: x
案例
# -*- coding = utf-8 -*-
from pyspark import SparkConf, SparkContext
import json
if __name__ == '__main__':
conf = SparkConf().setAppName("Transformation_operators_example").setMaster("local[*]")
sc = SparkContext(conf=conf)
# 将数据按|分割,得到一组json数据(字符串)
# 通过Python自带的json库将每个json字符串转为字典
# 根据字典的值过滤出包含北京的数据
# 组合 北京 和 商品类型 形成新的字符串
# 去重
rdd = sc.textFile("../data/input/order.text")\
.flatMap(lambda line : line.split("|"))\
.map(lambda json_str: json.loads(json_str))\
.filter(lambda d: d['areaName'] == '北京')\
.map(lambda x: x['areaName'] + '_' + x['category'])\
.distinct()
print(rdd.collect())
动作算子
算子 | 参数 | 功能 | 语法 | 注 |
---|---|---|---|---|
countByKey | - | 统计key出现的次数 | rdd.countByKey() | 一般适用于kv型RDD,返回值为字典{(key:count)} |
collect | 将RDD各分区中的数据收集到Driver中,形成一个List对象 | 要拉取到Driver中,结果不能过大,否则会撑爆Driver内存 | ||
reduce | 函数名 | 将RDD数据集按传入方法的逻辑进行聚合 | rdd.reduce(lambda a,b:a+b) | 返回值类型不确定,可以使int或str等等 |
fold | init_value, 函数名 | 接收传入逻辑进行聚合,但是聚合带有初始值 | rdd.fold(10, lambda a,b:a+b) | 初始值会同时作用在分区内和分区间 |
first | 取出RDD的第一个元素 | 返回值类型为rdd中的元素类型 | ||
take | int | 取出RDD的前n个元素 | rdd.take(5) | 返回 List |
top | int | 对RDD降序排序,并取前n个 | rdd.top(5) | 返回 List |
count | 返回RDD中的数据数量 | |||
takeSample | bool, 采样数,随机数种子 | 随机抽样RDD数据 | bool为True表示允许取同一个位置的数据,可以重复抽样;随机种子可以省略 | |
takeOrdered | int,函数名 | 对RDD按参数函数处理后的值排序,然后取前n个值对应的原始数据 | 默认降序,若要升序可以用参数函数将数据取负 | |
foreach | 函数名 | 功能和map一样 | rdd.foreach(lambda x: print(x*10)) | 该方法没有返回值;foreach的执行结果由Executor直接输出,而不像collect等将结果汇集到driver后输出,效率高,如在lambda函数中执行数据库插入操作 |
saveAsTextFile | 将RDD的数据写入文本文件中 | 支持本地写出和HDFS,一个分区中的数据写到一个文件中,由Executor执行 |
分区操作算子
算子 | 参数 | 功能 | 语法 | 注 |
---|---|---|---|---|
mapPartitions | 函数名 | 按分区,根据参数函数的逻辑,处理RDD中的数据 | 参数函数的参数是一个分区的数据(迭代器),返回值也是一个迭代器,map是单个元素;减少了网络IO的次数,性能优于map | |
foreachPartition | 函数 | foreach的分区计算版本 | 性能会优于foreach | |
partitionBy | int,函数名 | 对RDD进行自定义分区 | int为新分区数;参数函数对key做运算,返回值需为int类型(分区编号) | |
repartition | int | 对rdd的分区重新分区 | 参数为新分区数量;一般不要修改分区,会影响并行计算(内存迭代的管道数量) ;一般不要增加分区(可能会导致shuffle) |
groupByKey 和 reduceByKey 的区别:
功能上:
groupByKey 只能 分组,reduceByKey 是 分组+聚合 一体化
性能上:
reduceByKey的性能要优于 groupByKey + reduce
groupByKey + reduce只能先分组,后聚合,分组时要对每条数据进行IO,消耗很大
reduceByKey 可以先预聚合,然后对预聚合的结果进行IO(分组),再最终聚合,减少被shuffle的数据量,提升性能
数据量越大,reduceByKey的优势就越高
总结
RDD创建方法:
本地集合并行化、读取文件(TextFile\WholeTextFile)
RDD分区数怎么查看
rdd.getNumPartitions()
Transformation 和 Action 的区别
转换算子必返回RDD,动作算子必不返回RDD
转换算子懒加载,不遇到动作算子不执行
哪两个Action算子的结果不经过Driver直接Executor输出
foreach 和 saveAsTextFile
reduceByKey 和 groupByKey 的区别
功能:reduceByKey 分组+聚合,groupByKey 聚合
性能:reduceByKey 分组前预聚合,再shuffle,传输IO小性能强
mapPartitions 和 foreachPartition的区别
mapPartitions有返回值,foreachPartition无返回值
mapPartitions是转换算子,foreachPartition是动作算子
二者都是以分区为单位处理数据,比普通算子性能强
分区操作的注意点:
尽量不要增加分区,否则可能破坏内存迭代的计算管道
RDD 持久化
RDD 的数据是过程数据
RDD之间是有血缘关系的,新 RDD 的生成,代表着老 RDD 的消失
一旦处理完成,RDD的数据就被清除了
这个特性可以最大化资源利用率,从内存中清除老 RDD,为后续计算腾出内存空间
当要多次使用同一个RDD时,第2次使用时只能基于血缘关系重新构建该RDD,这性能损耗就很厉害了,怎么解决?RDD 持久化技术!(缓存 和 CheckPoint)
RDD 缓存
Spark 提供了缓存API,用于将 RDD 数据保存在内存或硬盘上
缓存API有两种
rdd.cache():缓存到内存中
rdd.persist(StoreageLevel):根据参数设置的级别将RDD数据存储到内存或硬盘中 from pyspark.storagelevel import StorageLevel导入参数类
rdd.unpersist():清理缓存
调用API后,每个Executor存储其所在服务器上的RDD分区数据到该Executor所在服务器的内存或硬盘上(分散存储),并且被缓存的RDD的前置血缘关系也会被存储(防止内存数据丢失)
RDD CheckPoint
CheckPoint 也是存储RDD的数据,但是它仅支持硬盘存储
再设计上CheckPoint被认为是安全的,因此它不保留RDD血缘关系
CheckPoint 存储RDD数据时是将每个RDD分区中的数据进行收集后存储(如存到HDFS中)
CheckPoint VS 缓存
CheckPoint | 缓存 | |
---|---|---|
存储方式 | 集中存储(存整个RDD) | 分散存储(以分区为单位存) |
风险 | 与分区数量无关 | 分区越多,风险越大 |
存储位置 | 支持HDFS和本地硬盘 | 内存或硬盘 |
性能 | 略差(集中存储涉及网络IO) | 好一点(能写内存,各Executor并行) |
血缘关系 | 不保留 | 保留 |
RDD 案例练习
需求:
用户搜索关键词分析
用户和关键词组合分析
热门搜索时间段分析
RDD 共享变量
广播变量
当 本地list 和 分布式RDD 对象有了 关联(如要将RDD对象内的部分数据用本地list中的部分数据替换),Driver 就会向托管每个分区的 Executor 发送 序列化的 本地list 对象,但若每个Executor 托管着多个分区,Executor 本质上是一个进程,进程内资源共享,向该进程内的多个分区发送 本地list 就会造成 多余的网络IO 和 内存浪费
怎么解决?广播变量!
将本地对象封装到广播变量当中,使用时从广播变量中取出本地变量即可
对于同一个Executor托管的分区,Driver只会向他们发送一份本地数据(一个广播变量),减少网络IO,节省内存
使用方法
将本地list转为RDD也可以实现功能,但是RDD关联RDD需要通过join算子,就有可能产生shuffle,性能不如本地list关联RDD
累加器
分布式场景下,当我们想要统计RDD中的某类数据的数量,可能会在map算子的参数函数中定义一个自增计数器变量count,用于统计被处理过的数据数量
那么问题就来了,非RDD代码在 Driver中执行,Driver定义了count,对分区执行map操作时要用到count,Driver会将count(只是数据)发给每个Executor,每个Executor内对count的累加操作只影响本Executor内的count,对Driver中的count无效,就无法实现分布式累加了
怎么办?使用Spark提供的累加器!累加器对象可以从各Executor中收集他们的执行结果并汇总,实现分布式累加
使用方法
acmlt = sc.accumulator(0) 创建累加器变量,参数为累加器初始值
注意
累加器的值和对应的RDD的创建相关,当一个RDD被多次生成时,累加器中的值也会多次累加,导致结果错误,可以通过RDD持久化(缓存或CheckPoint)解决
总结
广播变量解决什么问题?
分布式集合RDD 和 本地集合进行关联使用时,降低内存占用,减少网络IO,提高性能
累加器解决什么问题?
全局累加
Spark 内核调度
DAG
有向无环图,描述了 一组单向依赖的RDD 的 逻辑执行流程 ,
Job 和 Action
Action算子会将该算子之前的一串rdd依赖链条执行起来
也就是说,一个Action会产生一个逻辑DAG,会在程序运行时产生一个Job
即:一个Application中,可以包含多个Job,每个Job对应着一个DAG,且每个Job都是由一个Action产生的
DAG 和 分区
DAG 的最终目的是:构建物理上的Spark详细执行计划
带有分区关系的DAG只有在代码运行起来之后才能画出来,因为只有运行起来后才能确定分区数量等
DAG的宽窄依赖和阶段划分
Spark中RDD的血缘依赖关系可以分为两类
- 窄依赖:父RDD的一个分区,全部将数据发给子RDD的一个分区
- 宽依赖:父RDD的一个分区,将数据发给子RDD的多个分区,也叫 shuffle
射出多个箭头的就是宽依赖
阶段划分
Spark中Job会被划分为不同的阶段(Stage),划分依据是:遇到宽依赖就划分出一个阶段
Stage内部一定都是窄依赖,窄依赖的一组RDD分区可以整整齐齐的运行
内存迭代计算
RDD的转换怎么实现?
不同RDD之间的转换用不同的线程执行(线程若属于不同Executor还需要网络IO进行交互)
不可行×
在RDD转换链中,具有窄依赖的一组RDD的转换由一个线程完成(线程内无需通过IO交互)
可行√
上述一组窄依赖RDD的转换被规划为一个Task(线程完成),是纯内存计算,构成内存计算管道
横向来看,一个分区只会被一个Task处理,
竖向来看,一个 RDD 中的每个分区都会被不同的 Task 处理
Spark 默认收到全局并行度的限制,除个别算子有特殊分区需求(如sort要保证全局排序要设为1),一般不建议在算子上再设置并行度,这样有可能破坏内存计算管道,产生额外的 shuffle(分区数不1:1就会宽依赖了)
1. Spark如何做内存计算?DAG的作用?Stage阶段划分的作用?
Spark会产生DAG图
DAG图会基于分区和宽窄依赖关系划分Stage
一个Stage内都是窄依赖,如果一组窄依赖的RDD内形成1:1的分区数量关系,就可以产生很多内存迭代计算管道
这些管道就是一个个具体的执行Task
一个Task是一个具体的线程,任务跑在一个线程内,就是走内存计算了
DAG 是为了内存计算
Stage划分 是为了构建内存计算管道
2. Spark 为什么比 MapReduce 快?
- Spark 算子丰富。MapReduce算子很少(Map 和 Reduce),处理复杂任务时往往需要多个MapReduce串联,这就需要通过磁盘交互数据,导致速度慢
- Spark 可以尽可能多的内存迭代计算。算子之间形成DAG,基于依赖划分阶段后,阶段内形成内存迭代管道,速度快,MapReduce中的Map和Reduce依旧通过磁盘交互
Spark 并行度
并行度:同一时间内同时运行的 Task 数量
并行度会影响到 RDD 的分区设定
如设置并行度 6,有 6 个 Task 并行的前提下,RDD 就被规划成 6 个分区了。先有并行度,才有分区规划
并行度设置位置:
优先级降序
代码 > 提交程序的客户端参数中 > 配置文件
默认并行度为 1
全局并行度设置方式:
集群中如何规划并行度?
设置为集群内CPU总核心数的2~10倍,确保是核心数的整数倍,最少2倍,不能过大,过大调度难度会很大
CPU 的一个核心一次只能干一件事,若Task的压力不均衡,某个 Task 先执行完了,就会导致部分 CPU核心 的空闲,所以多设置并行度可以保证 Task 运行完后有新 Task 补上,不让 CPU 闲下来,最大程度利用集群的资源
Spark 任务调度
Spark 的任务,由 Driver 进行调度,主要包括
- 逻辑DAG产生
- 分区DAG产生
- Task划分
- 将Task分配给Executor并监控其工作
Spark 程序的调度流程如上图:
- 构建Driver
- 构建SparkContext(执行环境入口对象)
- 将Job代码提交给 DAG Scheduler(DAG调度器),构建分区DAG图,和基于此图的Task分配
- 基于 Task Scheduler(Task调度器)将Task分配到各个Executor上干活,并监控他们
Driver内的两大组件:
DAG 调度器:根据代码逻辑生成逻辑DAG,基于分区关系生成分区DAG,基于分区DAG得到逻辑上的Task划分(即每个Task做什么,Task之间的交互关系)
Task 调度器:基于逻辑 Task 划分,来规划这些 Task 应该在哪些物理Executor上运行,并监控管理
Spark 概念名词大全(挖坑)
Term | Meaning |
---|---|
Application | 用户代码被提交到Spark运行时就会形成一个Application,由一个Driver和多个Executor构成 |
Driver program | 管理main方法的入口,程序运行的调度者和管理者,负责构建SparkContext |
Cluster manager | 外部服务,用于管理集群资源(Master角色,如Standalone manager 和 Yarn 中的 resource Manager) |
Deploy mode | |
Spark 层级运行关系
一个 Spark环境,可以运行 多个Application(一个运行中的代码)
Application 内可以有 多个 Job
每个 Job 由一个 Action算子 产生,且每个 Job 有自己的 DAG图
一个 Job 的 DAG,根据宽窄依赖划分成 不同Stage
每个 Stage 基于分区数量形成 多个内存迭代管道
每个内存管道形成一个 Task(DAG调度器划分Job成Stage,Stage被分为具体的Task任务,)
总结
DAG是什么有啥用?
有向无环图,描述任务执行流程,主要作用是协助DAG调度器构建Task分配用作任务管理
内存迭代 / 阶段划分?
基于DAG中RDD的宽窄依赖划分阶段,阶段内部是窄依赖可以构建内存迭代的管道
DAG 调度器?
构建Task分配,用以任务管理