Spark2.1.0——深入理解事件总线

概览

  Spark程序在运行的过程中,Driver端的很多功能都依赖于事件的传递和处理,而事件总线在这中间发挥着至关重要的纽带作用。事件总线通过异步线程,提高了Driver执行的效率。

Spark定义了一个特质[1]ListenerBus,可以接收事件并且将事件提交到对应事件的监听器。为了对ListenerBus有个直观的理解,我们先来看看它的代码实现,见代码清单1。

代码清单1        ListenerBus的定义

private[spark] trait ListenerBus[L <: AnyRef, E] extends Logging {

  private[spark] val listeners = new CopyOnWriteArrayList[L]

  final def addListener(listener: L): Unit = {
listeners.add(listener)
} final def removeListener(listener: L): Unit = {
listeners.remove(listener)
} final def postToAll(event: E): Unit = {
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)
}
}
} protected def doPostEvent(listener: L, event: E): Unit private[spark] def findListenersByClass[T <: L : ClassTag](): Seq[T] = {
val c = implicitly[ClassTag[T]].runtimeClass
listeners.asScala.filter(_.getClass == c).map(_.asInstanceOf[T]).toSeq
} }

代码清单1中展示了ListenerBus是个泛型特质,其泛型参数为 [L <: AnyRef, E],其中L是代表监听器的泛型参数,可以看到ListenerBus支持任何类型的监听器,E是代表事件的泛型参数。ListenerBus中各个成员的作用如下:

  • listeners:用于维护所有注册的监听器,其数据结构为CopyOnWriteArrayList[L];
  • addListener:向listeners中添加监听器的方法,由于listeners采用CopyOnWriteArrayList来实现,所以addListener方法是线程安全的;
  • removeListener:从listeners中移除监听器的方法,由于listeners采用CopyOnWriteArrayList来实现,所以removeListener方法是线程安全的;
  • postToAll:此方法的作用是将事件投递给所有的监听器。虽然CopyOnWriteArrayList本身是线程的安全的,但是由于postToAll方法内部引入了“先检查后执行”的逻辑,因而postToAll方法不是线程安全的,所以所有对postToAll方法的调用应当保证在同一个线程中;
  • doPostEvent:用于将事件投递给指定的监听器,此方法只提供了接口定义,具体实现需要子类提供;
  • findListenersByClass:查找与指定类型相同的监听器列表。

下面将分别对以下内容进行介绍:

  1. ListenerBus的继承体系
  2. SparkListenerBus详解
  3. LiveListenerBus详解

[1] 特质是Scala语言中提供真正的多重继承的语法特性,类似于Java的Interface,但是又可以实现方法。有关Scala特质的更多介绍请访问Scala官网http://www.scala-lang.org。

ListenerBus的继承体系

理解了ListenerBus的定义后,本小节一起来看看有哪些类继承了它。ListenerBus的类继承体系如图1所示。

图1  ListenerBus的类继承体系

从图1中可以看到有三种ListenerBus的具体实现,分别为:

  • SparkListenerBus:用于将SparkListenerEvent类型的事件投递到SparkListenerInterface类型的监听器;
  • StreamingQueryListenerBus:用于将StreamingQueryListener.Event类型的事件投递到StreamingQueryListener类型的监听器,此外还会将StreamingQueryListener.Event类型的事件交给SparkListenerBus;
  • StreamingListenerBus:用于将StreamingListenerEvent类型的事件投递到StreamingListener类型的监听器,此外还会将StreamingListenerEvent类型的事件交给SparkListenerBus。

SparkListenerBus也有两种实现:

  • LiveListenerBus:采用异步线程将SparkListenerEvent类型的事件投递到SparkListener类型的监听器;
  • ReplayListenerBus:用于从序列化的事件数据中重播事件。

有了对事件总线的这些介绍,读者已经在宏观上对其有所认识。但是如果没有具体的实现,ListenerBus本身也无法发挥作用。下一小节我们将选择对SparkListenerBus从更加微观的角度说明如何使用事件总线。

SparkListenerBus详解

  有了上一节对ListenerBus类继承体系的介绍,本小节将详细介绍SparkListenerBus的实现,见代码清单2。

代码清单2         SparkListenerBus的实现

