RDD - Shuffle Partitions
A Shuffle is the physical movement of data across different worker nodes in a cluster. It is triggered by Wide Transformations (such as reduceByKey, groupByKey, join, and repartition) when Spark needs to group records with the same keys together.
Shuffling is the single most expensive operation in a distributed application. Tuning the number of partitions during a shuffle is critical: too few partitions can cause memory overload, while too many partitions create slow task-scheduling overhead.
This guide provides a detailed exploration of Shuffle Partitions and default parallelism configurations in PySpark, complete with practical code examples.
RDD Parallelism vs. DataFrame Shuffle Partitions
A common point of confusion is the difference between RDD default parallelism and Spark SQL shuffle partitions:
1. spark.sql.shuffle.partitions (Default: 200)
- API Scope: Spark SQL / DataFrame API only.
- Behavior: Controls the partition count for shuffles triggered by DataFrame transformations (like
df.groupBy().sum()). - Note: Changing this value does not affect RDD transformations!
2. spark.default.parallelism
- API Scope: Core RDD API.
- Behavior: Controls the default number of partitions for RDDs created by
sc.parallelize()or returned by wide RDD transformations (likereduceByKey) when you do not explicitly pass a partition count. - Default Values:
- Local Mode: Number of CPU cores on the local machine.
- Cluster Mode (YARN/K8s): Total number of cores on all executor nodes combined, or 2 (whichever is larger).
Performance Impacts of Partition Counts
graph TD
A["Shuffle Partition Count"] --> B["Too Low (e.g. 1 - 5 partitions)"]
A --> C["Too High (e.g. 10,000 partitions)"]
B --> B1["Out Of Memory (OOM) errors"]
B --> B2["Disk Spilling (data doesn't fit in RAM)"]
B --> B3["Under-utilization (most cores idle)"]
C --> C1["Massive Task Scheduler overhead"]
C --> C2["Tiny data files (metadata bloat)"]
C --> C3["Network saturation (too many connections)"]
style A fill:#fff3e0,stroke:#e65100,stroke-width:2px;
style B fill:#ffebee,stroke:#c62828,stroke-width:2px;
style C fill:#ffebee,stroke:#c62828,stroke-width:2px;
PySpark Code Examples
A. Setting Default RDD Parallelism in the Builder
You can define the default RDD parallelism when initializing the SparkSession using the .config() method:
from pyspark.sql import SparkSession
# 1. Initialize Spark and set default RDD parallelism to exactly 8
spark = SparkSession.builder \
.appName("Day01 Shuffle Partitions") \
.master("local[*]") \
.config("spark.default.parallelism", "8") \
.getOrCreate()
sc = spark.sparkContext
# 2. Parallelize a list without specifying slices
# Spark will automatically assign the default parallelism of 8
default_rdd = sc.parallelize([1, 2, 3, 4, 5, 6])
print("Default Parallelism Partitions:", default_rdd.getNumPartitions())
# Output: Default Parallelism Partitions: 8
B. Overriding Partition Counts Directly in Transformations
In the RDD API, the most robust way to control shuffle partition counts is to explicitly pass the partition number as an optional argument directly inside the wide transformation.
This is highly recommended in production because different stages of a pipeline require different levels of parallelism depending on data size!
# 1. Input RDD representing user clicks: (UserID, PagesVisited)
clicks_rdd = sc.parallelize([
("User1", 5), ("User2", 12), ("User1", 3),
("User3", 8), ("User2", 4), ("User1", 1)
])
# 2. Aggregating values by key
# By default, reduceByKey would use 'spark.default.parallelism' (8)
# Here, we explicitly override it to use exactly 3 partitions for this operation
aggregated_rdd = clicks_rdd.reduceByKey(lambda x, y: x + y, numPartitions=3)
# 3. View the partition count of the resulting RDD
print("
--- Custom Partitioning in Action ---")
print("Aggregated RDD Partition Count:", aggregated_rdd.getNumPartitions()) # 3
# 4. View how data is distributed inside the 3 partition buckets
partitioned_data = aggregated_rdd.glom().collect()
for idx, partition in enumerate(partitioned_data):
print(f" Partition {idx}: {partition}")
# Expected Output:
# Aggregated RDD Partition Count: 3
# Data distribution:
# Partition 0: [('User1', 9)]
# Partition 1: [('User2', 16)]
# Partition 2: [('User3', 8)]
# (Note: Elements are hashed and split into exactly 3 partitions)
C. Contrast: Configuring SQL Shuffle Partitions
For reference, let's look at how to verify and change the shuffle partition count for DataFrames (which uses a different config option):
# 1. Query the default SQL Shuffle partition count (usually 200)
default_sql_partitions = spark.conf.get("spark.sql.shuffle.partitions")
print("Default SQL Shuffle Partitions:", default_sql_partitions) # 200
# 2. Change SQL shuffle partitions to 10 for a smaller dataset
spark.conf.set("spark.sql.shuffle.partitions", "10")
updated_sql_partitions = spark.conf.get("spark.sql.shuffle.partitions")
print("Updated SQL Shuffle Partitions:", updated_sql_partitions) # 10
Best Practices for Tuning Partitions
- Size of Partition: Ideally, each partition of data should contain between 100MB and 200MB of raw data in memory.
- Calculate Ideal Partitions:
Ideal Partitions = Dataset Size (MB) / 128
- Cluster Utilization: Ensure the partition count is at least $2\times$ to $4\times$ the total CPU cores in your cluster. This ensures that when a task finishes quickly on one node, other task slices are immediately available to keep all worker cores fully saturated.