创建测试数据。
创建一个名为“words6.3.3.txt”的新文件,并将其上传到HDFS根路径下。文件内容如下(中间所有分隔符均为“\t”):
goodbye Alice googbye Bob goodbye Thomas
goodbye Alice googbye Bob goodbye Thomas
goodbye Alice googbye Bob goodbye Thomas
通过Spark-shell输入以下代码,注意:textFile()的第二个参数,我修改为3了。
scala> val rddData1 = sc.textFile("hdfs://mycluster:8020/words6.3.3.txt", 3)
rddData1: org.apache.spark.rdd.RDD[String] = hdfs://mycluster:8020/words6.3.3.txt MapPartitionsRDD[6] at textFile at <console>:24
scala> val rddData2 = rddData1.flatMap(_.split("\t"))
rddData2: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[7] at flatMap at <console>:26
scala> val rddData3 = rddData2.map((_, 1))
rddData3: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[8] at map at <console>:28
scala> val rddData4 = rddData3.reduceByKey(_ + _, 1)
rddData4: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[9] at reduceByKey at <console>:30
scala> rddData4.collect
访问http://10.222.113.237:4040/jobs/,可见图1(注:你的IP可能和我的不同哈):
图 1
分析如下:
(1)产生Job(工作)。
将程序提交给Master服务之后,会产生Spark Application(应用程序),随即产生一个Driver进程。在代码中,执行多少次行动操作,就会产生多少个Job。在本例中,只针对rddData4执行了collect操作,所以只产生了一个Job,见图1。
补充,点击超链接“collect at :33”,可见图2:
图2
图2中为什么有2个阶段(Stage)而不是1个、3个或其他呢?书上没有直接说,但在其他章节曾提到:从最后一个RDD,即这里的rddData4,向第一个RDD回溯,当遇到宽依赖时就新生成一个阶段(Stage)。本文中,仅有reduceByKey产生宽依赖,因此,只有2个阶段(Stage)。
(2)第一个阶段:Stage0。
每一个阶段内所有的RDD之间的转换关系都遵循窄依赖定义。点击超链接“map at :28”,可以见到图3。
图3
图3中为什么是3个Task,而不是其他呢?每个 stage 里面 Task 的数目由该 stage 最后一个 RDD 中的 分区(Partition)个数决定。下面这行代码我这里和书上不一样,我显式指定了minPartitions为3,书上的代码未指定,默认minPartitions为2。
val rddData1 = sc.textFile("hdfs://linux01:8020/words6.3.3.txt", 3)
(3)第二个阶段:Stage1。
由于reduceByKey操作产生了宽依赖,这属于Shuffle的过程,因此划分出一个新的阶段:Stage1,见图4。
图4
(4)生产Task(任务)。
同一个Stage阶段内,RDD之间都属于窄依赖。这意味着:在同一个Stage阶段中,每个RDD的分区数量是一致的。所以,在同一个Stage阶段中,单个RDD的分区数量也是这个Stage阶段中Task任务的数量。
**提示:在每个Worker服务所在的节点上,允许启动多个Executor进程,每个Executor进程被分配到若干个CPU内核。严格意义上讲,同一个CPU内核同一时间只能执行一个Task(任务)。所以,Spark程序的并发数受CPU内核数量限制。**结合图5,多多体会这句话!!!
图5
摘录自《Spark大数据分析——源码解析与实例详解》,6.3.4 实例36:词频统计——总结运算过程中涉及的概念