簡體   English   中英

Scala 與 Python 的 Spark 性能

[英]Spark performance for Scala vs Python

相比 Scala,我更喜歡 Python。 但是,由於 Spark 本身是用 Scala 編寫的,我希望我的代碼在 Scala 中運行得比 Python 版本更快,原因很明顯。

有了這個假設,我想為大約 1 GB 的數據學習和編寫一些非常常見的預處理代碼的 Scala 版本。 數據來自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 表演第 0 階段(38 分鍾),第 1 階段(18 秒) 在此處輸入圖片說明

Python 性能第 0 階段(11 分鍾)、第 1 階段(7 秒) 在此處輸入圖片說明

兩者都生成不同的 DAG 可視化圖(由於這兩張圖顯示了 Scala ( map ) 和 Python ( reduceByKey ) 的不同階段 0 函數)

但是,基本上這兩個代碼都試圖將數據轉換為(dimension_id,值列表字符串)RDD 並保存到磁盤。 輸出將用於計算每個維度的各種統計數據。

在性能方面,像這樣的真實數據的 Scala 代碼似乎比 Python 版本慢 4 倍 對我來說好消息是它給了我繼續使用 Python 的良好動力。 壞消息是我不太明白為什么?


可以在下面找到討論代碼的原始答​​案。


首先,您必須區分不同類型的 API,每種 API 都有自己的性能考慮。

RDD API

(具有基於 JVM 的編排的純 Python 結構)

這是受 Python 代碼性能和 PySpark 實現細節影響最大的組件。 雖然 Python 性能不太可能成為問題,但您至少需要考慮幾個因素:

  • JVM 通信的開銷。 實際上,所有進出 Python 執行器的數據都必須通過套接字和 JVM 工作線程傳遞。 雖然這是一種相對有效的本地通信,但它仍然不是免費的。
  • 基於進程的執行程序 (Python) 與基於線程的(單 JVM 多線程)執行程序 (Scala)。 每個 Python 執行器都在自己的進程中運行。 作為副作用,它提供了比 JVM 更強的隔離和對執行程序生命周期的一些控制,但可能會顯着提高內存使用量:

    • 解釋器內存占用
    • 加載的庫的足跡
    • 廣播效率較低(每個進程都需要自己的廣播副本)
  • 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

(JVM 執行的 Python 代碼僅限於驅動程序)

這些可能是標准數據處理任務的最佳選擇。 由於 Python 代碼主要限於驅動程序上的高級邏輯操作,因此 Python 和 Scala 之間應該沒有性能差異。

