在spark scala中将行合并到单个结构列中存在效率问题,我们如何做得更好?
我试图加快并限制获取多个列及其值并将它们插入同一行中的映射的成本。这是一个要求,因为我们有一个从这个工作中读取的遗留系统,它还没有准备好进行重构。还有另一张地图,其中包含一些需要与此结合的数据 目前,我们有几种解决方案,所有这些解决方案似乎都能在同一集群上实现大致相同的运行时间,在Parquet中存储大约1TB的数据:在spark scala中将行合并到单个结构列中存在效率问题,我们如何做得更好?,scala,apache-spark,Scala,Apache Spark,我试图加快并限制获取多个列及其值并将它们插入同一行中的映射的成本。这是一个要求,因为我们有一个从这个工作中读取的遗留系统,它还没有准备好进行重构。还有另一张地图,其中包含一些需要与此结合的数据 目前,我们有几种解决方案,所有这些解决方案似乎都能在同一集群上实现大致相同的运行时间,在Parquet中存储大约1TB的数据: import org.apache.spark.sql.functions._ import org.apache.spark.sql.types._ import org.js
import org.apache.spark.sql.functions._
import org.apache.spark.sql.types._
import org.json4s._
import org.json4s.jackson.JsonMethods._
import spark.implicits._
def jsonToMap(s: String, map: Map[String, String]): Map[String, String] = {
implicit val formats = org.json4s.DefaultFormats
val jsonMap = if(!s.isEmpty){
parse(s).extract[Map[String, String]]
} else {
Map[String, String]()
}
if(map != null){
map ++ jsonMap
} else {
jsonMap
}
}
val udfJsonToMap = udf(jsonToMap _)
def addMap(key:String, value:String, map: Map[String,String]): Map[String,String] = {
if(map == null) {
Map(key -> value)
} else {
map + (key -> value)
}
}
val addMapUdf = udf(addMap _)
val output = raw.columns.foldLeft(raw.withColumn("allMap", typedLit(Map.empty[String, String]))) { (memoDF, colName) =>
if(colName.startsWith("columnPrefix/")){
memoDF.withColumn("allMap", when(col(colName).isNotNull, addMapUdf(substring_index(lit(colName), "/", -1), col(colName), col("allTagsMap")) ))
} else if(colName.equals("originalMap")){
memoDF.withColumn("allMap", when(col(colName).isNotNull, udfJsonToMap(col(colName), col("allMap"))))
} else {
memoDF
}
}
在9 m5.xlarge上大约需要1小时
val resourceTagColumnNames = raw.columns.filter(colName => colName.startsWith("columnPrefix/"))
def structToMap: Row => Map[String,String] = { row =>
row.getValuesMap[String](resourceTagColumnNames)
}
val structToMapUdf = udf(structToMap)
val experiment = raw
.withColumn("allStruct", struct(resourceTagColumnNames.head, resourceTagColumnNames.tail:_*))
.select("allStruct")
.withColumn("allMap", structToMapUdf(col("allStruct")))
.select("allMap")
在同一集群上运行约1小时
这段代码都可以工作,但速度不够快,它比我们现在使用的其他变换长10倍左右,这对我们来说是一个瓶颈
有没有其他更有效的方法来获得这个结果
编辑:我也尝试过用一个键限制数据,但是因为我正在合并的列中的值可以更改,尽管键保持不变,但我无法限制数据大小而不冒数据丢失的风险 Tl;DR:仅使用spark sql内置函数可以显著加快计算速度 如中所述,spark sql本机函数比 性能优于用户定义的功能。因此,我们可以尝试只使用 spark sql本机函数 我展示了实现的两个主要版本。一个使用上一版本中存在的所有sql函数 在我写这个答案的时候,Spark是可用的,它是Spark 3.0。另一个只使用sql函数 问题提出时,spark版本中存在,因此spark 2.3中存在函数。所有使用的函数 在该版本中,Spark 2.2中也提供了 使用sql函数实现Spark 3.0
import org.apache.spark.sql.functions_
导入org.apache.spark.sql.types.{MapType,StringType}
val mapFromPrefixedColumns=map\u过滤器(
map(raw.columns.filter(u.startsWith(“columnprix/”)))).flatMap(c=>Seq(lit(c.dropWhile(!='/')).tail),col(c)):*),
(u,v)=>v.isNotNull
)
val mapFromOriginalMap=when(col(“originalMap”).isNotNull和col(“originalMap”).notEqual(“”),
来自_json(col(“originalMap”),映射类型(StringType,StringType))
).否则(
地图()
)
val comprehensiveMapExpr=map_concat(mapFromPrefixedColumns,mapFromOriginalMap)
原始。带列(“所有地图”,综合地图地图)
使用sql函数实现Spark 2.2
在spark 2.2中,我们没有功能map\u concat
(在spark 2.4中提供)和map\u filter
(在spark 3.0中提供)。
我用用户定义的函数替换它们:
import org.apache.spark.sql.functions_
导入org.apache.spark.sql.types.{MapType,StringType}
def filterNull(map:map[String,String]):map[String,String]=map.toSeq.filter(u.\u 2!=null).toMap
val filter\u null\u udf=udf(filterNull)
def mapConcat(map1:Map[String,String],map2:Map[String,String]):Map[String,String]=map1++map2
val map_concat_udf=udf(mapConcat)
val mapFromPrefixedColumns=filter\u null\u udf(
map(raw.columns.filter(u.startsWith(“columnprix/”)))).flatMap(c=>Seq(lit(c.dropWhile(!='/')).tail),col(c)):*)
)
val mapFromOriginalMap=when(col(“originalMap”).isNotNull和col(“originalMap”).notEqual(“”),
来自_json(col(“originalMap”),映射类型(StringType,StringType))
).否则(
地图()
)
val comprehensiveMapExpr=map\u concat\u udf(mapFromPrefixedColumns,mapFromOriginalMap)
原始。带列(“所有地图”,综合地图地图)
使用无json映射的sql函数实现
问题的最后一部分包含一个简化的代码,没有对json列进行映射,也没有对
结果映射中的空值。我为这个具体案例创建了以下实现。因为我不使用函数
在spark 2.2和spark 3.0之间添加的,我不需要此实现的两个版本:
import org.apache.spark.sql.functions_
val mapFromPrefixedColumns=map(原始的.columns.filter(u.startsWith(“columnprofix/”)).flatMap(c=>Seq(lit(c),col(c)):*)
原始.withColumn(“allMap”,mapFromPrefixedColumns)
跑
对于以下数据帧作为输入:
+--------------------+--------------------+--------------------+----------------+
|columnPrefix/column1|columnPrefix/column2|columnPrefix/column3|originalMap |
+--------------------+--------------------+--------------------+----------------+
|a |1 |x |{"column4": "k"}|
|b |null |null |null |
|c |null |null |{} |
|null |null |null |null |
|d |2 |null | |
+--------------------+--------------------+--------------------+----------------+
我获得以下allMap
列:
+--------------------------------------------------------+
|allMap |
+--------------------------------------------------------+
|[column1 -> a, column2 -> 1, column3 -> x, column4 -> k]|
|[column1 -> b] |
|[column1 -> c] |
|[] |
|[column1 -> d, column2 -> 2] |
+--------------------------------------------------------+
对于不带json列的映射:
+---------------------------------------------------------------------------------+
|allMap |
+---------------------------------------------------------------------------------+
|[columnPrefix/column1 -> a, columnPrefix/column2 -> 1, columnPrefix/column3 -> x]|
|[columnPrefix/column1 -> b, columnPrefix/column2 ->, columnPrefix/column3 ->] |
|[columnPrefix/column1 -> c, columnPrefix/column2 ->, columnPrefix/column3 ->] |
|[columnPrefix/column1 ->, columnPrefix/column2 ->, columnPrefix/column3 ->] |
|[columnPrefix/column1 -> d, columnPrefix/column2 -> 2, columnPrefix/column3 ->] |
+---------------------------------------------------------------------------------+
基准
我生成了一个包含1000万行的csv文件,未压缩(约800 Mo),其中包含一列,没有列前缀,
九列带有列前缀,一列包含json作为字符串的冒号:
+---+--------------------+--------------------+--------------------+--------------------+--------------------+--------------------+--------------------+--------------------+--------------------+-------------------+
|id |columnPrefix/column1|columnPrefix/column2|columnPrefix/column3|columnPrefix/column4|columnPrefix/column5|columnPrefix/column6|columnPrefix/column7|columnPrefix/column8|columnPrefix/column9|originalMap |
+---+--------------------+--------------------+--------------------+--------------------+--------------------+--------------------+--------------------+--------------------+--------------------+-------------------+
|1 |iwajedhor |ijoefzi |der |ob |galsu |ril |le |zaahuz |fuzi |{"column10":"true"}|
|2 |ofo |davfiwir |lebfim |roapej |lus |roum |te |javes |karutare |{"column10":"true"}|
|3 |jais |epciel |uv |piubnak |saajo |doke |ber |pi |igzici |{"column10":"true"}|
|4 |agami |zuhepuk |er |pizfe |lafudbo |zan |hoho |terbauv |ma |{"column10":"true"}|
...
基准是读取此csv文件,创建列allMap
,然后将此列写入parquet。我在我的本地机器上运行了这个,得到了以下结果
+--------------------------+--------------------+-------------------------+-------------------------+
| implementations | current (with udf) | sql functions spark 3.0 | sql functions spark 2.2 |
+--------------------------+--------------------+-------------------------+-------------------------+
| execution time | 138 seconds | 48 seconds | 82 seconds |
| improvement from current | 0 % faster | 64 % faster | 40 % faster |
+--------------------------+--------------------+-------------------------+-------------------------+
我还遇到了问题中的第二个实现,即删除json列的映射和映射中空值的过滤
+--------------------------+-----------------------+------------------------------------+
| implementations | current (with struct) | sql functions without json mapping |
+--------------------------+-----------------------+------------------------------------+
| execution time | 46 seconds | 35 seconds |
| improvement from current | 0 % | 23 % faster |
+--------------------------+-----------------------+------------------------------------+
当然,基准测试非常基本,但与使用用户定义函数的实现相比,我们可以看到一个改进
结论
当您遇到性能问题并且使用用户定义的函数时,最好尝试使用
spark sql函数感谢Vincent,我们最终用与您在2.x版本中实现的类似的解决方案解决了这个问题。我完全忘记了这个问题,也没有再提这个问题。谢谢你做了这些工作。@alexddupree太好了!你还记得它提高了你的跑步时间吗?我有点好奇,因为在我运行的小型基准测试中,我的性能得到了广泛的提高,从64%提高到23%。我想性能的提高取决于输入数据。