DataFrame'in bölümlenmesi nasıl tanımlanır?


129

Spark 1.4.0'da Spark SQL ve DataFrames kullanmaya başladım. Scala'da DataFrames üzerinde özel bir bölümleyici tanımlamak istiyorum, ancak bunun nasıl yapılacağını göremiyorum.

Üzerinde çalıştığım veri tablolarından biri, aşağıdaki örneğe göre, hesaba göre, silimar işlemlerin bir listesini içeriyor.

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

En azından başlangıçta, hesaplamaların çoğu bir hesaptaki işlemler arasında gerçekleşecektir. Bu nedenle, bir hesabın tüm işlemlerinin aynı Spark bölümünde olması için verilerin bölümlenmiş olmasını isterim.

Ama bunu tanımlamanın bir yolunu görmüyorum. DataFrame sınıfı, oluşturulacak bölümlerin sayısını belirtebileceğiniz 'yeniden bölümleme (Int)' adlı bir yönteme sahiptir. Ancak, bir RDD için belirtilebilecek gibi, DataFrame için özel bir bölümleyici tanımlamak için kullanılabilecek herhangi bir yöntem görmüyorum.

Kaynak veriler Parquet'te saklanır. Gördüm ki, bir DataFrame'i Parquet'e yazarken, bölümlenecek bir sütun belirtebilirsiniz, bu yüzden muhtemelen Parquet'e verilerini 'Hesap' sütununa göre bölümlemesini söyleyebilirim. Ancak milyonlarca hesap olabilir ve eğer Parquet'i doğru anlıyorsam, her Hesap için ayrı bir dizin oluşturacaktı, bu yüzden bu mantıklı bir çözüm gibi görünmüyordu.

Spark'ın bu DataFrame'i bir Hesap için tüm verilerin aynı bölümde olması için bölümlemesini sağlamanın bir yolu var mı?



Parquet'e hesaba göre bölümlemesini söyleyebilirseniz, muhtemelen bölümleme yapabilir int(account/someInteger)ve böylece dizin başına makul sayıda hesap elde edebilirsiniz .
Paul

1
@ABC: Bu bağlantıyı gördüm. Bu partitionBy(Partitioner)yöntemin eşdeğerini arıyordu , ancak RDD'ler yerine DataFrame'ler için. Şimdi bunun partitionBysadece Çift RDD'ler için mevcut olduğunu görüyorum, bunun neden olduğundan emin değilim.
tırmık

@Paul: Tarif ettiğiniz şeyi yapmayı düşündüm. Bir kaç şey beni geri düzenlenen:
tırmık

devam ediyor .... (1) Bu "Parke bölme" içindir. Spark-partitioning'in aslında Parquet-partitioning kullanacağını belirten herhangi bir belge bulamadım. (2) Parquet belgelerini anlarsam, yeni bir alan "foo" tanımlamam gerekir, bu durumda her Parquet dizininin "foo = 123" gibi bir adı olur. Ancak, AccountID'yi içeren bir sorgu oluşturursam , Spark / hive / parquet foo ve AccountID arasında herhangi bir bağlantı olduğunu nasıl anlar ?
komisyon

Yanıtlar:


177

Kıvılcım> = 2.3.0

SPARK-22614, aralık bölümlemesini açığa çıkarır.

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 , Veri Kaynağı API v2'de harici format bölümlemesini ortaya çıkarır .

Kıvılcım> = 1.6.0

Spark> = 1.6'da sorgu ve önbelleğe alma için sütuna göre bölümlemeyi kullanmak mümkündür. Bakınız: SPARK-11410 ve SPARK-4849repartition yöntemi kullanarak :

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]

RDDsSpark'ın aksine Dataset( Dataset[Row]aka dahil DataFrame) şimdilik özel bölümleyici kullanamaz. Bunu genellikle yapay bir bölümleme sütunu oluşturarak ele alabilirsiniz, ancak bu size aynı esnekliği vermez.

Kıvılcım <1.6.0:

Yapabileceğiniz bir şey, giriş verilerini oluşturmadan önce ön bölümlemektir. 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)

Yana DataFramebir gelen yaratma RDDbölüm düzeni varolan sadece basit bir harita aşamasını gerektirir * korunması gerektiğini:

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

Aynı şekilde var olanı yeniden bölümlere ayırabilirsiniz DataFrame:

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

