Spark: Tại sao Python vượt trội hơn hẳn Scala trong trường hợp sử dụng của tôi?


16

Để so sánh hiệu suất của Spark khi sử dụng Python và Scala, tôi đã tạo ra cùng một công việc trong cả hai ngôn ngữ và so sánh thời gian chạy. Tôi dự kiến ​​cả hai công việc sẽ mất khoảng thời gian như nhau, nhưng công việc Python chỉ mất 27min, trong khi công việc Scala mất 37min(lâu hơn gần 40%!). Tôi cũng đã thực hiện công việc tương tự trong Java và nó 37minutescũng vậy. Làm thế nào điều này có thể là Python nhanh hơn nhiều?

Ví dụ kiểm chứng tối thiểu:

Công việc Python:

# Configuration
conf = pyspark.SparkConf()
conf.set("spark.hadoop.fs.s3a.aws.credentials.provider", "org.apache.hadoop.fs.s3a.AnonymousAWSCredentialsProvider")
conf.set("spark.executor.instances", "4")
conf.set("spark.executor.cores", "8")
sc = pyspark.SparkContext(conf=conf)

# 960 Files from a public dataset in 2 batches
input_files = "s3a://commoncrawl/crawl-data/CC-MAIN-2019-35/segments/1566027312025.20/warc/CC-MAIN-20190817203056-20190817225056-00[0-5]*"
input_files2 = "s3a://commoncrawl/crawl-data/CC-MAIN-2019-35/segments/1566027312128.3/warc/CC-MAIN-20190817102624-20190817124624-00[0-3]*"

# Count occurances of a certain string
logData = sc.textFile(input_files)
logData2 = sc.textFile(input_files2)
a = logData.filter(lambda value: value.startswith('WARC-Type: response')).count()
b = logData2.filter(lambda value: value.startswith('WARC-Type: response')).count()

print(a, b)

Công việc Scala:

// Configuration
config.set("spark.executor.instances", "4")
config.set("spark.executor.cores", "8")
val sc = new SparkContext(config)
sc.setLogLevel("WARN")
sc.hadoopConfiguration.set("fs.s3a.aws.credentials.provider", "org.apache.hadoop.fs.s3a.AnonymousAWSCredentialsProvider")

// 960 Files from a public dataset in 2 batches 
val input_files = "s3a://commoncrawl/crawl-data/CC-MAIN-2019-35/segments/1566027312025.20/warc/CC-MAIN-20190817203056-20190817225056-00[0-5]*"
val input_files2 = "s3a://commoncrawl/crawl-data/CC-MAIN-2019-35/segments/1566027312128.3/warc/CC-MAIN-20190817102624-20190817124624-00[0-3]*"

// Count occurances of a certain string
val logData1 = sc.textFile(input_files)
val logData2 = sc.textFile(input_files2)
val num1 = logData1.filter(line => line.startsWith("WARC-Type: response")).count()
val num2 = logData2.filter(line => line.startsWith("WARC-Type: response")).count()

println(s"Lines with a: $num1, Lines with b: $num2")

Chỉ cần nhìn vào mã, chúng dường như giống hệt nhau. Tôi đã xem một DAG và họ không cung cấp bất kỳ thông tin chi tiết nào (hoặc ít nhất là tôi thiếu bí quyết để đưa ra lời giải thích dựa trên chúng).

Tôi thực sự sẽ đánh giá cao bất kỳ con trỏ.


Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
Samuel Liew

1
Tôi đã bắt đầu phân tích, trước khi hỏi bất cứ điều gì, bằng cách định thời gian cho các khối và câu lệnh tương ứng để xem liệu có một nơi cụ thể nào mà phiên bản python nhanh hơn không. Sau đó, bạn có thể đã có thể làm sắc nét câu hỏi thành 'tại sao câu lệnh trăn này nhanh hơn'.
Terry Jan Reedy

Câu trả lời:


11

