web-dev-qa-db-ja.com

DataFrameのパーティショニングを定義する方法

私はSpark 1.4.0でSpark SQLとDataFrameを使い始めました。私はScalaでDataFramesにカスタムパーティショナを定義したいのですが、これを行う方法を見ていません。

私が取り組んでいるデータテーブルの1つに、アカウント別のトランザクションのリストが含まれています。

Account   Date       Type       Amount
1001    2014-04-01  Purchase    100.00
1001    2014-04-01  Purchase     50.00
1001    2014-04-05  Purchase     70.00
1001    2014-04-01  Payment    -150.00
1002    2014-04-01  Purchase     80.00
1002    2014-04-02  Purchase     22.00
1002    2014-04-04  Payment    -120.00
1002    2014-04-04  Purchase     60.00
1003    2014-04-02  Purchase    210.00
1003    2014-04-03  Purchase     15.00

少なくとも最初は、ほとんどの計算はアカウント内のトランザクション間で行われます。そのため、アカウントのすべてのトランザクションが同じSparkパーティションに入るようにデータを分割したいと思います。

しかし、私はこれを定義する方法を見ていません。 DataFrameクラスには 'repartition(Int)'というメソッドがあり、作成するパーティションの数を指定できます。しかし、RDDに指定できるような、DataFrame用のカスタムパーティショナーを定義するために使用できる方法はありません。

ソースデータはParquetに保存されています。私は、ParquetにDataFrameを書くとき、あなたが分割するカラムを指定できることを見ました、それでおそらく私はParquetに 'Account'カラムによってデータを分割するように言うことができます。しかし、何百万ものアカウントがある可能性があり、もし私がParquetを正しく理解していれば、それは各アカウントに対して別々のディレクトリを作成するでしょう、それでそれは合理的な解決策のように聞こえませんでした。

アカウントのすべてのデータが同じパーティションに入るようにSparkにこのDataFrameをパーティション分割させる方法はありますか?

113
rake

スパーク> = 2.3.0

SPARK-22614 は範囲分割を公開します。

val partitionedByRange = df.repartitionByRange(42, $"k")

partitionedByRange.explain
// == Parsed Logical Plan ==
// 'RepartitionByExpression ['k ASC NULLS FIRST], 42
// +- AnalysisBarrier Project [_1#2 AS k#5, _2#3 AS v#6]
// 
// == Analyzed Logical Plan ==
// k: string, v: int
// RepartitionByExpression [k#5 ASC NULLS FIRST], 42
// +- Project [_1#2 AS k#5, _2#3 AS v#6]
//    +- LocalRelation [_1#2, _2#3]
// 
// == Optimized Logical Plan ==
// RepartitionByExpression [k#5 ASC NULLS FIRST], 42
// +- LocalRelation [k#5, v#6]
// 
// == Physical Plan ==
// Exchange rangepartitioning(k#5 ASC NULLS FIRST, 42)
// +- LocalTableScan [k#5, v#6]

SPARK-22389 は、 データソースAPI v2 で外部フォーマットのパーティション化を公開しています。

スパーク> = 1.6.0

Spark> = 1.6では、クエリとキャッシュのためにカラムによるパーティショニングを使うことが可能です。参照: SPARK-11410 および SPARK-4849repartitionメソッドの使用:

val df = Seq(
  ("A", 1), ("B", 2), ("A", 3), ("C", 1)
).toDF("k", "v")

val partitioned = df.repartition($"k")
partitioned.explain

// scala> df.repartition($"k").explain(true)
// == Parsed Logical Plan ==
// 'RepartitionByExpression ['k], None
// +- Project [_1#5 AS k#7,_2#6 AS v#8]
//    +- LogicalRDD [_1#5,_2#6], MapPartitionsRDD[3] at rddToDataFrameHolder at <console>:27
// 
// == Analyzed Logical Plan ==
// k: string, v: int
// RepartitionByExpression [k#7], None
// +- Project [_1#5 AS k#7,_2#6 AS v#8]
//    +- LogicalRDD [_1#5,_2#6], MapPartitionsRDD[3] at rddToDataFrameHolder at <console>:27
// 
// == Optimized Logical Plan ==
// RepartitionByExpression [k#7], None
// +- Project [_1#5 AS k#7,_2#6 AS v#8]
//    +- LogicalRDD [_1#5,_2#6], MapPartitionsRDD[3] at rddToDataFrameHolder at <console>:27
// 
// == Physical Plan ==
// TungstenExchange hashpartitioning(k#7,200), None
// +- Project [_1#5 AS k#7,_2#6 AS v#8]
//    +- Scan PhysicalRDD[_1#5,_2#6]

RDDs Sparkとは異なり、DatasetDataset[Row] a.k.a DataFrameを含む)は今のところカスタムパーティショナーを使用できません。通常は、人工的なパーティション化列を作成することで対処できますが、同じ柔軟性は得られません。

スパーク<1.6.0:

できることの1つは、DataFrameを作成する前に入力データを事前にパーティション化することです。

import org.Apache.spark.sql.types._
import org.Apache.spark.sql.Row
import org.Apache.spark.HashPartitioner

val schema = StructType(Seq(
  StructField("x", StringType, false),
  StructField("y", LongType, false),
  StructField("z", DoubleType, false)
))

val rdd = sc.parallelize(Seq(
  Row("foo", 1L, 0.5), Row("bar", 0L, 0.0), Row("??", -1L, 2.0),
  Row("foo", -1L, 0.0), Row("??", 3L, 0.6), Row("bar", -3L, 0.99)
))

val partitioner = new HashPartitioner(5) 

val partitioned = rdd.map(r => (r.getString(0), r))
  .partitionBy(partitioner)
  .values

val df = sqlContext.createDataFrame(partitioned, schema)

DataFrameからRDDを作成するために必要なのは単純なマップフェーズのみであるため、既存のパーティションレイアウトは保持する必要があります。

assert(df.rdd.partitions == partitioned.partitions)

同じ方法で既存のDataFrameを再分割できます。

sqlContext.createDataFrame(
  df.rdd.map(r => (r.getInt(1), r)).partitionBy(partitioner).values,
  df.schema
)

それは不可能ではないようです。まったく意味があるのであれば、問題は残ります。ほとんどの場合、そうではないと私は主張します。

  1. 再パーティション化は高価なプロセスです。典型的なシナリオでは、ほとんどのデータをシリアライズ、シャッフル、そしてデシリアライズする必要があります。一方、事前に分割されたデータから恩恵を受けることができる操作の数は比較的少なく、内部APIがこのプロパティを利用するように設計されていない場合はさらに制限されます。

    • いくつかのシナリオでは参加しますが、内部サポートが必要になります。
    • ウィンドウ関数は、対応するパーティショナを使って呼び出します。上記と同じですが、単一ウィンドウ定義に限定されています。ただし、すでに内部でパーティション分割されているため、事前パーティション分割は冗長になる可能性があります。
    • GROUP BYを使用した単純な集約 - 一時バッファのメモリ使用量を削減することは可能ですが**、全体的なコストははるかに高くなります。多かれ少なかれgroupByKey.mapValues(_.reduce)(現在の動作)とreduceByKey(事前分割)と同等です。実際には役に立たないでしょう。
    • SqlContext.cacheTableによるデータ圧縮。ランレングスエンコーディングを使用しているように見えるので、OrderedRDDFunctions.repartitionAndSortWithinPartitionsを適用すると圧縮率を向上させることができます。
  2. パフォーマンスは鍵の配布に大きく依存します。歪んでいると、リソースの使用率が最適とは言えません。最悪のシナリオでは、仕事を完全に終えることは不可能です。

  3. 高レベルの宣言型APIを使用することの全体的なポイントは、低レベルの実装の詳細から自分自身を切り離すことです。 @ dwysakowicz@ RomiKuntsman で既に述べたように、最適化は仕事です。 Catalystオプティマイザー の。それはかなり洗練された獣です、そして私はあなたがその内部にそれほど深く飛び込むことなしにそれを容易に改良できることを本当に疑います。

関連する概念

JDBCソースによるパーティション化

JDBCデータソースは predicates引数 をサポートします。次のように使用できます。

sqlContext.read.jdbc(url, table, Array("foo = 1", "foo = 3"), props)

述語ごとに単一のJDBCパーティションが作成されます。個々の述語を使用して作成された集合が互いに素ではない場合、結果の表に重複が表示されます。

partitionByDataFrameWriterメソッド:

Spark DataFrameWriterは、書き込み時にデータを「分割」するために使用できるpartitionByメソッドを提供します。提供された列のセットを使用して書き込み時にデータを分離します

val df = Seq(
  ("foo", 1.0), ("bar", 2.0), ("foo", 1.5), ("bar", 2.6)
).toDF("k", "v")

df.write.partitionBy("k").json("/tmp/foo.json")

これにより、キーに基づくクエリの述語「読み取り時にプッシュダウン」が有効になります。

val df1 = sqlContext.read.schema(df.schema).json("/tmp/foo.json")
df1.where($"k" === "bar")

しかし、それはDataFrame.repartitionと同等ではありません。特に集約:

val cnts = df1.groupBy($"k").sum()

まだTungstenExchangeが必要です。

cnts.explain

// == Physical Plan ==
// TungstenAggregate(key=[k#90], functions=[(sum(v#91),mode=Final,isDistinct=false)], output=[k#90,sum(v)#93])
// +- TungstenExchange hashpartitioning(k#90,200), None
//    +- TungstenAggregate(key=[k#90], functions=[(sum(v#91),mode=Partial,isDistinct=false)], output=[k#90,sum#99])
//       +- Scan JSONRelation[k#90,v#91] InputPaths: file:/tmp/foo.json

