任务监控

 

一、 Spark Web UI 

spark 如何查看yarn 提交的任务 查看spark运行日志_kafka

对于 Spark Streaming 任务的监控可以直观的通过 Spark Web UI ,该页面包括 Input Rate, Scheduling Delay、Processing Time 等,但是这种方法运维成本较高,需要人工不间断的巡视。

这其中包括接受的记录数量,每一个batch内处理的记录数,处理时间,以及总共消耗的时间。

 在上述参数之中最重要的两个参数分别是Porcessing Time 以及 Scheduling Delay

 Porcessing Time  用来统计每个batch内处理数据所消费的时间

 Scheduling Delay 用来统计在等待被处理所消费的时间

 如果PT比SD大,或者SD持续上升,这就表明此系统不能对产生的数据实时响应,换句话来说就是,出现了处理时延,每个batch time 内的处理速度小于数据的产生速度。

在这种情况下,读者需要想法减少数据的处理速度,即需要提升处理效率。 

spark streaming应用程序,放到线上后,怎么监测spark streaming程序的阻塞状态, 运行情况,虽然spark 提供了spark webUI去查看,但是作为开发人员总不能天天去看spark webUI页面吧, 去官网看,貌似可以通过请求spark 自带的jetty 服务器可以获取相关检测统计信息,


2.命令行版

yarn application -list

所有状态:ALL,NEW,NEW_SAVING,SUBMITTED,ACCEPTED,RUNNING,FINISHED,FAILED,KILLED多个用","分隔;
yarn application -list -appStates RUNNING

yarn logs -applicationId application_1517538889175_2550 > logs.txt
通过vim进行查看logs.txt文件

-status 列出 某个application 的状态
yarn application -status application_1526100291229_206393

yarn application -kill application_1526100291229_206393

yarn node -list 查看节点清单,显示数据节点Data Node列表

yarn queue -status 队列名称

yarn top 查看集群资源使用信息

 Spark 的日志

基本上每个程序都要有日志记录的功能,日志的记录最重要的用处就是监控程序的运行状态和错误查找。如果程序没有写日志,那程序就像瞎子一样,瞎跑,你都不知道为什么数据不对,为什么程序报错,程序运行成功还是失败。

在流式计算Sparkstreaming中,日志的记录是非常重要的;因为流式计算是需要7*24小时一直运行的,你不记日志你根本不知道程序运行成功还是错误(当然你可以通过spark的history来查看),但是sparkstreaming的日志它只记录程序的内部错误,并不会记录你程序的计算逻辑错误(所以会导致你整个程序计算结果都不对了都不知道)。

SparkStreming的日志是比较特别的,特别之处在于,它的日志只有在运行完成之后才能被load到本地查看的,不然它的日志是一直在hdfs上的。因为sparkstreaming程序永远不停机,就算你开启hadoop的log聚合也没用,只有当sparkstreaming程序停掉,hadoop的log聚合才能把所有的log收集到一个目录里面,所以其他的非sparkstreaming程序,比如MR,Spark 运行完后,如果开启log聚合,hadoop会负责把运行在各个节点上的log给统一收集到HDFS上。

spark on yarn应用在运行时和完成后日志的存放位置是不同的,一般运行时是存放在各个运行节点,完成后会归集到hdfs。

当这个application正常或者由于某种原因异常结束后,yarn默认会将所有日志归集到hdfs上,所以yarn也提供了一个查询已结束application日志的方法,即

spark on yarn模式,如果你的hadoop集群有100台,那么意味着你的sparkstreaming的log有可能会随机分布在100台中,你想查看log必须登录上每台机器上,一个个查看,如果通过Hadoop的8088页面查看,你也得打开可能几十个页面才能看到所有的log,那么问题来了? 

能不能将这个job运行所有的log统一收集到某一个目录里面呢? 如果收集到一起的话排查log就非常方便了。 

那么,如果我们需要一个这样的功能:需要一个监控程序来监控Spark的运行情况,并且一旦程序出问题要马上发邮件到自己的邮箱,然后自己重启程序。

二、SparkStreaming的log4j日志记录

为什么我们要写日志 

解决的思路

1、简单的思路:把SparkStreaming的运行模式选为yarn-client模式,那么程序的driver就会是在固定的机子上,你可以选择把日志都写在这台机子上,然后在这台机子上写一个脚本来监控这个日志文件(tail -f 文件路径);你可以一直看到你日志的写出情况。

当遇到一个error日志的时候你就可以选择处理方法。这个方法的缺点是:如果程序以yarn-client模式运行的话,而且写脚本。

