【计算引擎】spark笔记-shuffle及通信设计、内存

1.shuffle

1.1 mapreduce shuffle过程

  1. Map方法之后Reduce方法之前这段处理过程叫Shuffle
  2. Map方法之后,数据首先进入到分区方法,把数据标记好分区,然后把数据发送到环形缓冲区;环形缓冲区默认大小100m,环形缓冲区达到80%时,进行溢写;溢写前对数据进行排序,排序按照对key的索引进行字典顺序排序,排序的手段快排;溢写产生大量溢写文件,需要对溢写文件进行归并排序;对溢写的文件也可以进行Combiner操作,前提是汇总操作,求平均值不行。最后将文件按照分区存储到磁盘,等待Reduce端拉取。
  3. 每个Reduce拉取Map端对应分区的数据。拉取数据后先存储到内存中,内存不够了,再存储到磁盘。拉取完所有数据后,采用归并排序将内存和磁盘中的数据都进行排序。在进入Reduce方法前,可以对数据进行分组操作。
    shuflle

1.1.1 Spill过程

spill过程包括输出、排序、溢写、合并等步骤。

  1. collect
  • 每个Map任务不断地以k,v的形式将数据写到环形缓冲区(kvbuffer),使用环形数据结构是为了更有效地使用内存空间,在内存中缓存尽可能多的数据。
  • 写的时候分别从一个分界点的两边开始记录:kvmeta(元数据信息,包括分区等等),kv(真实的数据)。分界点随着每次spill都会随之更新。
  1. sortAndSpill
  • sort:环形缓冲区默认大小100m,环形缓冲区达到80%时,进行溢写;溢写前对数据进行排序,先把Kvbuffer中的数据按照partition值和key两个关键字按照升序排序,移动的知识索引数据,排序结果是kvmeta中数据按照partition为单位聚集在一起,同一partition内的按照key的字典顺序排序,排序方法为快排。
  • spill:移动了索引信息,通过spill产生一个新的索引文件 最后进行merge,随着kvbuffer上的数据增多,默认当数据占内存80%的时候,将数据从内存刷到磁盘上再接着往内存写数据。

1.1.2 merge

从所有本地目录上扫描得到Index文件,然后把索引信息存储在一个列表里。接着对溢写文件进行归并排序,merge过程中创建一个叫file.out的文件和一个叫file.out.Index的文件用来存储最终的输出和索引。

map端的shuffle过程到此结束

1.1.3 copy

每个节点都会启动一个常驻的Http sever,其中一项服务就是响应Reduce拖取Map数据,当mapoutput的http请求过来时,http sever就读取相应的map输出文件中对应这个reduce部分的数据通过网络流输出给reduce。

1.1.4 merge sort

拖一个map数据过来就会在内存或磁盘中创建一个文件,当文件数量到达到一定阈值,开始启动磁盘文件merge,把这些文件合并并输出到一个文件。这里使用的merge和map端使用的merge过程是一样的,map的输出数据已经是有序的,merge进行一次归并排序,所谓reduce端的sort过程就是这个过程。需要注意的是,reduce端的是一边copy一边sort,及copy和sort两个阶段是重叠而不是完全分开的。

reduce端的shuffle过程至此结束
shuffle

1.2 spark shuffle

一个shuffle包含两组任务:

  1. 产生shuffle数据的阶段,称为Shuffle write。
  2. 使用shuffle数据的阶段,称为Shuffle read。

spark shuffle演进的历史:
Spark 0.8 及以前 Hash Based Shuffle
Spark 0.8.1 为Hash Based Shuffle引入File Consolidation机制
Spark 0.9 引入ExternalAppendOnlyMap
Spark 1.1 引入Sort Based Shuffle,但默认仍为Hash Based Shuffle
Spark 1.2 默认的Shuffle方式改为Sort Based Shuffle
Spark 1.4 引入Tungsten-Sort Based Shuffle
Spark 1.6 Tungsten-sort并入Sort Based Shuffle
Spark 2.0 Hash Based Shuffle退出历史舞台

主要可总结为三个主要阶段:

  1. 未优化的 Hash Shuffle
  2. 优化后的 Hash Shuffle
  3. 普通的 Sort Shuffle
  4. bupass 机制的 Sort Shuffle

