Apache Spark基础及架构

Apache Spark是一款高效的大数据处理引擎,以其快速、易用、通用和多模式运行而著称。相较于MapReduce,Spark利用内存计算大幅提升性能,提供丰富API简化开发。Spark的组件包括Spark Core、Spark SQL、Spark Streaming、MLlib和GraphX,覆盖批处理、流处理和机器学习等领域。Spark架构中,SparkContext是核心入口,SparkSession是2.0后的统一编程接口。RDD是其核心数据结构,具备分区、compute函数、依赖、分区器和位置列表等特性。Spark支持多种运行模式,如Standalone、Yarn和Mesos。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

认识Spark

Apache Spark是大数据领域最活跃的项目之一,其活跃度目前远超Hadoop。特点是快速、易用、通用及多种运行模式。
1、快速
Spark是面向内存的大数据处理引擎,这使得Spark能够为多个不同数据源的数据提供近乎实时的处理性能,是用于需要多次操作特定数据集的应用场景。
面向磁盘的MapReduce受限于磁盘读/写性能和网络I/O性能的约束,在处理迭代计算、实时计算、交互式数据查询等方面并不高效,但是这些却在图计算、数据挖掘和机器学习等相关应用领域中非常常见。针对这一不足,将数据存储在内存中并基于内存进行计算是一个有效的解决途径。
2、易用
一方面,Spark提供了支持多种语言的API,如Scala、Java、Python、R等,使得用户开发Spark程序十分方便。另一方面,Spark是基于Scala语言开发的,由于Scala是一种面向对象的、函数式的静态编程语言,其强大的类型推断、模式匹配、隐式转换等一系列功能结合丰富的描述能力使得Spark应用程序代码非常简洁。Spark的易用性还体现在其针对数据处理提供了丰富的操作。
在使用MapReduce开发应用程序时,通常用户关注的重点与难点是如何将一个需求job(作业)拆分成两个操作,即Map与Reduce,因此系统开发人员需要解决的一个难题是如何把数据处理的业务逻辑合理有效的封装在对应的两个类中国。与之相对比,Spark提供了80多个针对数据处理的基本操作,如map、flatMap、reduceByKey、filter、cache、collect、textfile等,这使得用户基于Spark进行应用程序开发非常简洁高效。以分词统计为例,虽然MapReduce固定的编程模式极大的简化了并行程序开发,但是代码至少几十行,若换成Spark,其核心代码最短仅需要一行。
关于开发语言的选择问题,在Spark的实际项目中多用Scala语言,约占70%,其次是Java,约占20%,而Python约占10%。
3、通用
位于底层的是Spark Core,其实现了Spark的作业调度、内存管理、容错、与存储系统交互等基本功能,并针对弹性分布式数据提供了丰富的操作。在Spark Core的基础上,Spark提供了一系列面向不同应用需求的组件,主要有Spark SQL、Spark Streaming、MLlib、GraphX。将这些组件放在一起,构成了Spark软件栈。基于这个软件栈,Spark提出并实现了大数据处理的一种理念——“一栈式解决方案”,即Spark可同时对大数据进行批处理、流式处理和交互式查询,如下图所示,借助于这一软件栈用户可以简单而低耗的把各种处理流程综合在一起,充分体现了Spark的通用性。
在这里插入图片描述
4、多种运行模式
Spark支持多种运行模式:本地local运行模式、分布式运行模式。Spark集群的底层资源可以借助于外部的框架进行管理,目前Spark对Mesos和Yarn提供了相对稳定的支持。在实际生产环境中,中小规模的Spark集群通常可满足一般企业大多数的业务需求,而在搭建此类集群时推荐采用Standalone模式(不采用外部的资源管理框架)。该模式使得Spark集群更加轻量级。
Spark on Yarn模式:在这一模式下,Spark作为一个提交程序的客户端将Spark任务提交到Yarn上,然后通过Yarn来调度和管理Spark任务执行过程中所需的资源。在搭建此模式的Spark集群过程中,需要先搭建Yarn集群,然后将Spark作为Hadoop中的一个组件纳入到Yarn的调度管理下,这样将更有利于系统资源的共享。
Spark on Mesoes模式:Spark和资源管理框架Mesos相结合的运行模式。Apache Mesos与Yarn类似,能够将CPU、内存、存储等资源从计算机的物理硬件中抽象的隔离出来,搭建了一个高容错、弹性配置的分布式系统。Mesos同样也采用Master/Slave架构,并支持粗粒度模式和细粒度模式两种调度模式。
Spark Standalone模式:该模式是不借助于第三方资源管理架构的完全分布式模式。Spark使用自己的Master进程对应用程序运行过程中所需的资源进行调度和管理。对于中小规模的Spark集群首选Standalone模式。

