死磕spark中的job、stage、task

写在前面

台风夜的电话面试里被问到了spark运行任务的过程中stage的划分依据。一下子就给整懵了,支支吾吾答非所问。从事大数据的开发也有一年半光景,spark任务的运行原理依旧知之甚少。因此就参阅各种优秀的文章,再配上一个自己工作中的实际项目,特意整理出这篇笔记,以此警示自己的自大与无知。

测试环境

本地开发环境

  • idea 2019.1.2
  • maven 3.6
  • spark 2.4.3
  • scala 2.1.8
  • jdk1.8

测试集群环境

  • spark 2.4.3
  • scala 2.1.8
  • jdk1.8
  • hadoop2.7.4
集群

测试项目例子

计算某一天店铺销售额\时段销售额top10

样例数据字段格式

filed1|filed2|filed3|store_no|filed5|filed6|filed7|filed8|amount|filed10|filed11|sale_time

这里不提供具体的测试数据,实验过程中需要自己模拟所用的数据。

样例demo

package com.dr.leo

import com.dr.leo.utils.StrUtils
import org.apache.hadoop.io.{LongWritable, Text}
import org.apache.hadoop.mapred.{FileSplit, InputSplit, TextInputFormat}
import org.apache.spark.SparkContext
import org.apache.spark.rdd.{HadoopRDD, RDD}
import org.apache.spark.sql.SparkSession

/**
  * @author leo.jie (weixiao.me@aliyun.com)
  * @organization DataReal
  * @version 1.0
  * @website https://www.jlpyyf.com
  * @date 2019-07-28 20:29
  * @since 1.0
  */
object WordCount {
  def main(args: Array[String]): Unit = {
    val spark: SparkSession = SparkSession.builder()
      .appName("WordCount")
      //.master("local[*]")
      .enableHiveSupport()
      .getOrCreate()
    val sc: SparkContext = spark.sparkContext
    val fileRddOri = loadFileToRdd(sc, "hdfs://leo/test/pos.DAT")
    val fileRdd = fileRddOri.map(x => for (data <- x._2.split("\\|")) yield if (data == null) "" else data.trim)
      .filter(x => x.length == 12)
      .map(x => (retailer_shop_code(x(3)), x(10).substring(10, 13), x(8).toFloat))
      .map(x => ((x._1, x._2), x._3))
      .reduceByKey(_ + _)

    fileRdd.top(10)(Ordering.by(e => e._2)).foreach(println(_))
    println("##########################################################")
    fileRdd.map(x => (x._1._1, x._2)).reduceByKey(_ + _).top(10)(Ordering.by(e => e._2)).foreach(println(_))
    println("##########################################################")
    println(fileRdd.count())
    println("##########################################################")
    println(fileRdd.first())
    println("##########################################################")
    fileRdd.take(10).foreach(println(_))
    while (true) {
      ;
    }
    spark.stop()
  }

  /**
    * 读取gbk编码的file
    * @param sc
    * @param path
    * @param encoding
    * @return
    */
  def loadFileToRdd(sc: SparkContext, path: String, encoding: String = "GBK"): RDD[(String, String, Int)] = {
    sc.hadoopFile[LongWritable, Text, TextInputFormat](path)
      .asInstanceOf[HadoopRDD[LongWritable, Text]]
      .mapPartitionsWithInputSplit((inputSplit: InputSplit, iterator: Iterator[(LongWritable, Text)]) => {
        val file = inputSplit.asInstanceOf[FileSplit]
        iterator.filter(x => x._2 != null).map(x => {
          (file.getPath.getName, new String(x._2.getBytes, 0, x._2.getLength, encoding), 1)
        })
      })
  }

  /**
    * 只是一个店铺号转换的函数
    * @param retailer_shop_code
    * @return
    */
  def retailer_shop_code(retailer_shop_code: String): String = {
    if (StrUtils.isBlank(retailer_shop_code)) ""
    else if (retailer_shop_code.length == 5) retailer_shop_code.substring(0, retailer_shop_code.length - 1).toUpperCase()
    else if (retailer_shop_code.length == 6) retailer_shop_code.substring(0, retailer_shop_code.length - 2).toUpperCase()
    else if (retailer_shop_code.length == 8) retailer_shop_code.substring(0, retailer_shop_code.length - 2).toUpperCase()
    else retailer_shop_code
  }
}

运行测试

程序打包后发往集群提交任务。所用命令

[hadoop@node1 leo_demo]$ spark-submit --master yarn --deploy-mode cluster --driver-memory 2G --driver-cores 2 --executor-memory 2g --num-executors 5 --executor-cores 2 --conf spark.yarn.executor.memoryOverhead=2048 --conf spark.network.timeout=30000 --class com.dr.leo.WordCount leo-study-spark.jar

spark-ui上的信息告诉了我们什么?

