Làm cách nào để xác định phân vùng DataFrame?


128

Tôi đã bắt đầu sử dụng Spark SQL và DataFrames trong Spark 1.4.0. Tôi muốn xác định một trình phân vùng tùy chỉnh trên DataFrames, trong Scala, nhưng không thấy cách thực hiện điều này.

Một trong những bảng dữ liệu tôi đang làm việc chứa một danh sách các giao dịch, theo tài khoản, silimar theo ví dụ sau.

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

Ít nhất là ban đầu, hầu hết các tính toán sẽ xảy ra giữa các giao dịch trong một tài khoản. Vì vậy, tôi muốn phân vùng dữ liệu để tất cả các giao dịch cho một tài khoản nằm trong cùng một phân vùng Spark.

Nhưng tôi không thấy một cách nào để định nghĩa điều này. Lớp DataFrame có một phương thức gọi là 'phân vùng lại (Int)', trong đó bạn có thể chỉ định số lượng phân vùng cần tạo. Nhưng tôi không thấy bất kỳ phương thức nào có sẵn để xác định trình phân vùng tùy chỉnh cho DataFrame, chẳng hạn như có thể được chỉ định cho RDD.

Dữ liệu nguồn được lưu trữ trong Parquet. Tôi đã thấy rằng khi viết DataFrame vào Parquet, bạn có thể chỉ định một cột để phân vùng theo, vì vậy có lẽ tôi có thể bảo Parquet phân vùng dữ liệu theo cột 'Tài khoản'. Nhưng có thể có hàng triệu tài khoản và nếu tôi hiểu đúng về Parquet, nó sẽ tạo một thư mục riêng cho từng Tài khoản, do đó nghe có vẻ không phải là một giải pháp hợp lý.

Có cách nào để Spark phân vùng DataFrame này để tất cả dữ liệu cho một Tài khoản nằm trong cùng một phân vùng không?


kiểm tra liên kết này stackoverflow.com/questions/23127329/
Mạnh

Nếu bạn có thể yêu cầu Parquet phân vùng theo tài khoản, có lẽ bạn có thể phân vùng theo int(account/someInteger)và nhờ đó có được số lượng tài khoản hợp lý cho mỗi thư mục.
Paul

1
@ABC: Tôi đã thấy liên kết đó. Đã tìm kiếm tương đương với partitionBy(Partitioner)phương thức đó , nhưng đối với DataFrames thay vì RDD. Bây giờ tôi thấy nó partitionBychỉ khả dụng cho Cặp RDD, không biết tại sao lại như vậy.
cào

@Paul: Tôi đã xem xét làm những gì bạn mô tả. Một vài điều giữ tôi lại:
cào

tiếp tục .... (1) Đó là cho "Phân vùng sàn gỗ". Tôi không thể tìm thấy bất kỳ tài liệu nào nói rằng phân vùng Spark sẽ thực sự sử dụng phân vùng Parquet. (2) Nếu tôi hiểu các tài liệu Parquet, tôi cần xác định một trường mới "foo", thì mỗi thư mục Parquet sẽ có một tên như "foo = 123". Nhưng nếu tôi xây dựng một truy vấn liên quan đến AccountID , làm thế nào Spark / hive / parquet biết rằng có bất kỳ mối liên kết nào giữa fooAccountID ?
cào

Câu trả lời:


177

Tia lửa> = 2.3.0

SPARK-22614 hiển thị phân vùng phạm vi.

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 hiển thị phân vùng định dạng bên ngoài trong API nguồn dữ liệu v2 .

Tia lửa> = 1.6.0

Trong Spark> = 1.6 có thể sử dụng phân vùng theo cột cho truy vấn và bộ đệm. Xem: SPARK-11410SPARK-4849 sử dụng repartitionphương pháp:

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]

Không giống như RDDsSpark Dataset(bao gồm Dataset[Row]aka DataFrame) không thể sử dụng phân vùng tùy chỉnh như bây giờ. Thông thường bạn có thể giải quyết điều đó bằng cách tạo cột phân vùng nhân tạo nhưng nó sẽ không cung cấp cho bạn tính linh hoạt tương tự.

Tia lửa <1.6.0:

Một điều bạn có thể làm là phân vùng dữ liệu đầu vào trước khi tạo 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)

DataFrameviệc tạo từ RDDchỉ yêu cầu bố trí phân vùng hiện tại của giai đoạn bản đồ đơn giản nên được giữ nguyên *:

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

Giống như cách bạn có thể phân vùng lại hiện có DataFrame:

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

