RDD 介绍
spark 最重要的一个概念叫 RDD,Resilient Distributed Dataset,弹性分布式数据集,它是 spark 的最基本的数据(也是计算)抽象。
代码中是一个抽象类,它代表一个 不可变、可分区、里面的元素可并行计算的数据集合。
RDD 的属性
拥有一组分区:数据集的基本组成单位
拥有一个计算每个分区的函数
拥有一个分区器,partitioner,即 RDD 的分片函数
RDD 间存在依赖关系 【下面 RDD 特点中有解释】
拥有一个列表,存储每个 partitioner 的优先位置
优先位置
这里有个概念叫移动数据不如移动计算,之前在 hadoop 中接触过这个概念,不多解释;
RDD 也是分区存储的,那么 RDD 的数据到底存储在哪个节点,必须有个记录;
当有请求过来时,为了提高运行效率,我们需要把任务下发到存储所需数据的节点,这样避免了网络传输
RDD 的特点
只读:无法进行更改,也就是不可变,如果需要改动,只能创建一个新的 RDD,也就是从一个 RDD 转换成另一个 RDD
依赖关系:一个 RDD 由另一个 RDD 转换得到,也就是 一个 RDD 依赖另一个 RDD,而且可以多层依赖,为了便于理解和描述,我们有时把依赖关系也叫血缘关系;
当 血缘关系 很长时,可以通过持久化 RDD 来切断血缘关系;
惰性执行:RDD 的执行是按照血缘关系延时计算的,也就是 惰性计算
依赖关系
依赖关系有两种
窄依赖:一对一;RDDs 之间分区 一 一对应,也就是一个分区完全由另一个分区转换得到,而不是由另外几个分区转换得到
宽依赖:多对一;与窄依赖相反,一个分区由另外几个分区转换得到
缓存
缓存是把 RDD 暂时存起来,用于如下场景:
1. 优化效率:一个程序中多次用到一个 RDD,可以先把这个 RDD 缓存起来,避免重复计算
2. 避免出错:
distFile = sc.textFile('README.md')
m = distFile.map(lambda x: len(x)) # map 是 转换 操作,并不立即执行
m.cache() # 把 map 的输出缓存到内存中,其实 cache 就是 执行 操作 #或者
m.persist()
RDD 的创建
创建 RDD 有 3 种方式
1. 基于内存:从内存中的数据创建 RDD,并行化已存在的序列
2. 基于外部存储介质:读取外部数据源,比如 hdfs 【外部存储介质中的数据,注意 spark 本身没有存储功能】
3. 由一个 RDD 转换得到:这种方式不多解释
并行化数据集合
并行化集合是通过在一个 迭代器或者集合 上 调用 SparkContext 的 parallelize 方法生成的 【内存中的数据】
可能还有个 makeRDD,我这里实验不成功,makeRDD 封装了 parallelize,功能是一样的
data = range(10000)
distData = sc.parallelize(data)
distData.reduce(lambda a, b: a+b)
为了创建一个能并行操作的分布式数据集,所有元素都将被拷贝;
然后我们可以调用 reduce 进行叠加,累计求和
并行化集合时一个重要的参数是将数据集切分的数量。一个切片对应一个 spark 任务,我们可指定切片数量
distData2 = sc.parallelize(data, 100) # 切 100 分
外部数据集
由于 spark 本身没有存储功能,一般是从 本地文件、hadoop 等获取外部数据集
本地文件
textFile(name, minPartitions=None, use_unicode=True)[source]
Read a text file from HDFS, a local file system (available on all nodes), or any Hadoop-supported file system URI, and return it as an RDD of Strings. If use_unicode is False, the strings will be kept as str (encoding as utf-8), which is faster and smaller than unicode. (Added in Spark 1.2)
minPartitions 指定最小分区数,也就是说,实际分区数可能比这个数大,在 hdfs 中,文件本身就是 以 block 存储的,此时这个 分区数 不能小于 block 数
示例代码
distFile = sc.textFile('README.md')
distFile = sc.textFile('xx.csv')
distFile = sc.textFile('logs') # 支持文件夹
# textFile("/my/directory/*.txt") # 支持通配符
# textFile("/my/directory/*.gz") # 支持压缩文件 type(sc.textFile('README.md')) # <class 'pyspark.rdd.RDD'> distFile.map(lambda x: int(x[-1])).reduce(lambda a, b: a + b)
distFile.map(lambda x: len(x)).reduce(lambda a, b: a + b) # map 的输入是每一行,map 的作用是求每一行的 len
# reduce 的输入是两个数字,reduce 的作用是求这两个数的和,
# 也就是把 所有行的 len 逐次求当前元素(当前累计 len 的和)与下一元素的和
读取文件后自动生成 RDD;
各种读取方式都支持 多种 文件格式,如 文件夹,通配符、压缩文件
批量读取本地文件
distFile = sc.wholeTextFiles('files') # 读取 files 文件夹下所有文件内容,返回 (filename, filecontent) 键值对
输入必须是 路径
读取 hdfs 文件
rdd = sc.textFile('hdfs://hadoop10:9000/spark/dwd.csv')
RDD 的读取与存储
读取和存储其实都是一种 action 操作,这里只做简单介绍
rdd.collect() # 读取
rdd.saveAsPickleFile('path')
rdd.saveAsTextFile('path')
存储路径事先不能存在;Pickle 序列化,编码存储;
RDD 算子
算子:认知心理学中的概念,它认为解决问题其实是将问题的初始状态,通过一系列的操作,对问题状态进行转换,然后达到解决状态,这个一系列操作称为算子。
只是一种高大上的叫法,其实就是 RDD 操作
RDD 支持两种类型的操作:transformations 和 actions
transformations:转换,就是从一种 RDD 转换成 另一种 符合 要求 的 RDD,类似于 map
actions:行动,执行计算,类似于 reduce,返回结果
值得注意的是,transformations 是惰性计算,也就是说 transformations 并不立即执行,只有 actions 时,才会执行 transformations
这使得 spark 高效,以 map-reduce 为例,如果我们 map 并不是目的,reduce 才是目的,那么 map 之后 就计算的话,输出存在哪里呢?
如果存在文件,一浪费时间,二浪费地方,如果我们 reduce 时才执行 map,直接把 map 的庞大输出存入内存即可,甚至 流式 传给 reduce,非常高效。
RDD 分区
我们通过实操来验证 RDD 是如何分区的
parallelize 分区
操作一:local 模式,不指定分区
data = range(5)
distData = sc.parallelize(data)
distData.saveAsPickleFile('output') # 存储路径事先不能存在
输出 4 个分区
_SUCCESS
part-00000 # 存放了 0
part-00001 # 存放了 1
part-00002 # 存放了 2
part-00003 # 存放了 3,4
4 从何而来呢?
[root@hadoop10 ~]# cat /proc/cpuinfo |grep "cores"|uniq
cpu cores : 4
即 CPU 核数,
也就是说,parallelize 默认分区数为 CPU 核数
操作二:local 模式,指定分区
data = range(5)
distData = sc.parallelize(data, 3)
distData.saveAsTextFile('output')
输出 3 个分区
_SUCCESS
part-00000 # 存放了 0
part-00001 # 存放了 12
part-00002 # 存放了 34
也就是说,指定了分区数,就分为多少个区,注意,请指定 大于等于 2 的数,不要瞎搞
操作三:local 模式,指定分区,且分区数大于数据量
data = range(5)
distData = sc.parallelize(data, 6)
distData.saveAsTextFile('output')
输出 6 个分区
_SUCCESS
part-00000 # 存放了 空
part-00001 # 存放了 0
part-00002 # 存放了 1
part-00000 # 存放了 2
part-00001 # 存放了 3
part-00002 # 存放了 4
也就是说,即使数据量不够多个分区瓜分,也会按指定个数生成分区,没分到数据的分区为空
通过源码知悉,parallelize 的分区数优先级 为 自定义 --> max(cpu 核数,2),也就是最小为 2
textFile 分区
这个相对来说比较麻烦,我们做个简单介绍。
首先看下 textFile 源码
可以看到,minPartitions 就是我们自定义的分区数;然后 调用了 hadoopFile 方法并把 minPartitions 传给了 hadoopFile;
minPartitions 的默认值是 调用 defaultMinPartitions 方法,源码如下
它取 defaultParallelism 和 2 的最小值;defaultParallelism 方法 源码如下
spark.default.parallelism 是在 SPARK_HOME/conf/spark-default.conf 中设置的一个参数,默认是没有这个配置的;
所以:
1. 如果没有在 textFile 中指定 minPartitions,而且也没有配置 spark.default.parallelism,那么 minPartitions 最大是 2
2. 如果没有在 textFile 中指定 minPartitions,但是配置了 spark.default.parallelism,那么 minPartitions 最大也是 2 【如果没有自定义分区数 minPartitions,那么 minPartitions = min(max(CPU 核数, 2), 2);假设我们认为核数大于 2 ,故 minPartitions = 2】
3. 如果没有在 textFile 中指定 minPartitions,minPartitions 最大是 2
4. 如果在 textFile 中指定了 minPartitions,minPartitions 就是指定数目
接下来,我们把 minPartitions 传给 hadoopFile,hadoopFile 会计算文件(如果有多个文件,计算全部文件大小)大小 totalSize,然后把文件 split 成 minPartitions 分,每个切片大小为 goalSize = totalSize / minPartitions,取整
每个 hadoop block 也是一个 文件,在 hadoop中 block 大小是有 最大值的,比如 64M,也就是说我们不能把文件切分成 大于 block 大小的块,也就是说 切片 取 min(goalSize, blockSize) 作为 真正的 切片大小,
但是 切片太小的话又没有意义了,所以 splitSize = max(minSize,min(goalSize, blockSize)), 而 minSize = max(SPLIT_MINSIZE,1),也就是说 minSize 最小为 1, 于是 splitSize 最小为 1,
分数区计算:totalSize / splitSize,如果整除,就是本身,如果不整除,取整,然后 + 1
那我们来测试下
dataFile = sc.textFile('data.txt')
dataFile.saveAsTextFile('output')
7 字节文件,不指定分区数,结果分为 3 个区;splitSize = 7 / 2 = 3.5 = 3, 分区数 = 7 / 3 = 2 + 1 = 3
8 字节文件,不指定分区数,结果分为 2 个区;splitSize = 8 / 2 = 4,分区数 = 8 / 4 = 2
8 字节文件,指定 4 个分区,splitSize = 8 / 4 = 2,分区数 = 8 / 2 = 4;经测试,正解
参考资料:
https://blog.csdn.net/zjwcsdn123/article/details/80489537 spark中textfile方法对文件的分片
https://www.jianshu.com/p/e33671341f0d spark通过textFile读取hdfs数据分区数量规则