1.2.1 前置知识:Shuffle中的任务个数

Spark Shuffle分为map阶段和reduce阶段,或者称之为ShuffleRead阶段和ShuffleWrite阶段,那么对于一次Shuffle,map过程和reduce过程都会由若干个task来执行,那么map task和reduce task的数量是如何确定的呢?

假设Spark任务从HDFS中读取数据,那么初始RDD分区个数由该文件的split个数决定,也就是一个split对应生成的RDD的一个partition,我们假设初始partition个数为N。

初始RDD经过一系列算子计算后(假设没有执行repartition和coalesce算子进行重分区,则分区个数不变,仍为N,如果经过重分区算子,那么分区个数变为M),我们假设分区个数不变,当执行到Shuffle操作时,map端的task个数和partition个数一致,即map task为N个。

reduce端的stage默认取spark.default.parallelism这个配置项的值作为分区数,如果没有配置,则以map端的最后一个RDD的分区数作为其分区数(也就是N),那么分区数就决定了reduce端的task的个数。

1.2.1 未优化的Hash Shuffle

shuffle write阶段,主要就是在一个stage结束计算之后,为了下一个stage可以执行shuffle类的算子(比如reduceByKey),而将每个task处理的数据按key进行“划分”。所谓“划分”,就是对key执行hash算法,从而将相同key都写入同一个磁盘文件中,而每一个磁盘文件都只属于下游stage的一个task。在将数据写入磁盘之前,会先将数据写入内存缓冲中,当内存缓冲填满之后,才会溢写到磁盘文件中去。

下一个stage的task有多少个,当前stage的每个task就要创建多少份磁盘文件。比如下一个stage总共有100个task,那么当前stage的每个task都要创建100份磁盘文件。如果当前stage有50个task,总共有10个Executor,每个Executor执行5个task,那么每个Executor上总共就要创建500个磁盘文件,所有Executor上会创建共5000个磁盘文件。由此可见,未经优化的shuffle write操作所产生的磁盘文件的数量是极其惊人的。

shuffle read阶段,通常就是一个stage刚开始时要做的事情。此时该stage的每一个task就需要将上一个stage的计算结果中的所有相同key,从各个节点上通过网络都拉取到自己所在的节点上,然后进行key的聚合或连接等操作。由于shuffle write的过程中,map task给下游stage的每个reduce task都创建了一个磁盘文件,因此shuffle read的过程中,每个reduce task只要从上游stage的所有map task所在节点上,拉取属于自己的那一个磁盘文件即可
hash shuffle

1.2.2 优化后的Hash Shuffle

上述Shuffle过程产生的文件过多问题,spark有另一钟改进的Shuffle过程:consolidation Shuffle,显著减少Shuffle文件的数量。为了优化HashShuffleManager我们可以设置一个参数,spark.shuffle. consolidateFiles,该参数默认值为false,将其设置为true即可开启优化机制,通常来说,如果我们使用HashShuffleManager,那么都建议开启这个选项。

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

当Executor的CPU core执行完一批task,接着执行下一批task时,下一批task就会复用之前已有的shuffleFileGroup,包括其中的磁盘文件,也就是说,此时task会将数据写入已有的磁盘文件中,而不会写入新的磁盘文件中。因此,consolidate机制允许不同的task复用同一批磁盘文件,这样就可以有效将多个task的磁盘文件进行一定程度上的合并,从而大幅度减少磁盘文件的数量,进而提升shuffle write的性能。

假设第二个stage有100个task,第一个stage有50个task,总共还是有10个Executor(Executor CPU个数为1),每个Executor执行5个task。那么原本使用未经优化的HashShuffleManager时,每个Executor会产生500个磁盘文件,所有Executor会产生5000个磁盘文件的。但是此时经过优化之后,每个Executor创建的磁盘文件的数量的计算公式为:CPU core的数量 * 下一个stage的task数量,也就是说,每个Executor此时只会创建100个磁盘文件,所有Executor只会创建1000个磁盘文件。
consolidation Shuffle
实现了一个资源的复用的过程,一个shuffleMapTask将数据写入ResultTask的本地文件,当下一个shuffleMapTask运行的时候,复用shuffleMapTask的内存缓存,磁盘文件的资源,
直接将数据写入之前shuffleMapTask的本地文件,相当于多个shuffleMapTask的输出进行了合并,大大减少了本地的磁盘数量。

