spark-listener详解

本文介绍Spark中的事件监听机制,即观察者模式的应用。通过实例详细解析了如何定义和使用监听器,以及监听器总线如何传递消息。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

注:不同spark版本源码可能不一样,本机spark版本是3.0.1

SparkListener

Spark中的事件监听机制,本质上其实就是观察者模式的实现,查看源码我们可以经常看到listener这种命名的类或对象,顾名思义,这就是监听器类或对象。下面就以SparkListener为例来解析事件监听是如何设计的。首先我们看SparkListener

*/***
 ** :: DeveloperApi ::*
 ** A* *default* *implementation* *for* `*SparkListenerInterface*` *that has no-op implementations* *for*
 ** all callbacks.*
 ***
 ** Note that* *this* *is an internal* *interface* *which* *might* *change* *in* *different* *Spark* *releases**.*
 **/*
@DeveloperApi
abstract class SparkListener extends SparkListenerInterface {
 //省略很多方法
 override def onApplicationStart(applicationStart: SparkListenerApplicationStart): Unit = { }
 override def onApplicationEnd(applicationEnd: SparkListenerApplicationEnd): Unit = { }
 
}

为了简化分析我们以应用的启动与结束来分析。这里我们可以看到onApplicationStart和onApplicationEnd两个方法,很明显是在应用执行前和执行完做一些事情。

监听器的结构

如果要要使得类在应用执行前和应用执行后做一些响应,就需要继承SparkListener类。

private class BasicJobCounter extends SparkListener{
  /**
   *app 开始时跳去的方法
   * 该方法可以获取 appId,appName ,app开始的时间以及 执行程序的用户
   * @param applicationStart
   */
  override def onApplicationStart(applicationStart: SparkListenerApplicationStart): Unit = {
    println("onApplicationStart")
    println(s"applicationStart.appAttemptId = ${applicationStart.appAttemptId}")
    println(s"applicationStart.appId = ${applicationStart.appId}")
    println(s"applicationStart.appName = ${applicationStart.appName}")
    println(s"applicationStart.driverLogs = ${applicationStart.driverLogs}")
    println(s"applicationStart.sparkUser = ${applicationStart.sparkUser}")
    println(s"applicationStart.time = ${applicationStart.time}")
    println("onApplicationStart")
  }
  /**
   * app结束时调用的方法
   * 可以获取app结束的时间点
   * @param applicationEnd
   */
  override def onApplicationEnd(applicationEnd: SparkListenerApplicationEnd): Unit = {
    println("onApplicationEnd")
    println(s"applicationEnd.time  =  ${applicationEnd.time}")
    println("onApplicationEnd")
  }

}

重载了onApplicationEnd和onApplicationStart方法

监听器如何使用

也许大家都会有一个困惑,BasicJobCounter仅仅继承SparkListener类就能获取事件的监听,甚至后者没有任何消息接受和处理逻辑,来实现事件响应呢?举个例子来给大家说明下

object ListenerTest {
 def main(args: Array[String]): Unit = {
  val listener = new BasicJobCounter
  val conf = new SparkConf().setAppName("listener").setMaster("local[*]")
   //.set("spark.extraListeners", classOf[BasicJobCounter].getName)
   .set("spark.executor.heartbeatInterval", "1000ms")
  val sc: SparkContext = new SparkContext(conf)
  sc.addSparkListener(listener)
  sc.setLogLevel("WARN")
  //2.source/读取数据
  //RDD:A Resilient Distributed Dataset (RDD):弹性分布式数据集,简单理解为分布式集合!使用起来和普通集合一样简单!
  //RDD[就是一行行的数据]
  val lines: RDD[String] = sc.textFile("data/input/words.txt")
  //3.transformation/数据操作/转换
  //切割:RDD【一个一个的单词】
  val words: RDD[String] = lines.flatMap(_.split(" "))
  // 记为1:RDD【(单词, 1)】
  val wordAndOnes: RDD[(String, Int)] = words.map((_,1))
  //分组聚合:groupBy + mapValues(_.map(_._2).reduce(_+_)) ===>在Spark里面分组+聚合一步搞定:reduceByKey
  val result: RDD[(String, Int)] = wordAndOnes.reduceByKey(_+_)
  //4.sink/输出
  //直接输出
  result.foreach(*println*)
  //收集为本地集合再输出
  //println(result.collect().toBuffer)
  //输出到指定path(可以是文件/夹)
  //result.repartition(1).saveAsTextFile("data/output/result")
  //为了便于查看Web-UI可以让程序睡一会
  //Thread.sleep(1000 * 6000)
  //*TODO 5.关闭资源*
  sc.stop()
 }
}

