Spark UI界面原理
  当Spark程序在运行时,会提供一个Web页面查看Application运行状态信息。是否开启UI界面由参数spark.ui.enabled(默认为true)来确定。下面列出Spark UI一些相关配置参数,默认值,以及其作用。
| 参数 | 默认值 | 作用描述 | 
|---|---|---|
| spark.ui.enabled | true | 是否开启UI界面 | 
| spark.ui.port | 4040(顺序探查空闲端口) | UI界面的访问端口号 | 
| spark.ui.retainedJobs | 1000 | UI界面显示的Job个数 | 
| spark.ui.retailedStages | 1000 | UI界面上显示的Stage个数 | 
| spark.ui.timeline.tasks.maximum | 1000 | Stage页面显示的Tasks个数 | 
| spark.ui.killEnabled | true | 是否运行页面上kill任务 | 
| spark.ui.threadDumpsEnabled | true | Executors页面是否可以展示线程运行状况 | 
本文接下来分成两个部分,第一部分基于Spark-1.6.0的源码,结合第二部分的图片内容来描述UI界面在Spark中的实现方式。第二部分以实例展示Spark UI界面显示的内容。
一、Spark UI界面实现方式
1、UI组件结构
  这部分先讲UI界面的实现方式,UI界面的实例在本文最后一部分。如果对这部分中的某些概念不清楚,那么最好先把第二部分了解一下。 
  从下面UI界面的实例可以看出,不同的内容以Tab的形式展现在界面上,对应每一个Tab在下方显示具体内容。基本上Spark UI界面也是按这个层次关系实现的。 
  以SparkUI类为容器,各个Tab,如JobsTab, StagesTab, ExecutorsTab等镶嵌在SparkUI上,对应各个Tab,有页面内容实现类JobPage, StagePage, ExecutorsPage等页面。这些类的继承和包含关系如下图所示: 
  
2、初始化过程
从上面可以看出,SparkUI类型的对象是UI界面的根对象,它是在SparkContext类中构造出来的。
private var _ui: Option[SparkUI] = None //定义
_ui = //SparkUI对象的生成
  if (conf.getBoolean("spark.ui.enabled", true)) {
    Some(SparkUI.createLiveUI(this, _conf, listenerBus, _jobProgressListener,
      _env.securityManager, appName, startTime = startTime))
  } else {
    // For tests, do not enable the UI
    None
  }
_ui.foreach(_.bind())  //启动jetty。bind方法继承自WebUI,该类负责和真实的Jetty Server API打交道上面这段代码中可以看到SparkUI对象的生成过程,结合上面的类结构图,可以看到bind方法继承自WebUI类,进入WebUI类中,
  protected val handlers = ArrayBuffer[ServletContextHandler]() // 这个对象在下面bind方法中会使用到。
  protected val pageToHandlers = new HashMap[WebUIPage, ArrayBuffer[ServletContextHandler]] // 将page绑定到handlers上
  /** 将Http Server绑定到这个Web页面 */
  def bind() {
    assert(!serverInfo.isDefined, "Attempted to bind %s more than once!".format(className))
    try {
      serverInfo = Some(startJettyServer("0.0.0.0", port, handlers, conf, name))
      logInfo("Started %s at http://%s:%d".format(className, publicHostName, boundPort))
    } catch {
      case e: Exception =>
        logError("Failed to bind %s".format(className), e)
        System.exit(1)
    }
  }上面代码中handlers对象维持了WebUIPage和Jetty之间的关系,org.eclipse.jetty.servlet.ServletContextHandler是标准jetty容器的handler。而对象pageToHandlers维持了WebUIPage到ServletContextHandler的对应关系。
各Tab页以及该页内容的实现,基本上大同小异。接下来以AllJobsPage页面为例仔细梳理页面展示的过程。
3、SparkUI中Tab的绑定
  从上面的类结构图中看到WebUIPage提供了两个重要的方法,render和renderJson用于相应页面请求,在WebUIPage的实现类中,具体实现了这两个方法。 
  在SparkContext中构造出SparkUI的实例后,会执行SparkUI#initialize方法进行初始化。如下面代码中,调用SparkUI从WebUI继承的attacheTab方法,将各Tab页面绑定到UI上。
  def initialize() {
    attachTab(new JobsTab(this))
    attachTab(stagesTab)
    attachTab(new StorageTab(this))
    attachTab(new EnvironmentTab(this))
    attachTab(new ExecutorsTab(this))
    attachHandler(createStaticHandler(SparkUI.STATIC_RESOURCE_DIR, "/static"))
    attachHandler(createRedirectHandler("/", "/jobs/", basePath = basePath))
    attachHandler(ApiRootResource.getServletHandler(this))
    // This should be POST only, but, the YARN AM proxy won't proxy POSTs
    attachHandler(createRedirectHandler(
      "/stages/stage/kill", "/stages/", stagesTab.handleKillRequest,
      httpMethods = Set("GET", "POST")))
  }4、页面内容绑定到Tab