2、复杂一点的思路:使用log4j收集日志,然后异步发送至kafka里面,供下层消费,这个方法的好处就是,程序和日志解耦。程序只管写他的日志,也不管写什么机子,只要给他一个brokerlist。日志也都记录在kafka,你可以写一个简单的java程序来监控它。spark照样可以使用yarn-cluster模式。

使用自定义的log4j配置

通常一个集群会有很多个的spark任务,每个任务都会记日志,如果都使用spark默认的日志配置文件,那将会很难控制和维护。所以需要使用自定义的log4j配置。

在默认情况下,Spark应用程序的日志级别是INFO的,我们可以自定义Spark应用程序的日志输出级别,可以到$SPARK_HOME/conf/log4j.properties(spark安装目录下面的log4j文件 文件里面进行修改,(也可指定到某一目录)

log4j.rootLogger=WARN,console,kafka  
  
#log4j.logger.com.demo.kafka=DEBUG,kafka  
# appender kafka  
log4j.appender.kafka=kafka.producer.KafkaLog4jAppender  
log4j.appender.kafka.topic=kp_diag_log  
# multiple brokers are separated by comma ",".  
log4j.appender.kafka.brokerList=192.168.201.6:9092,192.168.201.7:9092,192.168.201.8:9092  
log4j.appender.kafka.compressionType=none  
log4j.appender.kafka.syncSend=false  
log4j.appender.kafka.layout=org.apache.log4j.PatternLayout  
#log4j.appender.kafka.layout.ConversionPattern=%d [%-5p] [%t] - [%l] %m%n  
log4j.appender.kafka.layout.ConversionPattern=[%d] [%p] [%t] %m%n  
  
# appender console  
log4j.appender.console=org.apache.log4j.ConsoleAppender  
log4j.appender.console.target=System.out  
log4j.appender.console.layout=org.apache.log4j.PatternLayout  
log4j.appender.console.layout.ConversionPattern=[%d] [%p] [%t] %m%n  
#log4j.appender.console.layout.ConversionPattern=%d [%-5p] [%t] - [%l] %m%n

自定义的log4j配置。参数将自定义的配置文件上传到应用程序的文件列表中。自己写一个log4j.properties然后在运行程序的时候指定用这个配置文件就好了

你自己写一个log4j.properties然后在运行程序的时候指定用这个配置文件就好了。yarn的运行有两种模式,两种的指定方式不一样这样Spark应用程序在运行的时候会打出WARN级别的日志,然后在提交Spark应用程序的时候使用--files参数指定上面的log4j.properties文件路径即可使用这个配置打印应用程序的日志。

#yarn-client:
–conf “spark.driver.extraJavaOptions=-Dlog4j.configuration=file:///data/test/log4j-spark.properties”

#  --files 参数将自定义的配置文件上传到应用程序的文件列表中
--driver-java-options "-Dlog4j.configuration=log4j-driver.properties" 
--conf spark.executor.extraJavaOptions="-Dlog4j.configuration=log4j-executor.properties" 
--files /home/hadoop/spark-workspace/log4j-driver.properties,/home/hadoop/spark-workspace/log4j-executor.properties 
 


#yarn-cluster:
–files /data/lmq/test/log4j-spark.properties 
–conf “spark.driver.extraJavaOptions=-Dlog4j.configuration=log4j-spark.properties” 
–conf “spark.executor.extraJavaOptions=-Dlog4j.configuration=log4j-spark.properties”

#不使用  --files 参数上传文件,直接使用文件。
--driver-java-options "-Dlog4j.configuration=file:/home/hadoop/spark-workspace/log4j-driver.properties " 
--conf spark.executor.extraJavaOptions="-Dlog4j.configuration=file:/home/hadoop/spark-workspace/log4j-executor.properties"

jars=`echo /home/spark/x_spark_job/streaming_lib/*jar | sed 's/ /,/g'`  
  
echo $jars  
  
#nohup /opt/bigdata/spark/bin/spark-submit  --class  com.bigdata.xuele.streaming.SparkStreamingKmd  --master yarn    --deploy-mode cluster --executor-cores 3  --driver-memory 4g   --executor-memory 4g  --num-executors 10  --conf "spark.executor.extraJavaOptions=-Dlog4j.configuration=logback.xml"   --jars  $jars    kpdiag-stream-1.0.0-SNAPSHOT.jar  &> streaming.log  &  
  
nohup /opt/bigdata/spark/bin/spark-submit    --class  com.bigdata.xuele.streaming.SparkStreamingKmd  --master yarn  --deploy-mode cluster \  
 --files "/home/spark/x_spark_job/log4j.properties" \  
 --executor-cores 3   --driver-memory 3g   --executor-memory 3g  --num-executors 12    --jars  $jars  \  
 --conf "spark.driver.extraJavaOptions=-Dlog4j.configuration=log4j.properties"   \  
 --driver-class-path /opt/bigdata/jars/spark/kafka-log4j-appender-0.9.0.0.jar:/opt/bigdata/jars/spark/kafka_2.11-0.8.2.1.jar:/opt/bigdata/jars/spark/metrics-core-2.2.0.jar:/opt/bigdata/jars/spark/kafka-clients-0.8.2.1.jar \  
 --driver-library-path /opt/bigdata/jars/spark/kafka-log4j-appender-0.9.0.0.jar:/opt/bigdata/jars/spark/kafka_2.11-0.8.2.1.jar:/opt/bigdata/jars/spark/metrics-core-2.2.0.jar:/opt/bigdata/jars/spark/kafka-clients-0.8.2.1.jar  \  
 --conf spark.executor.extraClassPath=/opt/bigdata/jars/spark/kafka_2.11-0.8.2.1.jar:/opt/bigdata/jars/spark/metrics-core-2.2.0.jar:/opt/bigdata/jars/spark/kafka-clients-0.8.2.1.jar   \  
 --conf spark.executor.extraLibraryPath=/opt/bigdata/jars/spark/kafka_2.11-0.8.2.1.jar:/opt/bigdata/jars/spark/metrics-core-2.2.0.jar:/opt/bigdata/jars/spark/kafka-clients-0.8.2.1.jar  \  
 kpdiag-stream-1.0.0-SNAPSHOT.jar &> kp.log &


这里需要注意一点,sparkstreaming运行时候,系统本身也有大量的log,如果把这个系统log也收集到kafka里面本身的量是非常大的,而且好多信息不重要,其实 我们只需要关注业务重点log即可,主要是WARN+ERROR级别的,调试的时候可以把info级别打开,代码里重点关注的log都放在warn级别,异常什么的放在ERROR即可, 这样排查问题时候也容易而且了避免了大量log的产生从应用本身性能的影响。 

提交任务后,在kafka的节点上执行消费者命令就能看到对应的log输出:  

kafka-console-consumer --zookeeper 192.168.201.5:2181 --topic kp_diag_log
收集到的log内容如下:

[2018-01-21 16:37:03,154] [WARN] [Driver] Support for Java 7 is deprecated as of Spark 2.0.0  
[2018-01-21 16:37:19,714] [WARN] [Executor task launch worker-2] 非客观题跳过:类型:0  
[2018-01-21 16:37:19,738] [WARN] [Executor task launch worker-2] 非客观题跳过:类型:0  
[2018-01-21 16:37:19,739] [WARN] [Executor task launch worker-2] 非客观题跳过:类型:0  
[2018-01-21 16:37:19,738] [WARN] [Executor task launch worker-2] 非客观题跳过:类型:0  
[2018-01-21 16:37:19,739] [WARN] [Executor task launch worker-2] 非客观题跳过:类型:0  
[2018-01-21 16:37:19,740] [WARN] [Executor task launch worker-2] 非客观题跳过:类型:0  
[2018-01-21 16:37:19,738] [WARN] [Executor task launch worker-2] 非客观题跳过:类型:0  
[2018-01-21 16:37:19,739] [WARN] [Executor task launch worker-2] 非客观题跳过:类型:0  
[2018-01-21 16:37:19,842] [WARN] [Executor task launch worker-0] 题目id:b07e88feff464659ab5a351bf1e68ee0在redis不存在

三、基于 Flume+Kafka+Spark Streaming 实现实时监控输出日志的报警系统

运用场景:

我们机器上每天或者定期都要跑很多任务,很多时候任务出现错误不能及时发现,导致发现的时候任务已经挂了很久了。 

解决方法:

基于 Flume+Kafka+Spark Streaming 的框架对这些任务的输出日志进行实时监控,当检测到日志出现Error的信息就发送邮件给项目的负责人。 

1、Flume

Flume是用来收集、汇聚并且传输日志数据Kafka去。可以设置多个sources对应多个任务的日志,到一个kafka sinks。配置文件如下:

#define agent	
agent_log.sources = s1 s2                                                                                                                  
agent_log.channels = c1                                                                                                                 
agent_log.sinks = k1                                                                                                                    
                  
#define sources.s1																      
agent_log.sources.s1.type=exec                                                                                                          
agent_log.sources.s1.command=tail -F /data/log1.log 
 
#define sources.s2																      
agent_log.sources.s2.type=exec                                                                                                          
agent_log.sources.s2.command=tail -F /data/log2.log  
 
#定义拦截器
agent_log.sources.s1.interceptors = i1
agent_log.sources.s1.interceptors.i1.type = static
agent_log.sources.s1.interceptors.i1.preserveExisting = false
agent_log.sources.s1.interceptors.i1.key = projectName
agent_log.sources.s1.interceptors.i1.value= project1
 
agent_log.sources.s2.interceptors = i2
agent_log.sources.s2.interceptors.i2.type = static
agent_log.sources.s2.interceptors.i2.preserveExisting = false
agent_log.sources.s2.interceptors.i2.key = projectName
agent_log.sources.s2.interceptors.i2.value= project2
                                                                                                                                                                                                                                                                                                                                   
#define channels
agent_log.channels.c1.type = memory
agent_log.channels.c1.capacity = 1000
agent_log.channels.c1.transactionCapacity = 1000
 
#define sinks
#设置Kafka接收器
agent_log.sinks.k1.type= org.apache.flume.sink.kafka.KafkaSink
#设置Kafka的broker地址和端口号
agent_log.sinks.k1.brokerList=cdh1:9092,cdh2:9092,cdh3:9092
#设置Kafka的Topic
agent_log.sinks.k1.topic=result_log
#包含header
agent_log.sinks.k1.useFlumeEventFormat = true
#设置序列化方式
agent_log.sinks.k1.serializer.class=kafka.serializer.StringEncoder
agent_log.sinks.k1.partitioner.class=org.apache.flume.plugins.SinglePartition
agent_log.sinks.k1.partition.key=1
agent_log.sinks.k1.request.required.acks=0
agent_log.sinks.k1.max.message.size=1000000
agent_log.sinks.k1.agent_log.type=sync
agent_log.sinks.k1.custom.encoding=UTF-8
 
# bind the sources and sinks to the channels                                                                      
agent_log.sources.s1.channels=c1    
agent_log.sources.s2.channels=c1  
agent_log.sinks.k1.channel=c1

执行flume-ng命令启动flume:

flume-ng agent -c /etc/flume-ng/conf -f result_log.conf -n agent_log

2、Kafka

Kafka是一个消息系统,可以缓冲消息。Flume收集的日志传送到Kafka消息队列中(Flume作为生产者),然后就可以被Spark Streaming消费了,而且可以保证不丢失数据。

#创建result_log主题
kafka-topics --zookeeper cdh1:2181,cdh1:2181,cdh3:2181 --create --topic result_log --partitions 3 --replication-factor 1

#测试-查看kafka主题列表,观察result_log是否创建成功
kafka-topics --list --zookeeper cdh1:2181,cdh1:2181,cdh3:2181

#测试-启动一个消费者测试flume传输日志到kafka这一环节是否正常运行
kafka-console-consumer --bootstrap-server cdh1:9092,cdh1:9092,cdh3:9092 --topic result_log

3、Spark Streaming  

我们用Zookeeper来管理spark streaming 消费者的offset。调用

KafkaUtils.createDirectStream[String, String](ssc, PreferConsistent, Subscribe[String, String](topics, kafkaParams, newOffset))
与kafka建立连接,返回InputDStream,获取数据流,    

stream.foreachRDD(rdd => {

//处理程序

})

发送邮件的功能配置org.apache.commons.mail这个包的 HtmlEmail 这个类,调用 HtmlEmail.send 发送邮件。

编写一个start.sh脚本启动 Spark Streaming 程序,最后 sh start.sh 启动脚本。

#!/bin/bash
export HADOOP_USER_NAME=hdfs
spark2-submit \
--master yarn \
--deploy-mode client \
--executor-cores 3 \
--num-executors 10 \
--driver-memory  2g \
--executor-memory 1G \
--conf spark.default.parallelism=30 \
--conf spark.storage.memoryFraction=0.5 \
--conf spark.shuffle.memoryFraction=0.3 \
--conf spark.reducer.maxSizeInFlight=128m \
--driver-class-path mysql-connector-java-5.1.38.jar \
--jars  mysql-connector-java-5.1.38.jar,qqwry-java-0.7.0.jar,fastjson-1.2.47.jar,spark-streaming-kafka-10_2.11-2.2.0.jar,hive-hbase-handler-1.1.0-cdh5.13.0.jar,commons-email-1.5.jar,commons-email-1.5-sources.jar,mail-1.4.7.jar \
--class com.lin.monitorlog.mianer.Handler \
monitorLog.jar

以上介绍日志的监控,当Spark任务spark streaming 发生阻塞 的时候,可以发送邮件

二、SparkListener与StreamingListener

   spark 提供了一系列整个任务生命周期中各个阶段变化的事件监听机制 通过这一机制可以在任务的各个阶段做一些自定义的各种动作, SparkListener与StreamingListener(StreamingListener是对SparkStream进行监控)便是这些阶段的事件监听接口类 通过实现这个类中的各种方法便可实现自定义的事件处理动作。 

 对spark任务的各种事件做相应的操作,嵌入回调代码。

       比如:你可以在sparkListener中的onApplicationStart方法中做driver端的第三方框架的连接池初始化(连接仅限driver端使用)以及其他变量的初始化,并放置到公共对象中,driver端直接就可以使用。且在onApplicaionComple方法中做连接的释放工作,以及变量的收集持久化操作,以次达到隐藏变量初始化的操作,做成公共jar包供其它人使用。

      又如:你可以在StreamingListener的onbatchStart操作中获取kafka读取的offset位置以及读取数据条数,在onBatchCompl方法中将这些offset信息保存到mysql/zk中,达到优雅隐藏容错代码的目的。同样可以做成公共jar共其他项目使用。

性能分析

       在使用过程中,大家可能比较关系另外一个问题:指标收集,会对流式计算性能产生多大的影响?

       答案就是,在指标收集这一块,对于流式计算或者spark core产生的影响会很小。因为即使你不收集SparkUI也会收集,这些指标一样会生成。只是对于driver端的开销会稍微变大,如果在流式计算场景可能需要你调大driver端的cpu和内存
 

SparkListener
使用方法:
       sparkListener是一个接口,我们使用时需要自定义监控类实现sparkListener接口中的各种抽象方法,SparkListener 下各个事件对应的函数名非常直白,即如字面所表达意思。 想对哪个阶段的事件做一些自定义的动作,变继承SparkListener实现对应的函数即可,这些方法会帮助我监控spark运行时各个阶段的数据量,从而我们可以获得这些数据量,具体的抽象方法下面一一介绍。

SparkListener的抽象方法:

abstract class SparkListener extends SparkListenerInterface {
  //阶段完成时触发的事件
  override def onStageCompleted(stageCompleted: SparkListenerStageCompleted): Unit   = { }
  //阶段提交时触发的事件
  override def onStageSubmitted(stageSubmitted: SparkListenerStageSubmitted): Unit   = { }
  //任务启动时触发的事件
  override def onTaskStart(taskStart: SparkListenerTaskStart): Unit = { }
  //下载任务结果的事件
  override def onTaskGettingResult(taskGettingResult: SparkListenerTaskGettingResult): Unit = { }
  //任务结束的事件
  override def onTaskEnd(taskEnd: SparkListenerTaskEnd): Unit = { }
  //job启动的事件
  override def onJobStart(jobStart: SparkListenerJobStart): Unit = { }
  //job结束的事件
  override def onJobEnd(jobEnd: SparkListenerJobEnd): Unit = { }
  //环境变量被更新的事件
  override def onEnvironmentUpdate(environmentUpdate:SparkListenerEnvironmentUpdate): Unit = { }
  //块管理被添加的事件
  override def onBlockManagerAdded(blockManagerAdded: 
  SparkListenerBlockManagerAdded): Unit = { }
  override def onBlockManagerRemoved(  blockManagerRemoved: SparkListenerBlockManagerRemoved): Unit = { }
  //取消rdd缓存的事件
  override def onUnpersistRDD(unpersistRDD: SparkListenerUnpersistRDD): Unit = { }
  //app启动的事件
  override def onApplicationStart(applicationStart: SparkListenerApplicationStart):  Unit = { }
  //app结束的事件 [以下各事件也如同函数名所表达各个阶段被触发的事件不在一一标注]
  override def onApplicationEnd(applicationEnd: SparkListenerApplicationEnd): Unit  = { } 
  override def onExecutorMetricsUpdate( executorMetricsUpdate: SparkListenerExecutorMetricsUpdate): Unit = { }
  override def onExecutorAdded(executorAdded: SparkListenerExecutorAdded): Unit = {  }
  override def onExecutorRemoved(executorRemoved: SparkListenerExecutorRemoved):  Unit = { }
  override def onExecutorBlacklisted( executorBlacklisted: SparkListenerExecutorBlacklisted): Unit = { }
  override def onExecutorUnblacklisted( executorUnblacklisted: SparkListenerExecutorUnblacklisted): Unit = { }
  override def onNodeBlacklisted( nodeBlacklisted: SparkListenerNodeBlacklisted): Unit = { }
  override def onNodeUnblacklisted( nodeUnblacklisted: SparkListenerNodeUnblacklisted): Unit = { }
  override def onBlockUpdated(blockUpdated: SparkListenerBlockUpdated): Unit = { }
  override def onOtherEvent(event: SparkListenerEvent): Unit = { }
}

StreamingListener
     StreamingListener 是一个特质,是针对spark streaming的各个阶段的事件监听机制 在用法上跟SparkListener很类似,但是有些细节区别。使用时和sparkListner一样,需要监听spark streaming中各个阶段的事件只需实现这个特质中对应的事件函数即可。

trait StreamingListener {
  /** streaming 启动的事件 */
  def onStreamingStarted(streamingStarted: StreamingListenerStreamingStarted) { }
  /** 接收启动事件 */
  def onReceiverStarted(receiverStarted: StreamingListenerReceiverStarted) { }
  /** Called when a receiver has reported an error */
  def onReceiverError(receiverError: StreamingListenerReceiverError) { }
  /** Called when a receiver has been stopped */
  def onReceiverStopped(receiverStopped: StreamingListenerReceiverStopped) { }
  /** Called when a batch of jobs has been submitted for processing. */
  /** 每个批次提交的事件 */
  def onBatchSubmitted(batchSubmitted: StreamingListenerBatchSubmitted) { }
  /** 每个批次启动的事件 */
  def onBatchStarted(batchStarted: StreamingListenerBatchStarted) { }
  /** 每个批次完成的事件  */
  def onBatchCompleted(batchCompleted: StreamingListenerBatchCompleted) { }
  /** Called when processing of a job of a batch has started. */
  def onOutputOperationStarted(outputOperationStarted: StreamingListenerOutputOperationStarted) { }
  /** Called when processing of a job of a batch has completed. */
  def onOutputOperationCompleted(outputOperationCompleted: StreamingListenerOutputOperationCompleted) { }
}

val spark:SparkSession=null
    val ssc:StreamingContext=null

    /*注册streamingListnener*/
    ssc.addStreamingListener(new MyStreamingListener)
    /*注册sparkListener*/
    spark.sparkContext.addSparkListener(new MySparkListener)

    /*自定义streamingListener*/
    class MyStreamingListener extends StreamingListener{
        //TODO 重载内置方法
    }

    /*自定义SparkListnener*/
    class MySparkListener extends SparkListener {
        //TODO 重载内置方法
    }

案例一

 实时监控spark Streaming运行时的延迟时间,每个批次处理的条数,然后将这些实时数据插入到influxDB数据进行实时展示。

/**
  * 自定义spark监控类实现获得延迟和批次条数的方法
  */
class JobListener(appId: String) extends StreamingListener {
  override def onBatchCompleted(batchCompleted: StreamingListenerBatchCompleted): 
  Unit = {
    //调度延迟,单位:毫秒
    val actualSchedulingDelay: Long = batchCompleted.batchInfo.schedulingDelay.get
    //处理记录数
    val numRecords: Long = batchCompleted.batchInfo.numRecords
    val processingDelay = batchCompleted.batchInfo.processingDelay.get
    JobListener.insert(appId, actualSchedulingDelay, numRecords, processingDelay)

//实时监控spark Streaming运行时的 执行时间:   调度延时 ,然后将这些实时数据输出日志。
    val batchInfo = batchCompleted.batchInfo
    val execTime = batchInfo.processingDelay.getOrElse(0L)
    val schedulingTime = batchInfo.schedulingDelay.getOrElse(0L)
    logInfo(s"执行时间: $execTime 调度延时 : $schedulingTime")
    }
}
 
object JobListener {
  private val config: Config = ConfigFactory.load()
 
  val resourceAsStream: InputStream = classOf[SendMail].getClassLoader.getResourceAsStream("conf.properties")
  val prop = new Properties
  prop.load(resourceAsStream)
 
  val influxDbUtil = new InfluxDbUtil(prop.getProperty("influx_db.url"), 
  prop.getProperty("influx_db.user"), prop.getProperty("influx_db.pwd"), 
  prop.getProperty("influx_db.database"))
  //val influxDbUtil = new InfluxDbUtil(config.getString("influx_db.url"), 
  config.getString("influx_db.user"), config.getString("influx_db.pwd"), 
  config.getString("influx_db.database"))
  influxDbUtil.setInfluxDB(influxDbUtil.getInfluxDB)
  influxDbUtil.setMeasurement(prop.getProperty("influx_db.measurement"))
  influxDbUtil.setDatabase(influxDbUtil.getDatabase)
 
  def insert(appIdName: String, actualSchedulingDelay: Long, numRecords: Long, 
  processingDelay: Long): Unit = {
    //标签
    val tagMap = new util.HashMap[String, String]
    //字段
    val fieldMap = new util.HashMap[String, String]
    tagMap.put("app_id", appIdName)
    fieldMap.put("delay", actualSchedulingDelay.toString)
    fieldMap.put("num_records", numRecords.toString)
    fieldMap.put("processing_delay", processingDelay.toString)
 
    influxDbUtil.insert(influxDbUtil.getMeasurement,tagMap,fieldMap)
  }
}