Yani imkansız değilmiş gibi görünüyor. Hiç mantıklıysa soru kalır. Çoğu zaman bunun olmadığını iddia edeceğim:

  1. Yeniden bölümleme pahalı bir süreçtir. Tipik bir senaryoda, verilerin çoğu serileştirilmeli, karıştırılmalı ve serileştirilmemelidir. Öte yandan, önceden bölümlenmiş bir veriden yararlanabilecek işlemlerin sayısı nispeten azdır ve dahili API bu özelliği kullanmak üzere tasarlanmadıysa daha da sınırlıdır.

    • bazı senaryolara katılır, ancak dahili bir destek gerektirir,
    • pencere işlevleri eşleşen bölümleyiciyle çağrılar. Yukarıdakinin aynısı, tek bir pencere tanımıyla sınırlıdır. Zaten dahili olarak bölümlenmiştir, bu nedenle ön bölümleme gereksiz olabilir,
    • basit toplamalar GROUP BY- geçici tamponların bellek ayak izini azaltmak mümkündür **, ancak toplam maliyet çok daha yüksektir. groupByKey.mapValues(_.reduce)(Mevcut davranış) - reduceByKey(ön bölümleme) ile aşağı yukarı eşdeğerdir . Pratikte yararlı olması pek olası değildir.
    • ile veri sıkıştırma SqlContext.cacheTable. Çalıştırma uzunluğu kodlaması kullanıyor gibi göründüğünden, uygulama OrderedRDDFunctions.repartitionAndSortWithinPartitionssıkıştırma oranını iyileştirebilir.
  2. Performans, büyük ölçüde anahtarların dağıtımına bağlıdır. Eğri olursa, yetersiz kaynak kullanımına neden olur. En kötü senaryoda, işi bitirmek imkansız olacaktır.

  3. Yüksek seviyeli bildirim temelli bir API kullanmanın tüm amacı, kendinizi düşük seviyeli uygulama ayrıntılarından izole etmektir. @Dwysakowicz ve @RomiKuntsman tarafından daha önce bahsedildiği gibi, bir optimizasyon Katalizör Doktorunun bir işidir . Oldukça sofistike bir canavar ve iç kısımlarına çok daha fazla dalmadan bunu kolayca geliştirebileceğinizden gerçekten şüpheliyim.

Ilgili kavramlar

JDBC kaynakları ile bölümleme :

JDBC veri kaynakları predicatesargümanı destekler . Aşağıdaki şekilde kullanılabilir:

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

Koşul başına tek bir JDBC bölümü oluşturur. Tek tek yüklemler kullanılarak oluşturulan kümeler ayrık değilse, ortaya çıkan tabloda kopyalar göreceğinizi unutmayın.

partitionByyöntemDataFrameWriter :

Spark DataFrameWriter, partitionByyazma sırasında verileri "bölümlemek" için kullanılabilecek bir yöntem sağlar . Verilen sütun setini kullanarak yazma sırasında verileri ayırır

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

Bu, anahtara dayalı sorgular için okumada aşağı doğru yüklemeyi etkinleştirir:

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

ama eşdeğer değildir DataFrame.repartition. Özellikle aşağıdaki gibi toplamalar:

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

yine de gerektirecek 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) içindeki yöntem :

bucketByile benzer uygulamalara sahiptir, partitionByancak yalnızca tablolar ( saveAsTable) için kullanılabilir . Gruplama bilgileri, birleştirmeleri optimize etmek için kullanılabilir:

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

* Bölüm düzeni ile sadece bir veri dağıtımını kastediyorum. partitionedRDD'nin artık bir bölümleyicisi yoktur. ** Erken bir tahmin olmadığı varsayılır. Toplama yalnızca küçük bir sütun alt kümesini kapsıyorsa, muhtemelen herhangi bir kazanç yoktur.


@bychance Evet ve hayır. Veri düzeni korunur, ancak AFAIK size bölüm budaması gibi faydalar sağlamaz.
zero323

@ zero323 Teşekkürler, df.save.write'ı doğrulamak için parke dosyasının bölüm tahsisini kontrol etmenin bir yolu var mı? Ve df.repartition ("A") yaparsam, o zaman df.write.repartitionBy ("B") yaparsam, fiziksel klasör yapısı B'ye göre bölümlenir ve her B değeri klasöründe, bölümü şu şekilde tutmaya devam eder: bir?
bychance

