这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战
一、RDD 的核心概念
RDD 是 Spark 最核心的数据结构,RDD(Resilient Distributed Dataset)全称为弹性分布式数据集,是 Spark 对数据的核心抽象,也是最关键的抽象,它实质上是一组分布式的 JVM 不可变对象集合,不可变决定了它是只读的,所以 RDD 在经过变换产生新的 RDD 时,(如下图中 A-B),原有 RDD 不会改变。
弹性主要表现在两个方面:
- 在面对出错情况(例如任意一台节点宕机)时,
Spark能通过RDD之间的依赖关系恢复任意出错的RDD(如B和D可以算出最后的RDD),RDD就像一块海绵一样,无论怎么挤压,都像海绵一样完整; - 在经过转换算子处理时,
RDD中的分区数以及分区所在的位置随时都有可能改变。
每个 RDD 都有如下几个成员:
- 分区的集合;
- 用来基于分区进行计算的函数(算子);
- 依赖(与其他
RDD)的集合; - 对于键-值型的
RDD的散列分区器(可选); - 对于用来计算出每个分区的地址集合(可选,如
HDFS上的块存储的地址)。
// 源码中解释如下:
Internally, each RDD is characterized by five main properties:
- A list of partitions
- A function for computing each split
- A list of dependencies on other RDDs
- Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
- Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file)
如下图所示,RDD_0 根据 HDFS 上的块地址生成,块地址集合是 RDD_0 的成员变量,RDD_1由 RDD_0 与转换(transform)函数(算子)转换而成,该算子其实是 RDD_0 内部成员。从这个角度上来说,RDD_1 依赖于 RDD_0,这种依赖关系集合也作为 RDD_1 的成员变量而保存。
(1)RDD 特点
特点:
- 分区
- 只读
- 依赖
- 缓存
checkpoint
- 分区
RDD逻辑上是分区的, 每个分区的数据是抽象存在的, 计算的时候会通过一个compute函数得到每个分区的数据。 如果RDD是通过已有的文件系统构建, 则compute函数是读取指定文件系统中的数据, 如果RDD是通过其他RDD转换而来, 则compute函数是执行转换逻辑将其他RDD的数据进行转换。
如图:
- 只读
RDD是只读的, 要想改变RDD中的数据, 只能在现有的RDD基础上创建新的RDD; 一个RDD转换为另一个RDD, 通过丰富的操作算子(map、filter、union、join、reduceByKey... ...)实现, 不再像MR那样只能写map和reduce了。
RDD 的操作算子包括两类:
transformation: 用来对RDD进行转化, 延迟执行(Lazy);action: 用来触发RDD的计算; 得到相关计算结果或者将RDD保存的文件系统中;
如图:
- 依赖
RDDs通过操作算子进行转换, 转换得到的新RDD包含了从其他RDDs衍生所必需的信息,RDDs之间维护着这种血缘关系(lineage,RDD之间的关系), 也称之为依赖。 依赖包括两种:
- 窄依赖:
RDDs之间分区是一一对应的(1:1或n:1)- 宽依赖: 子
RDD每个分区与父RDD的每个分区都有关, 是多对多的关系(即n:m)。有shuffle发生。
如图:
- 缓存
可以控制存储级别(内存、磁盘等)来进行缓存。 如果在应用程序中多次使用同一个
RDD, 可以将该RDD缓存起来, 该RDD只有在第一次计算的时候会根据血缘关系得到分区的数据, 在后续其他地方用到该RDD的时候, 会直接从缓存处取而不用再根据血缘关系计算, 这样就加速后期的重用。
如图:
checkpoint
虽然
RDD的血缘关系天然地可以实现容错, 当RDD的某个分区数据失败或丢失, 可以通过血缘关系重建。 但是于长时间迭代型应用来说, 随着迭代的进行,RDDs之间的血缘关系会越来越长, 一旦在后续迭代过程中出错, 则需要通过非常长的血缘关系去重建, 势必影响性能。RDD支持checkpoint将数据保存到持久化的存储中, 这样就可以切断之前的血缘关系, 因为checkpoint后的RDD不需要知道它的父RDDs了, 它可以从checkpoint处拿到数据。
(2)RDD 源码
在 Spark 源码中,RDD 是一个抽象类,根据具体的情况有不同的实现,比如 RDD_0 可以是 MapPartitionRDD,而 RDD_1 由于产生了 Shuffle(数据混洗),则是 ShuffledRDD。
RDD 源码:
// 表示RDD之间的依赖关系的成员变量
@transient private var deps: Seq[Dependency[_]]
// 分区器成员变量
@transient val partitioner: Option[Partitioner] = None
// 该RDD所引用的分区集合成员变量
@transient private var partitions_ : Array[Partition] = null
// 得到该RDD与其他RDD之间的依赖关系
protected def getDependencies: Seq[Dependency[_]] = deps
// 得到该RDD所引用的分区
protected def getPartitions: Array[Partition]
// 得到每个分区地址
protected def getPreferredLocations(split: Partition): Seq[String] = Nil
// distinct算子
def distinct(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] =
withScope {
map(x => (x, null)).reduceByKey((x, y) => x, numPartitions).map(_._1)
}
特别注意其中一条:
@transient private var partitions_ : Array[Partition] = null
RDD 是分区的集合,本质上还是一个集合,所以在理解时,可以用分区之类的概念去理解,但是在使用时,就可以忘记这些,把其当做是一个普通的集合。
举例:
val list: List[Int] = List(1,2,3,4,5)
println(list.map(x => x + 1).filter { x => x > 1}.reduce(_ + _))
......
val list: List[Int] = spark.sparkContext.parallelize(List(1,2,3,4,5))
println(list.map(x => x + 1).filter { x => x > 1}.reduce(_ + _))
(3)编程模型
RDD 编程模型:
- 创建
RDD tranformation算子操作action算子操作- 其他操作
二、如何创建 RDD
在创建 RDD 之前,可以将 RDD 的类型分为以下几类:
- 并行集合
- 从
HDFS中读取 - 从外部数据源读取
PairRDD
建议: Standalone 模式 或 本地模式学习 RDD 的各种算子;
不需要
HA; 不需要IDEA
-- 本地模式启动
$ spark-shell --master local
-- 实操如下:
[root@linux121 ~]# spark-shell --master local
21/02/19 09:46:16 WARN NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
Setting default log level to "WARN".
To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel).
Spark context Web UI available at http://linux121:4040
Spark context available as 'sc' (master = local, app id = local-1613699181443).
Spark session available as 'spark'.
Welcome to
____ __
/ __/__ ___ _____/ /__
_\ \/ _ \/ _ `/ __/ '_/
/___/ .__/\_,_/_/ /_/\_\ version 2.4.5
/_/
Using Scala version 2.12.10 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_231)
Type in expressions to have them evaluated.
Type :help for more information.
特别需要注意的是:spark-shell 中默认设定 spark context 为 sc。
Spark context available as 'sc' (master = local, app id = local-1613699181443).
(1)集合中创建 RDD
从集合中创建 RDD, 主要用于测试。Spark 提供了以下函数: parallelize、makeRDD、range
这种
RDD纯粹是为了学习,将内存中的集合变量转换为RDD,没太大实际意义。备注:
rdd.collect方法在生产环境中不要使用, 会造成Driver OOM
val rdd1 = sc.parallelize(Array(1,2,3,4,5))
val rdd2 = sc.parallelize(1 to 100)
// 检查 RDD 分区数
rdd2.getNumPartitions
rdd2.partitions.length
rdd3 = sc.makeRDD(List(1,2,3,4,5))
val rdd4 = sc.makeRDD(1 to 100)
rdd4.getNumPartitions
val rdd5 = sc.range(1, 100, 3)
rdd5.getNumPartitions
val rdd6 = sc.range(1, 100, 2 ,10)
rdd6.getNumPartitions
// 实操如下:
scala> val rdd1 = sc.parallelize(Array(1,2,3,4,5))
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at <console>:24
scala> rdd1.getNumPartitions
res0: Int = 1
scala> rdd1.partitions.length
res1: Int = 1
scala> val rdd1 = sc.makeRDD(1 to 100)
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[1] at makeRDD at <console>:24
scala> rdd1.getNumPartitions
res2: Int = 1
(2)从文件系统创建 RDD
用 textFile() 方法来从文件系统中加载数据创建 RDD。方法将文件的 URI 作为参数, 这个 URI 可以是:
-
本地文件系统
使用本地文件系统要注意: 该文件是不是在所有的节点存在(在
Standalone模式下),若是使用local模式则没有问题。 -
分布式文件系统
HDFS的地址从
HDFS中读取,这种生成RDD的方式是非常常用的。 -
Amazon S3的地址
// 从本地文件系统加载数据
val lines = sc.textFile("file:///root/data/wc.txt")
// 从分布式文件系统加载数据
val lines = sc.textFile("hdfs://linux121:9000/user/root/data/uaction.dat")
val lines = sc.textFile("/user/root/data/uaction.dat")
val lines = sc.textFile("data/uaction.dat")
(3)从外部数据源读取
Spark 从 MySQL 中读取数据返回的 RDD 类型是 JdbcRDD,顾名思义,是基于 JDBC 读取数据的,这点与 Sqoop 是相似的,但不同的是 JdbcRDD 必须手动指定数据的上下界,也就是以 MySQL 表某一列的最值作为切分分区的依据。
//val spark: SparkSession = .......
val lowerBound = 1
val upperBound = 1000
val numPartition = 10
val rdd = new JdbcRDD(spark.sparkcontext,() => {
Class.forName("com.mysql.jdbc.Driver").newInstance()
DriverManager.getConnection("jdbc:mysql://localhost:3306/db", "root", "123456")
},
"SELECT content FROM mysqltable WHERE ID >= ? AND ID <= ?",
lowerBound,
upperBound,
numPartition,
r => r.getString(1)
)
既然是基于 JDBC 进行读取,那么所有支持 JDBC 的数据库都可以通过这种方式进行读取,也包括支持 JDBC 的分布式数据库,但是需要注意的是,从代码可以看出,这种方式的原理是利用多个 Executor 同时查询互不交叉的数据范围,从而达到并行抽取的目的。但是这种方式的抽取性能受限于 MySQL 的并发读性能,单纯提高 Executor 的数量到某一阈值后,再提升对性能影响不大。
上面介绍的是通过 JDBC 读取数据库的方式,对于 HBase 这种分布式数据库来说,情况有些不同,HBase 这种分布式数据库,在数据存储时也采用了分区的思想,HBase 的分区名为 Region,那么基于 Region 进行导入这种方式的性能就会比上面那种方式快很多,是真正的并行导入。
//val spark: SparkSession = .......
val sc = spark.sparkcontext
val tablename = "your_hbasetable"
val conf = HBaseConfiguration.create()
conf.set("hbase.zookeeper.quorum", "zk1,zk2,zk3")
conf.set("hbase.zookeeper.property.clientPort", "2181")
conf.set(TableInputFormat.INPUT_TABLE, tablename)
val rdd= sc.newAPIHadoopRDD(conf, classOf[TableInputFormat],
classOf[org.apache.hadoop.hbase.io.ImmutableBytesWritable],
classOf[org.apache.hadoop.hbase.client.Result])
// 利用HBase API解析出行键与列值
rdd_three.foreach{case (_,result) => {
val rowkey = Bytes.toString(result.getRow)
val value1 = Bytes.toString(result.getValue("cf".getBytes,"c1".getBytes))
}
值得一提的是 HBase 有一个第三方组件叫 Phoenix,可以让 HBase 支持 SQL 和 JDBC,在这个组件的配合下,第一种方式也可以用来抽取 HBase 的数据,此外,Spark 也可以读取 HBase 的底层文件 HFile,从而直接绕过 HBase 读取数据。
通过第三方库的支持,Spark 几乎能够读取所有的数据源,例如 Elasticsearch
(4)从 RDD 创建RDD
PairRDD 与其他 RDD 并无不同,只不过它的数据类型是 Tuple2[K,V],表示键值对,因此这种 RDD 也被称为 PairRDD,泛型为 RDD[(K,V)]。
而普通 RDD 的数据类型为 Int、String 等。这种数据结构决定了 PairRDD 可以使用某些基于键的算子,如分组、汇总等。
PairRDD 可以由普通 RDD 转换得到:
//val spark: SparkSession = .......
val a = spark.sparkcontext.textFile("/user/me/wiki").map(x => (x,x))