示例代码应用

//streamingListener不需要在配置中设置,可以直接添加到streamingContext中
object My{
    def main(args : Array[String]) : Unit = {
        val sparkConf = new SparkConf()
        val ssc = new StreamingContext(sparkConf,Seconds(20))
        ssc.addStreamingListener(new JobListener(ssc))
        ....
    }
}

案例二、当发生阻塞的时候,可以发送邮件,以下实现比较简单

class BJJListener(private val appName:String, private val duration: Int) extends StreamingListener{

  private val logger = LoggerFactory.getLogger("BJJListener")

  override def onReceiverStarted(receiverStarted: StreamingListenerReceiverStarted): Unit = {
    super.onReceiverStarted(receiverStarted)
  }

  override def onReceiverError(receiverError: StreamingListenerReceiverError): Unit = super.onReceiverError(receiverError)

  override def onReceiverStopped(receiverStopped: StreamingListenerReceiverStopped): Unit = super.onReceiverStopped(receiverStopped)

  override def onBatchSubmitted(batchSubmitted: StreamingListenerBatchSubmitted): Unit = {
    super.onBatchSubmitted(batchSubmitted)
    val batchInfo = batchSubmitted.batchInfo
    val batchTime = batchInfo.batchTime
    logger.info("BJJListener  batchTime : ", batchTime)
  }

