SparkUI的分析与定制

背景

很多情况下,对于现有的SparkUI的功能或是页面不能够完全满足需要,所以在原有的基础上根据实际需求增加自己所需要的页面或功能
对于Spark中的APPStatusStore也可以重新包装以达到自己的需求
Spark版本3.2.2

SparkUI处理流程

Spark UI在SparkContext里面进行初始化
_ui =
if (conf.get(UI_ENABLED)) {
        Some(SparkUI.create(Some(this), _statusStore, _conf, _env.securityManager, appName, "",
                            startTime))
      } else {
        // For tests, do not enable the UI
        None
      }
// Bind the UI before starting the task scheduler to communicate
// the bound port to the cluster manager properly
_ui.foreach(_.bind())

这里有两个步骤
①、SparkUI.create()创建一个SparkUI实例
SparkUI继承WebUI

/**
* Create a new UI backed by an AppStatusStore.
*/
def create(
    sc: Option[SparkContext],
    store: AppStatusStore,
    conf: SparkConf,
    securityManager: SecurityManager,
    appName: String,
    basePath: String,
    startTime: Long,
    appSparkVersion: String = org.apache.spark.SPARK_VERSION): SparkUI = {

    new SparkUI(store, sc, conf, securityManager, appName, basePath, startTime, appSparkVersion)
  }

②、通过_ui.foreach(_.bind())中bind方法和Jetty Server API真实打交道

/** Binds to the HTTP server behind this web interface. */
def bind(): Unit = {
    assert(serverInfo.isEmpty, s"Attempted to bind $className more than once!")
    try {
      val host = Option(conf.getenv("SPARK_LOCAL_IP")).getOrElse("0.0.0.0")
      // 与Jetty Server 绑定
      val server = startJettyServer(host, port, sslOptions, conf, name, poolSize)
      handlers.foreach(server.addHandler(_, securityManager))
      serverInfo = Some(server)
      logInfo(s"Bound $className to $host, and started at $webUrl")
    } catch {
      case e: Exception =>
      logError(s"Failed to bind $className", e)
      System.exit(1)
    }
  }

和传统的Web服务不一样,Spark并没有使用什么页面模板引擎,而是自己定义了一套页面体系。我们把这些对象分成两类:

  1. 框架类,就是维护各个页面关系,和Jetty API有关联,负责管理的相关类。
    ● SparkUI,该类继承子WebUI,中枢类,负责启动jetty,保存页面和URL Path之间的关系等。
    ● WebUI
  2. 页面类,比如页面的Tab,页面渲染的内容等
    ● SparkUITab(继承自WebUITab) ,就是首页的标签栏
    ● WebUIPage,这个是具体的页面。
    SparkUI 负责整个Spark UI构建是,同时它是一切页面的根对象。
    对应的层级结构为: SparkUI -> WebUITab -> WebUIPage

在SparkContext初始化的过程中,SparkUI会启动一个Jetty。而建立起Jetty 和WebUIPage的桥梁是org.apache.spark.ui.WebUI类,该类有个变量如下:
protected val handlers = ArrayBufferServletContextHandler
这个org.eclipse.jetty.servlet.ServletContextHandler是标准的jetty容器的handler,而
protected val pageToHandlers = new HashMap[WebUIPage, ArrayBuffer[ServletContextHandler]]
pageToHandlers 则维护了WebUIPage到ServletContextHandler的对应关系。
这样,我们就得到了WebUIPage 和 Jetty Handler的对应关系了。一个Http请求就能够被对应的WebUIPage给承接。

从 MVC的角度而言,WebUIPage 更像是一个Controller(Action)。内部实现是WebUIPage被包括进了一个匿名的Servlet. 所以实际上Spark 实现了一个对Servlet非常Mini的封装。如果你感兴趣的话,可以到org.apache.spark.ui.JettyUtils 详细看看。

