《深入理解Spark:核心思想与源码分析》一书前言的内容请看链接《深入理解SPARK:核心思想与源码分析》一书正式出版上市

《深入理解Spark:核心思想与源码分析》一书第一章的内容请看链接《第1章 环境准备》

《深入理解Spark:核心思想与源码分析》一书第二章的内容请看链接《第2章 SPARK设计理念与基本架构》

由于本书的第3章内容较多,所以打算分别开辟四篇随笔分别展现。

《深入理解Spark:核心思想与源码分析》一书第三章第一部分的内容请看链接《深入理解Spark:核心思想与源码分析》——SparkContext的初始化(伯篇)》

《深入理解Spark:核心思想与源码分析》一书第三章第二部分的内容请看链接《深入理解Spark:核心思想与源码分析》——SparkContext的初始化(仲篇)》

《深入理解Spark:核心思想与源码分析》一书第三章第二部分的内容请看链接《深入理解Spark:核心思想与源码分析》——SparkContext的初始化(叔篇)》

本文展现第3章第三部分的内容:

3.9 启动测量系统MetricsSystem

MetricsSystem使用codahale提供的第三方测量仓库Metrics,有关Metrics的具体信息可以参考附录D。MetricsSystem中有三个概念:

q  Instance:指定了谁在使用测量系统;

q  Source:指定了从哪里收集测量数据;

q  Sink:指定了往哪里输出测量数据。

Spark按照Instance的不同,区分为Master、Worker、Application、Driver和Executor。

Spark目前提供的Sink有ConsoleSink、CsvSink、JmxSink、MetricsServlet、GraphiteSink等。

Spark中使用MetricsServlet作为默认的Sink。

MetricsSystem的启动代码如下。

val metricsSystem = env.metricsSystem
metricsSystem.start()

MetricsSystem的启动过程包括以下步骤:

1) 注册Sources;

2) 注册Sinks;

3) 给Sinks增加Jetty的ServletContextHandler。

MetricsSystem启动完毕后,会遍历与Sinks有关的ServletContextHandler,并调用attachHandler将它们绑定到SparkUI上。

metricsSystem.getServletHandlers.foreach(handler => ui.foreach(_.attachHandler(handler)))

3.9.1 注册Sources

  registerSources方法用于注册Sources,它的实现见代码清单3-44。注册Sources的过程分为以下步骤:

1) 从metricsConfig获取Driver的Properties,默认为创建MetricsSystem的过程中解析的{sink.servlet.class=org.apache.spark.metrics.sink.MetricsServlet, sink.servlet.path=/metrics/json}。

2) 从Driver的Properties中用正则匹配以source.开头的属性。然后将属性中的Source反射得到的实例,加入ArrayBuffer[Source]。

3) 将每个Source的metricRegistry(也是MetricSet的子类型)注册到ConcurrentMap<String, Metric> metrics。这里的registerSource方法已在3.8.2节讲解过。

代码清单3-44         MetricsSystem

private def registerSources() {

    val instConfig = metricsConfig.getInstance(instance)
val sourceConfigs = metricsConfig.subProperties(instConfig, MetricsSystem.SOURCE_REGEX) // Register all the sources related to instance
sourceConfigs.foreach { kv =>
val classPath = kv._2.getProperty("class")
try {
val source = Class.forName(classPath).newInstance()
registerSource(source.asInstanceOf[Source])
} catch {
case e: Exception => logError("Source class " + classPath + " cannot be instantiated", e)
}
}
}

3.9.2 注册Sinks

  registerSinks方法用于注册Sinks,它的实现见代码清单3-45。注册Sinks的步骤如下:

1) 从Driver的Properties中用正则匹配以sink.开头的属性,如:{sink.servlet.class=org.apache.spark.metrics.sink.MetricsServlet, sink.servlet.path=/metrics/json}。将其转换为Map(servlet -> {class=org.apache.spark.metrics.sink.MetricsServlet, path=/metrics/json})。

2) 将子属性class对应的类metricsServlet反射得到MetricsServlet实例。如果属性的key是servlet,将其设置为metricsServlet;如果是Sink,则加入到ArrayBuffer[Sink]中。

代码清单3-45         MetricsSystem注册Sinks的实现

  private def registerSinks() {
val instConfig = metricsConfig.getInstance(instance)
val sinkConfigs = metricsConfig.subProperties(instConfig, MetricsSystem.SINK_REGEX)
sinkConfigs.foreach { kv =>
val classPath = kv._2.getProperty("class")
if (null != classPath) {
try {
val sink = Class.forName(classPath)
.getConstructor(classOf[Properties], classOf[MetricRegistry], classOf[SecurityManager])
.newInstance(kv._2, registry, securityMgr) if (kv._1 == "servlet") {
metricsServlet = Some(sink.asInstanceOf[MetricsServlet])
} else {
sinks += sink.asInstanceOf[Sink]
}
} catch {
case e: Exception => logError("Sink class "+ classPath + " cannot be instantialized",e)
}
}
}
}