Giả định cơ bản của bạn, rằng Scala hoặc Java sẽ nhanh hơn cho tác vụ cụ thể này, là không chính xác. Bạn có thể dễ dàng xác minh nó với các ứng dụng cục bộ tối thiểu. Scala một:

import scala.io.Source
import java.time.{Duration, Instant}

object App {
  def main(args: Array[String]) {
    val Array(filename, string) = args

    val start = Instant.now()

    Source
      .fromFile(filename)
      .getLines
      .filter(line => line.startsWith(string))
      .length

    val stop = Instant.now()
    val duration = Duration.between(start, stop).toMillis
    println(s"${start},${stop},${duration}")
  }
}

Python một

import datetime
import sys

if __name__ == "__main__":
    _, filename, string = sys.argv
    start = datetime.datetime.now()
    with open(filename) as fr:
        # Not idiomatic or the most efficient but that's what
        # PySpark will use
        sum(1 for _ in filter(lambda line: line.startswith(string), fr))

    end = datetime.datetime.now()
    duration = round((end - start).total_seconds() * 1000)
    print(f"{start},{end},{duration}")

Kết quả (300 lần lặp lại mỗi lần, Python 3.7.6, Scala 2.11.12), Posts.xmltừ kết xuất dữ liệu hermeneutics.stackexchange.com với sự pha trộn của các mẫu phù hợp và không khớp:

boxplots of durartion in millis cho các chương trình trên

  • Con trăn 273,50 (258,84, 288,16)
  • Scala 634,13 (533,81, 734,45)

Như bạn thấy Python không chỉ nhanh hơn một cách có hệ thống mà còn phù hợp hơn (mức độ lây lan thấp hơn).

Tin nhắn mang đi là - đừng tin FUD không có căn cứ - các ngôn ngữ có thể nhanh hơn hoặc chậm hơn đối với các tác vụ cụ thể hoặc với các môi trường cụ thể (ví dụ ở đây Scala có thể bị tấn công bởi JVM và / hoặc GC và / hoặc JIT), nhưng nếu bạn tuyên bố như "XYZ là X4 nhanh hơn" hoặc "XYZ chậm so với ZYX (..) Khoảng 10 lần, chậm hơn" thường có nghĩa là ai đó đã viết mã thực sự xấu để kiểm tra mọi thứ.

Chỉnh sửa :

Để giải quyết một số mối quan tâm nêu trong các ý kiến:

  • Trong dữ liệu mã OP được truyền theo hầu hết theo một hướng (JVM -> Python) và không yêu cầu tuần tự hóa thực sự (đường dẫn cụ thể này chỉ chuyển qua kiểm tra nguyên trạng và giải mã trên UTF-8 ở phía bên kia). Đó là rẻ như nó được khi nói đến "tuần tự hóa".
  • Những gì được truyền lại chỉ là một số nguyên duy nhất theo phân vùng, do đó, tác động theo hướng đó là không đáng kể.
  • Giao tiếp được thực hiện qua các ổ cắm cục bộ (tất cả các giao tiếp trên worker ngoài kết nối ban đầu và auth được thực hiện bằng cách sử dụng bộ mô tả tệp được trả về local_connect_and_authvà không có gì khác ngoài tệp liên quan đến socket ). Một lần nữa, rẻ như nó có được khi nói đến giao tiếp giữa các quy trình.
  • Xem xét sự khác biệt về hiệu suất thô được hiển thị ở trên (cao hơn nhiều so với những gì bạn thấy trong chương trình của bạn), có rất nhiều lợi nhuận cho các chi phí được liệt kê ở trên.
  • Trường hợp này hoàn toàn khác với các trường hợp mà các đối tượng đơn giản hoặc phức tạp phải được chuyển đến và từ trình thông dịch Python ở dạng có thể truy cập được cho cả hai bên dưới dạng bãi chứa tương thích với dưa chua (ví dụ đáng chú ý nhất bao gồm UDF kiểu cũ, một số phần cũ kiểu MLLib).