总结:

  1. 未优化版本中,每一个task任务都会根据reduce任务的个数创建对应数量的bucket,bucket其实就是写入缓冲区,每一个bucket都会存入一个文件,这个文件叫blocks,最大的劣势是产生的文件过多
  2. 在优化版本中,主要通过consolidation这个参数进行优化,实现了ShuffleFileGroup的概念,不同批次的task任务可以复用最终写入的文件,来整体减少文件的数量

1.2.3 Sort Shuffle

从1.2.0开始默认认为sort shuffle(spark.shuffle.manager = sort),实现逻辑类似于hadoop mapreduce,hash shuffle 每一个reduce产生一个文件,但是sort shuffle只是产生一个按照reducer id排序可索引的文件和一个数据文件,属于同一个reduce的数据但不排序。与hash shuffle相比,shuffle文件数量减少,内存使用更加可控,但同时排序会影响速度。

SortShuffleManager的运行机制主要分成两种,一种是普通运行机制,另一种是bypass运行机制。当shuffle read task的数量小于等于spark.shuffle.sort. bypassMergeThreshold参数的值时(默认为200),就会启用bypass机制。

使用byPass机制,Hashshuffle在reduce数量比较少的时候性能要比Sortshuffle要高,所以如果reduce数量少于bypass定义的数值的时候,sortshuffle在task任务写出的时候会采用hash的方式,而不会采用ApplyOnlyMap(本质上是一个内存溢出时会spill到硬盘的哈希map)以及排序。
sortshuffle
在普通运行机制下,数据会先写入一个内存数据结构中,此时根据不同的shuffle算子,可能选用不同的数据结构。如果是reduceByKey这种聚合类的shuffle算子,那么会选用Map数据结构,一边通过Map进行聚合,一边写入内存;如果是join这种普通的shuffle算子,那么会选用Array数据结构,直接写入内存。接着,每写一条数据进入内存数据结构之后,就会判断一下,是否达到了某个临界阈值。如果达到临界阈值的话,那么就会尝试将内存数据结构中的数据溢写到磁盘,然后清空内存数据结构。

在溢写到磁盘文件之前,会先根据key对内存数据结构中已有的数据进行排序。排序过后,会分批将数据写入磁盘文件。默认的batch数量是10000条,也就是说,排序好的数据,会以每批1万条数据的形式分批写入磁盘文件。写入磁盘文件是通过Java的BufferedOutputStream实现的。BufferedOutputStream是Java的缓冲输出流,首先会将数据缓冲在内存中,当内存缓冲满溢之后再一次写入磁盘文件中,这样可以减少磁盘IO次数,提升性能。

一个task将所有数据写入内存数据结构的过程中,会发生多次磁盘溢写操作,也就会产生多个临时文件。最后会将之前所有的临时磁盘文件都进行合并,这就是merge过程,此时会将之前所有临时磁盘文件中的数据读取出来,然后依次写入最终的磁盘文件之中。此外,由于一个task就只对应一个磁盘文件,也就意味着该task为下游stage的task准备的数据都在这一个文件中,因此还会单独写一份索引文件,其中标识了下游各个task的数据在文件中的start offset与end offset。

SortShuffleManager由于有一个磁盘文件merge的过程,因此大大减少了文件数量。比如第一个stage有50个task,总共有10个Executor,每个Executor执行5个task,而第二个stage有100个task。由于每个task最终只有一个磁盘文件,因此此时每个Executor上只有5个磁盘文件,所Executor只有50个磁盘文件。

1.3 spark目前运作的实现模式

在每个map任务中,数据被写入本地磁盘,然后在reduce任务中会远程请求读取这些数据。由于shuffle使用的是all-to-all模式,任何map任务输出的记录组都可能用于任意reduce。一个job在map时的shuffle操作基于以下原则:所有用于同一个reduce操作的结果都会被写入到相邻的组别中,以便获取数据时更为简单

Spark默认的shuffle实现,即hash-based shuffle,实现时spark实现时Spark必须维持大量的内存消耗,或者造成大量的随机磁盘I/O,同时产生生总共M(map的数量)*R(reduce的数量)个数量的临时文件。

