Sparkでは、出力ファイルのファイルサイズを制御する最良の方法は何ですか。たとえば、log4jでは、ファイルのローテーション後に最大ファイルサイズを指定できます。
寄木細工のファイルの同様の解決策を探しています。ファイルを書き込むときに使用できる最大ファイルサイズオプションはありますか?
回避策はほとんどありませんが、良い方法はありません。ファイルを64MBに制限する場合、1つのオプションは、データを再パーティション化して一時的な場所に書き込むことです。次に、一時的な場所のファイルサイズを使用してファイルをマージします。しかし、正しいファイルサイズを取得することは困難です。
SparkでParquetファイルのサイズを制御することはできません。メモリ内のDataFrameをディスクに書き込む前にエンコードおよび圧縮する必要があるためです。このプロセスが完了する前に、実際のサイズを見積もる方法はありません。ディスク上のファイルサイズ。
だから私の解決策は:
df.write.parquet(path)
ディレクトリサイズを取得し、ファイル数を計算する
val fs = FileSystem.get(sc.hadoopConfiguration)
val dirSize = fs.getContentSummary(path).getLength
val fileNum = dirSize/(512 * 1024 * 1024) // let's say 512 MB per file
ディレクトリを読み取り、HDFSに再書き込みします
val df = sqlContext.read.parquet(path)
df.coalesce(fileNum).write.parquet(another_path)
元のdf
を再利用しないでください。再利用すると、ジョブが2回トリガーされます。
古いディレクトリを削除し、新しいディレクトリの名前を元に戻します
fs.delete(new Path(path), true)
fs.rename(new Path(newPath), new Path(path))
このソリューションには、データを2回書き込む必要があるという欠点があり、ディスクIOが2倍になりますが、現時点ではこれが唯一のソリューションです。
他の人が述べたように、ファイルごとの目標サイズを明示的にヒットすることはできません。ただし、すべての出力ファイルにほぼ同じ数の行を含めることができます。平均して圧縮率がどのように見えるかがわかっている場合、max_rowsまでの出力ファイル全体に行を均等に分散すると、ターゲットのサイズを一定に保つことができます。
これは、書き込む前にpartitionByを実行している場合よりも簡単に言えます。これを行うための擬似コードを次に示します。
-- #3 distribute partitionC's rows based on partitions plus random integer that pertains to file number
select * from dataframe_table as t4
inner join
-- #2 calculate the number of output files per partition
((select t1.partitionA, t1.partitionB, cast(t2.partition_num_rows / max_rows as int) + 1 as partition_num_files from dataframe_table) as t1
inner join
-- #1 determine number of rows in output partition
(select partitionA, partitionB, count(*) as partition_num_rows from dataframe_table group by (partitionA, partitionB)) as t2
on t1.partitionA = t2.partitionA and t1.partitionB = t2.partitionB) as t3
on t3.partitionA = t4.partitionA and t3.partitionB=t4.partitionB
distribute by (t4.partitionA, t4.partitionC, floor(Rand() * t3.partition_num_files)) sort by (partitionC, sortfield)
私たちのユースケースでは、パフォーマンスへの影響を最小限に抑えながら圧縮を大幅に改善するため、ここではパーティションにソートを含めました。
また、ステップ1と2の結果が十分に小さい場合Sparkは、ブロードキャストに参加してそれらを高速化できる場合があります。
ターゲットファイルのサイズ、メモリ使用量、実行時間を考慮に入れた、ここで私の完璧な方法です。これらのファイルには、スナッピー圧縮と辞書エンコーディングも含まれています。
私のHDFSブロックサイズは128 MB(128 * 1024 * 1024)です。
<property>
<name>dfs.blocksize</name>
<value>134217728</value>
</property>
これが私の最終的な寄木細工のファイルで、すべてhdfsブロックサイズに非常に近いものです。
133916650 2018-07-06 07:05 /year=2018/month=01/HoldingDetail_201801_0001.parquet
133459404 2018-07-06 07:05 /year=2018/month=01/HoldingDetail_201801_0002.parquet
133668445 2018-07-06 07:05 /year=2018/month=01/HoldingDetail_201801_0003.parquet
134004329 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0004.parquet
134015650 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0005.parquet
132053162 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0006.parquet
132917851 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0007.parquet
122594040 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0008.parquet
これは私がこれをやった方法です。
A.おおよその行数を考えて、10 MB程度のSMALL寄木細工ファイルの束を生成します。私の場合、200,000レコードを選択しました。単一のファイル内のデータの種類が多い場合、辞書のエンコードやその他の圧縮技術が放棄されるため、多くの小さな寄木細工ファイルは1つの大きな寄木細工ファイルよりもスペース効率が高くなります。一度に約10 MBを書き込むと、メモリも解放されます。
ファイルは次のようになります。
07916650 2018-07-06 07:05 /year=2018/month=01/HoldingDetail_201801_0001.parquet
12259404 2018-07-06 07:05 /year=2018/month=01/HoldingDetail_201801_0002.parquet
11368445 2018-07-06 07:05 /year=2018/month=01/HoldingDetail_201801_0003.parquet
07044329 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0004.parquet
13145650 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0005.parquet
08534162 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0006.parquet
12178451 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0007.parquet
11940440 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0008.parquet
09166540 2018-07-06 07:05 /year=2018/month=01/HoldingDetail_201801_0009.parquet
12594044 2018-07-06 07:05 /year=2018/month=01/HoldingDetail_201801_0010.parquet
11684245 2018-07-06 07:05 /year=2018/month=01/HoldingDetail_201801_0011.parquet
07043129 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0012.parquet
13153650 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0013.parquet
08533162 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0014.parquet
12137851 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0015.parquet
11943040 2018-07-06 07:06 /year=2018/month=01/HoldingDetail_201801_0016.parquet
B.すべての小さい寄木細工ファイルのリストを作成します。ファイルサイズを一緒に追加すると、HDFSブロックサイズを超えません。上記の例では:
/year=2018/month=01/HoldingDetail_201801_0001.parquet
to
/year=2018/month=01/HoldingDetail_201801_0012.parquet
plus
/year=2018/month=01/HoldingDetail_201801_0014.parquet
133,408,651バイトを占有します。
C. HoldingDetail_201801_temp.parquetという新しいファイルを開きます
リスト内のすべての小さいファイルを1つずつ読み取り、それらを一時ファイルに寄木細工の行グループとして書き込みます。各ファイルを行グループとして書き込むことは非常に重要です。これは、圧縮エンコーディングを保持し、書き込まれるバイトの量(スキーマメタデータを除く)が元のファイルサイズと同じになることを保証します。
リスト内の小さいファイルをすべて削除します。一時ファイルの名前をHoldingDetail_201801_0001.parquetに変更します。
残りの小さいファイルに対してステップBとCを繰り返して、* _ 0002.parquet、* _ 0003.parquet、* _ 0004.parquetなどを作成します。これらは、hdfsブロックサイズのすぐ下のサイズのターゲットファイルになります。
(ファイルサイズの合計が0.95 * dfs.blocksizeの場合は、先に進み、見つかったファイルをマージすることも確認します)
ここに私の解決策があり、それは私にとって楽しい作品です。
val repartition_num = 20
val hqc = new org.Apache.spark.sql.Hive.HiveContext(sc)
val t1 = hqc.sql("select * from customer")
// 20 parquet files will be generated in hdfs dir
// JUST control your file with partition number
t1.repartition(repartition_num ).saveAsParquetFile(parquet_dir)
そしてこれが結果です:
> hadoop fs -ls /tpch-parquet/customer/*.parquet | wc -l
20
Spark=にはまだ特定のサイズのロール後のオプションはありませんが、最適な秒数:特定の数のレコードの後にロールします。
Spark 2.2 以降、maxRecordsPerFile
を設定することが可能です。