  override def onBatchStarted(batchStarted: StreamingListenerBatchStarted): Unit = {
    val batchInfo = batchStarted.batchInfo
    val processingStartTime = batchInfo.processingStartTime
    logger.info("BJJListener  processingStartTime : ", processingStartTime)
  }

  override def onBatchCompleted(batchCompleted: StreamingListenerBatchCompleted): Unit = {
    val batchInfo = batchCompleted.batchInfo
    val processingStartTime = batchCompleted.batchInfo.processingStartTime

    val processingEndTime = batchInfo.processingEndTime
    val processingDelay = batchInfo.processingDelay
    val totalDelay = batchInfo.totalDelay

    if(totalDelay.get >= 6 * duration * 1000 && totalDelay.get >= 10 * duration * 1000){
      val monitorTitle = s"spark streaming $appName 程序阻塞异常警告"
      val monitorContent = s"BJJListener : processingStartTime -> ${processingStartTime.get}, processingEndTime -> ${processingEndTime.get} , " +
        s"processingDelay -> ${processingDelay.get} , totalDelay -> ${totalDelay.get}, 请及时检查!"
      val monitorContent = s"BJJListener : 
                            processingStartTime -> ${processingStartTime.get}, 
                            processingEndTime -> ${processingEndTime.get} , 
                            processingDelay -> ${processingDelay.get} , 
                            totalDelay -> ${totalDelay.get}, 
                            请及时检查!"

      EmailSender.sendMail(monitorTitle, monitorContent)
    }
    logger.info("BJJListener  processingEndTime : ", processingEndTime)
    logger.info("BJJListener  processingDelay : ", processingDelay)
    logger.info("BJJListener  totalDelay : ", totalDelay)
  }

