[Spark2.4] 增加Prometheus Sink带来的问题,并通过源码分析原因

目录

背景

Prometheus Sink

如何对外暴露Http Endpoint

服务注册源码

服务发现源码

如何采集spark metrics

MetricsReporter源码

配置spark conf启用prometheus sink

Prometheus Sink启动的问题

Spark Metrics System

spark启动过程源码分析

spark-submit.sh

SparkSubmit执行流程 

--jars、--driver-library-path和--driver-class-path

spark main jar

YarnClusterApplication

AM启动流程

AM CLASSPASH环境变量的设置

Container资源上传到HDFS

启动AM

ApplicationMaster 

解决ClassNotFound

executor也出现ClassNotFoundException

AM和Driver的http server暴露相同的内容

代码结构

PrometheusSink重构

 PrometheusReporter重构

单例的Prometheus Collector

运行结果

Executor进程的metrics信息为空


背景

        当前有一个multi-module project,其中有ModuleA,ModuleB,ModuleC。

        ModuleA:定义了main函数,会通过SPI和反射等机制,从ModuleC中加载Spark foreachRDD方法的具体执行逻辑。

        ModuleB:定义了sparkstreaming context的配置,以及DStream的处理顺序,Prometheus Sink相关的代码就写在这个工程中

        ModuleC:主要定义了foreachRDD的具体执行逻辑,例如将RDD对象反序列化后,写入Hbase中。

        spark job使用spark-submit启动,使用yarn-cluster部署模型。

Prometheus Sink

        增加prometheus,主要有两个挑战。

如何对外暴露Http Endpoint

        由于spark提交的任务数量是不确定的,且使用yarn进行部署,因此要求动态分配port;且由于prometheus采用的pull模型,因此要求http endpoint需要支持服务发现的能力。

        prometheus-2.30支持Http Service Discovery功能,因此决定使用zookeeper+springboot的方式实现服务注册和服务发现功能。

服务注册源码

package ...;

import com.codahale.metrics.MetricRegistry;
import ....PrometheusReporter;
import io.prometheus.client.exporter.HTTPServer;
import org.apache.commons.lang3.StringUtils;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.RetryForever;
import org.apache.spark.metrics.sink.Sink;
import org.apache.zookeeper.CreateMode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.charset.StandardCharsets;
import java.util.Properties;
import java.util.concurrent.TimeUnit;

/**
 * @Author:
 * @Date: 2022/1/17
 * @TIME: 11:08
 */
public class PrometheusSink implements Sink {
    private final static Logger LOG = LoggerFactory.getLogger(PrometheusSink.class);
    private final static String ZK_HOSTS_KEY = "zkHosts";
    private final static String ZK_SESSION_TIMEOUT_MS = "sessionTimeoutMs";
    private final static String ZK_CONNECT_TIMEOUT_MS = "connectTimeoutMs";
    private final static String ZK_RETRY_INTERVAL_MS = "retryIntervalMs";
    private final static String SPARK_JOB_NAME = "jobName";
    private final Properties property;
    private final MetricRegistry metricRegistry;
    private HTTPServer httpServer;
    private CuratorFramework zkClient;
    private PrometheusReporter prometheusReporter;
    public PrometheusSink(Properties property,
                          MetricRegistry registry,
                          org.apache.spark.SecurityManager securityMgr) {
        this.property = property;
        this.metricRegistry = registry;
    }

    @Override
    public void start() {
        try {
            String zkHosts = this.property.getProperty(ZK_HOSTS_KEY);
            if (StringUtils.isEmpty(zkHosts))
                throw new RuntimeException("[[instance].sink.prometheus.zkHosts]不能为空");
            int sessionTimeoutMs = Integer.parseInt(this.property
                    .getProperty(ZK_SESSION_TIMEOUT_MS, "30000"));
            int connectTimeoutMs = Integer.parseInt(this.property
                    .getProperty(ZK_CONNECT_TIMEOUT_MS, "10000"));
            int retryInterval = Integer.parseInt(this.property
                    .getProperty(ZK_RETRY_INTERVAL_MS, "3000"));
            String jobName = this.property.getProperty(SPARK_JOB_NAME);
            if (StringUtils.isEmpty(jobName))
                throw new RuntimeException("[[instance].sink.prometheus.jobName]不能为空");
            this.prometheusReporter = new PrometheusReporter(this.metricRegistry,jobName);
            int retryCount = 0;
            InetSocketAddress inetSocketAddress = new InetSocketAddress(InetAddress.getLocalHost(), 0);
            String url = "http://%s:%d/metrics";
            String ip = null;
            int port;
            while (true) {
                try {
                    try (ServerSocket serverSocket = new ServerSocket()) {
                        serverSocket.bind(inetSocketAddress);
                        ip = inetSocketAddress.getAddress().getHostAddress();
                        port = serverSocket.getLocalPort();
                        url = String.format(url, ip, port);
                    }
                    this.httpServer = new HTTPServer(port);
                    break;
                } catch (IOException ioException) {
                    if(++retryCount==3) {
                        throw ioException;
                    }
                }
            }
            LOG.info("start [Prometheus] metrics sink\nHttp EndPoint:{}",String.format(url,ip, port));

            //注册ZK
            this.zkClient = CuratorFrameworkFactory.newClient(zkHosts,sessionTimeoutMs,connectTimeoutMs,new RetryForever(retryInterval));
            this.zkClient.start();
            LOG.info("启动zk client,zkHosts=[{}],sessionTimeout=[{}] ms,connectTimeout=[{}] ms," +
                            "failOverRetryInterval=[{}] ms",zkHosts,sessionTimeoutMs,connectTimeoutMs,
                    retryInterval);
            this.zkClient.blockUntilConnected();
            String zNodePath = "/waterfall/spark/metrics/promethues/"+jobName+"/config";
            this.zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
                    .forPath(zNodePath,url.getBytes(StandardCharsets.US_ASCII));
        }catch (Exception e) {
            throw new RuntimeException("启动prometheus metrics失败",e);
        }
        this.prometheusReporter.start(15, TimeUnit.SECONDS);
    }

    @Override
    public void stop() {
        if(zkClient!=null) {
            zkClient.close();
        }
        if(this.httpServer!=null)
            this.httpServer.close();
        this.prometheusReporter.stop();
    }

    @Override
    public void report() {
        //源码来看,只有在JVM停止前才会触发report
        LOG.info("JVM进程即将停止,会自动调用一次report");
        this.prometheusReporter.report();
    }
}

服务发现源码

package ...;

import ....config.zookeeper.ZookeeperConfigProperties;
import ....config.mvc.exception.BusinessException;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.TreeCache;
import org.apache.curator.framework.recipes.cache.TreeCacheEvent;
import org.apache.curator.retry.RetryForever;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

import java.io.Closeable;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @Author: 
 * @Date: 2021/12/15
 * @TIME: 16:55
 */
public class FlinkServiceDiscovery implements Closeable {
    private final static Logger LOG = LoggerFactory.getLogger(FlinkServiceDiscovery.class);

    private final ZookeeperConfigProperties zkConfig;
    private final Path parentPath;
    private CuratorFramework zkClient;
    private TreeCache treeCache;

