《Apache Spark 基础及架构》


一、认识 Spark

Apache Spark 诞生于加州大学伯克利分校AMP实验室,是一个基于内存的分布式计算框架

Apache Spark 是大数据领域最活跃的项目之一,其活跃度目前远超 Hadoop。特点是快速、易用、通用及多种运行模式。

1.1、Spark 特点

1.1.1、快速

Spark 是面向内存的大数据处理引擎,这使得 Spark 能够为多个不同数据源的数据提供近乎实时的处理性能,适用于需要多次操作特定数据集的应用场景。

面向磁盘的 MapReduce 受限于磁盘读 / 写性能和网络 I/O 性能的约束,在处理迭代计算、实时计算、交互式数据查询等方面并不高效,但是这些却在图计算、数据挖掘和机器学习等相关应用领域中非常常见。针对这一不足,将数据存储在内存中并基于内存进行计算是一个有效的解决途径。

1.1.2、易用

一方面,Spark 提供了支持多种语言的 API,如 Scala、Java、Python、R 等,使得用户开发 Spark 程序十分方便。另一方面,Spark 是基于 Scala 语言开发的,由于 Scala 是一种面向对象的、函数式的静态编程语言,其强大的类型推断、模式匹配、隐式转换等一系列功能结合丰富的描述能力使得 Spark 应用程序代码非常简洁。Spark 的易用性还体现在其针对数据处理提供了丰富的操作。

在使用 MapReduce 开发应用程序时,通常用户关注的重点与难点是如何将一个需求 Job(作业)拆分成 Map 和 Reduce。由于 MapReduce 中仅为数据处理提供了两个操作,即 Map 和 Reduce,因此系统开发人员需要解决的一个难题是如何把数据处理的业务逻辑合理有效地封装在对应的两个类中。与之相对比,Spark 提供了 80 多个针对数据处理的基本操作,如 map、flatMap、reduceByKey、filter、cache、collect、textFile 等,这使得用户基于 Spark 进行应用程序开发非常简洁高效。以分词统计为例,虽然 MapReduce 固定的编程模式极大地简化了并行程序开发,但是代码至少几十行,若换成 Spark,其核心代码最短仅需一行。

关于开发语言的选择问题,在 Spark 的实际项目开发中多用 Scala 语言,约占 70%;其次是 Java,约占 20%;而 Python 约占 10%。

1.1.3、通用

位于底层的是 Spark Core,其实现了 Spark 的作业调度、内存管理、容错、
与存储系统交互等基本功能,并针对弹性分布式数据集提供了丰富的操作。在 Spark Core 的基础上,Spark 提供了一系列面向不同应用需求的组件,主要有 Spark SQL、Spark Streaming、MLlib、GraphX。将这些组件放在一起,构成了 Spark 软件栈。基于这个软件栈,Spark 提出并实现了大数据处理的一种理念——“一栈式解决方案(one stack to rule them all)”,即 Spark 可同时对大数据进行批处理、流式处理和交互式查询,如下图所示。借助于这一软件栈用户可以简单而低耗地把各种处理流程综合在一起,充分体现了 Spark 的通用性。
在这里插入图片描述 在这里插入图片描述

1.1.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 模式。

1.2、Spark 与 MapReduce 比较

1.2.1、易用性

由于 MapReduce 中仅为数据处理提供了两个操作,即 Map 和 Reduce,因此系统开发人员需要解决的一个难题是如何把数据处理的业务逻辑合理有效地封装在对应的两个类中。而通常同样的功能若换成 Spark,其核心代码最短仅需一行。

MapReduce 自身并没有交互模式,需要借助 Hive 和 Pig 等附加模块。Spark 则提供了一种命令行交互模式,即 Spark Shell,使得用户可以获取到查询和其他操作的即时反馈。

1.2.2、效率

Map 中间结果写入磁盘,效率低下,不适合迭代运算。Spark Job 中间输出结果可以保存在内存,不再需要读写 HDFS。

1.2.3、任务启动开销

Spark 和 Hadoop MapReduce 都实现了异步并发模型,而 MapReduce 采用的是多进程模型,Spark 采用了多线程模型。

