Spark

一、Spark简介

1.1 Spark简介

Apache Spark是专门为大数据处理而设计的通用的计算引擎。spark拥有MapReduce所具有的优点,但不同于Map Reduce的是Job中间输出结果可以缓存到内存中,从而不再需要读写HDFS,减少磁盘数据交互,因此Spark能更好的适应机器学习和数据挖掘等需要迭代的算法。

Spark提供了Sparkcore RDD Spark SQL Spark Streaming Spark MLlib 、 Spark GraphX等技术组件,可以一站式地完成大数据领域的离线批处理、交互式查询、流式计算、机器学习、图计算等常见的任务。这就是 spark 一站式开发的特点。

1.2 Spark与MR的区别

spark的执行流程如下 

都是分布式计算框架, Spark 计算中间结果基于内存缓存, MapReduce 基于 HDFS 存储。也正因此,Spark 处理数据的能力一般是 MR 的三到五倍以上, Spark 中除了基于内存计算这一个计算快的原因,还有 DAGDAGShecdule有向无环图来切分任务的执行先后顺序。

二、SparkCore

2.1 Partition

2.1.1 概念

Spark RDD是一种分布式数据集,由于数据量很大,因此要被切分存储在各个节点的分区当中。

Spark中,RDDResilient Distributed Dataset是最基本的数据抽象集,其中每个RDD由若干个Partition组成

RDD1包含了5个PartitionRDD2包含了3Partition,这些Partition分布在4个节点中。

 2.1.2 分区方式

Spark包含俩中分区方式:HashPartition(哈希分区)和RangePartitioner(范围分区)。

在Spark Shuffle阶段中,共分为Shuffle Write阶段和Shuffle Read阶段,其中在Shuffle Write中,Shuffle Map Task对数据进行处理产生中间数据,再根据数据分区方式对中间数据进行分区。最终Shuffle Read阶段中的Shuffle Read Task会拉取Shuffle Write阶段中产生并已经分区好的中间数据。

 HashPartitioner

Hash分区

HashPartitioner采用哈希的方式对<KeyValue>键值对数据进行分区。

其数据分区规则为 partitionId = Key.hashCode % numPartitions

partitionId代表该Key对应的键值对数据应当分配到的Partition标识

Key.hashCode表示该Key的哈希值

numPartitions表示包含的Partition个数。

RangePartitioner

范围分区

Spark引入RangePartitioner的目的是为了解决HashPartitioner所带来的分区倾斜问题,也即分区中包含的数据量不均衡问题。

HashPartitioner采用哈希的方式将同一类型的Key分配到同一个Partition中,当某几种类型数据量较多时,就会造成若干Partition中包含的数据过大

Job执行过程中,一个Partition对应一个Task,此时就会使得某几个Task运行过慢。

RangePartitioner基于抽样的思想来对数据进行分区

 2.1.3 HDFS与Partition

hdfs中的block是分布式存储的最小单元,类似于盛放文件的盒子,一个文件可能要占多个盒子,但一个盒子里的内容只可能来自同一份文件。假设block设置为128M,你的文件是260M,那么这 份文件占3block128+128+4)。这样的设计虽然会有一部分磁盘空间的浪费,但是整齐的block大小,便于快速找到、读取对应的内容。(p.s. 考虑到hdfs冗余设计,默认三份拷贝,实际上3*3=9block的物理空间。)

spark中的partition 是弹性分布式数据集RDD的最小单元,RDD是由分布在各个节点上的partition组成的。partition 是指的spark在计算过程中,生成的数据在计算空间内最小单元,同一份数据(RDD)的partition 大小不一,数量不定,是根据application里的算子和最初读入的数据分块数量决定的

block位于存储空间、partition 位于计算空间,block的大小是固定的、partition 大小是不固定的, block是有冗余的、不会轻易丢失,partitionRDD)没有冗余设计、丢失之后重新计算得到.

SparkHDFS读入文件的分区数默认等于HDFS文件的块数(blocks)HDFS中的block是分布式存储的最小单元。如果我们上传一个30GB的非压缩的文件到HDFSHDFS默认的块容量大小128MB,因此该文件在HDFS上会被分为235(30GB/128MB)Spark读取SparkContext.textFile()读取该文件,默认分区数等于块数即235

2.2 RDD

