Approach: identify skewed keys, reduce shuffle size, and lower per-executor heap/GC pressure via serialization, off-heap shuffle, and tuned executor resources. Use map-side aggregation, salting for heavy keys, broadcast small tables, and avoid wide joins by pre-aggregating.Cluster / Spark conf changes (concrete):- Use Kryo: spark.serializer=org.apache.spark.serializer.KryoSerializer; register classes for speed.- Shuffle / IO: spark.shuffle.manager=sort, spark.shuffle.service.enabled=true, spark.shuffle.compress=true, spark.shuffle.spill.compress=true- Off-heap and memory fractions: spark.memory.offHeap.enabled=true, spark.memory.offHeap.size=4g (if supported). Tune spark.memory.fraction=0.6, spark.memory.storageFraction=0.2.- GC tuning (YARN/containers): use G1GC for Java 11+: -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+UnlockExperimentalVMOptions -XX:+UseStringDeduplication- Executor sizing: prefer more smaller executors to reduce GC pauses. Example: for nodes with 64GB RAM, set executor memory ~14-20GB, cores 3-4 (avoid >5 cores).- Reduce shuffle block sizes: spark.reducer.maxSizeInFlight=96m, spark.shuffle.file.buffer=32k- Enable adaptive execution: spark.sql.adaptive.enabled=true, spark.sql.adaptive.skewJoin.enabled=trueCode changes / patterns:- Broadcast small dimension tables:python
from pyspark.sql import functions as F
small = spark.table("dim_small")
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", 50*1024*1024) # 50MB
df = big.join(F.broadcast(small), "key")
- Pre-aggregate before join to avoid wide joins:python
agg = big.groupBy("key").agg(F.sum("val").alias("sum_val"))
join = agg.join(dim, "key")
- Use map-side combiners (reduceByKey / aggregateByKey) in RDD APIs or groupByKey avoidance.Skew mitigation & repartition pseudocode (salting + heavy-key handling):python
# 1. sample to find heavy keys
sampled = big.sample(0.01).groupBy("join_key").count().orderBy(F.desc("count"))
heavy_keys = [r['join_key'] for r in sampled.take(100) if r['count'] > THRESHOLD]
# 2. split into heavy and light
heavy = big.filter(F.col("join_key").isin(heavy_keys))
light = big.filter(~F.col("join_key").isin(heavy_keys))
# 3. salt heavy keys: add random salt and expand dimension side similarly
NUM_SALTS = 50
heavy = heavy.withColumn("salt", (F.rand()*NUM_SALTS).cast("int"))
dim_heavy = dim.filter(F.col("key").isin(heavy_keys)).withColumn("salt", F.explode(F.array([F.lit(i) for i in range(NUM_SALTS)])))
# 4. repartition to distribute heavy load
light = light.repartition(200, "join_key") # keyed shuffle for light data
heavy = heavy.repartition(200, "join_key","salt") # salted distribution
# 5. perform joins and union results
joined_light = light.join(dim, light.join_key==dim.key, "left")
joined_heavy = heavy.join(dim_heavy, (heavy.join_key==dim_heavy.key) & (heavy.salt==dim_heavy.salt), "left")
result = joined_light.unionByName(joined_heavy)
Key reasoning:- Salting spreads heavy-key records across many partitions so no single reducer OOMs/Gc-stalls.- Pre-aggregation reduces data size before shuffles.- Kryo + small executors reduce object overhead and GC pause durations.- Adaptive execution + skew handling further reduces manual tuning.Edge cases and notes:- Reassemble salted results if aggregation by key required (remove salt + re-aggregate).- Choose NUM_SALTS based on heavy key volume; too large increases overhead.- Monitor with Spark UI: task duration, shuffle read/write, GC time per executor.