我正在尝试利用spark分区。我试图做像
这样的事情data.write.partitionBy("key").parquet("/location")
这里的问题每个分区都会产生大量的镶木地板文件,如果我试图从根目录中读取,会导致读取速度慢。
为了避免我尝试
data.coalese(numPart).write.partitionBy("key").parquet("/location")
然而,这会在每个分区中创建numPart数量的镶木地板文件。 现在我的分区大小不同了。所以我理想的是希望每个分区有单独的合并。然而,这看起来并不容易。我需要访问所有分区合并到一定数量并存储在一个单独的位置。
如何在写入后使用分区来避免多个文件?
答案 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")
这不会改变文件的数量,但是当您查询给定key
和id
的镶木地板文件时,它会提高性能。参见例如https://www.slideshare.net/RyanBlue3/parquet-performance-tuning-the-missing-guide和https://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可能会出现问题。
另一种方法是编写自己的自定义分区程序,但是我不知道其中涉及什么,所以我无法提供任何代码。