Inhaltsverzeichnis22 Abschnitte
- TL;DR
- Mental Model: Warum Spark-Jobs langsam sind
- 1. Verstehe deinen Job mit der Spark UI
- 2. Memory-Konfiguration
- 3. Shuffle-Optimierung
- Shuffle-Partitions tunen
- Broadcast Joins — Shuffle für kleine Tabellen eliminieren
- Shuffle-Datenmenge reduzieren
- 4. Adaptive Query Execution (AQE)
- 5. Mit Data Skew umgehen
- 6. Caching-Strategie
- 7. Parallelism und Cluster-Sizing
- 8. Häufige Anti-Patterns
- 9. Photon Engine
- 10. Tuning-Checklist
- Fazit
- FAQ
- Was ist der schnellste Spark-Tuning-Hebel auf Databricks?
- Wann lohnt sich Photon, wann nicht?
- Wie wähle ich die richtige Cluster-Größe?
- Wie monitore ich Spark-Job-Performance über Zeit?
- Weiterlesen
Spark Performance Tuning: Der Praxis-Guide für Data Engineers
Spark ist mächtig, aber unangetastete Default-Konfigurationen killen deine Query-Performance bei Skalierung. Egal ob du einen 10-Node- oder einen 200-Node-Cluster fährst — dieselben Performance-Prinzipien gelten. Dieser Guide zeigt die praktischen Tuning-Techniken, die den Unterschied zwischen einem 2-Stunden-Job und einem 10-Minuten-Job machen.
TL;DR
- Drei Quellen für Slowness: Shuffle, Memory-Pressure, Skew
- AQE einschalten und Shuffle-Partitions tunen ist der höchste ROI
- Broadcast-Joins für kleine Tabellen — ein One-Liner mit großem Effekt
- Photon auf Databricks gibt 2-8× Performance ohne Code-Änderung
- Cluster-Sizing: Memory-optimized für ETL, GPU für ML, Serverless SQL für Analytics
Mental Model: Warum Spark-Jobs langsam sind
Bevor du tunst, musst du die drei Hauptquellen für Spark-Slowness verstehen:
- Shuffle — Daten zwischen Stages über das Netzwerk bewegen (durch Joins, GroupBys, Repartitions)
- Memory-Pressure — Spill auf Disk, wenn Executors die Daten nicht im RAM halten können
- Skew — Wenige Partitionen sind 100× größer als andere, der Job wartet auf die Nachzügler
Jede Tuning-Entscheidung adressiert eines oder mehrere davon. Gehen wir der Reihe nach durch.
1. Verstehe deinen Job mit der Spark UI
Bevor du irgendwas tunst, profile zuerst. Öffne die Spark UI (über die Databricks-Cluster-Seite erreichbar) und schau auf:
- Stage Duration: Welche Stages dauern am längsten?
- Shuffle Read/Write: Viel Shuffle = Optimierungspotenzial
- Spill: Memory Spill auf Disk ist ein Hauptindikator für Slowdown
- Skew: Task-Duration-Varianz innerhalb einer Stage (manche Tasks 100× langsamer = Skew)
# Enable event logging for detailed profiling
spark.conf.set("spark.eventLog.enabled", "true")
spark.conf.set("spark.eventLog.dir", "dbfs:/spark-events")
# Check partition size distribution
df = spark.table("prod.gold.events")
df.groupBy(spark_partition_id()).count().orderBy("count", ascending=False).show(20)
2. Memory-Konfiguration
Executor-Memory ist in Regionen aufgeteilt. Falsch konfiguriert: Spill.
# Executor memory allocation
spark.conf.set("spark.executor.memory", "16g")
spark.conf.set("spark.executor.memoryOverhead", "4g") # Off-heap for native memory
# Memory fraction split: execution vs storage
spark.conf.set("spark.memory.fraction", "0.8") # 80% of heap for Spark
spark.conf.set("spark.memory.storageFraction", "0.3") # 30% of that for caching
# Driver memory (for collect(), toPandas(), large broadcasts)
spark.conf.set("spark.driver.memory", "8g")
spark.conf.set("spark.driver.memoryOverhead", "2g")
Memory Spill erkennen:
# Check for spill in the SQL metrics (Spark UI > SQL tab)
# Or programmatically via the metrics system
# High "Spill (Memory)" and "Spill (Disk)" values = increase executor memory
3. Shuffle-Optimierung
Shuffle ist die teuerste Operation in Spark. Shuffle-Datenmenge zu reduzieren ist immer die Optimierung mit dem höchsten ROI.
Shuffle-Partitions tunen
Der Default ist 200 Shuffle-Partitions — fast immer falsch für deine Workload:
# Rule of thumb: target 128MB–256MB per partition after shuffle
# Check your shuffle output size in Spark UI → Stages → Shuffle Write
# Then: num_partitions = total_shuffle_bytes / 200_000_000
# For small datasets (< 10GB after shuffle):
spark.conf.set("spark.sql.shuffle.partitions", "50")
# For large datasets (> 100GB after shuffle):
spark.conf.set("spark.sql.shuffle.partitions", "1000")
# Or let AQE handle it automatically (recommended on Databricks):
spark.conf.set("spark.sql.adaptive.enabled", "true")
spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "true")
Broadcast Joins — Shuffle für kleine Tabellen eliminieren
Die wirkungsvollste Einzeloptimierung für join-lastige Jobs:
from pyspark.sql.functions import broadcast
# Manual broadcast hint
result = large_orders_df.join(
broadcast(small_country_codes_df),
on="country_code",
how="left"
)
# Or configure the threshold (default: 10MB — often too conservative)
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", str(100 * 1024 * 1024)) # 100MB
# Disable broadcast entirely when it causes OOM on driver
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", "-1")
Shuffle-Datenmenge reduzieren
# Filter and project BEFORE joining — push down predicates
result = (
spark.table("prod.gold.events")
.filter("event_date >= '2024-01-01'") # Filter early!
.select("user_id", "event_type", "amount") # Only needed columns
.join(users_df, "user_id")
)
# Avoid Python UDFs where possible — use Spark SQL built-ins
# BAD: Python UDF (serialization overhead)
from pyspark.sql.functions import udf
@udf("string")
def slow_udf(x): return x.upper()
# GOOD: Native Spark function
from pyspark.sql.functions import upper
df.withColumn("name_upper", upper("name"))
4. Adaptive Query Execution (AQE)
AQE ist Sparks 3.x-Runtime-Optimizer. Schalte es ein — es übernimmt viele Tuning-Entscheidungen automatisch:
# Enable AQE (default ON in Databricks Runtime 8+)
spark.conf.set("spark.sql.adaptive.enabled", "true")
# Automatic partition coalescing (merges small partitions after shuffle)
spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "true")
spark.conf.set("spark.sql.adaptive.coalescePartitions.minPartitionNum", "1")
spark.conf.set("spark.sql.adaptive.advisoryPartitionSizeInBytes", "256mb")
# Automatic skew join handling
spark.conf.set("spark.sql.adaptive.skewJoin.enabled", "true")
spark.conf.set("spark.sql.adaptive.skewJoin.skewedPartitionFactor", "5")
spark.conf.set("spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes", "256mb")
# Convert sort-merge joins to broadcast joins at runtime when possible
spark.conf.set("spark.sql.adaptive.localShuffleReader.enabled", "true")
5. Mit Data Skew umgehen
Skew tritt auf, wenn ein Key massiv mehr Daten hat als andere (z. B. eine customer_id, die 40 % deiner Orders-Tabelle ausmacht). Eine Partition verarbeitet Millionen Zeilen, während die anderen 999 in Sekunden fertig sind.
# Detect skew
from pyspark.sql.functions import spark_partition_id, count
df = spark.table("prod.gold.orders")
(df.groupBy(spark_partition_id().alias("partition_id"))
.agg(count("*").alias("row_count"))
.orderBy("row_count", ascending=False)
.show(20))
# Fix 1: Salting technique for skewed joins
from pyspark.sql.functions import col, concat, lit, rand, floor, explode, array
SALT_FACTOR = 50
# Skewed side: add random salt
skewed_df = (
spark.table("prod.gold.large_orders")
.withColumn("salt", (rand() * SALT_FACTOR).cast("int"))
.withColumn("customer_id_salted", concat(col("customer_id"), lit("_"), col("salt")))
)
# Small side: replicate across all salt values
small_df = spark.table("prod.gold.customers")
small_df_salted = (
small_df
.withColumn("salt_array", array([lit(i) for i in range(SALT_FACTOR)]))
.withColumn("salt", explode(col("salt_array")))
.withColumn("customer_id_salted", concat(col("customer_id"), lit("_"), col("salt")))
.drop("salt_array", "salt")
)
result = skewed_df.join(small_df_salted, "customer_id_salted")
AQE Automatic Skew Handling (einfacher, wenn es funktioniert):
# AQE splits skewed partitions automatically
spark.conf.set("spark.sql.adaptive.skewJoin.enabled", "true")
# Works for sort-merge joins where one side is skewed
6. Caching-Strategie
Cache DataFrames, die im selben Job mehrfach genutzt werden. Aber falsch gecacht verschwendest du wertvollen Memory.
# Cache only what's reused
customer_lookup = spark.table("prod.gold.customers").cache()
customer_lookup.count() # Trigger the cache materialization
# Multiple downstream uses — cache pays off
orders_with_customers = orders_df.join(customer_lookup, "customer_id")
returns_with_customers = returns_df.join(customer_lookup, "customer_id")
# Explicit unpersist when done
customer_lookup.unpersist()
# Use DISK_ONLY for very large DataFrames you can't afford in memory
from pyspark import StorageLevel
huge_df.persist(StorageLevel.DISK_ONLY)
# Check what's cached
spark.catalog.listTables("prod.gold")
spark.sparkContext.statusTracker().getExecutorInfos()
Stattdessen Delta für Cross-Job-Reuse:
# Write a checkpoint table instead of in-memory cache for multi-job reuse
expensive_computation.write.format("delta").mode("overwrite").saveAsTable("prod.temp.checkpoint_table")
7. Parallelism und Cluster-Sizing
# Default parallelism: controls RDD partition count for raw data
spark.conf.set("spark.default.parallelism", str(num_executors * num_cores * 2))
# For reading: control parallelism via maxPartitionBytes
spark.conf.set("spark.sql.files.maxPartitionBytes", str(256 * 1024 * 1024)) # 256MB/partition
spark.conf.set("spark.sql.files.openCostInBytes", str(4 * 1024 * 1024)) # 4MB open cost
Cluster-Sizing-Faustregeln:
| Workload-Typ | Node-Type | Worker-Count |
|---|---|---|
| ETL / Batch-Joins | Memory-optimized | 8–32 |
| ML-Training | GPU oder Compute-optimized | 4–16 |
| Streaming (low-latency) | General Purpose | 2–8 |
| SQL-Analytics | Serverless SQL Warehouse | Auto-Scale |
8. Häufige Anti-Patterns
# BAD: Collect large DataFrames to driver
all_rows = df.collect() # OOM risk for large DFs
# GOOD: Process in Spark, collect only aggregations
summary = df.groupBy("category").agg({"amount": "sum"}).collect()
# BAD: Nested loops with Spark actions
for id in user_ids:
user_df = df.filter(f"user_id = {id}") # Creates a new job per iteration
user_df.show()
# GOOD: Filter once, process all
df.filter(col("user_id").isin(user_ids)).show()
# BAD: Using pandas on large data
pandas_df = spark_df.toPandas() # Pulls everything to driver
result = pandas_df.groupby("category").sum()
# GOOD: Use Spark SQL or Pandas API on Spark
import pyspark.pandas as ps
ps_df = spark_df.to_pandas_on_spark()
result = ps_df.groupby("category").sum()
9. Photon Engine
Databricks Photon ist eine native vektorisierte Query-Engine, die die JVM-basierte Spark-Engine für SQL- und DataFrame-Operationen ersetzt. Aktiviere es für 2-8× Performance-Boost auf geeigneten Workloads — ohne Konfigurationsaufwand:
# Enable Photon when creating a cluster (UI or CLI)
databricks clusters create --json '{
"cluster_name": "photon-etl",
"spark_version": "14.3.x-photon-scala2.12",
"node_type_id": "Standard_D8ds_v5",
"num_workers": 8
}'
Photon beschleunigt: Scans, Filter, Joins, Aggregations, Window Functions, Sorting. Photon beschleunigt nicht: Python-UDFs, RDD-Operationen, Streaming.
10. Tuning-Checklist
| Beobachtetes Problem | Fix |
|---|---|
| Lange Shuffle-Stages | Shuffle-Partitions erhöhen, AQE einschalten |
| Memory Spill | Executor-Memory erhöhen, Partition-Size reduzieren |
| Skewed Tasks | AQE Skew Join aktivieren oder Salting nutzen |
| Langsame Joins | Kleine Tabellen broadcasten |
| Langsame Scans | File-Compaction, Z-Order, Bloom-Filter ergänzen |
| Niedrige CPU-Auslastung | Partition-Count reduzieren, Parallelism erhöhen |
| OOM auf Driver | Kein collect() auf großen DFs, Driver-Memory erhöhen |
Fazit
Spark Performance Tuning ist zu gleichen Teilen Wissenschaft und Intuition. Profile zuerst mit der Spark UI, identifiziere deinen Bottleneck (Shuffle vs. Memory vs. Skew), wende gezielte Fixes an und miss erneut. Ändere nicht mehrere Dinge gleichzeitig — sonst weißt du nicht, was geholfen hat.
Die Änderungen mit dem höchsten ROI in dieser Reihenfolge: AQE einschalten, Shuffle-Partitions tunen, kleine Tabellen broadcasten, Python-UDFs eliminieren, Cluster richtig dimensionieren.
FAQ
Was ist der schnellste Spark-Tuning-Hebel auf Databricks?
AQE einschalten (spark.sql.adaptive.enabled = true) plus Auto-Coalesce. Damit übernimmt Spark Runtime die Shuffle-Partition-Größen und Skew-Behandlung automatisch. In neueren Databricks Runtimes ist das schon Default — prüfe es trotzdem.
Wann lohnt sich Photon, wann nicht?
Photon lohnt sich für SQL- und DataFrame-Workloads mit Scans, Aggregations, Joins, Window Functions. Es lohnt sich NICHT für Python-UDF-lastige Pipelines, RDD-basierten Code oder Streaming-Jobs. Bei reinen SQL-ETL siehst du oft 3-5× Speedup bei moderatem Kostenaufschlag.
Wie wähle ich die richtige Cluster-Größe?
Starte mit 4-8 Memory-optimized Workern und beobachte die Spark UI: Sind Executor-CPUs unter 70 % ausgelastet, verkleinere. Spillen Executors auf Disk, vergrößere oder nimm größere Nodes. Für variable Workloads sind Autoscaling-Cluster mit min=2, max=16 ein guter Start.
Wie monitore ich Spark-Job-Performance über Zeit?
Schreibe Metriken pro Job-Run in eine Delta-Table (Dauer, Shuffle-Bytes, Spill, Records). Visualisiere Trends in Databricks SQL Dashboards. Achte besonders auf wachsende Shuffle-Volumen oder steigende Spill-Bytes — beide sind Frühindikatoren für Performance-Regression.
Weiterlesen
Stand: 15. Mai 2026.
Geschrieben von
Harbinger Team
Cloud-, Data- und AI-Engineer in DACH. Schreibt seit 2018 über infrastrukturkritische Tech-Entscheidungen — keine Marketing- Folien, sondern echte Trade-offs aus Production-Workloads.
Hat dir das geholfen?
Jede Woche ein neuer Artikel über DACH-Cloud, Data und AI — direkt in dein Postfach. Kein Spam, kein Marketing-Sprech.
Kein Spam. 1-Klick-Abmeldung. Datenschutz bei Loops.so.