| von Rick Moritz

Performance critical Spark

Performance: Ist Apache Spark schnell genug?

Die Abfrageperformance kann oft geschäftskritische Ausmaße annehmen – deshalb rückt sie häufig nach der PoC-Phase eines Projekts in den Mittelpunkt. Gerade bei Big-Data-Lösungen ist der Unterschied zwischen einem funktionalen Prototypen und einer skalierbaren, produktionsreifen Lösung gewaltig. Auch die hohe Performance die Spark verspricht, reicht hier nicht immer aus. Neben Fallstricken wie den vermeintlich komfortablen Python-UDFs, die zuminest in aktuellen Spark-Versionen (<=2.2) massiven Serialisierungsoverhead haben, ist auch die JVM alleine schon häufig eine Performancebremse.


Vor diesem Hintergrund ist diese Stack-Overflow-Frage sehr interessant, besonders, da die Antwort von Vidya (siehe https://stackoverflow.com/questions/43411234/spark-sql-whether-to-use-row-transformation-or-udf/43413294#43413294) mich zu einer sehr interessanten Folie gebracht hat, die sich hier findet: https://www.slideshare.net/cfregly/advanced-apache-spark-meetup-project-tungsten-nov-12-2015/56.

In der Vergangenheit bot Java, wenn Performance in den Vordergrund gerückt ist, zwei Möglichkeiten. Zum einen wurde der JIT-Compiler eingeführt und mehr nativer Code mit dem JRE verteilt, aber die ultimative Leistung der CPU konnte man häufig nur über das Java Native Interface (JNI) und externe Aufrufe auf optimierten C-Code ausschöpfen. Das JNI verknüpft dabei Business-Logik in Java mit High-Performance Code in plattformoptimierten, nativen Bibliotheken.

Spark versteckt vor dem Programmierer viel Logik, benutzt aber selbst bereits Code-Generierung im Hintergrund. Die Logik hinter Project Tungsten generiert zur Laufzeit Code, und führt diesen auf den Exekutoren aus. Aktuell wird dabei optimierter Java-Code generiert um virtuelle Funktionausaufrufe zu vermeiden. Ein Engpass entsteht jedoch, wenn man UDFs verwenden möchte: Dieser Code, ob in Scala oder Python, kann nicht automatisch optimiert werden.

Performance Level
Bildquelle: Shutterstock / Bildnr. 137811743

Hand-optimierter Code mit Spark CodeGen

Zur weiteren Optimierung führen nun zwei Wege: Man kann zum einen den Code mit Java CodeGen als sql.function umsetzen (siehe dieses Beispiel für die Levenshtein-Distanz unter: https://github.com/apache/spark/pull/7214/files). Dabei sollte man schon auf Vektorisierung und Spaltenoptimierung achten. Aber was, wenn selbst das nicht ausreicht, um die Hardware effizient zu nutzen? Man kann den Code, der Performance kritisch ist, weiter nativ in C implementieren und einbinden. Hierzu nutzt man die Code-Generierung von Spark um C-Bibliotheken via JNI aufrufen. Wenn man die Anwendung und ihre Umgebung hinreichend genau im Griff hat, ist es sogar möglich Hand-optimierte Assemblerlogik einzubinden. Klar ist, dass hier etwas Integrations-Overhead entsteht. Schließlich muss man die entstehenden zusätzlichen Abhängigkeiten und die Spark-Integration verwalten.

Im Regelfall wird man die Java/Scala-UDF und Bytecode verwenden, mit dem entsprechenden Performance Malus. Aber gut zu wissen, dass hier eine Erweiterungsmöglichkeit besteht. Allerdings hält Spark auch in dieser Thematik nicht still – selbst die als unerträglich langsam bekannten Python-UDFs generieren in naher Zukunft vektorisierten Code. Trotzdem bleiben vorerst die Grenzen der JVM erhalten: SIMD-Operationen werden nur eingeschränkt unterstützt und sind längst nicht auf dem Niveau einer MKL.

Apache and Apache Spark are either registered trademarks or trademarks of the Apache Software Foundation in the United States and/or other countries. No endorsement by The Apache Software Foundation is implied by the use of these marks.

Teile diesen Artikel mit anderen

Über den Autor

Rick hat sich mit Big Data Technologien und Konzepten beschäftigt und unsere Kunden bis Anfang 2018 als Solution Architect Big Data u.a. in der Entwicklung und mit Big-Data-Schulungen unterstützt. Seine Lieblingstechnologien sind aktuell Spark und Scala, mit einer Prise DevOps.

Zur Übersicht Blogbeiträge