Câu trả lời ban đầu thảo luận về mã có thể được tìm thấy dưới đây.
Trước hết, bạn phải phân biệt giữa các loại API khác nhau, mỗi loại có các cân nhắc về hiệu suất riêng.
API RDD
(cấu trúc Python thuần túy với sự phối hợp dựa trên JVM)
Đây là thành phần sẽ bị ảnh hưởng nhiều nhất bởi hiệu suất của mã Python và các chi tiết triển khai PySpark. Mặc dù hiệu năng của Python dường như không phải là một vấn đề, nhưng có ít nhất một vài yếu tố bạn phải xem xét:
- Chi phí truyền thông JVM. Thực tế, tất cả dữ liệu đến và từ trình thực thi Python phải được truyền qua một socket và một công nhân JVM. Mặc dù đây là một giao tiếp địa phương tương đối hiệu quả nhưng nó vẫn không miễn phí.
Các bộ thực thi dựa trên quy trình (Python) so với các bộ thực thi dựa trên luồng (nhiều luồng JVM đơn) (Scala). Mỗi trình thực thi Python chạy trong tiến trình riêng của nó. Là một tác dụng phụ, nó cung cấp sự cô lập mạnh hơn so với đối tác JVM của nó và một số điều khiển đối với vòng đời của người thực thi nhưng có khả năng sử dụng bộ nhớ cao hơn đáng kể:
- bộ nhớ phiên dịch
- dấu chân của các thư viện được tải
- phát sóng kém hiệu quả hơn (mỗi quy trình yêu cầu bản sao phát sóng riêng)
Hiệu suất của mã Python. Nói chung Scala nhanh hơn Python nhưng nó sẽ thay đổi tùy theo nhiệm vụ. Ngoài ra, bạn có nhiều tùy chọn bao gồm các JIT như Numba , phần mở rộng C ( Cython ) hoặc các thư viện chuyên ngành như Theano . Cuối cùng, nếu bạn không sử dụng ML / MLlib (hoặc đơn giản là NumPy stack) , hãy xem xét sử dụng PyPy như một trình thông dịch thay thế. Xem SPARK-3094 .
- Cấu hình PySpark cung cấp
spark.python.worker.reuse
tùy chọn có thể được sử dụng để lựa chọn giữa việc hủy quy trình Python cho từng tác vụ và sử dụng lại quy trình hiện có. Tùy chọn thứ hai có vẻ hữu ích để tránh việc thu gom rác đắt tiền (nó gây ấn tượng hơn là kết quả của các thử nghiệm có hệ thống), trong khi tùy chọn trước (mặc định) là tối ưu trong trường hợp phát sóng và nhập khẩu đắt tiền.
- Đếm tham chiếu, được sử dụng làm phương pháp thu gom rác dòng đầu tiên trong CPython, hoạt động khá tốt với khối lượng công việc Spark điển hình (xử lý giống như luồng, không có chu trình tham chiếu) và giảm nguy cơ tạm dừng GC dài.
MLlib
(thực thi hỗn hợp Python và JVM)
Những cân nhắc cơ bản là khá nhiều như trước đây với một vài vấn đề bổ sung. Trong khi các cấu trúc cơ bản được sử dụng với MLlib là các đối tượng RDD Python đơn giản, tất cả các thuật toán được thực thi trực tiếp bằng Scala.
Nó có nghĩa là một chi phí bổ sung để chuyển đổi các đối tượng Python thành các đối tượng Scala và ngược lại, tăng mức sử dụng bộ nhớ và một số hạn chế bổ sung mà chúng tôi sẽ đề cập sau.
Kể từ bây giờ (Spark 2.x), API dựa trên RDD đang ở chế độ bảo trì và được lên lịch để xóa trong Spark 3.0 .
API DataFrame và Spark ML
(Thực thi JVM với mã Python giới hạn trong trình điều khiển)
Đây có lẽ là sự lựa chọn tốt nhất cho các tác vụ xử lý dữ liệu tiêu chuẩn. Do mã Python hầu hết bị giới hạn ở các hoạt động logic mức cao trên trình điều khiển, nên không có sự khác biệt về hiệu năng giữa Python và Scala.
Một ngoại lệ duy nhất là việc sử dụng các UDF Python thông minh có hiệu quả thấp hơn đáng kể so với tương đương Scala của chúng. Mặc dù có một số cơ hội để cải tiến (đã có sự phát triển đáng kể trong Spark 2.0.0), hạn chế lớn nhất là làm tròn hoàn toàn giữa biểu diễn bên trong (JVM) và trình thông dịch Python. Nếu có thể, bạn nên ưu tiên một bố cục các biểu thức dựng sẵn ( ví dụ: hành vi UDF của Python đã được cải thiện trong Spark 2.0.0, nhưng nó vẫn không tối ưu so với thực thi gốc.
Điều này có thể được cải thiện trong tương lai đã được cải thiện đáng kể với việc giới thiệu các UDF được vector hóa (SPARK-21190 và các phần mở rộng hơn nữa) , sử dụng Arrow Streaming để trao đổi dữ liệu hiệu quả với khử lưu lượng không sao chép. Đối với hầu hết các ứng dụng, tổng phí phụ của chúng có thể bị bỏ qua.
Ngoài ra hãy chắc chắn để tránh truyền dữ liệu không cần thiết giữa DataFrames
và RDDs
. Điều này đòi hỏi phải tuần tự hóa và giải tuần tự tốn kém, chưa kể chuyển dữ liệu đến và từ trình thông dịch Python.
Điều đáng chú ý là các cuộc gọi Py4J có độ trễ khá cao. Điều này bao gồm các cuộc gọi đơn giản như:
from pyspark.sql.functions import col
col("foo")
Thông thường, điều đó không thành vấn đề (chi phí không đổi và không phụ thuộc vào lượng dữ liệu) nhưng trong trường hợp ứng dụng thời gian thực mềm, bạn có thể xem xét bộ đệm / sử dụng lại các trình bao bọc Java.
Các dữ liệu của GraphX và Spark
Hiện tại (Spark 1.6 2.1) không ai cung cấp API PySpark để bạn có thể nói rằng PySpark tệ hơn Scala vô cùng.
Đồ thị
Trong thực tế, việc phát triển GraphX đã dừng gần như hoàn toàn và dự án hiện đang ở chế độ bảo trì với các vé JIRA liên quan đã đóng vì sẽ không khắc phục được . Thư viện GraphFrames cung cấp một thư viện xử lý đồ thị thay thế với các ràng buộc Python.
Bộ dữ liệu
Nói một cách chủ quan, không có nhiều chỗ để gõ tĩnh Datasets
trong Python và ngay cả khi việc triển khai Scala hiện tại quá đơn giản và không mang lại lợi ích hiệu suất như DataFrame
.
Truyền phát
Từ những gì tôi đã thấy cho đến nay, tôi thực sự khuyên bạn nên sử dụng Scala trên Python. Nó có thể thay đổi trong tương lai nếu PySpark nhận được hỗ trợ cho các luồng có cấu trúc nhưng ngay bây giờ Scala API dường như mạnh mẽ, toàn diện và hiệu quả hơn nhiều. Kinh nghiệm của tôi khá hạn chế.
Phát trực tuyến có cấu trúc trong Spark 2.x dường như giảm khoảng cách giữa các ngôn ngữ nhưng hiện tại nó vẫn còn trong những ngày đầu. Tuy nhiên, API dựa trên RDD đã được tham chiếu là "truyền phát di sản" trong Tài liệu Databricks (ngày truy cập 2017-03-03)) vì vậy rất hợp lý để mong đợi những nỗ lực thống nhất hơn nữa.
Cân nhắc không thực hiện
Tính năng tương đương
Không phải tất cả các tính năng của Spark đều được hiển thị thông qua API PySpark. Hãy chắc chắn kiểm tra nếu các phần bạn cần đã được thực hiện và cố gắng hiểu những hạn chế có thể.
Điều này đặc biệt quan trọng khi bạn sử dụng MLlib và các bối cảnh hỗn hợp tương tự (xem Gọi hàm Java / Scala từ một tác vụ ). Để công bằng, một số phần của API PySpark, như mllib.linalg
, cung cấp một bộ phương thức toàn diện hơn Scala.
Thiết kế API
API PySpark phản ánh chặt chẽ đối tác Scala của nó và như vậy không chính xác là Pythonic. Điều đó có nghĩa là việc ánh xạ giữa các ngôn ngữ khá dễ dàng nhưng đồng thời, mã Python có thể khó hiểu hơn đáng kể.
Kiến trúc phức tạp
Luồng dữ liệu PySpark tương đối phức tạp so với thực thi JVM thuần túy. Lý do về các chương trình PySpark hoặc gỡ lỗi khó hơn nhiều. Hơn nữa, ít nhất là sự hiểu biết cơ bản về Scala và JVM nói chung là khá nhiều phải có.
Spark 2.x và hơn thế nữa
Sự thay đổi liên tục đối với Dataset
API, với API RDD đóng băng mang lại cả cơ hội và thách thức cho người dùng Python. Mặc dù các phần cấp cao của API dễ dàng hiển thị hơn trong Python, các tính năng nâng cao hơn hầu như không thể được sử dụng trực tiếp .
Hơn nữa, các hàm Python nguyên gốc tiếp tục là công dân hạng hai trong thế giới SQL. Hy vọng rằng điều này sẽ cải thiện trong tương lai với tuần tự hóa Mũi tên Apache ( dữ liệu mục tiêu nỗ lực hiện tạicollection
nhưng UDF serde là mục tiêu dài hạn ).
Đối với các dự án mạnh mẽ phụ thuộc vào cơ sở mã Python, các lựa chọn thay thế Python thuần túy (như Dask hoặc Ray ) có thể là một lựa chọn thú vị.
Nó không phải là một so với khác
API Spark DataFrame (SQL, Dataset) cung cấp một cách thức thanh lịch để tích hợp mã Scala / Java trong ứng dụng PySpark. Bạn có thể sử dụng DataFrames
để hiển thị dữ liệu thành mã JVM riêng và đọc lại kết quả. Tôi đã giải thích một số tùy chọn ở một nơi khác và bạn có thể tìm thấy một ví dụ hoạt động của roundtrip Python-Scala trong Cách sử dụng lớp Scala bên trong Pyspark .
Nó có thể được tăng thêm bằng cách giới thiệu các loại do người dùng xác định (xem Cách xác định lược đồ cho loại tùy chỉnh trong Spark SQL? ).
Có gì sai với mã được cung cấp trong câu hỏi
(Tuyên bố miễn trừ trách nhiệm: Quan điểm của Pythonista. Rất có thể tôi đã bỏ lỡ một số thủ thuật Scala)
Trước hết, có một phần trong mã của bạn không có ý nghĩa gì cả. Nếu bạn đã có (key, value)
các cặp được tạo bằng cách sử dụng zipWithIndex
hoặc enumerate
điểm nào trong việc tạo chuỗi chỉ để phân tách chuỗi ngay sau đó? flatMap
không hoạt động đệ quy vì vậy bạn chỉ có thể mang lại các bộ dữ liệu và bỏ qua map
bất cứ điều gì.
Một phần khác tôi thấy có vấn đề là reduceByKey
. Nói chung, reduceByKey
rất hữu ích nếu áp dụng chức năng tổng hợp có thể làm giảm lượng dữ liệu phải được xáo trộn. Vì bạn chỉ cần nối chuỗi, không có gì để đạt được ở đây. Bỏ qua các công cụ cấp thấp, như số lượng tài liệu tham khảo, lượng dữ liệu bạn phải chuyển hoàn toàn giống như đối với groupByKey
.
Thông thường tôi sẽ không quan tâm đến điều đó, nhưng theo như tôi có thể nói đó là một nút cổ chai trong mã Scala của bạn. Tham gia các chuỗi trên JVM là một hoạt động khá tốn kém (xem ví dụ: Việc nối chuỗi trong scala có tốn kém như trong Java không? ). Nó có nghĩa là một cái gì đó như thế này _.reduceByKey((v1: String, v2: String) => v1 + ',' + v2)
tương đương với input4.reduceByKey(valsConcat)
mã của bạn không phải là một ý tưởng tốt.
Nếu bạn muốn tránh groupByKey
bạn có thể thử sử dụng aggregateByKey
với StringBuilder
. Một cái gì đó tương tự như thế này nên thực hiện các mẹo:
rdd.aggregateByKey(new StringBuilder)(
(acc, e) => {
if(!acc.isEmpty) acc.append(",").append(e)
else acc.append(e)
},
(acc1, acc2) => {
if(acc1.isEmpty | acc2.isEmpty) acc1.addString(acc2)
else acc1.append(",").addString(acc2)
}
)
nhưng tôi nghi ngờ nó là giá trị tất cả các fuss.
Hãy ghi nhớ những điều trên, tôi đã viết lại mã của bạn như sau:
Scala :
val input = sc.textFile("train.csv", 6).mapPartitionsWithIndex{
(idx, iter) => if (idx == 0) iter.drop(1) else iter
}
val pairs = input.flatMap(line => line.split(",").zipWithIndex.map{
case ("true", i) => (i, "1")
case ("false", i) => (i, "0")
case p => p.swap
})
val result = pairs.groupByKey.map{
case (k, vals) => {
val valsString = vals.mkString(",")
s"$k,$valsString"
}
}
result.saveAsTextFile("scalaout")
Python :
def drop_first_line(index, itr):
if index == 0:
return iter(list(itr)[1:])
else:
return itr
def separate_cols(line):
line = line.replace('true', '1').replace('false', '0')
vals = line.split(',')
for (i, x) in enumerate(vals):
yield (i, x)
input = (sc
.textFile('train.csv', minPartitions=6)
.mapPartitionsWithIndex(drop_first_line))
pairs = input.flatMap(separate_cols)
result = (pairs
.groupByKey()
.map(lambda kv: "{0},{1}".format(kv[0], ",".join(kv[1]))))
result.saveAsTextFile("pythonout")
Các kết quả
Ở local[6]
chế độ (Intel (R) Xeon (R) CPU E3-1245 V2 @ 3.40GHz) với bộ nhớ 4GB cho mỗi người thi hành, phải mất (n = 3):
- Scala - có nghĩa là: 250,00, stdev: 12,49
- Python - có nghĩa là: 246.66s, stdev: 1.15
Tôi khá chắc chắn rằng phần lớn thời gian đó được dành cho việc xáo trộn, tuần tự hóa, giải tuần tự hóa và các nhiệm vụ phụ khác. Để giải trí, đây là mã đơn luồng ngây thơ trong Python thực hiện cùng một tác vụ trên máy này trong chưa đầy một phút:
def go():
with open("train.csv") as fr:
lines = [
line.replace('true', '1').replace('false', '0').split(",")
for line in fr]
return zip(*lines[1:])