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数据分区数量规则