如何使用结构化流的writestream进行分区分区的文件写入?

时间:2019-12-10 18:34:27

标签: apache-spark pyspark spark-streaming spark-structured-streaming

我有一个结构化的流代码,可以从Kafka读取数据并将其转储到HDFS。转储数据时,我根据三列对数据进行分区。我面临的问题是在批处理过程中生成了许多小文件。我想在每个partitionBy的批处理过程中仅生成一个文件。我不确定如何在这种情况下应用分区,因为它似乎不起作用。

        query = df.selectExpr("CAST(value as STRING)") \
                .repartition(1) \
                .writeStream.partitionBy('host', 'dt', 'h') \ ==> repartition(1) is not working here
                .format("parquet") \
                .outputMode("append") \
                .option("checkpointLocation", self.checkpoint_location) \
                .option('path', self.hdfs_path) \
                .option('failOnDataLoss', 'false') \
                .option("startingOffset", "earliest") \
                .trigger(processingTime='2 seconds').start()

我不想编写另一个清理作业,该作业从路径中读取数据,对其进行重新分区,并在每个分区中存储具有所需文件数的数据。

1 个答案:

答案 0 :(得分:1)

我使用分区进行了一些测试,它似乎对我有用。 我创建了一个测试Kafka主题,它具有字符串格式getCell()的数据。然后,在流式代码中,我在id-value上拆分了value并使用-写入数据来模仿您的代码行为。我正在使用kafka经纪人0.10和Spark版本2.4.3。

请参见以下代码:

partitionBy('id')

使用from pyspark.sql.functions import col, split df = spark \ .readStream \ .format("kafka") \ .option("kafka.bootstrap.servers", "localhost:9092") \ .option("subscribe", "partitionTestTopic") \ .option("startingOffsets", "earliest") \ .load()

repartition(1)

输出:

df.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)") \
  .withColumn('id', split(col('value'), '-').getItem(0)) \
  .repartition(1) \
  .writeStream.partitionBy('id') \
  .format("parquet") \
  .outputMode("append") \
  .option('path', 'test-1/data') \
  .option("checkpointLocation", "test-1/checkpoint") \
  .trigger(processingTime='20 seconds') \
  .start()

使用├── id=1 │   └── part-00000-9812bd07-3c0f-442e-a80c-5c09553f20e8.c000.snappy.parquet ├── id=2 │   └── part-00000-522e99b6-3900-4702-baf7-2c55819b775c.c000.snappy.parquet ├── id=3 │   └── part-00000-5ed9bef0-4941-451f-884e-8e94a351323f.c000.snappy.parquet

repartition(3)

输出:

df.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)") \
  .withColumn('id', split(col('value'), '-').getItem(0)) \
  .repartition(3) \
  .writeStream.partitionBy('id') \
  .format("parquet") \
  .outputMode("append") \
  .option('path', 'test-3/data') \
  .option("checkpointLocation", "test-3/checkpoint") \
  .trigger(processingTime='20 seconds') \
  .start()

您提到您正在使用├── id=1 │   ├── part-00000-ed6ed5dd-b376-40a2-9920-d0cb36c7120f.c000.snappy.parquet │   ├── part-00001-fa64e597-a4e1-4ac2-967f-5ea8aae96c13.c000.snappy.parquet │   └── part-00002-0e0feab8-57d8-4bd2-a94f-0206ff90f16e.c000.snappy.parquet ├── id=2 │   ├── part-00000-c417dac5-271f-4356-b577-ff6c9f45792e.c000.snappy.parquet │   ├── part-00001-7c90eb8a-986a-4602-a386-50f8d6d85e77.c000.snappy.parquet │   └── part-00002-0e59e779-84e8-4fcf-ad62-ef3f4dbaccd5.c000.snappy.parquet ├── id=3 │   ├── part-00000-8a555649-1141-42fe-9cb5-0acf0efc5997.c000.snappy.parquet │   ├── part-00001-ce4aaa50-e41b-4f5f-837c-661459b747b8.c000.snappy.parquet │   └── part-00002-9f95261e-bd4c-4f1e-bce2-f8ab3b8b01ec.c000.snappy.parquet 批次,因此也应相应地更新触发间隔。现在是10-minute,但应该是2 seconds(请参阅trigger(processingTime='10 minutes') here)。这可能就是为什么您得到太多小文件的原因。

如果您将class ProcessingTime(Trigger)reaprtition(1)一起使用,将会有大量的数据改组,并且只有一个内核(每个10 minute batch)将最终写入所有数据,而您将不会能够在一定程度上使用火花并行性。还有一个缺点是分区比host, dt and h大。由于您没有在查询中进行任何聚合,因此应该128MB并使用decrease your batch size,以便可以具有可接受的分区大小。