在上一节中,JobsTab标签绑定到SparkUI上之后,在JobsTab上绑定了AllJobsPage和JobPage类。AllJobsPage页面即访问SparkUI页面时列举出所有Job的那个页面,JobPage页面则是点击单个Job时跳转的页面。通过调用JobsTab从WebUITab继承的attachPage方法与JobsTab进行绑定。
private[ui] class JobsTab(parent: SparkUI) extends SparkUITab(parent, "jobs") {
  val sc = parent.sc
  val killEnabled = parent.killEnabled
  val jobProgresslistener = parent.jobProgressListener
  val executorListener = parent.executorsListener
  val operationGraphListener = parent.operationGraphListener
  def isFairScheduler: Boolean =
    jobProgresslistener.schedulingMode.exists(_ == SchedulingMode.FAIR)
  attachPage(new AllJobsPage(this))
  attachPage(new JobPage(this))
}5、页面内容的展示
知道了AllJobsPage页面如何绑定到SparkUI界面后,接下来分析这个页面的内容是如何显示的。进入AllJobsPage类,主要观察render方法。在页面展示上Spark直接利用了Scala对html/xml的语法支持,将页面的Html代码嵌入Scala程序中。具体的页面生成过程可以查看下面源码中的注释。这里可以结合第二部分的实例进行查看。
  def render(request: HttpServletRequest): Seq[Node] = {
    val listener = parent.jobProgresslistener //获取jobProgresslistener对象,页面展示的数据都是从这里读取
    listener.synchronized {
      val startTime = listener.startTime // 获取application的开始时间,默认值为-1L
      val endTime = listener.endTime // 获取application的结束时间,默认值为-1L
      val activeJobs = listener.activeJobs.values.toSeq // 获取当前application中处于active状态的job
      val completedJobs = listener.completedJobs.reverse.toSeq // 获取当前application中完成状态的job
      val failedJobs = listener.failedJobs.reverse.toSeq  // 获取当前application中失败状态的job
      val activeJobsTable =
        jobsTable(activeJobs.sortBy(_.submissionTime.getOrElse(-1L)).reverse)
      val completedJobsTable =
        jobsTable(completedJobs.sortBy(_.completionTime.getOrElse(-1L)).reverse)
      val failedJobsTable =
        jobsTable(failedJobs.sortBy(_.completionTime.getOrElse(-1L)).reverse)
      val shouldShowActiveJobs = activeJobs.nonEmpty
      val shouldShowCompletedJobs = completedJobs.nonEmpty
      val shouldShowFailedJobs = failedJobs.nonEmpty
      val completedJobNumStr = if (completedJobs.size == listener.numCompletedJobs) {
        s"${completedJobs.size}"
      } else {
        s"${listener.numCompletedJobs}, only showing ${completedJobs.size}"
      }
      val summary: NodeSeq =
        <div>
          <ul class="unstyled">
            <li>
              <strong>Total Uptime:</strong> // 显示当前Spark应用运行时间
              {// 如果还没有结束,就用系统当前时间减开始时间。如果已经结束,就用结束时间减开始时间
                if (endTime < 0 && parent.sc.isDefined) {
                  UIUtils.formatDuration(System.currentTimeMillis() - startTime)
                } else if (endTime > 0) {
                  UIUtils.formatDuration(endTime - startTime)
                }
              }
            </li>
            <li>
              <strong>Scheduling Mode: </strong> // 显示调度模式,FIFO或FAIR
              {listener.schedulingMode.map(_.toString).getOrElse("Unknown")}
            </li>
            {
              if (shouldShowActiveJobs) { // 如果有active状态的job,则显示Active Jobs有多少个
                <li>
                  <a href="#active"><strong>Active Jobs:</strong></a>
                  {activeJobs.size}
                </li>
              }
            }
            {
              if (shouldShowCompletedJobs) { // 如果有完成状态的job,则显示Completed Jobs的个数
                <li id="completed-summary">
                  <a href="#completed"><strong>Completed Jobs:</strong></a>
                  {completedJobNumStr}
                </li>
              }
            }
            {
              if (shouldShowFailedJobs) { // 如果有失败状态的job,则显示Failed Jobs的个数
                <li>
                  <a href="#failed"><strong>Failed Jobs:</strong></a>
                  {listener.numFailedJobs}
                </li>
              }
            }
          </ul>
        </div>
      var content = summary // 将上面的html代码写入content变量,在最后统一显示content中的内容
      val executorListener = parent.executorListener // 这里获取EventTimeline中的信息
      content ++= makeTimeline(activeJobs ++ completedJobs ++ failedJobs,
          executorListener.executorIdToData, startTime)
// 然后根据当前application中是否存在active, failed, completed状态的job,将这些信息显示在页面上。
      if (shouldShowActiveJobs) {
        content ++= <h4 id="active">Active Jobs ({activeJobs.size})</h4> ++
          activeJobsTable // 生成active状态job的展示表格,具体形式可参看第二部分。按提交时间倒序排列
      }
      if (shouldShowCompletedJobs) {
        content ++= <h4 id="completed">Completed Jobs ({completedJobNumStr})</h4> ++
          completedJobsTable
      }
      if (shouldShowFailedJobs) {
        content ++= <h4 id ="failed">Failed Jobs ({failedJobs.size})</h4> ++
          failedJobsTable
      }
      val helpText = """A job is triggered by an action, like count() or saveAsTextFile().""" +
        " Click on a job to see information about the stages of tasks inside it."
      UIUtils.headerSparkPage("Spark Jobs", content, parent, helpText = Some(helpText)) // 最后将content中的所有内容全部展示在页面上
    }
  }接下来以activeJobsTable代码为例分析Jobs信息展示表格的生成。这里主要的方法是makeRow,接收的是上面代码中的activeJobs, completedJobs, failedJobs。这三个对象都是包含在JobProgressListener对象中的,在JobProgressListener中的定义如下:
// 这三个对象用于存储数据的主要是JobUIData类型,
  val activeJobs = new HashMap[JobId, JobUIData]
  val completedJobs = ListBuffer[JobUIData]()
  val failedJobs = ListBuffer[JobUIData]()将上面三个对象传入到下面这段代码中,继续执行。
  private def jobsTable(jobs: Seq[JobUIData]): Seq[Node] = {
    val someJobHasJobGroup = jobs.exists(_.jobGroup.isDefined)
    val columns: Seq[Node] = { // 显示的信息包括,Job Id(Job Group)以及Job描述,Job提交时间,Job运行时间,总的Stage/Task数,成功的Stage/Task数,以及一个进度条
      <th>{if (someJobHasJobGroup) "Job Id (Job Group)" else "Job Id"}</th>
      <th>Description</th>
      <th>Submitted</th>
      <th>Duration</th>
      <th class="sorttable_nosort">Stages: Succeeded/Total</th>
      <th class="sorttable_nosort">Tasks (for all stages): Succeeded/Total</th>
    }
    def makeRow(job: JobUIData): Seq[Node] = {
      val (lastStageName, lastStageDescription) = getLastStageNameAndDescription(job)
      val duration: Option[Long] = {
        job.submissionTime.map { start => // Job运行时长为系统时间,或者结束时间减去开始时间
          val end = job.completionTime.getOrElse(System.currentTimeMillis())
          end - start
        }
      }
      val formattedDuration = duration.map(d =>  // 格式化任务运行时间,显示为a h:b m:c s格式UIUtils.formatDuration(d)).getOrElse("Unknown")
      val formattedSubmissionTime = // 获取Job提交时间job.submissionTime.map(UIUtils.formatDate).getOrElse("Unknown")
      val jobDescription = UIUtils.makeDescription(lastStageDescription, parent.basePath) // 获取任务描述
      val detailUrl = // 点击单个Job下面链接跳转到JobPage页面,传入参数为jobId
        "%s/jobs/job?id=%s".format(UIUtils.prependBaseUri(parent.basePath), job.jobId)
      <tr id={"job-" + job.jobId}>
        <td sorttable_customkey={job.jobId.toString}>
          {job.jobId} {job.jobGroup.map(id => s"($id)").getOrElse("")}
        </td>
        <td>
          {jobDescription}
          <a href={detailUrl} class="name-link">{lastStageName}</a>
        </td>
        <td sorttable_customkey={job.submissionTime.getOrElse(-1).toString}>
          {formattedSubmissionTime}
        </td>
        <td sorttable_customkey={duration.getOrElse(-1).toString}>{formattedDuration}</td>
        <td class="stage-progress-cell">
          {job.completedStageIndices.size}/{job.stageIds.size - job.numSkippedStages}
          {if (job.numFailedStages > 0) s"(${job.numFailedStages} failed)"}
          {if (job.numSkippedStages > 0) s"(${job.numSkippedStages} skipped)"}
        </td>
        <td class="progress-cell"> // 进度条
          {UIUtils.makeProgressBar(started = job.numActiveTasks, completed = job.numCompletedTasks,
           failed = job.numFailedTasks, skipped = job.numSkippedTasks,
           total = job.numTasks - job.numSkippedTasks)}
        </td>
      </tr>
    }
    <table class="table table-bordered table-striped table-condensed sortable">
      <thead>{columns}</thead> // 显示列名
      <tbody>
        {jobs.map(makeRow)} // 调用上面的row生成方法,具体显示Job信息
      </tbody>
    </table>
  }  从上面这些代码中可以看到,Job页面显示的所有数据,都是从JobProgressListener对象中获得的。SparkUI可以理解成一个JobProgressListener对象的消费者,页面上显示的内容都是JobProgressListener内在的展现。 
  在接下来一篇文章Spark-1.6.0之Application运行信息记录器JobProgressListener中会分析运行状态数据是如何写入JobProgressListener中的。 
  
二、Spark UI界面实例
  默认情况下,当一个Spark Application运行起来后,可以通过访问hostname:4040端口来访问UI界面。hostname是提交任务的Spark客户端ip地址,端口号由参数spark.ui.port(默认值4040,如果被占用则顺序往后探查)来确定。由于启动一个Application就会生成一个对应的UI界面,所以如果启动时默认的4040端口号被占用,则尝试4041端口,如果还是被占用则尝试4042,一直找到一个可用端口号为止。 
  下面启动一个Spark ThriftServer服务,并用beeline命令连接该服务,提交sql语句运行。则ThriftServer对应一个Application,每个sql语句对应一个Job,按照Job的逻辑划分Stage和Task。
1、Jobs页面
 
  连接上该端口后,显示的就是上面的页面,也是Job的主页面。这里会显示所有Active,Completed, Cancled以及Failed状态的Job。默认情况下总共显示1000条Job信息,这个数值由参数spark.ui.retainedJobs(默认值1000)来确定。 
  从上面还看到,除了Jobs选项卡之外,还可显示Stages, Storage, Enviroment, Executors, SQL以及JDBC/ODBC Server选项卡。分别如下图所示。
2、Stages页面
3、Storage页面
4、Enviroment页面
5、Executors页面
6、单个Job包含的Stages页面
7、Task页面
 
  
Spark UI界面原理的更多相关文章
- [看图说话] 基于Spark UI性能优化与调试——初级篇
		Spark有几种部署的模式,单机版.集群版等等,平时单机版在数据量不大的时候可以跟传统的java程序一样进行断电调试.但是在集群上调试就比较麻烦了...远程断点不太方便,只能通过Log的形式,进行分析 ... 
- Spark(四十七):Spark UI 数据可视化
		导入: 1)Spark Web UI主要依赖于流行的Servlet容器Jetty实现: 2)Spark Web UI(Spark2.3之前)是展示运行状况.资源状态和监控指标的前端,而这些数据都是由度 ... 
- 使用AsyncTask异步更新UI界面及原理分析
		概述: AsyncTask是在Android SDK 1.5之后推出的一个方便编写后台线程与UI线程交互的辅助类.AsyncTask的内部实现是一个线程池,所有提交的异步任务都会在这个线程池中的工作线 ... 
- spark on yarn UI界面详解
		参考: spark on yarn图形化任务监控利器:History-server帮你理解spark的任务执行过程 spark内存分配原理 yarn运行原理详解 task,executor,core等 ... 
- Android异步处理一:使用Thread+Handler实现非UI线程更新UI界面
		Android应用的开发过程中需要把繁重的任务(IO,网络连接等)放到其他线程中异步执行,达到不阻塞UI的效果. 下面将由浅入深介绍Android进行异步处理的实现方法和系统底层的实现原理. 本文介绍 ... 
- Flash Stage3D  在2D UI 界面上显示3D模型问题完美解决
		一直以来很多Stage3D开发者都在为3D模型在2DUI上显示的问题头疼.Stage3D一直是在 Stage2D下面.为了做到3D模型在2DUI上显示通常大家有几种实现方式,下面来说说这几种实现方式吧 ... 
- Spark源码剖析 - SparkContext的初始化(三)_创建并初始化Spark UI
		3. 创建并初始化Spark UI 任何系统都需要提供监控功能,用浏览器能访问具有样式及布局并提供丰富监控数据的页面无疑是一种简单.高效的方式.SparkUI就是这样的服务. 在大型分布式系统中,采用 ... 