Spark与MapReduce比较

对比MapReduce与Spark的主要区别
1)易用性
由于MapReduce中仅为数据处理提供了两个操作,即Map和Reduce,因此系统开发人员需要解决的一个难题是如何把数据处理的业务逻辑合理有效的封装在对应的两个类中。而通常同样的功能若换成Spark,其核心代码最短仅需要一行。
MapReduce自身并没有交互模式,需要借助Hive和Pig等附加模块。Spark则提供了一种命令行交互模式,即Spark Shell,使得用户可以获取到查询和其他操作的及时反馈。
2)效率
Map中间结果写入磁盘,效率低下,不适合迭代运算。Spark job中间输出结果可以保存在内存,不再需要读写HDFS。
3)任务启动开销
Spark 和Hadoop MapReduce都实现了异步并发模型,而MapReduce采用的是多进程模型,Spark采用了多线程模型。
多进程模型便于细粒度控制每个任务占用的资源,但会消耗较多的启动时间,不适合运行低延迟类型的作业,这是MapReduce广为诟病的原因之一。多线程模型则相反,该模型使得Spark很适合运行低延迟类型的作业。

Spark技术栈

这里对Spark的组件制作概述性介绍
1、Spark Core
核心组件,分布式计算引擎。其实现了Spark的作业调度、内存管理、容错、与存储系统交互等基本功能,并针对弹性分布式数据集提供了丰富的操作。
2、Spark SQL
又一个高性能的基于Hadoop的SQL解决方案。部分用法与Hive非常相似。
3、Spark Streaming
基于Spark Core实现的高吞吐量、具备容错机制的准实时流处理系统。将流式计算分解成一系列小批处理作业,也称微批处理。
4、Spark GraphX
分布式图处理框架,支持图并行计算。现在已经提供了很多算法,新的算法还在不断加入。
5、Spark MLlib
构建在Spark上的分布式机器学习库。是Spark对常用的机器学习算法的实现库,还提供了相关的测试与数据生成器。

了解Spark架构与运行环境

Spark环境部署

Spark架构

先了解Spark的架构中的术语。具体解释术语时,结合“WordCount”示例在运行架构图中指出各部分代码对应的组件。
Application:建立在Spark上的用户程序,包括Driver代码和运行在集群各节点Executor中的代码。
Driver program:驱动程序。Application中的main函数并创建SparkContext。
Cluster Manager:在集群(Standalone、Mesos、YARN)上获取资源的外部服务。
Worker Node:集群中任何可以运行Application代码的节点。
Executor:某个Application运行在worker节点上的一个进程。
Task:被送到某个Executor上的工作单元。
job:包含多个Task组成的并行计算,往往由Spark Action算子触发生成,一个Application中往往会产生多个job。
Stage:每个job会被拆分为多组Task,作为一个TaskSet,其名称为Stage。
在这里插入图片描述
重点注意:
SparkRDD包括四类算子:创建算子、转换算子、持久化算子和Action算子。一个Action算子对应一个job,其余的算子对应一个Task。

Spark编程入口