多进程模型便于细粒度控制每个任务占用的资源,但会消耗较多的启动时间,不适合运行低延迟类型的作业,这是 MapReduce 广为诟病的原因之一。多线程模型则相反,该模型使得 Spark 很适合运行低延迟类型的作业。

1.3、Spark 技术栈

1.3.1、Spark Core

核心组件,分布式计算引擎。其实现了 Spark 的作业调度、内存管理、容错、与存储系统交互等基本功能,并针对弹性分布式数据集(RDD)提供了丰富的操作。

1.3.2、Spark SQL

又一个高性能的基于 Hadoop 的 SQL 解决方案。部分用法与 Hive 非常类似。

1.3.3、Spark Streaming

基于 Spark Core 实现的高吞吐量、具备容错机制的准实时流处理系统。将流式计算分解成一系列小批处理作业,也称微批处理。

1.3.4、Spark GraphX

分布式图处理框架,支持图并行计算。现在已经提供了很多算法,新的算法还在不断加入。

1.3.5、Spark MLlib

构建在 Spark 上的分布式机器学习库。是 Spark 对常用的机器学习算法的实现库,还提供了相关的测试与数据生成器。

二、Spark环境部署

2.1、Spark安装

2.2、Standalone 模式

独立模式,自带完整的服务,可单独部署到一个集群中,无需依赖任
何其他资源管理系统
。从一定程度上说,该模式是其他两种的基础。借鉴 Spark 开发模式,我们可以得到一种开发新型计算框架的一般思路:先设计出它的 standalone 模式,为了快速开发,起初不需要考虑服务(比如 master/slave)的容错性,之后再开发相应的 wrapper,将 stanlone 模式下的服务原封不动的部署到资源管理系统 yarn 或者 mesos 上,由资源管理系统负责服务本身的容错。目前 Spark 在 standalone 模式下是没有任何单点故障问题的,这是借助 zookeeper 实现的,思想类似于 Hbase master 单点故障解决方案。将 Spark standalone 与 MapReduce 比较,会发现它们两个在架构上是完全一致的:

(1) 都是由 master/slaves 服务组成的,且起初 master 均存在单点故障,后来均通过 zookeeper 解决(Apache MRv1 的 JobTracker 仍存在单点问题,但 CDH 版本得到了解决)

(2) 各个节点上的资源被抽象成粗粒度的 slot,有多少 slot 就能同时运行多少 task。不同的是,MapReduce 将 slot 分为 map slot 和 reduce slot,它们分别只能供 Map Task 和 Reduce Task 使用,而不能共享,这是 MapReduce 资源利率低效的原因之一,而 Spark 则更优化一些,它不区分 slot 类型,只有一种 slot,可以供各种类型的 Task 使用,这种方式可以提高资源利用率,但是不够灵活,不能为不同类型的 Task 定制 slot 资源。总之,这两种方式各有优缺点

  • master: 指定运行模式,spark://host:port, mesos://host:port, yarn, or local[n]
  • master 和 worker 是物理节点,driver 和 executor 是进程
  • Client :客户端进程,负责提交作业到 Master
  • Master :Standalone 模式中主控节点,负责接收 Client 提交的作业,管理
    Worker,并命令 Worker 启动 Driver 和 Executor(集群模式)
  • 当 deployMode 为 CLIENT 时,driver 会在客户端直接运行

2.2.1、master 和 worker 节点

搭建 spark 集群的时候我们就已经设置好了 master 节点和 worker 节点,一个集群有多个 master 节点和多个 worker 节点。

master 节点常驻 master 守护进程,负责管理 worker 节点,我们从 master 节点提交应用。

worker 节点常驻 worker 守护进程,与 master 节点通信,并且管理 executor 进程。

一台机器可以同时作为 master 和 worker 节点(举个例子:你有四台机器,你可以选择一台设置为 master 节点,然后剩下三台设为 worker 节点,也可以把四台都设为 worker 节点,这种情况下,有一个机器既是 master 节点又是 worker 节点)

类似与 Hadoop 中的 namenode 和 datanode,主从关系。

2.2.2、driver 和 executor 进程

