Spark学习总结

本文为Spark学习总结

一、Spark概述

Apache Spark是用于大数据处理的集群计算框架,它并未使用MapReduce作为执行引擎,而是使用自己的分布式运行环境在集群上执行工作。Spark最突出的表现在于它能将作业与作业之间产生的大规模数据集存储在内存中

Spark还是用于构建分析工具的出色平台。为此,Spark项目包括机器学习(MLlib)、图算法(GraphX)、流计算(Spark Streaming)和SQL查询(Spark SQL)等模块。这些模块本文暂不讨论

二、弹性分布式数据集

弹性分布式数据集(RDD)是Spark最核心的概念,它是在集群中跨多个机器分区存储的一个只读的对象集合

在典型的Spark程序中,首先要加载一个或多个RDD,它们作为输入通过一系列转换得到一组目标RDD,然后对这些目标RDD执行一个动作,例如计算出结果或者写入持久存储器

“弹性”指的是Spark可以通过重新安排计算来自动重建丢失的分区

1)创建
RDD的创建有三种方法:来自一个内存中的对象集合(也称并行化一个集合);使用外部存储器(例如HDFS)中的数据集;对现有的RDD进行转换

2)转换和动作

  • Spark为RDD提供了两大类操作:转换(transformation)和动作(action)。转换是从现有的RDD生成新的RDD,动作则触发对RDD的计算并对计算结果执行某种操作,要么返回给用户,要么保存到外部存储器中
  • 动作的效果是立竿见影的,但转换不是,转换是惰性的
  • 要想判断一个操作是转换还是动作,我们可以观察其返回类型:如果返回的类型是RDD,那么它是一个转换,否则就是一个动作
  • Spark中的MapReduce:Spark中的map()和reduce()操作与Hadoop MapReduce中的同名函数并没有直接的对应关系,想要在Spark中模拟Hadoop MapReduce的一种简单方法是使用两个flatmap()操作,并且两者之间使用groupByKey()和sortByKey()分隔,它们分别执行的是MapReduce的混洗(shuffle)和排序(sort)操作
  • reduceByKey()、foldByKey()或aggregateByKey()比groupByKey()的效率更高,因为它们也可以作为combiner在map任务中运行
  • 聚合转换:按键来为键值对RDD进行聚合操作的三个主要转换函数分别为reduceByKey()、foldByKey()和aggregateByKey()。它们的工作方式稍有不同,但都用于聚合给定键的值,并为每个键产生一个值,其中aggregateByKey()可以改变聚合结果值的类型,前两者不能

3)持久化

  • 调用cache()命令可以将中间数据集缓存到内存中,但并不会立即缓存RDD,相反,它用一个标志来对该RDD进行标记,以指示该RDD应当在Spark作业运行时被缓存
  • 被缓存的RDD只能由同一应用的作业来读取,如果要在应用之间共享数据集,则必须在第一个应用中使用saveAs*()(例如saveAsTextFile()等等)将其写入外部存储器,然后在第二个应用中使用SparkContext的相应方法(例如textFile()等等)进行加载。同理,当应用终止时,它缓存的所有RDD都将被销毁,除非这些RDD已被显示保存,否则无法再次访问
  • 调用cache()将会在executer的内存中持久化保存RDD的每个分区。如果executer没有足够的内存来存储RDD分区,计算并不会失败。只不过是根据需要重新计算分区,对于包含大量转换操作的复杂程序来说,重新计算的代价可能太高,因此Spark提供了不同级别的持久化行为,我们可以通过调用persist()并指定StorageLevel参数来做出选择
  • 默认持久化级别是MEMORY_ONLY,它使用对象在内存中的常规表示方法。对于一种更紧凑的表示方法是通过把分区中的元素序列化为字节数组来实现。这一级别称为MEMORY_ONLY_SER。与MEMORY_ONLY相比,MEMORY_ONLY_SER多了一份CPU开销。但是如果它生成的序列化RDD分区的大小适合被保存到内存中,而常规的表示方法却无法做到这一点,是值得的。MEMORY_ONLY_SER还能减少垃圾回收的压力,因为每个RDD被存储为一个字节数组,而不是大量的对象