private[spark] trait SparkListenerBus
extends ListenerBus[SparkListenerInterface, SparkListenerEvent] { protected override def doPostEvent(
listener: SparkListenerInterface,
event: SparkListenerEvent): Unit = {
event match {
case stageSubmitted: SparkListenerStageSubmitted =>
listener.onStageSubmitted(stageSubmitted)
case stageCompleted: SparkListenerStageCompleted =>
listener.onStageCompleted(stageCompleted)
case jobStart: SparkListenerJobStart =>
listener.onJobStart(jobStart)
case jobEnd: SparkListenerJobEnd =>
listener.onJobEnd(jobEnd)
case taskStart: SparkListenerTaskStart =>
listener.onTaskStart(taskStart)
case taskGettingResult: SparkListenerTaskGettingResult =>
listener.onTaskGettingResult(taskGettingResult)
case taskEnd: SparkListenerTaskEnd =>
listener.onTaskEnd(taskEnd)
case environmentUpdate: SparkListenerEnvironmentUpdate =>
listener.onEnvironmentUpdate(environmentUpdate)
case blockManagerAdded: SparkListenerBlockManagerAdded =>
listener.onBlockManagerAdded(blockManagerAdded)
case blockManagerRemoved: SparkListenerBlockManagerRemoved =>
listener.onBlockManagerRemoved(blockManagerRemoved)
case unpersistRDD: SparkListenerUnpersistRDD =>
listener.onUnpersistRDD(unpersistRDD)
case applicationStart: SparkListenerApplicationStart =>
listener.onApplicationStart(applicationStart)
case applicationEnd: SparkListenerApplicationEnd =>
listener.onApplicationEnd(applicationEnd)
case metricsUpdate: SparkListenerExecutorMetricsUpdate =>
listener.onExecutorMetricsUpdate(metricsUpdate)
case executorAdded: SparkListenerExecutorAdded =>
listener.onExecutorAdded(executorAdded)
case executorRemoved: SparkListenerExecutorRemoved =>
listener.onExecutorRemoved(executorRemoved)
case blockUpdated: SparkListenerBlockUpdated =>
listener.onBlockUpdated(blockUpdated)
case logStart: SparkListenerLogStart => // ignore event log metadata
case _ => listener.onOtherEvent(event)
}
} }

我们看到SparkListenerBus已经实现了ListenerBus的doPostEvent方法,通过对SparkListenerEvent事件的匹配,执行SparkListenerInterface监听器的相应方法。

这里的SparkListenerEvent其实是个特质,代码清单2中列出的SparkListenerStageSubmitted、SparkListenerStageCompleted等都是继承了SparkListenerEvent特质的样例类[2]。为说明问题,这里仅仅摘选SparkListenerEvent及部分SparkListenerEvent子类的实现如下:

@DeveloperApi
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "Event")
trait SparkListenerEvent {
protected[spark] def logEvent: Boolean = true
} @DeveloperApi
case class SparkListenerStageSubmitted(stageInfo: StageInfo, properties: Properties = null)
extends SparkListenerEvent @DeveloperApi
case class SparkListenerStageCompleted(stageInfo: StageInfo) extends SparkListenerEvent @DeveloperApi
case class SparkListenerTaskStart(stageId: Int, stageAttemptId: Int, taskInfo: TaskInfo)
extends SparkListenerEvent
// 省略其他SparkListenerEvent的实现
private[spark] case class SparkListenerLogStart(sparkVersion: String) extends SparkListenerEvent

  SparkListenerInterface也是一个特质,其中定义了所有SparkListener应当遵守的接口规范。由于SparkListenerInterface中定义了很多接口,为说明问题只摘抄SparkListenerInterface中的部分接口定义,代码如下:

private[spark] trait SparkListenerInterface {
def onStageCompleted(stageCompleted: SparkListenerStageCompleted): Unit
def onStageSubmitted(stageSubmitted: SparkListenerStageSubmitted): Unit
// 省略其他接口方法
def onOtherEvent(event: SparkListenerEvent): Unit
}

结合代码清单2,我们知道以上代码片段中的onStageCompleted和onStageSubmitted将在SparkListenerBus的doPostEvent方法中分别匹配到SparkListenerStageCompleted和SparkListenerStageSubmitted事件时执行,而对于doPostEvent中无法匹配的事件,都将执行onOtherEvent方法。

在详细介绍了ListenerBus及SparkListenerBus后,我们知道当有事件需要通知监听器的时候,可以调用ListenerBus的postToAll方法,postToAll方法遍历所有监听器并调用SparkListenerBus实现的doPostEvent方法,doPostEvent方法对事件类型进行匹配后调用监听器的不同方法。整个投递事件的过程是通过方法调用实现的,所以这是一个同步调用。在监听器比较多的时候这个过程会相对比较耗时(比如用于写日志的EventLoggingListener在调度频繁的时候,有可能导致写入延迟,这将导致部分事件的丢失。此问题已在spark2.3.0版本中得到改进。),在Spark UI(在《Spark内核设计的艺术 架构设计与实现》一书的第4章中详细介绍)中为了达到页面的即时刷新 ,实现了SparkListenerBus的子类LiveListenerBus。下一小节将围绕LiveListenerBus来详细说明异步投递消息的实现细节。