查看任务的运行信息

spark任务运行的过程中,我们可以点击 <code style="color:red">ApplicationMaster</code> 跳转任务运行的界面。

yarn

运行流程之:job

job

此时我们提交的任务的所有job都已经运行成功,只因为程序中任务执行完毕后是一段无限循环,所以这个界面会一直存在,直到我们手动在yarn上kill掉这个application。

我们写的代码被提交运行的过程中,会先被划分为一个又一个job,这些job按照被划分的先后顺序会依次执行。

图示中我们已经知道,我们提交的任务,最终被划分成了5个job。

<p style="color:red">
所谓一个job,就是由一个rdd action触发的动作。简单理解为,当你需要执行一个rdd的action操作的时候,就会生成一个job。
</p>

这里不会赘述什么是rdd的action操作。

结合代码与图示我们可以知道:

job-0 的产生是由于触发了top操作

<code style="color:red">top at WordCount.scala:33</code>

job-1 的产生是由于触发了top操作

<code style="color:red">top at WordCount.scala:35</code>

job-2 的产生是由于触发了count操作

<code style="color:red">count at WordCount.scala:37</code>

job-3 的产生是由于触发了first操作

<code style="color:red">first at WordCount.scala:39</code>

job-4 的产生是由于触发了take操作

<code style="color:red">take at WordCount.scala:41</code>

运行流程之:stage

选择任意一个job,点击链接去查看该job的detail,这里我们选择job-0。

job-detail

由图示我们可知,job-0由两个stage组成,并且每个stage都有8个task,说明每个stage的数据都在8个partition上。

下面我们将详细说明stage以及stage的划分依据。

stage 概念、划分

貌似没有十分明确的概念来十分清楚地说明spark stage究竟是什么?这里只记录从众多优秀的博客里提取的直言片语,以及本人的一点见解。

stage的划分是以shuffle操作作为边界的。也就是说某个action导致了shuffle操作,就会划分出两个stage。

stage的划分在RDD的论文中也有详细介绍,简单的说是以shuffle和result这两种类型来划分。在spark中有两类task,一类是shuffle map task,一类是result task,第一类task的输出是shuffle所需数据,第二类task的输出是result,stage的划分也依次为依据,shuffle之前的所有变换是一个stage,shuffle之后的操作是另一个stage。比如 rdd.parallize(1 to 10).foreach(println) 这个操作没有shuffle,直接就输出了,那么只有它的task是resultTask,stage也只有一个;如果是rdd.map(x => (x, 1)).reduceByKey(_ + _).foreach(println), 这个job因为有reduce,所以有一个shuffle过程,那么reduceByKey之前的是一个stage,执行shuffleMapTask,输出shuffle所需的数据,reduceByKey到最后是一个stage,直接就输出结果了。如果job中有多次shuffle,那么每个shuffle之前都是一个stage。

在DAGScheduler中,会将每个job划分成多个stage,每个stage会创建一批task并且计算task的最佳位置,一个task对应一个partition。DAGScheduler的stage划分算法如下:它会从触发action操作的那个RDD开始往前推,首先会为最后一个RDD创建一个stage,然后往前倒推的时候,如果发现对某个RDD是宽依赖,那么就会将宽依赖的那个RDD创建一个新的stage,那个RDD就是新的stage的最后一个RDD,然后依次类推,继续往前倒推,根据窄依赖或者宽依赖进行stage的划分,直到所有的RDD全部遍历完成为止。

spark任务会根据RDD之间的依赖关系,形成一个DAG有向无环图,DAG会提交给DAGScheduler,DAGScheduler会把DAG划分成相互依赖的多个stage,划分stage的依据就是RDD之间的宽窄依赖。<code style="color:red">遇到宽依赖就划分stage</code>,每个stage包含一个或多个task任务。然后将这些task以task set的形式交给TaskScheduler运行。<code style="color:red">stage是由一组并行的task组成</code>。

窄依赖

父RDD和子RDD partition之间的关系是一对一的。不会有shuffle的产生。父RDD的一个分区去到子RDD的一个分区中。

宽依赖

父RDD与子RDD partition之间的关系是一对多的。会有shuffle的产生。父RDD的一个分区去到子RDD的不同分区里面。

其实区分宽窄依赖,主要就是看父RDD的一个partition的流向,要是流向一个的话就是窄依赖,流向多个的话就是宽依赖。以WordCount为例,看图理解:

spark stage

运行流程之:task

task是stage下的一个任务执行单元,一般来说,一个rdd有多少个partition,就会有多少个task,因为每一个task只是处理一个partition上的数据。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,881评论 4 368
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,052评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,598评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,407评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,823评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,872评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,037评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,778评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,505评论 1 247
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,745评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,233评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,568评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,231评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,141评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,939评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,954评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,784评论 2 275

推荐阅读更多精彩内容