【spark】metrics信息的获取(监听器等)

一、背景

最近看到了一篇文章,说的是spark小文件合并的问题

Spark 小文件合并优化实践:https://mp.weixin.qq.com/s/195nFBH0kpZEXekHiQAfrA(作者:偷闲小苑)

其实关于小文件合并我之前也写过类似的文章,大体的方案也就是
1、存之前做一个数据量的预估,然后repartition/coalesce

2、存完之后,触发一个merge合并小文件(可以是hive也可以是spark任务)

3、使用一些数据湖(delta lake,hudi,iceberg)方案,不直接写入hdfs,写入中间层,让中间层自己去管理hdfs上的文件样式

前两者,其实都需要拿到数据的metrics信息,才能做数据量的判断

拿metrics其实spark代码中有挺多地方都可以拿到的:

(1)、最常用的就是监听器(SparkListener)

(2)、其次在数据落盘的时候也能拿到(FileFormatWriter)

(3)、最后其实一些shuflle操作啥的也能拿到(BypassMergeSortShuffleWriter),但这个粒度太细了,要做的话改动会很大

二、metrics获取

1、监听器获取

spark有一个类:SparkListener,我们可以自定义监听类,来实现当触发了spark任务执行到某个阶段,触发你自定义的代码,有点像AOP

这种方式是最方便的,因为不需要修改源码,也不嵌入业务逻辑,只需要写上自己的一个新的类,注册到上下文中

直接上代码:


class ListenerDemo1(conf: SparkConf) extends SparkListener with Logging {
  override def onApplicationStart(applicationStart: SparkListenerApplicationStart): Unit = {
    println("onApplicationStart")
  }

  /**
   * 一个Application可能会有多个job,一个action操作就是一个job
   * 所以如果代码中,执行了count和save,那么起码会有2个job
   * 每个job又有各自的stage
   * @param jobStart
   */
  override def onJobStart(jobStart: SparkListenerJobStart): Unit = {
    println("onJobStart")
    println("该任务总共有 : " + jobStart.stageIds.length+" 个stage")
  }

  /**
   * 第一个stage提交,task开始,task结束,stage结束
   * 然后开始下一个stage的提交。。。
   * 如果有多个task,会执行多次task开始和task结束
   */
  override def onStageSubmitted(stageSubmitted: SparkListenerStageSubmitted): Unit = {
    println("onStageSubmitted")
    println("stageId = "+stageSubmitted.stageInfo.stageId + " 已经提交")
  }
  override def onTaskStart(taskStart: SparkListenerTaskStart): Unit = {
    println("在executorld = "+taskStart.taskInfo.executorId + "(" +taskStart.taskInfo.host +") " +
      "上启动了一个task = "+taskStart.taskInfo.taskId +"," +
      "taskLocality = "+taskStart.taskInfo.taskLocality)
    println("onTaskStart")
  }
  override def onTaskEnd(taskEnd: SparkListenerTaskEnd): Unit = {
    //区分task归属的stageId,taskid全局递增,不同stage的taskid也不会重复
    println("stageId = "+taskEnd.stageId+",taskId = "+taskEnd.taskInfo.taskId)
    println("TaskType = " + taskEnd.taskType)
    /**
     * taskinfo中会有一些internal的metrics
     * 如果TaskType=ShuffleMapTask,证明是上游的task,那么就会有
     * internal.metrics.shuffle.write.recordsWritten = 9
     * internal.metrics.shuffle.write.bytesWritten = 636
     * 如果TaskType=ResultTask,那就是下游的task,那么就会有
     * internal.metrics.output.recordsWritten = 3
     * internal.metrics.output.bytesWritten = 22
     * internal.metrics.shuffle.read.recordsRead = 3
     * 这些指标均是每个task处理的数据量的情况
     */
    taskEnd.taskInfo.accumulables.foreach(
      task => {
        println(task.name.getOrElse("") +" = "+ task.value.getOrElse(""))
      }
    )
    println("onTaskEnd")
  }
  override def onStageCompleted(stageCompleted: SparkListenerStageCompleted): Unit = {
    println("onStageCompleted")
  }



  override def onJobEnd(jobEnd: SparkListenerJobEnd): Unit = {
    println("onJobEnd")
  }
  override def onApplicationEnd(applicationEnd: SparkListenerApplicationEnd): Unit = {
    println("onApplicationEnd")
  }


}

然后在创建spark上下文的时候,做一个注册

 

2、数据落盘的时候拿到metrics

(1)、修改BasicWriteTaskStats代码

我其实没有很认真读FileFormatWriter里面的代码

但是里面有个write方法,会执行一个executeTask方法,然后会判断使用哪一种WriteTask来写数据(总共有三种),然后会调用WriteTask的execute方法,execute方法中又会用到statsTrackers对象,这个对象能够收集到FileFormatWriter中每个task的一些metrics信息,这个trait有一个具体实现:BasicWriteTaskStats

所以我关注到了这个类中的processStats方法,这个方法会将多个task的结果做一个累加

可以获得输出的文件的大小和条数

(2)、将metrics信息传递出去

然后这里有个重点,我想了挺久的

就是怎么把这里的值传递出去,使用spark的广播变量和累加器好像都实现不了

所以要么直接往外部的存储系统里面存,要么做成一个rdd,并且cache了通过sc的getPersistentRDDs来获取