4)序列化
默认情况下,Spark在通过网络将数据从一个executer发送到另一个executer时,或者以序列化的形式缓存数据时,所使用的都是Java序列化机制。Java序列化机制为程序员所熟知,但从性能和大小来看,这种做法效率并不高。使用Kryo序列化机制对于大多数Spark程序都是一个更好的选择。Kryo是一个高效通用的Java序列化库

三、共享变量

1)广播变量
广播变量在经过序列化后被发送给各个executer,然后缓存在那里,以便后期任务可以在需要时访问它。它与常规变量不同,常规变量是作为闭包函数的一部分被序列化的,因此它们在每个任务中都要通过网络被传输一次。广播变量的作用类似于MapReduce中的分布式缓存,两者的不同之处在于Spark将数据保存在内存中,只有在内存耗尽才会溢出到磁盘上

2)累加器
广播变量是单向传播的,即从driver到任务,因此一个广播变量是没有办法更新的,也不可能将更新传回driver。要想做到这一点,我们需要累加器。累加器是在任务中只能对它做加法的共享变量,类似于MapReduce中的计数器。当作业完成后,driver程序可以检测累加器的最终值

四、剖析Spark作业运行机制

Spark作业有两个独立的实体:driver和executer。driver负责托管应用(SparkContext)并为作业调度任务。executer专属于应用,它在应用运行期间运行,并执行该应用的任务。通常,driver作为一个不由集群管理器(cluster manager)管理的客户端来运行,而executer运行在集群的计算机上

1)作业提交
当对RDD执行一个动作时,会自动提交一个Spark作业。从内部看,它导致对SparkContext调用runJob(),然后将调用传递给作为driver的一部分运行的调度程序。调度程序由两部分组成:DAG调度程序和任务调度程序。DAG调度程序把作业分解为若干阶段,并由这些阶段构成一个DAG。任务调度程序则负责把每个阶段的任务提交到集群

2)DAG的构建
要想了解一个作业如何被划分为阶段,首先需要了解在阶段中运行的任务的类型。有两种类型的任务:shuffle map任务和result任务。从任务类型的名称可以看出Spark会怎样处理任务的输出

shuffle map任务
shuffle map任务就像是MapReduce中shuffle的map端部分,每个shuffle map任务在一个RDD分区上运行计算,并根据分区函数把输出写入一组新的分区中,以允许在后面的阶段中取用(后面的阶段可能由shuffle map任务组成,也可能由result任务组成)。shuffle map任务运行在除最终阶段之外的其他所有阶段中

result任务
result任务运行在最终阶段,并将结果返回给用户程序(例如count())。每个result任务在它自己的RDD分区上运行计算,然后把结果发送回driver,再由driver将每个分区的计算结果汇集成最终结果。最简单的Spark作业不需要使用shuffle,因此它只有一个由result任务就构成阶段,这就像是MapReduce中的仅有的map作业一样。比较复杂的作业要涉及到分组操作,并且要求一个或多个shuffle阶段

3)任务调度
当任务集合被发送到任务调度程序后,任务调度程序为该应用运行的executer的列表,在斟酌位置偏好的同时构建任务到executer的映射。接着,任务调度程序将任务分配给具有可用内核的executer(如果同一应用中的另一个作业在运行,则有可能分配不完整),并且在executer完成任务运行时继续分配更多的任务,直到任务集合全部完成。默认情况下,每个任务分配到一个内核,不过也可以通过设置spark.task.cpus来更改
请注意,任务调度程序在为某个executer分配任务时,首先分配的是进程本地(process-local)任务,再分配节点本地(node-local)任务,然后分配机架本地(rack-local)任务,最后分配任意(非本地)任务或者推测任务,如果没有其他任务候选者的话。这些被分配的任务通过调度程序后端启动。调度程序后端向executer后端发送远程启动的消息,以告知executer开始运行任务
当任务完成或失败时,executer都会向driver发送状态更新消息。如果失败了,任务调度程序将在另一个executer上重新提交任务。若是启动了推测任务(默认不启用),它还会为运行缓慢的任务启动推测任务