在开发过程中,常用的API主要有:SparkContext、SparkSession、RDD、DataSet及DataFrame,这里主要介绍SparkContext、SparkSession。因为RDD、DataSet及DataFrame有一定的相似之处

SparkContext

从前面Spark运行架构图中可以看出,SparkContext是连接Driver、Worker以及Cluster Manager(Master)的桥梁。作为Spark应用程序的核心、编程入口,SparkContext主要完成了如下工作。
在这里插入图片描述
具体可自行查看SparkContext源码,如下图所示。
这里对主要功能进行解释:
RDD graph:生成RDD依赖关系图DAG。SparkContext会根据用户提交的计算逻辑中的RDD的转换和动作来生成RDD之间的依赖关系,同时这个计算链也就生成了逻辑上的DAG。
DAGScheduler:为高级的、基于Stage的调度器,负责创建job,将DAG中的RDD划分到不同的Stage,并将Stage作为Tasksets提交给底层调度器TaskSheduler执行。
TaskScheduler:为Spark的任务调度器,Spark通过它提交任务并且请求集群调度任务。因其调度的Task由DAGScheduler创建,所以DAGSheduler是TaskScheduler的前置调度。
SchedulerBackend:是一个trait,作用是分配当前可用的资源。具体即向当前等待分配计算资源的Task分配计算资源(即Executor)。
ListenerBus:SparkContext中的事件总线,可以接收各种使用方的事件,并且异步传递Spark事件监听与SparkListeners监听器的注册。
BlockManager:属于SparkEnv组件中的成员,一个嵌入在spark中的key-value型分布式存储系统(类似HDFS)。SparkEnv是SparkContext中非常重要的类,它维护着Spark的执行环境,所有的线程都可以通过SparkContext访问到同一个SparkEnv对象。SparkEnv还包括ShuffleManager、SecurityManager、CacheManager等。

SparkSession

SparkSession是SparkSQL的入口,是在2.0中引入的新的API。旨在为Spark编程提供统一的编程入口,意味着SparkSession整合了SparkConf、SparkContext、SQLContext、HiveContext以及StreamingContext,其中SQLContext、HiveContext是为了保持兼容被保留,StreamingContext在未来可能会被加入。
可以发现,当创建SparkSession对象后,可以间接拿到sparkContext和sqlContext对象。所以在2.0版本后推荐使用SparkSession作为编程入口。
在2.0之前的spark版本中,spark shell会自动创建一个SparkContext对象sc,2.0+中Spark shell则会额外创建一个SparkSession对象spark。如下图所示。
在这里插入图片描述
手动创建SparkSession:

import org.apache.spark.SparkSession
val spark = SparkSession.builder.master("local[2]").appName("appName").getOrCreate()

注意,SparkSession封装许多隐式转换,如RDD -> DataSet。前面提到需要导入隐式转换到当前作用域才能生效。SparkSession使用implicits(单例对象)进行封装,SparkSession是类,所以在导入时使用如下方式:

import spark.implicits._   // 假设SparkSession的实例为spark

理解Spark核心数据结构——RDD

RDD概念

RDD称为弹性分布式数据集,它是一种分布式的内存抽象,允许在大型集群上执行基于内存的计算,为用户屏蔽了底层复杂的计算和映射环境。
1、简单的解释
RDD是将数据项拆分为多个多区的集合,存储在集群的工作节点上的内存和磁盘中,并执行正确的操作。
2、复杂的解释
1)RDD用于数据转换的接口,比如map、filter、groupBy、join等
2)RDD指向了存储在HDFS、Cassandra、HBase等、或缓存(内存、内存+磁盘、仅磁盘等),或在故障或缓存收回时重新计算其他RDD分区中的数据。从这个意义上讲,RDD不包含任何待处理数据。

RDD特性

RDD有五个重要属性
RDD是个抽象类,实现类有很多。
他们都需要实现或重写compute、getPartitions等方法。这些方法分别表示什么含义呢?主要便是RDD的五大属性:分区、分区计算函数、依赖、分区器及分区优先位置列表。