2
@bychance DataFrameWriter.partitionBymantıksal olarak aynı değildir DataFrame.repartition. Önceden karıştırılmaz, çıktıyı basitçe ayırır. İlk soru ile ilgili olarak. - veri bölüm başına kaydedilir ve karıştırma yapılmaz. Dosyaları tek tek okuyarak kolayca kontrol edebilirsiniz. Ama gerçekten istediğin buysa Spark'ın tek başına bunu bilmesinin bir yolu yok.
zero323

11

Spark <1.6'da HiveContextDüz eski değil, bir oluşturursanız SqlContext, HiveQL'i kullanabilirsiniz DISTRIBUTE BY colX...(her N düşürücünün çakışmayan x aralıklarını almasını sağlar) & CLUSTER BY colX...(Dağıtma Ölçütü ve Sıralama Ölçütü kısayolu) örneğin;

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

Bunun Spark DF api ile nasıl uyduğundan emin değilim. Bu anahtar sözcükler normal SqlContext'te desteklenmez (HiveContext'i kullanmak için bir kovan meta deposuna sahip olmanız gerekmediğini unutmayın)

DÜZENLEME: Spark 1.6+ artık buna yerel DataFrame API'sinde sahip


1
Veri çerçevesi kaydedilirken bölümler korunuyor mu?
Sim

kovan ql örneğinde kaç bölüme sahip olabileceğinizi nasıl kontrol edersiniz? Örneğin, ikili RDD yaklaşımında, bunu 5 bölüm oluşturmak için yapabilirsiniz: val bölümleyici = yeni HashPartitioner (5)
Minnie

tamam, cevap bulundu, şu şekilde yapılabilir: sqlContext.setConf ("spark.sql.shuffle.partitions", "5") 5 dakika sınırını kaçırdığım için önceki yorumu düzenleyemedim
Minnie

7

Yani bir tür cevapla başlamak için :) - Yapamazsınız

Ben bir uzman değilim, ancak DataFrame'leri anladığım kadarıyla, bunlar rdd'ye eşit değiller ve DataFrame'de Partitioner diye bir şey yok.

Genel olarak DataFrame'in fikri, bu tür sorunları kendi başına çözen başka bir soyutlama düzeyi sağlamaktır. DataFrame'deki sorgular, RDD'lerdeki işlemlere daha da çevrilen mantıksal plana çevrilir. Önerdiğiniz bölümleme muhtemelen otomatik olarak uygulanacak veya en azından uygulanmalıdır.

SparkSQL'e bir tür optimum iş sağlayacağına güvenmiyorsanız, yorumlarda önerildiği gibi DataFrame'i her zaman RDD'ye [Satır] dönüştürebilirsiniz.


7

Şu şekilde döndürülen DataFrame'i kullanın:

yourDF.orderBy(account)

partitionByBir DataFrame'de, yalnızca bir PairRDD'de kullanmanın açık bir yolu yoktur , ancak bir DataFrame'i sıraladığınızda, bunu LogicalPlan içinde kullanacaktır ve bu, her bir Hesapta hesaplamalar yapmanız gerektiğinde yardımcı olacaktır.

Hesaba göre bölümlemek istediğim bir veri çerçevesiyle aynı sorunla karşılaştım. "Bir hesabın tüm işlemlerinin aynı Spark bölümünde olması için verilerin bölümlendirilmesini istiyorum" dediğinizde, bunu ölçek ve performans için istediğinizi, ancak kodunuzun buna bağlı olmadığını varsayıyorum (kullanmak gibi mapPartitions()vb), değil mi?


3
MapPartitions kullandığınız için kodunuz buna bağlıysa ne olacak?
Nightwolf

2
DataFrame'i bir RDD'ye dönüştürebilir ve ardından Bölümlere ayırabilirsiniz (örneğin, aggregatByKey () kullanarak ve özel bir
Bölümleyici geçirebilirsiniz

5

Bunu RDD kullanarak yapabildim. Ama bunun sizin için kabul edilebilir bir çözüm olup olmadığını bilmiyorum. Bir RDD olarak DF'ye sahip olduğunuzda, repartitionAndSortWithinPartitionsverilerin özel olarak yeniden bölümlendirilmesi için başvurabilirsiniz .

İşte kullandığım bir örnek:

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)
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.