本博文将详细分析和总结Spark SQL及其DataFrame、Dataset的相关原理和优化过程。
Catalyst Optimizer是基于scala中的函数编写的。
Catalyst Optimizer支持基于规则和基于成本优化。
Spark SQL优化可提高开发人员的生产力以及他们编写的查询的性能。一个好的查询优化器会自动重写关系查询,以使用诸如早期过滤数据,利用可用索引,甚至确保以最有效的顺序连接不同的数据源之类的技术来更有效地执行。
通过执行这些转换,优化器改善了关系查询的执行时间,并使开发人员从专注于其应用程序的语义而非性能上解放出来。
Catalyst利用Scala的强大功能(例如模式匹配和运行时元编程),使开发人员可以简明地指定复杂的关系优化。
在spark2.0版本之前
val conf=newSparkConf()
val sc = new SparkContext(conf)
val hc = new hiveContext(sc)
val ssc = new streamingContext(sc).
在spark2.0及其后续版本
val session = SparkSession.builder()
.enableHiveSupport() //提供了与hive metastore的连接。
.getOrCreate()
// Import the implicits, unlike in core Spark the implicits are defined
// on the context.
import session.implicits._
和RDD一样,Spark SQL下的DataFrame和Dataset也是一个分布式的集合。但是相对于RDD,DataFrame和Dataset多了一个额外的Schema信息。如上面所述,Schemas可在Catalyst优化器中使用。
case class RawPanda(id: Long, zip: String, pt: String, happy: Boolean, attributes: Array[Double])
case class PandaPlace(name: String, pandas: Array[RawPanda])
def createAndPrintSchema() = {
val damao = RawPanda(1, "M1B 5K7", "giant", true, Array(0.1, 0.1))
val pandaPlace = PandaPlace("toronto", Array(damao))
val df = session.createDataFrame(Seq(pandaPlace))
df.printSchema()
}
import org.apache.spark.{SparkContext, SparkConf}
val personRDD = sc.textFile(args(0)).map(_.split(" "))
//通过StructType直接指定每个字段的schema
val schema = StructType(
List(
StructField("id", IntegerType, true),
StructField("name", StringType, true),
StructField("age", IntegerType, true)
)
)
//将RDD映射到rowRDD
val rowRDD = personRDD.map(p => Row(p(0).toInt, p(1).trim, p(2).toInt))
//将schema信息应用到rowRDD上
val personDataFrame = sqlContext.createDataFrame(rowRDD, schema)
DataFrame与RDD类似,同样拥有 不变性,弹性,分布式计算的特性,也有惰性设计,有transform(转换)与action(执行)操作之分。相对于RDD,它能处理大量结构化数据,DataFrame包含带有Schema的行,类似于pandas的DataFrame的 header行。
注意:相对于RDD的lazy设计,DataFrame只是部分的lazy,例如schema是立即执行的。
相对于RDD,DataFrame提供了内存管理和优化的执行计划。
关于jvm内存,可查看 JVM中的堆外内存(off-heap memory)与堆内内存(on-heap memory)
更多关于 Tungsten,可查看 Tungsten-github
DataFrame 经验 tips:
def minMeanSizePerZip(pandas: DataFrame): DataFrame = {
// Compute the min and mean
pandas.groupBy(pandas("zip")).agg(
min(pandas("pandaSize")), mean(pandas("pandaSize")))
}
def registerTable(df: DataFrame): Unit = {
df.registerTempTable("pandas") //将pandas注册为一个临时table
df.write.saveAsTable("perm_pandas")
}
def querySQL(): DataFrame = {
sqlContext.sql("SELECT * FROM pandas WHERE size > 0") //即可利用sql表达式对临时表进行查询操作,返回的也是一个DataFrame
}
Dataset是SparkSQL中的一种数据结构,它是强类型的,包含指定的schema(指定了变量的类型)。Dataset是对DataFrame API的扩展。Spark Dataset 提供了类型安全和面向对象的编程接口。
关于强类型和类型安全的定义可参考 Magic lies here - Statically vs Dynamically Typed Languages
Dataset有以下特点:
RDD转DataFrame(行动操作,立即执行)时,需要指定schema信息,有如下三种方法:
def createFromCaseClassRDD(input: RDD[PandaPlace]) = {
// Create DataFrame explicitly using session and schema inference
val df1 = session.createDataFrame(input)
// Create DataFrame using session implicits and schema inference
val df2 = input.toDF()
// Create a Row RDD from our RDD of case classes
val rowRDD = input.map(pm => Row(pm.name,
pm.pandas.map(pi => Row(pi.id, pi.zip, pi.happy, pi.attributes))))
val pandasType = ArrayType(StructType(List(
StructField("id", LongType, true),
StructField("zip", StringType, true),
StructField("happy", BooleanType, true),
StructField("attributes", ArrayType(FloatType), true))))
// Create DataFrame explicitly with specified schema
val schema = StructType(List(StructField("name", StringType, true),
StructField("pandas", pandasType)))
val df3 = session.createDataFrame(rowRDD, schema)
}
DataFrame转RDD(转换操作,行动操作再执行),简单的df.rdd得到的是个Row Object,因为每行可以包含任意内容,你需要指定特别的类型,这样你才能获取每列的内容:
def toRDD(input: DataFrame): RDD[RawPanda] = {
val rdd: RDD[Row] = input.rdd
rdd.map(row => RawPanda(row.getAs[Long](0), row.getAs[String](1),
row.getAs[String](2), row.getAs[Boolean](3), row.getAs[Array[Double]](4)))
}
转Dataset
def fromDF(df: DataFrame): Dataset[RawPanda] = {
df.as[RawPanda]//RawPanda为一个case class
}
// rdd转 Dataset,可以先转 DataFrame再转Dataset
/**
* Illustrate converting a Dataset to an RDD
*/
def toRDD(ds: Dataset[RawPanda]): RDD[RawPanda] = {
ds.rdd
}
/**
* Illustrate converting a Dataset to a DataFrame
*/
def toDF(ds: Dataset[RawPanda]): DataFrame = {
ds.toDF()
}
如果你用的是 Spark SQL 的查询语句,要直到运行时你才会发现有语法错误(这样做代价很大),而如果你用的是 DataFrame 和 Dataset,你在编译时就可以捕获syntax errors(这样就节省了开发者的时间和整体代价)。也就是说,当你在 DataFrame 中调用了 API 之外的函数时,编译器就可以发现这个错。不过,如果你使用了一个不存在的字段名字,那就要到运行时才能发现错误了。
Dataset API 都是用 lambda 函数和 JVM 类型对象表示的,所有不匹配的类型参数都可以在编译时发现。而且在使用 Dataset 时,你的Analysis errors 也会在编译时被发现,这样就节省了开发者的时间和代价。例如DataFrame编译时不检查列信息(例如无论你写df.select(“name”) 还是 df.select(“naame”) 编译时均不会报错,而实际运行时才会报错),而Dataset在编译时就会检查到该类错误。
如何针对数据分布自定义分区方式,这对于避免令人头痛的数据倾斜非常重要。
val repartRdd = originRdd
// 切割
.flatMap(_.split(" "))
// 映射为元组
.map((_, 1))
// 给key加上随机数,聚合时候具有随机性
.map(t => {
val rnum = Random.nextInt(partitionNum)
(t._1 + "_" + rnum, 1)
})
// 初次聚合
.reduceByKey(_ + _)
// 去除key随机后缀
.map(e => {
val word = e._1.toString().substring(0, e._1.toString().indexOf("_"))
val count = e._2
(word, count)
})
// 再次聚合
.reduceByKey(_ + _)
// 排序(false->降序, true->升序)
.map(e => (e._2, e._1)).sortByKey(false).map(e => (e._2, e._1))
可参考 Spark中的分区方法详解
,个人感觉这篇博客已经写得非常详细完整。
Spark.DataFrame 与 DataSet 无自定义分区方式,可先将rdd自定分区完成,再转成DataFrame。
sqlContext.createDataFrame(
df.rdd.map(r => (r.getInt(1), r)).partitionBy(partitioner).values,
df.schema
)
User-defined functions(udfs) 和 user-defined aggregate functions(udafs) 提供了使用自己的自定义代码扩展DataFrame和SQL API的方法,同时保留了Catalyst优化器。这对性能的提高非常有用,否则您需要将数据转换为RDD(并可能再次转换)来执行任意函数,这非常昂贵。udf和udaf也可以使用SQL查询表达式进行 内部访问。
注:使用python编写udf和udaf函数,会丢失性能优势。
spark 2.x:
def get_max(x: Double, y: Double): Double={
if ( x > y )
x
else
y
}
val udf_get_max = udf(get_max _)
df = df.withColumn("max_fea", udf_get_max(df("fea1"), df("fea2")))
相对于udfs,udafs编写较为复杂,需要继承 UserDefinedAggregateFunction 和重写里面的部分函数,且UDAFs的性能相当好。可以直接在列上使用UDAF,也可以像对非聚合UDF那样将其添加到函数注册表中。
计算平均值的UDAF例子代码:
import org.apache.spark.sql.Row
import org.apache.spark.sql.expressions.{MutableAggregationBuffer, UserDefinedAggregateFunction}
import org.apache.spark.sql.types._
object AverageUserDefinedAggregateFunction extends UserDefinedAggregateFunction {
// 聚合函数的输入数据结构
override def inputSchema: StructType = StructType(StructField("input", LongType) :: Nil)
// 缓存区数据结构
override def bufferSchema: StructType = StructType(StructField("sum", LongType) :: StructField("count", LongType) :: Nil)
// 聚合函数返回值数据结构
override def dataType: DataType = DoubleType
// 聚合函数是否是幂等的,即相同输入是否总是能得到相同输出
override def deterministic: Boolean = true
// 初始化缓冲区
override def initialize(buffer: MutableAggregationBuffer): Unit = {
buffer(0) = 0L
buffer(1) = 0L
}
// 给聚合函数传入一条新数据进行处理
override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
if (input.isNullAt(0)) return
buffer(0) = buffer.getLong(0) + input.getLong(0)
buffer(1) = buffer.getLong(1) + 1
}
// 合并聚合函数缓冲区
override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
buffer1(0) = buffer1.getLong(0) + buffer2.getLong(0)
buffer1(1) = buffer1.getLong(1) + buffer2.getLong(1)
}
// 计算最终结果
override def evaluate(buffer: Row): Any = buffer.getLong(0).toDouble / buffer.getLong(1)
}
然后在主函数里注册并使用该函数:
spark.read.json("data/user").createOrReplaceTempView("v_user")
spark.udf.register("u_avg", AverageUserDefinedAggregateFunction)
// 将整张表看做是一个分组对求所有人的平均年龄
spark.sql("select count(1) as count, u_avg(age) as avg_age from v_user").show()
// 按照性别分组求平均年龄
spark.sql("select sex, count(1) as count, u_avg(age) as avg_age from v_user group by sex").show()