Come definire il partizionamento di DataFrame?


128

Ho iniziato a utilizzare Spark SQL e DataFrames in Spark 1.4.0. Voglio definire un partizionatore personalizzato su DataFrames, in Scala, ma non vedo come farlo.

Una delle tabelle di dati con cui sto lavorando contiene un elenco di transazioni, per account, silimar nell'esempio seguente.

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

Almeno inizialmente, la maggior parte dei calcoli avverrà tra le transazioni all'interno di un conto. Quindi vorrei avere i dati partizionati in modo che tutte le transazioni per un account siano nella stessa partizione Spark.

Ma non vedo un modo per definirlo. La classe DataFrame ha un metodo chiamato 'repartition (Int)', in cui è possibile specificare il numero di partizioni da creare. Ma non vedo alcun metodo disponibile per definire un partizionatore personalizzato per un DataFrame, come può essere specificato per un RDD.

I dati di origine sono memorizzati in Parquet. Ho visto che quando scrivevo un DataFrame su Parquet, puoi specificare una colonna da partizionare, quindi presumibilmente potrei dire a Parquet di partizionare i suoi dati dalla colonna 'Account'. Ma potrebbero esserci milioni di account e, se capisco correttamente Parquet, creerebbe una directory distinta per ciascun account, quindi non sembra una soluzione ragionevole.

C'è un modo per ottenere Spark per partizionare questo DataFrame in modo che tutti i dati per un account siano nella stessa partizione?



Se puoi dire a Parquet di partizionare per account, probabilmente puoi partizionare int(account/someInteger)e quindi ottenere un numero ragionevole di account per directory.
Paul,

1
@ABC: ho visto quel link. Stava cercando l'equivalente di quel partitionBy(Partitioner)metodo, ma per DataFrames invece di RDD. Ora vedo che partitionByè disponibile solo per Pair RDDs, non so perché.
rastrello il

@Paul: ho pensato di fare quello che descrivi. Alcune cose mi hanno trattenuto:
rastrello il

continua .... (1) Questo è per "Parquet-partitioning". Non sono riuscito a trovare alcun documento che affermi che il partizionamento Spark utilizzerà effettivamente il partizionamento Parquet. (2) Se capisco i documenti di Parquet, devo definire un nuovo campo "pippo", quindi ogni directory di Parquet avrebbe un nome come "pippo = 123". Ma se costruissi una query che coinvolgesse AccountID , come farebbe Spark / hive / parquet a sapere che c'era un legame tra foo e AccountID ?
rastrello il

Risposte:


177

Scintilla> = 2.3.0

SPARK-22614 espone il partizionamento dell'intervallo.

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 espone il partizionamento di formato esterno nell'API origine dati v2 .

Scintilla> = 1.6.0

In Spark> = 1.6 è possibile utilizzare il partizionamento per colonna per query e memorizzazione nella cache. Vedere: SPARK-11410 e SPARK-4849 usando il repartitionmetodo:

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]

A differenza di RDDsSpark Dataset(incluso Dataset[Row]aka DataFrame) non è possibile utilizzare il partizionatore personalizzato per ora. In genere puoi risolverlo creando una colonna di partizionamento artificiale ma non ti darà la stessa flessibilità.

Scintilla <1.6.0:

Una cosa che puoi fare è pre-partizionare i dati di input prima di creare un 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)

Poiché la DataFramecreazione da un RDDrichiede solo una semplice fase della mappa, è necessario conservare il layout di partizione esistente *:

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

Allo stesso modo è possibile ripartizionare esistenti DataFrame:

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

Quindi sembra che non sia impossibile. La domanda rimane se ha senso. Sosterrò che il più delle volte non lo fa:

  1. Il ripartizionamento è un processo costoso. In uno scenario tipico, la maggior parte dei dati deve essere serializzata, mescolata e deserializzata. D'altra parte il numero di operazioni che possono beneficiare di dati pre-partizionati è relativamente piccolo ed è ulteriormente limitato se l'API interna non è progettata per sfruttare questa proprietà.

    • si unisce in alcuni scenari, ma richiederebbe un supporto interno,
    • le funzioni della finestra chiama con il partizionatore corrispondente. Come sopra, limitato a una singola finestra. Tuttavia, è già partizionato internamente, quindi il pre-partizionamento può essere ridondante,
    • semplici aggregazioni con GROUP BY- è possibile ridurre il footprint di memoria dei buffer temporanei **, ma il costo complessivo è molto più elevato. Più o meno equivalente a groupByKey.mapValues(_.reduce)(comportamento attuale) vs reduceByKey(pre-partizionamento). Difficilmente sarà utile in pratica.
    • compressione dei dati con SqlContext.cacheTable. Poiché sembra che stia utilizzando la codifica della lunghezza della corsa, l'applicazione OrderedRDDFunctions.repartitionAndSortWithinPartitionspotrebbe migliorare il rapporto di compressione.
  2. Le prestazioni dipendono fortemente dalla distribuzione delle chiavi. Se è inclinato, si otterrà un utilizzo delle risorse non ottimale. Nel peggiore dei casi, sarà impossibile completare il lavoro.

  3. Un intero punto dell'utilizzo di un'API dichiarativa di alto livello è isolarsi da dettagli di implementazione di basso livello. Come già accennato da @dwysakowicz e @RomiKuntsman, l'ottimizzazione è un lavoro dell'ottimizzatore Catalyst . È una bestia piuttosto sofisticata e dubito davvero che tu possa facilmente migliorarla senza immergerti molto più a fondo nei suoi interni.