RDD(Resilient Distributed Dataset) 弹性分布式数据集。

2.2.1 RDD五大属性

A list of partition

RDD是由一系列的Partition组成的

function for computing each split

函数是作用在每一个 partition/split 上。

A list of dependencies on other RDDs

RDD之间有一系列的依赖关系

Optionally, a Partitioner for key-value RDDs

分区器是作用在 (K,V) 格式的 RDD 上。

Optionally, a list of preferred locations to compute each split on

RDD提供一系列最佳的计算位置。

2.2.2 RDD流程图

注意

 textFile 方法底层封装的是 MR 读取文件的方式,读取文件之前先进行 split 切片,默认 split大小是一个 block 大小。

RDD实际上不存储数据,这里方便理解,暂时理解为存储数据

什么是K,V格式的RDD ?

如果RDD 里面存储的数据都是二元组对象,那么这个 RDD 我们就叫做 K,V格式的RDD

哪里体现 RDD 的弹性(容错)?         

partition数量,大小没有限制,体现了 RDD 弹性

RDD之间依赖关系,可以基于上一个 RDD 重新计算出 RDD

哪里体现 RDD 的分布式?

RDD是由 Partition 组成, partition 是分布在不同节点上的。

RDD提供计算最佳位置,体现了数据本地化。体现了大数据中计算移动数据不移动的理念。

2.2.3 Lineage血统

RDD的最重要的特性之一就是血缘关系(Lineage ),它描述了一个 RDD 是如何从父 RDD 计算得来的。如果某个 RDD 丢失了,则可以根据血缘关系,从父 RDD 计算得来。

2.3 系统架构

 Master ( standalone 模式):资源管理的主节点进程)。

Cluster Manager :在集群获取资源的外部服务(例如: standalone yarn mesos )。

Worker ( standalone 模式):资源管理的从节点(进程)或者说是是管理本机资源的进程

Application :基于 Spark 的用户程序,包含 driver 程序和运行在集群上的 executor 程序,即一个完整的 spark 应用 。

Dirver program ):用来连接工作进程( worker )的程序

Executor :是在一个 worker 进程所管理的节点上为某 Application 启动的一个个进程,这个进程负责运行任务,并且负责将数据存在内存或者磁盘上,每个应用之间都有各自独立的executors

Task :被发送到 executor 上的工作单元。

Job :包含很多任务( Task )的并行计算,和 action 算子对应。

Stage :一个 job 会被拆分成很多组任务,每组任务被称为 Stage (就像 MapReduce 分为 MapTask ReduceTask 一样)。

三、算子

Spark 记录了 RDD 之间的生成和依赖关系。但是只有当 F 进行行动操作时,Spark 才会根据 RDD的依赖关系生成 DAG,并从起点开始真正的计算。

3.1 转换算子

Transformations 类算子叫做转换算子(本质就是函数), Transformations 算子是延迟执行,也叫懒加载执行。

常见Transformations类算子

filter:过滤符合条件的记录数, true 保留, false 过滤掉。

map:将一个 RDD 中的每个数据项,通过 map 中的函数映射变为一个新的元素。特点:输入 一条,输出一条数据

flatMap:先 map flat 。与 map 类似,每个输入项可以映射为0到多个输出项

sample:随机抽样算子,根据传进去的小数按比例进行有放回或者无放回的抽样。

reduceByKey 将相同的 Key 根据相应的逻辑进行处理。

sortByKey / sortBy 作用在 K,V格式的RDD 上,对 key 进行升序或者降序排序。

3.2 行动算子

概念

Action 类算子叫做行动算子, Action 类算子是触发执行

一个application 应用程序中有几个 Action 类算子执行,就有几个 job 运行。

常见Action 类算子

count :返回数据集中的元素数。会在结果计算完成后回收到 Driver 端。

take(n) :返回一个包含数据集前 n 个元素的集合。

first:效果等同于 take(1) ,返回数据集中的第一个元素。

foreach:循环遍历数据集中的每个元素,运行相应的逻辑。

collect:将计算结果回收到 Driver 端。

3.3 控制算子

概念

RDD 持久化,持久化的单位是 partition

控制算子有三种, cache , persist , checkpoint cache persist 都是执行的。必须有一个 action 类算子触发执行。

checkpoint 算子不仅能将 RDD 持久化到磁盘,还能切断 RDD 之间的依赖关系