Vì vậy, có vẻ như nó không phải là không thể. Câu hỏi vẫn còn nếu nó có ý nghĩa gì cả. Tôi sẽ lập luận rằng hầu hết thời gian nó không:

  1. Phân vùng lại là một quá trình tốn kém. Trong một kịch bản điển hình, hầu hết các dữ liệu phải được tuần tự hóa, xáo trộn và giải tuần tự hóa. Mặt khác, số lượng hoạt động có thể được hưởng lợi từ dữ liệu được phân vùng trước là tương đối nhỏ và bị hạn chế hơn nữa nếu API nội bộ không được thiết kế để tận dụng thuộc tính này.

    • tham gia vào một số tình huống, nhưng nó sẽ cần hỗ trợ nội bộ,
    • chức năng cửa sổ gọi với phân vùng phù hợp. Tương tự như trên, giới hạn trong một định nghĩa cửa sổ duy nhất. Nó đã được phân vùng nội bộ, vì vậy phân vùng trước có thể là dư thừa,
    • tập hợp đơn giản với GROUP BY- có thể giảm dung lượng bộ nhớ của bộ đệm tạm thời **, nhưng chi phí tổng thể cao hơn nhiều. Nhiều hơn hoặc ít hơn tương đương với groupByKey.mapValues(_.reduce)(hành vi hiện tại) so với reduceByKey(phân vùng trước). Không có khả năng hữu ích trong thực tế.
    • nén dữ liệu với SqlContext.cacheTable. Vì có vẻ như nó đang sử dụng mã hóa chiều dài chạy, áp dụng OrderedRDDFunctions.repartitionAndSortWithinPartitionscó thể cải thiện tỷ lệ nén.
  2. Hiệu suất phụ thuộc rất nhiều vào sự phân phối các phím. Nếu nó bị lệch, nó sẽ dẫn đến việc sử dụng tài nguyên dưới mức tối ưu. Trong trường hợp xấu nhất sẽ không thể hoàn thành công việc.

  3. Toàn bộ quan điểm của việc sử dụng API khai báo cấp cao là tự cô lập bản thân khỏi chi tiết triển khai cấp thấp. Như đã được đề cập bởi @dwysakowicz@RomiKuntsman, tối ưu hóa là một công việc của Trình tối ưu hóa Catalyst . Nó là một con thú khá tinh vi và tôi thực sự nghi ngờ bạn có thể dễ dàng cải thiện điều đó mà không cần lặn sâu hơn vào bên trong nó.

Các khái niệm liên quan

Phân vùng với các nguồn JDBC :

Đối số nguồn dữ liệu JDBC hỗ trợ predicatesđối số . Nó có thể được sử dụng như sau:

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

Nó tạo ra một phân vùng JDBC duy nhất cho mỗi vị từ. Hãy nhớ rằng nếu các tập hợp được tạo bằng các biến vị ngữ riêng lẻ không phân biệt bạn sẽ thấy các bản sao trong bảng kết quả.

partitionByphương pháp trongDataFrameWriter :

Spark DataFrameWritercung cấp partitionByphương thức có thể được sử dụng để "phân vùng" dữ liệu khi ghi. Nó phân tách dữ liệu trên ghi bằng cách sử dụng bộ cột được cung cấp

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

Điều này cho phép vị ngữ đẩy xuống đọc cho các truy vấn dựa trên khóa:

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

nhưng nó không tương đương với DataFrame.repartition. Trong các tập hợp cụ thể như:

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

vẫn sẽ yêu cầu 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

bucketByphương thức trongDataFrameWriter (Spark> = 2.0):

bucketBycó các ứng dụng tương tự như partitionBynhưng nó chỉ có sẵn cho các bảng ( saveAsTable). Thông tin khóa có thể được sử dụng để tối ưu hóa các phép nối:

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

* Theo cách bố trí phân vùng, ý tôi chỉ là phân phối dữ liệu. partitionedRDD không còn là một phân vùng. ** Giả sử không có chiếu sớm. Nếu tập hợp chỉ bao gồm các tập hợp con nhỏ của cột thì có lẽ không có lợi ích gì.


@bychance Có và không. Bố cục dữ liệu sẽ được giữ nguyên nhưng AFAIK nó sẽ không mang lại cho bạn những lợi ích như cắt tỉa phân vùng.
zero323

@ zero323 Cảm ơn, có cách nào để kiểm tra phân bổ phân vùng tệp parquet để xác thực df.save.write thực sự lưu bố cục không? Và nếu tôi thực hiện df.repartition ("A"), sau đó thực hiện df.write.repartitionBy ("B"), cấu trúc thư mục vật lý sẽ được phân vùng bởi B và trong mỗi thư mục giá trị B, nó sẽ vẫn giữ phân vùng theo A?
tạm biệt