Concetti correlati

Partizionamento con fonti JDBC :

predicatesArgomento di supporto delle origini dati JDBC . Può essere usato come segue:

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

Crea una singola partizione JDBC per predicato. Tieni presente che se i set creati utilizzando predicati individuali non sono disgiunti, vedrai i duplicati nella tabella risultante.

partitionBymetodo inDataFrameWriter :

Spark DataFrameWriterfornisce un partitionBymetodo che può essere utilizzato per "partizionare" i dati in scrittura. Separa i dati in scrittura utilizzando il set di colonne fornito

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")

Ciò consente il push predicato sulla lettura per le query basate sulla chiave:

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

ma non è equivalente a DataFrame.repartition. In particolare aggregazioni come:

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

richiederà comunque 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

bucketBymetodo inDataFrameWriter (Spark> = 2.0):

bucketByha applicazioni simili a partitionByma è disponibile solo per tables ( saveAsTable). Le informazioni sul bucket possono essere utilizzate per ottimizzare i join:

// 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>

* Per layout di partizione intendo solo una distribuzione di dati. partitionedRDD non ha più un partizionatore. ** Supponendo che non ci siano proiezioni anticipate. Se l'aggregazione copre solo un piccolo sottoinsieme di colonne, probabilmente non vi è alcun guadagno.


@bychance Sì e no. Il layout dei dati verrà conservato ma AFAIK non ti darà vantaggi come la potatura delle partizioni.
zero323,

@ zero323 Grazie, c'è un modo per controllare l'allocazione delle partizioni del file parquet per convalidare df.save.write, davvero salvare il layout? E se eseguo df.repartition ("A"), quindi eseguo df.write.repartitionBy ("B"), la struttura della cartella fisica verrà partizionata da B e all'interno di ciascuna cartella del valore B, manterrà comunque la partizione di UN?
caso

2
@bychance non DataFrameWriter.partitionByè logicamente uguale a DataFrame.repartition. Il precedente non si mescola, separa semplicemente l'output. Per quanto riguarda la prima domanda: i dati vengono salvati per partizione e non c'è shuffle. Puoi facilmente verificarlo leggendo i singoli file. Ma Spark da solo non ha modo di saperlo se questo è quello che vuoi davvero.
zero323,

11

In Spark <1.6 Se si crea a HiveContext, non solo il vecchio, SqlContextè possibile utilizzare HiveQL DISTRIBUTE BY colX... (garantisce che ciascuno dei riduttori N ottenga intervalli non sovrapposti di x) & CLUSTER BY colX...(scorciatoia per Distribuisci per e Ordina per) ad esempio;

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

Non sono sicuro di come si adatta a Spark DF API. Queste parole chiave non sono supportate nel normale SqlContext (nota che non è necessario disporre di un meta store hive per utilizzare HiveContext)

EDIT: Spark 1.6+ ora ha questo nell'API DataFrame nativa


1
Le partizioni vengono conservate durante il salvataggio del frame di dati?
Sim

come controlli quante partizioni puoi avere nell'esempio hl ql? ad es. nell'approccio RDD di coppia, è possibile farlo per creare 5 partizioni: val partitioner = new HashPartitioner (5)
Minnie

ok, ho trovato una risposta, può essere fatto in questo modo: sqlContext.setConf ("spark.sql.shuffle.partitions", "5") Non ho potuto modificare il commento precedente perché ho perso un limite di 5 minuti
Minnie

7

Quindi, per iniziare con una sorta di risposta:) - Non puoi

Non sono un esperto, ma per quanto ho capito DataFrames, non sono uguali a rdd e DataFrame non ha nulla come Partitioner.

L'idea di DataFrame è generalmente quella di fornire un altro livello di astrazione che gestisca tali problemi da soli. Le query su DataFrame vengono tradotte in un piano logico che viene ulteriormente tradotto in operazioni su RDD. Il partizionamento che hai suggerito verrà probabilmente applicato automaticamente o almeno dovrebbe esserlo.

Se non ti fidi di SparkSQL che fornirà un tipo di lavoro ottimale, puoi sempre trasformare DataFrame in RDD [Row] come suggerito nei commenti.


7

Utilizzare il DataFrame restituito da:

yourDF.orderBy(account)

Non esiste un modo esplicito di utilizzare partitionBysu un DataFrame, solo su un PairRDD, ma quando si ordina un DataFrame, lo utilizzerà nel suo LogicalPlan e sarà di aiuto quando è necessario effettuare calcoli su ciascun Account.

Mi sono appena imbattuto nello stesso problema esatto, con un frame di dati che voglio partizionare per account. Suppongo che quando dici "vuoi avere i dati partizionati in modo che tutte le transazioni per un account siano nella stessa partizione Spark", li vuoi per dimensioni e prestazioni, ma il tuo codice non dipende da esso (come usare mapPartitions()ecc), giusto?


3
Che dire se il tuo codice dipende da questo perché stai usando mapPartitions?
NightWolf

2
È possibile convertire DataFrame in un RDD e quindi partizionarlo (ad esempio utilizzando aggregatByKey () e passare un partizionatore personalizzato)
Romi Kuntsman

5

Sono stato in grado di farlo usando RDD. Ma non so se questa sia una soluzione accettabile per te. Una volta che hai il DF disponibile come RDD, puoi fare domanda repartitionAndSortWithinPartitionsper eseguire il ripartizionamento personalizzato dei dati.

Ecco un esempio che ho usato:

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)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.