Spark UI, memory model and tuning

PySpark · Expert · 20 min

Expert Spark work is mostly diagnosis: finding the slow stage, the skewed task or the spilling shuffle, and fixing the right setting.

The Spark UI (port 4040 for a running app, or the History Server afterwards) is the main diagnostic tool:

  • Jobs and Stages: find the stage that takes the time. Stage boundaries are shuffles.
  • Task summary: compare max with median task duration and input size. A max many times the median means skew.
  • Spill (memory/disk): tasks ran out of execution memory and wrote to disk; partitions are too large.
  • Shuffle read/write sizes: how much data crosses the network.
  • SQL tab: the executed plan with row counts and timings per operator, including AQE changes.
  • Executors: GC time, memory, failed tasks.

Executor memory model: spark.executor.memory is the JVM heap. After about 300 MB is reserved, spark.memory.fraction (0.6) forms a unified region shared by execution (shuffles, joins, sorts, aggregations) and storage (cache); execution can evict cached blocks. The rest holds user data structures. spark.executor.memoryOverhead covers off-heap and native memory, and Python workers need spark.executor.pyspark.memory or enough overhead, or containers get killed.

Tuning priorities: fix the data first (pruning, filter early, avoid skew, right file sizes), then partitioning (advisoryPartitionSizeInBytes, maxPartitionBytes), then join strategy, and only then resources: typically 4 to 5 cores per executor, with memory per core sized to partition size.

Example

from pyspark.sql import SparkSession, functions as F
spark = SparkSession.builder.master("local[2]").getOrCreate()

skewed = spark.range(400_000).withColumn(
    "k", F.when(F.col("id") < 300_000, 0).otherwise(F.col("id") % 100))

per_part = (skewed.repartition(8, "k")
            .withColumn("pid", F.spark_partition_id())
            .groupBy("pid").count().orderBy(F.desc("count")).collect())
sizes = [r["count"] for r in per_part]
print(sizes[0], sorted(sizes)[len(sizes) // 2])      # one huge partition vs a small median
print(sizes[0] > 10 * sorted(sizes)[len(sizes) // 2])   # True -> skew

salted = (skewed.withColumn("salt", (F.rand(seed=7) * 8).cast("int"))
          .repartition(8, "k", "salt")
          .groupBy(F.spark_partition_id().alias("pid")).count().collect())
print(max(r["count"] for r in salted) < sizes[0])     # True: work spread out

print(spark.conf.get("spark.memory.fraction", "0.6"))   # 0.6

Remember

  • Find the slow stage, then compare max vs median tasks to spot skew
  • Spill means partitions too large for execution memory
  • Unified memory: execution and storage share spark.memory.fraction of the heap

Watch out for

  • Raising executor memory to hide skew or oversized partitions instead of fixing the data layout.

Check yourself

  1. In a stage's task summary, median duration is 2 s and max is 9 minutes with 50x the input. Most likely cause?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: spark.apache.org/docs/latest/tuning.html