3.3.1 cache

默认RDD 的数据持久化到内存中。 cache 执行。

cache() = persist() = persist(StorageLevel.Memory_Only)

rdd.cache().count() 返回的不是持久化的RDD,而是一个数值

3.3.2 persist

可以指定持久化的级别。最常用的是 MEMORY_ONLY MEMORY_AND_DISK

3.3.3 checkpoint

checkpoint RDD 持久化到磁盘,还可以切断 RDD 之间的依赖关系,也是懒执行。

执行原理

RDD job 执行完毕后,会从 finalRDD 从后往前回溯。

回溯到某一个 RDD 调用了 checkpoint 方法,会对当前的 RDD 做一个标记。

Spark 框架会自动启动一个新的 job ,重新计算这个 RDD 的数据,将数据持久化到Checkpint目录   中。

使用checkpoint 时常用优化手段:

RDD 执行 checkpoint 之前,最好对这个 RDD 先执行 cache

这样新启动的 job 只需要将内存中的数据拷贝到Checkpint目录中就可以,省去了重新计算这一步。 

四、任务提交方式

4.1 Standalone-client

执行流程

 

 client模式提交任务后,会在客户端启动 Driver 进程。

Driver会向 Master 申请启动 Application 启动的资源。资源申请成功,Driver 端将 task 分发到 worker 端执行,启动 executor 进程(任务的分发)。

worker端( exectuor 进程)将 task 执行结果返回到 Driver 端(任务结果的回收)。

总结

client模式适用于测试调试程序Driver 进程是在客户端启动的,这里的客户端就是指提交应用程序的当前节点。在 Driver 端可以看到 task 执行的情况。生产环境下不能使用 client 模式,是因为:假设要提交100application 到集群运行, Driver 每次都会在 client 端启动,那么就会导致客户端100网卡流量暴增的问题。

4.2 Standalone-cluster

执行流程

 cluster 模式提交应用程序后,会向 Master 请求启动 Driver

Master 接受请求,随机集群一台节点启动 Driver 进程。

Driver 启动后为当前的应用程序申请资源。Driver 端发送 task worker 节点上执行(任务的分发)。

worker 上的 executor 进程将执行情况和执行结果返回给 Driver 端(任务结果的回收)。

总结

Standalone-cluster 提交方式,应用程序使用的所有 jar 包和文件,必须保证所有的 worker 节点要有,因为此种方式, spark 不会自动上传包。

将所有的依赖包和文件打到同一个包中,然后放在 hdfs 上。 将所有的依赖包和文件各放一份worker 节点上。

4.3 yarn-client

执行流程

户端提交一个 Application ,在客户端启动一个 Driver 进程。

应用程序启动后会向 RS ( ResourceManager )(相当于 standalone 模式下的 master 进程) 发送请求,启动 AM ( ApplicationMaster ) 

RS收到请求,随机选择一台 NM ( NodeManager )启动 AM 。这里的 NM 相当于 Standalone 中的 Worker 进程。

AM启动后,会向 RS 请求一批 container 资源,用于启动 Executor

RM会找到一批 NM (包含 container )返回给 AM ,用于启动 Executor

AM会向 NM 发送命令启动 Executor

Executor 启动后,会反向注册Driver Driver 发送 task Executor ,执行情况和结果返回给 Driver 端。

总结

Yarn-client 模式同样是适用于测试,因为 Driver 运行在本地, Driver 会与 yarn 集群中的 Executor 进行大量的通信

ApplicationMaster (executorLauncher)的在此模式中的作用:

当前的 Application 申请资源

给NodeManager 发送消息启动 Executor

注意: ApplicationMaster 在此种模式下没有作业调度的功能。

4.4 yarn-cluster 

执行流程

客户机提交 Application 应用程序,发送请求到 RS ( ResourceManager ),请求启动AM ( ApplicationMaster )

RS收到请求后随机在一台 NM ( NodeManager )上启动 AM (相当于 Driver 端)。

 AM启动, AM 发送请求到 RS ,请求一批 container 用于启动 Excutor

RS返回一批 NM 节点给 AM

AM连接到 NM ,发送请求到 NM 启动 Excutor

Excutor 反向注册到 AM 所在的节点的 Driver Driver 发送 task Excutor

总结