Chỉnh sửa 2 :

jasper-m quan tâm đến chi phí khởi động ở đây, người ta có thể dễ dàng chứng minh rằng Python vẫn có lợi thế đáng kể so với Scala ngay cả khi kích thước đầu vào được tăng lên đáng kể.

Dưới đây là kết quả cho 2003360 dòng / 5.6G (cùng một đầu vào, chỉ được nhân đôi nhiều lần, 30 lần lặp lại), vượt quá mọi thứ bạn có thể mong đợi trong một tác vụ Spark.

nhập mô tả hình ảnh ở đây

  • Con trăn 22809.57 (21466,26, 24152,87)
  • Scala 27315.28 (24367.24, 30263.31)

Xin lưu ý khoảng tin cậy không chồng chéo.

Chỉnh sửa 3 :

Để giải quyết một bình luận khác từ Jasper-M:

Phần lớn tất cả quá trình xử lý vẫn đang diễn ra bên trong một JVM trong trường hợp Spark.

Điều đó chỉ đơn giản là không chính xác trong trường hợp cụ thể này:

  • Công việc đang nói đến là công việc bản đồ với mức giảm toàn cầu duy nhất bằng cách sử dụng RDD của PySpark.
  • PySpark RDD (không giống như giả sử DataFrame) thực hiện tổng số chức năng nguyên bản trong Python, với giao tiếp ngoại lệ, đầu ra và giao tiếp giữa các nút.
  • Vì nó là công việc một giai đoạn và đầu ra cuối cùng đủ nhỏ để bị bỏ qua, nên trách nhiệm chính của JVM (nếu là với nitpick, điều này được thực hiện chủ yếu trong Java chứ không phải Scala) là gọi định dạng đầu vào Hadoop và đẩy dữ liệu qua ổ cắm gửi tới Python.
  • Phần đọc giống hệt với API JVM và Python, vì vậy nó có thể được coi là chi phí không đổi. Nó cũng không đủ điều kiện là phần lớn của xử lý , ngay cả đối với công việc đơn giản như thế này.

3
cách tiếp cận tuyệt vời của vấn đề. Cảm ơn bạn đã chia sẻ điều này
Alexandros Biratsis

1
@egordoe Alexandros nói "không có UDF nào được gọi ở đây" không phải là "Python không được gọi" - điều đó tạo ra tất cả sự khác biệt. Chi phí tuần tự hóa rất quan trọng khi dữ liệu được trao đổi giữa các hệ thống (tức là khi bạn muốn truyền dữ liệu tới UDF và quay lại).
10938362

1
@egordoe Bạn nhầm lẫn rõ ràng hai điều - chi phí nối tiếp, đó là vấn đề trong đó các đối tượng không tầm thường được truyền qua lại. Và chi phí liên lạc. Có rất ít hoặc không có chi phí tuần tự hóa ở đây, bởi vì bạn chỉ cần vượt qua và giải mã bytestrings, và điều đó xảy ra chủ yếu theo hướng, khi quay lại, bạn nhận được một số nguyên cho mỗi phân vùng. Truyền thông là một số mối quan tâm, nhưng truyền dữ liệu qua các ổ cắm cục bộ là hiệu quả vì nó thực sự có được khi liên lạc giữa các quá trình. Nếu điều đó không rõ ràng, tôi khuyên bạn nên đọc nguồn này - nó không khó và sẽ được khai sáng.
10938362

1
Ngoài ra các phương pháp tuần tự hóa chỉ là không được thực hiện như nhau. Vì trường hợp Spark cho thấy các phương thức tuần tự hóa tốt có thể cắt giảm chi phí đến mức không còn quan tâm nữa (xem Pandas UDF với Mũi tên) và khi nó xảy ra, các yếu tố khác có thể chiếm ưu thế (ví dụ so sánh hiệu suất giữa các chức năng của cửa sổ Scala và tương đương với Pandas UDF - Python thắng với tỷ suất lợi nhuận cao hơn nhiều so với câu hỏi này).
10938362

