读取,转换和写入DataFrame中每个分区内的数据

时间:2019-05-10 10:30:36

标签: scala apache-spark apache-spark-sql

语言-Scala

火花版本-2.4

我对Scala和Spark都是陌生的。 (我来自python背景,所以整个JVM生态系统对我来说还是很新的)

我想编写一个火花程序来并行执行以下步骤:

  1. 从数据框中的S3读取数据

  2. 转换此数据框的每一行

  3. 在新位置将更新的数据帧写回到S3

假设我有3个项目,A,B和C。对于每个项目,我都想执行以上3个步骤。

我想对所有这三个项目并行执行此操作。

我尝试创建一个具有3个分区的RDD,其中每个分区分别具有一项,即A,B和C。

然后,我尝试使用mapPartition方法为每个分区编写逻辑(上面提到的3个步骤)。

我遇到Task not serializable个错误。尽管我了解此错误的含义,但我不知道如何解决。

val items = Array[String]("A", "B", "C")
val rdd = sc.parallelize(items, 3)

rdd.mapPartitions(
partition => {
    val item = partition.next()

    val filePath = new ListBuffer[String]()
    filePath += s"$basePath/item=$item/*"

    val df = spark.read.format("parquet").option("basePath",s"$basePath").schema(schema).load(filePaths: _*)

    //Transform this dataframe
    val newDF = df.rdd.mapPartitions(partition => {partition.map(row =>{methodToTransformAndReturnRow(row)})})

    newDf.write.mode(SaveMode.Overwrite).parquet(path)
})

我的用例是,对于每个项目,从S3中读取数据,对其进行转换(我将用例的新列直接添加到每一行中),然后将每个项目的最终结果写回到S3。

注意-我可以读取整个数据,按项目重新分区,进行转换并将其写回,但是重新分区会导致随机播放,这是我想要避免的,而我尝试的方法是,可以读取执行程序本身中每个项目的数据,以便它可以处理所获取的任何数据,而无需进行重新排序。

1 个答案:

答案 0 :(得分:-1)

我不确定您要使用所展示的方法要实现什么,但是我觉得您可能会以困难的方式进行操作。除非有充分的理由这样做,否则通常最好让Spark(尤其是spark 2.0+)让它自己做。在这种情况下,只需使用一个操作即可处理所有三个分区。 Spark通常会很好地管理您的数据集。它还可能会自动引入您没有想到的优化,或者如果您尝试过多地控制过程,则可能无法实现的优化。话虽如此,如果它不能很好地管理流程,那么您可以通过尝试进行更多控制和手动操作来开始争论。到目前为止,至少这是我的经验。

例如,我曾经进行过一系列复杂的转换,为每个步骤/ DataFrame添加了更多的逻辑。如果我强迫Spark评估每个中间帧(例如在中间数据帧上进行计数或显示),我最终会由于无法满足需要而无法评估一个DataFrame(即,它无法进行计数)资源。但是,如果我忽略了这一点,并添加了更多的转换,Spark可以将某些优化推向较早的步骤(从较晚的步骤开始)。这意味着可以正确地评估后续的DataFrame(并且重要的是我的最终DataFrame)。最终评估可能是尽管,但无法评估中间的DataFrame本身仍在整个过程中。

请考虑以下内容。我使用CSV,但是对镶木地板也一样。

这是我的输入内容:

data
├── tag=A
│   └── data.csv
├── tag=B
│   └── data.csv
└── tag=C
    └── data.csv

以下是其中一个数据文件(tag = A / data.csv)的示例

id,name,amount
1,Fred,100
2,Jane,200

这是一个可识别此结构内分区的脚本(即tag是列之一)。

scala> val inDataDF = spark.read.option("header","true").option("inferSchema","true").csv("data")
inDataDF: org.apache.spark.sql.DataFrame = [id: int, name: string ... 2 more fields]

scala> inDataDF.show
+---+-------+------+---+
| id|   name|amount|tag|
+---+-------+------+---+
| 31|  Scott|  3100|  C|
| 32|Barnaby|  3200|  C|
| 20|   Bill|  2000|  B|
| 21|  Julia|  2100|  B|
|  1|   Fred|   100|  A|
|  2|   Jane|   200|  A|
+---+-------+------+---+


scala> inDataDF.printSchema
root
 |-- id: integer (nullable = true)
 |-- name: string (nullable = true)
 |-- amount: integer (nullable = true)
 |-- tag: string (nullable = true)


scala> inDataDF.write.partitionBy("tag").csv("outData")

scala> 

同样,我使用csv而不是parquet,因此您可以省去读取标头并推断模式的选项(parquet将自动执行此操作)。但除此之外,它将以相同的方式工作。

上面产生了以下目录结构:

outData/
├── _SUCCESS
├── tag=A
│   └── part-00002-9e13ec13-7c63-4cda-b5af-e2d69cb76278.c000.csv
├── tag=B
│   └── part-00001-9e13ec13-7c63-4cda-b5af-e2d69cb76278.c000.csv
└── tag=C
    └── part-00000-9e13ec13-7c63-4cda-b5af-e2d69cb76278.c000.csv

如果您想操纵内容,一定要在读写之间添加任何映射操作,连接,过滤或其他所需的操作。

例如,将金额加500:

scala> val outDataDF = inDataDF.withColumn("amount", $"amount" + 500)
outDataDF: org.apache.spark.sql.DataFrame = [id: int, name: string ... 2 more fields]

scala> outDataDF.show(false)
+---+-------+------+---+
|id |name   |amount|tag|
+---+-------+------+---+
|31 |Scott  |3600  |C  |
|32 |Barnaby|3700  |C  |
|20 |Bill   |2500  |B  |
|21 |Julia  |2600  |B  |
|1  |Fred   |600   |A  |
|2  |Jane   |700   |A  |
+---+-------+------+---+

然后只需写出DataDF而不是inDataDF。