3.9.3给Sinks增加Jetty的ServletContextHandler

MetricsSystem的getServletHandlers方法,实现如下。

  def getServletHandlers = {
require(running, "Can only call getServletHandlers on a running MetricsSystem")
metricsServlet.map(_.getHandlers).getOrElse(Array())
}

可以看到调用了metricsServlet的getHandlers,其实现如下。

def getHandlers = Array[ServletContextHandler](
createServletHandler(servletPath,
new ServletParams(request => getMetricsSnapshot(request), "text/json"), securityMgr)
)

最终生成处理/metrics/json请求的ServletContextHandler,而请求的真正处理由getMetricsSnapshot方法,利用fastjson解析。生成的ServletContextHandler通过SparkUI的attachHandler方法,也被绑定到SparkUI。createServletHandler与attachHandler方法都已经在3.4.4节详细阐述。最终我们可以使用以下这些地址来访问测量数据。

http://localhost:4040/metrics/applications/json

http://localhost:4040/metrics/json

http://localhost:4040/metrics/master/json

3.10 创建和启动ExecutorAllocationManager

  ExecutorAllocationManager用于动态分配executor,创建和启动ExecutorAllocationManager的代码如下。

  private[spark] val executorAllocationManager: Option[ExecutorAllocationManager] =

    if (conf.getBoolean("spark.dynamicAllocation.enabled", false)) {
Some(new ExecutorAllocationManager(this, listenerBus, conf))
} else {
None
} executorAllocationManager.foreach(_.start())

默认情况下不会创建ExecutorAllocationManager,可以修改属性spark.dynamicAllocation.enabled为true来创建。ExecutorAllocationManager可以设置动态分配最小Executor数量、动态分配最大Executor数量、每个Executor可以运行的Task数量等配置信息,并对配置信息进行校验。start方法将ExecutorAllocationListener加入到listenerBus中,ExecutorAllocationListener通过监听listenerBus里的事件,动态添加删除executor。并且通过Thread不断的添加executor,并且遍历executor,将超时的executor杀掉并且移除。ExecutorAllocationListener的实现与其他SparkListener类似,不再赘述。ExecutorAllocationManager的关键代码见代码清单3-46。

代码清单3-46         ExecutorAllocationManagerr的关键代码

  private val intervalMillis: Long = 100
private var clock: Clock = new RealClock
private val listener = new ExecutorAllocationListener def start(): Unit = {
listenerBus.addListener(listener)
startPolling()
} private def startPolling(): Unit = { val t = new Thread {
override def run(): Unit = {
while (true) {
try {
schedule()
} catch {
case e: Exception => logError("Exception in dynamic executor allocation thread!", e)
} Thread.sleep(intervalMillis)
}
}
} t.setName("spark-dynamic-executor-allocation")
t.setDaemon(true)
t.start()
}

根据3.4.1节的内容,我们知道listenerBus内置了线程listenerThread,此线程不断从eventQueue中拉出事件对象,调用监听器的监听方法。要启动此线程,需要调用listenerBus的start方法,代码如下。

  listenerBus.start()

3.11 ContextCleaner的创建与启动

  由于配置属性spark.cleaner.referenceTracking默认是true,所以会构造并启动ContextCleaner,代码如下。

  private[spark] val cleaner: Option[ContextCleaner] = {
if (conf.getBoolean("spark.cleaner.referenceTracking", true)) {
Some(new ContextCleaner(this))
} else {
None
}
} cleaner.foreach(_.start())

ContextCleaner用于清理那些超出应用范围的RDD、ShuffleDependency和Broadcast对象。ContextCleaner的组成如下:

q  referenceQueue:缓存顶级的AnyRef引用;

q  referenceBuffer:缓存AnyRef的虚引用;

q  listeners:缓存清理工作的监听器数组;

q  cleaningThread:用于具体清理工作的线程。

ContextCleaner的工作原理和listenerBus一样,也采用监听器模式,由线程来处理,此线程实际只是调用keepCleaning方法。keepCleaning的实现见代码清单3-47。