  override def onOutputOperationStarted(outputOperationStarted: StreamingListenerOutputOperationStarted): Unit =
    super.onOutputOperationStarted(outputOperationStarted)

  override def onOutputOperationCompleted(outputOperationCompleted: StreamingListenerOutputOperationCompleted): Unit =
    super.onOutputOperationCompleted(outputOperationCompleted)

}
public class EmailSender {

    private static boolean sendTextMail(EmailSendInfo mailInfo) {
        boolean sendStatus = false;//发送状态
        // 判断是否需要身份认证
        EmailAuthenticator authenticator = null;
        Properties pro = mailInfo.getProperties();
        if (mailInfo.isValidate()) {
            // 如果需要身份认证,则创建一个密码验证器
            authenticator = new EmailAuthenticator(mailInfo.getUserName(), mailInfo.getPassword());
        }
        // 根据邮件会话属性和密码验证器构造一个发送邮件的session
        Session sendMailSession = Session.getInstance(pro, authenticator);
        //【调试时使用】开启Session的debug模式
        sendMailSession.setDebug(true);
        try {
            // 根据session创建一个邮件消息
            MimeMessage mailMessage = new MimeMessage(sendMailSession);
            // 创建邮件发送者地址
            Address from = new InternetAddress(mailInfo.getFromAddress());
            // 设置邮件消息的发送者
            mailMessage.setFrom(from);
            // 创建邮件的接收者地址,并设置到邮件消息中
            Address to = new InternetAddress(mailInfo.getToAddress());
            mailMessage.setRecipient(Message.RecipientType.TO,to);
            // 设置邮件消息的主题
            mailMessage.setSubject(mailInfo.getSubject(), "UTF-8");
            // 设置邮件消息发送的时间
            mailMessage.setSentDate(new Date());
            // 设置邮件消息的主要内容
            String mailContent = mailInfo.getContent();
            mailMessage.setText(mailContent, "UTF-8");

            // 发送邮件
            Transport.send(mailMessage);
            sendStatus = true;
        } catch (MessagingException ex) {
            ex.printStackTrace();
        }
        return sendStatus;
    }


