Spark高阶编程-如何编写高效代码

Spark高阶编程


1.Spark源码解析

​ 以下以yarn-client提交sparkPI任务流程为例

  • ​ 执行${SPARK_HOME}/bin/spark-submit 提交任务命令

    spark-submit --master yarn --deploy-mode client --executor-cores 1 --num-executors 1 --class org.apache.spark.examples.SparkPi  ${SPARK_HOME}/examples/jars/spark-examples_2.11-2.3.2.jar 1000
    
  • spark-submit执行脚本内会执行spark-class脚本,spark-class脚本内执行submit.main方法

    build_command() {
      "$RUNNER" -Xmx128m -cp "$LAUNCH_CLASSPATH" org.apache.spark.launcher.Main "$@"
      printf "%d\0" $?
    }
    
  • 执行org.apache.spark.launcher.Main方法,这个方法主要是环境参数准备和校验,执行结束返回至spark-classshell脚本,执行到最后一行

    CMD=("${CMD[@]:0:$LAST}")
    #增加的代码输出命令内容
    echo ${CMD[@]}
    exec "${CMD[@]}
    
    #echo 输出内容
    /usr/local/java/jdk1.8.0_162/bin/java -cp /home/rose/app/hadoop/spark-2.4.5-bin-hadoop2.7/conf/:/home/rose/app/hadoop/spark-2.4.5-bin-hadoop2.7/jars/*:/home/rose/app/hadoop/hadoop-2.7.7/etc/hadoop/ -Xmx800m org.apache.spark.deploy.SparkSubmit --master yarn --deploy-mode client --class org.apache.spark.examples.SparkPi --executor-cores 1 --num-executors 1 spark-examples_2.11-2.3.2.jar 30
    
  • 上面命令执行org.apache.spark.deploy.SparkSubmit中main方法

    依次执行doSubmit >submit >runMain

    runMain中先对sparkConf中必要的一些参数及参数校验,然后构造JavaMainApplication(),执行start启动命令

    start中利用反射执行我们提交的jar包中的指定的–class org.apache.spark.examples.SparkPi中的main方法

  • SparkPi中的main方法

    def main(args: Array[String]) {
        val spark = SparkSession
          .builder
          .appName("Spark Pi")
          .getOrCreate()
        val slices = if (args.length > 0) args(0).toInt else 2
        val n = math.min(100000L * slices, Int.MaxValue).toInt // avoid overflow
        val count = spark.sparkContext.parallelize(1 until n, slices).map { i =>
          val x = random * 2 - 1
          val y = random * 2 - 1
          if (x*x + y*y <= 1) 1 else 0
        }.reduce(_ + _)
        println(s"Pi is roughly ${4.0 * count / (n - 1)}")
        spark.stop()
      }
    
  • 首先构建SparkSession,SparkSession中构建SparkContext

  • SparkContext构建过程,也是整个Spark应用的发动核心,只列出核心步骤代码

    //sparkConf拷贝校验
    _conf = config.clone()
    _conf.validateSettings()
    //获取用户上传的计算jar包
    _jars = Utils.getUserJars(_conf)
    //构建事件监听总线
    _listenerBus = new LiveListenerBus(_conf)
    //构建状态存储并加入事件监听总线
    _statusStore = AppStatusStore.createLiveStore(conf)
    listenerBus.addToStatusQueue(_statusStore.listener.get)
    //创建spark运行环境,netty RpcEnv工厂类、serializerManager序列化器、BroadcastManager、MapOutputTrackerMaster、shuffleManager、memoryManagerblock、ManagerMaster、blockManager
    _env = createSparkEnv(_conf, isLocal, listenerBus)
    //sparkEnv创建过程中创建netty Rpc工厂类,并在本机创建Driver
    val rpcEnv = RpcEnv.create(systemName, bindAddress, advertiseAddress, port.getOrElse(-1), conf,
          securityManager, numUsableCores, !isDriver)
    //sparkContext创建SparkUI
    Some(SparkUI.create(Some(this), _statusStore, _conf, _env.securityManager, appName, "",
              startTime))
    //创建HeartbeatReceiver
    _heartbeatReceiver = env.rpcEnv.setupEndpoint(
          HeartbeatReceiver.ENDPOINT_NAME, new HeartbeatReceiver(this))
    //创建taskScheduler、DAGScheduler
    val (sched, ts) = SparkContext.createTaskScheduler(this, master, deployMode)
        _schedulerBackend = sched
        _taskScheduler = ts
        _dagScheduler = new DAGScheduler(this)
        _heartbeatReceiver.ask[Boolean](TaskSchedulerIsSet)
    //启动任务调度器,内部通过yarn Client客户端通过RPC在yarn集群中创建ApplicationMaster容器,负责计算任务资源申请
    _taskScheduler.start()
    //driver端blockManger初始化,并向Driver 中blockMangerMaster注册
    _env.blockManager.initialize(_applicationId)
    //指标监控系统启动,监控每个任务计算状态
     _env.metricsSystem.start()
        _env.metricsSystem.getServletHandlers.foreach(handler => ui.foreach(_.attachHandler(handler)))
    //创建executorAllocationManager,并通过schedulerBackend(TaskScheduler)向RM申请创建执行器容器,挨个启动执行器,每个执行器向driver Actor端注册生效,同时每个执行器blockManger向Driver actor端blockManagerMasger注册生效
    _executorAllocationManager =
          if (dynamicAllocationEnabled) {
            schedulerBackend match {
              case b: ExecutorAllocationClient =>
                Some(new ExecutorAllocationManager(
                  schedulerBackend.asInstanceOf[ExecutorAllocationClient], listenerBus, _conf,
                  _env.blockManager.master))
              case _ =>
                None
            }
          } else {
            None
          }
        _executorAllocationManager.foreach(_.start())
    //ContextCleaner创建,主要负责清理RDD、shuffle、broadcast、checkpoint、Accumulator
     _cleaner =
          if (_conf.getBoolean("spark.cleaner.referenceTracking", true)) {
            Some(new ContextCleaner(this))
          } else {
            None
          }
        _cleaner.foreach(_.start())
    //启动事件监听总线setupAndStartListenerBus,刷新事件总线
    setupAndStartListenerBus()
    postEnvironmentUpdate()
    postApplicationStart()
    //增加ShutdownHook,平滑关闭应用
    _shutdownHookRef = ShutdownHookManager.addShutdownHook(
          ShutdownHookManager.SPARK_CONTEXT_SHUTDOWN_PRIORITY) { () =>
          logInfo("Invoking stop() from shutdown hook")
          try {
            stop()
          } catch {
            case e: Throwable =>
              logWarning("Ignoring Exception while stopping SparkContext from shutdown hook", e)
          }
        }
    
  • 至此,SparkContext创建完成,整个计算组件都已创建完成,包括Driver,ApplicationMaster,Executors,接着执行计算任务代码

  • driver端会执行算子,transform算子为Lazy懒执行,除非执行到Action算子,Action算子内执行sc.runJob方法触发计算

    //SparkPi中的计算代码
    val count = spark.sparkContext.parallelize(1 until n, slices).map { i =>
          val x = random * 2 - 1
          val y = random * 2 - 1
          if (x*x + y*y <= 1) 1 else 0
        }.reduce(_ + _)
    

    RDD中的reduce算子代码

    //RDD中的reduce算子
    def reduce(f: (T, T) => T): T = withScope {
        val cleanF = sc.clean(f)
        val reducePartition: Iterator[T] => Option[T] = iter => {
          if (iter.hasNext) {
            Some(iter.reduceLeft(cleanF))
          } else {
            None
          }
        }
        var jobResult: Option[T] = None
        val  = (index: Int, taskResult: Option[T]) => {
          if (taskResult.isDefined) {
            jobResult = jobResult match {
              case Some(value) => Some(f(value, taskResult.get))
              case None => taskResult
            }
          }
        }
        //触发计算核心代码
        sc.runJob(this, reducePartition, mergeResult)
        // Get the final result out of our Option, or throw an exception if the RDD was empty
        jobResult.getOrElse(throw new UnsupportedOperationException("empty collection"))
      }
    
  • sparkContext中的runJob方法执行流程

    dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, resultHandler, localProperties.get)
    

    dagScheduler.runJob方法中执行

    val waiter = submitJob(rdd, func, partitions, callSite, resultHandler, properties)
    

    submitJob方法中执行

    val waiter = new JobWaiter(this, jobId, partitions.size, resultHandler)
        eventProcessLoop.post(JobSubmitted(
          jobId, rdd, func2, partitions.toArray, callSite, waiter,
          SerializationUtils.clone(properties)))
    

    其中eventProcessLoop是DAGSchedulerEventProcessLoop类型继承自EventLoop,EventLoop内创建了个eventThread事件监听线程,该线程在DAGScheduler中最后一行被启动,监听是否有事件传入执行DAGSchedulerEventProcessLoop中doOnReceive方法,处理我们提交的job action算子

  • doOnReceive方法执行步骤解析

    private def doOnReceive(event: DAGSchedulerEvent): Unit = event match {
        case JobSubmitted(jobId, rdd, func, partitions, callSite, listener, properties) =>
          dagScheduler.handleJobSubmitted(jobId, rdd, func, partitions, callSite, listener, properties)
        case MapStageSubmitted(jobId, dependency, callSite, listener, properties) =>
          dagScheduler.handleMapStageSubmitted(jobId, dependency, callSite, listener, properties)
        case StageCancelled(stageId, reason) =>
          dagScheduler.handleStageCancellation(stageId, reason)
        case JobCancelled(jobId, reason) =>
          dagScheduler.handleJobCancellation(jobId, reason)
        ........
    

    该方法内处理各种事件类型,包含任务提交、取消、任务失败、执行器丢失等等事件,本例中是通过eventProcessLoop.post()方法提交了JobSubmitted类型事件,所以执行case JobSubmitted分支,执行dagScheduler.handleJobSubmitted方法

    handleJobSubmitted方法执行的核心代码

    //创建resultStage
    handleJobSubmittedfinalStage = createResultStage(finalRDD, func, partitions, jobId, callSite)
    //提交进行stage划分及stage提交计算
    submitStage(finalStage)
    
  • submitStage方法功能是Stage划分及每个stage构造TaskSet提交执行

    private def submitStage(stage: Stage) {
        val jobId = activeJobForStage(stage)
        if (jobId.isDefined) {
          logDebug(s"submitStage($stage (name=${stage.name};" +
            s"jobs=${stage.jobIds.toSeq.sorted.mkString(",")}))")
          if (!waitingStages(stage) && !runningStages(stage) && !failedStages(stage)) {
              //划分stage核心方法
            val missing = getMissingParentStages(stage).sortBy(_.id)
            logDebug("missing: " + missing)
            if (missing.isEmpty) {
              logInfo("Submitting " + stage + " (" + stage.rdd + "), which has no missing parents")
                //提交每个Stage任务方法,由于是递归方法,任务Stage提交会由前到后依次执行
              submitMissingTasks(stage, jobId.get)
            } else {
              for (parent <- missing) {
                  //递归执行向上级查找Stage
                submitStage(parent)
              }
              waitingStages += stage
            }
          }
        } else {
          abortStage(stage, "No active job for stage " + stage.id, None)
        }
      }
    

    getMissingParentStages方法为Stage划分的核心算法,核心代码如下:

    //迭代当前rdd的所有依赖
    for (dep <- rdd.dependencies) {
                dep match {
                    //模式匹配是否是shuffle依赖
                  case shufDep: ShuffleDependency[_, _, _] =>
                    val mapStage = getOrCreateShuffleMapStage(shufDep, stage.firstJobId)
                    if (!mapStage.isAvailable) {
                        //是shuffle依赖的话加入到结果集返回
                      missing += mapStage
                    }
                    //窄依赖匹配
                  case narrowDep: NarrowDependency[_] =>
                    waitingForVisit.push(narrowDep.rdd)
                }
              }
    

    submitMissingTasks负责对当前的Stage构造TaskSet,提交这些TaskSet到executor TaskManager执行核心代码:

    taskScheduler.submitTasks(new TaskSet(
            tasks.toArray, stage.id, stage.latestInfo.attemptNumber, jobId, properties))
    

2.启动流程

  1. Application,自己编写的spark程序。
  2. spark-submit,利用shell来提交自己的spark程序
  3. Driver,standalone提交方式,会通过反射构造出一个Driver进程。Driver进程会执行application程序
  4. SparkContext,Driver进程执行Application程序时会构造一个SparkContext。它会构造出DAGScheduler和TaskScheduler。构造TaskScheduler时,会去寻找集群的master,通过一个后台进程,然后向master注册Application。Master收到注册请求后会在spark集群上找worker,然后启动相应的Executor。
  5. Executor,进程。Executor通过上面的步骤启动后会反向注册到taskScheduler上去。所有Executor反向注册完成后,会结束SparkContext初始化,会继续执行Application代码。Executor每接收一个Task都会用TaskRunner封装,然后从线程池中找一个线程执行。TaskRunner就将我们写的代码,要执行的算子和函数拷贝,反序列化,然后执行Task。
  6. Job,每执行一个action就会提交一个job提交给DAGScheduler。
  7. DAGScheduler,DAGScheduler会将job划分为多个stage,然后为每一个stage创建一个taskset,stage划分算法很重要。TaskScheduler会把TaskSet里面的每一个Task提交到executor上面去执行,task分配算法
  8. ShuffleMapTask and ResultTask,Task有两种,只有最后一个stage的Task才是ResultTask,其余的都是ShuffleMapTask
  9. 最后spark应用的执行,就是stage分批次作为task提交到executor执行,每个task针对RDD的一个Partition,执行我们定义的算子和函数,依次类推直到结束。

3.yarn client与yarn cluster提交流程比较

  • yarn client模式提交流程

在这里插入图片描述

详细过程:

  1. Client Application会初始化SparkContext,这是Driver端;
  2. 提交Application到ResourceManager,ResourceManager选取一台NodeManager节点启动ApplicationMaster容器运行
  3. AM向RM申请 执行器资源,资源足够且权限允许情况下获取RM返回的可用NM节信息,AM向这些节点发送启动启动执行器命令启动执行器
  4. Driver端开始执行业务代码,通过Action算子触发DAGScheduler Stage划分依次提交每个Stage下TaskSet到执行器执行计算
  5. Executors与SparkContext初始化后的通信模块保持通信,因为是与Client端通信,所以Client不能关闭。
  • yarn cluster模式提交流程

    在这里插入图片描述

    详细过程:

    1. 客户端提交Application到RM,这个过程做的工作有判断集群资源是否满足需求、读取配置文件、设置环境变量、设置Application名字等等;
    2. RM在某一台NodeManager上启动Application Master容器进程,AM所在的机器是YARN选取的,无法预定
    3. AM初始化SparkContext,此时Driver端就在AM内,这个NodeManager节点便是Driver端;
    4. AM向ResourceManager申请资源,并在每台NodeManager上启动相应的executors;
    5. 初始化后的SparkContext中的通信模块可以通过netty与NodeManager上的容器进行通信。
    6. Driver端开始执行业务代码,通过Action算子触发DAGScheduler Stage划分,依次提交每个Stage下的TaskSet到执行器执行计算
  • 两者比较

    1. 最主要的区别就是Driver端所在位置不同,client模式Driver端在提交任务的机器上,cluster模式Driver端在可能的任意一个NodeManager节点上,根据选取算法来选择AM的位置,也就是Driver的位置
    2. client模式在提交后控制台可打印所有的日志信息(除了执行器),cluster模式日志都要到yarn页面上去看
    3. client模式一旦关闭控制台或杀死client进程,计算任务会立即停止,SparkContext也会自动关闭,cluster模式提交一旦ApplicationMaster启动成功,任务便交由AM控制调度,包含Driver启动,Executor启动,计算job执行等,所以就算杀掉client端进程,计算也不会停止
    4. client模式任务提交由于driver端在当前提交任务机器上,driver会与executor产生大量的网络通信,数据传输等等。当一个任务提交时没啥问题,但是当有N个任务都通过client方式提交时,而且往往都在一台机器上提交时,产生N个Driver端进程与各自的Executor网络通信,会造成网络IO瓶颈,导致任务频繁超时,甚至失败,也影响整个大数据集群的稳定性
    5. client模式提交也会对当前提交机器的内存、cpu产生不小压力
    6. cluster模式不会产生这种网络、cpu、内存压力,Driver端来自RM的选取,而且会根据NM的cpu或内存状态动态选取,优先选择压力较小的机器去运行AM(内启动Driver)
  • 综上当我们在调试时可以使用client模式,但在生产环境下一定要使用cluster模式提交!!!

4.DAG执行调度

  • 明确几个概念

    1. job:job是由rdd的action来划分,每一个action操作是在spark任务执行时是一个job。(action的区分:rdd分为行动操作和转化操作,因为我们知道rdd是惰性加载的,除非遇到行动操作,前面的所有的转化操作才会执行,这也就是为什么spark任务由job来划分执行了,区分行动操作和转化操作最简单的方法就是看,rdd放回的值,如果返回的是一个rdd则是转化操作,例如map,如果返回的是一个其他的数据类型则是行动操作,例如count)

    2. stage:根据rdd的宽窄依赖来划分(shuffle来区分),遇到shuffle,则将shuffle之前的窄依赖归来一个stage;

    3. task:task是由最后的executor执行的最小任务,它最终落到各个executor上,实现分布式执行;

    4. 简单的归纳一下他们的关系:job -> stage -> task (job中有多个stage,stage中有多个task);

    5. spark运行时,一个任务由client提交,再由driver划分逻辑实现图DAG,最后分配给各个executor上执行task;

  • DAG创建与执行步骤

    1. DAGScheduler创建自SparkContext构造时new DAGScheduler(this)
    2. DAG执行发生在SparkContext创建完成即Driver端创建完成后执行到业务代码中的Action算子时
    3. Action算子中的sc.runJob,最终执行DAGScheduler.runJob
    4. 然后按宽依赖还是窄依赖递归划分Stage,每个Stage按RDD分区划分成Task Set,再经netty提交到Executor执行一个个Tast Set
  • 流程图

在这里插入图片描述

  • 要学会看代码画DAG图,也要会看执行页面的DAG图

5.Dataset API算子解析

  • Dataset与RDD、Dataframe区别

    • RDD

      优点:

      1. 编译时类型安全
        编译时就能检查出类型错误
      2. 面向对象的编程风格
        直接通过类名点的方式来操作数据

      缺点:

      1. 序列化和反序列化的性能开销
        无论是集群间的通信, 还是IO操作都需要对对象的结构和数据进行序列化和反序列化.
      2. GC的性能开销
        频繁的创建和销毁对象, 势必会增加GC
      case class Person(id: Int, age: Int)
      val idAgeRDDPerson = sc.parallelize(Array(Person(1, 30), Person(2, 29), Person(3, 21)))
      
    • Dataframe

      DataFrame引入了schema和off-heap

      • schema : RDD每一行的数据, 结构都是一样的. 这个结构就存储在schema中. Spark通过schame就能够读懂数据, 因此在通信和IO时就只需要序列化和反序列化数据, 而结构的部分就可以省略了.
      • off-heap : 意味着JVM堆以外的内存, 这些内存直接受操作系统管理(而不是JVM)。Spark能够以二进制的形式序列化数据(不包括结构)到off-heap中, 当要操作数据时, 就直接操作off-heap内存. 由于Spark理解schema, 所以知道该如何操作.
        通过schema和off-heap, DataFrame解决了RDD的缺点, 但是却丢了RDD的优点. DataFrame不是类型安全的, API也不是面向对象风格的,如下代码编写schema很恶心的,还容易出错。。。
      val idAgeRDDRow = sc.parallelize(Array(Row(1, 30), Row(2, 29), Row(4, 21)))
      val schema = StructType(Array(StructField("id", DataTypes.IntegerType), StructField("age", DataTypes.IntegerType)))
      val idAgeDF = sqlContext.createDataFrame(idAgeRDDRow, schema)
      
    • Dataset
      官网文档介绍原文:

      Datasets are similar to RDDs, however, instead of using Java serialization or Kryo they use a specialized Encoder to 
      serialize the objects for processing or transmitting over the network. While both encoders and standard serialization 
      are responsible for turning an object into bytes, encoders are code generated dynamically and use a format that allows 
      Spark to perform many operations like filtering, sorting and hashing without deserializing the bytes back into an object.
      

      DataSet结合了RDD和DataFrame的优点, 并带来的一个新的概念Encoder

      • Encoder

      当序列化数据时, Encoder产生字节码与off-heap进行交互, 能够达到按需访问数据的效果, 而不用反序列化整个对象,只要反序列化对应字段就能计算,减少大量的序列化反序列化时间,所以性能要比RDD和Dataframe要高很多,当前的Encoder只支持Seq类型,不支持Map、Set,所以Dataset算子返回值只能时Seq类型不能是Map、Set,现在也可以自定义Encoder,然后作为类型参数传入即可,Dataset的序列化也不需要使用kryo序列化编码器,自带编码器和内部优化

      • 强类型

        字段名或类型错误在编译时就能发现,提高程序的开发提交效率

      • 与RDD或Dataframe转换方便且可逆,代码更简洁高效

      • Dataframe就是种特殊的Dataset:Dataset[Row]

      //需要引入spark的隐式转换
      import spark.implicits._
      val df = spark.sql("select name,age from technology where level = 2")
      df.show()
      // 若自定义编码器:指定编码器,隐式参数,一般不需要自定义
      implicit val mapEncoder = org.apache.spark.sql.Encoders.kryo[Map[String, Any]]
      val ds = df.as[Person].map(p => p.copy(age=p.age+10))
      ds.show(false)
      

      转换需要引入import spark.implicits._隐式转换!!!
      RDD转Dataset,然后rdd.toDS()就行
      Dataframe转Dataset: 要通过df.as[Type]
      Dataset转rdd: ds.rdd
      Dataset转Dataframe: ds.toDF(),括号可传变长参数,表示重命名的列名

  • Dataset API

    算子跟RDD的都类似,用起来几乎感觉不到差别,部分RDD的算子Dataset没有比如reduceByKey

    除了一般的算子,还有带状态的算子比如mapGroupWithState,可以保存每个组的状态信息在Structured Streaming中很有用

6.业务逻辑分析

  • 合理设计业务逻辑

    1. 首先要根据计算的数据量有个预估,要用多少内存,多少执行器,多少核心,如果计算资源不够就需要对数据进行分批计算,一般按时间分区来挨个计算,最后合并计算
    2. 每个批次计算中还要考虑尽量减少shuffle操作,多采用广播哈希join
    3. 每个批次计算结果如何保存,是persist到内存还是磁盘,还是直接保存成文件持久化到HDFS或是HIVE
    4. 容错处理,一旦有一个批次计算时因各种原因异常报错终止了,下次重新执行要跳过已经计算成功的批次数据,减少计算时间,同时容错性也较高
    5. 计算每个批次与合并所有批次结果任务分开提交。考虑批次计算或许用的内存很少,但执行器核心要多,而合并计算批次结果是内存使用较多,而执行器核心利用不多,所以考虑要分开成两个任务提交,降低总体资源占用,同时充分利用资源

7.内存及执行器负载估算

  • scala数据类型在JVM中所占内存估算

    • 值类型:

      Int : 16个字节

      Long: 24字节

      String:40+2n

    • 引用类型:

      Array[Obj]: 80+n*Obj

      HashMap[String,Obj]: 大约 200+20 * 2n+n*obj

  • RDD数据加载到内存 = 单条数据占用内存 X 数据行数

  • 数据加载到执行器的堆中,总可用空间为执行器内存*0.6

  • 执行器内存负载就是总内存/执行器个数

8.高效数据结构构造

  • RDD中构造的数据类型选择

尽量采用Seq类型比如Array,比map或Set类型占用空间少很多倍,所以Dataset中编码器仅支持Seq类型

  • 进行广播 join时需要针对广播的数据类型进行选择,优先选择HashMap类型或HaseSet类型,充分利用Hash查找算法的优势,否则只能进行遍历或二分,join效率要低很多倍

  • 机器学习中可以使用稀疏矩阵向量降低内存占用

9.灵活使用查找算法

  • 优先采用下标查找,时间复杂度为O(1),也不要重复取下标
  • 优先使用Hash查找算法,时间复杂度最低为O(1)
  • 需要迭代时也要考虑使用二分查找算法时间复杂度为O(log2 n)
  • 还可以考虑对数据分块索引,构造分块索引数据结构,然后利用hash或二分,大大降低查找次数
  • 最低效的查找算法就是遍历了

10.Stage并行度设置

​ 有时候Executors执行某个Stage的Tast Set时,会发现执行很慢,查看UI监控页面发现RUNNING的执行器只有几个,大多数在等待状态,或者每个执行器下的Tast数太多,而且分布不均匀,这就是Stage并行度没设置合理导致。

Stage并行度与那些因素有关呢?

  • Spark中Stage并行度对应RDD或Dataframe或Dataset分区数
  • 当读取文件或HIVE表时与文件在HDFS上的分块个数有关,文件共有多少块就决定了第一个Stage的并行度,我们读完数据一定要进行重分区操作!否则整个Stage的执行并行度会过大或过小影响整体效率
  • 当遇到ShuffleStage时 Shuffle操作后的Stage的并行度等于Shuffle后的分区数,一般也会过大或过小,严重导致数据偏移内存溢出,所以在Shuffle操作后一般要进行重分区操作
  • 在过滤一个数据集后,会导致很多分区的数据量减少,甚至出现很多空分区,这时执行器执行时会出现有的执行快有的慢,执行时间很不均衡,影响整体效率,这就需要减小分区数或重分区操作
  • 同样的道理在uion之后要要进行重分区
  • 在读取Hbase数据表时,一个Region对应一个分区,也要进行重分区操作,防止并行度不合理影响整体效率
  • 并行度一般设置成执行器总核心数的2-3倍!!!可通过shell提交脚本参数传入

11.缓存与释放缓存

​ 当我们进行重复使用一个Dataset时,会发现整个Dataset的Stage计算会重复被执行多次,严重影响总体效率,这就是重复计算的问题,所以可以将该Dataset进行缓存到内存或磁盘,下次重复使用时直接取结果,就会大大提升效率

  • 缓存的使用时机,一定是需要重复利用到的DS或RDD或DF才需要缓存,否则白白占用过多的执行器堆内存空间
  • 缓存的释放,一定要在依赖该DS的所有DS的Job计算完成后才能释放,否则缓存无效
  • 缓存一定要及时释放,否则随着计算进程推进,执行器堆内存Storage空间会越来越少,当占满时触发FullGC,Task计算会卡住,接着就会内存溢出
  • 缓存分为内存级和磁盘级,内存级的缓存不能确保不发生重复计算,因为一旦堆空间占用超过Storage的规划大小阈值,会按缓存顺序优先释放掉前面的缓存结果,导致下次重复利用该DS的Job又重新触发了该DS的计算。磁盘级的缓存不会出现这种情况,当执行器堆内存不足时会溢写到磁盘

12.监控指标查看

​ 总体监控分为Job、Stage、Storage、Environment、Executors这几项

在这里插入图片描述

在应用运行时可以在Yarn监控页面(8088)导致对应application,点右侧的ApplicationMaster查看

在程序执行完成后这个页面会关闭消失,要想看具体监控指标要到spark JobHistory页面(18080)查看

  • 首页就是Jobs指标监控,会显示正在运行的job,已经完成的Job,每个Job执行了多长时间,还有进度条,点进一个job会进入该Job的Stage监控页面,在点进一个Stage会自动转到第二个选项卡Stages页面

  • Stages页面

在这里插入图片描述

最上边的DAG图点开,会显示当前Job的Stage执行流程图

接着下面的SummaryMetricfs显示当前Stage的Task监控指标:Task总数,被多少执行器并发执行(并行度),执行器内存峰值,GC时间等

  • Storage页面内存占用指标

  • Environment环境变量和配置参数

  • Executors页面

在这里插入图片描述

需要重点指出的是执行器的内存占用和GC时间,来判断执行器内存设置是否合理,还有Executor最右边的Logs可以查看执行器内(map等算子内)打印的日志信息,一般用于调试非常有用

13.数据偏移处理

​ 当数据经Spark读取后或经过GroupBy等shuffle操作后导致不同的分区数据量相差很大,有的分区数据量爆满,有的却几乎是空的,在执行job时会导致数据量爆满的Task 分区计算分到的执行器执行很慢,其他数据量极少的Task 分配到的执行器毫秒级计算完成,处理时间差距极大。而且内存使用情况也是一个两个极端,大的极可能会导致执行器内存溢出,从而计算Job失败。

那么如何处理这种情况呢?

  • 数据源导致的:

    1. 从数据偏移发生的根源分析,如果是数据块本身的原因,如HDFS的数据块分布不均衡,数据块都集中在几台新加的DataNode节点上,导致数据读取效率极低
    2. 这就需要从数据源ETL处进行处理,尤其是小文件的合并,大量的小文件也会降低HDFS的空间利用率
    3. 还需要对HDFS进行定期的rebalance操作进行数据再平衡
  • 分组导致的:

    1. 从原因上去分析,由于不同的分区Key的数据量不同导致,可以从key上去找方法
    2. 将key增加随机值前缀或后缀,扩展key总量,在分组后增加组数,同时也增加了Tast分区数
    3. 在扩展key后进行聚合操作
    4. 在聚合操作后再把key中的随机值删除,再二次进行分组操作,再进行聚合
    5. 这样将单次分组聚合改成两次乃至多次分组聚合,每次都会大大减少Executor的数据执行压力与内存压力

14.降低内存占用

​ 降低内存占用要从数据类型选择上、集合类型选择上分析,还要从join类型选择上、广播变量、缓存使用上、算法选择上综合分析,共同作用才能降低内存资源占用

  • 数据类型

    1. String类型少用,多用Int,Long,Flot,Double等类型

    2. 集合类型选择Seq 类型如Array,不要用HashMap ,HashSet,TreeSet等类型,在涉及广播HashJoin时广播变量可以选择HashSet等类型,提高效率

    3. 序列化优先选择Kryo序列化,否则默认采用Java序列化

  • join方式选择

    普通的join算子需要大量的shuffle过程,可以使用广播HashJoin方式,没有shuffle过程,效率也高

  • 广播变量选择

    在涉及两个数据集广播HashJoin时,广播变量优先选择小表数据集,降低广播到每个执行器的内存压力

  • 缓存使用

    缓存一定要合理使用,一定时重复使用依赖的DS时才用

    在所有依赖该DS的Job计算结束完成后才能释放,否则缓存无效

    缓存数据量不大的情况下优先采用内存级,但数据量大的情况下优先选择带磁盘级的缓存

  • 算法逻辑

    合理设计执行的算法逻辑式降低内存占用的最有效方法,比如将一天的数据按时间分成微批挨个进行处理,最后合并

15.日志查看与分析

​ spark计算日志查看要分几种情况

  • yarn client

    直接在控制台就能看到几乎所有执行日志(执行器除外)

    spark-submit --master yarn --deploy-mode client --executor-cores 1 --num-executors 1 --class com.ly.spark.jobs.SparkPi  delta-lake-1.0.jar 30
    20/06/02 11:31:56 WARN spark.SparkConf: The configuration key 'spark.yarn.jar' has been deprecated as of Spark 2.0 and may be removed in the future. Please use the new key 'spark.yarn.jars' instead.
    20/06/02 11:31:56 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
    开始构建SparkSession...
    20/06/02 11:31:57 WARN spark.SparkConf: The configuration key 'spark.yarn.jar' has been deprecated as of Spark 2.0 and may be removed in the future. Please use the new key 'spark.yarn.jars' instead.
    20/06/02 11:31:57 INFO spark.SparkContext: Running Spark version 2.4.5
    20/06/02 11:31:57 INFO spark.SparkContext: Submitted application: Spark Pi
    20/06/02 11:31:57 INFO spark.SecurityManager: Changing view acls to: rose
    20/06/02 11:31:57 INFO spark.SecurityManager: Changing modify acls to: rose
    20/06/02 11:31:57 INFO spark.SecurityManager: Changing view acls groups to: 
    20/06/02 11:31:57 INFO spark.SecurityManager: Changing modify acls groups to: 
    20/06/02 11:31:57 INFO spark.SecurityManager: SecurityManager: authentication disabled; ui acls disabled; users  with view permissions: Set(rose); groups with view permissions: Set(); users  with modify permissions: Set(rose); groups with modify permissions: Set()
    20/06/02 11:31:57 INFO util.Utils: Successfully started service 'sparkDriver' on port 43935.
    20/06/02 11:31:57 INFO spark.SparkEnv: Registering MapOutputTracker
    20/06/02 11:31:57 INFO spark.SparkEnv: Registering BlockManagerMaster
    20/06/02 11:31:57 INFO storage.BlockManagerMasterEndpoint: Using org.apache.spark.storage.DefaultTopologyMapper for getting topology information
    20/06/02 11:31:57 INFO storage.BlockManagerMasterEndpoint: BlockManagerMasterEndpoint up
    20/06/02 11:31:57 INFO storage.DiskBlockManager: Created local directory at /tmp/blockmgr-55f81560-c729-423d-98db-0486775fb776
    20/06/02 11:31:57 INFO memory.MemoryStore: MemoryStore started with capacity 246.9 MB
    20/06/02 11:31:57 INFO spark.SparkEnv: Registering OutputCommitCoordinator
    20/06/02 11:31:57 INFO util.log: Logging initialized @1490ms
    20/06/02 11:31:57 INFO server.Server: jetty-9.3.z-SNAPSHOT, build timestamp: unknown, git hash: unknown
    20/06/02 11:31:57 INFO server.Server: Started @1558ms
    20/06/02 11:31:57 INFO server.AbstractConnector: Started ServerConnector@438bad7c{HTTP/1.1,[http/1.1]}{0.0.0.0:4040}
    20/06/02 11:31:57 INFO util.Utils: Successfully started service 'SparkUI' on port 4040.
    .........
    

    执行时会打印出drvier端执行的代码输出

    还会打印各种异常信息,内存溢出,执行器丢失…

  • yarn cluster

    Driver端日志要在Yarn页面(8088)中的application才能看到,要开启yarn mapreduce日志聚合功能

    执行器日志要在Spark History Server页面(18080)点开执行器才能看到

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3eKFi6zQ-1591971699594)(D:\markdown\imags\批注 2020-06-04 17.png)]

  • k8s

    要到Driver 所在的k8s pod内查看

  • standalone

    不经常用,除非个人学习使用,类似yarn

16.减少shuffle

  1. 减少分区操作,禁止无用的分区操作
  2. 用coalesce代替repartition,尤其是filter操作后,coalesce可选是否shuffle,分区差距过大也要走shuffle
  3. join操作优先选择广播Hash Join,没有shuffle

17.容错处理

  1. 优先采用Dataset API
  2. 微批结果优先持久化保存,下次计算自动跳过已经计算成功的微批,防止中间异常导致要从头再算一遍
  3. 数据字段增加正则校验

18.选择高效文件存储格式

  1. 优先采用orc,parquet,avro等存储格式,文件占用空间少,查找快
  2. 少用csv,ObjectFile保存结果,占用空间太大,csv是parquent的26倍,objectFile是上万倍差距,而且读取也慢,需要所有字段都读取

参考与引用

spark官网文档与源码(当前最新2.4.5): http://spark.apache.org/docs/latest/
yarn-client与yarn-cluster对比: https://www.jianshu.com/p/b9ec3c2ff8dd
DAGScheduler源码解析: https://www.cnblogs.com/yankang/p/9769720.html

已标记关键词 清除标记
相关推荐
<p> <b><span style="background-color:#FFE500;">【超实用课程内容】</span></b> </p> <p> <br /> </p> <p> <br /> </p> <p> 本课程内容包含讲解<span>解读Nginx的基础知识,</span><span>解读Nginx的核心知识、带领学员进行</span>高并发环境下的Nginx性能优化实战,让学生能够快速将所学融合到企业应用中。 </p> <p> <br /> </p> <p style="font-family:Helvetica;color:#3A4151;font-size:14px;background-color:#FFFFFF;"> <b><br /> </b> </p> <p style="font-family:Helvetica;color:#3A4151;font-size:14px;background-color:#FFFFFF;"> <b><span style="background-color:#FFE500;">【课程如何观看?】</span></b> </p> <p style="font-family:Helvetica;color:#3A4151;font-size:14px;background-color:#FFFFFF;"> PC端:<a href="https://edu.csdn.net/course/detail/26277"><span id="__kindeditor_bookmark_start_21__"></span></a><a href="https://edu.csdn.net/course/detail/27216">https://edu.csdn.net/course/detail/27216</a> </p> <p style="font-family:Helvetica;color:#3A4151;font-size:14px;background-color:#FFFFFF;"> 移动端:CSDN 学院APP(注意不是CSDN APP哦) </p> <p style="font-family:Helvetica;color:#3A4151;font-size:14px;background-color:#FFFFFF;"> 本课程为录播课,课程永久有效观看时长,大家可以抓紧时间学习后一起讨论哦~ </p> <p style="font-family:"color:#3A4151;font-size:14px;background-color:#FFFFFF;"> <br /> </p> <p class="ql-long-24357476" style="font-family:"color:#3A4151;font-size:14px;background-color:#FFFFFF;"> <strong><span style="background-color:#FFE500;">【学员专享增值服务】</span></strong> </p> <p class="ql-long-24357476" style="font-family:"color:#3A4151;font-size:14px;background-color:#FFFFFF;"> <b>源码开放</b> </p> <p class="ql-long-24357476" style="font-family:"color:#3A4151;font-size:14px;background-color:#FFFFFF;"> 课件、课程案例代码完全开放给你,你可以根据所学知识,自行修改、优化 </p> <p class="ql-long-24357476" style="font-family:"color:#3A4151;font-size:14px;background-color:#FFFFFF;"> 下载方式:电脑登录<a href="https://edu.csdn.net/course/detail/26277"></a><a href="https://edu.csdn.net/course/detail/27216">https://edu.csdn.net/course/detail/27216</a>,播放页面右侧点击课件进行资料打包下载 </p> <p> <br /> </p> <p> <br /> </p> <p> <br /> </p>
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页