    //正在运行的flink task
    private final Map<String, Set<String>> activeServiceMap = new HashMap<>();
    private final ReadWriteLock serviceMapLock = new ReentrantReadWriteLock();
    //下面两个属性,用于实现第一次TreeNode Cache初始化成功后才启动spring上下文
    private final AtomicBoolean hasStart = new AtomicBoolean(false);
    private final CountDownLatch countDownLatch = new CountDownLatch(1);
    public FlinkServiceDiscovery(ZookeeperConfigProperties zkConfig) {
        this.zkConfig = zkConfig;
        parentPath = Paths.get(this.zkConfig.getFlinkMetricsZnodePath());
    }

    /**
     *
     * @return true:启动成功;flase:启动失败
     * @throws InterruptedException
     */
    public boolean start() throws InterruptedException {
        LOG.info("start flink metrics server的服务发现功能,连接的zookeeper:[{}],监控的ZNode Path:[{}]",
                this.zkConfig.getZkHosts(), this.zkConfig.getFlinkMetricsZnodePath());
        this.zkClient = CuratorFrameworkFactory
                .newClient(this.zkConfig.getZkHosts(), new RetryForever(3000));
        this.zkClient.start();
        //等待连接建立成功
        this.zkClient.blockUntilConnected();
        try {
            Stat stat = this.zkClient.checkExists()
                    .forPath(this.zkConfig.getFlinkMetricsZnodePath());
            if (stat == null) {
                LOG.warn("不存在ZNode路径:[{}],因此不会启动服务发现功能",this.zkConfig.getFlinkMetricsZnodePath());
                return false;
            }
            LOG.info("开始对ZNode:[{}]建立监听", this.zkConfig.getFlinkMetricsZnodePath());
            this.treeCache = TreeCache.newBuilder(this.zkClient, this.zkConfig.getFlinkMetricsZnodePath())
                    .setCacheData(true)
                    .build();
            treeCache.getListenable().addListener(this::ZnodeListenerFunction);
            this.treeCache.start();
            return true;
        } catch (Exception exception) {
            LOG.warn("建立服务发现功能失败", exception);
            try {
                this.close();
            } catch (IOException ioException) {
                //do nothing
            }
        }
        return false;
    }

    /**
     * 等待TreeNode的Cache建立完成
     * @throws InterruptedException
     */
    public void waitUntilWatcherReady() throws InterruptedException {
        LOG.info("等待watcher Node Cache initial");
        if(!this.hasStart.get()) {
            this.countDownLatch.await();
        }
        LOG.info("watcher Node Cache初始化完成");
    }

    /**
     * 获取当前所有存活的flink task的taskName和prometheus servers的URL[]
     *
     * @return
     */
    public Map<String, Set<String>> findAllActiveServices() {
        try {
            this.serviceMapLock.readLock().lock();
            return this.activeServiceMap;
        }finally {
            this.serviceMapLock.readLock().unlock();
        }
    }

    /**
     * ZNode变化的回调方法
     *
     * @param curatorFramework
     * @param event
     */
    private void ZnodeListenerFunction(CuratorFramework curatorFramework, TreeCacheEvent event) {
        switch (event.getType()) {
            case NODE_ADDED:
                this.addServiceMap(event);
                break;
            case NODE_UPDATED:
                LOG.warn("node update,{}", event.getData().toString());
                //理论上不会发生
                break;
            case NODE_REMOVED:
                this.deleteServiceMap(event);
                break;
            case CONNECTION_LOST:
            case CONNECTION_SUSPENDED:
                LOG.warn("zookeeper连接丢失,event type={}", event.getType());
                break;
            case INITIALIZED:
                LOG.debug("event type={}", event.getType());
                if(this.hasStart.compareAndSet(false,true)) {
                    this.countDownLatch.countDown();
                }
            default:
                LOG.debug("event type={}", event.getType());
        }
    }

    /**
     * 增加
     *
     * @param addEvent
     */
    private void addServiceMap(TreeCacheEvent addEvent) {
        String watchPath = addEvent.getData().getPath();
        byte[] watchDataArray = addEvent.getData().getData();
        String watchData = "";
        if (watchDataArray != null && watchDataArray.length > 0) {
            watchData = new String(watchDataArray, StandardCharsets.UTF_8);
        }
        LOG.debug("node add,path={},data={}", watchPath, watchData);
        //由于zookeeper使用的资源定位方式类似于Path,因此直接通过java.nio.Path类来处理
        Path path = Paths.get(watchPath);
        Path subPath = this.parentPath.relativize(path);
        if (subPath.getNameCount() == 2) {
            String taskName = subPath.getName(0).toString();
            try {
                this.serviceMapLock.writeLock().lock();
                if (this.activeServiceMap.containsKey(taskName)) {
                    Set<String> set = this.activeServiceMap.get(taskName);
                    set.add(watchData);
                } else {
                    Set<String> set = new HashSet<>();
                    set.add(watchData);
                    this.activeServiceMap.put(taskName, set);
                }
            } finally {
                this.serviceMapLock.writeLock().unlock();
            }
        } else
            LOG.warn("ADD变化的ZNode Path={}", watchPath);
    }

    /**
     * 删除
     *
     * @param delEvent
     */
    private void deleteServiceMap(TreeCacheEvent delEvent) {
        String watchPath = delEvent.getData().getPath();
        byte[] watchDataArray = delEvent.getData().getData();
        String watchData = "";
        if (watchDataArray != null && watchDataArray.length > 0) {
            watchData = new String(watchDataArray, StandardCharsets.UTF_8);
        }
        LOG.debug("node delete,path={},data={}", watchPath, watchData);
        Path path = Paths.get(watchPath);
        Path subPath = this.parentPath.relativize(path);
        if (subPath.getNameCount() == 1) {
            String taskName = subPath.getFileName().toString();
            LOG.warn("ZNode被手动删除,Path={}",watchPath);
            if(!StringUtils.hasText(taskName))
                return;
            try {
                this.serviceMapLock.writeLock().lock();
                this.activeServiceMap.remove(taskName);
            }finally {
                this.serviceMapLock.writeLock().unlock();
            }
        } else if (subPath.getNameCount() == 2) {
            String taskName = subPath.getName(0).toString();
            LOG.debug("flink task:[{}], metric server:[{}]的进程结束",taskName,watchData);
            try {
                this.serviceMapLock.writeLock().lock();
                if(this.activeServiceMap.containsKey(taskName)) {
                    this.activeServiceMap.get(taskName).remove(watchData);
                    if(this.activeServiceMap.get(taskName).isEmpty()) {
                        this.activeServiceMap.remove(taskName);
                    }
                }
            }finally {
                this.serviceMapLock.writeLock().unlock();
            }
        } else
            LOG.warn("DELETE变化的ZNode Path={}", watchPath);
    }

    @Override
    public void close() throws IOException {
        LOG.info("关闭flink metrics服务发现功能");
        if (this.treeCache != null)
            this.treeCache.close();
        if (this.zkClient != null)
            this.zkClient = null;
    }
}

如何采集spark metrics

        spark的metrics system使用的数据结构定义,与prometheus不兼容,因此需要实现一个Reporter类,专门负责数据结构的转换工作。

MetricsReporter源码

package ...;

import com.codahale.metrics.*;

import java.util.Map;
import java.util.SortedMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @Author: 
 * @Date: 2022/1/17
 * @TIME: 13:53
 */