代码清单3-47         ContextCleaner的实现

  private def keepCleaning(): Unit = Utils.logUncaughtExceptions {

    while (!stopped) {
try {
val reference = Option(referenceQueue.remove(ContextCleaner.REF_QUEUE_POLL_TIMEOUT))
.map(_.asInstanceOf[CleanupTaskWeakReference]) // Synchronize here to avoid being interrupted on stop()
synchronized {
reference.map(_.task).foreach { task =>
logDebug("Got cleaning task " + task)
referenceBuffer -= reference.get
task match {
case CleanRDD(rddId) =>
doCleanupRDD(rddId, blocking = blockOnCleanupTasks) case CleanShuffle(shuffleId) =>
doCleanupShuffle(shuffleId, blocking = blockOnShuffleCleanupTasks) case CleanBroadcast(broadcastId) =>
doCleanupBroadcast(broadcastId, blocking = blockOnCleanupTasks)
}
}
}
} catch {
case ie: InterruptedException if stopped => // ignore
case e: Exception => logError("Error in cleaning thread", e)
}
}
}

3.12 Spark环境更新

  在SparkContext的初始化过程中,可能对其环境造成影响,所以需要更新环境,代码如下。

postEnvironmentUpdate()
postApplicationStart()

SparkContext初始化过程中,如果设置了spark.jars属性, spark.jars指定的jar包将由addJar方法加入到httpFileServer的jarDir变量指定的路径下。spark.files指定的文件将由addFile方法加入到httpFileServer的fileDir变量指定的路径下。见代码清单3-48。

代码清单3-48         依赖文件处理

  val jars: Seq[String] =
conf.getOption("spark.jars").map(_.split(",")).map(_.filter(_.size != 0)).toSeq.flatten val files: Seq[String] =
conf.getOption("spark.files").map(_.split(",")).map(_.filter(_.size != 0)).toSeq.flatten // Add each JAR given through the constructor
if (jars != null) {
jars.foreach(addJar)
} if (files != null) {
files.foreach(addFile)
}

httpFileServer的addFile和addJar方法,见代码清单3-49。

代码清单3-49         HttpFileServer提供对依赖文件的访问

  def addFile(file: File) : String = {
addFileToDir(file, fileDir)
serverUri + "/files/" + file.getName
} def addJar(file: File) : String = {
addFileToDir(file, jarDir)
serverUri + "/jars/" + file.getName
} def addFileToDir(file: File, dir: File) : String = {
if (file.isDirectory) {
throw new IllegalArgumentException(s"$file cannot be a directory.")
} Files.copy(file, new File(dir, file.getName))
dir + "/" + file.getName
}

postEnvironmentUpdate的实现见代码清单3-50,其处理步骤如下:

1) 通过调用SparkEnv的方法environmentDetails最终影响环境的JVM参数、Spark 属性、系统属性、classPath等,参见代码清单3-51。

2) 生成事件SparkListenerEnvironmentUpdate,并post到listenerBus,此事件被EnvironmentListener监听,最终影响EnvironmentPage页面中的输出内容。

代码清单3-50         SparkContext环境更新

  private def postEnvironmentUpdate() {

    if (taskScheduler != null) {
val schedulingMode = getSchedulingMode.toString
val addedJarPaths = addedJars.keys.toSeq
val addedFilePaths = addedFiles.keys.toSeq
val environmentDetails =
SparkEnv.environmentDetails(conf, schedulingMode, addedJarPaths, addedFilePaths)
val environmentUpdate = SparkListenerEnvironmentUpdate(environmentDetails) listenerBus.post(environmentUpdate)
}
}

代码清单3-51         environmentDetails的实现

   val jvmInformation = Seq(
("Java Version", s"$javaVersion ($javaVendor)"),
("Java Home", javaHome),
("Scala Version", versionString)
).sorted val schedulerMode =
if (!conf.contains("spark.scheduler.mode")) {
Seq(("spark.scheduler.mode", schedulingMode))
} else {
Seq[(String, String)]()
} val sparkProperties = (conf.getAll ++ schedulerMode).sorted // System properties that are not java classpaths
val systemProperties = Utils.getSystemProperties.toSeq
val otherProperties = systemProperties.filter { case (k, _) =>
k != "java.class.path" && !k.startsWith("spark.")
}.sorted // Class paths including all added jars and files val classPathEntries = javaClassPath
.split(File.pathSeparator)
.filterNot(_.isEmpty)
.map((_, "System Classpath"))
val addedJarsAndFiles = (addedJars ++ addedFiles).map((_, "Added By User"))
val classPaths = (addedJarsAndFiles ++ classPathEntries).sorted Map[String, Seq[(String, String)]](
"JVM Information" -> jvmInformation,
"Spark Properties" -> sparkProperties,
"System Properties" -> otherProperties,
"Classpath Entries" -> classPaths)
}

