코드에 대한 원래 답변은 아래에서 확인할 수 있습니다.
우선, 각각 고유 한 성능 고려 사항이있는 서로 다른 유형의 API를 구별해야합니다.
RDD API
(JVM 기반 오케스트레이션으로 순수한 Python 구조)
이것은 파이썬 코드의 성능과 PySpark 구현의 세부 사항에 가장 큰 영향을받는 구성 요소입니다. 파이썬 성능은 문제가되지 않지만, 고려해야 할 최소한의 요소는 다음과 같습니다.
- JVM 통신 오버 헤드 실제로 파이썬 실행기와주고받는 모든 데이터는 소켓과 JVM 작업자를 통해 전달되어야합니다. 이것은 비교적 효율적인 로컬 통신이지만 여전히 무료는 아닙니다.
프로세스 기반 실행기 (Python) 대 스레드 기반 (단일 JVM 다중 스레드) 실행기 (Scala). 각 Python 실행 프로그램은 자체 프로세스에서 실행됩니다. 부작용으로, JVM보다 강력한 격리 기능을 제공하고 실행기 수명주기에 대한 일부 제어 기능을 제공하지만 잠재적으로 메모리 사용량이 크게 증가합니다.
- 인터프리터 메모리 풋 프린트
- 로드 된 라이브러리의 설치 공간
- 덜 효율적인 방송 (각 프로세스에는 자체 방송 사본이 필요함)
파이썬 코드 자체의 성능. 일반적으로 스칼라는 파이썬보다 빠르지 만 작업마다 다릅니다. 또한 Numba 와 같은 JIT , C 확장 ( Cython ) 또는 Theano 와 같은 특수 라이브러리를 포함한 여러 옵션이 있습니다 . 마지막으로, 당신은 ML / MLlib (또는 단순히 NumPy와 스택)를 사용하지 않는 경우 , 사용을 고려 PyPy를 대체 통역. SPARK-3094를 참조하십시오 .
- PySpark 구성은
spark.python.worker.reuse
각 작업마다 Python 프로세스를 포크하는 것과 기존 프로세스를 재사용하는 것 중에서 선택할 수있는 옵션을 제공합니다 . 후자의 옵션은 값 비싼 가비지 수집 (체계적인 테스트의 결과보다 더 인상적 임)을 피하는 데 유용하지만, 전자 (기본값)는 값 비싼 방송 및 수입의 경우에 최적입니다.
- CPython에서 첫 번째 가비지 수집 방법으로 사용되는 참조 횟수는 일반적인 Spark 워크로드 (스트림과 같은 처리, 참조주기 없음)에서 잘 작동하며 긴 GC 일시 중지 위험을 줄입니다.
MLlib
(혼합 된 Python 및 JVM 실행)
기본 고려 사항은 몇 가지 추가 문제와 함께 이전과 거의 동일합니다. MLlib와 함께 사용되는 기본 구조는 일반 Python RDD 객체이지만 모든 알고리즘은 Scala를 사용하여 직접 실행됩니다.
그것은 파이썬 객체를 스칼라 객체로 변환하는 추가 비용과 다른 방법으로 메모리 사용이 증가하고 나중에 다룰 추가 제한 사항을 의미합니다.
현재 (Spark 2.x) RDD 기반 API는 유지 보수 모드이며 Spark 3.0에서 제거 될 예정 입니다.
DataFrame API 및 Spark ML
(드라이버로 제한된 Python 코드로 JVM 실행)
이들은 아마도 표준 데이터 처리 작업에 가장 적합한 선택 일 것입니다. 파이썬 코드는 대부분 드라이버에서 높은 수준의 논리 연산으로 제한되기 때문에 파이썬과 스칼라간에 성능 차이가 없어야합니다.
단 하나의 예외는 스칼라 동등 물보다 훨씬 덜 효율적인 행 단위 파이썬 UDF의 사용입니다. 개선의 여지가 있지만 (Spark 2.0.0에는 상당한 발전이 있었음) 가장 큰 한계는 내부 표현 (JVM)과 Python 인터프리터 사이의 완전한 왕복입니다. 가능하면 내장 식의 구성을 선호해야합니다 ( 예 : Spark 2.0.0에서 Python UDF 동작이 개선되었지만 기본 실행에 비해 여전히 차선책입니다.
이것은 미래에 개선 할 수 대폭의 도입으로 개선 벡터화 UDF를 (SPARK-21190 및 추가 확장) 제로 복사 직렬화와 효율적인 데이터 교환을 위해 화살표 스트리밍을 사용합니다. 대부분의 응용 프로그램에서 보조 오버 헤드는 무시해도됩니다.
또한 사이에 불필요한 데이터 전달을 방지해야 DataFrames
하고 RDDs
. 파이썬 인터프리터와의 데이터 전송은 말할 것도없고 값 비싼 직렬화 및 역 직렬화가 필요합니다.
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
파이썬 에는 정적으로 유형을 지정할 수있는 장소가 많지 않으며 현재 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는 스칼라 대응 API를 밀접하게 반영하므로 정확히 Pythonic이 아닙니다. 언어간에 매핑하기가 쉽지만 동시에 파이썬 코드는 이해하기가 훨씬 어려울 수 있습니다.
복잡한 아키텍처
PySpark 데이터 흐름은 순수한 JVM 실행에 비해 비교적 복잡합니다. PySpark 프로그램에 대해 추론하거나 디버그하기가 훨씬 어렵습니다. 또한 스칼라와 JVM에 대한 기본적인 이해는 일반적으로 거의 필요합니다.
Spark 2.x 이상
Dataset
동결 된 RDD API를 통해 API 로의 지속적인 변화 는 파이썬 사용자에게 기회와 도전을 제공합니다. API의 높은 수준의 부분은 Python에서 노출하기가 훨씬 쉽지만 고급 기능은 직접 사용하기가 거의 불가능합니다 .
게다가 네이티브 파이썬 함수는 SQL 세계에서 계속해서 2 등 시민입니다. 다행스럽게도 향후 Apache Arrow 직렬화를 통해 개선 될 것입니다 ( 현재의 노력collection
은 데이터를 목표로 하지만 UDF serde는 장기적인 목표입니다 ).
Python 코드베이스에 의존하는 프로젝트의 경우 Dask 또는 Ray 와 같은 순수한 Python 대안 이 흥미로운 대안이 될 수 있습니다.
한 대 다른 대일 필요는 없습니다.
Spark DataFrame (SQL, Dataset) API는 PySpark 응용 프로그램에서 Scala / Java 코드를 통합하는 우아한 방법을 제공합니다. DataFrames
데이터를 기본 JVM 코드에 노출하고 결과를 다시 읽는 데 사용할 수 있습니다 . 다른 곳에서 몇 가지 옵션을 설명 했으며 Pyspark에서 Scala 클래스를 사용하는 방법 에서 Python-Scala 왕복의 실제 예제를 찾을 수 있습니다 .
사용자 정의 유형을 도입하여 추가 기능을 보강 할 수 있습니다 ( Spark SQL에서 사용자 정의 유형에 대한 스키마를 정의하는 방법 참조 ).
질문에 제공된 코드에 어떤 문제가 있습니까?
(면책 조항 : Pythonista 관점. 스칼라 트릭을 놓친 것 같습니다.)
우선, 코드에는 전혀 이해가되지 않는 부분이 있습니다. 이미 (key, value)
쌍을 사용하여 만들었 zipWithIndex
거나 enumerate
바로 문자열을 만들 때 요점은 무엇입니까? flatMap
재귀 적으로 작동하지 않으므로 단순히 튜플을 생성하고 다른 것을 따라 건너 뛸 수 있습니다 map
.
내가 문제를 찾는 다른 부분은 reduceByKey
입니다. 일반적으로 reduceByKey
집계 함수를 적용하면 셔플해야하는 데이터 양을 줄일 수있는 경우에 유용합니다. 단순히 문자열을 연결하기 때문에 여기서 얻을 것이 없습니다. 참조 수와 같은 저수준 항목을 무시하면 전송해야하는 데이터의 양은와 정확히 동일합니다 groupByKey
.
일반적으로 나는 그것에 머물지 않을 것이지만, 내가 말할 수있는 한 스칼라 코드의 병목 현상입니다. JVM에서 문자열을 결합하는 것은 비용이 많이 드는 작업입니다 (예 : 스칼라에서 문자열 연결이 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)
}
)
그러나 나는 그것이 모든 소란의 가치가 있는지 의심합니다.
위의 내용을 염두에두고 다음과 같이 코드를 다시 작성했습니다.
스칼라 :
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]
모드 (인텔 (R) 제온 (R) CPU E3-1245 V2 @ 3.40GHz) 기가 바이트 걸리는 실행기 당 메모리 (N = 3)과 :
- 스칼라-평균 : 250.00s, 표준 : 12.49
- 파이썬-평균 : 246.66 초, stdev : 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:])