public class PrometheusReporter extends ScheduledReporter {
    private static final Pattern APPLICATION_ID_REGEX = Pattern.compile("(application_\\d+_\\d+)");

    private static io.prometheus.client.Gauge GAUGE_COLLECTOR = null;

    //NOTE:这里涉及到spark submit的源码部分,AM会阻塞(默认100S)等待driver端的sparkContext初始化结束
    private static AtomicInteger INDENTITY_COUNTER = new AtomicInteger(0);//1:driver,2:AM

    public PrometheusReporter(MetricRegistry registry, String jobName) {
        super(registry, "prometheus-reporter", MetricFilter.ALL, TimeUnit.SECONDS, TimeUnit.SECONDS);
        jobName = jobName.replaceAll("\\.", "_");
        String gaugeNameSuffix = null;
        if(INDENTITY_COUNTER.incrementAndGet()==1) {
            gaugeNameSuffix = "_driver";
        }else {
            gaugeNameSuffix = "_am";
        }
        GAUGE_COLLECTOR = io.prometheus.client.Gauge.build("spark_guage_" + jobName+gaugeNameSuffix,
                "spark gauge metrics")
                .labelNames("application_id", "origin_lable").register();
    }

    @SuppressWarnings("all")
    @Override
    public void report(SortedMap<String, Gauge> gaugeMap,
                       SortedMap<String, Counter> countMap,
                       SortedMap<String, Histogram> histogramMap,
                       SortedMap<String, Meter> meterMap,
                       SortedMap<String, Timer> timerMap) {
        /**
         * 1、目前先只解析gauge和counter两类指标
         */
        if (gaugeMap != null) {
            for (Map.Entry<String, Gauge> gaugeEntry : gaugeMap.entrySet()) {
                String metricName = gaugeEntry.getKey();
                String[] splitKeyworlds = this.spilitMetricName(metricName);
                Gauge gauge = gaugeEntry.getValue();
                if(gauge.getValue() instanceof Number) {
                    GAUGE_COLLECTOR.labels(splitKeyworlds[1],splitKeyworlds[0]).set(Double.parseDouble(String.valueOf(gauge.getValue())));
                }
            }
        }

        if(countMap!=null) {
            for (Map.Entry<String, Counter> counterEntry : countMap.entrySet()) {
                String metricName = counterEntry.getKey();
                String[] splitKeyworlds = this.spilitMetricName(metricName);
                Counter counter = counterEntry.getValue();
                GAUGE_COLLECTOR.labels(splitKeyworlds[1],splitKeyworlds[0])
                        .set(counter.getCount());
            }
        }
    }

    private String[] spilitMetricName(String metricName) {
        String[] result = new String[2];
        result[0] = metricName;
        Matcher matcher = APPLICATION_ID_REGEX.matcher(metricName);
        if(matcher.find()) {
            result[0] = metricName.replaceAll("application_\\d+_\\d+","appid");
            result[1] = matcher.group(1);
        }else {
            result[1] = "NaN";
        }
        result[0] = result[0].replaceAll("\\.","_");
        return result;
    }


}

配置spark conf启用prometheus sink

        配置方式有很多,例如通过${SPKAR_HOME}/conf/metrics.properties增加配置。我个人更喜欢通过spark-submit --conf的方式设置。

spark-submit --master yarn --deploy-mode cluster \
--conf spark.metrics.conf.*.sink.prometheus.class=...spark.metric.sink.PrometheusSink \
--conf spark.metrics.conf.*.sink.prometheus.zkHosts=xxx:2181 \
--conf spark.metrics.conf.*.sink.prometheus.sessionTimeoutMs=30000 \
--conf spark.metrics.conf.*.sink.prometheus.connectTimeoutMs=10000 \
--conf spark.metrics.conf.*.sink.prometheus.retryIntervalMs=3000 \
--conf spark.metrics.conf.*.sink.prometheus.jobName=MyJob1 \
--jars moduleB.jar,moduleC.jar \
--class mainClass \
/tmp/moduleA.jar \
<args...>

        到这里为止,已经按照官方的metric system文档完成了所有的开发工作,但在运行起来后,你会发现下面的问题

Prometheus Sink启动的问题

22/01/27 10:20:16 ERROR metrics.MetricsSystem: Sink class ....spark.metric.sink.PrometheusSink cannot be instantiated
22/01/27 10:20:16 ERROR yarn.ApplicationMaster: Uncaught exception: 
java.lang.ClassNotFoundException: ....spark.metric.sink.PrometheusSink
	at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Class.java:348)
	at org.apache.spark.util.Utils$.classForName(Utils.scala:242)
	at org.apache.spark.metrics.MetricsSystem$$anonfun$registerSinks$1.apply(MetricsSystem.scala:198)
	at org.apache.spark.metrics.MetricsSystem$$anonfun$registerSinks$1.apply(MetricsSystem.scala:194)
	at scala.collection.mutable.HashMap$$anonfun$foreach$1.apply(HashMap.scala:130)
	at scala.collection.mutable.HashMap$$anonfun$foreach$1.apply(HashMap.scala:130)
	at scala.collection.mutable.HashTable$class.foreachEntry(HashTable.scala:236)
	at scala.collection.mutable.HashMap.foreachEntry(HashMap.scala:40)
	at scala.collection.mutable.HashMap.foreach(HashMap.scala:130)
	at org.apache.spark.metrics.MetricsSystem.registerSinks(MetricsSystem.scala:194)
	at org.apache.spark.metrics.MetricsSystem.start(MetricsSystem.scala:102)
	at org.apache.spark.deploy.yarn.ApplicationMaster.createAllocator(ApplicationMaster.scala:433)
	at org.apache.spark.deploy.yarn.ApplicationMaster.runDriver(ApplicationMaster.scala:460)
	at org.apache.spark.deploy.yarn.ApplicationMaster.run(ApplicationMaster.scala:275)
	at org.apache.spark.deploy.yarn.ApplicationMaster$$anon$3.run(ApplicationMaster.scala:805)
	at org.apache.spark.deploy.yarn.ApplicationMaster$$anon$3.run(ApplicationMaster.scala:804)
	at java.security.AccessController.doPrivileged(Native Method)
	at javax.security.auth.Subject.doAs(Subject.java:422)
	at org.apache.hadoop.security.UserGroupInformation.doAs(UserGroupInformation.java:1875)
	at org.apache.spark.deploy.yarn.ApplicationMaster$.main(ApplicationMaster.scala:804)
	at org.apache.spark.deploy.yarn.ApplicationMaster.main(ApplicationMaster.scala)

        在spark-submit中,通过--jars moduleB.jar设置了prometheus sink的代码(位于ModuleB中),理论上来说应该没有问题。

Spark Metrics System

spark metrics system的文档中有这么一段话:

syntax: [instance].sink|source.[name].[options]=[value]

#  This file configures Spark's internal metrics system. The metrics system is
#  divided into instances which correspond to internal components.
#  Each instance can be configured to report its metrics to one or more sinks.
#  Accepted values for [instance] are "master", "worker", "executor", "driver",
#  and "applications". A wildcard "*" can be used as an instance name, in
#  which case all instances will inherit the supplied property.

        也就是说,一个正常运行的spark on yarn任务,应该包含applications|driver|executor三个metric instance,而我们上面的配置是*,也就是这三个instance都增加了prometheus sink,下面我修改了配置:

--conf spark.metrics.conf.driver.sink.prometheus.class=...spark.metric.sink.PrometheusSink \
--conf spark.metrics.conf.driver.sink.prometheus.zkHosts=xxx:2181 \
--conf spark.metrics.conf.driver.sink.prometheus.sessionTimeoutMs=30000 \
--conf spark.metrics.conf.driver.sink.prometheus.connectTimeoutMs=10000 \
--conf spark.metrics.conf.driver.sink.prometheus.retryIntervalMs=3000 \
--conf spark.metrics.conf.driver.sink.prometheus.jobName=MyJob1 \

        以这个配置重写spark-submit,是可以正常运行的(这里注意:如果上面的配置,driver改为executor,依然会报ClassNotFoundException,这个原因和ApplicationMaster是一样的)。

        到这里,可以说明spark driver和YarnAM之间加载的jar包是不一样的,需要通过源码分析一下。

spark启动过程源码分析

spark-submit.sh

SparkSubmit执行流程 

        这个类的主要作用是解析spark-submit传入的args,并反射调用org.apache.spark.deploy.yarn.YarnClusterApplication的main函数,源码很长,不过多解读,只了解几个很重要的地方。

--jars、--driver-library-path和--driver-class-path

        在经过SparkSubmit处理后,进入YarnClusterApplication方法之前,会构造出三个spark config出来:

  1. spark.yarn.dist.jars:等价于--jars中指定的jar包列表,但会做一些URI转换工作,例如相对路径转决定路径,globPath解析,FTP文件下载到本地等工作。
  2. spark.driver.extra.classPath:等价于--driver-class-path,不做任何修改。
  3. spark.driver.extra.libraryPath:等价于--driver-library-path,不做任何修改。

spark main jar

        spark main jar,在源码中被称为primary resource,这个信息在SparkSubmit中被构造为YarnClusterApplication的main函数args入参,形式为:

--jar moduleA.jar
--class moduleA.mainClassName
--arg moduleA.main.args

        到这里,SparkSubmit逻辑结束,进入YarnClusterApplication中

YarnClusterApplication

        这个类用于启动yarn AM,并在AM中启动spark driver端(也就是ModuleA)的代码。

其核心在AM的jar包是如何设置的。

AM启动流程

def submitApplication(): ApplicationId = {
    var appId: ApplicationId = null
    try {
      launcherBackend.connect()
      yarnClient.init(hadoopConf)
      yarnClient.start()

      logInfo("Requesting a new application from cluster with %d NodeManagers"
        .format(yarnClient.getYarnClusterMetrics.getNumNodeManagers))

      // Get a new application from our RM
      val newApp = yarnClient.createApplication()
      val newAppResponse = newApp.getNewApplicationResponse()
      appId = newAppResponse.getApplicationId()

      new CallerContext("CLIENT", sparkConf.get(APP_CALLER_CONTEXT),
        Option(appId.toString)).setCurrentContext()

      // Verify whether the cluster has enough resources for our AM
      verifyClusterResources(newAppResponse)

      // 重点!!!这里构造AM Container的上下文,所有jar包和Classpath相关都在这里面
      val containerContext = createContainerLaunchContext(newAppResponse)
      val appContext = createApplicationSubmissionContext(newApp, containerContext)

      // Finally, submit and monitor the application
      logInfo(s"Submitting application $appId to ResourceManager")
      yarnClient.submitApplication(appContext)
      launcherBackend.setAppId(appId.toString)
      reportLauncherState(SparkAppHandle.State.SUBMITTED)

      appId
    } catch {
      case e: Throwable =>
        if (appId != null) {
          cleanupStagingDir(appId)
        }
        throw e
    }
  }

AM CLASSPASH环境变量的设置

private def createContainerLaunchContext(newAppResponse: GetNewApplicationResponse)
    : ContainerLaunchContext = {
    logInfo("Setting up container launch context for our AM")
    val appId = newAppResponse.getApplicationId
    val appStagingDirPath = new Path(appStagingBaseDir, getAppStagingDir(appId))
    val pySparkArchives =
      if (sparkConf.get(IS_PYTHON_APP)) {
        findPySparkArchives()
      } else {
        Nil
      }

    //appStagingDirPath=hdfs://nameservice1/user/spark_cdh/.sparkStaging
    //pySparkArchives:不需要关注
    // 构造AM的Environment,其中包含CLASSPATH
    val launchEnv = setupLaunchEnv(appStagingDirPath, pySparkArchives)
    // 用于设置yarn container需要下载的资源,例如jar包,log4j.properties文件等
    val localResources = prepareLocalResources(appStagingDirPath, pySparkArchives)

    val amContainer = Records.newRecord(classOf[ContainerLaunchContext])
    amContainer.setLocalResources(localResources.asJava)
    amContainer.setEnvironment(launchEnv.asJava)

    //下面用于构造AM启动的脚本,就是一个java -server -jar ... <mainClass> <args>的命令
  }
private def setupLaunchEnv(
      stagingDirPath: Path,
      pySparkArchives: Seq[String]): HashMap[String, String] = {
    logInfo("Setting up the launch environment for our AM container")
    val env = new HashMap[String, String]()

    // 重点!!! AM的classpath支持扩展,通过这个配置项控制,而这个配置项又是通过--driver-class-path设置的
    // DRIVER_CLASS_PATH = spark.driver.extraClassPath
    /**
     * env.put("CLASSPATH",origin+"DRIVER_CLASS_PATH")
     * env.put("CLASSPATH",origin+"{{PWD}}")
     * evn.put("CLASSPATH",origin+"{{PWD}}/__spark_conf__")
     * env.put("CLASSPATH",origin+"{{PWD}}/__spark_libs__/"*"")
     * env.put("CLASSPATH",origin+"/opt/cloudera/parcels/CDH-6.3.2-1.cdh6.3.2.p0.1605554/lib/spark/jars/"*")
     * env.put("CLASSPATH",origin+"/opt/cloudera/parcels/CDH-6.3.2-1.cdh6.3.2.p0.1605554/lib/spark/hive/"*")
     * env.put("CLASSPATH","{{PWD}}/__spark_conf__/__hadoop_conf__")
     * env.put("CLASSPATH","环境变量:SPARK_DIST_CLASSPATH")
     */
    populateClasspath(args, hadoopConf, sparkConf, env, sparkConf.get(DRIVER_CLASS_PATH))
    env("SPARK_YARN_STAGING_DIR") = stagingDirPath.toString
    env("SPARK_USER") = UserGroupInformation.getCurrentUser().getShortUserName()

    // Pick up any environment variables for the AM provided through spark.yarn.appMasterEnv.*
    val amEnvPrefix = "spark.yarn.appMasterEnv."

    /**
     * env.put("MKL_NUM_THREADS",1)
     * env.put("OPENBLAS_NUM_THREADS",1)
     */
    sparkConf.getAll
      .filter { case (k, v) => k.startsWith(amEnvPrefix) }
      .map { case (k, v) => (k.substring(amEnvPrefix.length), v) }
      .foreach { case (k, v) => YarnSparkHadoopUtil.addPathToEnvironment(env, k, v) }

    // SPARK_DIST_CLASSPATH
    /**
     * 这里注意:env之设置最后一个
     */
    sys.env.get(ENV_DIST_CLASSPATH).foreach { dcp =>
      env(ENV_DIST_CLASSPATH) = dcp
    }

    env
  }