目前spark 支持三种形态的http渲染结果:
● text/json
● text/html
● text/plain
一般而言一个WebUIPage会对应两个Handler

/** Attaches a page to this UI. */
def attachPage(page: WebUIPage): Unit = {
    val pagePath = "/" + page.prefix
    val renderHandler = createServletHandler(pagePath,
                                             (request: HttpServletRequest) => page.render(request), conf, basePath)
    val renderJsonHandler = createServletHandler(pagePath.stripSuffix("/") + "/json",
                                                 (request: HttpServletRequest) => page.renderJson(request), conf, basePath)
    attachHandler(renderHandler)
    attachHandler(renderJsonHandler)
    val handlers = pageToHandlers.getOrElseUpdate(page, ArrayBuffer[ServletContextHandler]())
    handlers += renderHandler
    handlers += renderJsonHandler
  }

在页面路径上,html和json的区别就是html的url path 多加了一个"/json"后缀。 这里可以看到,一般一个page最好实现一下两个方法

另外值得一提的是,上面的代码也展示了URL Path和对应的处理逻辑(Controller/Action)是如何关联起来的。其实就是pagePath -> Page的render函数。

Executors页面分析
以 Executors 显示列表页 为例子,来讲述怎么自定义开发一个Page。
首先你需要定义个Tab,也就是ExecutorsTab,如下:

private[ui] class ExecutorsTab(parent: SparkUI) extends SparkUITab(parent, "executors") {

  init()

  private def init(): Unit = {
    val threadDumpEnabled =
    parent.sc.isDefined && parent.conf.get(UI_THREAD_DUMPS_ENABLED)

    attachPage(new ExecutorsPage(this, threadDumpEnabled))
    if (threadDumpEnabled) {
      attachPage(new ExecutorThreadDumpPage(this, parent.sc))
    }
  }

}

ExecutorsTab会作为一个标签显示在Spark首页上。
接着定义一个ExecutorsPage,作为标签页的呈现内容,并且通过一下代码,关联上 ExecutorsTab 和 ExecutorsPage。

attachPage(new ExecutorsPage(this, threadDumpEnabled))

ExecutorsPage 的定义如下:

private[ui] class ExecutorsPage(
  parent: SparkUITab,
  threadDumpEnabled: Boolean)
extends WebUIPage("") {

    def render(request: HttpServletRequest): Seq[Node] = {
    val content =
    {
        <div id="active-executors"></div> ++
        <script src={UIUtils.prependBaseUri(request, "/static/utils.js")}></script> ++
        <script src={UIUtils.prependBaseUri(request, "/static/executorspage.js")}></script> ++
        <script>setThreadDumpEnabled({threadDumpEnabled})</script>
      }

    UIUtils.headerSparkPage(request, "Executors", content, parent, useDataTables = true)
  }
  }

实现ExecutorsPage.render方法:

def render(request: HttpServletRequest): Seq[Node] = {
    val content =
    {
        <div id="active-executors"></div> ++
        <script src={UIUtils.prependBaseUri(request, "/static/utils.js")}></script> ++
        <script src={UIUtils.prependBaseUri(request, "/static/executorspage.js")}></script> ++
        <script>setThreadDumpEnabled({threadDumpEnabled})</script>
      }

    UIUtils.headerSparkPage(request, "Executors", content, parent, useDataTables = true)
  }

最后一步调用以下方法,输出设置页面头并且输出content页面内容

UIUtils.headerSparkPage(request, "Executors", content, parent, useDataTables = true)

Spark 并没有使用类似Freemarker或者Velocity等模板引擎,而是直接利用了Scala对html/xml的语法支持。类似这样,写起来也蛮爽的。如果想使用变量,使用{}即可。

val execTable =  <table class={UIUtils.TABLE_CLASS_STRIPED}>    
<thead>      
<th>Executor ID</th>     
<th>Address</th>      
<th>RDD Blocks</th>     
<th><span data-toggle="tooltip" title={ToolTips.STORAGE_MEMORY}>Storage Memory</span>
</th>      
<th>Disk Used</th>      
<th>Active Tasks</th>

