Spark Core源碼精讀計(jì)劃#14:Spark Web UI界面的實(shí)現(xiàn)

目錄

前言

我們已經(jīng)在SparkEnv的世界里摸爬滾打了很長時間,對RPC環(huán)境、廣播變量、序列化和壓縮、度量系統(tǒng)這幾個相對獨(dú)立的組件有了一定的了解?,F(xiàn)在是時候抽身出來,繼續(xù)move forward,跟著SparkContext初始化的流程走下去。按照順序,本文要講的是Spark Web UI。正好,上一篇文章剛剛講過度量系統(tǒng),本文可以說是水到渠成了。

Spark Web UI主要依賴于流行的Servlet容器Jetty實(shí)現(xiàn),本文為避免跑題,在涉及Jetty相關(guān)細(xì)節(jié)的時候都不會詳細(xì)地展開。

創(chuàng)建SparkUI

由于距離講SparkContext的初始化已經(jīng)過去許久了,因此先看看SparkUI在SparkContext的創(chuàng)建流程。

SparkContext中的操作

代碼#14.1 - SparkContext中創(chuàng)建SparkUI的代碼

    _ui =
      if (conf.getBoolean("spark.ui.enabled", true)) {
        Some(SparkUI.create(Some(this), _statusStore, _conf, _env.securityManager, appName, "",
          startTime))
      } else {
        None
      }
    _ui.foreach(_.bind())

其中,_statusStore是早先初始化的AppStatusStore,它是包裝過的KVStore和AppStatusListener,前者用于存儲監(jiān)控?cái)?shù)據(jù),后者注冊到事件總線中的appStatus隊(duì)列中。_env.securityManager則是SparkEnv中初始化的安全管理器。

SparkContext通過調(diào)用SparkUI伴生對象中的create()方法來直接new出SparkUI實(shí)例,然后調(diào)用bind()方法將SparkUI綁定到Jetty服務(wù)。bind()方法之后再說,現(xiàn)在先來看SparkUI類的事情。

初始化SparkUI

以下是SparkUI類中的屬性成員,以及構(gòu)造方法。

代碼#14.2 - o.a.s.ui.SparkUI類中的成員屬性和initialize()方法

private[spark] class SparkUI private (
    val store: AppStatusStore,
    val sc: Option[SparkContext],
    val conf: SparkConf,
    securityManager: SecurityManager,
    var appName: String,
    val basePath: String,
    val startTime: Long,
    val appSparkVersion: String)
  extends WebUI(securityManager, securityManager.getSSLOptions("ui"), SparkUI.getUIPort(conf),
    conf, basePath, "SparkUI")
  with Logging with UIRoot {
  val killEnabled = sc.map(_.conf.getBoolean("spark.ui.killEnabled", true)).getOrElse(false)
  var appId: String = _
  private var streamingJobProgressListener: Option[SparkListener] = None

  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))
    attachHandler(createStaticHandler(SparkUI.STATIC_RESOURCE_DIR, "/static"))
    attachHandler(createRedirectHandler("/", "/jobs/", basePath = basePath))
    attachHandler(ApiRootResource.getServletHandler(this))

    attachHandler(createRedirectHandler(
      "/jobs/job/kill", "/jobs/", jobsTab.handleKillRequest, httpMethods = Set("GET", "POST")))
    attachHandler(createRedirectHandler(
      "/stages/stage/kill", "/stages/", stagesTab.handleKillRequest,
      httpMethods = Set("GET", "POST")))
  }

  initialize()
}

SparkUI類中有3個屬性成員:

  • killEnabled由配置項(xiàng)spark.ui.killEnabled控制,如果為true,會在UI界面中展示強(qiáng)行殺掉Spark Job的開關(guān)。
  • appId就是當(dāng)前的Application ID。
  • streamingJobProgressListener是用于Spark Streaming作業(yè)進(jìn)度的監(jiān)聽器。

在initialize()方法中,首先創(chuàng)建了5個Tab,并調(diào)用了attachTab()方法注冊到Web UI。所謂Tab就是Spark UI中的標(biāo)簽頁,如下圖中最上面的一欄所示,名稱也是一一對應(yīng)的。

圖#14.1 - Spark Web UI頁面

接下來,調(diào)用createStaticHandler()方法創(chuàng)建靜態(tài)資源的ServletContextHandler,又調(diào)用createRedirectHandler()創(chuàng)建一些重定向的ServletContextHandler?!静逡痪洌篠ervletContextHandler是Jetty中一個功能完善的處理器,負(fù)責(zé)接收并處理HTTP請求,再投遞給Servlet?!孔詈?,逐一調(diào)用attachHandler()方法注冊到Web UI。

那么上面的這一系列方法(也包含上一節(jié)的bind()方法)是哪兒來的呢?答案是WebUI抽象類,也就是SparkUI的基類。下面來閱讀它的源碼。

WebUI的具體實(shí)現(xiàn)

WebUI是Spark里所有可以在瀏覽器中展示的內(nèi)容的頂級組件,因此SparkUI類也會繼承它。