监听器如何工作

从代码可以看出,在申明一个BasicJobCounter对象之后,需要将该对象加入sc中,sc其实就是一个Spark application的入口。SparkContext中addSparkListener方法的代码如下:

*/***
 ** :: DeveloperApi ::*
 ** Register a listener to receive up-calls* *from* *events that happen during execution.*
 **/*
@DeveloperApi
def addSparkListener(listener: SparkListenerInterface): Unit = {
 listenerBus.addToSharedQueue(listener)
}

这里的listenerBus,是一个监听器总线对象,其声明如下:

// An asynchronous listener bus for Spark events
private[spark] def listenerBus: LiveListenerBus = _listenerBus

现在思路就变得很清晰了,BasicJobCounter对象是注册到LiveListenerBus对象中,然后通过LiveListenerBus对象来实现事件监听,其实这里我们通过取名就可以知道其设计思路,类似于计算机中的总线,设备都通过总线来传递消息,而LiveListenerBus就刚好充当了总线的角色,一个个SparkListener子类对象就是一个个的设备,它们可以接受来自总线的消息并作出相应的处理。

监听器总线如何传递消息

我们可以看一下LiveListenerBus类,该类实现了SparkListenerBus接口,直接看其入口start方法:

*/***
 ** Start an asynchronous thread to dispatch events to the underlying listeners.*
 ***
 *** ***@param\*** ***sc\*** *Used to stop the SparkContext in* *case* *the async dispatcher fails.*
 **/*
private[scheduler] def start(sc: SparkContext): Unit = {
 if (started.compareAndSet(false, true)) {
  this.sc = sc
  dispatchThread.start()
 } else {
  throw new IllegalStateException(s"$name already started!")
 }
}

直接启动了一个dispatchThread

private val dispatchThread = new Thread(s"spark-listener-group-$name") {
 setDaemon(true)
 override def run(): Unit = Utils.tryOrStopSparkContext(sc) {
  dispatch()
 }
}
private def dispatch(): Unit = LiveListenerBus.withinListenerThread.withValue(true) {
 var next: SparkListenerEvent = *eventQueue.take()
 while (next != POISON_PILL) {
  val ctx = processingTime.time()
  try {
   super.postToAll(next)
  } finally {
   ctx.stop()
  }
  eventCount.decrementAndGet()
  next = eventQueue.take()
 }
 eventCount.decrementAndGet()
}

可以看到这个dispatchThread是一个守护线程调用了dispatch方法,其核心逻辑就是不停地在一个事件队列eventQueue里取出事件,如果事件合法且LiverListenerBus没有被关停,就将事件通知给所有注册的listener中,postToAll方法在ListenerBus接口中实现:

final def postToAll(event: E): Unit = {
 // JavaConverters can create a JIterableWrapper if we use asScala.
 // However, this method will be called frequently. To avoid the wrapper cost, here we use
 // Java Iterator directly.
 val iter = listeners.iterator
 while (iter.hasNext) {
  val listener = iter.next()
  try {
   doPostEvent(listener, event)
  } catch {
   case NonFatal(e) =>
    logError(s"Listener ${Utils.getFormattedClassName(listener)} threw an exception", e)
  }
 }
}

思路很清晰,就是用迭代器遍历listener,逐个将消息发送。而doPostEvent方法是一个抽象方法,其具体实现要由继承自ListerBus的类负责,比如之前举例中的SparkListener,就有相应的SparkListenerBus接口:

private[spark] trait SparkListenerBus
 extends ListenerBus[SparkListenerInterface, SparkListenerEvent] {
 protected override def doPostEvent(
   listener: SparkListenerInterface,
   event: SparkListenerEvent): Unit = {
  event match {
      //部分代码省略
   case executorAdded: SparkListenerExecutorAdded =>
    listener.onExecutorAdded(executorAdded)
   case executorRemoved: SparkListenerExecutorRemoved =>
    listener.onExecutorRemoved(executorRemoved)
    //部分代码省略
   case _ => listener.onOtherEvent(event)
  }
 }
}

这里对每个事件进行类型匹配,比如doPostEvent需要将onApplicationStart事件告知一个listener,对应地,这个listener就调用一下自己的onApplicationStart方法,对该事件作出自己的反应.

小结

总结之,Spark中监听器的实现核心其实就是一个个需要对事件响应的监听器对象,注册到一个监听器总线,需要发送事件消息的组件将发生的事件消息提交到总线,然后总线将事件消息转发给一个个注册在它上面的监听器,最后监听器对事件进行响应。其实就是一个典型的观察者模式使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值