虽然是一名后端开发,由于工作需要,大数据的知识后续需要用到,之前学习过一阵hadoop相关的知识。后续有空将继续深入了解大数据的技术栈。边学习,边总结,希望不会半途而废。
Apache Spark是一个开源的分布式计算框架,Spark被设计用于处理大规模数据处理任务,可以在大规模数据集上进行快速的批处理、流处理和机器学习等操作。
spark官网:http://spark.incubator.apache.org/
Spark在速度、处理模型和编程语言等方面比Hadoop更为灵活和高效,同时Spark也与Hadoop兼容,可以与Hadoop组件一起使用,为大数据处理提供更多的选择和灵活性。
除了这些核心模块外,Spark还提供了许多其他的模块和扩展,例如:
后续我们将具体实操下几种常见方式
在Spark的运行架构中,驱动器程序和Executor之间通过网络通信来传递数据和任务,集群管理器负责调度资源和管理集群。由于Spark采用的是内存计算,因此可以大大提高数据处理和分析的速度和效率。
在Spark中,Executor是运行在工作节点上的进程,它们负责执行驱动器程序分配给它们的任务,并将任务执行结果返回给驱动器程序。每个Executor都有自己的JVM进程,可以在任务执行期间缓存数据,从而加速任务执行速度。
而Core(核)是Executor中的计算资源单元,每个Executor都由多个Core组成。Core的数量通常由硬件配置决定,例如一个节点有16个CPU核心,则可以将Executor配置为使用8个核心。
在Spark中,Executor的数量和每个Executor所使用的Core数量可以通过配置文件来进行设置。通过增加Executor的数量和每个Executor所使用的Core的数量,可以提高Spark应用程序的并行度和运行速度。然而,如果Executor数量过多或者每个Executor所使用的Core数量过多,可能会导致资源浪费或者资源不足,从而影响Spark应用程序的性能和稳定性。因此,在设置Executor数量和Core数量时需要根据具体的应用场景和硬件配置进行调整。
应用程序相关启动参数如下:
名称 | 说明 |
---|---|
–num-executors | 配置Executor的数量 |
–executor-memory | 配置每个Executor的内存大小 |
–executor-cores | 配置每个Executor的虚拟CPU core数量 |
并行度(Parallelism)是指同时处理多个任务或数据的能力。在计算机科学中,它通常用于描述一个应用程序或系统可以同时处理多少个任务或数据。在Spark中,并行度是指同时处理Spark应用程序中的多个任务或数据的能力,通常使用Executor的数量和每个Executor所使用的Core的数量来表示。
在Spark应用程序中,提高并行度可以提高任务执行的速度和效率,从而加快数据处理和分析的速度。在实际应用中,提高并行度的方法主要有以下几种:
有向无环图(Directed Acyclic Graph,简称DAG)是一种由有向边连接的节点组成的图形结构,在该结构中不存在环(即任意节点通过边连接不能构成一个环),通常用于描述计算流程或数据处理过程中的依赖关系。
在Spark中,DAG是一个非常重要的概念,它用于描述Spark应用程序中的数据处理流程。在Spark应用程序中,DAG由一系列的RDD(Resilient Distributed Datasets)和转换操作组成。每个RDD代表一个分布式的数据集,而转换操作则用于对RDD进行变换和计算。Spark应用程序中的每个操作都会生成一个新的RDD,并将其加入到DAG中。DAG中的每个节点表示一个RDD,每个有向边表示一个转换操作。
在Spark应用程序中,DAG的生成和优化是由Spark的任务调度器负责的。当Spark应用程序被提交到集群上运行时,Spark会将应用程序中的每个操作转化为一组任务,然后将任务按照依赖关系组织成一个DAG。然后,Spark会对DAG进行优化,如合并相邻的操作,移除无用的操作等,以便于提高任务的执行效率和并行度。最后,Spark会将DAG拆分成多个阶段,每个阶段都可以并行执行,从而提高任务执行的效率。
简单来说,DAG是一种图形结构,用于描述数据处理流程中的依赖关系,而在Spark中,DAG则用于描述RDD之间的转换关系和操作顺序,通过DAG可以优化任务的执行计划,提高任务的并行度和执行效率。
在 client 模式下,Driver 程序运行在提交 Spark 应用程序的客户端进程中,而 Executor 程序运行在集群中的计算节点上。这种提交方式通常用于调试和开发环境中,因为可以更方便地查看和调试应用程序的运行状态和结果。
在 cluster 模式下,Driver 程序运行在集群中的某个节点上,而 Executor 程序也在集群中的其他节点上运行。这种提交方式通常用于生产环境中,因为可以更好地利用集群资源,提高任务的并行度和执行效率。
国内工作中,将Spark引用部署到Yarn环境中会更多一些,所以本课程中的提交流程是基于Yarn环境的。
RDD(Resilient Distributed Datasets):RDD是Spark中最基础的分布式数据结构之一,它是一个不可变的分布式对象集合。RDD中的每个分区都存储着一部分数据,并且分布在集群的多个节点上。RDD提供了许多转换操作和行动操作,可以对其进行变换和计算。
累加器(Accumulator):累加器是一种特殊的变量,在分布式环境下可以进行并行操作。累加器只能进行加法操作,而且只能由Driver端向Executor端累加数据,不能反过来。累加器主要用于计数、求和等聚合操作。
广播变量(Broadcast Variable):广播变量是一种可以在集群中共享的只读变量。在分布式计算过程中,如果有一些变量需要在多个节点之间共享,可以将这些变量使用广播变量的方式进行传输。广播变量只会被发送一次,然后在Executor端被缓存起来供后续使用,可以减少网络传输和内存占用。
RDD(Resilient Distributed Datasets)是Spark中最基础的分布式数据结构之一,它是一个不可变的分布式对象集合。RDD中的每个分区都存储着一部分数据,并且分布在集群的多个节点上。RDD提供了许多转换操作和行动操作,可以对其进行变换和计算。
RDD的特点如下:
在Yarn环境中,Spark的RDD工作原理如下:
val rdd = sc.parallelize(1 to 100)
val rdd = sc.textFile("hdfs://localhost:9000/data.txt")
val rdd1 = sc.parallelize(1 to 100)
val rdd2 = rdd1.map(_ * 2)
val conf = new Configuration()
val file = sc.newAPIHadoopFile("hdfs://localhost:9000/data.txt", classOf[TextInputFormat], classOf[LongWritable], classOf[Text], conf)
val rdd = file.map(pair => pair._2.toString)
转换算子 | 含义 |
---|---|
map(func) | 返回一个新的 RDD,该 RDD 由每一个输入元素经过 func 函数转换后组成 |
filter(func) | 返回一个新的 RDD,该 RDD 由经过 func 函数计算后返回值为 true 的输入元素组成 |
flatMap(func) | 类似于 map,但是每一个输入元素可以被映射为 0 或多个输出元素(所以 func 应该返回一个序列,而不是单一元素) |
mapPartitions(func) | 类似于 map,但独立地在 RDD 的每一个分片上运行,因此在类型为 T 的 RDD 上运行时,func 的函数类型必须是 Iterator[T] => Iterator[U] |
mapPartitionsWithIndex(func) | 类似于 mapPartitions,但 func 带有一个整数参数表示分片的索引值,因此在类型为 T 的 RDD 上运行时,func 的函数类型必须是(Int, Interator[T]) => Iterator[U] |
sample(withReplacement, fraction, seed) | 根据 fraction 指定的比例对数据进行采样,可以选择是否使用随机数进行替换,seed 用于指定随机数生成器种子 |
union(otherDataset) | 对源 RDD 和参数 RDD 求并集后返回一个新的 RDD |
intersection(otherDataset) | 对源 RDD 和参数 RDD 求交集后返回一个新的 RDD |
distinct([numTasks])) | 对源 RDD 进行去重后返回一个新的 RDD |
groupByKey([numTasks]) | 在一个(K,V)的 RDD 上调用,返回一个(K, Iterator[V])的 RDD |
reduceByKey(func, [numTasks]) | 在一个(K,V)的 RDD 上调用,返回一个(K,V)的 RDD,使用指定的 reduce 函数,将相同 key 的值聚合到一起,与 groupByKey 类似,reduce 任务的个数可以通过第二个可选的参数来设置 |
aggregateByKey(zeroValue)(seqOp, combOp, [numTasks]) | 对 PairRDD 中相同的 Key 值进行聚合操作,在聚合过程中同样使用了一个中立的初始值。和 aggregate 函数类似,aggregateByKey 返回值的类型不需要和 RDD 中 value 的类型一致 |
sortByKey([ascending], [numTasks]) | 在一个(K,V)的 RDD 上调用,K 必须实现 Ordered 接口,返回一个按照 key 进行排序的(K,V)的 RDD |
sortBy(func,[ascending], [numTasks]) | 与 sortByKey 类似,但是更灵活 |
join(otherDataset, [numTasks]) | 在类型为(K,V)和(K,W)的 RDD 上调用,返回一个相同 key 对应的所有元素对在一起的(K,(V,W))的 RDD |
cogroup(otherDataset, [numTasks]) | 在类型为(K,V)和(K,W)的 RDD 上调用,返回一个(K,(Iterable,Iterable))类型的 RDD |
cartesian(otherDataset) | 笛卡尔积 |
pipe(command, [envVars]) | 对 rdd 进行管道操作 |
coalesce(numPartitions) | 减少 RDD 的分区数到指定值。在过滤大量数据之后,可以执行此操作 |
repartition(numPartitions) | 重新给 RDD 分区 |
动作算子 | 含义 |
---|---|
reduce(func) | 通过 func 函数聚集 RDD 中的所有元素,这个功能必须是可交换且可并联的 |
collect() | 在驱动程序中,以数组的形式返回数据集的所有元素 |
count() | 返回 RDD 的元素个数 |
first() | 返回 RDD 的第一个元素(类似于 take(1)) |
take(n) | 返回一个由数据集的前 n 个元素组成的数组 |
takeSample(withReplacement,num, [seed]) | 返回一个数组,该数组由从数据集中随机采样的 num 个元素组成,可以选择是否用随机数替换不足的部分,seed 用于指定随机数生成器种子 |
takeOrdered(n, [ordering]) | 返回自然顺序或者自定义顺序的前 n 个元素 |
saveAsTextFile(path) | 将数据集的元素以 textfile 的形式保存到 HDFS 文件系统或者其他支持的文件系统,对于每个元素,Spark 将会调用 toString 方法,将它装换为文件中的文本 |
saveAsSequenceFile(path) | 将数据集中的元素以 Hadoop sequencefile 的格式保存到指定的目录下,可以使 HDFS 或者其他 Hadoop 支持的文件系统 |
saveAsObjectFile(path) | 将数据集的元素,以 Java 序列化的方式保存到指定的目录下 |
countByKey() | 针对(K,V)类型的 RDD,返回一个(K,Int)的 map,表示每一个 key 对应的元素个数 |
foreach(func) | 在数据集的每一个元素上,运行函数 func 进行更新 |
foreachPartition(func) | 在数据集的每一个分区上,运行函数 func |
算子 | 含义 |
---|---|
count | 个数 |
mean | 均值 |
sum | 求和 |
max | 最大值 |
min | 最小值 |
variance | 方差 |
sampleVariance | 从采样中计算方差 |
stdev | 标准差:衡量数据的离散程度 |
sampleStdev | 采样的标准差 |
stats | 查看统计结果 |
RDD序列化是Spark中的一个重要概念,它是指将RDD中的数据对象转换为字节流的过程,以便在不同的节点之间进行网络传输或磁盘存储。在Spark中,需要对RDD进行序列化是因为RDD在分布式计算中需要在多个节点之间传输和存储,而这些节点的操作系统和硬件环境可能不同,因此需要将RDD数据对象进行序列化,以便能够在不同的节点之间传输和存储。
Spark支持两种类型的RDD序列化方式:Java序列化和Kryo序列化。Java序列化是JVM自带的序列化机制,它具有通用性,但是效率较低。Kryo序列化是一个高性能的序列化库,它能够将对象序列化成较小的字节数组,从而提高网络传输和磁盘存储的效率。在使用Kryo序列化时,需要先注册需要序列化的类,以便Kryo能够正确地序列化和反序列化这些对象。
在Spark中,默认情况下使用Java序列化方式,但是可以通过设置SparkConf的“spark.serializer”属性来指定使用Kryo序列化方式,例如:
val conf = new SparkConf()
conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
val sc = new SparkContext(conf)
在Spark中,RDD的依赖关系是指一个RDD与其他RDD之间的关系,包括它们之间的转换操作和依赖类型。根据依赖类型的不同,RDD的依赖关系可以分为两种:宽依赖和窄依赖。
窄依赖(Narrow Dependency):指一个RDD的每个分区只依赖于另一个RDD的一个或多个分区。例如,map、filter等转换操作都是窄依赖。
宽依赖(Wide Dependency):指一个RDD的每个分区依赖于另一个RDD的多个分区,即一个RDD的每个分区需要与另一个RDD的所有分区进行计算。例如,reduceByKey、groupByKey等转换操作都是宽依赖。
在Spark中,RDD的依赖关系是以有向无环图(DAG)的形式组织起来的。每个RDD都有一组父RDD和一组子RDD,每个父RDD与子RDD之间都有一条有向边来表示依赖关系。在一个DAG中,每个RDD都是通过一系列转换操作从原始数据集导出的,最终得到的RDD被称为输出RDD。
在Spark中,DAG的构建是惰性的,也就是说,只有在需要执行操作时才会计算DAG。这种惰性计算的好处是能够避免不必要的计算,提高计算效率。同时,由于DAG是有向无环图,因此可以通过优化DAG来减少计算的开销,提高计算性能。
在Spark中,RDD持久化(Persistence)指的是将RDD的数据缓存到内存或磁盘中,以便后续重复使用,可以使用RDD的persist()方法或cache()方法实现。这两个方法的作用是一样的,都可以将RDD的数据缓存到内存或磁盘中。
这两个方法的使用方式非常简单,只需要在RDD上调用persist()方法或cache()方法,并指定缓存的级别即可。例如:
val rdd = sc.parallelize(Seq(1, 2, 3))
rdd.persist(StorageLevel.MEMORY_ONLY)
或者:
val rdd = sc.parallelize(Seq(1, 2, 3))
rdd.cache()
其中,StorageLevel.MEMORY_ONLY指定了缓存级别为内存缓存,表示将RDD的数据存储在内存中。除了MEMORY_ONLY外,Spark还支持其他多种缓存级别,如MEMORY_ONLY_SER、MEMORY_AND_DISK、MEMORY_AND_DISK_SER等,可以根据实际需求进行选择。
需要注意的是,持久化RDD需要消耗内存或磁盘空间,因此需要根据实际情况来选择合适的缓存级别。另外,可以使用unpersist()方法来释放缓存的RDD数据,例如:
rdd.unpersist()
当调用persist()或cache()方法后,Spark会根据缓存级别和当前的可用内存或磁盘空间来决定RDD数据是放在内存中还是磁盘中。如果数据太大无法全部缓存到内存中,Spark会按照缓存级别和LRU算法来淘汰不常用的数据。如果数据在内存中被淘汰,则后续使用该数据时需要重新计算。
在Spark中,RDD的分区(Partition)是指将一个RDD的数据集划分成若干个小的数据块,每个数据块称为一个分区,每个分区都可以被一个Task并行处理。RDD的分区是Spark实现高效计算的关键,因为它可以将数据并行处理,从而提高计算性能。
而RDD分区器(Partitioner)则是对RDD的分区进行进一步的优化,即对数据进行重新分区,以便更好地利用Spark的并行计算能力。RDD分区器主要用于控制RDD的数据如何被分配到不同的节点上进行计算,从而更好地利用集群的资源。
在Spark中,RDD分区器分为两种类型:哈希分区器和范围分区器。哈希分区器(HashPartitioner)是根据键的哈希值来对RDD进行分区,而范围分区器(RangePartitioner)则是根据键的范围来对RDD进行分区。对于哈希分区器,Spark默认使用的是HashPartitioner,而对于范围分区器,Spark会根据数据的范围和RDD的分区数自动选择使用RangePartitioner。
在使用RDD的groupByKey、reduceByKey等聚合操作时,需要使用到哈希分区器。在使用RDD的sortByKey、join等操作时,需要使用到范围分区器。可以通过对RDD调用partitionBy方法来指定分区器,例如:
val rdd = sc.parallelize(Seq(("a", 1), ("b", 2), ("c", 3)))
val partitioner = new HashPartitioner(2)
val partitionedRDD = rdd.partitionBy(partitioner)
在上面的例子中,我们创建了一个包含三个元素的RDD,并使用HashPartitioner对其进行分区,分为两个分区。其中,HashPartitioner的构造函数需要指定分区数,这里指定为2。然后,我们使用RDD的partitionBy方法将RDD重新分区,并指定使用HashPartitioner进行分区。这样,RDD的数据就会被重新分配到两个节点上,以便更好地利用集群的资源进行计算。
在Spark中,可以使用RDD的读取和保存操作,从而将数据加载到RDD中,或将RDD中的数据保存到外部存储系统中。常用的RDD读取和保存方式包括:
从文件中读取数据
可以使用sc.textFile()方法从文件中读取数据,例如:
val rdd = sc.textFile("path/to/file")
其中,path/to/file指定要读取的文件路径,该方法返回一个包含文件中所有行的RDD。
将RDD保存到文件中
可以使用RDD.saveAsTextFile()方法将RDD中的数据保存到文件中,例如:
val rdd = sc.parallelize(Seq("Hello", "World", "Spark"))
rdd.saveAsTextFile("path/to/output")
其中,path/to/output指定要保存的文件路径,该方法将RDD中的数据保存为文本格式。
从Hadoop文件系统中读取数据
可以使用sc.hadoopFile()方法从Hadoop文件系统中读取数据,例如:
val rdd = sc.hadoopFile("path/to/hdfs/file", classOf[TextInputFormat], classOf[LongWritable], classOf[Text])
其中,path/to/hdfs/file指定要读取的Hadoop文件路径,TextInputFormat指定要读取的文件格式,LongWritable和Text分别指定键和值的类型,该方法返回一个包含Hadoop文件中所有行的RDD。
将RDD保存到Hadoop文件系统中
可以使用RDD.saveAsHadoopFile()方法将RDD中的数据保存到Hadoop文件系统中,例如:
val rdd = sc.parallelize(Seq("Hello", "World", "Spark"))
rdd.saveAsHadoopFile("path/to/hdfs/output", classOf[Text], classOf[Text], classOf[TextOutputFormat])
其中,path/to/hdfs/output指定要保存的Hadoop文件路径,Text指定要保存的键和值的类型,TextOutputFormat指定要保存的文件格式,该方法将RDD中的数据保存为Hadoop文件格式。