分区

RDD是由多个分区构成的。RDD#partitions返回RDD的所有分区信息。每个Partition都有一个唯一索引编号,可通过partition#index访问。RDD分区概念与MapReduce的输入切片概念类似。对每个分区的运算会被一个当做一个Task执行,换句话说,分区是Spark任务执行的 基本单位。举例:如果有100个分区,那么RDD上有n个操作将会产生n*100个任务。
我们使用分区的时候了解两条规则:
1)只有key-value类型的RDD才有分区器,非key-value类型的RDD(PairRDD)分区器的值是None。
2)每个RDD的分区ID范围:0~numPartitions-1,决定这个值是属于哪个分区的。

compute函数

RDD的每个分区上都有一个函数去作用,Spark中的RDD的计算是以分区为单位的,每个RDD都会实现compute函数已达到这个目的。不同的RDD的compute函数逻辑各不一样,比如:
1、MapPartitionsRDD的compute是将用户的转换逻辑作用到指定的Partition上。因为RDD的map算子产生MapPartitionsRDD,而map算子的参数(具体操作逻辑)是变化的。
2、HadoopRDD的compute是读取指定Partition数据。因为“sc.hadoopFile(“path”)”读取HDFS文件返回的RDD具体类型便是HadoopRDD,所以只需要读取数据即可。
3、CheckpointRDD的compute是直接读取检查点的数据。一旦RDD进行checkpoint,将变成CheckpointRDD。

RDD间的依赖

RDD有依赖性,通常情况下一个RDD是来源于另一个RDD,这个叫做 lineage。RDD会记录下这些依赖,方便容错。也称DAG,RDD所依赖的其他RDD作为构造器参数之一。

分区器

RDD的分区器是一个可选项,如果RDD里面存的数据key-value形式,则可以传递一个自定义的Partitioner进行重新分区,例如这里自定义的Partitioner是基于key进行分区,那则会将不同RDD里面的相同key的数据放到同一个Partition里面。
Spark包含两种数据分区方式:HashPartitioner(哈希分区)和RangePartitioner(范围分区)。
HashPartitioner是默认的分区方式,其算法逻辑是:
partition = key.hashCode() % numPartitions
RangePartitioner通过两个步骤来实现:
第一步:先重整个RDD中抽取出样本数据,将样本数据排序,计算出每个分区的最大key值,形成一个Array[KEY]类型的数组变量rangeBounds;
第二步:判断key在rangeBounds中所处的范围,给出该key值在下一个RDD中的分区id下标;该分区器要求RDD中的Key类型必须是可以排序的。

分区优先位置列表

该列表存储了存取每个分区的优先位置。对于一个HDFS文件来说,这个列表保存了每个分区所在的数据块的位置。按照“移动数据不如移动计算”的理念,Spark在进行任务调度的时候,会尽可能的将计算任务移动到所要处理的数据块的存储位置。
preferredLocations返回每个分区所在的机器名或者IP地址,如果分区数据是多份存储,那么返回多个机器地址。
在这里插入图片描述

掌握RDD的数据变换及操作

创建算子

1、使用集合创建RDD
通过集合创建RDD有两种方法:parallelize和makeRDD。
parallelize源码:

def parallelize[T: ClassTag]( 
	seq: Seq[T], 
	numSlices: Int = defaultParallelism): RDD[T] = withScope { 
		assertNotStopped() 
		new ParallelCollectionRDD[T](this, seq, numSlices, Map[Int, Seq[String]]()) 
	}

makeRDD源码:

def makeRDD[T: ClassTag]( 
	seq: Seq[T], 
	numSlices: Int = defaultParallelism): RDD[T] = withScope { 
		parallelize(seq, numSlices) 
	}

两个函数接口功能类似。不同的是makeRDD多一个重载方法:

def makeRDD[T:ClassTag] (seq:Seq[T,Seq[String]]):RDD[T] = withScope {
	assertNotStopped()
	val indexToPrefs = seq.zipWithIndex.map(t => (t._2,t._1._2)).toMap
	new ParallelCollectionRDD[T](this,seq.map(_._1), math.max(seq.size,1), indexToPrefs)
}

该makeRDD的重载分配一系列本地Scala集合形成一个RDD,可以为每个集合对象创建一个分区,并指定优先位置便于在运行中优化调度。
使用本地集合创建RDD的问题在于:由于这种方法需要用到一台机器中集合的全部数据,所以这种方式在测试和圆形构造之外很少使用。
2、使用外部存储创建RDD
任何Hadoop支持的存储类型都可以用于创建RDD,包括:本地文件系统、HDFS、HBase、Cassandra等。
对于本地文件、HDFS及Hadoop支持的文件系统使用textFile创建RDD,其中文件中每一行作为RDD中的一条数据。
textFile源码:

def textFile( 
	path: String, 
	minPartitions: Int = defaultMinPartitions): RDD[String] = withScope { 
		assertNotStopped() 
		hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text], minPartitions).map(pair => pair._2.toString).setName(path) 
}

关于分区数量:
1)默认情况下不能超过2。原因如下图所示。

def defaultMinPartitions: Int = math.min(defaultParallelism,2)
override def defaultParallelism(): Int = {
	conf.getInt("spark.default.parallelism", math.max(totalCoreCount.get(), 2))
}

其中,totalCoreCount是一个来跟踪集群中的核心总数原子变量。
2)对HDFS文件,Spark为每一个文件块创建一个分区。
3、从其他RDD创建
从已有RDD创建新的RDD通过转换算子实现。通常涉及:map、filter、count、distinct、flatMap等,这些操作与Scala集合操作类似。

转换算子

对于转换操作,RDD的所有转换都不会直接计算结果。Spark仅记录作用于RDD上的转换操作逻辑,当遇到动作算子时才会进行真正计算。RDD全部转换算子如下表:

Transformation描述
map(func)通过函数func作用于源RDD中的每个元素,返回一个新的RDD
filter(func)选择源RDD中的使得函数func为true的元素,返回一个新的RDD
flatMap(func)与map类似,但是每个输入项可以映射到0或多个输出项(因此func应该返回一个Seq,而不是单个项)
mapPartitions(func)与map类似,但是在RDD的每个分区上单独运行,所以func在类型为T的RDD上运行时,必须是类型 Iterator => Iterator
mapPartitionsWithindex(func)与mapPartitions类似,但为func多提供一个分区编号,所以func类型为:(Int, Iterator) = Iterator
sample(withReplacement, fraction, seed)使用给定的随机数生成器种子对数据的一部分进行采样
union(otherDataset)返回一个新数据集,该数据集包含源数据集中的元素和参数的并集
intersection(otherDataset)返回一个新的RDD,其中包含源数据集中的元素和参数的交集
distinct([numPartitions])返回包含源数据集的不同元素的新数据集
groupByKey([numPartitions])当调用一个(K, V)对的数据集时,返回一个(K, Iterable)对的数据集
aggregateByKey(zeroValue)(seqOp, combOp, [numPartitions])seqOp操作会聚合个分区中的元素,然后combOp操作把所有分区的聚合结果再次聚合,两个操作的初始值都是zeroValue,seqOp的操作是遍历分区中的所有元素(T), 第一个T跟zeroValue做操作,结果再作为与第二个T做操作的zeroValue,直到遍历完整个分区。combOp操作是把各分区聚合的结果,再聚合。
sortByKey([ascending], [numPartitions])根据key进行排序,默认为升序。ascending: Boolean = true
join(otherDataset, [numPartitions])当在类型(K, V) 和(K, W) 的数据集上调用时,返回一个(K, (V, W))对的数据集,其中包含每个键的所有对元素。外部连接由leftOuterJoin、rightOuterjoin 和 fullOuterJoin支持。
cogroup(otherDataset, [numPartitions])当调用类型(K, V) 和(K, W) 的数据集时,返回一个(K, (Iterator, Iterator)) 元组的数据集,这个操作称为groupWith。
cartesian(otherDataset)在类型为T 和U 的数据集上调用时,返回一个(T, U) 对(所有对元素)的数据集。
pipe(command, [envVars])通过shell命令(例如Perl或bash脚本)对RDD 的每个分区进行管道传输,将RDD元素写入进程的stdin,并将其输出到stdout的行为字符串RDD返回。
coalece(numPartitions)将RDD中的分区数量减少到numpartition。
repartition(numPartitions)随机的重新Shuffle RDD中的数据,一创建更多或更少的分区,并在它们之间进行平衡。