之后,采用Shuffle consolidation将这个数量减至C(是同时能够运行的map任务数量)* R个文件,即便这样修改后,在运行的reducer数量过多时还是经常会出现“文件打开过多”的限制。

为了进一步提高shuffle的稳定性与性能,从1.1版本开始,Spark引入了 “sortbased shuffle” 实现,其功能与MapReduce使用的map方式十分类似。在部署时每个任务的 map输出结果都会被储存在内存里(直到可用内存耗尽),然后在reduce任务中进行排序,之后再spill到一个单独的文件。如果在单个任务中该操作发生了多次,那么这个任务的输出将被合并。

在reduced的过程中,一组线程负责抓取远程的map输出blocks。当数据进入后,它们会被反序列化,再转化成一个适用于执行all-to-all操作的数据结构。在类似groupByKey、reduceByKey,还有aggregateByKey之类的聚合操作中,其结果会变成一个ExternalAppendOnlyMap(本质上是一个内存溢出时会spill到硬盘的哈希map)。在类似sortByKey的排序操作中,输出结果会变成一个ExternalSorter(将结果分类后可能会spill到硬盘,并在对结果进行排序后返回一个迭代程序)。

上述所描述的方式有两个弊端

  • 每个Spark reduce的任务都需要同时打开大量的反序列化记录,从而导致内存的大量消耗,而大量的Java对象对JVM的垃圾收集(garbagecollection)产生压力,会造成系统变慢和卡顿,同时由于这个版本较之序列化的版本内存消耗更为巨大,因而Spark必须更早更频繁的spill,造成硬盘I/O也更为频繁。此外,由于判断反序列化对象的内存占用情况时难以达到100%的准确率,因此保持大量的反序列化对象会加剧内存不足的可能性。

  • 在引导需要在分片内的排序操作时,我们需要进行两次排序:mapper时按分片排序,reducer时按Key排序。

如何改进呢?

  1. 可以在map时在分片内按Key对结果进行排序,这样在reduce时我们只要合并每个map任务排序后的blocks即可。我们可以按照序列化的模式将每个block存到内存中,然后在合并时逐一地将结果反序列化。这样任何时候,内存中反序列化记录的最大数量就是己经合并的blocks总量。

  2. 单个reduce任务可以接收来自数以千计map任务的blocks,为了使得这个多路归并更加高效,尤其是在数据超过可用内存的情况下,则引入了分层合并(tiered merge)的概念。

1.4 mapreduce和spark的shuffle区别

  1. 从整体功能上看,两者并没有大的差别。
    都是将 mapper(Spark 里是 ShuffleMapTask)的输出进行 partition,不同的 partition 送到不同的 reducer(Spark 里 reducer 可能是下一个 stage 里的 ShuffleMapTask,也可能是 ResultTask)。Reducer 以内存作缓冲区,边 shuffle 边 aggregate 数据,等到数据 aggregate 好以后进行 reduce() (Spark 里可能是后续的一系列操作)。
  2. 从流程的上看,两者差别不小。
    MapReduce是sort-based,进入combine()和reduce()的records必须先sort。这样的好处在于combine/reduce()可以处理大规模的数据,因为其输入数据可以通过外排得到(mapper对每段数据先做排序,reducer 的 shuffle 对排好序的每段数据做归并)。
    以前Spark默认选择的是hash-based,通常使用HashMap来对shuffle来的数据进行aggregate,不会对数据进行提前排序。如果用户需要经过排序的数据,那么需要自己调用类似sortByKey()的操作;如果你是Spark 1.1的用户,可以将spark.shuffle.manager设置为sort,则会对数据进行排序。在Spark 1.2中,sort将作为默认的Shuffle实现。
  3. 从流程实现角度来看,两者有不少差别。
    MapReduce将处理流程划分出明显的几个阶段:map()、spill、merge、shuffle、sort、reduce()等。每个阶段各司其职,可以按照过程式的编程思想来逐一实现每个阶段的功能。
    Spark中,没有这样功能明确的阶段,只有不同的stage和一系列的transformation(),所以spill、merge、aggregate等操作需要蕴含在transformation()中。
