จะกำหนดการแบ่งพาร์ติชันของ DataFrame ได้อย่างไร?


129

ฉันเริ่มใช้ Spark SQL และ DataFrames ใน Spark 1.4.0 ฉันต้องการกำหนดพาร์ติชันเนอร์ที่กำหนดเองบน DataFrames ใน Scala แต่ไม่เห็นวิธีการทำเช่นนี้

หนึ่งในตารางข้อมูลที่ฉันกำลังใช้งานประกอบด้วยรายการธุรกรรมตามบัญชีซิลิมาร์ดังตัวอย่างต่อไปนี้

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)' ซึ่งคุณสามารถระบุจำนวนพาร์ติชันที่จะสร้างได้ แต่ฉันไม่เห็นวิธีการใด ๆ ที่พร้อมใช้งานในการกำหนดพาร์ติชันเนอร์แบบกำหนดเองสำหรับ DataFrame เช่นสามารถระบุได้สำหรับ RDD

แหล่งข้อมูลจะถูกเก็บไว้ในไม้ปาร์เก้ ฉันเห็นว่าเมื่อเขียน DataFrame เป็น Parquet คุณสามารถระบุคอลัมน์ที่จะแบ่งพาร์ติชันได้ดังนั้นฉันจึงสามารถบอกให้ Parquet แบ่งพาร์ติชันข้อมูลได้โดยใช้คอลัมน์ 'บัญชี' แต่อาจมีหลายล้านบัญชีและถ้าฉันเข้าใจ Parquet อย่างถูกต้องก็จะสร้างไดเรกทอรีที่แตกต่างกันสำหรับแต่ละบัญชีดังนั้นจึงไม่ได้เป็นวิธีแก้ปัญหาที่สมเหตุสมผล

มีวิธีให้ Spark แบ่งพาร์ติชัน DataFrame นี้เพื่อให้ข้อมูลทั้งหมดของบัญชีอยู่ในพาร์ติชันเดียวกันหรือไม่



หากคุณสามารถบอกให้ Parquet แบ่งพาร์ติชันตามบัญชีได้คุณอาจแบ่งพาร์ติชั่นได้int(account/someInteger)และด้วยเหตุนี้จึงได้รับจำนวนบัญชีที่เหมาะสมต่อไดเรกทอรี
พอล

1
@ABC: ฉันเห็นลิงค์นั้น กำลังมองหาวิธีการที่เทียบเท่ากันpartitionBy(Partitioner)แต่สำหรับ DataFrames แทนที่จะเป็น RDD ตอนนี้ฉันเห็นว่าpartitionByใช้ได้เฉพาะกับคู่ RDD เท่านั้นไม่แน่ใจว่าทำไมถึงเป็นเช่นนั้น
คราด

@ พอล: ฉันคิดว่าจะทำตามที่คุณอธิบาย มีบางสิ่งรั้งฉันไว้:
คราด

ดำเนินการต่อ .... (1) สำหรับ "การแบ่งพาร์เก้ต์" ฉันไม่พบเอกสารใด ๆ ที่ระบุว่า Spark-partitioning จะใช้ Parquet-partitioning จริงๆ (2) ถ้าฉันเข้าใจเอกสารปาร์เก้ฉันต้องกำหนดฟิลด์ใหม่ "foo" จากนั้นไดเร็กทอรี Parquet แต่ละรายการจะมีชื่อเช่น "foo = 123" แต่ถ้าผมสร้างแบบสอบถามที่เกี่ยวข้องกับAccountIDวิธีจะ Spark / รัง / ไม้ปาร์เก้ทราบว่ามีการเชื่อมโยงใด ๆ ระหว่างfooและAccountID ?
คราด

คำตอบ:


177

จุดประกาย> = 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 exposes รูปแบบการแบ่งภายนอกในAPI v2

จุดประกาย> = 1.6.0

ใน Spark> = 1.6 เป็นไปได้ที่จะใช้การแบ่งพาร์ติชันตามคอลัมน์สำหรับการสืบค้นและการแคช ดู: SPARK-11410และSPARK-4849โดยใช้repartitionวิธีการ:

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 Dataset(รวมถึงDataset[Row]aka DataFrame) ไม่สามารถใช้พาร์ติชันเนอร์แบบกำหนดเองได้ในตอนนี้ โดยทั่วไปคุณสามารถจัดการสิ่งนั้นได้โดยการสร้างคอลัมน์การแบ่งพาร์ติชันเทียม แต่จะไม่ให้ความยืดหยุ่นเท่าเดิม

จุดประกาย <1.6.0:

