Blog
Spark-Speicherverwaltung Teil 2 - Gehen Sie an die Grenzen

In Spark Memory Management Part 1 - Push it to the Limits habe ich erwähnt, dass der Speicher in Big Data-Anwendungen eine entscheidende Rolle spielt.
Dieser Artikel analysiert einige populäre Speicherprobleme und beschreibt, wie Apache Spark mit ihnen umgeht.
Streitfall #1: Ausführung und Speicherung
Der folgende Abschnitt befasst sich mit dem Problem der Wahl der richtigen Größe von Ausführungs- und Speicherregionen innerhalb eines Executor-Prozesses.
Statische Zuweisung
Von Spark 1.0, Mai 2014
Der erste Ansatz zur Lösung dieses Problems bestand in der Verwendung fester Ausführungs- und Speichergrößen. Das Problem bei diesem Ansatz ist, dass der Speicher in einer bestimmten Region knapp wird(auch wenn in der anderen Region reichlich zur Verfügung steht) und auf die Festplatte ausgelagert wird - was natürlich schlecht für die Leistung ist.
Die Zwischenspeicherung wird in Form von Blöcken ausgedrückt. Wenn der Speicherplatz knapp wird, verschiebt Spark den LRU-Block ("least recently used") auf die Festplatte.
Um diese Methode zu verwenden, muss der Benutzer viele Parameter anpassen, was die Gesamtkomplexität der Anwendung erhöht.
Vereinheitlichte Speicherverwaltung
Ab Spark 1.6+, Jan 2016
Anstatt Ausführung und Speicherung in zwei getrennten Chunks auszudrücken, kann Spark eine einheitliche Region (M) verwenden, die sich beide teilen. Wenn der Ausführungsspeicher nicht verwendet wird, kann der Speicher den gesamten
verfügbaren Speicher übernehmen und umgekehrt. Die Ausführung kann den Speicher bei Bedarf auslagern, allerdings nur, solange die Gesamtnutzung des Speichers unter einem bestimmten Schwellenwert (R) liegt.
R ist der Speicherbereich innerhalb von M, in dem zwischengespeicherte Blöcke immun dagegen sind, von der Ausführung verdrängt zu werden - Sie können dies mit einer bestimmten Eigenschaft angeben.
Mit anderen Worten: R beschreibt einen Unterbereich innerhalb von M, in dem zwischengespeicherte Blöcke niemals verdrängt werden - was bedeutet, dass der Speicher die Ausführung aufgrund von Komplikationen bei der Implementierung nicht verdrängen kann. Diese Lösung
funktioniert in der Regel wie erwartet und wird in den aktuellen Spark-Versionen standardmäßig verwendet.
Eigenschaften
Statische Zuordnung:
spark.memory.useLegacyMode- die Option, den Heap-Speicher in Regionen mit fester Größe aufzuteilen (Standardwert false)spark.shuffle.memoryFraction- der Anteil des Heaps, der für Aggregation und Cogroup bei Shuffles verwendet wird. Funktioniert nur, wennspark.memory.useLegacyMode=true(Standardwert 0.2)spark.storage.memoryFraction- der Anteil des Heaps, der für den Speicher-Cache von Spark verwendet wird. Funktioniert nur, wennspark.memory.useLegacyMode=true(Standardwert 0.6)spark.storage.unrollFraction- der Anteil vonspark.storage.memoryFraction, der für das Abrollen von Blöcken im Speicher verwendet wird. Dieser wird dynamisch zugewiesen, indem vorhandene Blöcke gelöscht werden, wenn nicht genügend freier Speicherplatz vorhanden ist, um den neuen Block vollständig auszurollen. Funktioniert nur, wennspark.memory.useLegacyMode=true(Voreinstellung 0.2).
Vereinheitlichte Speicherverwaltung:
spark.memory.storageFraction- drückt die Größe vonRals einen Bruchteil vonMaus. Je höher dieser Wert ist, desto weniger Arbeitsspeicher steht für die Ausführung zur Verfügung und die Aufgaben können häufiger auf auf der Festplatte landen (Standardwert 0,5).
Streitfall #2: Parallel laufende Aufgaben
In diesem Fall beziehen wir uns auf die Aufgaben, die innerhalb eines einzelnen Threads laufen und um die Ressourcen des Executors konkurrieren.
Statische Zuweisung
Ab Spark 1.0+, Mai 2014
Der Benutzer gibt die maximale Menge an Ressourcen für eine feste Anzahl von Aufgaben (N) an, die gleichmäßig unter ihnen aufgeteilt werden. Das Problem ist, dass sehr oft nicht alle verfügbaren Ressourcen genutzt werden, was
nicht zu einer optimalen Leistung führt.
Dynamische Zuordnung
Ab Spark 1.0+, Mai 2014
Die Menge der jeder Aufgabe zugewiesenen Ressourcen hängt von der Anzahl der aktiv laufenden Aufgaben ab (N ändert sich dynamisch). Diese Option bietet eine gute Lösung für den Umgang mit "Nachzüglern" (
sind die zuletzt ausgeführten Aufgaben, die aus Schiefständen in den Partitionen resultieren).
Eigenschaften
Es gibt keine Einstellungsmöglichkeiten - die dynamische Zuweisung wird standardmäßig verwendet.
Streitfall #3: Operatoren, die innerhalb der gleichen Aufgabe laufen
Nach der Ausführung einer Abfrage (z.B. Aggregation) erstellt Spark einen internen Abfrageplan (bestehend aus Operatoren wie scan, aggregate, sort, usw.), der
innerhalb eines Tasks ausgeführt wird. Auch hier muss der verfügbare Taskspeicher zwischen den einzelnen Tasks aufgeteilt werden.
Wir gehen davon aus, dass jede Aufgabe über eine bestimmte Anzahl von Speicherseiten verfügt (die Größe der einzelnen Seiten spielt keine Rolle).
Eine Seite für jeden Betreiber
Jeder Operator reserviert eine Seite des Speichers - das ist einfach, aber nicht optimal. Bei einer größeren Anzahl von Operatoren (oder hochkomplexen Operatoren wie z.B. aggregate) ist dies natürlich ein Problem.
Kooperatives Verschütten
Ab Spark 1.6+, Jan 2016
Die Operatoren handeln den Bedarf an Seiten untereinander (dynamisch) während der Ausführung der Aufgabe aus.
Eigenschaften
Es gibt keine Einstellungsmöglichkeiten - standardmäßig wird kooperatives Spilling verwendet.
Projekt Wolfram
Project Tungsten ist eine Spark SQL-Komponente, die Operationen effizienter macht, indem sie direkt auf Byte-Ebene arbeitet.
Diese Funktion wurde in Spark 1.5 zum Standard und kann in früheren Versionen durch die Einstellung spark.sql.tungsten.enabled=true aktiviert werden. Sie ist für die Hardware-Architektur optimiert und funktioniert für alle verfügbaren Schnittstellen (SQL, Python, Java/Scala, R) unter Verwendung der DataFrame Abstraktion.
Selbst wenn Tungsten deaktiviert ist, versucht Spark immer noch, den Speicher-Overhead zu minimieren, indem es das spaltenförmige Speicherformat und die Kryo-Serialisierung verwendet.
Tungsten verwendet Kodierer/Dekodierer, um JVM-Objekte als hochspezialisierte Spark SQL-Typ-Objekte darzustellen, die dann serialisiert und auf hochperformante Weise (effizient und GC-freundlich) verarbeitet werden können.
Einige Verbesserungen umfassen:
- Speicherung von Daten im binären Zeilenformat - reduziert den Gesamtspeicherbedarf
- keine Notwendigkeit für Serialisierung und Deserialisierung - die Zeile ist bereits serialisiert
- Cache-bewusste Berechnung; (Layout-Datensätze werden im Speicher gehalten, was einer höheren L1-, L2- und L3-Cache-Trefferrate zuträglich ist).
Die Checkliste zum Mitnehmen
Im Folgenden finden Sie eine kurze Checkliste, die Sie bei Leistungsproblemen beachten sollten:
- Werden die Partitionen meiner zwischengespeicherten RDDs im Laufe der Zeit entfernt und neu erstellt (überprüfen Sie dies in der Spark-Benutzeroberfläche)?
- Dauert die GC-Phase zu lange (vielleicht wäre es besser, Off-Heap-Speicher zu verwenden)?
- Vielleicht gibt es zu viel ungenutzten Arbeitsspeicher (passen Sie ihn mit der Eigenschaft
spark.memory.fractionan)? - Sind die Daten in
DataFramesgespeichert (damit Tungsten Optimierungen vornehmen kann)? - Gibt es Datenverzerrungen(stellen Sie die Partitionierung innerhalb der Appein )?
Quellen
Unsere Ideen
Weitere Blogs
Contact