持久化算子

持久化算子包括:cache、persist及checkpoint。

行动算子

Action描述
reduce(func)使用函数func(它接受两个参数并返回一个)聚合数据集的元素
collect()在驱动程序(Driver)中以数组的形式返回数据集的所有元素。
count()返回数据集中的元素数量
first()返回数据集中的第一个元素(类似于take(1) )
take(n)返回一个包含数据集前n个元素的数组
takeSample(withReplacement, num, [seed])返回一个数组,其中包含数据集的随机num元素样本,可以替换,也可以不替换,可以预先指定随机数生成器种子。
takeOrdered(n, [ordering])使用RDD的自然顺序或自定义比较器返回RDD的前n个元素。
saveAsTextFile(path)将数据集的元素作为文本文件(或文本文件集)写入本地文件系统、HDFS或任何其他hadoop支持的文件系统的给定目录中。Spark将对每个元素调用toString, 将其转换为文件中的每一行文本。
saveAsSequenceFile(path)将数据集的元素作为Hadoop SequenceFile写入本地文件系统、HDFS或任何其他Hadoop支持的文件系统的给定路径中。这在实现Hadoop的可写接口的键值对的RDDs上是可用的。在scala中,他也可用于隐式转换为可写的类型(Spark包括对Int、Double、String等基本类型的转换)。
saveAsObjectFile(path)使用Java序列化以简单的格式编写数据集的元素,然后可以使用SparkContext.objectFile() 加载这些元素。
countByKey()仅在类型(K,V) 的RDDs上可用。返回(K, Int) 对的Map表示每个键的计数。
foreach(func)对数据集的每个元素运行函数func。

RDD与闭包

Spark的难点之一便是理解跨集群执行代码时变量和方法的范围和生命周期。在范围之外修改变量的RDD操作可能经常会引起混淆。比如,在foreach() 之外修改除累加器之外的其他变量可能会导致未定义的行为。如下代码所示:

var counter = 0
var rdd = sc.parallelize(1 to 100)
rdd.foreach(x => counter += x)
println("Counter value:" + counter)  // 永远为0

上述代码的行为可能无法按预期工作,为了执行作业,Spark将RDD操作的处理分解为任务,每个任务由执行程序执行。在执行之前,Spark计算任务的闭包,闭包是那些执行程序在RDD上执行其计算时必须可见的变量和方法(在本例中为foreach() )。这个闭包被序列化并发送给每个执行器。
传递给每个执行器的比保重的变量现在是副本,因此,当在foreach函数中引用counter时,它不再是驱动节点上的计数器。在驱动节点的内存中仍然有一个计数器,但它对执行器不再可见!执行者只看到来自序列化闭包的副本。因此,counter的最终值仍然是零,因为counter上的所有操作都引用了序列化比保重的值。
为了确保在这类场景中定义良好的行为,应该使用累加器。Spark中的累加器专门用于提供一种机制,以便在集群中的工作节点之间执行分割时安全的更新变量。
一般来说,像循环或局部定义方法这样的闭包结构不应该用来改变全局状态。一些这样做的代码可能在本地模式下工作,但那只是偶然的,而且这样的代码在分布模式下不会像预期的那样工作。如果需要全局聚合,则使用累加器。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值