Spark UI (基於Yarn) 分析與定製

firefule發表於2021-09-09

這篇文章的主旨在於讓你瞭解Spark UI體系,並且能夠讓你有能力對UI進行一些定製化增強。在分析過程中,你也會深深的感受到Scala語言的魅力。

前言

有時候我們希望能對Spark UI進行一些定製化增強。並且我們希望儘可能不更改Spark的原始碼。為了達到此目標,我們會從如下三個方面進行闡述:

  1. 理解Spark UI的處理流程

  2. 現有Executors頁面分析

  3. 自己編寫一個HelloWord頁面

Spark UI 處理流程

Spark UI 在SparkContext 物件中進行初始化,對應的程式碼:

_ui =  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 
 }// Bind the UI before starting the task scheduler to communicate// the bound port to the cluster manager properly_ui.foreach(_.bind())

這裡做了兩個動作,

  1. 透過SparkUI.createLiveUI 建立一個SparkUI例項 _ui

  2. 透過 _ui.foreach(_.bind())啟動jetty。bind 方法是繼承自WebUI,該類負責和真實的Jetty Server API打交道。

和傳統的Web服務不一樣,Spark並沒有使用什麼頁面模板引擎,而是自己定義了一套頁面體系。我們把這些物件分成兩類:

  1. 框架類,就是維護各個頁面關係,和Jetty API有關聯,負責管理的相關類。

  2. 頁面類,比如頁面的Tab,頁面渲染的內容等

框架類有:

  • SparkUI,該類繼承子WebUI,中樞類,負責啟動jetty,儲存頁面和URL Path之間的關係等。

  • WebUI

頁面類:

  • SparkUITab(繼承自WebUITab) ,就是首頁的標籤欄

  • WebUIPage,這個是具體的頁面。

SparkUI 負責整個Spark UI構建是,同時它是一切頁面的根物件。

對應的層級結構為:

 SparkUI -> WebUITab ->  WebUIPage

在SparkContext初始化的過程中,SparkUI會啟動一個Jetty。而建立起Jetty 和WebUIPage的橋樑是org.apache.spark.ui.WebUI類,該類有個變數如下:

protected val handlers = ArrayBuffer[ServletContextHandler]()

這個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,

val renderHandler = createServletHandler(
 pagePath, 
 (request: HttpServletRequest) => page.render(request), 
securityManager,
 basePath)

val renderJsonHandler = createServletHandler(pagePath.stripSuffix("/") + "/json",  (request: HttpServletRequest) => page.renderJson(request), securityManager, basePath)

在頁面路徑上,html和json的區別就是html的url path 多加了一個"/json"字尾。 這裡可以看到,一般一個page最好實現

  • render

  • renderJson

兩個方法,以方便使用。

另外值得一提的是,上面的程式碼也展示了URL Path和對應的處理邏輯(Controller/Action)是如何關聯起來的。其實就是pagePath -> Page的render函式。

Executors頁面分析

我們以 Executors 顯示列表頁 為例子,來講述怎麼自定義開發一個Page。

首先你需要定義個Tab,也就是ExecutorsTab,如下:

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

ExecutorsTab會作為一個標籤顯示在Spark首頁上。
接著定義一個ExecutorsPage,作為標籤頁的呈現內容,並且透過

attachPage(new ExecutorsPage(this, threadDumpEnabled))

關聯上 ExecutorsTab 和  ExecutorsPage。

ExecutorsPage 的定義如下:

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

實現ExecutorsPage.render方法:

 def render(request: HttpServletRequest): Seq[Node]

最後一步呼叫

 SparkUIUtils.headerSparkPage("Executors (" + execInfo.size + ")",     content, parent)

輸出設定頁面頭並且輸出content頁面內容。

這裡比較有意思的是,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() {  
attachTab(new JobsTab(this))  attachTab(stagesTab)  
attachTab(new StorageTab(this))  
attachTab(new EnvironmentTab(this))  
attachTab(new ExecutorsTab(this))

那我們新增的該怎麼辦?其實也很簡單啦,透過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>

實現新增一個HelloWord頁面

我們的例子很簡單,類似下面的圖:

圖片描述

無標題.png

按前文的描述,我們需要一個Tab頁,以及一個展示Tab對應內容的Page頁。其實就下面兩個類。

org.apache.spark.streaming.ui2.KKTab:

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}/**
 * 1/1/16 WilliamZhu(allwefantasy@gmail.com)
 */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!")
    }
  }
}

org.apache.spark.streaming.ui2.TTPage 如下:

import org.apache.spark.Loggingimport org.apache.spark.ui.{UIUtils => SparkUIUtils, WebUIPage}import org.json4s.JsonAST.{JNothing, JValue}import scala.xml.Node/**
 * 1/1/16 WilliamZhu(allwefantasy@gmail.com)
 */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
}

記得新增上面提到的jetty依賴。



作者:祝威廉
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/1834/viewspace-2818598/,如需轉載,請註明出處,否則將追究法律責任。

相關文章