Yarn-Cluster 主要用于生产环境中,因为 Driver 运行在 Yarn 集群中某一台 nodeManager中,每次提交任务的 Driver 所在的机器都是不再是提交任务的客户端机器,而是多个 NM 节点中的一台,不会产生某一台机器网卡流量激增的现象,但同样也有缺点,任务提交后不能看 到日志。只能通过 yarn 查看日志

ApplicationMaster 在此模式中的的作用:

当前的 Application 申请资源;给 NodeManger 发送消息启动 Executor 。 任务调度

五、算子

5.1 转换算子

转换算组join

leftOuterJoin、rightOuterJoin、fullOuterJoin,这些 join 都是作用在 K,V 格式的 RDD 上。根据 key 值进行连接,例如: (K,V)join(K,W)返 回(K,(V,W)) 注意join 后的分区数RDD分区数多的那一个相同

union

合并两个数据集。两个数据集的类型要一致。返回新的 RDD 的分区数是合并 RDD 分区数的总和。

intersection

取两个数据集的交集。

subtract

取两个数据集的差集。

mapPartition

mapPartitionmap 类似,单位是每个 partition 上的数据。

distinct(map+reduceByKey+map)

RDD 内数据去重。

cogroup

当调用类型 (K,V) (KW) 的数据上时,返回一个数据集 (K, (Iterable<V>,Iterable<W>))

5.2 行动算子

foreachPartition

遍历的数据是每个 partition 的数据。

六、窄依赖和宽依赖

RDD之间有一系列的依赖关系,依赖关系又分为窄依赖宽依赖

 6.1 窄依赖

RDD 和子 RDD partition 之间的关系是一对一的。或者父 RDD 和子 RDD partition 关系是多对一的。不会有 shuffle 的产生。

6.2 宽依赖

RDD 与子 RDD partition 之间的关系是一对多。会有 shuffle 的产生。

6.3 宽窄依赖图理解

 七、Stage

Spark任务会根据 RDD 之间的依赖关系,形成一个 DAG 有向无环图DAG 会提交给 DAGScheduler DAGScheduler 会把 DAG 划分成相互依赖的多个 stage ,划分 stage 的依据就是 RDD 之间的宽窄依赖。遇到宽依赖就划分 stage ,每个 stage 包含一个或多个 task 任务。然后将这些 task taskSet 的形式提交给 TaskScheduler 运行。

stage 是由一组并行task 组成。

7.1 stage切割规则

切割规则:从后往前,遇到宽依赖就切割 stage

1.从后向前推理,遇到宽依赖就断开,遇到窄依赖就把当前的RDD加入到Stage中;

2.每个Stage里面的Task的数量是由该Stage中最后一个RDDPartition数量决定的;

3.最后一个Stage里面的任务的类型是ResultTask,前面所有其他Stage里面的任务类型都是ShuffleMapTask

4.代表当前Stage的算子一定是该Stage的最后一个计算步骤;

总结:由于sparkstage的划分是根据shuffle来划分的,而宽依赖必然有shuffle过程,因此可以说spark是根据宽窄依赖来划分stage的。

 7.2 stage计算模式

 pipeline 管道计算模式, pipeline 只是一种计算思想、模式。

sparkpipeline是一个partition对应一个partition,所以在stage内部只有窄依赖。

数据一直在管道里面什么时候数据会落地

对RDD 进行持久化cache persist )。

shuffle write 的时候。

Stage task 并行度是由 stage 的最后一个 RDD 分区数来决定的 。

如何改变 RDD 分区数

reduceByKey(XXX,3)、GroupByKey(4)、sc.textFile(path,numpartition)

使用算子时传递 分区num参数 就是分区 partition 的数量。

八、SparkShuffle

8.1 SparkShuffle概念

reduceByKey会将上一个RDD中的每一个key对应的所有value聚合成一个value,然后生成一个新 RDD,元素类型是<key,value>对的形式,这样每一个key对应一个聚合起来的value

问题:

聚合之前,每一个key对应的value不一定都是在一个partition中,也不太可能在同一个节点上,因为RDD是分布式的弹性的数据集,RDDpartition极有可能分布在各个节点上。

如何聚合?

Shuffle Write:上一个stage的每个map task就必须保证将自己处理的当前分区的数据相同的key写入一个分区文件中,可能会写入多个不同的分区文件中。