[2] 样例类是Scala语言的语法特性。样例类是一种特殊的类型,常用作事件、参数、模式匹配等。有关样例类的更多介绍,请读者阅读Scala语言的相关资料。

LiveListenerBus详解

  LiveListenerBus继承了SparkListenerBus,并实现了将事件异步投递给监听器,达到实时刷新UI界面数据的效果。LiveListenerBus主要由以下部分组成:

  • eventQueue:是SparkListenerEvent事件的阻塞队列,队列大小可以通过Spark属性spark.scheduler.listenerbus.eventqueue.size进行配置,默认为10000(Spark早期版本中属于静态属性,固定为10000,这导致队列堆满时,只得移除一些最老的事件,最终导致各种问题与bug);
  • started:标记LiveListenerBus的启动状态的AtomicBoolean类型的变量;
  • stopped:标记LiveListenerBus的停止状态的AtomicBoolean类型的变量;
  • droppedEventsCounter:使用AtomicLong类型对删除的事件进行计数,每当日志打印了droppedEventsCounter后,会将droppedEventsCounter重置为0;
  • lastReportTimestamp:用于记录最后一次日志打印droppedEventsCounter的时间戳;
  • processingEvent:用来标记当前正有事件被listenerThread线程处理;
  • logDroppedEvent:AtomicBoolean类型的变量,用于标记是否由于eventQueue已满,导致新的事件被删除;
  • eventLock:用于当有新的事件到来时释放信号量,当对事件进行处理时获取信号量;
  • listeners:继承自LiveListenerBus的监听器数组;
  • listenerThread:处理事件的线程。

异步事件处理线程

listenerThread用于异步处理eventQueue中的事件,为了便于说明,这里将展示listenerThread及LiveListenerBus中的主要代码片段,见代码清单3。

代码清单3         LiveListenerBus主要逻辑的代码片段

  private lazy val EVENT_QUEUE_CAPACITY = validateAndGetQueueSize()
private lazy val eventQueue = new LinkedBlockingQueue[SparkListenerEvent](EVENT_QUEUE_CAPACITY) private def validateAndGetQueueSize(): Int = {
val queueSize = sparkContext.conf.get(LISTENER_BUS_EVENT_QUEUE_SIZE)
if (queueSize <= 0) {
throw new SparkException("spark.scheduler.listenerbus.eventqueue.size must be > 0!")
}
queueSize
} private val started = new AtomicBoolean(false)
private val stopped = new AtomicBoolean(false)
private val droppedEventsCounter = new AtomicLong(0L)
@volatile private var lastReportTimestamp = 0L
private var processingEvent = false
private val logDroppedEvent = new AtomicBoolean(false)
private val eventLock = new Semaphore(0) private val listenerThread = new Thread(name) {
setDaemon(true)
override def run(): Unit = Utils.tryOrStopSparkContext(sparkContext) {
LiveListenerBus.withinListenerThread.withValue(true) {
while (true) {
eventLock.acquire() // 获取信号量
self.synchronized {
processingEvent = true
}
try {
val event = eventQueue.poll //从eventQueue中获取事件
if (event == null) {
// Get out of the while loop and shutdown the daemon thread
if (!stopped.get) {
throw new IllegalStateException("Polling `null` from eventQueue means" +
" the listener bus has been stopped. So `stopped` must be true")
}
return
}
postToAll(event) // 事件处理
} finally {
self.synchronized {
processingEvent = false
}
}
}
}
}
}

通过分析代码清单3,listenerThread的工作步骤为:

  1. 不断获取信号量(当可以获取信号量时,说明还有事件未处理);
  2. 通过同步控制,将processingEvent设置为true;
  3. 从eventQueue中获取事件;
  4. 调用超类ListenerBus的postToAll方法(postToAll方法对监听器进行遍历,并调用SparkListenerBus的doPostEvent方法对事件进行匹配后执行监听器的相应方法);
  5. 每次循环结束依然需要通过同步控制,将processingEvent设置为false;

值得一提的是,listenerThread的run方法中调用了Utils的tryOrStopSparkContext,tryOrStopSparkContext方法可以保证当listenerThread的内部循环抛出异常后启动一个新的线程停止SparkContext(SparkContext的内容将在第4章详细介绍,tryOrStopSparkContext方法的具体实现请阅读Utils工具类的实现)。

