SPARK中metrics是怎么传递的

简介: SPARK中metrics是怎么传递的

背景


本文基于spark 3.3.0


在看spark源码的时候,总是会看到类似longMetric("numOutputRows")的信息,但是一般来说这种metrics的定义一般是在Driver端,而真正的+1或者-1操作都是在executor进行的,这种指标到底是怎么传递的呢?我们分析一下


分析


以FilterExec物理计划为例:

case class FilterExec(condition: Expression, child: SparkPlan)
  extends UnaryExecNode with CodegenSupport with GeneratePredicateHelper {
  ...
  override lazy val metrics = Map(
    "numOutputRows" -> SQLMetrics.createMetric(sparkContext, "number of output rows"))
  ...
  protected override def doExecute(): RDD[InternalRow] = {
    val numOutputRows = longMetric("numOutputRows")
    child.execute().mapPartitionsWithIndexInternal { (index, iter) =>
      val predicate = Predicate.create(condition, child.output)
      predicate.initialize(0)
      iter.filter { row =>
        val r = predicate.eval(row)
        if (r) numOutputRows += 1
        r
      }
    }
  }

为什么这么写可以


  • "numOutputRows" -> SQLMetrics.createMetric(sparkContext, "number of output rows")),这里只定义了一个numOutputRows的指标,用来记录该物理操作过滤了多少行的数据


  • if (r) numOutputRows += 1 这个操作会在executor端执行
    其实要看懂这个操作,我们要深入一下SQLMetrics.createMetric的实现
 def createMetric(sc: SparkContext, name: String): SQLMetric = {
    val acc = new SQLMetric(SUM_METRIC)
    acc.register(sc, name = metricsCache.get(name), countFailedValues = false)
    acc
  }
 ...
 abstract class AccumulatorV2[IN, OUT] extends Serializable {

其中SQLMetric类是继承AccumulatorV2,从而继承了Serializable,所以这个类是可序列化的,而且是可java序列化的,这一点很重要。再看SQLMetric的register方法,

 private[spark] def register(
    sc: SparkContext,
    name: Option[String] = None,
    countFailedValues: Boolean = false): Unit = {
  if (this.metadata != null) {
    throw new IllegalStateException("Cannot register an Accumulator twice.")
  }
  this.metadata = AccumulatorMetadata(AccumulatorContext.newId(), name, countFailedValues)
  AccumulatorContext.register(this)
  sc.cleaner.foreach(_.registerAccumulatorForCleanup(this))
}
  • this.metadata = AccumulatorMetadata(AccumulatorContext.newId(), name, countFailedValues)分配一个拥有全局唯一的id的AccumulatorMetadata实例


  • AccumulatorContext.register(this) 这个调用了往map中登记了以全局唯一id为key,value为WeakReference的值,这里登记到map的作用就是后续Task会对该metrics的值进行操作,下面会说到

  • sc.cleaner.foreach(_.registerAccumulatorForCleanup(this)) 这步操作和之前的文章说的一样SPARK 是怎么清除Shuffle中间结果数据的,只不过这里只是清理了Driver端的metrics


这里很重要:


在scala里会有闭包的概念(这里可以自己网上查找原理),但是spark也会对闭包进一步进行处理,详见ClosureCleaner.clean方法。总结一下,简单来说,就是exeuctor会序列化用到的变量,所以说SQLMetric必须是可java序列化的(同时全局唯一的id也会被序列化)。


executor端的变量怎么传递到Driver端的


  1. 1.我们先来看AccumulatorV2的readObject方法:
 private def readObject(in: ObjectInputStream): Unit = Utils.tryOrIOException {
  in.defaultReadObject()
  if (atDriverSide) {
    atDriverSide = false
    // Automatically register the accumulator when it is deserialized with the task closure.
    // This is for external accumulators and internal ones that do not represent task level
    // metrics, e.g. internal SQL metrics, which are per-operator.
    val taskContext = TaskContext.get()
    if (taskContext != null) {
      taskContext.registerAccumulator(this)
    }
  } else {
    atDriverSide = true
  }
}

这个代码会在Executor执行,所以会执行taskContext.registerAccumulator(this)从而调用taskMetrics.registerAccumulator(a),从而保存在名为externalAccums的ArrayBuffer中


2. 再看task端的执行TaskRunner的run()方法:


task = ser.deserialize[Task[Any]]( taskDescription.serializedTask, Thread.currentThread.getContextClassLoader) ... valaccumUpdates = task.collectAccumulatorUpdates()


  • 这里会调用ser.deserialize方法,从而触发AccumulatorV2的readObject方法,从而该AccumulatorV2变量会保存在executor端,且保留了全局唯一id

  • val accumUpdates = task.collectAccumulatorUpdates() 收集spark内置的metrics(如remoteBlocksFetched)和自定义的metrics,这个会通过execBackend.statusUpdate方法,传达Driver端,最终调用到DAGScheduler的updateAccumulators方法更新指标:
private def updateAccumulators(event: CompletionEvent): Unit = {
val task = event.task
val stage = stageIdToStage(task.stageId)
event.accumUpdates.foreach { updates =>
  val id = updates.id
  try {
  // Find the corresponding accumulator on the driver and update it
  val acc: AccumulatorV2[Any, Any] = AccumulatorContext.get(id) match {
    case Some(accum) => accum.asInstanceOf[AccumulatorV2[Any, Any]]
    case None =>
      throw SparkCoreErrors.accessNonExistentAccumulatorError(id)
  }
  acc.merge(updates.asInstanceOf[AccumulatorV2[Any, Any]])
  // To avoid UI cruft, ignore cases where value wasn't updated
  if (acc.name.isDefined && !updates.isZero) {
    stage.latestInfo.accumulables(id) = acc.toInfo(None, Some(acc.value))
    event.taskInfo.setAccumulables(
      acc.toInfo(Some(updates.value), Some(acc.value)) +: event.taskInfo.accumulables)
  }

acc.merge这个方法就完成了指标的更新。


event.taskInfo.setAccumulables这个是给当前event更新到最新的metrics,因为最终driver调用SparkListenerTaskEnd方法,从而被 AppStatusListener的onTaskEnd方法接受,从而完成Spark UI的更新(被AppStatusStore调用)。


同时也被SQLAppStatusListener的onTaskEnd方法接受,这里读者自己看代码即可,结果也是完成Spark UI的更新(被SQLAppStatusStore调用)


3.再看Executor端的reportHeartBeat方法:

   private def reportHeartBeat(): Unit = {
    ...
    val accumulatorsToReport =
      if (HEARTBEAT_DROP_ZEROES) {
        taskRunner.task.metrics.accumulators().filterNot(_.isZero)
      } else {
        taskRunner.task.metrics.accumulators()
      }
    accumUpdates += ((taskRunner.taskId, accumulatorsToReport))
    }
    ... 
     val message = Heartbeat(executorId, accumUpdates.toArray, env.blockManager.blockManagerId,
  executorUpdates)
  try {
  val response = heartbeatReceiverRef.askSync[HeartbeatResponse](
    message, new RpcTimeout(HEARTBEAT_INTERVAL_MS.millis, EXECUTOR_HEARTBEAT_INTERVAL.key))

这个reportHeartBeat会被周期的性的调用,用来向driver发送心跳信息,同时会带上metrics信息(包括spark内置的metrics和自定义的metrics),该方法通过向driver发送Heartbeat消息,最终会调用到DAGScheduler的executorHeartbeatReceived方法,从而被AppStatusListener的onExecutorMetricsUpdate方法接受:

  override def onExecutorMetricsUpdate(event: SparkListenerExecutorMetricsUpdate): Unit = {
  val now = System.nanoTime()
  event.accumUpdates.foreach { case (taskId, sid, sAttempt, accumUpdates) =>
    liveTasks.get(taskId).foreach { task =>
      val metrics = TaskMetrics.fromAccumulatorInfos(accumUpdates)
      val delta = task.updateMetrics(metrics)
      maybeUpdate(task, now)
      Option(liveStages.get((sid, sAttempt))).foreach { stage =>
       stage.metrics = LiveEntityHelpers.addMetrics(stage.metrics, delta)
       maybeUpdate(stage, now)
       val esummary = stage.executorSummary(event.execId)
       esummary.metrics = LiveEntityHelpers.addMetrics(esummary.metrics, delta)
       maybeUpdate(esummary, now)
     }
   }
}

这里更新的是正在运行的task的指标更新,从而更新到Spark UI界面(被AppStatusStore调用)。


还有被SQLAppStatusListener的onExecutorMetricsUpdate方法接受,这里读者自己看代码即可,结果也是完成Spark UI的更新(被SQLAppStatusStore调用)


总结


在Driver端定义的metrics,会被反序列化到Executor端,在Executor端,通过两种方式传回Driver端:


  • 在任务运行期间,利用heartbeat心跳来传递metrics

  • 在任务结束以后,利用任务结果的更新来传递metrics

  • 最终,都是通过sparkListener:SQLAppStatusListener和 AppStatusListener分别完成Spark UI状态的更新。
相关文章
|
3月前
|
人工智能 分布式计算 Serverless
EMR Serverless Daft 如何简化多模态数据处理:视频抽帧、清洗、标注全流程与具身智能实践
阿里云 EMR Serverless Spark 引入 Ray 分布式计算框架与 Daft 高性能数据引擎,为用户提供了一套开箱即用、免运维且极致高效的多模态数据处理基础设施。
|
7月前
|
人工智能 开发工具 C++
LangGraph vs Semantic Kernel:状态图与内核插件的两条技术路线对比
本文对比2026年最新版LangGraph(v1.0)与Semantic Kernel(v1.28.1),澄清过时认知:LangGraph已成为LangChain执行引擎,支持持久化状态机;SK则原生集成MCP协议,定位AI中间件。二者架构迥异——图编排vs插件组合,运行时托管状态vs开发者自主管理。附代码实操与选型指南。
531 2
LangGraph vs Semantic Kernel:状态图与内核插件的两条技术路线对比
|
9月前
|
设计模式 存储 人工智能
LLM全新智能体架构:核心组件、工作流程与设计模式全解析
随着生成式AI迈向生产力工具,智能体(Agent)架构成为关键。本文系统拆解其四大核心组件:大脑(LLM)、规划、记忆与工具,详解“感知-思考-行动”闭环流程及主流设计模式,助力开发者构建工业级AI应用。(238字)
1243 1
|
5月前
|
人工智能 安全 搜索推荐
AI技术在少儿英语学习中的应用
AI正推动少儿英语学习迈入“智能体时代”:针对3–12岁儿童注意力短、发音稚嫩、认知具象等特点,打造多模态虚拟外教、AI互动绘本、童音定制评测、动态生成故事四大核心场景,深度融合SLA教学理论、儿童专用语音模型与内容安全护栏,并通过家长端AI成长报告实现教育闭环。(239字)
|
10月前
|
SQL 存储 分布式计算
别让大数据“全表扫描”掏空你:数据分区策略与分区裁剪的实战心经
别让大数据“全表扫描”掏空你:数据分区策略与分区裁剪的实战心经
470 3
|
4月前
|
缓存 BI API
企业多模型 API 网关设计:路由、熔断、降级和计费
多模型网关的目标不是把架构画复杂,而是让企业在模型快速变化时有选择权。模型会继续升级,业务系统不应该跟着频繁重写。
580 0
|
9月前
|
机器学习/深度学习 传感器 算法
Python | K折交叉验证的参数优化的KNN(k近邻)预测及可视化算法
本教程介绍基于Python的KNN回归预测及四种参数优化方法(网格/随机/贝叶斯搜索、K折交叉验证),涵盖地球科学、医学、工程等多领域应用,附完整代码与数据可视化,助你快速掌握机器学习建模流程。
391 0
|
SQL 分布式计算 HIVE
|
IDE 程序员 开发工具
只用正版!教你5个方法,白嫖JetBrains家族的所有产品,包含:IntelliJ IDEA、PyCharm、WebStorm、CLion、Rider
程序员晚枫分享了5种官方认证的免费使用JetBrains家族产品的方法,包括内容创作者计划、开源项目支持、教育许可证、用户组支持和开发者认可计划。这些方法帮助个人开发者与小型团队合法获取强大开发工具,如IntelliJ IDEA、PyCharm等,降低开发成本,提升效率。同时提醒大家遵守使用规范,尊重知识产权。
3297 13
|
弹性计算 Ubuntu Shell
用1C1G ECS搭建《阿里云大模型高级工程师ACP认证》学习环境
阿里云推出了《[阿里云大模型高级工程师ACP认证》,配套的学习资料较系统性的梳理了提示词、RAG、Agent插件、微调等系列LLM相关知识。推荐大家学习。 该学习课程需要搭建学习环境,可以直接在ECS上构建该环境即可,所需的资源很少,1C1G20G系统盘最低配置即可,本文介绍了详细搭建过程。
1033 0