Hiệu suất Spark cho Scala vs Python


178

Tôi thích Python hơn Scala. Nhưng, vì Spark được viết bằng Scala, tôi đã hy vọng mã của mình chạy nhanh hơn trong Scala so với phiên bản Python vì những lý do rõ ràng.

Với giả định đó, tôi nghĩ sẽ học và viết phiên bản Scala của một số mã tiền xử lý rất phổ biến cho khoảng 1 GB dữ liệu. Dữ liệu được chọn từ cuộc thi SpringLeaf trên Kaggle . Chỉ để đưa ra một cái nhìn tổng quan về dữ liệu (nó chứa 1936 kích thước và 145232 hàng). Dữ liệu bao gồm nhiều loại khác nhau, ví dụ int, float, string, boolean. Tôi đang sử dụng 6 lõi trong số 8 để xử lý Spark; đó là lý do tại sao tôi sử dụng minPartitions=6để mọi lõi đều có thứ gì đó để xử lý.

Mã Scala

val input = sc.textFile("train.csv", minPartitions=6)

val input2 = input.mapPartitionsWithIndex { (idx, iter) => 
  if (idx == 0) iter.drop(1) else iter }
val delim1 = "\001"

def separateCols(line: String): Array[String] = {
  val line2 = line.replaceAll("true", "1")
  val line3 = line2.replaceAll("false", "0")
  val vals: Array[String] = line3.split(",")

  for((x,i) <- vals.view.zipWithIndex) {
    vals(i) = "VAR_%04d".format(i) + delim1 + x
  }
  vals
}

val input3 = input2.flatMap(separateCols)

def toKeyVal(line: String): (String, String) = {
  val vals = line.split(delim1)
  (vals(0), vals(1))
}

val input4 = input3.map(toKeyVal)

def valsConcat(val1: String, val2: String): String = {
  val1 + "," + val2
}

val input5 = input4.reduceByKey(valsConcat)

input5.saveAsTextFile("output")

Mã Python

input = sc.textFile('train.csv', minPartitions=6)
DELIM_1 = '\001'


def drop_first_line(index, itr):
  if index == 0:
    return iter(list(itr)[1:])
  else:
    return itr

input2 = input.mapPartitionsWithIndex(drop_first_line)

def separate_cols(line):
  line = line.replace('true', '1').replace('false', '0')
  vals = line.split(',')
  vals2 = ['VAR_%04d%s%s' %(e, DELIM_1, val.strip('\"'))
           for e, val in enumerate(vals)]
  return vals2


input3 = input2.flatMap(separate_cols)

def to_key_val(kv):
  key, val = kv.split(DELIM_1)
  return (key, val)
input4 = input3.map(to_key_val)

def vals_concat(v1, v2):
  return v1 + ',' + v2

input5 = input4.reduceByKey(vals_concat)
input5.saveAsTextFile('output')

Scala Performance Giai đoạn 0 (38 phút), Giai đoạn 1 (18 giây) nhập mô tả hình ảnh ở đây

Giai đoạn thực hiện Python 0 (11 phút), Giai đoạn 1 (7 giây) nhập mô tả hình ảnh ở đây

Cả hai đều tạo ra các biểu đồ trực quan DAG khác nhau (do cả hai hình ảnh hiển thị các hàm giai đoạn 0 khác nhau cho Scala ( map) và Python ( reduceByKey))

Nhưng, về cơ bản, cả hai mã đều cố gắng chuyển đổi dữ liệu thành (height_id, chuỗi danh sách các giá trị) RDD và lưu vào đĩa. Đầu ra sẽ được sử dụng để tính toán các số liệu thống kê khác nhau cho mỗi thứ nguyên.

Hiệu suất khôn ngoan, mã Scala cho dữ liệu thực như thế này dường như chạy chậm hơn 4 lần so với phiên bản Python. Tin tốt cho tôi là nó đã cho tôi động lực tốt để ở lại với Python. Tin xấu là tôi đã không hiểu tại sao?


8
Có lẽ đây là mã và ứng dụng phụ thuộc khi tôi nhận được kết quả khác, đó là con trăn tia lửa apache chậm hơn scala, khi tổng kết một tỷ thuật ngữ của công thức Leibniz cho π
Paul

3
Câu hỏi thú vị! Btw, cũng đã xem ở đây: blankpipes.org/2015/01/17/python-vs-scala-vs-spark Bạn càng có nhiều lõi, bạn càng ít thấy sự khác biệt giữa các ngôn ngữ.
Markon

Bạn đã xem xét chấp nhận câu trả lời hiện có?
10465355 nói Phục hồi lại

Câu trả lời:


357

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.reusetù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 DataFramesRDDs. Đ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 Datasetstrong 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 DatasetAPI, 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 zipWithIndexhoặc enumerateđiểm nào trong việc tạo chuỗi chỉ để phân tách chuỗi ngay sau đó? flatMapkhô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 mapbất cứ điều gì.

Một phần khác tôi thấy có vấn đề là reduceByKey. Nói chung, reduceByKeyrấ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 groupByKeybạn có thể thử sử dụng aggregateByKeyvớ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:])

22
Một trong những câu trả lời rõ ràng, toàn diện và hữu ích nhất mà tôi đã gặp trong một thời gian. Cảm ơn!
etov

Thật là một chàng trai tuyệt vời!
DennisLi

-4

Mở rộng cho câu trả lời trên -

Scala chứng minh nhanh hơn bằng nhiều cách so với python nhưng có một số lý do hợp lệ tại sao python trở nên phổ biến hơn mà scala, hãy xem một vài trong số chúng -

Python cho Apache Spark khá dễ học và sử dụng. Tuy nhiên, đây không phải là lý do duy nhất tại sao Pyspark là lựa chọn tốt hơn Scala. Còn nữa.

API Python cho Spark có thể chậm hơn trên cụm, nhưng cuối cùng, các nhà khoa học dữ liệu có thể làm được nhiều hơn với nó so với Scala. Sự phức tạp của Scala vắng mặt. Giao diện đơn giản và toàn diện.

Nói về khả năng đọc mã, bảo trì và làm quen với API Python cho Apache Spark tốt hơn nhiều so với Scala.

Python đi kèm với một số thư viện liên quan đến học máy và xử lý ngôn ngữ tự nhiên. Hỗ trợ này trong phân tích dữ liệu và cũng có số liệu thống kê đã trưởng thành và được thử nghiệm nhiều thời gian. Ví dụ, numpy, gấu trúc, scikit-learn, seaborn và matplotlib.

Lưu ý: Hầu hết các nhà khoa học dữ liệu sử dụng phương pháp lai trong đó họ sử dụng tốt nhất cả hai API.

Cuối cùng, cộng đồng Scala thường trở nên ít hữu ích hơn cho các lập trình viên. Điều này làm cho Python trở thành một bài học có giá trị. Nếu bạn có đủ kinh nghiệm với bất kỳ ngôn ngữ lập trình được gõ tĩnh nào như Java, bạn có thể ngừng lo lắng về việc không sử dụng Scala hoàn toàn.

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.