具体实现:

 

在driver中,通过如下代码获取

 

但是这里我发现一个奇怪的问题,假设我在driver中存了调用了多次落盘数据,如果只在最后一次落盘之后,调用getPersistentRDDs获取数据,会发现只有最后一次落盘的Metrics(我也没看出是为什么),不过无伤大雅,因为这个方法,应该是在每次落盘的时候就调用一次,所以可以自己封装一个落盘方法,把Metrics信息的处理也加入其中

 

driver完整代码示例如下:

def main(args: Array[String]): Unit = {
    val sparkSession = SparkSession.builder()
      .appName("test")
      .master("local[*]")
      .config("spark.extraListeners", "com.xxx.ListenerDemo1")
      .getOrCreate()
    ;

    import sparkSession.implicits._

    val student = sparkSession.sparkContext.parallelize(Seq(
      ("xxx",10),
      ("aaa",12),
       ...
      ("ll",2)),
      3).toDF("name","age")
        val student1 = sparkSession.sparkContext.parallelize(Seq(
      ("lsr",10),
      ("zvsvl",123),
      ("lzz",4),
      ("lasdl",32),
      ("lsr",10),
      ("zjl",12)),1).toDF("name","age")
    student1.write.mode("overwrite")
      .format("csv").save("D:\\sparktest\\file1");
    handleMetrics(sparkSession)
    val student2 = sparkSession.sparkContext.parallelize(Seq(
      ("lsr",110),
      ("zjl",12),
      ("lzasdz",4),
      ("ll",2),
      ("lswqr",10),
      ("lswqr",10),
      ("lswqr",10),
      ("lswqr",10),
      ("lswqr",10),
      ("lswqr",10),
      ("lswqr",10),
      ("zjl",12)),1).toDF("name","age")
    student2.write.mode("overwrite")
      .format("csv").save("D:\\sparktest\\file2");
    handleMetrics(sparkSession)
    val stuFilter = student
      .filter("name <> 'xxx'")

    stuFilter
      .repartition(10)
      .write.mode("overwrite")
      .format("csv").save("D:\\sparktest\\file");
    handleMetrics(sparkSession)
   
}
  def handleMetrics(sparkSession: SparkSession): Unit ={
    val rddMap: collection.Map[Int, RDD[_]] = sparkSession.sparkContext.getPersistentRDDs
    rddMap.foreach{
      case (id,rdd) =>{
        /**
         * 由于业务逻辑也可能有cache的rdd
         * 必须确认当前这个rdd是metrices所属的rdd
         * take(1),拿出一条数据出来,做类型匹配
         * 如果是MyMetrice类型,再做相应的代码,否则跳过
         */
        rdd.take(1).head match {
          case metrics:MyMetrics =>{
            val myMetrics = rdd.asInstanceOf[RDD[MyMetrics]]
            myMetrics.collect().map(data =>{
              println("==totalNumBytes== : "+data.totalNumBytes)
              println("==totalNumOutput== : "+data.totalNumOutput)
              println("==numFiles== : "+data.numFiles)

            })
            //用完释放
            rdd.unpersist(true)
          }
          case _ =>
        }
      }
    }
  }

(3)、另外

如下的metrics信息,会展示到sparkui的

 

 

 三、总结

 通过如上的metrics信息,我们可以监控到spark每个阶段的信息,存储到外部空间,做后续的一些任务优化的分析,比如是否触发小文件合并,是否提示用户某些任务不够合理(比如同一阶段的部分task,输入的量和输出的量大大偏离了平均值),当然能做的事情应该不止这些,我觉得还是看痛点是什么?急迫要解决的问题是什么,是降本优化,还是监控告警,都是我们需要更深入思考的问题!

好了,菜鸡一只,要多思考要谦虚要努力

最近看了《觉醒年代》,点赞,确实好看

历史课本上的人物有血有肉的出现在银幕上,被他们的个人魅力所惊讶所震撼

看到1919年巴黎和会上的《凡尔赛和约》,五四青年运动,深深觉得“真理只在剑锋之上,尊严只在大炮射程之内”,中国一定会越来越好的!

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
Sparkmetrics是指在Spark中用于监控和度量系统性能的指标。它是基于Coda Hale Metrics Library的可配置Metrics系统。通过配置文件,我们可以对SparkMetrics系统进行配置,并将Metrics信息报告到不同的Sink,比如HTTP、JMX以及CSV文件。\[1\] 在Spark的安装包中的conf路径下,有一个metrics.properties文件,Spark在启动时会自动加载该文件。我们可以在该文件中进行Metrics的配置。如果该文件不存在,可以将metrics.properties.template重命名为metrics.properties即可。\[2\] 在Spark源码中,我们经常会看到类似longMetric("numOutputRows")的信息。这些指标的定义通常是在Driver端,而实际的+1或-1操作是在executor进行的。这些指标是通过Spark内部的机制进行传递的,具体的传递方式可以通过分析源码来了解。\[3\] #### 引用[.reference_title] - *1* *2* [Spark Metrics配置详解](https://blog.csdn.net/wuzhongdehua1/article/details/50868574)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [SPARKmetrics是怎么传递的](https://blog.csdn.net/monkeyboy_tech/article/details/128294869)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值