例子
object Work02App {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setMaster("local[2]").setAppName(this.getClass.getSimpleName)
val sc = new SparkContext(sparkConf)
/* 用户 节目 列表 点击
001,一起看|电视剧|军旅|亮剑,1,1
001,一起看|电视剧|军旅|亮剑,1,0
002,一起看|电视剧|军旅|士兵突击,1,1
==>
001,一起看,2,1
001,电视剧,2,1
001,军旅,2,1
001,亮剑,2,1
*/
val lines = sc.parallelize(List(
"001,一起看|电视剧|军旅|亮剑,1,1",
"001,一起看|电视剧|军旅|亮剑,1,0",
"002,一起看|电视剧|军旅|士兵突击,1,1"
))
/**
* ((001,一起看),(1,0))
* ((001,电视剧),(1,0))
* ((001,军旅),(1,0))
* ((001,亮剑),(1,0))
* ((002,一起看),(1,1))
* ((002,电视剧),(1,1))
* ((002,军旅),(1,1))
* ((002,士兵突击),(1,1))
* ((001,一起看),(1,1))
* ((001,电视剧),(1,1))
* ((001,军旅),(1,1))
* ((001,亮剑),(1,1))
*/
val rdd1 = lines.flatMap(x=>{
val splits = x.split(",")
val id = splits(0)
val word = splits(1)
val imp = splits(2).toInt
val click = splits(3).toInt
val words = word.split("\\|")
words.map(x=>((id,x),(imp,click)))
})
/**
* ((002,军旅),(1,1))
* ((001,军旅),(2,1))
* ((002,一起看),(1,1))
* ((001,亮剑),(2,1))
* ((001,一起看),(2,1))
* ((002,电视剧),(1,1))
* ((001,电视剧),(2,1))
* ((002,士兵突击),(1,1))
*/
val rdd2 = rdd1.groupByKey().mapValues(x=>{
val imps = x.map(_._1).sum
val clicks = x.map(_._2).sum
(imps,clicks)
})
rdd2.reduceByKey((a,b)=>(a._1+b._1,a._2+b._2)).foreach(println(_))
sc.stop()
}
}
多目录输出
1、实现多目录输出自定义类
class MyMultipleTextOutputFormat extends MultipleTextOutputFormat[Any,Any]{
//生成最终的key类型,这里不要,给null
override def generateActualKey(key: Any, value: Any): Any = NullWritable.get()
//生成最终生成的value类型,这里是String
override def generateActualValue(key: Any, value: Any): Any = {
value.asInstanceOf[String]
}
//生成文件名
override def generateFileNameForKeyValue(key: Any, value: Any, name: String): String = {
s"$key/$name"
}
}
2、主类,使用saveAsHadoopFile(path, keyClass, valueClass, fm.runtimeClass.asInstanceOf[Class[F]])方法保存数据,指定参数
object MultiDirectory {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setMaster("local[*]").setAppName(this.getClass.getSimpleName)
val sc = new SparkContext(sparkConf)
val out = "D:\\ssc\\out"
FileUtils.deleteOutput(sc.hadoopConfiguration,out)
sc.textFile("D:\\ssc\\access.log")
.map(x=>{
val splits = x.split("\t")
(splits(1),x)
}).saveAsHadoopFile(out,classOf[String],classOf[String],classOf[MyMultipleTextOutputFormat])
sc.stop
}
}
广播变量和累加器
在spark程序中,当一个传递给Spark操作(例如map和reduce)的函数在远程节点上面运行时,Spark操作实际上操作的是这个函数所用变量的一个独立副本。这些变量会被复制到每台机器上,并且这些变量在远程机器上的所有更新都不会传递回驱动程序。通常跨任务的读写变量是低效的,但是,Spark还是为两种常见的使用模式提供了两种有限的共享变量:广播变量(broadcast variable)和累加器(accumulator)
计数器
在spark应用程序中,我们经常会有这样的需求,如异常监控,调试,记录符合某特性的数据的数目,这种需求都需要用到计数器,如果一个变量不被声明为一个累加器,那么它将在被改变时不会再driver端进行全局汇总,即在分布式运行时每个task运行的只是原始变量的一个副本,并不能改变原始变量的值,但是当这个变量被声明为累加器后,该变量就会有分布式计数的功能。
计数器种类很多,但是经常使用的就是两种,longAccumulator和collectionAccumulator
需要注意的是计数器是lazy的,只有触发action才会进行计数,在不持久的情况下重复触发action,计数器会重复累加
1、LongAccumulator
Accumulators是只能通过associative和commutative操作"added"的变量,因此有效的并行支持,他们可以用于实现计数器(如MapReduce)和Spark本身支持数字类型的累加器,程序员还可以添加对新类型的支持
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("MyLongAccumulator")
val sc = new SparkContext(sparkConf)
val acc = sc.longAccumulator("计数")
val rdd = sc.parallelize(List(1,2,3,4,5,6,7,8,9))
val forRDD = rdd.map(x=>{
acc.add(1L)
})
forRDD.count()
println(acc.value) //9
forRDD.count()
println(acc.value) //18
forRDD.count()
println(acc.value) //27
sc.stop()
使用longAccumulator做计数的时候要小心重复执行action导致的acc.value的变化,这是因为重复执行了count,累加器的数量成倍增长,解决方法,在action操作之前调用rdd的cache方法(或persist),这样在count后数据集就会被缓存下来,而无需从头开始计算
forRDD.cache()
forRDD.count()
println(acc.value) //9
forRDD.count()
println(acc.value) //9
forRDD.count()
println(acc.value) //9
2、CollectionAccumulator
CollectionAccumulator,集合计数器,计数器中保存的是集合元素,通过泛型指定
def main(args: Array[String]): Unit = {
/**
* 需求:id后三位相同的加入计数器
*/
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("MyLongAccumulator")
val sc = new SparkContext(sparkConf)
//生成集合计数器
val acc = sc.collectionAccumulator[People]("集合计数器")
//生成RDD
val rdd = sc.parallelize(Array(People("p1", 100000), People("p2", 100001),
People("p3", 100222), People("p4", 100003)))
rdd.map(x=>{
val id = x.id.toString.reverse
//满足条件就加入计数器
if(id(0) == id(1) && id(0) == id(2)){
acc.add(x)
}
}).count()
println(acc.value) //[People(p1,100000), People(p3,100222)]
sc.stop()
}
case class People(name:String,id:Long);
注意
- 计数器在Driver端定义赋初始值,计数器只能在Driver端读取最后的值
- 计数器不是一个调优操作
广播变量
如果我们要在分布式计算里面分发大对象,例如:字典,集合,黑白名单等,这个都会由Driver端进行分发,一般来讲,如果这个变量不是广播变量,那么每个task就会分发一份,这在task数目十分多的情况下Driver的带宽会成为系统的瓶颈,而且会大量消耗task服务器上的资源,如果将这个变量声明为广播变量,那么只是每个executor拥有一份,这个executor启动的task会共享这个变量,节省了通信的成本和服务器的资源。
小表广播案例
spark有一种常见的优化方式就是小表广播,使用map join来代替reduce join,我们通过把小表的数据集广播到各个节点上,节省了shuffle操作
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setMaster("local[2]")
.setAppName(this.getClass.getSimpleName)
val sc = new SparkContext(sparkConf)
// Fact table 航线(起点机场, 终点机场, 航空公司, 起飞时间)
val flights = sc.parallelize(List(
("SEA", "JFK", "DL", "7:00"),
("SFO", "LAX", "AA", "7:05"),
("SFO", "JFK", "VX", "7:05"),
("JFK", "LAX", "DL", "7:10"),
("LAX", "SEA", "DL", "7:10")))
// Dimension table 机场(简称, 全称, 城市, 所处城市简称)
val airports = sc.parallelize(List(
("JFK", "John F. Kennedy International Airport", "New York", "NY"),
("LAX", "Los Angeles International Airport", "Los Angeles", "CA"),
("SEA", "Seattle-Tacoma International Airport", "Seattle", "WA"),
("SFO", "San Francisco International Airport", "San Francisco", "CA")))
// Dimension table 航空公司(简称,全称)
val airlines = sc.parallelize(List(
("AA", "American Airlines"),
("DL", "Delta Airlines"),
("VX", "Virgin America")))
//最终统计结果:
//出发城市 终点城市 航空公司名称 起飞时间
//Seattle New York Delta Airlines 7:00
//San Francisco Los Angeles American Airlines 7:05
//San Francisco New York Virgin America 7:05
//New York Los Angeles Delta Airlines 7:10
//Los Angeles Seattle Delta Airlines 7:10
val airportsBc = sc.broadcast(airports.map(x => (x._1, x._3)).collectAsMap())
val airlinesBc = sc.broadcast(airlines.collectAsMap())
flights.map{
case (a,b,c,d) => (airportsBc.value.get(a).get,
airportsBc.value.get(b).get,
airlinesBc.value.get(c).get,
d
)
}.foreach(println)
sc.stop()
}
为什么只能 broadcast 只读的变量
这就涉及一致性的问题,如果变量可以被更新,那么变量被某个节点更新,其他节点需要一块更新,这涉及了事务一致性。
注意事项
- 变量一旦被定义为一个广播变量,那么这个变量只能读,不能修改
- 能不能将一个RDD使用广播变量广播出去?因为RDD是不存储数据的。可以将RDD的结果广播出去。
- 广播变量只能在Driver端定义,不能在Executor端定义。
- 在Driver端可以修改广播变量的值,在Executor端无法修改广播变量的值。
- 如果Executor端用到了Driver的变量,不使用广播变量在Executor有多少task就有多少Driver端的变量副本。
- 如果Executor端用到了Driver的变量,使用广播变量在每个Executor中只有一份Driver端的变量副本。
持久化
Spark非常重要的一个功能特性就是可以将RDD持久化在内存中。当对RDD执行持久化操作时,每个节点都会将自己操作的RDD的partition持久化到内存中,并且在之后对该RDD的反复使用中,直接使用内存缓存的partition。这样的话,对于针对一个RDD反复执行多个操作的场景,就只要对RDD计算一次即可,后面直接使用该RDD,而不需要反复计算多次该RDD。
巧妙使用RDD持久化,甚至在某些场景下,可以将spark应用程序的性能提升10倍。对于迭代式算法和快速交互式应用来说,RDD持久化,是非常重要的。
持久化的存储级别很多,常用的是MEMORY_ONLY、MEMORY_ONLY_SER、MEMORY_AND_DISK
Storage Level | Meaning |
---|---|
MEMORY_ONLY | 将RDD作为不序列化的Java对象存储在JVM中。如果RDD不适合内存,那么一些分区将不会被缓存,而是在需要它们时动态地重新计算。这是默认级别。 |
MEMORY_AND_DISK | 将RDD作为不序列化的Java对象存储在JVM中。如果RDD不适合内存,那么将不适合的分区存储在磁盘上,并在需要时从磁盘中读取它们。 |
MEMORY_ONLY_SER (Java and Scala) | 将RDD存储为序列化的Java对象(每个分区一个字节数组)。这通常比反序列化对象更节省空间,特别是在使用快速序列化器时,但读取时需要更多cpu。 |
如何选择存储级别
Storage Level的选择是内存和CPU的权衡
- 内存多:MEMORY_ONLY (不进行序列化)
- CPU跟的上:MEMORY_ONLY_SER (进行了序列化,推介)
- 不建议写Disk
使用cache()和persist()进行持久化操作,它们都是lazy的,需要action才能触发,默认使用MEMORY_ONLY
scala> forRDD.cache
res18: forRDD.type = MapPartitionsRDD[9] at map at <console>:27
scala> forRDD.count
res19: Long = 8
结果可以在Web UI的Storage中查看
如果需要清除缓存,使用unpersist(),清除缓存数据是立即执行的
scala> forRDD.unpersist()
res8: forRDD.type = MapPartitionsRDD[3] at map at <console>:28
修改存储级别
val forRDD = rdd.map(x => {
//计数器做累加
acc.add(1L)
}).persist(StorageLevel.MEMORY_ONLY_SER).count()
cache和persist有什么区别?
- cache调用的persist,persist调用的persist(storage level)
序列化和非序列化有什么区别?
- 序列化将对象转换成字节数组了,节省空间,占CPU
- 开启kyro序列化(需要注册)
- spark默认序列化是java的序列化: implements java.io.Serializable
- 使用spark的序列化必须要注册:conf.set(“spark.serializer”, “org.apache.spark.serializer.KryoSerializer”)
object SerializationApp {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf()
/**
* 采用spark的序列化需要注册的
*/
sparkConf.registerKryoClasses(Array(classOf[Info]))
val sc = new SparkContext(sparkConf)
val flag = sc.getConf.getInt("spark.flag",0)
val infos = new ArrayBuffer[Info]()
val names = Array("feifei","jiajia","jianren")
val genders = Array("male","female")
val addresses = Array("beijing","shanghai","hangzhou","chengdu")
for (i <- 1 to 5000000){
val name = names(Random.nextInt(3))
val age = Random.nextInt(100)
val gender = genders(Random.nextInt(2))
val address= addresses(Random.nextInt(4))
infos += Info(name,age,gender,address)
}
val rdd = sc.parallelize(infos)
if(flag == 0) {
rdd.persist(StorageLevel.MEMORY_ONLY)
} else {
rdd.persist(StorageLevel.MEMORY_ONLY_SER)
}
println(rdd.count())
Thread.sleep(1000 * 60)
sc.stop()
}
case class Info(name: String, age: Int, gender: String,address:String)
}
启动脚本
spark-submit \
--class com.ruozedata.bigdata.SerializationApp \
--name SerializationApp \
--master local[2] \
--conf spark.serializer=org.apache.spark.serializer.KryoSerializer \
--conf spark.flag=1 \
/home/ruoze/lib/ruozedata-spark-core-1.0.jar
结果说明:
不使用序列化:上述数据保存到内存是171.7MB
使用java序列化:126.8MB
spark序列化不注册:165.0MB
spark序列化注册:120.1MB
缓存的移除
- Spark自动监视每个节点上的缓存使用情况,并以最近最少使用(LRU)的方式删除旧的数据分区。如果想要手动删除一个RDD,而不是等待它从缓存中消失,那么可以使用RDD.unpersist()方法。