    public static void sendMail(String monitorTitle, String monitorContent){
        String fromaddr = "xxxx@yqzbw.com";
        String toaddr = "xxxx@yqzbw.com";
        String port = "25";
        String host = "smtp.exmail.qq.com";
        String userName = "xxxxxg@yqzbw.com";
        String password = "12345678";

        EmailSendInfo mailInfo = new EmailSendInfo();
        mailInfo.setMailServerHost(host);
        mailInfo.setValidate(true);
        mailInfo.setUserName(userName);
        mailInfo.setPassword(password);
        mailInfo.setFromAddress(fromaddr);
        mailInfo.setToAddress(toaddr);
        mailInfo.setSubject(monitorTitle);
        mailInfo.setContent(monitorContent);
        //发送文体格式邮件
        EmailSender.sendTextMail(mailInfo);
    }
}

spark streaming程序测试的例子

object test {
  def main(args: Array[String]): Unit = {

    System.setProperty("hadoop.home.dir", "D:\\mcyarn\\hadoop-common-2.2.0-bin-master")
    Logger.getLogger("org.apache.spark").setLevel(Level.ERROR)
    Logger.getLogger("org.eclipse.jetty.server").setLevel(Level.OFF)
    val appName = "spark Steaming test"
    val conf = new SparkConf().setMaster("local[2]").setAppName("test")
    val sc = new SparkContext(conf)
    val ssc = new StreamingContext(sc, Seconds(10))

    val brokerList = "localhost:9092"
    val zookeeperConnect = "localhost:2181"
    val groupId = "baasdf20180302"
    val newsTopic = "test"

    val kafkaParams = Map("metadata.broker.list" -> brokerList, "group.id" -> groupId,
      "zookeeper.connect"->zookeeperConnect,
      "auto.offset.reset" -> kafka.api.OffsetRequest.LargestTimeString)

    val kafkaStream = KafkaUtils.createDirectStream[String,String,StringDecoder, StringDecoder](ssc, kafkaParams,
      topics = Set(newsTopic)).map(_._1)

    kafkaStream.foreachRDD(rdd=>{
      if(!rdd.isEmpty()){
        val rdds = rdd.union(rdd).union(rdd).union(rdd)
        val transform = rdds.map(news=>{
          if(news!=null){
            val split = NlpAnalysis.parse(news).toStringWithOutNature(" ")
            split
          }else{
            null
          }
        })

        val wordCount = transform.map(word=>(word, 1)).reduceByKey(_+_)
        wordCount.foreach(println)
        println(rdd.count())
      }
    })
    // 通过可插拔的方式添加自己实现的listener
    ssc.addStreamingListener(new BJJListener(appName, 10)) 
    ssc.start()
    ssc.awaitTermination()
  }
}

3、 除此之外你还可以自己写 Python 脚本在 yarn 管理界面解析该应用的 ApplicationMaster 的地址,之后再通过 Spark Streaming 的 UI 去获取相关参数。