Spark:私のユースケースでPythonがScalaを大幅に上回っているのはなぜですか?


16

PythonとScalaを使用しているときのSparkのパフォーマンスを比較するために、両方の言語で同じジョブを作成し、ランタイムを比較しました。私は両方のジョブにほぼ同じ時間がかかると予想していましたが、Pythonのジョブだけがかかりましたが27min、Scalaのジョブはかかりました37min(ほぼ40%長くなります!)。私はJavaにも同じジョブを実装しましたが、それもかかり37minutesました。どうしてこれがPythonがそんなに速いのか

最小限の検証可能な例:

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)

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

コードを見るだけで、それらは同じように見えます。私はDAGを調べましたが、DAGは洞察を提供しませんでした(または少なくとも、それらに基づいて説明を作成するためのノウハウが不足しています)。

私はすべてのポインタをいただければ幸いです。


コメントは詳細な議論のためのものではありません。この会話はチャットに移動しました
Samuel Liew

1
何かを尋ねる前に、対応するブロックとステートメントのタイミングを調整して、Pythonバージョンの方が速い特定の場所があったかどうかを確認することで、分析を開始しました。次に、「なぜこのpythonステートメントが速いのか」という質問を明確にすることができたかもしれません。
Terry Jan Reedy

回答:


11

ScalaまたはJavaがこの特定のタスクに対してより高速である必要があるというあなたの基本的な仮定は、正しくありません。最小限のローカルアプリケーションで簡単に確認できます。Scala one:

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 one

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

結果(各300回の繰り返し、Python 3.7.6、Scala 2.11.12)、hermeneutics.stackexchange.comデータダンプPosts.xmlから、一致するパターンと一致しないパターンが混在するデータダンプ

上記のプログラムのミリ秒単位のデュラティオンの箱ひげ図

  • Python 273.50(258.84、288.16)
  • Scala 634.13(533.81、734.45)

ご覧のように、Pythonは体系的に高速であるだけでなく、一貫性が高くなっています(拡散が低い)。

要点のメッセージは‒根拠のないFUDを信じていない‒特定のタスクまたは特定の環境では言語が速くなったり遅くなったりする可能性があります(たとえば、ここではScalaはJVMの起動および/またはGCおよび/またはJITの影響を受ける可能性があります)。 「XYZはX4が速い」または「XYZはZYXと比較して遅い(..)およそ10倍遅い」のように、それは通常、誰かが本当に悪いコードを書いて物事をテストしたことを意味します。

編集

コメントで提起されたいくつかの懸念に対処するには:

  • OPコードでは、データは主に一方向(JVM-> Python)で渡され、実際のシリアル化は必要ありません(この特定のパスはバイトストリングをそのまま渡し、反対側でUTF-8でデコードするだけです)。それは「シリアライゼーション」になるとそれが得るのと同じくらい安いです。
  • 返されるのはパーティションごとの単一の整数なので、その方向への影響は無視できます。
  • 通信はローカルソケットを介して行われます(最初の接続と認証を超えたワーカーでのすべての通信は、から返されたファイル記述子local_connect_and_authと、ソケットに関連付けられたファイルを使用して実行されます)。繰り返しになりますが、プロセス間の通信に関しては、できるだけ安価です。
  • 上記の生のパフォーマンスの違い(プログラムで表示されるパフォーマンスよりもはるかに高い)を考慮すると、上記のオーバーヘッドには多くのマージンがあります。
  • このケースは、単純または複雑なオブジェクトをPythonインタープリターとの間で受け渡し可能な形式でPythonインタープリターに渡したり、ピクル互換のダンプとして両方からアクセスしたりする必要がある場合とはまったく異なります(最も注目すべき例には、古いスタイルのUDF、古いバージョンの一部が含まれます)スタイルのMLLib)。

編集2

以来碧玉-mは、ここで起動時のコストを懸念して、人は簡単Pythonはまだ入力サイズが大幅に増加した場合でもスカラ座の上に大きな利点を持っていることを証明することができます。

2003360ライン/ 5.6G(同じ入力、複数回複製、30回の繰り返し)の結果は次のとおりです。これは、単一のSparkタスクで期待できるすべてを超えています。

ここに画像の説明を入力してください

  • Python 22809.57(21466.26、24152.87)
  • Scala 27315.28(24367.24、30263.31)

重複しない信頼区間に注意してください。

編集3

Jasper-Mからの別のコメントに対処するには:

Sparkのケースでは、すべての処理の大部分がまだJVM内で行われています。