屬性成員和Getter方法

代碼#14.3 - o.a.s.ui.WebUI類的屬性成員和Getter方法

  protected val tabs = ArrayBuffer[WebUITab]()
  protected val handlers = ArrayBuffer[ServletContextHandler]()
  protected val pageToHandlers = new HashMap[WebUIPage, ArrayBuffer[ServletContextHandler]]
  protected var serverInfo: Option[ServerInfo] = None
  protected val publicHostName = Option(conf.getenv("SPARK_PUBLIC_DNS")).getOrElse(
    conf.get(DRIVER_HOST_ADDRESS))
  private val className = Utils.getFormattedClassName(this)

  def getBasePath: String = basePath
  def getTabs: Seq[WebUITab] = tabs
  def getHandlers: Seq[ServletContextHandler] = handlers
  def getSecurityManager: SecurityManager = securityManager

屬性成員有以下6個。

  • tabs:持有WebUITab(即圖#14.1中的標(biāo)簽頁)的緩存。
  • handlers:持有Jetty ServletContextHandler的緩存。
  • pageToHandlers:保存WebUIPage(WebUITab的下一級組件)與其對應(yīng)的ServletContextHandler的映射關(guān)系。
  • serverInfo:當(dāng)前Web UI對應(yīng)的Jetty服務(wù)器信息。
  • publicHostName:當(dāng)前Web UI對應(yīng)的Jetty服務(wù)主機(jī)名。先通過系統(tǒng)環(huán)境變量SPARK_PUBLIC_DNS獲取,再通過spark.driver.host配置項(xiàng)獲取。
  • className:當(dāng)前類的名稱,用Utils.getFormattedClassName()方法格式化過。

Getter方法有4個,getTabs()和getHandlers()都是簡單地獲得對應(yīng)屬性的值。getBasePath()取得構(gòu)造參數(shù)中定義的Web UI基路徑,getSecurityManager()則取得構(gòu)造參數(shù)中傳入的安全管理器。

WebUI提供的attach/detach類方法

這類方法都是成對的,一共有3對:attachTab()/detachTab(),用于注冊和移除WebUITab;attachPage()/detachPage(),用于注冊和移除WebUIPage;attachHandler()/detachHandler(),用于注冊和移除ServletContextHandler。以下是它們的代碼。

代碼#14.4 - WebUI提供的attach/detach類方法

  def attachTab(tab: WebUITab) {
    tab.pages.foreach(attachPage)
    tabs += tab
  }

  def detachTab(tab: WebUITab) {
    tab.pages.foreach(detachPage)
    tabs -= tab
  }

  def detachPage(page: WebUIPage) {
    pageToHandlers.remove(page).foreach(_.foreach(detachHandler))
  }

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

  def attachHandler(handler: ServletContextHandler) {
    handlers += handler
    serverInfo.foreach(_.addHandler(handler))
  }

  def detachHandler(handler: ServletContextHandler) {
    handlers -= handler
    serverInfo.foreach(_.removeHandler(handler))
  }

看起來并不難理解,我們就來讀讀其中最長的attachPage()方法。它的流程是:調(diào)用Jetty工具類JettyUtils的createServletHander()方法,為WebUIPage的兩個渲染方法render()和renderJson()創(chuàng)建ServletContextHandler,也就是一個WebUIPage需要對應(yīng)兩個處理器。然后,調(diào)用上述attachHandler()方法向Jetty注冊處理器,并將映射關(guān)系寫入handlers結(jié)構(gòu)中。

綁定WebUI到Jetty服務(wù)

這里就是在前一章節(jié)提到的bind()方法了。

代碼#14.5 - o.a.s.ui.WebUI.bind()方法

  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")
      serverInfo = Some(startJettyServer(host, port, sslOptions, handlers, conf, name))
      logInfo(s"Bound $className to $host, and started at $webUrl")
    } catch {
      case e: Exception =>
        logError(s"Failed to bind $className", e)
        System.exit(1)
    }
  }

該方法調(diào)用了JettyUtils.startJettyServer()方法來啟動Jetty服務(wù),具體不再贅述。

Spark Web UI的展示

Spark Web UI實(shí)際上是一個三層的樹形結(jié)構(gòu),根節(jié)點(diǎn)為WebUI,中層節(jié)點(diǎn)為WebUITab,葉子節(jié)點(diǎn)為WebUIPage。UI界面的展示就主要靠WebUITab與WebUIPage來實(shí)現(xiàn)。在Spark UI界面中,一個Tab可以包含一個或多個Page,并且Tab是可選的。

WebUITab與WebUIPage的定義

以下是WebUITab的代碼。

代碼#14.6 - o.a.s.ui.WebUITab抽象類

private[spark] abstract class WebUITab(parent: WebUI, val prefix: String) {
  val pages = ArrayBuffer[WebUIPage]()
  val name = prefix.capitalize

  def attachPage(page: WebUIPage) {
    page.prefix = (prefix + "/" + page.prefix).stripSuffix("/")
    pages += page
  }

  def headerTabs: Seq[WebUITab] = parent.getTabs

  def basePath: String = parent.getBasePath
}

