我试图将存储在S3中的数据作为JSON-per-line文本文件转换为结构化的柱状格式,如ORC或S3上的Parquet。
源文件包含多个方案的数据(例如HTTP请求,HTTP响应......),需要将其解析为正确类型的不同Spark Dataframe。
示例模式:
val Request = StructType(Seq(
StructField("timestamp", TimestampType, nullable=false),
StructField("requestId", LongType),
StructField("requestMethod", StringType),
StructField("scheme", StringType),
StructField("host", StringType),
StructField("headers", MapType(StringType, StringType, valueContainsNull=false)),
StructField("path", StringType),
StructField("sessionId", StringType),
StructField("userAgent", StringType)
))
val Response = StructType(Seq(
StructField("timestamp", TimestampType, nullable=false),
StructField("requestId", LongType),
StructField("contentType", StringType),
StructField("contentLength", IntegerType),
StructField("statusCode", StringType),
StructField("headers", MapType(keyType=StringType, valueType=StringType, valueContainsNull=false)),
StructField("responseDuration", DoubleType),
StructField("sessionId", StringType)
))
我让那部分工作正常,但是尽可能有效地将数据写回S3似乎是个问题。
我尝试了3种方法:
在第一种情况下,JVM内存不足,而在第二种情况下,机器的磁盘空间不足。
第三个我尚未经过全面测试,但这似乎并不能有效利用处理能力(因为只有一个集群节点(这个特定分区所在的节点)实际上会写入数据退回S3)。
相关代码:
val allSchemes = Schemes.all().keys.toArray
if (false) {
import com.realo.warehouse.multiplex.implicits._
val input = readRawFromS3(inputPrefix) // returns RDD[Row]
.flatMuxPartitions(allSchemes.length, data => {
val buffers = Vector.tabulate(allSchemes.length) { j => ArrayBuffer.empty[Row] }
data.foreach {
logItem => {
val schemeIndex = allSchemes.indexOf(logItem.logType)
if (schemeIndex > -1) {
buffers(schemeIndex).append(logItem.row)
}
}
}
buffers
})
allSchemes.zipWithIndex.foreach {
case (schemeName, index) =>
val rdd = input(index)
writeColumnarToS3(rdd, schemeName)
}
} else if (false) {
// Naive approach
val input = readRawFromS3(inputPrefix) // returns RDD[Row]
.persist(StorageLevel.MEMORY_AND_DISK)
allSchemes.foreach {
schemeName =>
val rdd = input
.filter(x => x.logType == schemeName)
.map(x => x.row)
writeColumnarToS3(rdd, schemeName)
}
input.unpersist()
} else {
class CustomPartitioner extends Partitioner {
override def numPartitions: Int = allSchemes.length
override def getPartition(key: Any): Int = allSchemes.indexOf(key.asInstanceOf[String])
}
val input = readRawFromS3(inputPrefix)
.map(x => (x.logType, x.row))
.partitionBy(new CustomPartitioner())
.map { case (logType, row) => row }
.persist(StorageLevel.MEMORY_AND_DISK)
allSchemes.zipWithIndex.foreach {
case (schemeName, index) =>
val rdd = input
.mapPartitionsWithIndex(
(i, iter) => if (i == index) iter else Iterator.empty,
preservesPartitioning = true
)
writeColumnarToS3(rdd, schemeName)
}
input.unpersist()
}
从概念上讲,我认为每个方案类型的代码应该有1个输出DStream,输入RDD应该选择将每个处理过的项放到正确的DStream上(通过批处理获得更好的吞吐量)。
有没有人对如何实现这一点有任何指示?和/或是否有更好的方法来解决这个问题?
答案 0 :(得分:0)
鉴于输入是json,您可以将其读入字符串的数据帧(每行是单个字符串)。然后,您可以从每个json中提取类型(通过使用UDF或使用get_json_object或json_tuple等函数)。
现在您有两列:类型和原始json。现在,您可以在编写数据帧时使用partitionBy dataframe选项。这将导致每种类型的目录,目录的内容将包括原始的jsons。
现在,您可以使用自己的架构读取每种类型。
你也可以用RDD做一个类似的事情,使用一个map将输入rdd变成一对rdd,其中key是类型,值是json转换为目标模式。然后,您可以使用partitionBy和map partition将每个分区保存到文件中,或者可以使用reduce by key写入不同的文件(例如,使用密钥设置文件名)。
您还可以查看Write to multiple outputs by key Spark - one Spark job
请注意,我在此假设目标是拆分为文件。根据您的具体用例,其他选项可能是可行的。例如,如果您的不同模式足够接近,则可以创建一个包含所有模式的超级模式,并直接从该模式创建数据框。然后你可以直接处理数据帧,也可以使用dataframe partitionBy将不同的子类型写入不同的目录(但这次已经保存到了镶木地板)。
答案 1 :(得分:0)
这是我最终提出的:
我使用自定义分区程序根据数据的方案加上行的哈希码对数据进行分区。
这里的原因是我们希望能够只处理某些分区,但仍允许所有节点参与(出于性能原因)。因此,我们不会仅在1个分区上传播数据,而是在X个分区上传播数据(在此示例中,X是节点数乘以2)。
然后对于每个方案,我们修剪我们不需要的分区,因此我们只处理我们所做的分区。
代码示例:
Item