คำตอบดั้งเดิมเกี่ยวกับรหัสสามารถพบได้ที่ด้านล่าง
ก่อนอื่นคุณต้องแยกแยะความแตกต่างของ API แต่ละประเภทด้วยการพิจารณาประสิทธิภาพของตัวเอง
API RDD
(โครงสร้าง Python ล้วนๆพร้อมการประสานแบบ JVM)
นี่คือองค์ประกอบที่จะได้รับผลกระทบมากที่สุดจากประสิทธิภาพของรหัส Python และรายละเอียดของการใช้งาน PySpark ในขณะที่ประสิทธิภาพของ Python นั้นไม่น่าเป็นปัญหา แต่อย่างน้อยก็มีปัจจัยบางอย่างที่คุณต้องพิจารณา:
- โอเวอร์เฮดของการสื่อสาร JVM ข้อมูลทั้งหมดที่มาถึงและจากผู้ดำเนินการหลามจวนจะต้องถูกส่งผ่านซ็อกเก็ตและผู้ปฏิบัติงาน JVM แม้ว่านี่จะเป็นการสื่อสารในท้องถิ่นที่มีประสิทธิภาพ แต่ก็ยังไม่ฟรี
ตัวประมวลผลแบบอิงกระบวนการ (Python) และตัวประมวลผล (Scala) แบบเธรด (เธรดเดี่ยว JVM หลายเธรด) ตัวจัดการ Python แต่ละตัวทำงานในกระบวนการของตัวเอง ในฐานะที่เป็นผลข้างเคียงมันให้การแยกที่แข็งแกร่งกว่า JVM คู่กันและการควบคุมวงจรชีวิตของตัวจัดการบางอย่าง แต่อาจมีการใช้หน่วยความจำที่สูงขึ้นอย่างมีนัยสำคัญ:
- ล่ามรอยหน่วยความจำ
- รอยเท้าของห้องสมุดที่โหลด
- การออกอากาศที่มีประสิทธิภาพน้อยกว่า (แต่ละกระบวนการต้องการสำเนาการออกอากาศ)
ประสิทธิภาพของรหัสหลามเอง โดยทั่วไปการพูด Scala เร็วกว่า Python แต่มันจะแตกต่างกันไปในแต่ละงาน นอกจากนี้คุณมีตัวเลือกหลายอย่างรวมทั้ง JITs เช่นNumbaส่วนขยาย C ( Cython ) หรือห้องสมุดเฉพาะเช่นTheano สุดท้ายถ้าคุณไม่ใช้ ML / MLlib (หรือเพียงแค่ NumPy stack)ให้ลองใช้PyPyเป็นล่ามทางเลือก ดูSPARK-3094
- การกำหนดค่า PySpark มี
spark.python.worker.reuse
ตัวเลือกที่สามารถใช้ในการเลือกระหว่างการดำเนินการ Python สำหรับแต่ละงานและนำกระบวนการที่มีอยู่กลับมาใช้ใหม่ ตัวเลือกหลังดูเหมือนว่าจะมีประโยชน์ในการหลีกเลี่ยงการเก็บขยะที่มีราคาแพง (มันเป็นความประทับใจมากกว่าผลของการทดสอบอย่างเป็นระบบ) ในขณะที่ตัวเลือกก่อนหน้า (ค่าเริ่มต้น) นั้นดีที่สุดสำหรับกรณีที่การออกอากาศและการนำเข้ามีราคาแพง
- การนับการอ้างอิงใช้เป็นวิธีการรวบรวมขยะบรรทัดแรกใน CPython ทำงานได้ดีกับปริมาณงาน Spark ทั่วไป (การประมวลผลคล้ายกระแสไม่มีรอบอ้างอิง) และลดความเสี่ยงของ GC หยุดชั่วคราว
MLlib
(การดำเนินการ Python และ JVM แบบผสม)
ข้อควรพิจารณาขั้นพื้นฐานคล้ายกับปัญหาเพิ่มเติมเล็กน้อยก่อนหน้านี้ ในขณะที่โครงสร้างพื้นฐานที่ใช้กับ MLlib เป็นวัตถุ Python RDD ธรรมดาขั้นตอนวิธีทั้งหมดจะถูกดำเนินการโดยตรงโดยใช้ Scala
มันหมายถึงค่าใช้จ่ายเพิ่มเติมในการแปลงออบเจ็กต์ Python ให้เป็นออบเจกต์ Scala และอีกวิธีหนึ่งการเพิ่มการใช้หน่วยความจำและข้อ จำกัด เพิ่มเติมบางอย่างที่เราจะกล่าวถึงในภายหลัง
ณ ตอนนี้ (Spark 2.x) API ของ RDD-based อยู่ในโหมดการบำรุงรักษาและมีกำหนดจะถูกลบออกใน Spark 3.0
DataFrame API และ Spark ML
(การดำเนินการ JVM ด้วยรหัส Python จำกัด เฉพาะไดรเวอร์)
สิ่งเหล่านี้อาจเป็นตัวเลือกที่ดีที่สุดสำหรับงานการประมวลผลข้อมูลมาตรฐาน เนื่องจากรหัส Python ส่วนใหญ่ถูก จำกัด ไว้ที่การดำเนินการทางตรรกะระดับสูงในไดรเวอร์จึงไม่ควรมีประสิทธิภาพที่แตกต่างระหว่าง Python และ Scala
ข้อยกเว้นเดียวคือการใช้ Python UDFs แบบชาญฉลาดซึ่งมีประสิทธิภาพน้อยกว่า Scala ที่เทียบเท่ากัน ในขณะที่มีโอกาสในการปรับปรุง (มีการพัฒนาอย่างมากใน Spark 2.0.0) ข้อ จำกัด ที่ใหญ่ที่สุดคือการไปกลับเต็มรูปแบบระหว่างการเป็นตัวแทนภายใน (JVM) และล่าม Python หากเป็นไปได้คุณควรสนับสนุนองค์ประกอบของนิพจน์ในตัว ( เช่นพฤติกรรม Python UDF ได้รับการปรับปรุงใน Spark 2.0.0 แต่ยังคงดีกว่าเมื่อเทียบกับการประมวลผลแบบเนทีฟ
สิ่งนี้อาจปรับปรุงในอนาคตได้ดีขึ้นอย่างมีนัยสำคัญด้วยการเปิดตัวvectorized UDFs (SPARK-21190 และส่วนขยายเพิ่มเติม)ซึ่งใช้ 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
DataFrame
สตรีมมิ่ง
จากสิ่งที่ฉันเห็นมาฉันขอแนะนำให้ใช้ Scala มากกว่า Python อาจมีการเปลี่ยนแปลงในอนาคตหาก PySpark ได้รับการสนับสนุนสำหรับสตรีมที่มีโครงสร้าง แต่ตอนนี้ Scala API ดูเหมือนจะแข็งแกร่งกว่ามีความครอบคลุมและมีประสิทธิภาพมากขึ้น ประสบการณ์ของฉันค่อนข้าง จำกัด
การสตรีมแบบมีโครงสร้างใน Spark 2.x ดูเหมือนจะลดช่องว่างระหว่างภาษา แต่ตอนนี้มันยังอยู่ในช่วงแรก ๆ อย่างไรก็ตาม API ที่ใช้ RDD นั้นถูกอ้างอิงเป็น "การสตรีมแบบดั้งเดิม" ในเอกสาร Databricks (วันที่เข้าถึง 2017-03-03) ดังนั้นจึงสมเหตุสมผลที่จะคาดหวังความพยายามในการรวมกันเพิ่มเติม
ข้อควรพิจารณาเกี่ยวกับประสิทธิภาพที่ไม่ใช่
ความเท่าเทียมกันของคุณสมบัติ
คุณสมบัติ Spark ทั้งหมดนั้นไม่ได้เปิดเผยผ่าน PySpark API ตรวจสอบให้แน่ใจว่าชิ้นส่วนที่คุณต้องการมีการใช้งานแล้วและพยายามเข้าใจข้อ จำกัด ที่เป็นไปได้
เป็นสิ่งสำคัญอย่างยิ่งเมื่อคุณใช้ MLlib และบริบทผสมที่คล้ายกัน (ดูที่การเรียกฟังก์ชัน Java / Scala จากงาน ) เพื่อให้เกิดความยุติธรรมบางส่วนของ PySpark API mllib.linalg
จะให้วิธีการที่ครอบคลุมกว่า Scala
การออกแบบ API
PySpark API สะท้อนให้เห็นถึงคู่ของ Scala อย่างใกล้ชิดและไม่ได้เป็น Pythonic หมายความว่ามันง่ายที่จะแมประหว่างภาษา แต่ในเวลาเดียวกันรหัส Python อาจเข้าใจยาก
สถาปัตยกรรมที่ซับซ้อน
การไหลของข้อมูล PySpark ค่อนข้างซับซ้อนเมื่อเทียบกับการดำเนินการ JVM บริสุทธิ์ เป็นการยากที่จะให้เหตุผลเกี่ยวกับโปรแกรมหรือดีบัก PySpark นอกจากนี้ความเข้าใจพื้นฐานอย่างน้อยเกี่ยวกับ Scala และ JVM โดยทั่วไปนั้นเป็นสิ่งที่ต้องมี
Spark 2.x และสูงกว่า
การเปลี่ยนไปใช้Dataset
API อย่างต่อเนื่องพร้อมกับ RDD API ที่แช่แข็งนำทั้งโอกาสและความท้าทายสำหรับผู้ใช้ Python ในขณะที่ชิ้นส่วนระดับสูงของ API เป็นเรื่องง่ายที่จะเปิดเผยในหลามที่คุณสมบัติที่สูงขึ้นจะสวยไปไม่ได้มากที่จะนำมาใช้โดยตรง
ยิ่งไปกว่านั้นฟังก์ชั่น Python ดั้งเดิมยังคงเป็นพลเมืองชั้นสองในโลก SQL หวังว่าสิ่งนี้จะปรับปรุงในอนาคตด้วยการทำอนุกรม Apache Arrow ( ข้อมูลเป้าหมายความพยายามในปัจจุบันcollection
แต่ UDF serde เป็นเป้าหมายระยะยาว )
สำหรับโครงการที่ขึ้นอยู่กับ Python codebase ทางเลือก Python ล้วน ๆ (เช่นDaskหรือRay ) อาจเป็นทางเลือกที่น่าสนใจ
มันไม่จำเป็นต้องเป็นหนึ่งกับอื่น ๆ
Spark DataFrame (SQL, Dataset) API เป็นวิธีการที่ยอดเยี่ยมในการรวมโค้ด Scala / Java ในแอปพลิเคชัน PySpark คุณสามารถใช้DataFrames
เพื่อเปิดเผยข้อมูลไปยังรหัส JVM ดั้งเดิมและอ่านผลลัพธ์ ผมได้อธิบายตัวเลือกบางอย่างที่อื่นและคุณสามารถหาตัวอย่างการทำงานของงูใหญ่-Scala บินตั้งอยู่ในวิธีการใช้ระดับ Scala ภายใน Pyspark
สามารถเพิ่มเพิ่มเติมได้โดยการแนะนำประเภทที่กำหนดโดยผู้ใช้ (ดูที่วิธีกำหนดสคีมาสำหรับประเภทที่กำหนดเองใน Spark SQL? )
มีอะไรผิดปกติกับรหัสที่ให้ไว้ในคำถาม
(ข้อจำกัดความรับผิดชอบ: มุมมอง Pythonista มีแนวโน้มมากที่ฉันพลาดเทคนิค Scala)
ก่อนอื่นมีส่วนหนึ่งในรหัสของคุณซึ่งไม่สมเหตุสมผลเลย หากคุณมี(key, value)
คู่ที่สร้างขึ้นโดยใช้zipWithIndex
หรือenumerate
สิ่งที่เป็นจุดในการสร้างสตริงเพียงเพื่อแยกมันในภายหลัง? flatMap
ไม่ทำงานซ้ำ ๆ เพื่อให้คุณสามารถให้สิ่งอันดับและข้ามไปติดตามmap
ใด ๆ
reduceByKey
อีกส่วนหนึ่งผมพบว่ามีปัญหาคือ โดยทั่วไปการพูดreduceByKey
จะมีประโยชน์หากการใช้ฟังก์ชั่นรวมสามารถลดจำนวนข้อมูลที่จะต้องสับ เมื่อคุณเชื่อมโยงสตริงเข้าด้วยกันจึงไม่มีอะไรให้ได้รับที่นี่ groupByKey
ละเว้นสิ่งที่ระดับต่ำเช่นจำนวนการอ้างอิงจำนวนของข้อมูลที่คุณมีในการถ่ายโอนเป็นสิ่งเดียวกับ
ปกติฉันจะไม่อยู่บนนั้น แต่เท่าที่ฉันสามารถบอกได้ว่ามันเป็นคอขวดในโค้ดสกาล่าของคุณ การรวมสตริงใน JVM เป็นการดำเนินการที่ค่อนข้างแพง (ดูตัวอย่าง: การต่อสตริงในสกาล่ามีราคาแพงเหมือนในจาวาหรือไม่ ) หมายความว่าบางสิ่งเช่นนี้_.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]
โหมด (Intel (R) Xeon (R) CPU E3-1245 V2 @ 3.40GHz) ที่มีหน่วยความจำ 4GB ต่อผู้ดำเนินการที่ใช้ (n = 3):
- สกาล่า - หมายถึง: 250.00s, มาตรฐาน: 12.49
- Python - หมายถึง: 246.66s, stdev: 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:])