Container资源上传到HDFS

        这个方法也很长,只需要了解:

  1. spark primary resource(也就是moduleA.jar)上传到HDFS,并重命名为__app__.jar。
  2. --jars的文件,上传到HDFS。

启动AM

        核心是生成一个command,其内容大致为:

LD_LIBRARY_PATH=\"{spark.driver.extraLibraryPath}:$LD_LIBRARY_PATH\"
{{JAVA_HOME}}/bin/java \ 
               -server \ 
               org.apache.spark.deploy.yarn.ApplicationMaster
               --class MyModuleAMainClass \
               --jar ModuleA.jar
               --arg <spark-submit mainClass args>
               1><LOG_DIR>/stdout \ 
               2><LOG_DIR>/stderr
               

        【这里有个问题没有弄懂】spark.driver.extraLibraryPath(也就是通过--driver-library-path设置的jar包),不会影响AM的CLASSPATH,那么设置LD_LIBRARY_PATH的意义纠结何在?

        从上面的命令可以分析出,通过spark-submit --jars设置的jar包,并不会作为AM的CLASSPATH,那么spark driver能启动,也就说明AM与driver的关系应该如下图:

ApplicationMaster 

//1、AM的入口,主线程
def main(args: Array[String]): Unit = {
    SignalUtils.registerLogger(log)
    val amArgs = new ApplicationMasterArguments(args)
    master = new ApplicationMaster(amArgs)
    System.exit(master.run())
  }

//2、kerberos
final def run(): Int = {
    doAsUser {
      runImpl()
    }
    exitCode
  }

private def runImpl(): Unit = {
      if (isClusterMode) {
        runDriver()
      } else {
        runExecutorLauncher()
      }
  }