- Android异步处理系列文章四篇之二 使用AsyncTask异步更新UI界面
		Android异步处理一:使用Thread+Handler实现非UI线程更新UI界面Android异步处理二:使用AsyncTask异步更新UI界面Android异步处理三:Handler+Loope ... 
- Android异步处理系列文章四篇之一使用Thread+Handler实现非UI线程更新UI界面
		目录: Android异步处理一:使用Thread+Handler实现非UI线程更新UI界面Android异步处理二:使用AsyncTask异步更新UI界面Android异步处理三:Handler+L ... 
随机推荐
- LOB对象在数据泵导出、导入后查询对象数量发现丢失
			问题描述:问题:源库的某个Schema使用数据泵Expdp元数据整体导出,在目标库导入且成功后,逻辑验证用户对象,发现缺失.分析查询后,缺失的对象,都是LOB类型(并不是所有的LOB都无法导入,是大部 ... 
- 用js来实现那些数据结构10(集合02-集合的操作)
			前一篇文章我们一起实现了自定义的set集合类.那么这一篇我们来给set类增加一些操作方法.那么在开始之前,还是有必要解释一下集合的操作有哪些.便于我们更快速的理解代码. 1.并集:对于给定的两个集合, ... 
- bzoj 1558: [JSOI2009]等差数列
			Description Solution 把原数组变为差分数组,然后剩下的就十分显然了 区间查询用线段树维护 修改操作就是区间加法和两个单点修改 一个等差数列实际上就是 开头一个数字+数值相等的一段 ... 
- [Codeforces]848C - Goodbye Souvenir
			题目大意:n个数字,m次操作,支持修改一个数字和查询一个区间内每种数字最大出现位置减最小出现位置的和.(n,m<=100,000) 做法:把每个数字表示成二维平面上的点,第一维是在数组中的位置, ... 
- PKUWC 2018 滚粗记
			day0 上午居然考了一场考试,大爆炸,攒了一波RP,下午也没有心思去落实题目,而是一心去搞颓废,到了晚上看时间还早,于是就看了一波上午考试的Solution,懵逼.jpg day1 上午考数学,前一 ... 
- 2015 多校联赛 ——HDU5323(搜索)
			Solve this interesting problem Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 32768/32768 K ... 
- solr6.6初探之查询篇
			关于搜索与查询,首先我们来看一张图: 这张图说明了solr查询原理: 1.当通过solr发起查询的时候,引擎会选择一个RequestHandler(从字面意思上来说就是请求处理器)来进行查询处理 2. ... 
- C++函数的重载
			两个以上的函数,具有相同的函数名,但是形参的个数或者类型不同,编译器根据实参与形参的类型以及个数的最佳匹配,自动确定调用的函数,这就是函数的重载. 两个名字相同的函数必须具有不同的形参,这里的不同指的 ... 
- IDE、SDK、API
			IDE 集成开发环境(IDE,Integrated Development Environment )是用于提供程序开发环境的应用程序,一般包括代码编辑器.编译器.调试器和图形用户界面等工具.集成了代 ... 
- MongoDb进阶实践之六 MongoDB查询命令详述(补充)
			一.引言 上一篇文章我们已经介绍了MongoDB数据库的查询操作,但是并没有介绍全,随着自己的学习的深入,对查询又有了新的东西,决定补充进来.如果大家想看上一篇有关MongoDB查询的 ... 