1
Và quan điểm của bạn là @ Jasper-M? Các tác vụ Spark riêng lẻ thường đủ nhỏ để có khối lượng công việc tương đương với điều này. Đừng đưa tôi sai cách, nhưng nếu bạn có bất kỳ ví dụ thực tế nào làm mất hiệu lực câu hỏi này hoặc toàn bộ câu hỏi, xin vui lòng gửi nó. Tôi đã lưu ý rằng các hành động thứ cấp đóng góp ở một mức độ nào đó vào giá trị này, nhưng chúng không chi phối chi phí. Tất cả chúng ta đều là kỹ sư (một số loại) ở đây - chúng ta hãy nói về số và mã, không phải niềm tin, phải không?
10938362

4

Công việc Scala mất nhiều thời gian hơn vì nó có cấu hình sai và do đó, các công việc Python và Scala đã được cung cấp với các tài nguyên không đồng đều.

Có hai lỗi trong mã:

val sc = new SparkContext(config) // LINE #1
sc.setLogLevel("WARN")
sc.hadoopConfiguration.set("fs.s3a.aws.credentials.provider", "org.apache.hadoop.fs.s3a.AnonymousAWSCredentialsProvider")
sc.hadoopConfiguration.set("spark.executor.instances", "4") // LINE #4
sc.hadoopConfiguration.set("spark.executor.cores", "8") // LINE #5
  1. LINE 1. Khi dòng đã được thực thi, cấu hình tài nguyên của công việc Spark đã được thiết lập và sửa chữa. Từ thời điểm này, không có cách nào để điều chỉnh bất cứ điều gì. Không phải số lượng người thi hành cũng như số lượng nhân trên mỗi người thi hành.
  2. ĐƯỜNG 4-5. sc.hadoopConfigurationlà một vị trí sai để đặt bất kỳ cấu hình Spark. Nó nên được đặt trong configtrường hợp bạn truyền đến new SparkContext(config).

[THÊM] Ghi nhớ những điều trên, tôi sẽ đề xuất thay đổi mã của công việc Scala thành

config.set("spark.executor.instances", "4")
config.set("spark.executor.cores", "8")
val sc = new SparkContext(config) // LINE #1
sc.setLogLevel("WARN")
sc.hadoopConfiguration.set("fs.s3a.aws.credentials.provider", "org.apache.hadoop.fs.s3a.AnonymousAWSCredentialsProvider")

và kiểm tra lại một lần nữa. Tôi cá là phiên bản Scala sẽ nhanh hơn X lần.


Tôi đã xác minh rằng cả hai công việc chạy song song 32 nhiệm vụ vì vậy tôi không nghĩ đây là thủ phạm?
maestromusica

cảm ơn vì đã chỉnh sửa, sẽ thử kiểm tra nó ngay bây giờ
maestromusica

hi @maestromusica nó phải là một cái gì đó trong cấu hình tài nguyên bởi vì, về bản chất, Python có thể không vượt trội hơn Scala trong trường hợp sử dụng cụ thể này. Một lý do khác có thể là một số yếu tố ngẫu nhiên không tương quan, tức là tải của cụm tại thời điểm cụ thể và tương tự. Btw, bạn sử dụng chế độ nào? độc lập, địa phương, sợi?
egordoe

Vâng, tôi đã xác minh rằng câu trả lời này không chính xác. Thời gian chạy là như nhau. Tôi cũng đã in cấu hình trong cả hai trường hợp và nó giống hệt nhau.
maestromusica

1
Tôi nghĩ bạn có thể đúng. Tôi đã hỏi câu hỏi này để điều tra tất cả các khả năng khác như lỗi trong mã hoặc có thể là tôi đã hiểu nhầm điều gì đó. Cảm ơn vì đầu vào của bạn.
maestromusica
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.