driver 进程就是应用的 main()函数并且构建 sparkContext 对象,当我们提交了应用之后,便会启动一个对应的 driver 进程,driver 本身会根据我们设置的参数占有一定的资源(主要指 cpu core 和 memory)
在这里插入图片描述
driver 可以运行在 master 上,也可以运行 worker 上(根据部署模式的不同 -deploy-mode: 指定将 driver 端运行在 client 还是在 cluster)

driver 首先会向集群管理者(standalone、yarn,mesos)申请 spark 应用所需的资源,也就是 executor,然后集群管理者会根据spark应用所设置的参数在各个 worker 上分配一定数量的 executor,每个 executor 都占用一定数量的 cpu 和 memory

在申请到应用所需的资源以后,driver 就开始调度和执行我们编写的应用代码了

driver 进程会将我们编写的 spark 应用代码拆分成多个 stage,每个 stage 执行一部分代码片段,并为每个 stage 创建一批 tasks,然后将这些 tasks 分配到各个 executor 中执行

executor 进程宿主在 worker 节点上,一个 worker 可以有多个 executor

每个 executor 持有一个线程池,每个线程可以执行一个 task,executor 执行完 task 以后将结果返回给 driver,每个 executor 执行的 task 都属于同一个应用

此外executor还有一个功能就是为应用程序中要求缓存的 RDD 提供内存式存储,RDD 是直接缓存在 executor 进程内的,因此任务可以在运行时充分利用缓存数据加速运算

2.3、yarn 运行模式

spark on yarn(spark 作为客户端,spark 需要做的事情是提交作业到 yarn 上执行)

yarn 支持 client 和 cluster 模式:driver 运行在哪里 -deploy-mode: 指定将 driver 端运行在 client 还是在 cluster

  • client:提交作业的进程是不能停止的否则作业就挂了
  • cluster:提交完作业,那么提交作业端就可以断开,因为 driver 是运行在 am 里面的
查看已经运行完的 yarn 的日志信息:yarn logs -applicationId <applicationId>

yarn 和 standalone 的区别:

yarn 模式:只需要一个节点,然后提交作业即可,不需要 spark 集群的(不需要启动 Maser 和 Worker)

standalone 模式:在 spark 集群上的每个节点都需要部署 spark,然后需要启动 spark 集群(需要 Maser 和 Worker 进程节点)

2.4、cluster 模式和 client 模式

2.4.1、运行 cluster 模式

spark-submit \
--class org.apache.spark.examples.SparkPi \
--master yarn \
--deploy-mode cluster \
--driver-memory 1g \
--executor-memory 1g \
--executor-cores 2 \
--queue default \
examples/jars/spark-examples*.jar \
100

2.4.2、运行 client 模式

spark-submit \
--class org.apache.spark.examples.SparkPi \
--master yarn \
--deploy-mode client \
--driver-memory 1g \
--executor-memory 1g \
--executor-cores 2 \
--queue default \
examples/jars/spark-examples*.jar \
10

spark-shell 必须使用 client 模式(默认)

spark-shell --master yarn --deploy-mode client

2.4.3、两种模式的区别

cluster 模式:Driver 程序在 YARN 中 AM 运行,应用的运行结果不能在客户端显示,所以最好运行那些将结果最终保存在外部存储介质(如 HDFS、Redis、Mysql)而非 stdout 输出的应用程序,客户端的终端显示的仅是作为 YARN 的 job 的简单运行状况。

client 模式:Driver 运行在 Client 上,应用程序运行结果会在客户端显示,所有适合运行结果有输出的应用程序(如 spark-shell)

2.4.4、cluster 模式原理

