一 需求二:广告点击量实时统计
描述:实时统计每天各地区各城市各广告的点击总流量,并将其存入MySQL。
(1) 思路分析
1)单个批次内对数据进行按照天维度的聚合统计;
2)结合MySQL数据跟当前批次数据更新原有的数据。
(2)MySQL建表
CREATE TABLE area_city_ad_count (
dt CHAR(10),
area CHAR(4),
city CHAR(4),
adid CHAR(2),
count BIGINT,
PRIMARY KEY (dt,area,city,adid)
);
(3)代码实现
DateAreaCityAdCountHandler
object DateAreaCityAdCountHandler {
//时间格式化对象
private val sdf: SimpleDateFormat = new SimpleDateFormat("yyyy-MM-dd")
/**
* 统计每天各大区各个城市广告点击总数并保存至MySQL中
*
* @param filterAdsLogDStream 根据黑名单过滤后的数据集
*/
def saveDateAreaCityAdCountToMysql(filterAdsLogDStream: DStream[Ads_log]): Unit = {
//1.统计每天各大区各个城市广告点击总数
val dateAreaCityAdToCount: DStream[((String, String, String, String), Long)] = filterAdsLogDStream.map(ads_log => {
//a.取出时间戳
val timestamp: Long = ads_log.timestamp
//b.格式化为日期字符串
val dt: String = sdf.format(new Date(timestamp))
//c.组合,返回
((dt, ads_log.area, ads_log.city, ads_log.adid), 1L)
}).reduceByKey(_ + _)
//2.将单个批次统计之后的数据集合MySQL数据对原有的数据更新
dateAreaCityAdToCount.foreachRDD(rdd => {
//对每个分区单独处理
rdd.foreachPartition(iter => {
//a.获取连接
val connection: Connection = JdbcUtil.getConnection
//b.写库
iter.foreach { case ((dt, area, city, adid), count) =>
JdbcUtil.executeUpdate(connection,
"""
|INSERT INTO area_city_ad_count (dt,area,city,adid,count)
|VALUES(?,?,?,?,?)
|ON DUPLICATE KEY
|UPDATE count=count+?;
""".stripMargin,
Array(dt, area, city, adid, count, count))
}
//c.释放连接
connection.close()
})
})
}
}
RealTimeApp
object RealTimeApp {
def main(args: Array[String]): Unit = {
//1.创建SparkConf
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("RealTimeApp")
//2.创建StreamingContext
val ssc = new StreamingContext(sparkConf, Seconds(3))
//3.读取Kafka数据 1583288137305 华南 深圳 4 3
val topic: String = PropertiesUtil.load("config.properties").getProperty("kafka.topic")
val kafkaDStream: InputDStream[ConsumerRecord[String, String]] = MyKafkaUtil.getKafkaStream(topic, ssc)
//4.将每一行数据转换为样例类对象
val adsLogDStream: DStream[Ads_log] = kafkaDStream.map(record => {
//a.取出value并按照" "切分
val arr: Array[String] = record.value().split(" ")
//b.封装为样例类对象
Ads_log(arr(0).toLong, arr(1), arr(2), arr(3), arr(4))
})
//5.根据MySQL中的黑名单表进行数据过滤
val filterAdsLogDStream: DStream[Ads_log] = adsLogDStream.filter(adsLog => {
//查询MySQL,查看当前用户是否存在。
val connection: Connection = JdbcUtil.getConnection
val bool: Boolean = JdbcUtil.isExist(connection, "select * from black_list where userid=?", Array(adsLog.userid))
connection.close()
!bool
})
filterAdsLogDStream.cache()
//6.对没有被加入黑名单的用户统计当前批次单日各个用户对各个广告点击的总次数,
// 并更新至MySQL
// 之后查询更新之后的数据,判断是否超过100次。
// 如果超过则将给用户加入黑名单
BlackListHandler.saveBlackListToMysql(filterAdsLogDStream)
//7.统计每天各大区各个城市广告点击总数并保存至MySQL中
dateAreaCityAdCountHandler.saveDateAreaCityAdCountToMysql(filterAdsLogDStream)
//10.开启任务
ssc.start()
ssc.awaitTermination()
}
}
二 需求三:最近一小时某个广告点击量趋势统计
结果展示:
1:List [15:50->10,15:51->25,15:52->30]
2:List [15:50->10,15:51->25,15:52->30]
3:List [15:50->10,15:51->25,15:52->30]
(1) 思路分析
1)开窗确定时间范围;
2)在窗口内将数据转换数据结构为((adid,hm),count);
3)按照广告id进行分组处理,组内按照时分排序。
(2) 代码实现
LastHourAdCountHandler
object LastHourAdCountHandler {
//时间格式化对象
private val sdf: SimpleDateFormat = new SimpleDateFormat("HH:mm")
/**
* 统计最近一小时(2分钟)广告分时点击总数
*
* @param filterAdsLogDStream 过滤后的数据集
* @return
*/
def getAdHourMintToCount(filterAdsLogDStream: DStream[Ads_log]): DStream[(String, List[(String, Long)])] = {
//1.开窗 => 时间间隔为1个小时 window()
val windowAdsLogDStream: DStream[Ads_log] = filterAdsLogDStream.window(Minutes(2))
//2.转换数据结构 ads_log =>((adid,hm),1L) map()
val adHmToOneDStream: DStream[((String, String), Long)] = windowAdsLogDStream.map(adsLog => {
val timestamp: Long = adsLog.timestamp
val hm: String = sdf.format(new Date(timestamp))
((adsLog.adid, hm), 1L)
})
//3.统计总数 ((adid,hm),1L)=>((adid,hm),sum) reduceBykey(_+_)
val adHmToCountDStream: DStream[((String, String), Long)] = adHmToOneDStream.reduceByKey(_ + _)
//4.转换数据结构 ((adid,hm),sum)=>(adid,(hm,sum)) map()
val adToHmCountDStream: DStream[(String, (String, Long))] = adHmToCountDStream.map { case ((adid, hm), count) =>
(adid, (hm, count))
}
//5.按照adid分组 (adid,(hm,sum))=>(adid,Iter[(hm,sum),...]) groupByKey
adToHmCountDStream.groupByKey()
.mapValues(iter =>
iter.toList.sortWith(_._1 < _._1)
)
}
}
RealTimeApp
object RealTimeApp {
def main(args: Array[String]): Unit = {
//1.创建SparkConf
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("RealTimeApp")
//2.创建StreamingContext
val ssc = new StreamingContext(sparkConf, Seconds(3))
//3.读取Kafka数据 1583288137305 华南 深圳 4 3
val topic: String = PropertiesUtil.load("config.properties").getProperty("kafka.topic")
val kafkaDStream: InputDStream[ConsumerRecord[String, String]] = MyKafkaUtil.getKafkaStream(topic, ssc)
//4.将每一行数据转换为样例类对象
val adsLogDStream: DStream[Ads_log] = kafkaDStream.map(record => {
//a.取出value并按照" "切分
val arr: Array[String] = record.value().split(" ")
//b.封装为样例类对象
Ads_log(arr(0).toLong, arr(1), arr(2), arr(3), arr(4))
})
//5.根据MySQL中的黑名单表进行数据过滤
val filterAdsLogDStream: DStream[Ads_log] = adsLogDStream.filter(adsLog => {
//查询MySQL,查看当前用户是否存在。
val connection: Connection = JdbcUtil.getConnection
val bool: Boolean = JdbcUtil.isExist(connection, "select * from black_list where userid=?", Array(adsLog.userid))
connection.close()
!bool
})
filterAdsLogDStream.cache()
//6.对没有被加入黑名单的用户统计当前批次单日各个用户对各个广告点击的总次数,
// 并更新至MySQL
// 之后查询更新之后的数据,判断是否超过100次。
// 如果超过则将给用户加入黑名单
BlackListHandler.saveBlackListToMysql(filterAdsLogDStream)
//7.统计每天各大区各个城市广告点击总数并保存至MySQL中
DateAreaCityAdCountHandler.saveDateAreaCityAdCountToMysql(filterAdsLogDStream)
//8.统计最近一小时(2分钟)广告分时点击总数
val adToHmCountListDStream: DStream[(String, List[(String, Long)])] = LastHourAdCountHandler.getAdHourMintToCount(filterAdsLogDStream)
//9.打印
adToHmCountListDStream.print()
//10.开启任务
ssc.start()
ssc.awaitTermination()
}
}
三 spark内核概述
Spark内核泛指Spark的核心运行机制,包括Spark核心组件的运行机制、Spark任务调度机制、Spark内存管理机制、Spark核心功能的运行原理等,熟练掌握Spark内核原理,能够更好地完成Spark代码设计,并能够帮助准确锁定项目运行过程中出现的问题的症结所在。
1 Spark核心组件回顾
(1)Driver
Spark驱动器节点,用于执行Spark任务中的main方法,负责实际代码的执行工作。Driver在Spark作业执行时主要负责:
- 将用户程序转化为作业(Job);
- 在Executor之间调度任务(Task);
- 跟踪Executor的执行情况;
- 通过UI展示查询运行情况;
(2)Executor
Spark Executor节点是一个JVM进程,负责在 Spark 作业中运行具体任务,任务彼此之间相互独立。Spark 应用启动时,Executor节点被同时启动,并且始终伴随着整个 Spark 应用的生命周期而存在。如果有Executor节点发生了故障或崩溃,Spark 应用也可以继续执行,会将出错节点上的任务调度到其他Executor节点上继续运行。
Executor有两个核心功能:
- 负责运行组成Spark应用的任务,并将结果返回给驱动器进程;
- 它们通过自身的块管理器(Block Manager)为用户程序中要求缓存的 RDD 提供内存式存储。RDD 是直接缓存在Executor进程内的,因此任务可以在运行时充分利用缓存数据加速运算。
2 Spark通用运行流程概述
上图为Spark通用运行流程图,体现了基本的Spark应用程序在部署中的基本提交流程。
这个流程是按照如下的核心步骤进行工作的:
- 任务提交后,都会先启动Driver程序;
- 随后Driver向集群管理器注册应用程序;
- 之后集群管理器根据此任务的配置文件分配Executor并启动;
- Driver开始执行main函数,Spark查询为懒执行,当执行到Action算子时开始反向推算,根据宽依赖进行Stage的划分,随后每一个Stage对应一个Taskset,Taskset中有多个Task,查找可用资源Executor进行调度;
- 根据本地化原则,Task会被分发到指定的Executor去执行,在任务执行的过程中,Executor也会不断与Driver进行通信,报告任务运行情况。
四 Spark部署模式
Spark支持多种集群管理器(Cluster Manager),分别为:
- Standalone:独立模式,Spark原生的简单集群管理器,自带完整的服务,可单独部署到一个集群中,无需依赖任何其他资源管理系统,使用Standalone可以很方便地搭建一个集群;
- Hadoop YARN:统一的资源管理机制,在上面可以运行多套计算框架,如MR、Storm等。根据Driver在集群中的位置不同,分为yarn client(集群外)和yarn cluster(集群内部)
- Apache Mesos:一个强大的分布式资源管理框架,它允许多种不同的框架部署在其上,包括Yarn。
- K8S : 容器式部署环境。
实际上,除了上述这些通用的集群管理器外,Spark内部也提供了方便用户测试和学习的本地集群部署模式和Windows环境。由于在实际工厂环境下使用的绝大多数的集群管理器是Hadoop YARN,因此关注的重点是Hadoop YARN模式下的Spark集群部署。
Spark的运行模式取决于传递给SparkContext的MASTER环境变量的值,个别模式还需要辅助的程序接口来配合使用,目前支持的Master字符串及URL包括:
Master URL | Meaning |
---|---|
local | 在本地运行,只有一个工作进程,无并行计算能力。 |
local[K] | 在本地运行,有K个工作进程,通常设置K为机器的CPU核心数量。 |
local[*] | 在本地运行,工作进程数量等于机器的CPU核心数量。 |
spark://HOST:PORT | 以Standalone模式运行,这是Spark自身提供的集群运行模式,默认端口号: 7077。详细文档见:Spark standalone cluster。 |
mesos://HOST:PORT | 在Mesos集群上运行,Driver进程和Worker进程运行在Mesos集群上,部署模式必须使用固定值:–deploy-mode cluster。详细文档见:MesosClusterDispatcher. |
yarn-client | 在Yarn集群上运行,Driver进程在本地,Executor进程在Yarn集群上,部署模式必须使用固定值:–deploy-mode client。Yarn集群地址必须在HADOOP_CONF_DIR or YARN_CONF_DIR变量里定义。 |
yarn-cluster | 在Yarn集群上运行,Driver进程在Yarn集群上,Work进程也在Yarn集群上,部署模式必须使用固定值:–deploy-mode cluster。Yarn集群地址必须在HADOOP_CONF_DIR or YARN_CONF_DIR变量里定义。 |
用户在提交任务给Spark处理时,以下两个参数共同决定了Spark的运行方式。
- master MASTER_URL :决定了Spark任务提交给哪种集群处理。
- deploy-mode DEPLOY_MODE:决定了Driver的运行方式,可选值为Client或者Cluster。
1 Standalone模式运行机制
Standalone集群有四个重要组成部分,分别是:
- Driver:是一个进程,之前编写的Spark应用程序就运行在Driver上,由Driver进程执行;
- Master(RM):是一个进程,主要负责资源的调度和分配,并进行集群的监控等职责;
- Worker(NM):是一个进程,一个Worker运行在集群中的一台服务器上,主要负责两个职责,一个是用自己的内存存储RDD的某个或某些partition;另一个是启动其他进程和线程(Executor),对RDD上的partition进行并行的处理和计算。
- Executor:是一个进程,一个Worker上可以运行多个Executor,Executor通过启动多个线程(task)来执行对RDD的partition进行并行计算,也就是执行我们对RDD定义的例如map、flatMap、reduce等算子操作。
(1)Standalone Client模式
在Standalone Client模式下,Driver在任务提交的本地机器上运行,Driver启动后向Master注册应用程序,Master根据submit脚本的资源需求找到内部资源至少可以启动一个Executor的所有Worker,然后在这些Worker之间分配Executor,Worker上的Executor启动后会向Driver反向注册,所有的Executor注册完成后,Driver开始执行main函数,之后执行到Action算子时,开始划分stage,每个stage生成对应的taskSet,之后将task分发到各个Executor上执行。
(2)Standalone Cluster模式
在Standalone Cluster模式下,任务提交后,Master会找到一个Worker启动Driver。Driver启动后向Master注册应用程序,Master根据submit脚本的资源需求找到内部资源至少可以启动一个Executor的所有Worker,然后在这些Worker之间分配Executor,Worker上的Executor启动后会向Driver反向注册,所有的Executor注册完成后,Driver开始执行main函数,之后执行到Action算子时,开始划分Stage,每个Stage生成对应的taskSet,之后将Task分发到各个Executor上执行。
注意,Standalone的两种模式下(client/Cluster),Master在接到Driver注册Spark应用程序的请求后,会获取其所管理的剩余资源能够启动一个Executor的所有Worker,然后在这些Worker之间分发Executor,此时的分发只考虑Worker上的资源是否足够使用,直到当前应用程序所需的所有Executor都分配完毕,Executor反向注册完毕后,Driver开始执行main程序。
2 YARN模式运行机制
(1)YARN Client模式
- 执行脚本提交任务,实际是启动一个SparkSubmit的JVM进程;
- SparkSubmit类中的main方法反射调用用户代码的main方法;
- 启动Driver线程,执行用户的作业,并创建ScheduleBackend;
- YarnClientSchedulerBackend向RM发送指令:bin/java ExecutorLauncher;
- Yarn框架收到指令后会在指定的NM中启动ExecutorLauncher(实际上还是调用ApplicationMaster的main方法);
object ExecutorLauncher {
def main(args: Array[String]): Unit = {
ApplicationMaster.main(args)
}
}
- AM向RM注册,申请资源;
- 获取资源后AM向NM发送指令:bin/java CoarseGrainedExecutorBackend;
- CoarseGrainedExecutorBackend进程会接收消息,跟Driver通信,注册已经启动的Executor;然后启动计算对象Executor等待接收任务
- Driver分配任务并监控任务的执行。
注意:SparkSubmit、ApplicationMaster和YarnCoarseGrainedExecutorBackend是独立的进程;Executor和Driver是对象。
(2) YARN Cluster模式
- 行脚本提交任务,实际是启动一个SparkSubmit的JVM进程;
- SparkSubmit类中的main方法反射调用YarnClusterApplication的main方法;
- YarnClusterApplication创建Yarn客户端,然后向Yarn服务器发送执行指令:bin/java ApplicationMaster;
- Yarn框架收到指令后会在指定的NM中启动ApplicationMaster;
- ApplicationMaster启动Driver线程,执行用户的作业;
- AM向RM注册,申请资源;
- 获取资源后AM向NM发送指令:bin/java YarnCoarseGrainedExecutorBackend;
- CoarseGrainedExecutorBackend进程会接收消息,跟Driver通信,注册已经启动的Executor;然后启动计算对象Executor等待接收任务
- Driver线程继续执行完成作业的调度和任务的执行。
- Driver分配任务并监控任务的执行。
注意:SparkSubmit、ApplicationMaster和CoarseGrainedExecutorBackend是独立的进程;Driver是独立的线程;Executor和YarnClusterApplication是对象。
注意:跟踪源码的时候添加一下依赖
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-yarn_2.11</artifactId>
<version>2.1.1</version>
</dependency>