สิ่งหนึ่งที่คุณสามารถทำได้คือการแบ่งข้อมูลอินพุตล่วงหน้าก่อนที่จะสร้างไฟล์ 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. การแบ่งพาร์ติชันใหม่เป็นกระบวนการที่มีราคาแพง ในสถานการณ์ทั่วไปข้อมูลส่วนใหญ่จะต้องถูกทำให้เป็นอนุกรมสับเปลี่ยนและ deserialized ในทางกลับกันจำนวนการดำเนินการที่สามารถได้รับประโยชน์จากข้อมูลที่แบ่งพาร์ติชันล่วงหน้านั้นค่อนข้างเล็กและมีข้อ จำกัด เพิ่มเติมหาก API ภายในไม่ได้ออกแบบมาเพื่อใช้ประโยชน์จากคุณสมบัตินี้

    • เข้าร่วมในบางสถานการณ์ แต่จะต้องมีการสนับสนุนภายใน
    • ฟังก์ชันหน้าต่างเรียกด้วยพาร์ติชันที่ตรงกัน เช่นเดียวกับข้างต้น จำกัด ไว้ที่คำจำกัดความของหน้าต่างเดียว มีการแบ่งพาร์ติชันภายในอยู่แล้วดังนั้นการแบ่งพาร์ติชันล่วงหน้าอาจซ้ำซ้อน
    • การรวมอย่างง่ายด้วยGROUP BY- สามารถลดรอยเท้าหน่วยความจำของบัฟเฟอร์ชั่วคราว ** แต่ค่าใช้จ่ายโดยรวมสูงกว่ามาก มากกว่าหรือน้อยกว่าเทียบเท่ากับgroupByKey.mapValues(_.reduce)(พฤติกรรมปัจจุบัน) เทียบกับreduceByKey(การแบ่งพาร์ติชันล่วงหน้า) ไม่น่าจะมีประโยชน์ในทางปฏิบัติ
    • การบีบอัดข้อมูลด้วยSqlContext.cacheTable. เนื่องจากดูเหมือนว่าจะใช้การเข้ารหัสความยาวรันการใช้OrderedRDDFunctions.repartitionAndSortWithinPartitionsสามารถปรับปรุงอัตราส่วนการบีบอัดได้
  2. ประสิทธิภาพขึ้นอยู่กับการกระจายของคีย์ หากมีการเบ้จะส่งผลให้เกิดการใช้ทรัพยากรที่ไม่เหมาะสม ในกรณีที่เลวร้ายที่สุดจะไม่สามารถทำงานให้เสร็จได้เลย

  3. จุดรวมของการใช้ API ที่เปิดเผยระดับสูงคือการแยกตัวเองออกจากรายละเอียดการใช้งานระดับต่ำ ดังที่ได้กล่าวไว้แล้วโดย@dwysakowiczและ@RomiKuntsmanเพิ่มประสิทธิภาพเป็นงานของตัวเร่งเพิ่มประสิทธิภาพ มันเป็นสัตว์ร้ายที่ค่อนข้างซับซ้อนและฉันสงสัยจริงๆว่าคุณสามารถปรับปรุงสิ่งนั้นได้อย่างง่ายดายโดยไม่ต้องดำน้ำลึกเข้าไปในภายใน

แนวคิดที่เกี่ยวข้อง

การแบ่งพาร์ติชันด้วยแหล่งที่มา JDBC :

JDBC แหล่งข้อมูลสนับสนุนข้อโต้แย้งpredicates สามารถใช้งานได้ดังนี้:

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

สร้างพาร์ติชัน JDBC เดียวต่อเพรดิเคต โปรดทราบว่าหากชุดที่สร้างขึ้นโดยใช้เพรดิเคตแต่ละตัวไม่ปะติดปะต่อกันคุณจะเห็นรายการที่ซ้ำกันในตารางผลลัพธ์

partitionByวิธีการในDataFrameWriter :

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

bucketByวิธีการในDataFrameWriter (Spark> = 2.0):