Shuffle Readreduce task就会从上一个stage的所有task所在的机器上寻找属于自己的那些分区文件,这样就可以保证每一个key所对应的value都会汇聚到同一个节点上去处理和聚合。

8.2 HashShuffle

8.2.1 普通机制

执行流程:

每一个map task将不同结果写到不同的buffer中,每个buffer的大小为32Kbuffer起到数据缓存的作用。

每个buffer文件最后对应一个磁盘小文件。reduce task来拉取对应的磁盘小文件。

总结:

map task的计算结果会根据分区器(默认是hashPartitioner)来决定写入到哪一个磁盘小文件中去。ReduceTask会去Map端拉取相应的磁盘小文件。

产生的磁盘小文件的个数:

Mmap task的个数)*Rreduce task的个数)

产生的磁盘小文件过多,会导致以下问题:

Shuffle Write过程中会产生很多写磁盘小文件的对象。在Shuffle Read过程中会产生很多读取磁盘小文件的对象。在JVM堆内存中对象过多会造成频繁gc,gc还无法解决运行所需要的内存的话,就会OOM。在数据传输过程中会有频繁的网络通信,频繁的网络通信出现通信故障的可能性大大增加,一 旦网络通信出现了故障会导致shuffle file cannot find 由于这个错误导致的task失败, TaskScheduler不负责重试,由DAGScheduler负责重试Stage

8.2.2 合并机制

 执行流程:

合并机制就是复用buffer,开启合并机制的配置是spark.shuffle.consolidateFiles。该参数默 认值为false,将其设置为true即可开启优化机制。

在shuffle write过程中,task就不是为下游stage的每个task创建一个磁盘文件了。此时会出 shuffleFileGroup的概念,每个shuffleFileGroup会对应一批磁盘文件,磁盘文件的数量与 下游stagetask数量是相同的。一个Executor上有多少个CPU core,就可以并行执行多少 task。而第一批并行执行的每个task都会创建一个shuffleFileGroup,并将数据写入对应的磁盘文件内。

假设第一个stage50task,第二个stage100task,总共还是有10Executor,每个Executor执行5task。那么原本使用未经优化的HashShuffleManager时,每个Executor会 产生500个磁盘文件,所有Executor会产生5000个磁盘文件的。但是此时经过优化之后,每个Executor创建的磁盘文件的数量的计算公式为:CPU core的数量 * 下一个stagetask数量。也就是说,每个Executor此时只会创建100个磁盘文件,所有Executor只会创建1000个磁盘文件。

总结:

产生磁盘小文件的个数: C(core的个数)*Rreduce的个数)

8.3 SortShuffle

8.3.1 普通机制

 执行流程:

maptask 的计算结果会写入到一个内存数据结构里面,内存数据结构默认是 5M 。在 shuffle 的时候会有一个定时器,不定期的去估算这个内存结构的大小,当内存结构中的 数据超过 5M 时,比如现在内存结构中的数据为 5.01M ,那么他会申请 5.01*2-5=5.02M 内存 给内存数据结构。如果申请成功不会进行溢写,如果申请不成功,这时候会发生溢写磁盘。在溢写之前内存结构中的数据会进行排序分区 然后开始溢写磁盘,写磁盘是以 batch 的形式去写(批量),一个 batch 1万条数据map task 执行完成后,会将这些 磁盘小文件 合并成一个大的磁盘文件,同时生成一个 索引文件。reduce task map 端拉取数据的时候,首先解析索引文件,根据索引文件再去拉取对应的 数据。

产生的磁盘小文件的个数: 2*Mmap task的个数)

 8.3.2 bypass机制

 bypass 运行机制的触发条件如下

shuffle reduce task 的数量小于 spark.shuffle.sort.bypassMergeThreshold 的参数 值。这个值默认是 200

不需要进行 map 端的预聚合,比如 groupBykey , join 。产生的磁盘小文件为: 2*Mmap task的个数) 。

 九、Spark资源调度和任务调度

 9.1 调度流程

启动集群后, Worker 节点会向 Master 节点汇报资源情况, Master 掌握了集群资源情况。 当 Spark 提交一个 Application 后,根据 RDD 之间的依赖关系将 Application 形成一个 DAG 有向 无环图任务提交后, Spark 会在 Driver 端创建两个对象: DAGScheduler TaskScheduler ,DAGScheduler 任务调度高层调度器,是一个对象。DAGScheduler 主要作用就是DAG 根据 RDD 之间的宽窄依赖关系划分为一个个的 Stage ,然后将这些 Stage TaskSet 的形式提交给 TaskScheduler TaskScheduler 是任务调度的低层 调度器,这里 TaskSet 其实就是一个集合,里面封装的就是一个个的 task 任务,也就是 stage 中 的并行的 task 任务)。TaskSchedule 会遍历 TaskSet 集合,拿到每个 task 后会将 task 发送到 Executor 中去执行(其 实就是发送到 Executor 中的线程池 ThreadPool 去执行)。task Executor 线程池中的运行情况会向 TaskScheduler 反馈,当 task 执行失败时,则由 TaskScheduler 负责重试,将 task 重新发送Executor 去执行,默认重试3次。如果重试3然失败,那么这个 task 所在的 stage 失败了。stage 失败了则由 DAGScheduler 来负责重试,重新发送 TaskSet TaskScheduler Stage 默认重试4次。如果重试4次以后依然失败,那么这个 job 就失败了。 job 失败了, Application 就 失败了。TaskScheduler 不仅能重试失败的 task ,还会重试 straggling 落后,缓慢task ( 也就是执 行速度比其他task慢太多的task )。如果有运行缓慢的 task 那么 TaskScheduler 会启动一个新的task 来与这个运行缓慢的 task 执行相同的处理逻辑。两个 task 哪个先执行完,就以哪个 task 的执行结果为准。这就是 Spark 推测执行机制。在 Spark 中推测执行默认是关闭的。推测执行 可以通过 spark.speculation 属性来配置。

9.2 流程图解

 9.3 粗细粒度资源申请

9.3.1 粗粒度资源申请(Spark

Application 执行之前,将所有的资源申请完毕,当资源申请成功后,才会进行任务的调度,当所有的 task 执行完成后,才会释放这部分资源。

优点:Application 执行之前,所有的资源都申请完毕,每一个 task 直接使用资源就可以了,不需要 task 在执行前自己去申请资源, task 启动就快了, task 执行快了, stage 执行就快了, job 就快了, application 执行就快了。

缺点:直到最后一个 task 执行完成才会释放资源,集群的资源无法充分利用。

9.3.2 细粒度资源申请(MR

Application 执行之前不需要先去申请资源,而是直接执行,让 job 中的每一个 task 在执行前自己去申请资源, task 执行完成就释放资源。

优点:集群的资源可以充分利用。

缺点:task 自己去申请资源, task 启动变慢, Application 的运行就响应的变慢了。

十、算子(分区)

10.1 转换算子

mapPartitionsWithIndex

类似mapPartitions ,除此之外还会携带分区的索引值

repartition

增加或减少分区。此算子会产生 shuffle

coalesce

coalesce 常用来减少分区,算子中第二个参数是减少分区的过程中是否产生 shuffle

true 为产生 shuffle false 不产生 shuffle 。默认是 false

如果coalesce 设置的分区数比原来的 RDD 的分区数还多的话,第二个参数设置为 false 不 会起作用(转换之后分区数大于之前),如果设置成 true ,效果和 repartition 一样。

groupbykey

作用KV 格式的 RDD 上。根据 Key 进行分组。作用在 (KV) ,返回 (K,Iterable <V>)

zip

两个 RDD 中的元素( KV格式/KV格式 )变成一个 KV 格式的 RDD ,两个 RDD 个数必须相

zipWithIndex

该函数将 RDD 中的元素和这个元素在 RDD 中的索引号(从0开始)组合成 K,V对。

10.2 行动算子

countByKey

作用到 K,V 格式的 RDD 上,根据 Key 计数相同 Key 的数据集元素。

countByValue

根据数据集每个元素相同的内容来计数。返回相同内容的元素对应的条数。

reduce

根据聚合逻辑聚合数据集中的每个元素。

十一、广播变量和累加器

11.1 广播变量

 

注意事项

广播变量只能在 Driver 端定义,不能在 Executor 端定义。

Driver 端可以修改广播变量的值,在 Executor 端无法修改广播变量的值。

11.2 累加器

 

 

注意事项

累加器Driver 端定义赋初始值,累加器只能在 Driver 端读取,在 Excutor 端更新。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值