postApplicationStart方法很简单,只是向listenerBus发送了SparkListenerApplicationStart事件,代码如下。

listenerBus.post(SparkListenerApplicationStart(appName, Some(applicationId),
startTime, sparkUser))

3.13 创建DAGSchedulerSource和BlockManagerSource

  在创建DAGSchedulerSource、BlockManagerSource之前首先调用taskScheduler的postStartHook方法,其目的是为了等待backend就绪,见代码清单3-52。postStartHook的实现见代码清单3-53。

  创建DAGSchedulerSource和BlockManagerSource的过程类似于ExecutorSource,只不过DAGSchedulerSource测量的信息是stage. failedStages、stage. runningStages、stage. waitingStages、stage. allJobs、stage. activeJobs,BlockManagerSource测量的信息是memory. maxMem_MB、memory. remainingMem_MB、memory. memUsed_MB、memory. diskSpaceUsed_MB。

代码清单3-52         创建DAGSchedulerSource和BlockManagerSource

  taskScheduler.postStartHook()

  private val dagSchedulerSource = new DAGSchedulerSource(this.dagScheduler)
private val blockManagerSource = new BlockManagerSource(SparkEnv.get.blockManager) private def initDriverMetrics() {
SparkEnv.get.metricsSystem.registerSource(dagSchedulerSource)
SparkEnv.get.metricsSystem.registerSource(blockManagerSource)
} initDriverMetrics()

代码清单3-53         等待backend就绪的实现

override def postStartHook() {
waitBackendReady()
} private def waitBackendReady(): Unit = {
if (backend.isReady) {
return
} while (!backend.isReady) {
synchronized {
this.wait(100)
}
}
}

3.14 将SparkContext标记为激活

  SparkContext初始化的最后将当前SparkContext的状态从contextBeingConstructed(正在构建中)改为activeContext(已激活),代码如下。

SparkContext.setActiveContext(this, allowMultipleContexts)

setActiveContext方法的实现如下。

  private[spark] def setActiveContext(
sc: SparkContext,
allowMultipleContexts: Boolean): Unit = { SPARK_CONTEXT_CONSTRUCTOR_LOCK.synchronized {
assertNoOtherContextIsRunning(sc, allowMultipleContexts)
contextBeingConstructed = None
activeContext = Some(sc)
}
}

3.15 小结

  回顾本章, Scala与Akka基于Actor的并发编程模型给人带来深刻的印象,改变了我本人每当需要提升性能时就想到使用多线程的传统观念,Actor与事件模型有类似之处,通过异步处理,减少线程切换开销,值得开发人员借鉴。listenerBus对于监听器模式的经典应用将处理转化为事件并交给统一的线程处理,减少了线程阻塞与切换,提升了性能,希望读者朋友能应用到自己的产品开发中去。此外,使用Netty所提供的异步网络框架构建的Block传输服务,基于Jetty构建的内嵌web服务、HTTP文件服务器和SparkUI,基于codahale提供的第三方测量仓库创建的测量系统,Executor中的心跳实现等内容,都值得借鉴。

后记:自己牺牲了7个月的周末和下班空闲时间,通过研究Spark源码和原理,总结整理的《深入理解Spark:核心思想与源码分析》一书现在已经正式出版上市,目前亚马逊、京东、当当、天猫等网站均有销售,欢迎感兴趣的同学购买。我开始研究源码时的Spark版本是1.2.0,经过7个多月的研究和出版社近4个月的流程,Spark自身的版本迭代也很快,如今最新已经是1.6.0。目前市面上另外2本源码研究的Spark书籍的版本分别是0.9.0版本和1.2.0版本,看来这些书的作者都与我一样,遇到了这种问题。由于研究和出版都需要时间,所以不能及时跟上Spark的脚步,还请大家见谅。但是Spark核心部分的变化相对还是很少的,如果对版本不是过于追求,依然可以选择本书。

京东(现有满100减30活动):http://item.jd.com/11846120.html

当当:http://product.dangdang.com/23838168.html