これは、この特定のケースでは単に正しくありません。

  • 問題のジョブは、PySpark RDDを使用した単一のグローバル削減を伴うマップジョブです。
  • PySpark RDD(たとえば、とは異なりますDataFrame)は、例外的な入力、出力、ノード間通信を除いて、Pythonでネイティブに総機能を実装します。
  • これは単一段階のジョブであり、最終出力は無視できるほど小さいので、JVMの主な責任(ニップピックする場合、これは主にScalaではなくJavaで実装されます)は、Hadoop入力フォーマットを呼び出し、ソケットを介してデータをプッシュすることです。 Pythonにファイルします。
  • 読み取り部分はJVMとPython APIで同一であるため、一定のオーバーヘッドと見なすことができます。また、このような単純なジョブであっても、処理の大部分とは見なされません。

3
問題の優れたアプローチ。これを共有していただきありがとうございます
アレクサンドロスBiratsis

1
@egordoe Alexandrosは、「ここではUDFが呼び出されない」と言ったのではなく、「Pythonが呼び出されていない」と述べた-それがすべての違いを生んでいる。シリアル化のオーバーヘッドは、システム間でデータを交換する場合(つまり、UDFにデータを渡したい場合)に重要です。
user10938362

1
@egordoe明らかに2つのことを混同します-シリアライゼーションのオーバーヘッド、これは重要なオブジェクトがやり取りされる問題です。そして、通信のオーバーヘッド。バイト列を渡してデコードするだけなので、シリアライゼーションのオーバーヘッドはほとんどまたはまったくありません。これは、パーティションごとに単一の整数を取得するため、ほとんどの場合、方向で行われます。通信にはいくつかの懸念がありますが、ローカルソケットを介したデータの受け渡しは、プロセス間通信の場合と同様に効率的です。それがはっきりしない場合は、ソースを読むことをお勧めします。難しいことではなく、啓発されます。
user10938362

1
さらに、シリアライゼーションの方法は同じではありません。Sparkのケースが示すように、適切なシリアライゼーションメソッドはコストを問題のないレベルに削減でき(Pandas UDF with Arrowを参照)、それが発生すると他の要因が支配する可能性があります(たとえば、Scalaウィンドウ関数と同等のPandasとのパフォーマンス比較を参照) UDF-Pythonは、この質問よりもはるかに高いマージンで勝っています)。
user10938362

1
そしてあなたのポイントは@ Jasper-Mですか?個々のSparkタスクは通常、これに匹敵するワークロードを持つのに十分小さいです。誤解しないでください。この質問または質問全体を無効にする実際の反例がある場合は、投稿してください。二次的なアクションがこの値にある程度寄与することはすでに述べましたが、それらはコストを支配しません。私たちはすべて(ある種の)エンジニアです-信念ではなく、数字とコードについて話しましょうか?
user10938362

4

Scalaジョブは構成に誤りがあり、PythonとScalaのジョブに等しくないリソースが提供されていたため、時間がかかりました。

コードには2つの誤りがあります。

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.ラインが実行されると、Sparkジョブのリソース構成がすでに確立され、修正されています。この時点から、何も調整する方法はありません。エグゼキューターの数も、エグゼキューターあたりのコア数も。
  2. LINE 4-5。sc.hadoopConfigurationSpark構成を設定する場所としては間違っています。config渡すインスタンスで設定する必要がありますnew SparkContext(config)

[追加]上記を念頭に置いて、Scalaジョブのコードを次のように変更することを提案します。

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

もう一度テストしてみてください。ScalaバージョンはX倍速くなると思います。


両方のジョブが32のタスクを並行して実行することを確認したので、これが原因ではないと思いますか?
maestromusica

編集をありがとう、今すぐテストしてみましょう
maestromusica

こんにちは@maestromusicaこれはリソース構成に含まれている必要があります。本質的に、この特定のユースケースではPythonがScalaよりも優れているとは限らないためです。別の理由は、いくつかの無相関のランダムな要因、つまり特定の瞬間におけるクラスターの負荷などです。ところで、あなたはどのモードを使いますか?スタンドアロン、ローカル、糸?
エゴルド

はい、この回答が正しくないことを確認しました。ランタイムは同じです。どちらの場合も設定を印刷しましたが、まったく同じです。
maestromusica

1
私はあなたが正しいかもしれないと思います。コードの間違いや、何か誤解している可能性など、他のすべての可能性を調査するためにこの質問をしました。ご入力いただきありがとうございます。
maestromusica
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.