最终的Tag是在SparkUI的initialize方法里面定义的:

 def initialize(): Unit = {
    val jobsTab = new JobsTab(this, store)
    attachTab(jobsTab)
    val stagesTab = new StagesTab(this, store)
    attachTab(stagesTab)
    attachTab(new StorageTab(this, store))
    attachTab(new EnvironmentTab(this, store))
    attachTab(new ExecutorsTab(this))
    addStaticHandler(SparkUI.STATIC_RESOURCE_DIR)
    attachHandler(createRedirectHandler("/", "/jobs/", basePath = basePath))
    attachHandler(ApiRootResource.getServletHandler(this))
    if (sc.map(_.conf.get(UI_PROMETHEUS_ENABLED)).getOrElse(false)) {
      attachHandler(PrometheusResource.getServletHandler(this))
    }

    // These should be POST only, but, the YARN AM proxy won't proxy POSTs
    attachHandler(createRedirectHandler(
      "/jobs/job/kill", "/jobs/", jobsTab.handleKillRequest, httpMethods = Set("GET", "POST")))
    attachHandler(createRedirectHandler(
      "/stages/stage/kill", "/stages/", stagesTab.handleKillRequest,
      httpMethods = Set("GET", "POST")))
  }

新增的页面添加的话,通过sparkContext获取到SparkUI对象,然后调用attachTab方法即可完成。具体如下:

sc.ui.getOrElse {  throw new SparkException("Parent SparkUI to attach this tab to not found!")}
.attachTab(new ExecutorsTab) 

如果是在spark-streaming里,则简单通过如下代码就能把你的页面页面添加进去:

ssc.start()
new KKTab(ssc).attach()
ssc.awaitTermination()

添加新的Tab可能会报错,scala报的错误比较让人困惑,可以试试加入下面依赖:

<dependency>    
<groupId>org.eclipse.jetty</groupId>    
<artifactId>jetty-servlet</artifactId>    <version>9.3.6.v20151106</version>
</dependency>

实践

需要一个Tab页,以及一个展示Tab对应内容的Page页。其实就下面两个类。

package org.apache.spark.streaming.ui2

import org.apache.spark.streaming.StreamingContext
import org.apache.spark.streaming.ui2.KKTab._
import org.apache.spark.ui.{SparkUI, SparkUITab}
import org.apache.spark.{Logging, SparkException}


class KKTab(val ssc: StreamingContext)
  extends SparkUITab(getSparkUI(ssc), "streaming2") with Logging {
  private val STATIC_RESOURCE_DIR = "org/apache/spark/streaming/ui/static"
  attachPage(new TTPage(this))

  def attach() {
    getSparkUI(ssc).attachTab(this)
    getSparkUI(ssc).addStaticHandler(STATIC_RESOURCE_DIR, "/static/streaming")
  }

  def detach() {
    getSparkUI(ssc).detachTab(this)
    getSparkUI(ssc).removeStaticHandler("/static/streaming")
  }
}

private[spark] object KKTab {
  def getSparkUI(ssc: StreamingContext): SparkUI = {
    ssc.sc.ui.getOrElse {
      throw new SparkException("Parent SparkUI to attach this tab to not found!")
    }
  }
}

import org.apache.spark.Logging
import org.apache.spark.ui.{UIUtils => SparkUIUtils, WebUIPage}
import org.json4s.JsonAST.{JNothing, JValue}

import scala.xml.Node

private[spark] class TTPage(parent: KKTab)
  extends WebUIPage("") with Logging {

  override def render(request: HttpServletRequest): Seq[Node] = {
    val content = <p>TTPAGE</p>
    SparkUIUtils.headerSparkPage("TT", content, parent, Some(5000))
  }
  override def renderJson(request: HttpServletRequest): JValue = JNothing
}


引用:sparkui

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值