LiveListenerBus的消息投递

在解释了异步线程listenerThread的工作内容后,还有一个要点没有解释:eventQueue中的事件是如何放进去的呢?由于eventQueue定义在LiveListenerBus中,因此ListenerBus和SparkListenerBus中并没有操纵eventQueue的方法,要将事件放入eventQueue只能依靠LiveListenerBus自己了,其post方法就是为此目的而生的,见代码清单4。

代码清单4        向LiveListenerBus投递SparkListenerEvent事件

  def post(event: SparkListenerEvent): Unit = {
if (stopped.get) {
logError(s"$name has already stopped! Dropping event $event")
return
}
val eventAdded = eventQueue.offer(event) // 向eventQueue中添加事件
if (eventAdded) {
eventLock.release()
} else {
onDropEvent(event)
droppedEventsCounter.incrementAndGet()
}
// 打印删除事件数的日志
val droppedEvents = droppedEventsCounter.get
if (droppedEvents > 0) {
if (System.currentTimeMillis() - lastReportTimestamp >= 60 * 1000) {
if (droppedEventsCounter.compareAndSet(droppedEvents, 0)) {
val prevLastReportTimestamp = lastReportTimestamp
lastReportTimestamp = System.currentTimeMillis()
logWarning(s"Dropped $droppedEvents SparkListenerEvents since " +
new java.util.Date(prevLastReportTimestamp))
}
}
}
}

从代码清单4看到post方法的处理步骤如下:

  1. 判断LiveListenerBus是否已经处于停止状态;
  2. 向eventQueue中添加事件。如果添加成功,则释放信号量进而催化listenerThread能够有效工作。如果eventQueue已满造成添加失败,则移除事件,并对删除事件计数器droppedEventsCounter进行自增;
  3. 如果有事件被删除,并且当前系统时间距离上一次打印droppedEventsCounter超过了60秒则将droppedEventsCounter打印到日志。

LiveListenerBus与监听器

与LiveListenerBus配合使用的监听器,并非是父类SparkListenerBus的类型参数SparkListenerInterface,而是继承自SparkListenerInterface的SparkListener及其子类。图2列出了Spark中监听器SparkListener以及它的6种最常用的实现[3]

图2     SparkListener的类继承体系

SparkListener虽然实现了SparkListenerInterface中的每个方法,但是其实都是空实现,具体的实现需要交给子类去完成。

本文首先对事件总线的接口定义进行了一些介绍,之后选择ListenerBus的子类SparkListenerBus与LiveListenerBus作为具体的实现例子进行分析,最后本文选择LiveListenerBus作为具体的实现例子进行分析,这里将通过图3更加直观的展示ListenerBus、SparkListenerBus及LiveListenerBus的工作原理。

图3     LiveListenerBus的工作流程图

最后对于图3作一些补充说明:图中的DAGScheduler、SparkContext、BlockManagerMasterEndpoint、DriverEndpoint及LocalSchedulerBackend都是LiveListenerBus的事件来源,它们都是通过调用LiveListenerBus的post方法将消息交给异步线程listenerThread处理的。


[3] 除了本节列出的的六种SparkListener的子类外,还有很多其他的子类,这里就不一一列出了,感兴趣的读者可以查阅Spark相关文档或阅读源码知晓。

关于《Spark内核设计的艺术 架构设计与实现》

经过近一年的准备,基于Spark2.1.0版本的《Spark内核设计的艺术 架构设计与实现》一书现已出版发行,图书如图:

纸质版售卖链接如下:

京东:https://item.jd.com/12302500.html