对比方向MapReduceSpark
collect在内存中构造了一块数据结构用于map输出的缓冲没有在内存中构造一块数据结构用于map输出的缓冲,而是直接把输出写到磁盘文件
sortmap输出的数据有排序map输出的数据没有排序
merge对此磁盘上的多个spill文件进行合并成一个输出文件在map端没有merge过程,在输出时直接是对应一个reduce的数据写到一个文件中,这些文件同时存在并发写,最后不需要合并成一个
通信jetty框架netty或者socket流
本地文件通过网络框架拖取数据不通过网络框架,对于在本节点上的map输出文件,采用本地读取的方式
copy过来的数据存放位置,先放在内存,内存放不下时写到磁盘一种方式全部放在内存;另一种方式先放内存,放不下时写到磁盘
merge sort最后会对磁盘文件和内存中的数据进行合并排序对于采用另一种方式时也会合并排序的过程

1.5 shuffle总结

Shuffle 过程本质上都是将 Map 端获得的数据使用分区器进行划分,并将数据发送给对应的 Reducer 的过程。

Shuffle作为处理连接map端和reduce端的枢纽,其shuffle的性能高低直接影响了整个程序的性能和吞吐量。map端的shuffle一般为shuffle的Write阶段,reduce端的shuffle一般为shuffle的read阶段。Hadoop和spark的shuffle在实现上面存在很大的不同,spark的shuffle分为两种实现,分别为HashShuffle和SortShuffle。

HashShuffle又分为普通机制和合并机制,普通机制因为其会产生MR个数的巨量磁盘小文件而产生大量性能低下的Io操作,从而性能较低,因为其巨量的磁盘小文件还可能导致OOM,HashShuffle的合并机制通过重复利用buffer从而将磁盘小文件的数量降低到CoreR个,但是当Reducer 端的并行任务或者是数据分片过多的时候,依然会产生大量的磁盘小文件。

SortShuffle也分为普通机制和bypass机制,普通机制在内存数据结构(默认为5M)完成排序,会产生2M个磁盘小文件。而当shuffle map task数量小于spark.shuffle.sort.bypassMergeThreshold参数的值。或者算子不是聚合类的shuffle算子(比如reduceByKey)的时候会触发SortShuffle的bypass机制,SortShuffle的bypass机制不会进行排序,极大的提高了其性能。

在Spark 1.2以前,默认的shuffle计算引擎是HashShuffleManager,因为HashShuffleManager会产生大量的磁盘小文件而性能低下,在Spark 1.2以后的版本中,默认的ShuffleManager改成了SortShuffleManager。

SortShuffleManager相较于HashShuffleManager来说,有了一定的改进。主要就在于,每个Task在进行shuffle操作时,虽然也会产生较多的临时磁盘文件,但是最后会将所有的临时文件合并(merge)成一个磁盘文件,因此每个Task就只有一个磁盘文件。在下一个stage的shuffle read task拉取自己的数据时,只要根据索引读取每个磁盘文件中的部分数据即可。

2. spark 内存管理

在执行Spark的应用程序时,Spark集群会启动Driver和Executor两种JVM进程,前者为主控进程,负责创建Spark上下文提交Spark作业(Job),并将作业转化为计算任务(Task),在各个Executor进程间协调任务的调度,后者负责在工作节点上执行具体的计算任务,并将结果返回给Driver,同时为需要持久化的RDD提供存储功能。由于Driver的内存管理相对来说较为简单,所以本文主要对Executor的内存管理进行分析,下文中的Spark内存均特指Executor的内存。

  1. 内存分配模式上,主要分为静态分配以及统一分配两种方式。静态就是固定大小,统一分配是存储区和Shuffle区可以动态占用。
  2. 有几种内存配置模式:
  • other区,一般占用20%的内存区域,主要是用于代码的运行以及相关数据结构的运行。
  • Execution区,这个区域一般占用20%的内存区域,主要通过spark.shuffle.memoryFraction参数指定。主要用于Shuffle过程的内存消耗。
  • Storage区,这个区域主要用于RDD的缓存,主要通过spark.storage.memoryFraction参数指定,一般会占用60%的区域。
  1. spark内存管理包括两部分:堆内,堆外

总体都不足存储到硬盘,部分不足占用其他的内存,用完归还

  1. 堆内包括:storage,exeution,other