4)任务执行
executer以如下方式运行任务。首先它确保任务JAR的包和文件依赖关系都是最新的。executer在本地高速缓存中保留了先前任务已使用的所有依赖,因此只有在它们更新的情况下才会重新下载。第二步,由于任务代码是以启动任务消息的一部分而发送的序列化字节,因此需要反序列化任务代码(包括用户自己的函数)。第三步,执行任务代码。请注意,因为任务运行在与executer相同的JVM中,因此任务的启动没有进程开销
任务可以向driver返回执行结果。这些执行结果被序列化并发送到executer后端,然后以状态更新消息的形式返回driver。shuffle map任务返回的是一些可以让下一个阶段检索其输出分区的信息,而result任务则返回其运行的分区的结果值,driver将这些结果值收集起来,并把最终结果返回给用户的程序

五、执行器和集群管理器

负责管理executer生命周期的是集群管理器(cluster manager),Spark提供了好多种具有不同特性的集群管理器

1)本地模式
本地模式时,有一个executer与driver运行在同一个JVM中。这种模式对测试或运行小规模作业非常有用。这种模式的主URL为local(使用一个线程)、local[n](n个线程)或local(*)(机器的每个内核一个线程)

2)独立模式
独立模式的集群管理器是一个简单的分布式实现,它运行了一个master以及一个或多个worker。当Spark应用启动时,master要求worker代表应用生成多个executer进程。这种模式的主URL为spark://host:port

3)Mesos模式
类似YARN模式,略

4)YARN模式
YARN是Hadoop中使用的资源管理器。每个运行的Spark应用对应一个YARN应用实例,每个executer在自己的YARN容器中运行。这种模式的主URL为yarn-client或yarn-cluster。YARN是唯一一个能够与Hadoop的Kerberos安全机制集成的集群管理器

为了在YARN上运行,Spark提供了两种部署模式:YARN客户端模式和YARN集群模式。YARN客户端模式的driver在客户端运行,而YARN集群模式的driver在YARN的application master集群上运行

1)YARN客户端模式
对于具有任何交互式组件的程序(例如spark-shell)都必须使用YARN客户端模式。客户端模式在构建Spark程序时也很有用,因为任何调试输出都是立即可见的

在YARN客户端模式下,当driver构建新的SparkContext实例时就启动了与YARN之间的交互。该Context向YARN资源管理器提交一个YARN应用,YARN资源管理器则启动集群节点管理器上的YARN容器,并在其中运行一个名为SparkExecutorLauncher的application master。ExecutorLauncher的工作是启动YARN容器中executer,为了做到这一点,ExecutorLauncher要向资源管理器请求资源,然后启动ExecuterBackend进程作为分配给它的容器
每个executer在启动时都会连接回SparkContext并注册自身。这就向SparkContext提供了关于可用于运行任务的executer的数量及其位置的信息,这些信息被用在任务的位置偏好策略中。启动的executer的数量在spark-shell、spark-submit或py-spark中设置(如果未设置,则默认为两个),同时还要设置每个executer使用的内核数(默认值为1)以及内存量(默认值为1024MB)。YARN资源管理器的地址并没有在主URL中指定,而是从HADOOP_CONF_DIR环境变量指定的目录中的Hadoop配置中选取

2)YARN集群模式
YARN集群模式适用于生成作业(Production job),因为整个应用在集群上运行,这样做更易于保留日志文件(包括来自driver的日志文件)以供稍后检查。如果application master出现故障,YARN还可以尝试重新运行该应用

在YARN集群模式下,用户的driver程序在YARN的application master进程中运行。使用spark-submit命令时需要输入yarn-cluster的主URL,所有其他的参数,比如–num-executors和应用JAR,都与YARN客户端模式相同。spark-submit客户端将会启动YARN应用,但是它不会运行任何用户代码。剩余的过程与客户端模式相同,除了application master在为executer分配资源之前先启动driver程序外

你可能感兴趣的:(Spark学习总结)