Spark: Python kullanım durumumda neden Scala'dan daha iyi?


16

Python ve Scala kullanırken Spark performansını karşılaştırmak için her iki dilde de aynı işi yarattım ve çalışma zamanını karşılaştırdım. Her iki işin de kabaca aynı süreyi almasını bekliyordum, ancak Python işi sadece aldı 27min, Scala işi aldı 37min(neredeyse% 40 daha uzun!). Aynı işi Java'da da uyguladım ve bu da sürdü 37minutes. Python'un bu kadar hızlı olması nasıl mümkün olabilir?

Asgari doğrulanabilir örnek:

Python işi:

# 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 işi:

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

Sadece koda bakarak aynı görünüyorlar. Ben bir DAGs baktı ve herhangi bir fikir vermediler (ya da en azından onlara dayalı bir açıklama gelmek için know-how eksikliği).

Gerçekten herhangi bir işaretçi takdir ediyorum.


Yorumlar uzun tartışmalar için değildir; bu görüşme sohbete taşındı .
Samuel Liew

1
Bir şey sormadan önce, python sürümünün daha hızlı olduğu belirli bir yer olup olmadığını görmek için karşılık gelen blokları ve ifadeleri zamanlayarak analize başlardım. O zaman 'neden bu python deyimi daha hızlı' sorusunu netleştirmiş olabilirsiniz.
Terry Jan Reedy

Yanıtlar:


11

Scala veya Java'nın bu özel görev için daha hızlı olması gerektiği temel varsayımınız yanlıştır. Minimum yerel uygulamalarla kolayca doğrulayabilirsiniz. Scala bir:

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 bir

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

Sonuçlar üzerinde (300 tekrarlar her Python 3.7.6, Scala 2.11.12), Posts.xmlgelen veri dökümü hermeneutics.stackexchange.com eşleme ve uyuşma kalıplarının karışımı ile:

yukarıdaki programlar için milis cinsinden süreklilik kutuları

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

Gördüğünüz gibi Python sadece sistematik olarak daha hızlı değil, aynı zamanda daha tutarlıdır (düşük yayılım).

Götürme mesajı - asılsız FUD'ye inanmayın - diller belirli görevlerde veya belirli ortamlarda daha hızlı veya daha yavaş olabilir (örneğin burada Scala, JVM başlatma ve / veya GC ve / veya JIT tarafından vurulabilir), ancak iddia ederseniz "XYZ X4 daha hızlıdır" veya "XYZ, ZYX'e kıyasla daha yavaştır (..) Yaklaşık 10 kat daha yavaş", genellikle birisinin bir şeyi test etmek için gerçekten kötü kod yazdığı anlamına gelir.

Düzenle :

Yorumlarda dile getirilen bazı endişeleri gidermek için:

  • OP kodunda veriler çoğunlukla bir yönde (JVM -> Python) geçirilir ve gerçek bir serileştirme gerekli değildir (bu özel yol sadece olduğu gibi bytestring'i geçer ve diğer tarafta UTF-8'de kod çözer). "Serileştirme" söz konusu olduğunda bu kadar ucuz.
  • Geriye aktarılan bölüme göre sadece tek bir tamsayıdır, bu nedenle etki ihmal edilebilir.
  • İletişim yerel soketler üzerinden yapılır (ilk bağlantının ötesinde çalışandaki tüm iletişim ve kimlik doğrulaması, döndürülen dosya tanıtıcısılocal_connect_and_auth ve soketle ilişkili dosyadan başka bir şey kullanılarak gerçekleştirilir ). Yine, süreçler arasındaki iletişim söz konusu olduğunda olduğu kadar ucuz.
  • Yukarıda gösterilen ham performans farkı (programınızda gördüğünüzden çok daha yüksek) göz önünde bulundurulduğunda, yukarıda listelenen genel masraflar için çok fazla marj vardır.
  • Bu durum, basit veya karmaşık nesnelerin Python yorumlayıcısına ve turşu uyumlu dökümler olarak her iki tarafın erişebileceği bir biçimde geçirilmesi gereken durumlardan tamamen farklıdır (en dikkate değer örnekler arasında eski stil UDF, bazı bölümleri eski tarzı MLLib).

Düzenleme 2 :

Yana Jasper-m burada başlangıç maliyeti hakkında endişe, kolayca Python hala giriş boyutu önemli ölçüde artmıştır bile Scala üzerinde önemli bir avantaja sahip olduğunu kanıtlayabilir.

İşte 2003360 satır / 5.6G (aynı giriş, sadece birden çok kez çoğaltılmış, 30 tekrar) sonuçları, bu şekilde tek bir Spark görevinde beklediğiniz her şeyi aşıyor.

resim açıklamasını buraya girin

  • Python 22809.57 (21466.26, 24152.87)
  • Scala 27315,28 (24367,24, 30263,31)

Çakışmayan güven aralıklarına dikkat edin.

Düzenleme 3 :

Jasper-M'den başka bir yorumu ele almak için :

Tüm işlemlerin büyük kısmı hala Spark durumundaki bir JVM içinde gerçekleşiyor.