堆内内存:Java分配的非空对象都是由java虚拟机的垃圾收集器管理的,这一部分称为堆内内存,虚拟机会定期对垃圾内存进行回收,在某些特定的时间点,它会进行一次彻底的回收(full gc)。彻底回收时,垃圾收集器会对所有分配的堆内内存进行完整的扫描,这意味着一个重要的事实——这样一次垃圾收集对Java应用造成的影响,跟堆的大小是成正比的。过大的堆会影响Java应用的性能。所以说给应用分配的内存越大,系统性能就越好,因为对应的垃圾回收的代价也就越大。

三个大部分比例为6:2:2,每个部分都有预留不会造成内存溢出的情况,storage主要处理RDD和broadcast数据,execution处理shuffle的数据,other管理的是元数据
内存1

  1. 堆外包括:storage,exeution

堆外内存: 把内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机)。这样做的结果就是能保持一个较小的堆,以减少垃圾收集对应用的影响。

缺点:内存难以控制,使用了堆外内存就间接失去了JVM管理内存的可行性,改由自己来管理,当发生内存溢出时排查起来非常困难。

两者比例为5:5,主要存储序列化后的二进制数据。
内存2

3. Spark RPC通信层设计:

Spark一开始使用 Akka, 2版本以后完全抛弃akka,全部使用Netty,最主要原因:

  1. spark对akka没有维护,需要akka更新,受到了牵制。
  2. akka版本之间无法通信。
  3. akka兼容性
  • 什么是Akka?
    Akka 使用一种叫 Actor 的编程模型,Actor 编程模型是和面向对象编程模型平行的一种编程模型。Actor 编程模型认为一切都是 Actor,Actor 之间也是通过消息传递实现复杂的功能,但是这里的消息是真正意义上的消息。不同于面向对象编程时,方法调用是同步阻塞的,也就是被调用者在处理完成之前,调用者必须阻塞等待;给 Actor 发送消息不需要等待 Actor 处理,消息发送完就不用管了,也就是说,消息是异步的。

Actor 编程模型很好地利用了多核 CPU 与分布式的特性,可以轻松实现并发、异步、分布式编程。

  • 什么是Netty?
    在软件栈中,Akka相比Netty要Higher一点,它专门针对RPC做了很多事情,而Netty相比更加基础一点,可以为不同的应用层通信协议(RPC,FTP,HTTP等)提供支持。

在早期的Akka版本,底层的NIO通信就是用的Netty;

最后,虽然Netty没有Akka协程级的性能优势,但是Netty内部高效的Reactor线程模型,无锁化的串行设计,高效的序列化,零拷贝,内存池等特性也保证了Netty不会存在性能问题。

Spark利用偏函数的特性,基于Netty“仿造”出一个简约版本的Actor模型。

4.wordcount运行过程及原理

sc.textFile(args(0)).flatMap(_.split(" ")).map(x => (x, 1)).reduceBykey(_ + _).take(10).foreach(printIn)

spark集群中运行wordcount程序其主要业务逻辑比较简单,涵盖了三个过程:

  1. 读取存储介质上的文本文件(一般存储在hdfs上)
  2. 对文本文件内容进行解析,按照单词进行分组统计汇总
  3. 将过程2的分组结果保存到存储介质上。(一般存储在hdfs或者RMDB上)
    wordcount
    程序在运行过程中涉及几个核心的RDD,主要有textFileRDD、flatMapRDD、maoToPairRDD、shuffleRDD(reduceByKey)等。
  • 具体流程:
  1. 首先应用程序通过textFile方法读取hdfs上的文本文件,数据分片的形式以RDD为统一模式将数据加载到不同的物理节点上;
  2. 通过一系列的数据转换,如利用flatMap将文本文件中对应每行数据进行拆分(文本文件中单词以空格为分割符号),形成一个以每个单词为核心新的数据集合RDD;
  3. 之后通过MapRDD继续转换形成形成(K,V)数据形式,以便进一步使用reduceByKey方法,该方法会触发shuffle行为,促使不同的单词到对应的节点上进行汇聚统计(实际上在夸节点进行数据shuffle之前会在本地先对相同单词进行合并累加),形成wordcount的统计结果;
  4. 最终通过 saveAsTextFile方法将数据保存到 hdfs上
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

孟知之

如果能帮助到你们,可否点个赞?

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值