在这里插入图片描述
Spark Driver 首先作为一个 ApplicationMaster 在 YARN 集群中启动,客户端提交给 ResourceManager 的每一个 job 都会在集群的 NodeManager 节点上分配一个唯一的 ApplicationMaster,由该 ApplicationMaster 管理全生命周期的应用。具体过程:

  1. 由 client 向 ResourceManager 提交请求,并上传 jar 到 HDFS 上
    这期间包括四个步骤:
    a)连接到 RM
    b)从 RM 的 ASM(ApplicationsManager )中获得 metric、queue 和 resource 等信息
    c)upload app jar and spark-assembly jar
    d)设置运行环境和 container 上下文(launch-container.sh 等脚本)

  2. ResouceManager 向 NodeManager 申请资源,创建 Spark ApplicationMaster(每个 SparkContext 都有一个 ApplicationMaster)

  3. NodeManager 启动 ApplicationMaster,并向 ResourceManager AsM 注册

  4. ApplicationMaster从HDFS中找到jar文件,启动SparkContext、DAGscheduler 和 YARN Cluster Scheduler

  5. ResourceManager 向 ResourceManager AsM 注册申请 container 资源

  6. ResourceManager 通知 NodeManager 分配 Container,这时可以收到来自 ASM 关于 container 的报告。(每个 container 对应一个 executor)

  7. Spark ApplicationMaster 直接和 container(executor)进行交互,完成这个分布式任务。

2.4.5、client 模式原理

在这里插入图片描述
在 client 模式下,Driver 运行在 Client 上,通过 ApplicationMaster 向 RM 获取资源。本地 Driver 负责与所有的 executor container 进行交互,并将最后的结果汇总。结束掉终端,相当于 kill 掉这个 spark 应用。一般来说,如果运行的结果仅仅返回到 terminal 上时需要配置这个。

客户端的 Driver 将应用提交给 Yarn 后,Yarn 会先后启动 ApplicationMaster和 executor,另外 ApplicationMaster 和 executor 都 是装载在 container 里运行,container 默认的内存是 1G,ApplicationMaster 分配的内存是 driver- memory,executor 分配的内存是 executor-memory。同时,因为 Driver 在客户端,所以程序的运行结果可以在客户端显 示,Driver 以进程名为 SparkSubmit 的形式存在。

三、Spark 架构

Spark 的架构中的术语:

  • 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

在这里插入图片描述
Spark RDD 包括四类算子:创建算子、转换算子、持久化算子及 Action 算子。一个 Action 算子对应一个 Job,其余的算子对应一个 Task。

3.1、Spark 编程入口

在开发过程中,常用 API 主要有:SparkContext、SparkSession、RDD、DataSet 及 DataFrame,这里主要介绍 SparkContext、SparkSession。因为 RDD、DataSet 及 DataFrame 有一定的相似之处,稍后对 RDD 深入介绍,DataSet 与 DataFrame 在 Spark SQL 中再介绍。

3.1.1、SparkContext

从前面 Spark 运行架构图中可以看出,SparkContext 是连接 Driver、Worker 以及 Cluster Manager(Master)的桥梁。作为 Spark 应用程序的核心、编程入口,SparkContext 主要完成了如下工作。
在这里插入图片描述
这里对主要功能进行解释:

RDD graph:生成 RDD 依赖关系图 DAG。SparkContext 会根据用户提交的计算逻辑(Application)中的 RDD 的转换和动作来生成 RDD 之间的依赖关系,同时这个计算链也就生成了逻辑上的 DAG

DAGScheduler:为高级的、基于 Stage 的调度器, 负责创建 Job,将 DAG 中 的 RDD 划分到不同的 Stage,并将 Stage 作为 Tasksets 提交给底层调度器 TaskScheduler 执行

TaskScheduler:为 Spark 的任务调度器,Spark 通过他提交任务并且请求集群调度任务。因其调度的 Task 由 DAGScheduler 创建,所以 DAGScheduler 是 TaskScheduler 的前置调度

SchedulerBackend:是一个 trait,作用是分配当前可用的资源。具体即向当前等待分配计算资源的 Task 分配计算资源(即 Executor)

ListenerBus:SparkContext 中的事件总线,可以接收各种使用方的事件,并且异步传递 Spark 事件监听与 SparkListeners 监听器的注册

BlockManager:属于 SparkEnv 组件中的成员,一个嵌入在 spark 中的 key-value 型分布式存储系统(类似 HDFS),SparkEnv 是 SparkContext 中非常重要的类,它维护着 Spark 的执行环境,所有的线程都可以通过 SparkContext 访问到同一个 SparkEnv 对象。SparkEnv 还包括 ShuffleManger、SecurityManager、CacheManger 等

3.1.2、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.sql.SparkSession