Bu özel durumda bu yanlıştır:

  • Söz konusu iş, PySpark RDD'lerini kullanarak tek bir küresel azaltma içeren harita işi.
  • PySpark RDD (diyelim aksine DataFrame), istisna girdi, çıktı ve düğümler arası iletişim ile Python'da yerel olarak brüt işlevsellik uygular.
  • Tek aşamalı bir iş olduğundan ve nihai çıktı göz ardı edilecek kadar küçük olduğundan, JVM'nin ana sorumluluğu (biri nitpick ise, bu çoğunlukla Scala değil Java'da uygulanır) Hadoop giriş formatını çağırmak ve verileri soketten geçirmek dosyasını Python'a aktarın.
  • Okunan kısım JVM ve Python API için aynıdır, bu nedenle sabit ek yük olarak kabul edilebilir. Ayrıca , böyle basit bir iş için bile , işlemin büyük kısmı olarak nitelendirilmez .

3
sorunun mükemmel yaklaşımı. Bunu paylaştığınız için teşekkür ederiz
Alexandros Biratsis

1
@Egordoe Alexandros, "Python çağrılmadı" değil, "burada çağrılan bir UDF yok" dedi - bu fark yaratıyor. Serileştirme yükü, veriler sistemler arasında değiş tokuş edildiğinde (yani, verileri bir UDF'ye ve geri aktarmak istediğinizde) önemlidir.
user10938362

1
@egordoe İki şeyi açıkça karıştırıyorsunuz - önemsiz nesnelerin ileri geri aktarıldığı sorun olan serileştirme yükü. Ve iletişim yükü. Burada serileştirme yükü çok azdır veya hiç yoktur, çünkü sadece test kodlarını geçip deşifre edersiniz ve bu bölüm başına tek tamsayı alırken çoğunlukla yönde olur. İletişim endişe vericidir, ancak yerel soketlerden veri aktarmak, süreçler arası iletişim söz konusu olduğunda gerçekten etkili olduğu için etkilidir. Bu net değilse kaynağı okumayı öneririm - zor değil ve aydınlatıcı olacaktır.
user10938362

1
Ayrıca serileştirme yöntemleri de eşit değildir. Spark durumunun iyi serileştirme yöntemlerinin maliyeti artık endişe duyulmayacağı seviyeye indirebileceği gibi (bkz. Ok ile Pandas UDF) ve bu durumda, diğer faktörler baskın olabilir (örneğin, Scala pencere fonksiyonları ve Pandalarla eşdeğerleri arasındaki performans karşılaştırmaları. UDF'ler - Python, bu sorudan çok daha yüksek bir farkla kazanıyor).
user10938362

1
Demek istediğin @ Jasper-M? Bireysel Spark görevleri genellikle buna benzer iş yüküne sahip olacak kadar küçüktür. Beni yanlış anlamayın, ancak bu ya da tüm soruyu geçersiz kılan gerçek bir karşı örneğiniz varsa, lütfen gönderin. İkincil eylemlerin bu değere bir ölçüde katkıda bulunduğunu zaten belirtmiştim, ancak maliyete hakim değiller. Hepimiz burada bir tür mühendisiz - hadi inançlardan değil, sayılardan ve koddan bahsedelim, olur mu?
user10938362

4

Scala işi daha uzun sürüyor çünkü yanlış bir yapılandırmaya sahip ve bu nedenle Python ve Scala işlerine eşit olmayan kaynaklar sağlanmıştı.

Kodda iki hata var:

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. Satır yürütüldüğünde, Spark işinin kaynak yapılandırması zaten kurulmuş ve düzeltilmiştir. Bu noktadan sonra, hiçbir şeyi ayarlamanın bir yolu yok. Ne yönetici sayısı ne de yönetici başına çekirdek sayısı.
  2. HAT 4-5. sc.hadoopConfigurationherhangi bir Spark yapılandırmasını ayarlamak için yanlış bir yerdir. Geçtiğiniz configörnekte ayarlanmalıdır new SparkContext(config).

[ADDED] Yukarıdakileri göz önünde bulundurarak, Scala işinin kodunu

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

ve tekrar test edin. Eminim Scala versiyonu şimdi X kat daha hızlı olacak.


Her iki işin de 32 görevi paralel olarak çalıştırdığını doğruladım, bu yüzden bunun suçlu olduğunu düşünmüyorum?
maestromusica

düzenleme için teşekkürler, şu anda test etmeye çalışacağız
maestromusica

merhaba @maestromusica kaynak konfigürasyonunda bir şey olmalı çünkü özünde Python bu özel kullanım durumunda Scala'dan daha iyi performans göstermeyebilir. Başka bir neden, ilişkisiz bazı rasgele faktörler, yani kümenin belirli bir andaki yükü ve benzerleri olabilir. Btw, hangi modu kullanıyorsun? bağımsız, yerel, iplik?
egordoe

Evet, bu cevabın yanlış olduğunu doğruladım. Çalışma zamanı aynı. Her iki durumda da yapılandırmayı yazdırdım ve aynı.
maestromusica

1
Ben doğru olabileceğini düşünüyorum. Bu soruyu, koddaki hata veya belki bir şeyleri yanlış anladığım gibi diğer tüm olasılıkları araştırmak için sordum. Girdiniz için teşekkürler.
maestromusica
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.