bucketByมีแอปพลิเคชั่นที่คล้ายกันpartitionByแต่ใช้ได้เฉพาะกับตาราง ( 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>

* ตามรูปแบบพาร์ติชันฉันหมายถึงเฉพาะการกระจายข้อมูล partitionedRDD ไม่มีตัวแบ่งพาร์ติชันอีกต่อไป ** สมมติว่าไม่มีการฉายในช่วงต้น หากการรวมครอบคลุมเฉพาะส่วนย่อยของคอลัมน์อาจไม่มีผลตอบแทนใด ๆ


@bychance ใช่และไม่ใช่ เค้าโครงข้อมูลจะถูกเก็บรักษาไว้ แต่ AFAIK จะไม่ให้ประโยชน์แก่คุณเช่นการตัดแบ่งพาร์ติชัน
zero323

@ zero323 ขอบคุณมีวิธีตรวจสอบการจัดสรรพาร์ติชั่นของไฟล์ปาร์เก้เพื่อตรวจสอบ df.save.write บันทึกเค้าโครงจริงหรือไม่ และถ้าฉันทำ df.repartition ("A") ให้ทำ df.write.repartitionBy ("B") โครงสร้างโฟลเดอร์ทางกายภาพจะถูกแบ่งพาร์ติชันโดย B และภายในโฟลเดอร์ค่า B แต่ละโฟลเดอร์จะยังคงเก็บพาร์ติชันไว้โดย หรือไม่ A
โอกาส

2
@bychance เป็นเหตุผลไม่เหมือนกันDataFrameWriter.partitionBy DataFrame.repartitionก่อนหน้านี้ไม่สับเปลี่ยนมันเป็นเพียงการแยกเอาท์พุท เกี่ยวกับคำถามแรก - ข้อมูลจะถูกบันทึกต่อพาร์ติชันและไม่มีการสับเปลี่ยน คุณสามารถตรวจสอบได้อย่างง่ายดายโดยการอ่านไฟล์แต่ละไฟล์ แต่ Spark คนเดียวไม่มีทางรู้ได้เลยว่านี่คือสิ่งที่คุณต้องการจริงๆ
zero323

11

ใน Spark <1.6 หากคุณสร้าง a HiveContextไม่ใช่แบบเก่าSqlContextคุณสามารถใช้HiveQL ได้ DISTRIBUTE 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 ปกติ (โปรดทราบว่าคุณไม่จำเป็นต้องมี hive meta store เพื่อใช้ HiveContext)

แก้ไข: Spark 1.6+ มีสิ่งนี้ใน DataFrame API ดั้งเดิมแล้ว


1
พาร์ติชันถูกเก็บรักษาไว้เมื่อบันทึกดาต้าเฟรมหรือไม่
ซิม

คุณจะควบคุมจำนวนพาร์ติชั่นในตัวอย่าง hive ql ได้อย่างไร เช่นในแนวทางคู่ RDD คุณสามารถทำได้เพื่อสร้างพาร์ติชัน 5 พาร์ติชัน: val partitioner = new HashPartitioner (5)
Minnie

โอเคพบคำตอบสามารถทำได้ดังนี้ sqlContext.setConf ("spark.sql.shuffle.partitions", "5") ฉันไม่สามารถแก้ไขความคิดเห็นก่อนหน้าได้เนื่องจากฉันพลาดเวลา จำกัด 5 นาที
มินนี่

7

เริ่มต้นด้วยคำตอบบางอย่าง :) - คุณทำไม่ได้

ฉันไม่ใช่ผู้เชี่ยวชาญ แต่เท่าที่ฉันเข้าใจ DataFrames พวกเขาไม่เท่ากับ rdd และ DataFrame ไม่มีสิ่งที่เรียกว่า Partitioner

โดยทั่วไปความคิดของ DataFrame คือการมอบสิ่งที่เป็นนามธรรมอีกระดับหนึ่งที่จัดการกับปัญหาดังกล่าวด้วยตัวมันเอง การสืบค้นบน DataFrame จะถูกแปลเป็นแผนลอจิคัลที่แปลเพิ่มเติมเป็นการดำเนินการบน RDD การแบ่งพาร์ติชันที่คุณแนะนำอาจถูกนำไปใช้โดยอัตโนมัติหรืออย่างน้อยก็ควรจะเป็น

หากคุณไม่ไว้วางใจ SparkSQL ว่าจะให้งานที่ดีที่สุดคุณสามารถเปลี่ยน DataFrame เป็น RDD [Row] ได้ตามที่แนะนำในความคิดเห็น


7

ใช้ DataFrame ที่ส่งคืนโดย:

yourDF.orderBy(account)

ไม่มีวิธีที่ชัดเจนในการใช้partitionByบน DataFrame เฉพาะบน PairRDD แต่เมื่อคุณจัดเรียง DataFrame จะใช้สิ่งนั้นใน LogicalPlan และจะช่วยเมื่อคุณต้องทำการคำนวณในแต่ละบัญชี

ฉันเพิ่งสะดุดกับปัญหาเดียวกันกับดาต้าเฟรมที่ฉันต้องการแบ่งพาร์ติชันตามบัญชี ฉันคิดว่าเมื่อคุณพูดว่า "ต้องการแบ่งข้อมูลเพื่อให้ธุรกรรมทั้งหมดของบัญชีอยู่ในพาร์ติชัน Spark เดียวกัน" คุณต้องการให้มีขนาดและประสิทธิภาพ แต่รหัสของคุณไม่ได้ขึ้นอยู่กับมัน (เช่นการใช้mapPartitions()ฯลฯ ) ใช่ไหม?


3
จะเกิดอะไรขึ้นถ้ารหัสของคุณขึ้นอยู่กับมันเนื่องจากคุณกำลังใช้ mapPartitions?
NightWolf

2
คุณสามารถแปลง DataFrame เป็น RDD แล้วแบ่งพาร์ติชั่นได้ (ตัวอย่างเช่นใช้ aggregatByKey () และส่งผ่านพาร์ติชันที่กำหนดเอง)
Romi Kuntsman

5

ฉันสามารถทำได้โดยใช้ RDD แต่ฉันไม่รู้ว่านี่เป็นทางออกที่ยอมรับได้สำหรับคุณหรือไม่ เมื่อคุณมี DF เป็น RDD แล้วคุณสามารถใช้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)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.