由于一個Tab可以包含多個Page,因此pages數(shù)組就用來緩存該Tab下所有的Page。attachPage()方法就用于將Tab的路徑前綴與Page的路徑前綴拼合起來,并將其加入pages數(shù)組中。

WebUIPage抽象類的定義更加簡單,只有兩個方法,前面已經(jīng)出現(xiàn)過。render()方法用于渲染頁面,renderJson()方法則用于生成對應(yīng)的JSON串,代碼就不再貼出來了。

WebUITab與WebUIPage各有很多的實(shí)現(xiàn)類,分別對應(yīng)一個Tab或一個Page。本來想拿IDEA生成兩張類圖,但是不知為何,所有表示繼承關(guān)系的箭頭都顯示不出來(可能IDEA對Scala的支持仍然不是很好吧),只得作罷。最后,我們來看看Spark UI上的內(nèi)容是怎樣展示出來的。

渲染Spark UI頁面

我們以Environment這一頁為例來探索,因?yàn)樗捻撁嬖叵喈?dāng)簡單,只是展示許多環(huán)境信息(如Spark配置、系統(tǒng)屬性、JVM信息、Classpath等等)的表格,干擾比較少。其頁面本身如下圖所示。

圖#14.2 - Spark UI Environment頁

首先來看EnvironmentTab的代碼,非常簡單。

代碼#14.7 - o.a.s.ui.env.EnvironmentTab類

private[ui] class EnvironmentTab(
    parent: SparkUI,
    store: AppStatusStore) extends SparkUITab(parent, "environment") {
  attachPage(new EnvironmentPage(this, parent.conf, store))
}

其中SparkUITab就是對WebUITab的簡單封裝,加上了Application名稱和Spark版本的屬性。EnvironmentTab類只有構(gòu)造方法,調(diào)用代碼#14.6中預(yù)先定義好的attachPage()方法,將EnvironmentPage加入。以下則是EnvironmentPage的具體實(shí)現(xiàn)。

代碼#14.8 - o.a.s.ui.env.EnvironmentPage類

private[ui] class EnvironmentPage(
    parent: EnvironmentTab,
    conf: SparkConf,
    store: AppStatusStore) extends WebUIPage("") {

  def render(request: HttpServletRequest): Seq[Node] = {
    val appEnv = store.environmentInfo()
    val jvmInformation = Map(
      "Java Version" -> appEnv.runtime.javaVersion,
      "Java Home" -> appEnv.runtime.javaHome,
      "Scala Version" -> appEnv.runtime.scalaVersion)
    val runtimeInformationTable = UIUtils.listingTable(
      propertyHeader, jvmRow, jvmInformation, fixedWidth = true)
    val sparkPropertiesTable = UIUtils.listingTable(propertyHeader, propertyRow,
      Utils.redact(conf, appEnv.sparkProperties.toSeq), fixedWidth = true)
    val systemPropertiesTable = UIUtils.listingTable(
      propertyHeader, propertyRow, appEnv.systemProperties, fixedWidth = true)
    val classpathEntriesTable = UIUtils.listingTable(
      classPathHeaders, classPathRow, appEnv.classpathEntries, fixedWidth = true)
    val content =
      <span>
        <h4>Runtime Information</h4> {runtimeInformationTable}
        <h4>Spark Properties</h4> {sparkPropertiesTable}
        <h4>System Properties</h4> {systemPropertiesTable}
        <h4>Classpath Entries</h4> {classpathEntriesTable}
      </span>
    UIUtils.headerSparkPage("Environment", content, parent)
  }

  private def propertyHeader = Seq("Name", "Value")
  private def classPathHeaders = Seq("Resource", "Source")
  private def jvmRow(kv: (String, String)) = <tr><td>{kv._1}</td><td>{kv._2}</td></tr>
  private def propertyRow(kv: (String, String)) = <tr><td>{kv._1}</td><td>{kv._2}</td></tr>
  private def classPathRow(data: (String, String)) = <tr><td>{data._1}</td><td>{data._2}</td></tr>
}

render()方法用來渲染頁面內(nèi)容,其流程如下:

  • 從AppStatusStore中取得所有環(huán)境信息。
  • 調(diào)用UIUtils.listingTable()方法,將對應(yīng)的表頭與添加了HTML標(biāo)簽的行封裝成表格。
  • 將4張表格排列好,調(diào)用UIUtils.headerSparkPage()方法,按照規(guī)定好的頁面布局展示在瀏覽器上。

這樣,圖#14.2的頁面就顯示出來了。

總結(jié)

本文從SparkContext中對Spark UI的初始化入手,首先介紹了SparkUI類的具體構(gòu)造。然后分析了SparkUI的基類WebUI的具體實(shí)現(xiàn),明確了整個UI界面的組成部分。最后簡要介紹WebUITab與WebUIPage,并以Spark UI中的Environment頁為例,分析了頁面的展示流程。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容