MetricSystem通过一个特定的Instance
创建,由sources和sinks组成,并周期性地将source中的指标数据推送到目标sink中去。MetricSystem使用codahale提供的第三方测量仓库Metrics。MetricSystem中有三个主要概念:
指标的配置格式如下:
[instance].[sink|source].[name].[options] = xxxx
[instance]
:可以是master、worker、executor、driver或者applications,这意味着只有对应的instance
会有这个属性。通配符*
则意味着所有的instance
将会有这个属性。[sink|source]
:表明这个属性是属于source还是sink,这个部分的选项只能是source或sink。[name]
:表明sink或source的名称。[options]
:表示这个source或sink的特定属性。下面以driver实例为例来介绍下MetricsSystem的创建与启动。
driver实例的MetricsSystem创建启动代码位于SparkContext类中。可以看到,创建好MetricsSystem后,则将所有的ServletContextHandler添加到Spark UI中,然后调用其start()
方法启动,最后注册其他几个source对象。
// 创建SparkEnv,在SparkEnv创建过程中创建MetricsSystem
_env = createSparkEnv(_conf, isLocal, listenerBus)
SparkEnv.set(_env)
// Driver的metrics system需要将app ID设置到spark.app.id属性值中,
// 因此它应该在我们从task scheduler中得到app ID(在SchedulerBackend构造函数中创建)并且设置spark.app.id属性后再启动
// 启动MetricsSystem
_env.metricsSystem.start()
// 在metrics system启动后,将driver metrics servlet handler添加到web ui中
_env.metricsSystem.getServletHandlers.foreach(handler => ui.foreach(_.attachHandler(handler)))
// 等待taskScheduler的backend就绪
_taskScheduler.postStartHook()
_env.metricsSystem.registerSource(_dagScheduler.metricsSource)
_env.metricsSystem.registerSource(new BlockManagerSource(_env.blockManager))
_executorAllocationManager.foreach { e =>
_env.metricsSystem.registerSource(e.executorAllocationManagerSource)
}
DAGSchedulerSource测量的信息是dagScheduler.failedStages、dagScheduler.runningStages、dagScheduler.waitingStages、dagScheduler.numTotalJobs、dagScheduler.activeJobs。
BlockManagerSource测量的信息是memory.maxMem_MB、memory.maxOnHeapMem_MB、memory.maxOffHeapMem_MB、memory.remainingMem_MB、memory.remainingOnHeapMem_MB、memory.remainingOffHeapMem_MB、memory.memUsed_MB、memory.onHeapMemUsed_MB、memory.offHeapMemUsed_MB、disk.diskSpaceUsed_MB。
MetricsSystem类部分代码如下:
private[spark] class MetricsSystem private (
val instance: String,
conf: SparkConf,
securityMgr: SecurityManager)
extends Logging {
// 存储指标配置信息
private[this] val metricsConfig = new MetricsConfig(conf)
// 存储sinks和sources
private val sinks = new mutable.ArrayBuffer[Sink]
private val sources = new mutable.ArrayBuffer[Source]
private val registry = new MetricRegistry()
// 没有定义为volatile,也即没有考虑多线程场景
private var running: Boolean = false
// 将MetricsServlet看成一个特殊的sink,因为它会暴露给web ui的添加handlers操作
private var metricsServlet: Option[MetricsServlet] = None
// 得到该metrics system使用到的所有UI handlers;只能在start()方法调用后才能被调用
def getServletHandlers: Array[ServletContextHandler] = {
require(running, "Can only call getServletHandlers on a running MetricsSystem")
metricsServlet.map(_.getHandlers(conf)).getOrElse(Array())
}
// 加载配置信息
metricsConfig.initialize()
// 启动该metrics system
def start() {
// 强制不能调用多次,而且不是多线程安全的,也即可能防止单线程中反复调用start方法场景
require(!running, "Attempting to start a MetricsSystem that is already running")
// 假如running=true执行后被hang住了,后面语句没有执行?也即未考虑多线程场景
running = true
StaticSources.allSources.foreach(registerSource)
registerSources()
registerSinks()
sinks.foreach(_.start)
}
...
}
总结来说,MetricsSystem的启动过程包括以下步骤:
MetricsSystem启动完毕后,会遍历与Sinks有关的ServletContextHandler,并调用attachHandler将它们绑定到Spark UI上。
创建MetricsSystem时,在其构造函数中调用metricsConfig.initialize()
来加载配置信息,具体实现如下:
/**
* 基于优先级从多个位置加载配置信息
* 如果同一个配置在这个方法中被再次赋值,则旧配置值会被覆盖
*/
def initialize() {
// 设置默认配置值以防止没有配置文件存在
setDefaultProperties(properties)
// 从配置文件中加载配置,可通过"spark.metrics.conf"指定,默认值为metrics.properties
loadPropertiesFromFile(conf.getOption("spark.metrics.conf"))
// 加载SparkConf中以"spark.metrics.conf."为前缀的配置值
val prefix = "spark.metrics.conf."
conf.getAll.foreach {
case (k, v) if k.startsWith(prefix) =>
properties.setProperty(k.substring(prefix.length()), v)
case _ =>
}
// 得到每个instance配置的子配置值。配置以"."分隔,第一个"."前的为instance作为key,
// 并将余下的配置kv值统一作为这个instance的key的value值。
// 比如输入属性值为("*.sink.servlet.class"->"class1", "*.sink.servlet.path"->"path1"),
// 那么返回Map("*" -> Properties("sink.servlet.class" -> "class1", "sink.servlet.path" -> "path1"))
perInstanceSubProperties = subProperties(properties, INSTANCE_REGEX)
// 如果instance包含通配符"*"配置,则将该配置设置给所有具体的instance。
if (perInstanceSubProperties.contains(DEFAULT_PREFIX)) {
val defaultSubProperties = perInstanceSubProperties(DEFAULT_PREFIX).asScala
for ((instance, prop) <- perInstanceSubProperties if (instance != DEFAULT_PREFIX);
(k, v) <- defaultSubProperties if (prop.get(k) == null)) {
prop.put(k, v)
}
}
}
注册Sources在start()
中被调用,包括两个步骤:
注册固定sources,包括CodegenMetrics和HiveCatalogMetrics;
注册配置的该instance对应的所有sources
// 注册固定sources,allSources = Seq(CodegenMetrics, HiveCatalogMetrics)
StaticSources.allSources.foreach(registerSource)
// 注册该instance对应的sources
registerSources()
private def registerSources() {
// 得到某instance对应的配置信息
val instConfig = metricsConfig.getInstance(instance)
// 得到source的配置信息
val sourceConfigs = metricsConfig.subProperties(instConfig, MetricsSystem.SOURCE_REGEX)
// 注册该instance相关的所有source
sourceConfigs.foreach { kv =>
val classPath = kv._2.getProperty("class")
try {
val source = Utils.classForName(classPath).newInstance()
registerSource(source.asInstanceOf[Source])
} catch {
case e: Exception => logError("Source class " + classPath + " cannot be instantiated", e)
}
}
}
注册并启动Sinks在start()
中被调用:
registerSinks()
// 调用每个具体Sink的start方法进行启动,start方法在子类中被实现
sinks.foreach(_.start)
注册Sinks主要包括两个步骤:
得到该instance对应的所有sinks的配置。
将子属性class对应的类metricsServlet反射得到MetricsServlet实例。如果属性的key是servlet,将其设置为metricsServlet;如果是Sink,则加入到ArrayBuffer[Sink]中。
private def registerSinks() {
// 得到该instance对应的所有sinks的配置
val instConfig = metricsConfig.getInstance(instance)
val sinkConfigs = metricsConfig.subProperties(instConfig, MetricsSystem.SINK_REGEX)
sinkConfigs.foreach { kv =>
val classPath = kv._2.getProperty("class")
if (null != classPath) {
try {
val sink = Utils.classForName(classPath)
.getConstructor(classOf[Properties], classOf[MetricRegistry], classOf[SecurityManager])
.newInstance(kv._2, registry, securityMgr)
if (kv._1 == "servlet") {
metricsServlet = Some(sink.asInstanceOf[MetricsServlet])
} else {
sinks += sink.asInstanceOf[Sink]
}
} catch {
case e: Exception =>
logError("Sink class " + classPath + " cannot be instantiated")
throw e
}
}
}
}
为了能够在SparkUI访问到测量数据,所以需要给Sinks增加Jetty的ServletContextHandler,这里主要用到MetricSystem的getServletHandlers方法实现。生成的ServletContextHandler通过SparkUI的attachHandler方法,被绑定到SparkUI。
// 在metrics system启动后,将driver metrics servlet handler添加到web ui中
_env.metricsSystem.getServletHandlers.foreach(handler => ui.foreach(_.attachHandler(handler)))
def getServletHandlers: Array[ServletContextHandler] = {
require(running, "Can only call getServletHandlers on a running MetricsSystem")
metricsServlet.map(_.getHandlers(conf)).getOrElse(Array())
}
我们可以使用以下这些地址访问测量数据(具体截图待补充):
Spark使用第三方库codahale中的MetricRegistry来连接Source和Sink。
Source接口的定义如下,可以看到每个Source中都包含MetricRegistry成员。
import com.codahale.metrics.MetricRegistry
private[spark] trait Source {
def sourceName: String
def metricRegistry: MetricRegistry
}
MetricRegistry类继承com.codahale.metrics.MetricSet
中的getMetrics()
方法来实现数据收集工作。
public class MetricRegistry implements MetricSet {}
以DAGSchedulerSource为例,向MetricRegistry中注册要收集的指标及指标收集类型与方法。
private[scheduler] class DAGSchedulerSource(val dagScheduler: DAGScheduler)
extends Source {
override val metricRegistry = new MetricRegistry()
override val sourceName = "DAGScheduler"
metricRegistry.register(MetricRegistry.name("stage", "failedStages"), new Gauge[Int] {
override def getValue: Int = dagScheduler.failedStages.size
})
...
}
向MetricSystem中注册Source时,将其MetricRegistry注册到MetricSystem的metricRegistry中。
def registerSource(source: Source) {
sources += source
try {
val regName = buildRegistryName(source)
registry.register(regName, source.metricRegistry)
} catch {
case e: IllegalArgumentException => logInfo("Metrics already registered", e)
}
}
在注册每个Sink时都会将MetricSystem中的metricRegistry传递进去:
private def registerSinks() {
val instConfig = metricsConfig.getInstance(instance)
val sinkConfigs = metricsConfig.subProperties(instConfig, MetricsSystem.SINK_REGEX)
sinkConfigs.foreach { kv =>
val classPath = kv._2.getProperty("class")
if (null != classPath) {
try {
val sink = Utils.classForName(classPath)
.getConstructor(classOf[Properties], classOf[MetricRegistry], classOf[SecurityManager])
.newInstance(kv._2, registry, securityMgr)
if (kv._1 == "servlet") {
metricsServlet = Some(sink.asInstanceOf[MetricsServlet])
} else {
sinks += sink.asInstanceOf[Sink]
}
} catch {
case e: Exception =>
logError("Sink class " + classPath + " cannot be instantiated")
throw e
}
}
}
}
以MetricsServlet这个Sink为例,其在收到请求后将registry的内容转换为字符串发送出去。
private[spark] class MetricsServlet(
val property: Properties,
val registry: MetricRegistry,
securityMgr: SecurityManager)
extends Sink {
def getHandlers(conf: SparkConf): Array[ServletContextHandler] = {
Array[ServletContextHandler](
createServletHandler(servletPath,
new ServletParams(request => getMetricsSnapshot(request), "text/json"), securityMgr, conf)
)
}
def getMetricsSnapshot(request: HttpServletRequest): String = {
mapper.writeValueAsString(registry)
}
}