private def runDriver(): Unit = {
    addAmIpFilter(None)
    //重点!!!启动spark driver
    userClassThread = startUserApplication()

    // This a bit hacky, but we need to wait until the spark.driver.port property has
    // been set by the Thread executing the user class.
    logInfo("Waiting for spark context initialization...")
    val totalWaitTime = sparkConf.get(AM_MAX_WAIT_TIME)
    try {
      //重点!!!等待sparkContext初始化结束(也就表示driver执行成功),但不会无限等待,默认100S
      val sc = ThreadUtils.awaitResult(sparkContextPromise.future,
        Duration(totalWaitTime, TimeUnit.MILLISECONDS))
      if (sc != null) {
        rpcEnv = sc.env.rpcEnv

        val userConf = sc.getConf
        val host = userConf.get("spark.driver.host")
        val port = userConf.get("spark.driver.port").toInt
        registerAM(host, port, userConf, sc.ui.map(_.webUrl))

        val driverRef = rpcEnv.setupEndpointRef(
          RpcAddress(host, port),
          YarnSchedulerBackend.ENDPOINT_NAME)

        //重点!!!这里面就是负责调用AM端的prometheus sink
        createAllocator(driverRef, userConf)
      }
  }




private def startUserApplication(): Thread = {
    // userClassLoader负责加载了__app__.jar(也就是moduleA.jar)和spark-submit --jars指定的所有jar包
    val mainMethod = userClassLoader.loadClass(args.userClass)
      .getMethod("main", classOf[Array[String]])

    val userThread = new Thread {
      //负责调用moduleA.jar的main方法,初始化sparkContext
    }
    userThread.setContextClassLoader(userClassLoader)
    userThread.setName("Driver")
    userThread.start()
    userThread
  }
/**
* 这里大概介绍一下spark的user jar first。
* 核心在于ChildFirstURLClassLoader extend URLClassloader,
* 这个类的特点是在调用URLClassLoader的构造函数时,parent传入的值为null。
* 而在ChildFirstURLClassLoader内部维护了一个URLClassloader parent的成员变量,
* 后续所有的双亲委派机制,都是自己实现的,
* 首先从ChildFirstURLClassLoader的Resource中加载class,失败后从parent中重试。
*/
private val userClassLoader = {
    val classpath = Client.getUserClasspath(sparkConf)
    val urls = classpath.map { entry =>
      new URL("file:" + new File(entry.getPath()).getAbsolutePath())
    }

    if (isClusterMode) {
      if (Client.isUserClassPathFirst(sparkConf, isDriver = true)) {
        new ChildFirstURLClassLoader(urls, Utils.getContextOrSparkClassLoader)
      } else {
        new MutableURLClassLoader(urls, Utils.getContextOrSparkClassLoader)
      }
    } else {
      new MutableURLClassLoader(urls, Utils.getContextOrSparkClassLoader)
    }
  }

解决ClassNotFound

        到这里我们知道了,AM的默认CLASSPATH是不包含moduleA.jar、moduleB.jar和moduleC.jar的,因此找不到prometheus sink很正常,添加如下配置让AM增加moduleB.jar

spark-submit ... --driver-class-path moduleB.jar --class xxx /moduleA.jar ...

        在最开始的背景部分提到了,moduleA、moduleB和moduleC之间存在类的引用,因此上面配置后,AM的JVM classloader结构如下:

         由于上面的类分布,在ModuleA启动时,会去加载ModuleB的ClassB,而ClassB需要使用ModuleC的ClassC,此时从ClassB去load ClassC时,就会出现ClassC Not Found Exception

        要解决上面的问题,只要保证通过--driver-class-path设置的jar不要引用--jars和main jar就可以了(但是main jar和--jars可以引用--driver-class-path的jar),我的解决方案是将prometheus sink的编码独立成一个jar包。

executor也出现ClassNotFoundException

        原因与AM出现的一样,spark-submit增加下面的配置即可解决。

--conf spark.executor.extraClassPath=spark-prometheus-metrics-1.0.jar

AM和Driver的http server暴露相同的内容

        由于AM和Driver都位于同一个JVM中,虽然AM和Driver的MetricSystem、Sink、Reporter都是独立的实例,但它们将自己的metric都写入了相同的prometheus Collector中(这是由于prometheus的官方推荐写法决定的,官方推荐Collector对象都要作为常量存在),从而导致AM和Driver暴露的两个HttpServer,其返回的内容是一模一样的,为此的解决方案如下

代码结构

PrometheusSink重构

package ;

import com.codahale.metrics.MetricRegistry;
import xxx.reporter.PrometheusReporter;
import io.prometheus.client.exporter.HTTPServer;
import org.apache.commons.lang3.StringUtils;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.RetryForever;
import org.apache.spark.metrics.sink.Sink;
import org.apache.zookeeper.CreateMode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.charset.StandardCharsets;
import java.util.Properties;
import java.util.concurrent.TimeUnit;

/**
 * spark 配置:
 * --conf spark.metrics.conf.*.sink.prometheus.class=xxx.sink.PrometheusSink \
 * --conf spark.metrics.conf.*.sink.prometheus.zkHosts=xxx \
 * --conf spark.metrics.conf.*.sink.prometheus.sessionTimeoutMs=30000 \
 * --conf spark.metrics.conf.*.sink.prometheus.connectTimeoutMs=10000 \
 * --conf spark.metrics.conf.*.sink.prometheus.retryIntervalMs=3000 \
 * --conf spark.metrics.conf.*.sink.prometheus.jobName=BLOOD_TEST1 \
 * --conf spark.executor.extraClassPath=spark-prometheus-metrics-1.0.jar \
 * --driver-class-path spark-prometheus-metrics-1.0.jar \
 *
 *
 * NOTE:在yarn cluster模式下,driver和applicationMaster位于同一个JVM中,
 * 此时sink会被new两次,
 * 但是prometheus的collector却是一个的,因此会导致两个http server的内容
 * 是一模一样的。
 * 解决方案:
 * 只启动一个sink,但由于使用反射构造sink,因此单例模式不可用,
 * 通过volatile来实现多线程下的单例
 * @Author:
 * @Date: 2022/1/17
 * @TIME: 11:08
 */
public class PrometheusSink implements Sink {
    private final static Logger LOG = LoggerFactory.getLogger(PrometheusSink.class);
    private static volatile boolean HAS_ONE_INSTANCE = false;
    private final static String ZK_HOSTS_KEY = "zkHosts";
    private final static String ZK_SESSION_TIMEOUT_MS = "sessionTimeoutMs";
    private final static String ZK_CONNECT_TIMEOUT_MS = "connectTimeoutMs";
    private final static String ZK_RETRY_INTERVAL_MS = "retryIntervalMs";
    private final static String SPARK_JOB_NAME = "jobName";
    private final Properties property;
    private final MetricRegistry metricRegistry;
    private final boolean canRun; //canRun表示允许允许http server,但无论是否canRun,都要允许reporter
    private HTTPServer httpServer;
    private CuratorFramework zkClient;
    private PrometheusReporter prometheusReporter;
    public PrometheusSink(Properties property,
                          MetricRegistry registry,
                          org.apache.spark.SecurityManager securityMgr) {
        this.property = property;
        this.metricRegistry = registry;
        if(!HAS_ONE_INSTANCE) {
            HAS_ONE_INSTANCE = true;
            this.canRun = true;
        }else
            this.canRun = false;
    }

    @Override
    public void start() {
        try {
            String zkHosts = this.property.getProperty(ZK_HOSTS_KEY);
            if (StringUtils.isEmpty(zkHosts))
                throw new RuntimeException("[[instance].sink.prometheus.zkHosts]不能为空");
            int sessionTimeoutMs = Integer.parseInt(this.property
                    .getProperty(ZK_SESSION_TIMEOUT_MS, "30000"));
            int connectTimeoutMs = Integer.parseInt(this.property
                    .getProperty(ZK_CONNECT_TIMEOUT_MS, "10000"));
            int retryInterval = Integer.parseInt(this.property
                    .getProperty(ZK_RETRY_INTERVAL_MS, "3000"));
            String jobName = this.property.getProperty(SPARK_JOB_NAME);
            if (StringUtils.isEmpty(jobName))
                throw new RuntimeException("[[instance].sink.prometheus.jobName]不能为空");
            this.prometheusReporter = new PrometheusReporter(this.metricRegistry,jobName);
            int retryCount = 0;
            //是否允许启动http endpoint
            if(this.canRun) {
                InetSocketAddress inetSocketAddress = new InetSocketAddress(InetAddress.getLocalHost(), 0);
                String url = "http://%s:%d/metrics";
                String ip = null;
                int port;
                while (true) {
                    try {
                        try (ServerSocket serverSocket = new ServerSocket()) {
                            serverSocket.bind(inetSocketAddress);
                            ip = inetSocketAddress.getAddress().getHostAddress();
                            port = serverSocket.getLocalPort();
                            url = String.format(url, ip, port);
                        }
                        this.httpServer = new HTTPServer(port);
                        break;
                    } catch (IOException ioException) {
                        if(++retryCount==3) {
                            throw ioException;
                        }
                    }
                }
                LOG.info("start [Prometheus] metrics sink\nHttp EndPoint:{}",String.format(url,ip, port));

                //注册ZK
                this.zkClient = CuratorFrameworkFactory.newClient(zkHosts,sessionTimeoutMs,connectTimeoutMs,new RetryForever(retryInterval));
                this.zkClient.start();
                LOG.info("启动zk client,zkHosts=[{}],sessionTimeout=[{}] ms,connectTimeout=[{}] ms," +
                                "failOverRetryInterval=[{}] ms",zkHosts,sessionTimeoutMs,connectTimeoutMs,
                        retryInterval);
                this.zkClient.blockUntilConnected();
                String zNodePath = "/waterfall/spark/metrics/promethues/"+jobName+"/config";
                this.zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
                        .forPath(zNodePath,url.getBytes(StandardCharsets.US_ASCII));
            }
        }catch (Exception e) {
            throw new RuntimeException("启动prometheus metrics失败",e);
        }
        this.prometheusReporter.start(15, TimeUnit.SECONDS);
    }

    @Override
    public void stop() {
        if(this.canRun) {
            if(zkClient!=null) {
                zkClient.close();
            }
            if(this.httpServer!=null)
                this.httpServer.close();
        }
        this.prometheusReporter.stop();
    }

    @Override
    public void report() {
        //源码来看,只有在JVM停止前才会触发report
        LOG.info("JVM进程即将停止,会自动调用一次report");
        this.prometheusReporter.report();
    }
}

 PrometheusReporter重构

package ;

import com.codahale.metrics.*;
import xxx.reporter.collector.SingletonCunterCollector;
import xxx.reporter.collector.SingletonGaugeCollector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.SortedMap;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * NOTE:当运行在YARN Cluster模式下时,AM和Driver任务都属于同一个JVM,但是sink会被new两次,
 * 且按照源码来看,第一次new一定是driver,第二次new一定是AM
 *
 * @Author:
 * @Date: 2022/1/17
 * @TIME: 13:53
 */
public class PrometheusReporter extends ScheduledReporter {
    private static final Logger LOG = LoggerFactory.getLogger(PrometheusReporter.class);
    private static final Pattern METRIC_NAME_PATTERN = Pattern.compile("(application_\\d+_\\d+)\\.([a-zA-z]+)\\.(.+)");
    //yarn-AppId||线程角色,例如driver、executor等||spark任务名||spark原生metric name
    private static final String[] LABLE_NAMES = {"applicationId", "role", "jobName", "originName"};
    private static final Map<Class<?>, Field> TIME_FIELD_CACHE;
    static {
        TIME_FIELD_CACHE = new HashMap<>();
        try {
            Field meterField = Timer.class.getDeclaredField("meter");
            meterField.setAccessible(true);
            TIME_FIELD_CACHE.put(Meter.class,meterField);
        } catch (NoSuchFieldException e) {
            LOG.warn("获取Timer的成员属性meter的Field对象失败",e);
        }
        try {
            Field histogramField = Timer.class.getDeclaredField("histogram");
            histogramField.setAccessible(true);
            TIME_FIELD_CACHE.put(Histogram.class,histogramField);
        } catch (NoSuchFieldException e) {
            LOG.warn("获取Timer的成员属性histogram的Field对象失败",e);
        }
    }
    private final String jobName;
    //用于存储origin-metric-name提取出来的string[3]数组,减少重复的正则计算次数
    private final Map<String, String[]> cache = new HashMap<>();

    public PrometheusReporter(MetricRegistry registry, String jobName) {
        super(registry, "prometheus-reporter", MetricFilter.ALL, TimeUnit.SECONDS, TimeUnit.SECONDS);
        this.jobName = this.formatName(jobName);
    }

    @SuppressWarnings("all")
    @Override
    public void report(SortedMap<String, Gauge> gaugeMap,
                       SortedMap<String, Counter> countMap,
                       SortedMap<String, Histogram> histogramMap,
                       SortedMap<String, Meter> meterMap,
                       SortedMap<String, Timer> timerMap) {
        if (gaugeMap != null) {
            for (Map.Entry<String, Gauge> gaugeEntry : gaugeMap.entrySet()) {
                this.gaugeAdaptor(gaugeEntry.getKey(), gaugeEntry.getValue());
            }
        }

        if (countMap != null) {
            for (Map.Entry<String, Counter> counterEntry : countMap.entrySet()) {
                this.counterAdaptor(counterEntry.getKey(), counterEntry.getValue());
            }
        }

        if (histogramMap != null) {
            for (Map.Entry<String, Histogram> histogramEntry : histogramMap.entrySet()) {
                this.histogramAdaptor(histogramEntry.getKey(), histogramEntry.getValue());
            }
        }

        if (meterMap != null) {
            for (Map.Entry<String, Meter> meterEntry : meterMap.entrySet()) {
                this.meterAdaptor(meterEntry.getKey(), meterEntry.getValue());
            }
        }

        if (timerMap != null) {
            for (Map.Entry<String, Timer> timerEntry : timerMap.entrySet()) {
                try {
                    //1、采集meter
                    Field meterField = TIME_FIELD_CACHE.get(Meter.class);
                    if(meterField!=null) {
                        Meter meter = (Meter)meterField.get(timerEntry.getValue());
                        this.meterAdaptor(timerEntry.getKey(),meter);
                    }
                } catch (IllegalAccessException e) {
                    LOG.warn("尝试读取Timer的私有成员属性meter失败",e);
                }

                //2、采集histogram
                try {
                    //1、采集meter
                    Field histogramField = TIME_FIELD_CACHE.get(Histogram.class);
                    if(histogramField!=null) {
                        Histogram histogram = (Histogram)histogramField.get(timerEntry.getValue());
                        this.histogramAdaptor(timerEntry.getKey(),histogram);
                    }
                } catch (IllegalAccessException e) {
                    LOG.warn("尝试读取Timer的私有成员属性histogram失败",e);
                }
            }
        }
    }

    /**
     * spark gauge类型转换为prometheus支持的类型<br/>
     * NOTE: spark gauge类型可能存储非NUMBER类型的数据,这里只采集number类型的
     *
     * @param originMetricName
     * @param originGauge
     */
    private void gaugeAdaptor(String originMetricName, Gauge<?> originGauge) {
        //1、originGauge只能是Number类型的
        Object originValue = originGauge.getValue();
        if (!(originValue instanceof Number))
            return;
        String[] arrContainer = this.analyzeOriginMetricName(originMetricName);
        if (arrContainer.length == 3) {
            String appId = arrContainer[0];
            String role = arrContainer[1];
            String newMetricName = arrContainer[2];
            //prometheus collector采集数据
            SingletonGaugeCollector collector = SingletonGaugeCollector.getInstance(this.jobName);
            collector.setValue(
                    newMetricName,
                    originMetricName,
                    ((Number) originValue).doubleValue(),
                    LABLE_NAMES,
                    appId, role, this.jobName, originMetricName);
        }
    }

    /**
     * spark counter类型转换为prometheus支持的类型
     *
     * @param originMetricName
     * @param originCounter
     */
    private void counterAdaptor(String originMetricName, Counter originCounter) {
        String[] arrContainer = this.analyzeOriginMetricName(originMetricName);
        if (arrContainer.length == 3) {
            String appId = arrContainer[0];
            String role = arrContainer[1];
            String newMetricName = arrContainer[2];
            //prometheus collector采集数据
            SingletonCunterCollector singletonCunterCollector
                    = SingletonCunterCollector.getInstance(this.jobName);
            singletonCunterCollector.incr(
                    newMetricName,
                    originMetricName,
                    originCounter.getCount(),
                    LABLE_NAMES,
                    appId, role, this.jobName, originMetricName);
        }
    }

    /**
     * 将spark histogram转换为prometheus支持的数据类型,这里直接转换为gauge
     *
     * @param originMetricName
     * @param originHistogram
     */
    private void histogramAdaptor(String originMetricName, Histogram originHistogram) {
        String[] arrContainer = this.analyzeOriginMetricName(originMetricName);
        if (arrContainer.length == 3) {
            String appId = arrContainer[0];
            String role = arrContainer[1];
            String newMetricNamePrefix = arrContainer[2];
            //prometheus collector采集数据
            SingletonGaugeCollector collector = SingletonGaugeCollector.getInstance(this.jobName);
            //1、histogram count
            String suffix = "count";
            String newMetricName = newMetricNamePrefix + "_" + suffix;
            collector.setValue(
                    newMetricName,
                    originMetricName + "," + suffix,
                    originHistogram.getCount(),
                    LABLE_NAMES,
                    appId, role, this.jobName, originMetricName + "," + suffix);
            //2、histogram max
            suffix = "max";
            newMetricName = newMetricNamePrefix + "_" + suffix;
            collector.setValue(
                    newMetricName,
                    originMetricName + "," + suffix,
                    originHistogram.getSnapshot().getMax(),
                    LABLE_NAMES,
                    appId, role, this.jobName, originMetricName + "," + suffix);
            //3、histogram mean
            suffix = "mean";
            newMetricName = newMetricNamePrefix + "_" + suffix;
            collector.setValue(
                    newMetricName,
                    originMetricName + "," + suffix,
                    originHistogram.getSnapshot().getMean(),
                    LABLE_NAMES,
                    appId, role, this.jobName, originMetricName + "," + suffix);
            //4、histogram min
            suffix = "min";
            newMetricName = newMetricNamePrefix + "_" + suffix;
            collector.setValue(
                    newMetricName,
                    originMetricName + "," + suffix,
                    originHistogram.getSnapshot().getMin(),
                    LABLE_NAMES,
                    appId, role, this.jobName, originMetricName + "," + suffix);
            //5、histogram p50
            suffix = "p50";
            newMetricName = newMetricNamePrefix + "_" + suffix;
            collector.setValue(
                    newMetricName,
                    originMetricName + "," + suffix,
                    originHistogram.getSnapshot().getMedian(),
                    LABLE_NAMES,
                    appId, role, this.jobName, originMetricName + "," + suffix);
            //6、histogram p75
            suffix = "p75";
            newMetricName = newMetricNamePrefix + "_" + suffix;
            collector.setValue(
                    newMetricName,
                    originMetricName + "," + suffix,
                    originHistogram.getSnapshot().get75thPercentile(),
                    LABLE_NAMES,
                    appId, role, this.jobName, originMetricName + "," + suffix);
            //7、histogram p95
            suffix = "p95";
            newMetricName = newMetricNamePrefix + "_" + suffix;
            collector.setValue(
                    newMetricName,
                    originMetricName + "," + suffix,
                    originHistogram.getSnapshot().get95thPercentile(),
                    LABLE_NAMES,
                    appId, role, this.jobName, originMetricName + "," + suffix);
            //8、histogram p98
            suffix = "p98";
            newMetricName = newMetricNamePrefix + "_" + suffix;
            collector.setValue(
                    newMetricName,
                    originMetricName + "," + suffix,
                    originHistogram.getSnapshot().get98thPercentile(),
                    LABLE_NAMES,
                    appId, role, this.jobName, originMetricName + "," + suffix);
            //9、histogram p99
            suffix = "p99";
            newMetricName = newMetricNamePrefix + "_" + suffix;
            collector.setValue(
                    newMetricName,
                    originMetricName + "," + suffix,
                    originHistogram.getSnapshot().get99thPercentile(),
                    LABLE_NAMES,
                    appId, role, this.jobName, originMetricName + "," + suffix);
            //10、histogram p999
            suffix = "p999";
            newMetricName = newMetricNamePrefix + "_" + suffix;
            collector.setValue(
                    newMetricName,
                    originMetricName + "," + suffix,
                    originHistogram.getSnapshot().get999thPercentile(),
                    LABLE_NAMES,
                    appId, role, this.jobName, originMetricName + "," + suffix);
            //11、histogram stddev
            suffix = "stddev";
            newMetricName = newMetricNamePrefix + "_" + suffix;
            collector.setValue(
                    newMetricName,
                    originMetricName + "," + suffix,
                    originHistogram.getSnapshot().getStdDev(),
                    LABLE_NAMES,
                    appId, role, this.jobName, originMetricName + "," + suffix);
        }
    }

    private void meterAdaptor(String originMetricName, Meter originMeter) {
        String[] arrContainer = this.analyzeOriginMetricName(originMetricName);
        if (arrContainer.length == 3) {
            String appId = arrContainer[0];
            String role = arrContainer[1];
            String newMetricNamePrefix = arrContainer[2];
            //prometheus collector采集数据
            SingletonGaugeCollector collector = SingletonGaugeCollector.getInstance(this.jobName);
            //1、meter count
            String suffix = "count";
            String newMetricName = newMetricNamePrefix + "_" + suffix;
            collector.setValue(
                    newMetricName,
                    originMetricName + "," + suffix,
                    originMeter.getCount(),
                    LABLE_NAMES,
                    appId, role, this.jobName, originMetricName + "," + suffix);
            //2、
            suffix = "ratePer1m";
            newMetricName = newMetricNamePrefix + "_" + suffix;
            collector.setValue(
                    newMetricName,
                    originMetricName + "," + suffix,
                    originMeter.getOneMinuteRate(),
                    LABLE_NAMES,
                    appId, role, this.jobName, originMetricName + "," + suffix);
            //3、
            suffix = "ratePer5m";
            newMetricName = newMetricNamePrefix + "_" + suffix;
            collector.setValue(
                    newMetricName,
                    originMetricName + "," + suffix,
                    originMeter.getFiveMinuteRate(),
                    LABLE_NAMES,
                    appId, role, this.jobName, originMetricName + "," + suffix);
            //4、
            suffix = "ratePer15m";
            newMetricName = newMetricNamePrefix + "_" + suffix;
            collector.setValue(
                    newMetricName,
                    originMetricName + "," + suffix,
                    originMeter.getFifteenMinuteRate(),
                    LABLE_NAMES,
                    appId, role, this.jobName, originMetricName + "," + suffix);
            //5、
            suffix = "meanRate";
            newMetricName = newMetricNamePrefix + "_" + suffix;
            collector.setValue(
                    newMetricName,
                    originMetricName + "," + suffix,
                    originMeter.getMeanRate(),
                    LABLE_NAMES,
                    appId, role, this.jobName, originMetricName + "," + suffix);
        }
    }

    /**
     * 格式化字符串,将“.”都转换为"_"
     *
     * @param originVal
     * @return
     */
    private String formatName(String originVal) {
        return originVal.replaceAll("\\.", "_");
    }

    /**
     * 解析spark metric name,提取出其中的信息
     *
     * @param originMetricName
     * @return {applicationId,role,originMetricName去掉applicationId和role}<br/>
     * NOTE:String[2]需要进行字符串替换,将"."都替换为"_"
     */
    private String[] analyzeOriginMetricName(String originMetricName) {
        String[] arrContainer = null;
        if ((arrContainer = this.cache.get(originMetricName)) == null) {
            Matcher matcher = METRIC_NAME_PATTERN.matcher(originMetricName);
            //全词匹配,这个regex一定满足一个完整的name的要求
            if (matcher.matches()) {
                arrContainer = new String[]{matcher.group(1), matcher.group(2), this.formatName(matcher.group(3))};
            } else {
                arrContainer = new String[0];
            }
            this.cache.put(originMetricName, arrContainer);
        }
        return arrContainer;
    }
}

单例的Prometheus Collector

package ;

import io.prometheus.client.Gauge;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @Author: 
 * @Date: 2022/2/9
 * @TIME: 10:56
 */
public class SingletonGaugeCollector {
    private static SingletonGaugeCollector INSTANCE = null;
    private static volatile boolean initFlag = false;
    private final String jobName;
    //key=metricname
    private final Map<String,Gauge> gaugeMap = new ConcurrentHashMap<>();
    private SingletonGaugeCollector(String jobName) {
        this.jobName = jobName+"_";
    }

    /**
     * 获取 prometheus Gauge Collector的单例对象
     * @param jobName spark 任务名称
     * @return
     */
    public static SingletonGaugeCollector getInstance(String jobName) {
        if(INSTANCE==null) {
            synchronized (SingletonGaugeCollector.class) {
                if(INSTANCE==null) {
                    INSTANCE = new SingletonGaugeCollector(jobName);
                    initFlag = true;
                }
            }
        }
        return INSTANCE;
    }

    /**
     * 采集数据
     * @param metricName metric name
     * @param help
     * @param val metric value
     * @param lableNames metric lables
     * @param lableValues metric lables values
     */
    public void setValue(String metricName,String help,double val,String[] lableNames,String... lableValues) {
        for(;;) {
            if(this.gaugeMap.containsKey(metricName)) {
                this.gaugeMap.get(metricName).labels(lableValues)
                        .set(val);
                break;
            }else {
                synchronized (this) {
                    if(!this.gaugeMap.containsKey(metricName)) {
                        Gauge gauge = Gauge.build(
                                Contant.COLLECTOR_NAME_PREFIX + jobName+metricName,
                                help)
                                .labelNames(lableNames).register();
                        this.gaugeMap.put(metricName,gauge);
                    }
                }
            }
        }
    }
}

运行结果

1个AM,1个Driver,1个Executor实例,共2个httpServer,其中一个包含了AM和Driver的metrics信息,符合要求。 

Executor进程的metrics信息为空

        原因处在PrometheusReporter的正则表达式上,executor的role部分(也就是正则表达式的group(2)),是一个数字,而原来的正则表达式只能匹配非数字的role,修改为[a-zA-z0-9]即可。

        还有一点需要注意,executor的metric name中,有形如"applicationId_xxx_xxx.1.A.B-B1.C"的字符串,其中"-"符号对于prometheus来说,是非法的,需要替换掉。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值