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,但它们没有提供任何见解(或者至少我缺乏基于它们提出解释的专业知识)。

我真的很感谢任何指示。


评论不作进一步讨论;此对话已转移至聊天
塞缪尔·刘

1
在问任何问题之前,我将通过计时相应的块和语句来开始分析,以查看是否有某个特定地方的python版本更快。然后,您可能可以使问题更加尖锐,即“为什么此python语句更快”。
Terry Jan Reedy

Answers:


11

您的基本假设(Scala或Java对于此特定任务应该更快)是不正确的。您可以使用最少的本地应用程序轻松地对其进行验证。Scala之一:

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一

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

Posts.xml来自hermeneutics.stackexchange.com数据转储的结果(每个重复300个重复,Python 3.7.6,Scala 2.11.12)具有匹配和不匹配模式的混合:

以上程序在毫毫秒中的持续时间框线图

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

如您所见,Python不仅在系统上更快,而且更一致(传播率更低)。

带走的信息是‒不要相信未经证实的FUD ‒语言在特定任务或特定环境下可能会更快或更慢(例如,此处的Scala可能会受到JVM启动和/或GC和/或JIT的攻击),但是如果您声明例如“ XYZ比XYX快X4”或“ XYZ比ZYX(..)慢大约10倍”,这通常意味着有人编写了非常糟糕的代码来测试事物。

编辑

为了解决评论中提出的一些问题:

  • 在OP代码中,数据主要在一个方向上传递(JVM-> Python),不需要真正的序列化(此特定路径仅按原样传递字节串,而在另一侧以UTF-8解码)。与“序列化”一样便宜。
  • 回传的只是按分区划分的单个整数,因此在这个方向上的影响可以忽略不计。
  • 通信是通过本地套接字完成的(除初始连接和auth之外,所有关于worker的通信都是使用从返回的文件描述符执行的local_connect_and_auth,除了套接字关联的文件)。再次,当涉及到进程之间的通信时,它变得便宜。
  • 考虑到上面显示的原始性能的差异(比您在程序中看到的要高得多),上面列出的间接费用有很大的余地。
  • 这种情况与简单或复杂对象必须以双方都可以通过与咸菜兼容的转储方式进行访问的方式传递给Python解释器(完全值得注意的例子包括旧式UDF,旧式的某些部分)。样式的MLLib)。

编辑2

由于jasper-m在这里担心启动成本,因此即使输入量大大增加,人们也可以轻松证明Python与Scala相比仍具有显着优势。

这是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的主要职责(如果是nitpick,这主要是在Java中而不是在Scala中实现)是调用Hadoop输入格式,并通过套接字推送数据文件到Python。
  • 读取的部分与JVM和Python API相同,因此可以将其视为恒定的开销。即使对于像这样的简单工作,它也不是处理的主要内容

3
解决问题的极好方法。感谢您的分享
Alexandros Biratsis

1
@egordoe Alexandros说:“这里没有调用UDF”,而不是“没有调用Python”,这一切都与众不同。在系统之间交换数据时(即,当您想将数据传递给UDF并返回时),串行化开销非常重要。
user10938362

1
@egordoe您显然混淆了两件事-序列化的开销,这是来回传递非平凡对象的问题。和通讯开销。这里很少或没有序列化开销,因为您只传递和解码字节串,并且这大部分发生在方向上,因为每个分区返回一个整数。通讯是一个令人担忧的问题,但是通过本地套接字传递数据非常有效,因为在进行进程间通讯时,它确实可以实现。如果不清楚的话,我建议您阅读源代码-它并不难,而且会有所启发。
user10938362

1
另外,只是序列化方法不相等。由于Spark案例显示了良好的序列化方法可以将成本削减到不再需要的水平(请参见带有箭头的Pandas UDF),并且在发生这种情况时,其他因素也可以起主导作用(例如,请参见Scala窗口函数及其与Pandas等效项之间的性能比较) UDF-Python赢得的利润要比这个问题高得多)。
user10938362

1
你的意思是@ Jasper-M?单个Spark任务通常很小,足以承担与此类似的工作量。不要误解我的意思,但是如果您有任何实际的反例使这个问题或整个问题无效,请发表。我已经注意到,次要行动在一定程度上促成了这一价值,但它们并没有控制成本。我们都是(某种)工程师,让我们谈谈数字和代码,而不是信念,对吗?
user10938362

4

Scala作业需要更长时间,因为它配置错误,因此为Python和Scala作业提供了不平等的资源。

代码中有两个错误:

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. 4-5行。sc.hadoopConfiguration设置任何Spark配置的位置错误。应该在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。另一个原因可能是一些不相关的随机因素,即群集在特定时刻的负载等。顺便说一句,您使用什么模式?独立,本地,纱线?
egordoe

是的,我已经证实此答案不正确。运行时间是相同的。在这两种情况下,我也都打印了配置,它是相同的。
maestromusica

1
我想你可能是对的。我问这个问题来调查所有其他可能性,例如代码中的错误或可能我误解了某些东西。感谢您的输入。
maestromusica
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.