Scala与Python的Spark性能


183

与Scala相比,我更喜欢Python。但是,由于Spark是用Scala原生编写的,出于明显的原因,我期望我的代码在Scala中的运行速度比Python版本快。

基于这个假设,我想学习和编写一些非常通用的预处理代码的Scala版本,用于大约1 GB的数据。数据选自Kaggle的SpringLeaf竞赛。只是为了概述数据(它包含1936个维度和145232行)。数据由各种类型组成,例如int,float,string,boolean。我正在使用8个内核中的6个进行Spark处理;minPartitions=6因此,我使用了每个内核都要处理的东西。

Scala代码

val input = sc.textFile("train.csv", minPartitions=6)

val input2 = input.mapPartitionsWithIndex { (idx, iter) => 
  if (idx == 0) iter.drop(1) else iter }
val delim1 = "\001"

def separateCols(line: String): Array[String] = {
  val line2 = line.replaceAll("true", "1")
  val line3 = line2.replaceAll("false", "0")
  val vals: Array[String] = line3.split(",")

  for((x,i) <- vals.view.zipWithIndex) {
    vals(i) = "VAR_%04d".format(i) + delim1 + x
  }
  vals
}

val input3 = input2.flatMap(separateCols)

def toKeyVal(line: String): (String, String) = {
  val vals = line.split(delim1)
  (vals(0), vals(1))
}

val input4 = input3.map(toKeyVal)

def valsConcat(val1: String, val2: String): String = {
  val1 + "," + val2
}

val input5 = input4.reduceByKey(valsConcat)

input5.saveAsTextFile("output")

Python代码

input = sc.textFile('train.csv', minPartitions=6)
DELIM_1 = '\001'


def drop_first_line(index, itr):
  if index == 0:
    return iter(list(itr)[1:])
  else:
    return itr

input2 = input.mapPartitionsWithIndex(drop_first_line)

def separate_cols(line):
  line = line.replace('true', '1').replace('false', '0')
  vals = line.split(',')
  vals2 = ['VAR_%04d%s%s' %(e, DELIM_1, val.strip('\"'))
           for e, val in enumerate(vals)]
  return vals2


input3 = input2.flatMap(separate_cols)

def to_key_val(kv):
  key, val = kv.split(DELIM_1)
  return (key, val)
input4 = input3.map(to_key_val)

def vals_concat(v1, v2):
  return v1 + ',' + v2

input5 = input4.reduceByKey(vals_concat)
input5.saveAsTextFile('output')

Scala Performance 阶段0(38分钟),阶段1(18秒) 在此处输入图片说明

Python Performance Stage 0(11分钟),Stage 1(7秒) 在此处输入图片说明

两者均产生不同的DAG可视化图(由于这两个图均显示了Scala(map)和Python(reduceByKey)的不同阶段0函数)

但是,本质上,这两个代码都试图将数据转换为(dimension_id,值列表的字符串)RDD并保存到磁盘。输出将用于计算每个维度的各种统计信息。

在性能方面,像这样的实际数据的Scala代码运行起来比Python版本慢4倍。对我来说好消息是,它给了我使用Python的良好动力。坏消息是我不太明白为什么吗?


8
也许这取决于代码和应用程​​序,因为我得到另一个结果,当将π的Leibniz公式的十亿项相加时
Paul

4
有趣的问题!顺便说一句,在这里也可以看看:emptypipes.org/2015/01/17/python-vs-scala-vs-spark您拥有的内核越多,看到的语言之间的差异就越少。
马肯2015年

您是否考虑过接受现有答案?
10465355说,恢复莫妮卡

Answers:


368

讨论代码的原始答​​案可以在下面找到。


首先,您必须区分不同类型的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及更高版本

不断向DatasetAPI过渡,使用冻结的RDD API给Python用户带来了机遇和挑战。尽管API的高级部分很容易在Python中公开,但更高级的功能几乎是不可能直接使用的。

而且,本机Python函数在SQL世界中仍然是二等公民。希望将来通过Apache Arrow序列化可以改善这种情况(当前的工作是针对数据,collection但UDF serde是一个长期目标)。

对于强烈依赖Python代码库的项目,纯Python替代品(例如DaskRay)可能是一个有趣的替代品。

不一定要一个与另一个

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:])

26
我已经遇到了最清晰,最全面,最有用的答案之一。谢谢!
etov '18

你真是个好人!
DennisLi

这是同一任务吗?最后一个zip不是很懒,并且没有保存到文件吗?
Dror Speiser

-5

扩展以上答案-

与python相比,Scala在许多方面都证明了更快,但是有一些合理的理由说明python在scala中越来越流行的原因。

适用于Apache Spark的Python非常易于学习和使用。但是,这并不是Pyspark比Scala更好的选择的唯一原因。还有更多。

用于Spark的Python API可能在群集上速度较慢,但​​最终,与Scala相比,数据科学家可以用它做更多的事情。没有Scala的复杂性。界面简单全面。

谈论代码的可读性,维护性以及对Apache Spark的Python API的熟悉程度远胜于Scala。

Python附带了一些与机器学习和自然语言处理有关的库。这有助于进行数据分析,并具有非常成熟且经过时间检验的统计信息。例如numpy,pandas,scikit-learn,seaborn和matplotlib。

注意:大多数数据科学家都使用混合方法,在这两种方法中,它们都使用了两种API中的最佳方法。

最后,Scala社区对程序员的帮助往往很少。这使Python成为非常有价值的学习。如果您对Java之类的任何静态类型的编程语言都有足够的经验,则可以不必担心完全不使用Scala。

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.