2
@bychance DataFrameWriter.partitionBylà logic không giống như DataFrame.repartition. Trước đây không xáo trộn, nó chỉ đơn giản là tách đầu ra. Về câu hỏi đầu tiên.- dữ liệu được lưu trên mỗi phân vùng và không có xáo trộn. Bạn có thể dễ dàng kiểm tra bằng cách đọc các tệp riêng lẻ. Nhưng một mình Spark không có cách nào để biết về nó nếu đây là điều bạn thực sự muốn.
zero323

11

Trong Spark <1.6 Nếu bạn tạo một HiveContext, không phải cũ đơn giản, SqlContextbạn có thể sử dụng HiveQL DISTRIBUTE BY colX... (đảm bảo mỗi bộ giảm N có phạm vi không chồng lấp của x) & CLUSTER BY colX...(ví dụ: phím tắt cho Phân phối theo và Sắp xếp theo);

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

Không chắc chắn làm thế nào điều này phù hợp với Spark DF api. Những từ khóa này không được hỗ trợ trong SqlContext bình thường (lưu ý bạn không cần phải có kho lưu trữ meta để sử dụng HiveContext)

EDIT: Spark 1.6+ hiện có cái này trong API DataFrame gốc


1
Các phân vùng có được bảo tồn khi dataframe được lưu không?
Sim

Làm thế nào để bạn kiểm soát có bao nhiêu phân vùng bạn có thể có trong ví dụ hive ql? ví dụ: trong cách tiếp cận RDD cặp, bạn có thể thực hiện việc này để tạo 5 phân vùng: val phân vùng = new HashPartitioner (5)
Minnie

ok, tìm thấy câu trả lời, nó có thể được thực hiện như thế này: sqlContext.setConf ("spark.sql.shuffle.partitions", "5") Tôi không thể chỉnh sửa nhận xét trước đó vì tôi đã bỏ lỡ giới hạn 5 phút
Minnie

7

Vì vậy, để bắt đầu với một số loại câu trả lời :) - Bạn không thể

Tôi không phải là một chuyên gia, nhưng theo như tôi hiểu về DataFrames, chúng không bằng rdd và DataFrame không có thứ gọi là Trình phân vùng.

Nói chung, ý tưởng của DataFrame là cung cấp một mức độ trừu tượng khác để tự xử lý các vấn đề đó. Các truy vấn trên DataFrame được dịch thành kế hoạch logic được dịch tiếp sang các hoạt động trên RDD. Phân vùng mà bạn đề xuất có thể sẽ được áp dụng tự động hoặc ít nhất nên được.

Nếu bạn không tin tưởng SparkQuery rằng nó sẽ cung cấp một số loại công việc tối ưu, bạn luôn có thể chuyển đổi DataFrame thành RDD [Row] như được đề xuất trong các nhận xét.


7

Sử dụng DataFrame được trả về bởi:

yourDF.orderBy(account)

Không có cách rõ ràng để sử dụng partitionBytrên DataFrame, chỉ trên PairRDD, nhưng khi bạn sắp xếp DataFrame, nó sẽ sử dụng nó trong LogicalPlan và nó sẽ giúp ích khi bạn cần tính toán trên mỗi Tài khoản.

Tôi chỉ vấp phải vấn đề chính xác tương tự, với một khung dữ liệu mà tôi muốn phân vùng theo tài khoản. Tôi giả sử rằng khi bạn nói "muốn phân vùng dữ liệu để tất cả các giao dịch cho một tài khoản nằm trong cùng một phân vùng Spark", bạn muốn nó cho quy mô và hiệu suất, nhưng mã của bạn không phụ thuộc vào nó (như sử dụng mapPartitions()v.v.) phải không?


3
Điều gì về nếu mã của bạn phụ thuộc vào nó bởi vì bạn đang sử dụng mapPartitions?
NightWolf

2
Bạn có thể chuyển đổi DataFrame thành RDD, sau đó Phân vùng nó (ví dụ: sử dụng tổng
hợpByKey

5

Tôi đã có thể làm điều này bằng cách sử dụng RDD. Nhưng tôi không biết nếu đây là một giải pháp chấp nhận được cho bạn. Khi bạn có sẵn DF dưới dạng RDD, bạn có thể áp dụng repartitionAndSortWithinPartitionsđể thực hiện phân vùng lại dữ liệu tùy chỉnh.

Đây là một mẫu tôi đã sử dụng:

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)
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.