Spark2.1.0——深入理解事件总线的更多相关文章

  1. Android 框架学习2:源码分析 EventBus 3.0 如何实现事件总线

    Go beyond yourself rather than beyond others. 上篇文章 深入理解 EventBus 3.0 之使用篇 我们了解了 EventBus 的特性以及如何使用,这 ...

  2. Spark2.1.0之源码分析——事件总线

    阅读提示:阅读本文前,最好先阅读<Spark2.1.0之源码分析——事件总线>.<Spark2.1.0事件总线分析——ListenerBus的继承体系>及<Spark2. ...

  3. Android事件总线分发库EventBus3.0的简单讲解与实践

    Android事件总线分发库EventBus的简单讲解与实践 导语,EventBus大家应该不陌生,EventBus是一款针对Android优化的发布/订阅事件总线.主要功能是替代Intent,Han ...

  4. 并发编程概述 委托(delegate) 事件(event) .net core 2.0 event bus 一个简单的基于内存事件总线实现 .net core 基于NPOI 的excel导出类,支持自定义导出哪些字段 基于Ace Admin 的菜单栏实现 第五节:SignalR大杂烩(与MVC融合、全局的几个配置、跨域的应用、C/S程序充当Client和Server)

    并发编程概述   前言 说实话,在我软件开发的头两年几乎不考虑并发编程,请求与响应把业务逻辑尽快完成一个星期的任务能两天完成绝不拖三天(剩下时间各种浪),根本不会考虑性能问题(能接受范围内).但随着工 ...

  5. Android开发事件总线之EventBus运用和框架原理深入理解

    [Android]事件总线之EventBus的使用背景 在我们的android项目开发过程中,经常会有各个组件如activity,fragment和service之间,各个线程之间的通信需求:项目中用 ...

  6. EventBus 事件总线之我的理解

    用例:假设公司发布了一个公告 需要通过短信 和 邮件分别2种方式 通知员工 1:首先我们建立领域模型 /// <summary> /// 领域核心基类 /// </summary&g ...

  7. vue中央事件总线eventBus的简单理解和使用

    公共事件总线eventBus的实质就是创建一个vue实例,通过一个空的vue实例作为桥梁实现vue组件间的通信.它是实现非父子组件通信的一种解决方案. 用法如下: 第一步:项目中创建一个js文件(我通 ...

  8. Android事件总线(一)EventBus3.0用法全解析

    前言 EventBus是一款针对Android优化的发布/订阅事件总线.简化了应用程序内各组件间.组件与后台线程间的通信.优点是开销小,代码更优雅,以及将发送者和接收者解耦.如果Activity和Ac ...

  9. 基于ASP.NET Core 5.0使用RabbitMQ消息队列实现事件总线(EventBus)

    文章阅读请前先参考看一下 https://www.cnblogs.com/hudean/p/13858285.html 安装RabbitMQ消息队列软件与了解C#中如何使用RabbitMQ 和 htt ...

随机推荐

  1. Anroid 手机助手 详细解析 概述(二)

    这篇主要说一下手机插入之后的一些动作. 1)  捕获窗口消息 插入拔出一个USB设备windows 会给所有的窗口发送特定的消息,只要我们捕获这些消息就可以处理设备插入和拔出.需要注意的是插入或者拔出 ...

  2. 异步多线程 ASP.NET 同步调用异步 使用Result产生死锁

    一个方法调用了async方法,要将这个方法本身设计为async. public class BlogController : Controller { public async Task<Act ...

  3. 第二天:Javascript事件

    事件:是可以被Javascript侦测到的行为,例如鼠标的点击,鼠标的移动,常见的事件如下   代码实现“点击事件”: <body> <button onclick="de ...

  4. HMAILSERVER集成WEB邮件系统(ROUNDCUBE WEBMAIL)

    hMailServer集成web邮件系统(Roundcube Webmail) 文/玄魂 前言 在上篇文章(使用hMailServer搭建邮件服务器)中,介绍了hMailServer的安装和简单配置. ...

  5. SQL Server--疑难杂症之坑爹的Windows故障转移群集

    --============================================================== 估计是春节前最后一次写博客,也估计是本年值班最后一次踩雷,感叹下成也S ...

  6. SignalR简介

    什么是SignalR? ASP.NET SignalR是ASP.NET开发人员的库,它简化了向应用程序添加实时Web功能的过程.实时Web功能是指服务器代码在连接的客户端可用时立即将内容推送到连接的客 ...

  7. 【转】[MySQL复制异常]Cannot execute statement: impossible to write to binary log since statement is in row for

    MySQL复制错误]Last_Errno: 1666 Last_Error: Error executing row event: 'Cannot execute statement: imposs ...

  8. C语言通过匿名管道实现反弹式CMDShell

    #pragma comment(lib,"ws2_32.lib") #ifdef _MSC_VER #pragma comment( linker, "/subsyste ...

  9. sublime text3: markdown 安装及常用语法简介

    自己上传到 github 上的 README.rdm 文件内容显示没有“美化”,所有内容都挤在一块儿了,很不舒服. 原因是:github 的文档 README.rdm 文件使用 markdown 编辑 ...

  10. 初印象至Vue路由

    初印象系列为快速了解一门技术的内容,后续会推出本人应用这门技术时发现的一些认识. Vue路由和传统路由的区别: Vue路由主要是用来实现单页面应用内各个组件之间的切换,同样支持传递参数等功能.而传统路 ...