1.spark的两种类型的共享变量:累加器(accumulator)与广播变量(broadcast variable),累加器用来对信息进行聚合,而广播变量用来高效分发较大的对象。
今天分析下累加器(accumulator):
提供了将工作节点中的值聚合到驱动器程序中的简单语法。
比如需要对Driver端的某个变量做累加操作,累加说的是,数值的相加或者字符串的拼接。如果直接用foreach是实现不了的(可以测试确实如此),因为该算子是无法把Executor累加的结果聚合到Driver端,这时可以用Accumulator累加器来实现累加的操作。
!!注意:
1.Accumulator只能实现累加,而且只能实现Driver端的变量做累加。
2.Executor端是无法读取累加的值的,只能Driver端读取
使用:
调用上下文的accumulator方法可以实现累加的,但该方法在spark2.0之后属于过期的方法,2.0之后我们要自定义Accumulator,必须继承AccumulatorV2,重写其中的方法。
//spark2.0 之前的做法
import org.apache.spark.{Accumulator, SparkConf, SparkContext}
object AccumulatorDemo2 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("AccumulatorDemo2").setMaster("local[2]")
val sc = new SparkContext(conf)
val nums = sc.parallelize(List(1, 2, 3, 4, 5, 6), 2)
//该变量是在Driver端累加的过程
val sum1: Accumulator[Int] = sc.accumulator(0)
//累加的过程是在多个Executor端进行分布式计算
nums.foreach(x => sum1 += x)
//不能用transformation类型的算子实现累加
// val res: RDD[Unit] = nums.map(x => sum1 += x)
println(sum1)
sc.stop()
}
}
时间到了Spark2.0,我们需要自定义accumulator
import org.apache.spark.{SparkConf, SparkContext}
/**
* 用2.0版本得AccumulatorV2实现累加
*/
object AccumulatorDemo3 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("AccumulatorDemo3").setMaster("local[2]")
val sc = new SparkContext(conf)
val numbers = sc.parallelize(List(1, 2, 3, 4, 5, 6), 2)
//创建自定义Accumulator
val accumulator = new AccumulatorTest()
//注册累加器
sc.register(accumulator, "acc")
numbers.foreach(accumulator.add(_))
println(accumulator.value)
sc.stop()
}
}
// 自定义的Accumulator
import org.apache.spark.util.AccumulatorV2
/**
* 在继承AccumulatorV2得时候需要实现泛型,给定输入和输出得类型
* 然后再重写几个方法
*/
class AccumulatorTest extends AccumulatorV2[Int, Int] {
//先创建输出值得变量
var sum: Int = _
//判断方法 初始值是否为空
override def isZero: Boolean = sum == 0
//copy一个新的累加器
override def copy(): AccumulatorV2[Int, Int] = {
//需要创建当前自定义累加器对象
val acc = new AccumulatorTest
//需要将当前数据拷贝到新的累加器数据里面
//也就是说原有累加器中得数据copy到新的累加器数据中
acc.sum = this.sum
acc
}
//重置一个累加器,将累加器中得数据初始化
override def reset(): Unit = sum = 0
//给定具体累加的过程,属于每一个分区进行累加的方法(局部累加方法)
override def add(v: Int): Unit = {
//v就是该分区中得某个元素
sum += v
}
//全局累加,合并每一个分区的累加器
override def merge(other: AccumulatorV2[Int, Int]): Unit = sum += other.value
//输出值
override def value: Int = sum
}
其实源码中也给出了一些实现
下面是一个Long类型的累加器LongAccumulator,可以看到是从2.0版本之后开始的,
/**
* An [[AccumulatorV2 accumulator]] for computing sum, count, and average of 64-bit integers.
*
* @since 2.0.0
*/
class LongAccumulator extends AccumulatorV2[jl.Long, jl.Long] {
private var _sum = 0L
private var _count = 0L
/**
* Adds v to the accumulator, i.e. increment sum by v and count by 1.
* @since 2.0.0
*/
override def isZero: Boolean = _sum == 0L && _count == 0
override def copy(): LongAccumulator = {
val newAcc = new LongAccumulator
newAcc._count = this._count
newAcc._sum = this._sum
newAcc
}
override def reset(): Unit = {
_sum = 0L
_count = 0L
}
/**
* Adds v to the accumulator, i.e. increment sum by v and count by 1.
* @since 2.0.0
*/
override def add(v: jl.Long): Unit = {
_sum += v
_count += 1
}
/**
* Adds v to the accumulator, i.e. increment sum by v and count by 1.
* @since 2.0.0
*/
def add(v: Long): Unit = {
_sum += v
_count += 1
}
/**
* Returns the number of elements added to the accumulator.
* @since 2.0.0
*/
def count: Long = _count
/**
* Returns the sum of elements added to the accumulator.
* @since 2.0.0
*/
def sum: Long = _sum
/**
* Returns the average of elements added to the accumulator.
* @since 2.0.0
*/
def avg: Double = _sum.toDouble / _count
override def merge(other: AccumulatorV2[jl.Long, jl.Long]): Unit = other match {
case o: LongAccumulator =>
_sum += o.sum
_count += o.count
case _ =>
throw new UnsupportedOperationException(
s"Cannot merge ${this.getClass.getName} with ${other.getClass.getName}")
}
private[spark] def setValue(newValue: Long): Unit = _sum = newValue
override def value: jl.Long = _sum
}
还有注意的是累加器的容错性和陷阱
Spark会自动重新运行执行失败的或者较慢的任务来应对有错误的或者比较慢的机器。例如,如果对某分区执行map操作的节点失败了,Spark会在另一个节点重新运行该任务。
即使该节点没有崩溃,只是处理速度比别的节点慢很多。Spark也可以抢占式的在另一个节点上启动一个"投机"(speculative)型的任务副本,如果该任务更早结束就可以直接获取结果。
即使没有节点失败,Spark有时也需要运行任务来获取缓存中被移除内存中的数据。因此最终结果就是一个同一个函数可能对同一个数据运行了多次。(听着就像mapreduce中的推测执行),
还好在于Spark中对于行动操作中使用的累加器,Spark只会把每个任务对各累加器的修改应用一次,因此,如果想要一个无论在失败还是重复计算时都绝对可靠的累加器,
我们必须把它放在foreach()这样的行动操作中。
对于RDD转换操作中使用的累加器,就不能保证这种情况了,这就是累加器的陷阱
官方这样解释:
For accumulator updates performed inside actions only,
Spark guarantees that each task’s update to the accumulator
will only be applied once, i.e. restarted tasks will not update the value.
In transformations, users should be aware of that each task’s update
may be applied more than once if tasks or job stages are re-executed.、
Spark中的转换操作会构成一长串的任务链,此时需要通过一个action操作来触发,accumulator也是如此。,所以我们可以调用cache或者persist,这样一来,前面的依赖就被切断了,后面的累加器就会被之前的转换操作影响到,
(注意!!如果内存不够,缓存被移除了又重新用到,这种非预期的多次更新就会发生),所以大家推荐的是accumulator in action,在转换操作中最好只是调试时使用。
3.关于SparkSQL
spark2.0之后,我们操作的sql对象为DataSet,DataFrame只是DataSet[Row](Row是弱类型)
DataSet有弱类型语言(untype)和强类型(typed)
(这里DataSet和DataFrame其实就是借鉴Flink的,如果你还不知道Flink,那么学完Spark,请一定找时间熟悉Flink。借外国友人说的分布式计算框架迭代过程 -First:mapreduce -> storm -> Spark ->Flink)
SparkSession作为新版本的上下文,可以用在sql和streaming中,SparkSession把SQLContext和HiveContext整合在一起
Spark2.0之前的sql是不支持开窗函数和子查询的,2.0之后开始实现了sql2003标准,开始支持了。
Spark2.0之前要用开窗函数和子查询怎么办,用Hive-on-Spark
Spark2.0可以支持csv格式数据的输入和输出
Spark2.0生成的默认数据格式为parquet(列式存储)
DataSet[Row] —DataFrame是包含RDD+schema信息(用来描述数据的数据)
SparkSql在编写的时候可以用DSL语言风格或者 SQL语句风格来操作
DataSet和DataFrame之间可以互相转换,用as方法
sparksql在获取json数据时,里面的数值数据会被解析成long类型
sparksql是无法对数据做增删改的,只能做查询
启用Hive:
当从Hive中读取数据时,Spark SQL支持任何Hive支持的存储格式(SerDe),包括文本文件、RCFiles、ORC、Parquet、Avro以及Protocol Buffer
要把SparkSql连接到已经部署好的Hive上,需要把hive-site.xml文件复制到Spark的./conf/目录下即可,如果不配置也是能跑的,此时使用本地的Hive元数据仓库。
代码中这样写,
1.在配置信息时加入config("spark.sql.warehouse.dir","d://spark-warehouse")
2.在getOrCreate方法之前调用enableHiveSupport()方法启动HIve支持
val spark = SparkSession
.builder()
.appName("SparkSQLDemo_3_Hive")
.master("local[2]")
.config("spark.sql.warehouse.dir", "d://spark-warehouse") // 指定warehouse的目录
.enableHiveSupport() //启用hive支持
.getOrCreate()
练习一下
import org.apache.spark.sql.SparkSession
/**
* 练习需求:统计各个部门员工的平局薪资和平均年龄
* 实现思路:
* 1.只统计年龄在20岁以上的员工
* 2.根据部门名称和员工性别进行分组统计
* 3.统计各个部门分性别的平均薪资和平均年龄
*/
object HomeWork01 {
def main(args: Array[String]): Unit = {
val spark = SparkSession
.builder()
.appName("HomeWork01")
.master("local[*]")
.getOrCreate()
import spark.implicits._
import org.apache.spark.sql.functions._
//获取数据
val employee = spark.read.json("d://temp/employee.json")
val department = spark.read.json("d://temp/department.json")
//开始统计
employee.filter("age > 20") // 只统计年龄在20岁以上的员工
.join(department, $"depId" === $"id") // 需要把部门信息join进来
.groupBy(department("name"), employee("gender"))
.agg(avg(employee("salary")), avg(employee("age"))) //进行聚合计算
.show() //调用action
spark.stop()
}
}
关于DataFrame和DataSet
转换:df.as[ElementType]这样就可以把DataFrame转化为DtaSet,
ds.toDF()这样可以把DataSet转换为DataFrame
import org.apache.spark.sql.{DataFrame, Dataset, SparkSession}
object HomeWork03_DataSet {
def main(args: Array[String]): Unit = {
//模板代码
val spark = SparkSession
.builder()
.appName("HomeWork03_DataSet")
.master("local[*]")
.getOrCreate()
val df: DataFrame = spark.read.json("hdfs://spark01:8020/data/people.json")
import spark.implicits._
//DataSet和DataFrame之间可以互相转换,用as方法
val personDS: Dataset[Person] = df.as[Person] // 调用as方法进行类型映射
//将现有的DataFrame转换为DataSet
val value1: Dataset[Int] = Seq(1, 2, 3, 4, 5, 6).toDS()
val value2: Dataset[(String, Int, Int)] = Seq(("xiaodong", 20, 90), ("dazhao", 25, 130)).toDS()
personDS.show()
value1.show()
value2.show()
}
}
//用于类型映射,字段名和数据中的字段名时以以映射的,需要一致
case class Person(name: String, age: Long, faceValue: Long)
6.SparkSQL运行原理
6.1通用SQL执行原理
在传统数据库中,最基本的 sql查询语句如SELECT fieldA,fieldB,fieldC FROM tableA WHERE fieldA > 10,由Projection(fieldA、fieldB、fieldC)、Data Source(tableA) 和Filter(fieldA > 10)三部分组成,分别对应SQL查询过程中的Result、Data Source和Operation,也就是说SQL语句按Result ->Data Source ->Operation 的次序来描述的,
但是实际执行中SQL语句的过程中时按照Operation -> Data Source ->Result 的顺序来执行的,与SQL语法刚好相反,其具体执行过程如下:
(1):词法和语法解析(Parse):对读入的SQL语句进行词法和语法解析(Parse),分辨出哪些是表达式,那些事Projection、那些是Data Source等,判断SQL语句是否规范,并形成逻辑计划。
(2):绑定(Bind):将SQL语句和数据库的数据字典(列,表和视图)进行绑定(Bind),如果相关的Projection和Data Source等都存在的话,则表示这个SQL语句是可以执行的。
(3)优化(Optimize):一般的数据库会提供几个执行计划,这些计划一般都有运行统计数据,数据库会在这些计划中·选择一个最优计划。
(4)执行(Execute):执行前面的步骤获取最优执行计划,返回数据库中查询的数据集。
一般来说,关系型数据库在运行的过程中,会在缓冲池缓存解析过的SQL语句,在后续的过程中能够命中缓存SQL就可以直接返回可执行的计划,比如重新运行刚才的运行过的SQL语句,可能0直接从数据库的缓冲池中返回结果。
未完待续。。。