コードを議論する元の答えは以下にあります。
まず最初に、異なるタイプのAPIを区別する必要があり、それぞれに独自のパフォーマンスの考慮事項があります。
RDD API
(JVMベースのオーケストレーションによる純粋なPython構造)
これは、PythonコードのパフォーマンスとPySpark実装の詳細によって最も影響を受けるコンポーネントです。Pythonのパフォーマンスが問題になる可能性はかなり低いですが、考慮すべき少なくともいくつかの要因があります。
- JVM通信のオーバーヘッド。実際には、Pythonエグゼキューターとの間でやり取りされるすべてのデータは、ソケットとJVMワーカーを介して渡される必要があります。これは比較的効率的なローカル通信ですが、まだ無料ではありません。
プロセスベースの実行プログラム(Python)とスレッドベースの実行プログラム(単一のJVM複数スレッド)の実行プログラム(Scala)。各Python executorは独自のプロセスで実行されます。副作用として、JVMの同等物よりも強力な分離が提供され、executorライフサイクルをある程度制御できますが、メモリ使用量が大幅に高くなる可能性があります。
- インタプリタのメモリフットプリント
- ロードされたライブラリのフットプリント
- 効率の悪いブロードキャスト(各プロセスにはブロードキャストの独自のコピーが必要)
Pythonコード自体のパフォーマンス。一般的に言えば、ScalaはPythonよりも高速ですが、タスクによって異なります。さらに、Numba、C拡張(Cython)などのJIT 、またはTheanoなどの専用ライブラリを含む複数のオプションがあります。最後に、ML / MLlib(または単にNumPyスタック)を使用しない場合は、PyPyを代替インタープリターとして使用することを検討してください。SPARK-3094を参照してください。
- PySpark構成は、
spark.python.worker.reuse
各タスクのPythonプロセスをフォークするか、既存のプロセスを再利用するかを選択できるオプションがされています。後者のオプションは、高価なガベージコレクションを回避するのに役立つようです(体系的なテストの結果よりも印象的です)。一方、前者のオプション(デフォルト)は、高価なブロードキャストとインポートの場合に最適です。
- CPythonの最初の行のガベージコレクションメソッドとして使用される参照カウントは、典型的なSparkワークロード(ストリームのような処理、参照サイクルなし)でかなり機能し、長いGC一時停止のリスクを軽減します。
MLlib
(PythonとJVMの混合実行)
基本的な考慮事項は以前とほとんど同じですが、いくつかの追加の問題があります。MLlibで使用される基本構造はプレーンなPython RDDオブジェクトですが、すべてのアルゴリズムはScalaを使用して直接実行されます。
これは、PythonオブジェクトをScalaオブジェクトに変換するなどの追加コスト、メモリ使用量の増加、および後で取り上げる追加の制限を意味します。
現在(Spark 2.x)、RDDベースのAPIはメンテナンスモードであり、Spark 3.0では削除される予定です。
DataFrame APIとSpark ML
(ドライバーに限定されたPythonコードでのJVM実行)
これらはおそらく、標準のデータ処理タスクに最適です。Pythonコードは主にドライバーの高レベルの論理演算に限定されているため、PythonとScalaの間にパフォーマンスの違いはありません。
単一の例外は、行単位のPython UDFの使用です。これは、Scalaの同等のものよりも効率が大幅に低下します。改善の余地はありますが(Spark 2.0.0では大幅な開発が行われています)、最大の制限は内部表現(JVM)とPythonインタープリター間の完全な往復です。可能であれば、組み込み式の構成を優先する必要があります(例: Spark 2.0.0ではPython UDFの動作が改善されていますが、ネイティブの実行と比較すると依然として最適ではありません。
これは将来改善される可能性があり、ベクトル化されたUDF(SPARK-21190およびその他の拡張機能)の導入により大幅に改善されました。ほとんどのアプリケーションでは、二次的なオーバーヘッドは無視できます。
また、DataFrames
との間でデータを不必要に渡さないようにしてくださいRDDs
。これには、Pythonインタプリタとの間のデータ転送は言うまでもなく、高価なシリアライゼーションとデシリアライゼーションが必要です。
Py4J呼び出しのレイテンシがかなり長いことは注目に値します。これには、次のような単純な呼び出しが含まれます。
from pyspark.sql.functions import col
col("foo")
通常、これは問題になりません(オーバーヘッドは一定であり、データの量に依存しません)。しかし、ソフトリアルタイムアプリケーションの場合は、Javaラッパーのキャッシュ/再利用を検討してください。
GraphXおよびSparkデータセット
今のところ(Spark 1.6 2.1)どちらもPySpark APIを提供していないため、PySparkはScalaよりもはるかに悪いと言えます。
GraphX
実際には、GraphX開発はほぼ完全に停止し、プロジェクトは現在メンテナンスモードにあり、関連するJIRAチケットは修正されないため閉じられています。GraphFramesライブラリは、Pythonバインディングを備えた代替のグラフ処理ライブラリを提供します。
データセット
主観的に言えばDatasets
、Python で静的に型付けする場所があまりなく、現在のScalaの実装が単純すぎても、と同じパフォーマンス上の利点は得られませんDataFrame
。
ストリーミング
これまで見てきたことから、PythonではなくScalaを使用することを強くお勧めします。PySparkが構造化ストリームをサポートするようになれば、将来的には変更される可能性がありますが、現時点では、Scala APIの方がはるかに堅牢で包括的かつ効率的です。私の経験はかなり限られています。
Spark 2.xの構造化ストリーミングは言語間のギャップを減らすようですが、現時点ではまだ初期段階です。それにもかかわらず、RDDベースのAPIは、Databricksドキュメント(アクセス日2017-03-03)で「レガシーストリーミング」としてすでに参照されているため、さらなる統合の取り組みを期待するのは妥当です。
パフォーマンス以外の考慮事項
機能のパリティ
すべてのSpark機能がPySpark APIを通じて公開されるわけではありません。必要なパーツがすでに実装されているかどうかを確認し、考えられる制限を理解してください。
MLlibおよび同様の混合コンテキストを使用する場合は特に重要です(タスクからのJava / Scala関数の呼び出しを参照)。)。公平にするために、などのPySpark APIの一部は、mllib.linalg
Scalaよりも包括的なメソッドセットを提供します。
APIデザイン
PySpark APIは、対応するScalaを厳密に反映しているため、正確にはPythonicではありません。つまり、言語間でのマッピングは非常に簡単ですが、同時にPythonコードは非常に理解しにくくなる可能性があります。
複雑なアーキテクチャ
PySparkのデータフローは、純粋なJVMの実行に比べて比較的複雑です。PySparkプログラムまたはデバッグについて推論することははるかに困難です。さらに、少なくともScalaとJVMの一般的な基本的な理解は、ほぼ必須です。
Spark 2.x以降
Dataset
RDD APIの凍結により、API への継続的な移行は、Pythonユーザーに機会と課題の両方をもたらします。APIの高レベルの部分はPythonで公開する方がはるかに簡単ですが、より高度な機能を直接使用することはほとんど不可能です。
さらに、ネイティブPython関数は引き続きSQLの世界で2番目のクラスの市民です。うまくいけば、Apache Arrowのシリアライゼーション(現在の取り組みではデータを対象としていますcollection
が、UDF serdeが長期的な目標です)によって、これが将来的に改善されることを願っています。
Pythonコードベースに強く依存するプロジェクトの場合、純粋なPythonの代替(DaskやRayなど)が興味深い代替になる可能性があります。
どちらかである必要はありません
Spark DataFrame(SQL、Dataset)APIは、PySparkアプリケーションにScala / Javaコードを統合するエレガントな方法を提供します。を使用DataFrames
して、データをネイティブJVMコードに公開し、結果を読み取ることができます。一部のオプションについては別の場所で説明しましたが、Python-Scala往復の実際の例は、Pyspark内でScalaクラスを使用する方法にあります。
ユーザー定義型を導入することでさらに拡張できます(「Spark SQLでカスタム型のスキーマを定義する方法」を参照)。
質問で提供されたコードの何が問題になっていますか
(免責事項:Pythonistaの視点。おそらく私はいくつかのScalaのトリックを逃した)
まず第一に、あなたのコードにはまったく意味をなさない部分があります。(key, value)
を使用してペアをすでに作成している場合、zipWithIndex
またはenumerate
すぐに分割するために文字列を作成するポイントは何ですか?flatMap
は再帰的に機能しないため、タプルを生成し、フォローをスキップできますmap
。
私が問題だと思う別の部分はreduceByKey
です。一般的に言えば、reduceByKey
集約関数を適用することで、シャッフルする必要があるデータの量を減らすことができる場合に役立ちます。単に文字列を連結するだけなので、ここでは何も得られません。参照の数などの低レベルのものを無視すると、転送する必要のあるデータの量はとまったく同じですgroupByKey
。
通常、それについては詳しく説明しませんが、私の知る限り、Scalaコードのボトルネックになります。JVMでの文字列の結合は、かなりコストのかかる操作です(たとえば、Javaの場合と同じように、Scalaでの文字列連結はコストがかかりますか?)。それはあなたのコードと_.reduceByKey((v1: String, v2: String) => v1 + ',' + v2)
同等のこのような何かinput4.reduceByKey(valsConcat)
が良い考えではないことを意味します。
回避したい場合は、groupByKey
を使用aggregateByKey
してみてくださいStringBuilder
。これに似た何かがうまくいくはずです:
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)
}
)
しかし、それはすべての大騒ぎの価値があるとは思いません。
上記を念頭に置いて、私はあなたのコードを次のように書き直しました:
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")
結果
local[6]
エグゼキュータ当たり4GBのメモリとモード(インテル(R)Xeonプロセッサ(R)CPU E3-1245 V2 @ 3.40GHz)はかかり(N = 3)。
- Scala-平均:250.00秒、標準偏差:12.49
- Python-平均:246.66s、標準偏差:1.15
そのほとんどの時間は、シャッフル、シリアライズ、デシリアライズ、その他の二次的なタスクに費やされていると思います。面白くするために、このマシンで同じタスクを1分未満で実行する単純なPythonのシングルスレッドコードを次に示します。
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:])