SparkContext的初始化(季篇)——测量系统、ContextCleaner及环境更新的更多相关文章

  1. 《深入理解Spark:核心思想与源码分析》——SparkContext的初始化(叔篇)——TaskScheduler的启动

    <深入理解Spark:核心思想与源码分析>一书前言的内容请看链接<深入理解SPARK:核心思想与源码分析>一书正式出版上市 <深入理解Spark:核心思想与源码分析> ...

  2. SparkContext的初始化(叔篇)——TaskScheduler的启动

    <深入理解Spark:核心思想与源码分析>一书前言的内容请看链接<深入理解SPARK:核心思想与源码分析>一书正式出版上市 <深入理解Spark:核心思想与源码分析> ...

  3. Spark源码剖析 - SparkContext的初始化(一)

    1. SparkContext概述 注意:SparkContext的初始化剖析是基于Spark2.1.0版本的 Spark Driver用于提交用户应用程序,实际可以看作Spark的客户端.了解Spa ...

  4. 《深入理解Spark-核心思想与源码分析》(三)第三章SparkContext的初始化

    3.1 SparkContext概述 SparkConf负责配置参数,主要通过ConcurrentHaspMap来维护各种Spark的配置属性. class SparkConf(loadDefault ...

  5. sparkContext之一:sparkContext的初始化分析

    Spark源码学习:sparkContext的初始化分析 spark可以运行在本地模式local下,可以运行在yarn和standalone模式下,但是本地程序是通过什么渠道和这些集群交互的呢?那就是 ...

  6. Spark源码剖析 - SparkContext的初始化(二)_创建执行环境SparkEnv

    2. 创建执行环境SparkEnv SparkEnv是Spark的执行环境对象,其中包括众多与Executor执行相关的对象.由于在local模式下Driver会创建Executor,local-cl ...

  7. Spark源码剖析 - SparkContext的初始化(十)_Spark环境更新

    12. Spark环境更新 在SparkContext的初始化过程中,可能对其环境造成影响,所以需要更新环境,代码如下: SparkContext初始化过程中,如果设置了spark.jars属性,sp ...

  8. Spark源码剖析 - SparkContext的初始化(三)_创建并初始化Spark UI

    3. 创建并初始化Spark UI 任何系统都需要提供监控功能,用浏览器能访问具有样式及布局并提供丰富监控数据的页面无疑是一种简单.高效的方式.SparkUI就是这样的服务. 在大型分布式系统中,采用 ...

  9. (十三) [终篇] 一起学 Unix 环境高级编程 (APUE) 之 网络 IPC:套接字

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

随机推荐

  1. VUE 入门基础(8)

    十,组件 使用组件 注册 可以通过以下这种方式创建一个Vue实例 new Vue({ el: '#some-element', }) 注册一个全局组件,你可以使用Vue.component(tagNa ...

  2. 用Linux自带的Logrotate来管理日志

    Logrotate是由cron控制,cron在规定的时间执行 " logrotate  /etc/logrotate.conf "命令.将对象日志进行转储,删除,压缩等操作... ...

  3. linux小程序--cmatrix

    wget http://www.asty.org/cmatrix/dist/cmatrix-1.2a.tar.gz .2a.tar.gz cd cmatrix-.2a yum install ncur ...

  4. Spark+Hadoop问题小结

    1.spark执行./start-all.sh报"WARN Utils: Service 'sparkWorker' could not bind on port 0. Attempting ...

  5. pascal闪电入门系列目录

    第一章  准备工作 第二章  Hello,world! 第三章  输出.输入 第四章  变量常量.基本数据类型 第五章  格式.注释 第六章  运算符.表达式.优先级 第七章  分支结构 第八章  数 ...

  6. {HDU}{2516}{取石子游戏}{斐波那契博弈}

    题意:给定一堆石子,每个人最多取前一个人取石子数的2被,最少取一个,最后取石子的为赢家,求赢家. 思路:斐波那契博弈,这个题的证明过程太精彩了! 一个重要的定理:任何正整数都可以表示为若干个不连续的斐 ...

  7. [转]Android音频底层调试-基于tinyalsa

    http://blog.csdn.net/kangear/article/details/38139669 [-] 编译tinyalsa配套工具 查看当前系统的声卡 tinymix查看混响器 使用ti ...

  8. vs2010 打开 vs2012 的解决方案

    vs2010 打开 vs2012 的解决方案   vs2012 出来了,但是MS还是一如既往的向下兼容. 废话不多说,直接主题 要使用vs2010打开vs2012的解决方案必须得改2个东西,解决方案 ...

  9. jdk的内存设置

    堆(Heap)和非堆(Non-heap)内存按照官方的说法:"Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配.堆是在 Java 虚拟机启动时创建的.在JV ...

  10. Direct3D学习笔记 - 浅析HDR Lighting Sample

    一.HDR简介 HDR(High Dynamic Range,高动态范围)是一种图像后处理技术,是一种表达超过了显示器所能表现的亮度范围的图像映射技术.高动态范围技术能够很好地再现现实生活中丰富的亮度 ...