目录
Spark常见算法
WordCount
sc.textFile("/home/test.txt")
.flatMap(_.split(" ")).map((_,1)).reduceByKey(_ + _).collect
2个stage
https://baijiahao.baidu.com/s?id=1597554245391147162&wfr=spider&for=pc
Spark温度二次排序
假设有以下输入文件data.txt(逗号分割的分别是“年,月,总数”):
2018,2,128
2019,1,3
2019,2,-43
2018,2,64
2019,1,4
2019,1,21
2019,2,35
2019,2,0
期望的输出如下的结果:
2018-2 64,128
2019-1 3,4,21
2019-2 -43,0,35
// 加载数据集
val inputPath = "file:///home/hduser/data/spark/data.txt"
val inputRDD = sc.textFile(inputPath)
// 实现二次排序
val sortedRDD = inputRDD
.map(line => {
val arr = line.split(",")
val key = arr(0) + "-" + arr(1)
val value = arr(2)
(key,value)
})
.groupByKey()
.map(t => (t._1, t._2.toList.sortWith(_.toInt < _.toInt).mkString(",")))
.sortByKey(true) // true:升序,false:降序
// 结果输出
sortedRDD.collect.foreach(t => println(t._1 + "\t" + t._2))
分组TopN
object GroupTopN {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("GTopN").setMaster("local")
val sc = new SparkContext(conf)
//获取数据
val rdd = sc.textFile("F:\\score.txt")
//将每行数据 转换成元组形式(a,b)
val tuple1: RDD[(String, String)] = rdd.map(t=>{
val str = t.split(" ")
val banji = str(0)
val score = str(1)
(banji,score)
})
//进行分组
val grouped: RDD[(String, Iterable[String])] = tuple1.groupByKey()
//对每一组的数据 value值是一个集合,toArray转化成数组,使用sortwith方法 从大到小排序,然后求take3
val topN: RDD[(String, Array[String])] =grouped.map(t=>{
val group = t._1
val list: Array[String] = t._2.toArray.sortWith(_>_).take(3)
(group,list)
})
topN.foreach(t=>{
println(t._1)
t._2.foreach(println)
println("--------------")
})
}
}
Spark中的Stage、Job、Task的划分
• Job的划分:根据Action算子进行划分,一个Action算子划分一个Job
• Stage的划分:按照DAG图,从后往前,遇到宽依赖就进行Stage的划分
• Task的划分:一个Stage中,最后一个RDD有多少个partition就划分多少个task
SparkSession与SparkContext
SparkContext 是什么?
- 驱动程序使用SparkContext与集群进行连接和通信,它可以帮助执行Spark任务,并与资源管理器(YARN)进行协调
- 使用SparkContext,可以访问其他上下文,比如SQLContext和HiveContext
- 使用SparkContext,可以为Spark作业设置配置参数
SparkSession 是什么?
SparkSession是在Spark 2.0中引入的,简化了对不同上下文的访问。通过访问SparkSession,我们可以自动访问SparkContext。SparkSession现在是Spark的新入口点,它替换了旧的SQLContext和HiveContext。一旦访问了SparkSession,就可以开始使用DataFrame和Dataset了。
SparkSQL 窗口函数原理
一般窗口函数主要分两部分:
1、window函数部分(window_func)
2、窗口定义部分
window函数部分
windows函数部分就是所要在窗口上执行的函数,Spark支持三中类型的窗口函数:
1、聚合函数 (aggregate functions)
2、排序函数(Ranking functions)
3、分析窗口函数(Analytic functions)
第一种就是常用的count 、sum、avg等
第二种就是row_number、rank这样的排序函数
第三种专门为窗口而生的函数比如:cume_dist函数计算当前值在窗口中的百分位数
窗口定义部分
这部分就是over里面的内容了,里面也有三部分:
partition by
order by
ROWS | RANGE BETWEEN
前两部分就是把数据分桶然后桶内排序,排好了序才能很好的定位出你需要向前或者向后取哪些数据来参与计算。这第三部分就是确定你需要哪些数据了。Spark提供了两种方式:ROWS BETWEEN
和RANGE BETWEEN
。
ROWS BETWEEN
即按照距离来取。例如:ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
就是取从最开始到当前这一条数据,row_number()这个函数就是这样取的。ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING
代表取前面两条和后面两条数据参与计算,比如计算前后五天内的移动平均就可以这样算。
RANGE BETWEEN
是以当前值为锚点进行计算。比如RANGE BETWEEN 20 PRECEDING AND 10 FOLLOWING
当前值为50的话就去前后的值在30到60之间的数据。
windows实现原理
从最终的执行层面来看看数据是怎么流转的
例子:
df = spark.range(10).selectExpr("id","id%3 as flag")
df.selectExpr("""sum(id) over(
partition by flag
order by id
ROWS BETWEEN 1 PRECEDING and 1 FOLLOWING
) as s""").explain()
它的执行计划:
== Physical Plan ==
*(3) Project [x#266L]
+- Window [sum(id#261L) windowspecdefinition(flag#263L, id#261L ASC NULLS FIRST, specifiedwindowframe(RowFrame, -1, 1)) AS x#266L], [flag#263L], [id#261L ASC NULLS FIRST]
+- *(2) Sort [flag#263L ASC NULLS FIRST, id#261L ASC NULLS FIRST], false, 0
+- Exchange hashpartitioning(flag#263L, 60)
+- *(1) Project [id#261L, (id#261L % 3) AS flag#263L]
+- *(1) Range (0, 10, step=1, splits=60)
可以看出是先按照partitionby的字段进行了重分区,把桶内的数据都聚集到一起。然后再进行排序。最后执行window函数。
Spark SQL特点
• 能够将 SQL 查询与 Spark 程序无缝混合,允许使用 SQL 或 DataFrame API 对结构化数据进行查询
• 支持多达上百种的外部数据源,包括 Hive,Avro,Parquet,ORC,JSON 和 JDBC 等
• 支持 HiveQL 语法以及 Hive SerDes 和 UDF,允许访问现有的 Hive 仓库
• 支持标准的 JDBC 和 ODBC 连接
• 支持扩展并能保证容错
• SparkSql窗口函数:
o 窗口函数是spark sql模块从1.4之后开始支持的,主要用于解决对一组数据进行操作,同时为每条数据返回单个结果,比如计算指定访问数据的均值、计算累进和或访问当前行之前行数据等
o Spark SQL支持三类窗口函数:排名函数、分析函数和聚合函数
task失败重试机制
在Spark程序中,task有失败重试机制(根据 spark.task.maxFailures
配置,默认是4次),当task执行失败时,并不会直接导致整个应用程序down掉,只有在重试了spark.task.maxFailures
次后仍然失败的情况下才会使程序down掉。另外,Spark on Yarn模式还会受Yarn的重试机制去重启这个spark程序,根据 yarn.resourcemanager.am.max-attempts
配置(默认是2次)。
task在Executor中执行,跟踪源码看task在失败后都干了啥?
1、在executor中task执行完不管成功与否都会向execBackend报告task的状态:
execBackend.statusUpdate(taskId, TaskState.FINISHED, serializedResult)
2、在CoarseGrainedExecutorBackend中会向driver发送StatusUpdate状态变更信息:
override def statusUpdate(taskId: Long, state: TaskState, data: ByteBuffer) {
val msg = StatusUpdate(executorId, taskId, state, data)
driver match {
case Some(driverRef) => driverRef.send(msg)
case None => logWarning(s"Drop $msg because has not yet connected to driver")
}
}
3、CoarseGrainedSchedulerBackend收到消息后有调用了scheduler的方法:
override def receive: PartialFunction[Any, Unit] = {
case StatusUpdate(executorId, taskId, state, data) =>
scheduler.statusUpdate(taskId, state, data.value)
......
4、由于代码繁琐,列出了关键的几行代码,嵌套调用关系,这里最后向eventProcessLoop发送了CompletionEvent事件:
taskResultGetter.enqueueFailedTask(taskSet, tid, state, serializedData)
scheduler.handleFailedTask(taskSetManager, tid, taskState, reason)
taskSetManager.handleFailedTask(tid, taskState, reason)
sched.dagScheduler.taskEnded(tasks(index), reason, null, accumUpdates, info)
eventProcessLoop.post(CompletionEvent(task, reason, result, accumUpdates, taskInfo))
5、在DAGSchedulerEventProcessLoop
处理方法中 handleTaskCompletion(event: CompletionEvent)
有着最为关键的一行代码,这里listenerBus把task的状态发了出去,凡是监听了SparkListenerTaskEnd
的listener都可以获取到对应的消息,而且这个是带了失败的原因(event.reason)。其实第一遍走源码并没有注意到前面提到的sched.dagScheduler.taskEnded(tasks(index), reason, null, accumUpdates, info)
方法,后面根据SparkUI的page页面往回追溯才发现。
listenerBus.post(SparkListenerTaskEnd(
stageId, task.stageAttemptId, taskType, event.reason, event.taskInfo, taskMetrics))
Spark文件切分
Spark初始化RDD的时候,需要读取文件,通常是HDFS文件,在读文件的时候可以指定最小partition数量。如果没有指定最小partition数量,初始化完成的RDD默认有多少个partition是怎样决定的呢?以SparkContext.textFile为例来看下代码:
/**
* Read a text file from HDFS, a local file system (available on all nodes), or any
* Hadoop-supported file system URI, and return it as an RDD of Strings.
*/
def textFile(
path: String,
minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {
assertNotStopped()
hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text],
minPartitions).map(pair => pair._2.toString).setName(path)
}
/**
* Default min number of partitions for Hadoop RDDs when not given by user
* Notice that we use math.min so the "defaultMinPartitions" cannot be higher than 2.
*/
def defaultMinPartitions: Int = math.min(defaultParallelism, 2)
def hadoopFile[K, V](
path: String,
inputFormatClass: Class[_ <: InputFormat[K, V]],
keyClass: Class[K],
valueClass: Class[V],
minPartitions: Int = defaultMinPartitions): RDD[(K, V)] = withScope {
assertNotStopped()
val confBroadcast = broadcast(new SerializableConfiguration(hadoopConfiguration))
val setInputPathsFunc = (jobConf: JobConf) => FileInputFormat.setInputPaths(jobConf, path)
new HadoopRDD(
this,
confBroadcast,
Some(setInputPathsFunc),
inputFormatClass,
keyClass,
valueClass,
minPartitions).setName(path)
}
可见会直接返回一个HadoopRDD,如果不传最小partition数量,会使用defaultMinPartitions(通常情况下是2),那么HadoopRDD是怎样实现的?
class HadoopRDD[K, V](
sc: SparkContext,
broadcastedConf: Broadcast[SerializableConfiguration],
initLocalJobConfFuncOpt: Option[JobConf => Unit],
inputFormatClass: Class[_ <: InputFormat[K, V]],
keyClass: Class[K],
valueClass: Class[V],
minPartitions: Int)
extends RDD[(K, V)](sc, Nil) with Logging {
...
override def getPartitions: Array[Partition] = {
val jobConf = getJobConf()
// add the credentials here as this can be called before SparkContext initialized
SparkHadoopUtil.get.addCredentials(jobConf)
val inputFormat = getInputFormat(jobConf)
val inputSplits = inputFormat.getSplits(jobConf, minPartitions)
val array = new Array[Partition](inputSplits.size)
for (i <- 0 until inputSplits.size) {
array(i) = new HadoopPartition(id, i, inputSplits(i))
}
array
}
...
protected def getInputFormat(conf: JobConf): InputFormat[K, V] = {
val newInputFormat = ReflectionUtils.newInstance(inputFormatClass.asInstanceOf[Class[_]], conf)
.asInstanceOf[InputFormat[K, V]]
newInputFormat match {
case c: Configurable => c.setConf(conf)
case _ =>
}
newInputFormat
}
决定分区数量的逻辑在getPartitions中,实际上调用的是InputFormat.getSplits,InputFormat是一个接口,
public interface InputFormat<K, V> {
InputSplit[] getSplits(JobConf var1, int var2) throws IOException;
RecordReader<K, V> getRecordReader(InputSplit var1, JobConf var2, Reporter var3) throws IOException;
}
每种文件格式都有自己的实现类,常见的文件格式avro、orc、parquet、textfile对应的实现类为AvroInputFormat,OrcInputFormat,MapredParquetInputFormat,CombineTextInputFormat,每个实现类都有自己的split逻辑,来看下默认实现大致过程如下:
getSplits首先会拿到所有需要读取的file列表,然后会迭代这个file列表,首先看一个file是否可以再分即isSplitable(默认是true可能被子类覆盖),如果不能再split则直接作为1个split,如果可以再split,则获取这个file的block信息,然后综合根据多个参数来计算出1个split的数据大小即splitSize,然后会将这个file的所有block划分为多个split,划分过程会考虑机架、host等因素,如果是大block,则直接作为一个split,如果是小block可能多个block合并在一个split里(这样能够尽量减少split数量),最终得到的split数量即partition数量。
SparkStreaming滑动窗口
手写一个SparkStreaming计算uv,需要滑动窗口,10min一个步长,1h一个窗口长度
Spark Streaming提供了滑动窗口操作的支持,从而让我们可以对一个滑动窗口内的数据执行计算操作。每次掉落在窗口内的RDD的数据,会被聚合起来执行计算操作,然后生成的RDD,会作为window DStream的一个RDD。
热点搜索词滑动统计,每隔10秒钟,统计最近60秒钟的搜索词的搜索频次,并打印出排名最靠前的3个搜索词以及出现次数
object WindowHotWordS {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("WindowHotWordS").setMaster("local[2]")
//Scala中,创建的是StreamingContext
val ssc = new StreamingContext(conf, Seconds(5))
val searchLogsDStream = ssc.socketTextStream("spark1", 9999)
val searchWordsDStream = searchLogsDStream.map { searchLog => searchLog.split(" ")(1) }
val searchWordPairDStream = searchWordsDStream.map { searchWord => (searchWord, 1) }
// reduceByKeyAndWindow
// 第二个参数,是窗口长度,这是是60秒
// 第三个参数,是滑动间隔,这里是10秒
// 也就是说,每隔10秒钟,将最近60秒的数据,作为一个窗口,进行内部的RDD的聚合,然后统一对一个RDD进行后续计算
// 而是只是放在那里
// 然后,等待我们的滑动间隔到了以后,10秒到了,会将之前60秒的RDD,因为一个batch间隔是5秒,所以之前60秒,就有12个RDD,给聚合起来,然后统一执行reduceByKey操作
// 所以这里的reduceByKeyAndWindow,是针对每个窗口执行计算的,而不是针对 某个DStream中的RDD
// 每隔10秒钟,出来 之前60秒的收集到的单词的统计次数
val searchWordCountsDStream = searchWordPairDStream.reduceByKeyAndWindow((v1: Int, v2: Int) => v1 + v2, Seconds(60), Seconds(10))
val finalDStream = searchWordCountsDStream.transform(searchWordCountsRDD => {
val countSearchWordsRDD = searchWordCountsRDD.map(tuple => (tuple._2, tuple._1))
val sortedCountSearchWordsRDD = countSearchWordsRDD.sortByKey(false)
val sortedSearchWordCountsRDD = sortedCountSearchWordsRDD.map(tuple => (tuple._1, tuple._2))
val top3SearchWordCounts = sortedSearchWordCountsRDD.take(3)
for (tuple <- top3SearchWordCounts) {
println("result : " + tuple)
}
searchWordCountsRDD
})
finalDStream.print()
ssc.start()
ssc.awaitTermination()
}
}
Spark Streaming中并行运行任务
在运行Spark Streaming程序时,有时需要并行化任务的执行。比如任务A需要每隔5s输出计算结果,任务B用到了时间窗口,每隔1hour计算一次并输出结果。在Spark程序内部(即每个Application中),任务是可以并行运行的。通过简单的多线程实现,只要Driver能读到多个action,那么就会把任务都提交上去,也就实现了job并行。
但是Spark Streaming流处理是不间断的,会一遍又一遍重复去执行任务,Spark是怎么处理的呢?可以简单这么理解,会先将代码逻辑解析出来,放到一个集合,然后再写个死循环,每隔一段时间去把集合里面的逻辑执行一遍。这样一来Spark Streaming就不局限于单个线程执行了,因为所有job都解析好了,只是要去执行job,那么当然可以开启一个线程池,直接去执行任务了。而事实上,底层实现也确实是这样,并且提供了spark.streaming.concurrentJobs
参数配置job的并发度,也就不用自己去写多线程了。
Scheduler Mode
首先,运行模式依然要设置为fair,这是必须的。但是Spark Streaming还有一个专用的参数用来设置可以并行的任务数:spark.streaming.concurrentJobs
。这个参数的意思是可以同时运行多少任务,默认是1。如果需要同时运行1个以上的任务,就要把这个值调高。所以,如果我想并行运行A和B两个任务,就要这样配置:
val conf: SparkConf = new SparkConf().setAppName("parallel")
conf.set("spark.scheduler.mode","FAIR")
conf.set("spark.streaming.concurrentJobs","2")
把spark.streaming.concurrentJobs设置为2,就可以同时运行两个任务了。
Fair Scheduler Pools
虽然调整了可并行运行任务的数量,可是默认情况下,所有的任务都是以同样的权重运行的。如果去看Spark的WEB UI界面,会在Stage标签内看到default pool,所有的任务都运行在默认的pool中。可是如何调整不同类型任务占用资源的权重呢?在官网的介绍中,要求在不同的线程提交任务才可以,那么Spark Streaming如何在不同线程提交任务呢?Spark Streaming不同的Action之间本来就是多线程执行的。所以可以使用这种方式设置:
dstream.foreachRDD(rdd =>
rdd.sparkContext.setLocalProperty("spark.scheduler.pool","pool_a")
)
这样就可以使用不同的pool了。这时候在WEB UI界面的Stage标签中就可以看到不同的pool了。而pool的资源分配权重和pool内部的调用方式依然在 $SPARK_HOME/conf/fairscheduler.xml 中配置。