val spark = SparkSession.builder
 .master("local[*]")
 .appName("appName")
 .getOrCreate()

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

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

四、Spark 核心数据结构—RDD

4.1、RDD 概念

RDD 称为弹性分布式数据集(Resilient Distributed Datasets),它是一种分布式的内存抽象,允许在大型集群上执行基于内存的计算(In-Memory Computing),为用户屏蔽了底层复杂的计算和映射环境。

(1)简单的解释
RDD 是将数据项拆分为多个分区的集合,存储在集群的工作节点上的内存和磁盘中,并执行正确的操作

(2)复杂的解释

  • RDD 是用于数据转换的接口,比如 map、filter、groupBy、join 等
  • RDD 指向了存储在 HDFS、Cassandra、HBase 等、或缓存(内存、内存+
    磁盘、仅磁盘等),或在故障或缓存收回时重新计算其他 RDD 分区中的数据。从这个意义上讲,RDD 不包含任何待处理数据

4.2、RDD 特性

RDD 的五大属性:分区、分区计算函数、依赖、分区器及分区优先位置列表

4.2.1、分区(Parition)

RDD 是由多个分区构成的。RDD#partitions 返回 RDD 的所有分区信息。每个 Partition 都有一个唯一索引编号 ,可通过 Partition#index 访问。RDD 分区概念与 MapReduce 的输入切片概念是类似的。对每个分区的运算会被一个当作一个 Task 执行。换句话说,分区是 Spark 任务执行的基本单位。举例:如果有 100 个分区,那么 RDD 上有 n 个操作将会产生有 n*100 个任务。

我们在使用分区的时候要了解两条规则:

  • 只有Key-Value类型的RDD才有分区器,非Key-Value类型的RDD(PairRDD)分区器的值是 None
  • 每个 RDD 的分区 ID 范围:0~numPartitions-1,决定这个值是属于那个分区的

4.2.2、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,如下图所示
在这里插入图片描述

4.2.3、RDD 间的依赖(DAG)

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

4.2.4、分区器(Partitioner)

RDD 的分区器是一个可选项,如果 RDD 里面存的数据是 key-value 形式,则可以传递一个自定义的 Partitioner 进行重新分区,例如这里自定义的 Partitioner 是基于 key 进行分区,那则会将不同 RDD 里面的相同 key 的数据放到同一个 Partition 里面

Spark 包 含 两 种 数 据 分 区 方 式 : HashPartitioner(哈希分区)和 RangePartitioner(范围分区)

(1)HashPartitioner 是默认的分区方式,其算法逻辑是:

partition = key.hashCode() % numPartitions

(2)RangePartitioner 通过两个步骤来实现:

第一步:先从整个 RDD 中抽取出样本数据,将样本数据排序,计算出每个分区的最大 key 值,形成一个 Array[KEY]类型的数组变量 rangeBounds
第二步:判断 key 在 rangeBounds 中所处的范围,给出该 key 值在下一个 RDD 中的分区 id 下标;该分区器要求 RDD 中的 KEY 类型必须是可以排序的

4.2.5、分区优先位置列表

该列表存储了存取每个分区的优先位置。对于一个 HDFS 文件来说,这个列表保存了每个分区所在的数据块的位置。按照“移动数据不如移动计算的”的理念,Spark 在进行任务调度的时候,会尽可能的将计算任务移动到所要处理的数据块的存储位置

preferredLocations 返回每个分区所在的机器名或者 IP 地址,如果分区数据是多份存储,那么返回多个机器地址,如下图所示
在这里插入图片描述

4.3、RDD 的创建及操作

RDD的创建及操作详情请见博客:Spark之RDD算子总汇

4.4、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 中的累加器专门用于提供一种机制,以便在集群中的工作节点之间执行分割时安全地更新变量。后面累加器部分更详细地讨论了这些

一般来说,像循环或局部定义方法这样的闭包结构不应该用来改变全局状态。一些这样做的代码可能在本地模式下工作,但那只是偶然的,而且这样的代码在分布式模式下不会像预期的那样工作。如果需要全局聚合,则使用累加器

猜你喜欢

转载自blog.csdn.net/qq_42578036/article/details/112234975