bucketByDataFrameWriterメソッド(Spark> = 2.0):

bucketBypartitionByと同様のアプリケーションがありますが、テーブル(saveAsTable)に対してのみ使用可能です。バケット情報は結合を最適化するために使用できます。

// Temporarily disable broadcast joins
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", -1)

df.write.bucketBy(42, "k").saveAsTable("df1")
val df2 = Seq(("A", -1.0), ("B", 2.0)).toDF("k", "v2")
df2.write.bucketBy(42, "k").saveAsTable("df2")

// == Physical Plan ==
// *Project [k#41, v#42, v2#47]
// +- *SortMergeJoin [k#41], [k#46], Inner
//    :- *Sort [k#41 ASC NULLS FIRST], false, 0
//    :  +- *Project [k#41, v#42]
//    :     +- *Filter isnotnull(k#41)
//    :        +- *FileScan parquet default.df1[k#41,v#42] Batched: true, Format: Parquet, Location: InMemoryFileIndex[file:/spark-warehouse/df1], PartitionFilters: [], PushedFilters: [IsNotNull(k)], ReadSchema: struct<k:string,v:int>
//    +- *Sort [k#46 ASC NULLS FIRST], false, 0
//       +- *Project [k#46, v2#47]
//          +- *Filter isnotnull(k#46)
//             +- *FileScan parquet default.df2[k#46,v2#47] Batched: true, Format: Parquet, Location: InMemoryFileIndex[file:/spark-warehouse/df2], PartitionFilters: [], PushedFilters: [IsNotNull(k)], ReadSchema: struct<k:string,v2:double>

*パーティションレイアウトとはデータの配布のみを意味します。 partitioned RDDには、もはやパーティショナがありません。 **早期の予測がないと仮定する。集計が列の小さなサブセットのみを対象としている場合は、おそらく何も得られないでしょう。

162
zero323

Spark <1.6では、普通のHiveContextではなくSqlContextを作成する場合は、 HiveQLDISTRIBUTE BY colX...を使用できます(N個のリデューサーのそれぞれが重複しない範囲になるようにします)例えば、x)&CLUSTER BY colX...([並べ替え]および[並べ替え]のショートカット)。

df.registerTempTable("partitionMe")
hiveCtx.sql("select * from partitionMe DISTRIBUTE BY accountId SORT BY accountId, date")

これがSpark DF apiにどのように適合するのかわからない。これらのキーワードは、通常のSqlContextではサポートされていません(HiveContextを使用するためにHiveメタストアを用意する必要はありません)。

編集:Spark 1.6以降ではこれがネイティブのDataFrame APIに含まれています。

11
NightWolf

以下によって返されるDataFrameを使用してください。

yourDF.orderBy(account)

DataFrameでpartitionByを使用する明示的な方法は、PairRDDでのみ使用できますが、DataFrameを並べ替えるときは、LogicalPlanでそれを使用するので、各アカウントで計算を行う必要があるときに役立ちます。

私はちょうど同じ問題に出くわしました。アカ​​ウントで分割したいデータフレームがあります。 「アカウントのすべてのトランザクションが同じSparkパーティションに収まるようにデータを分割したい」と言った場合は、規模とパフォーマンスの面でそれが望ましいと思いますが、コードはそれに依存しません(以下のように)。 mapPartitions()など)ね。

7
Romi Kuntsman

私はRDDを使ってこれを行うことができました。しかし、これがあなたにとって許容できる解決策であるかどうかはわかりません。 RDDとしてDFを使用できるようになったら、 repartitionAndSortWithinPartitions を適用してデータのカスタム再分割を実行できます。

これが私が使ったサンプルです:

class DatePartitioner(partitions: Int) extends Partitioner {

  override def getPartition(key: Any): Int = {
    val start_time: Long = key.asInstanceOf[Long]
    Objects.hash(Array(start_time)) % partitions
  }

  override def numPartitions: Int = partitions
}

myRDD
  .repartitionAndSortWithinPartitions(new DatePartitioner(24))
  .map { v => v._2 }
  .toDF()
  .write.mode(SaveMode.Overwrite)
5
Developer

それで、ある種の答えで始めるために:) - あなたはできません

私は専門家ではありませんが、DataFrameを理解する限り、それらはrddと同じではなく、DataFrameにはPartitionerのようなものはありません。

一般的にDataFrameの考えは、そのような問題自体を処理する別のレベルの抽象化を提供することです。 DataFrameに対するクエリは、RDDに対する操作にさらに変換される論理プランに変換されます。あなたが提案したパーティション分割はおそらく自動的に適用されるか、少なくとも適用されるでしょう。

SparkSQLがある種の最適な仕事を提供するとは思わないのであれば、コメントで示唆されているように常にDataFrameをRDD [Row]に変換できます。

4