Spark镶木地板分区:大量文件

时间:2017-06-28 16:49:52

标签: apache-spark spark-dataframe rdd apache-spark-2.0 bigdata

我正在尝试利用spark分区。我试图做像

这样的事情
data.write.partitionBy("key").parquet("/location")

这里的问题每个分区都会产生大量的镶木地板文件,如果我试图从根目录中读取,会导致读取速度慢。

为了避免我尝试

data.coalese(numPart).write.partitionBy("key").parquet("/location")

然而,这会在每个分区中创建numPart数量的镶木地板文件。 现在我的分区大小不同了。所以我理想的是希望每个分区有单独的合并。然而,这看起来并不容易。我需要访问所有分区合并到一定数量并存储在一个单独的位置。

如何在写入后使用分区来避免多个文件?

3 个答案:

答案 0 :(得分:31)

首先,我真的会避免使用coalesce,因为这通常会在转换链中被进一步推升,并可能破坏你工作的并行性(我在这里问过这个问题:How to prevent Spark optimization)< / p>

每个镶木地板分区写1个文件非常容易(参见Spark dataframe write method writing many small files):

data.repartition($"key").write.partitionBy("key").parquet("/location")

如果要设置任意数量的文件(或具有相同大小的文件),则需要使用可能使用的其他属性进一步重新分区数据(我无法告诉您在您的情况下这可能是什么) ):

data.repartition($"key",$"another_key").write.partitionBy("key").parquet("/location")

another_key可以是数据集的另一个属性,也可以是对现有属性使用某些模运算或舍入运算的派生属性。您甚至可以使用row_number超过key的窗口函数,然后通过类似

的方式对其进行舍入
data.repartition($"key",floor($"row_number"/N)*N).write.partitionBy("key").parquet("/location")

这会将N条记录放入1个镶木地板文件中

使用orderBy

您还可以通过相应地排序数据框来控制文件数量而无需重新分区:

data.orderBy($"key").write.partitionBy("key").parquet("/location")

这将导致所有分区总共spark.sql.shuffle.partitions(默认为200)。在$key之后添加第二个排序列甚至是有益的,因为镶木地板将记住数据帧的排序并相应地写入统计数据。例如,您可以通过ID订购:

data.orderBy($"key",$"id").write.partitionBy("key").parquet("/location")

这不会改变文件的数量,但是当您查询给定keyid的镶木地板文件时,它会提高性能。参见例如https://www.slideshare.net/RyanBlue3/parquet-performance-tuning-the-missing-guidehttps://db-blog.web.cern.ch/blog/luca-canali/2017-06-diving-spark-and-parquet-workloads-example

Spark 2.2 +

从Spark 2.2开始,您还可以使用新选项maxRecordsPerFile来限制每个文件的记录数。如果你有N个分区,你仍然会得到至少N个文件,但你可以将1个分区(任务)写入的文件拆分成更小的块:

df.write
.option("maxRecordsPerFile", 10000)
...

参见例如http://www.gatorsmile.io/anticipated-feature-in-spark-2-2-max-records-written-per-file/spark write to disk with N files less than N partitions

答案 1 :(得分:3)

让我们用另一种方法扩展Raphael Roth的答案,该方法将为每个分区可以包含的文件数量创建上限,as discussed in this answer

import org.apache.spark.sql.functions.rand

df.repartition(numPartitions, $"some_col", rand)
  .write.partitionBy("some_col")
  .parquet("partitioned_lake")

This blog post详细说明了所有与partitionBy结合使用的分区选项。

答案 2 :(得分:-1)

这对我很好:

protected INavigationService NavigationService { get; private set; }

    protected ICommunicationService CommunicationService;        

    private ObservableAsPropertyHelper<bool> _isBusy;
    public bool IsBusy => _isBusy.Value;

    private ObservableAsPropertyHelper<bool> _connected;        
    public bool Connected => _connected.Value;

    public ReactiveCommand<Unit, bool> Connect { get; }
    public ReactiveCommand<Unit, bool> Disconnect { get; }

    public MainPageViewModel(INavigationService navigationService, ICommunicationService communicationService)
    {
        CommunicationService = communicationService;
        NavigationService = navigationService;

        _isBusy = this
            .WhenAnyObservable(x => x.Connect.IsExecuting)
            .ObserveOn(RxApp.MainThreadScheduler)
            .ToProperty(this, x => x.IsBusy, false);                            

        _connected = this
            .WhenAnyValue(x => x.CommunicationService.Connected)
            .ObserveOn(RxApp.MainThreadScheduler)
            .ToProperty(this, x => x.Connected, false);

        Connect = ReactiveCommand.CreateFromTask(
            CommunicationService.ConnectAsync, 
            this.WhenAnyValue(
                x => x.IsBusy, y => y.Connected, 
                (isBusy, isConnected) => CanConnect(isBusy,isConnected))
                .ObserveOn(RxApp.MainThreadScheduler));

        Disconnect = ReactiveCommand.CreateFromTask(
            CommunicationService.DisconnectAsync, 
            this.WhenAnyValue(
                x => x.IsBusy, y => y.Connected, 
                (isBusy, isConnected) => CanDisonnect(isBusy, isConnected))
                .ObserveOn(RxApp.MainThreadScheduler));            

    }

    private bool CanConnect(bool isBusy, bool isConnected)
    {
        return !isBusy && !isConnected;
    }

    private bool CanDisonnect(bool isBusy, bool isConnected)
    {
        return !isBusy && isConnected;
    }

它在每个输出分区(目录)中产生N个文件,并且(偶然地)比使用public interface ICommunicationService { bool Connected { get; } Task<bool> ConnectAsync(); Task<bool> DisconnectAsync(); } public class CommunicationService : ReactiveObject, ICommunicationService { private bool _connected; public bool Connected { get => _connected; private set => this.RaiseAndSetIfChanged(ref _connected, value); } public async Task<bool> ConnectAsync() { await Task.Delay(1000); Connected = true; return Connected; } public async Task<bool> DisconnectAsync() { await Task.Delay(1000); Connected = false; return Connected; } } (同样,在我的数据集上)要快,仅比重新分区要快在输出上。

如果您使用的是S3,我还建议您在本地驱动器上进行所有操作(Spark在写出过程中会执行大量文件创建/重命名/删除操作),一旦全部解决,请使用hadoop data.repartition(n, "key").write.partitionBy("key").parquet("/location") (或者只是aws cli)复制所有内容:

coalesce

编辑:根据评论中的讨论:

您的数据集的分区列为YEAR,但是每个给定的YEAR中都有大量不同的数据。因此,一年可能有1GB的数据,而另一年可能有100GB的数据。

以下是处理此问题的一种方式的伪代码:

FileUtil

但是,我实际上不知道这将起作用。每个列分区读取数量可变的文件时,Spark可能会出现问题。

另一种方法是编写自己的自定义分区程序,但是我不知道其中涉及什么,所以我无法提供任何代码。