讨论代码的原始答案可以在下面找到。
首先,您必须区分不同类型的API,每种类型都有其自己的性能注意事项。
RDD API
(具有基于JVM的编排的纯Python结构)
这是受Python代码性能和PySpark实现细节影响最大的组件。尽管Python性能不太可能成为问题,但您至少需要考虑以下因素:
- JVM通信的开销。几乎所有来自Python执行器的数据都必须通过套接字和JVM工作者传递。尽管这是一种相对高效的本地通信,但它仍然不是免费的。
基于进程的执行器(Python)与基于线程的(单个JVM多线程)执行器(Scala)。每个Python执行程序都在自己的进程中运行。副作用是,与JVM相比,它提供了更强的隔离性,并且可以对执行程序的生命周期进行某些控制,但可能显着提高内存使用量:
- 解释器内存占用
- 加载库的占用空间
- 广播效率较低(每个过程都需要自己的广播副本)
Python代码本身的性能。一般而言,Scala比Python快,但因任务而异。此外,您有多种选择,包括Numba等JIT,C扩展(Cython)或Theano等专用库。最后,如果您不使用ML / MLlib(或仅使用NumPy堆栈),请考虑使用PyPy作为替代解释器。参见SPARK-3094。
- PySpark配置提供的
spark.python.worker.reuse
选项可用于在为每个任务分叉Python进程与重用现有进程之间进行选择。后一种选择似乎对避免昂贵的垃圾收集很有用(它比系统测试的结果更令人印象深刻),而前一种选择(默认)对于广播和导入昂贵的情况是最佳的。
- 引用计数用作CPython中的第一行垃圾回收方法,可与典型的Spark工作负载(类似流的处理,无引用周期)一起很好地工作,并减少了长时间GC暂停的风险。
图书馆
(混合执行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解释器之间的完全往返。如果可能的话,您应该偏向于内置表达式的组合(例如。Python UDF行为在Spark 2.0.0中得到了改善,但是与本机执行相比,它仍然不是最佳的。
随着矢量化UDF的引入(SPARK-21190和其他扩展)的出现,这种情况在将来可能会得到显着改善。矢量化UDF使用Arrow Streaming进行零拷贝反序列化的有效数据交换。对于大多数应用程序,其次要开销可以忽略不计。
另外,请确保避免在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和类似的混合上下文时,这一点尤其重要(请参阅从task调用Java / Scala函数)。公平地说,PySpark API的某些部分(例如mllib.linalg
)提供了比Scala更全面的方法集。
API设计
PySpark API紧密反映了它的Scala对应内容,因此也不完全是Pythonic。这意味着在各种语言之间映射非常容易,但与此同时,Python代码可能很难理解。
复杂的架构
与纯JVM执行相比,PySpark数据流相对复杂。关于PySpark程序或调试的理由要困难得多。而且,至少对Scala和JVM的基本了解至少是必不可少的。
Spark 2.x及更高版本
不断向Dataset
API过渡,使用冻结的RDD API给Python用户带来了机遇和挑战。尽管API的高级部分很容易在Python中公开,但更高级的功能几乎是不可能直接使用的。
而且,本机Python函数在SQL世界中仍然是二等公民。希望将来通过Apache Arrow序列化可以改善这种情况(当前的工作是针对数据,collection
但UDF serde是一个长期目标)。
对于强烈依赖Python代码库的项目,纯Python替代品(例如Dask或Ray)可能是一个有趣的替代品。
不一定要一个与另一个
Spark DataFrame(SQL,数据集)API提供了一种将Scala / Java代码集成到PySpark应用程序中的优雅方法。您可以DataFrames
用来将数据公开给本地JVM代码并读回结果。我已经在其他地方解释了一些选项,您可以在如何在Pyspark中使用Scala类中找到Python-Scala往返的有效示例。
可以通过引入用户定义的类型来进一步增强它(请参阅如何在Spark SQL中为自定义类型定义架构?)。
问题中提供的代码有什么问题
(免责声明:Pythonista的观点。很可能我错过了一些Scala技巧)
首先,您的代码中只有一部分完全没有意义。如果您已经有(key, value)
使用创建的对,zipWithIndex
或者enumerate
创建字符串后立即将其拆分的意义何在?flatMap
不能递归工作,因此您可以简单地生成元组并跳过map
任何内容。
我觉得有问题的另一部分是reduceByKey
。一般来说,reduceByKey
如果应用聚合函数可以减少必须重新整理的数据量,则很有用。由于您只是连接字符串,因此没有任何好处。忽略低级内容(例如引用数),您必须传输的数据量与完全相同groupByKey
。
通常,我不会对此进行详细介绍,但是据我所知,这是您Scala代码中的瓶颈。在JVM上连接字符串是一项相当昂贵的操作(例如,请参见:scala中的字符串连接是否像Java中那样昂贵?)。这意味着在您的代码中_.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]
模式(英特尔®至强®CPU E3-1245 V2 @ 3.40GHz),每个执行器具有4GB内存的情况下,它需要(n = 3):
- Scala-平均值:250.00s,stdev:12.49
- Python-平均值:246.66s,标准偏差:1.15
我很确定大部分时间都花在改组,序列化,反序列化和其他辅助任务上。只是为了好玩,这是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:])