這篇文章的主旨在于讓你了解Spark UI體系,并且能夠讓你有能力對UI進行一些定制化增強。在分析過程中,你也會深深的感受到Scala語言的魅力。
前言
有時候我們希望能對Spark UI進行一些定制化增強。并且我們希望盡可能不更改Spark的源碼。為了達到此目標,我們會從如下三個方面進行闡述:
- 理解Spark UI的處理流程
- 現有Executors頁面分析
- 自己編寫一個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())
這里做了兩個動作,
- 通過
SparkUI.createLiveUI
創建一個SparkUI實例_ui
- 通過
_ui.foreach(_.bind())
啟動jetty。bind 方法是繼承自WebUI,該類負責和真實的Jetty Server API打交道。
和傳統的Web服務不一樣,Spark并沒有使用什么頁面模板引擎,而是自己定義了一套頁面體系。我們把這些對象分成兩類:
- 框架類,就是維護各個頁面關系,和Jetty API有關聯,負責管理的相關類。
- 頁面類,比如頁面的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頁面
我們的例子很簡單,類似下面的圖:
按前文的描述,我們需要一個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.Logging
import 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依賴。