一個例外是使用行式 Python UDF,其效率明顯低於 Scala 等效項。 雖然有一些改進的機會(Spark 2.0.0 有了實質性的發展),但最大的限制是內部表示 (JVM) 和 Python 解釋器之間的完整往返。 如果可能,您應該支持內置表達式的組合(例如,Spark 2.0.0 中改進了 Python UDF 行為,但與本機執行相比,它仍然不是最佳的。

隨着矢量化 UDF(SPARK-21190 和進一步擴展)的引入,這可能會在未來得到顯着改善,它使用 Arrow Streaming 進行零拷貝反序列化的高效數據交換。 對於大多數應用程序,它們的次要開銷可以忽略不計。

還要確保避免在DataFramesRDDs之間傳遞不必要的數據。 這需要昂貴的序列化和反序列化,更不用說與 Python 解釋器之間的數據傳輸了。

值得注意的是,Py4J 調用具有相當高的延遲。 這包括簡單的調用,例如:

from pyspark.sql.functions import col

col("foo")

通常,這無關緊要(開銷是恆定的並且不取決於數據量),但是在軟實時應用程序的情況下,您可以考慮緩存/重用 Java 包裝器。

GraphX 和 Spark 數據集

至於現在(Spark 1.6 2.1)都沒有提供 PySpark API,所以你可以說 PySpark 比 Scala 差很多。

圖X

在實踐中,GraphX 開發幾乎完全停止,該項目目前處於維護模式, 相關 JIRA 票證已關閉,無法修復 GraphFrames庫提供了一個帶有 Python 綁定的替代圖形處理庫。

數據集

主觀上講,Python 中靜態類型的Datasets並沒有太多位置,即使當前的 Scala 實現也過於簡單,無法提供與DataFrame相同的性能優勢。

流媒體

從我目前所見,我強烈建議使用 Scala 而不是 Python。 如果 PySpark 獲得對結構化流的支持,未來可能會發生變化,但現在 Scala API 似乎更加健壯、全面和高效。 我的經驗非常有限。

Spark 2.x 中的結構化流似乎縮小了語言之間的差距,但目前它仍處於早期階段。 盡管如此,基於 RDD 的 API 已經在Databricks 文檔(訪問日期2017 年 3月 3 日)中被稱為“傳統流”,因此有理由期待進一步的統一努力。

非績效考慮

功能奇偶校驗

並非所有 Spark 功能都通過 PySpark API 公開。 請務必檢查您需要的部分是否已經實現,並嘗試了解可能的限制。

當您使用 MLlib 和類似的混合上下文時,這一點尤其重要(請參閱從任務中調用 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 替代方案(如DaskRay )可能是一個有趣的替代方案。

不一定非要一對一

Spark DataFrame (SQL, Dataset) API 提供了一種在 PySpark 應用程序中集成 Scala/Java 代碼的優雅方式。 您可以使用DataFrames將數據公開給本機 JVM 代碼並讀回結果。 我已經在其他地方解釋了一些選項,您可以在How to use a Scala class inside Pyspark 中找到 Python-Scala 往返的工作示例。

可以通過引入用戶定義類型進一步增強它(請參閱如何在 Spark SQL 中為自定義類型定義模式? )。


問題中提供的代碼有什么問題

(免責聲明:Pythonista 的觀點。很可能我錯過了一些 Scala 技巧)

首先,您的代碼中有一個部分根本沒有意義。 如果您已經使用zipWithIndex創建了(key, value)對,或者enumerate創建字符串只是為了在之后立即拆分它有什么意義? flatMap不能遞歸工作,因此您可以簡單地生成元組並跳過任何map

我發現有問題的另一部分是reduceByKey 一般來說,如果應用聚合函數可以減少必須打亂的數據量, reduceByKey很有用。 由於您只是連接字符串,因此這里沒有任何好處。 忽略低級的東西,比如引用的數量,你必須傳輸的數據量與groupByKey

通常我不會細說,但據我所知,這是 Scala 代碼中的瓶頸。 在 JVM 上連接字符串是一項相當昂貴的操作(參見例如: Scala 中的字符串連接是否與 Java 中的一樣昂貴? )。 這意味着像這樣的_.reduceByKey((v1: String, v2: String) => v1 + ',' + v2)相當於代碼中的input4.reduceByKey(valsConcat)不是一個好主意。

如果您想避免groupByKey您可以嘗試將aggregateByKeyStringBuilder一起使用。 與此類似的事情應該可以解決問題:

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

但我懷疑這是否值得大驚小怪。

牢記上述內容,我已將您的代碼重寫如下:

斯卡拉

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

蟒蛇

 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]模式(Intel(R) Xeon(R) CPU E3-1245 V2 @ 3.40GHz)下,每個執行器需要 4GB 內存(n = 3):

  • Scala - 平均值:250.00s,標准差: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:])

對上述答案的擴展 -

與 Python 相比,Scala 在許多方面都被證明更快,但 Python 比 Scala 更受歡迎的原因有很多,讓我們看看其中的幾個——

Python for Apache Spark 非常容易學習和使用。 然而,這並不是 Pyspark 比 Scala 更好的選擇的唯一原因。 還有更多。

用於 Spark 的 Python API 在集群上可能更慢,但最終,與 Scala 相比,數據科學家可以用它做更多的事情。 Scala 的復雜性不存在。 界面簡單而全面。

說起代碼的可讀性、維護性以及對Python API for Apache Spark 的熟悉程度,都遠勝於Scala。

Python 附帶了幾個與機器學習和自然語言處理相關的庫。 這有助於數據分析,並且還具有非常成熟且經過時間考驗的統計數據。 例如,numpy、pandas、scikit-learn、seaborn 和 matplotlib。

注意:大多數數據科學家使用混合方法,他們使用兩種 API 中的優點。

最后,Scala 社區通常對程序員的幫助不大。 這使得 Python 成為非常有價值的學習。 如果您對任何靜態類型的編程語言(如 Java)有足夠的經驗,您就可以不必擔心完全不使用 Scala。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM