Big Data Technologies

Chapter 7
Distributed Processing with Apache Spark

Bachelors Program in IT
Big Data Technologies   •   Bachelor

Distributed Processing with Apache Spark

Introduction and Context

Modern organizations collect data whose volume, velocity, and variety exceed the practical limits of single-machine processing. Apache Spark is a distributed data-processing engine designed to execute computation across clusters while presenting developers with relatively high-level APIs. It supports batch processing, interactive SQL, streaming, graph processing, and machine learning within one ecosystem.

Spark is not a database and is not itself a storage system. It normally reads data from systems such as HDFS, Amazon S3, Azure Data Lake Storage, relational databases, Kafka, or local files; performs distributed computation; and writes results to an appropriate destination. Its central contribution is a programming model that combines distributed execution, fault tolerance, in-memory processing, and declarative optimization.

This chapter explains Spark’s execution model, resilient distributed datasets (RDDs), DataFrames and Spark SQL, performance optimization, machine-learning workflows, and the differences between Spark and classic MapReduce.

Learning Objectives

By the end of this chapter, you should be able to:

  1. Explain Spark’s architecture and execution model.
  2. Distinguish transformations from actions and narrow from wide dependencies.
  3. Use RDD, DataFrame, and Spark SQL APIs appropriately.
  4. Explain lineage, stages, tasks, partitions, shuffles, and caching.
  5. Compare Spark with disk-oriented MapReduce.
  6. Diagnose common performance bottlenecks and select suitable remedies.
  7. Construct a basic Spark machine-learning workflow.
  8. Evaluate governance, reliability, and operational considerations in Spark deployments.

1. Spark’s Architectural Model

A Spark application consists of a driver program and executor processes. The driver creates the Spark session, constructs the logical computation, coordinates execution, and receives results. Executors run tasks on worker nodes and may cache partitions in memory or local storage. A cluster manager allocates resources. Common cluster managers include Standalone, YARN, Kubernetes, and managed cloud services.

The main components are:

ComponentResponsibility
DriverCreates the application, builds the execution plan, and coordinates work
Cluster managerAllocates CPU, memory, and executor containers or pods
ExecutorRuns tasks and stores cached data for the application
Worker nodePhysical or virtual machine hosting executors
TaskSmallest unit of work sent to an executor
PartitionA logical chunk of distributed data processed by one task at a time

A Spark application commonly follows this sequence:

  1. The driver creates a SparkSession.
  2. The program defines transformations such as filtering, projection, or joining.
  3. Spark records a logical plan rather than immediately executing every operation.
  4. An action, such as count() or write, triggers execution.
  5. The driver creates a physical plan, divides it into stages, and submits tasks.
  6. Executors process partitions and return results or write output.

This delayed execution is called lazy evaluation. It allows Spark to combine operations, eliminate unnecessary work, and choose more efficient physical strategies.

2. RDDs: The Foundational Abstraction

An RDD, or resilient distributed dataset, is an immutable, partitioned collection of records that can be processed in parallel. “Resilient” refers to its ability to recover lost partitions by recomputing them from lineage: the record of transformations used to construct the RDD.

RDDs have four important properties:

  • Immutability: An operation creates a new RDD rather than modifying an existing one.
  • Partitioning: Records are divided across cluster nodes.
  • Fault tolerance: Lost partitions can be recomputed from their dependencies.
  • Functional operations: Transformations such as map, filter, and reduceByKey describe distributed computation.

An RDD can be created from a distributed storage system or from an in-memory collection. For example, in PySpark:

rdd = spark.sparkContext.textFile("/data/logs.txt")
errors = rdd.filter(lambda line: "ERROR" in line)
count = errors.count()

filter is a transformation; count is an action. The file is not necessarily scanned when filter is defined. The scan normally occurs when count is requested.

Transformations and Actions

Transformations produce a new distributed dataset. Examples include map, filter, flatMap, distinct, union, join, and reduceByKey. Actions return a value to the driver or write data externally. Examples include count, collect, first, take, saveAsTextFile, and DataFrame write operations.

A useful distinction is that transformations describe a computation, whereas actions demand a result. Calling collect() on a very large dataset is dangerous because it transfers all records to the driver and may cause an out-of-memory failure.

Narrow and Wide Dependencies

A narrow dependency allows each output partition to be computed from a small number of input partitions. map and filter are typical examples. A wide dependency requires records to be redistributed between partitions. This redistribution is called a shuffle. groupByKey, many joins, distinct, and orderBy may require shuffles.

Narrow operations can often be pipelined within one stage. A wide dependency usually creates a stage boundary because data must be exchanged across executors. Shuffles involve serialization, network transfer, sorting or aggregation, disk spill, and synchronization; therefore, they are often among the most expensive Spark operations.

Lineage and Checkpointing

Lineage gives Spark a logical recipe for recomputing lost data. For moderate dependency chains, this is efficient and avoids replicating every intermediate result. However, very long or complex lineage can make planning and recovery expensive. Checkpointing materializes an RDD or DataFrame to reliable storage and truncates its lineage.

Checkpointing is useful for iterative algorithms, streaming applications, and computations with long dependency chains. It costs I/O, so it should be applied deliberately rather than to every intermediate dataset.

3. Partitions, Stages, and Tasks

A partition is the unit of parallelism. If a dataset has 200 partitions, Spark can potentially schedule approximately 200 tasks for a stage, subject to available executor cores and scheduling constraints. Too few partitions limit parallelism; too many create scheduling and serialization overhead.

A Spark job is triggered by an action. The scheduler divides the job into stages separated by shuffle boundaries. Within each stage, tasks operate on partitions. If a stage has 500 partitions, it generally produces about 500 tasks, although the precise execution plan and data source can affect this relationship.

An approximate idealized execution time can be expressed as:

T ≈ max(Tcompute, Tnetwork, Tdisk) + Tscheduling + Tserialization

Here, T_compute is processor time, T_network is time for data transfer, T_disk is time for reads, writes, and spills, T_scheduling is task and stage coordination time, and T_serialization is the cost of converting objects to transferable or storable representations. This is not an exact performance law: real workloads overlap activities and are affected by skew, contention, garbage collection, and failures. It is a diagnostic model for identifying dominant costs.

A task’s effective parallelism is constrained by available execution slots. If there are C simultaneously available task slots and N tasks in a stage, the stage requires at least:

R = ⌈N ÷ C⌉

waves of task execution under simplified assumptions. R is the number of waves, N is the number of tasks, and C is the number of concurrently available slots. This estimate ignores unequal task durations and scheduling overhead. If one partition is much larger than the others, the final task may determine stage completion time even when most tasks finish earlier.

4. DataFrames and Spark SQL

Although RDDs are flexible, most modern analytical Spark applications use DataFrames or typed Dataset APIs. A DataFrame is a distributed collection organized into named columns. Its schema gives Spark information about data types and enables the Catalyst optimizer and Tungsten execution improvements.

In PySpark:

from pyspark.sql import functions as F

sales = spark.read.parquet("/warehouse/sales")
summary = (
    sales.filter(F.col("year") == 2025)
         .groupBy("region")
         .agg(F.sum("amount").alias("revenue"))
         .orderBy(F.desc("revenue"))
)
summary.show()

The same logical operation can be expressed in SQL:

sales.createOrReplaceTempView("sales")
summary = spark.sql("""
    SELECT region, SUM(amount) AS revenue
    FROM sales
    WHERE year = 2025
    GROUP BY region
    ORDER BY revenue DESC
""")

DataFrame and SQL operations are usually preferable for structured data because Spark can inspect the schema and optimize the complete query. RDDs remain valuable when records are highly irregular, when custom low-level control is necessary, or when using APIs not available in DataFrame form.

Catalyst and Physical Planning

Catalyst is Spark SQL’s extensible query optimizer. It can perform such transformations as predicate pushdown, column pruning, constant folding, and simplification of expressions. Predicate pushdown attempts to apply filters close to the data source. Column pruning reads only required columns. These optimizations reduce I/O and data movement.

The physical planner selects an execution strategy. For a join, it may select a broadcast hash join, sort-merge join, or another strategy depending on statistics, configuration, and available memory. The explain() method reveals logical and physical plans:

summary.explain("formatted")

An explain plan should be interpreted rather than accepted blindly. A plan containing unexpected exchanges may indicate a shuffle. A broadcast join may be efficient for a genuinely small table but harmful if the table is larger than executor memory.

Data Sources and File Layout

Spark supports formats such as Parquet, ORC, JSON, CSV, and JDBC sources. Columnar formats such as Parquet and ORC are generally effective for analytical workloads because they support column pruning, compression, and predicate pushdown. CSV is convenient but lacks robust typing and is usually less efficient for repeated analysis.

Partitioned directories can reduce scanning. For example, data stored under year=2025/month=01/ allows partition pruning when a query filters on those columns. Excessive small files, however, create metadata and task-launch overhead. File sizes should be managed through compaction and suitable write strategies.

5. Comparing Spark with MapReduce

Classic Hadoop MapReduce generally executes a map phase, materializes intermediate results to local disk, transfers them to reducers, and writes final output to a distributed file system. This design is robust and appropriate for many large batch jobs, but repeated disk materialization can make iterative algorithms and interactive queries slow.

Spark can also execute map-like and reduce-like operations, but it provides a broader execution engine with DAG scheduling, memory caching, SQL optimization, streaming, and machine-learning libraries. Spark may keep reusable data in memory, although it still uses disk when data does not fit or when shuffle and persistence policies require it.

DimensionMapReduceApache Spark
Execution modelFixed map and reduce phasesGeneral directed acyclic graph of stages
Intermediate dataCommonly materialized to diskCan be pipelined or cached; shuffle data may use disk
Iterative algorithmsOften expensiveMore efficient when data is persisted appropriately
Query interfaceProgramming-orientedSQL, DataFrame, Dataset, and RDD APIs
LatencyCommonly higher for repeated jobsOften lower, but depends on workload and configuration
Fault toleranceReplication and recomputationLineage, recomputation, checkpointing, and storage reliability
Best fitStable disk-oriented batch pipelinesMulti-stage analytics, interactive queries, and iterative workloads

Spark is not automatically faster. A poorly partitioned Spark job with a large shuffle can be slower than a well-designed MapReduce job. The appropriate platform depends on latency, workload shape, operational constraints, cost, and existing infrastructure.

6. Performance Engineering

Performance optimization should begin with measurement, not guesswork. Spark’s web UI, event logs, executor metrics, SQL plans, and storage metrics provide evidence about bottlenecks.

Common Bottlenecks and Remedies

Data skew. Skew occurs when a small number of keys contain disproportionate amounts of data. One reducer-like task may run far longer than the others. Symptoms include a stage with one or a few very slow tasks. Remedies include salting hot keys, pre-aggregating data, using broadcast joins where appropriate, and redesigning the data model.

Excessive shuffling. Repeated groupByKey, unnecessary distinct, and poorly planned joins move large volumes across the network. Prefer map-side aggregation such as reduceByKey for RDDs, filter early, select only needed columns, and inspect physical plans.

Small files. Thousands of tiny input files increase file-listing and task overhead. Compact files and choose an appropriate target file size while preserving useful partitioning.

Insufficient or excessive partitions. A partition that is too large can cause memory pressure and long tasks. Too many tiny partitions increase scheduling overhead. Adaptive Query Execution can adjust some SQL execution decisions, but data layout and initial partitioning still matter.

Driver overload. collect(), large toPandas(), or excessive metadata operations can exhaust driver memory. Prefer distributed writes, limit, sampling, or aggregate summaries.

Poor serialization. Python object serialization and user-defined functions can be expensive. Prefer built-in Spark SQL functions, which are visible to the optimizer, over opaque Python UDFs when equivalent expressions exist.

Improper caching. Caching every intermediate result consumes memory and may reduce performance. Cache only datasets reused across actions or iterations, choose an appropriate storage level, and call unpersist() when data is no longer needed.

Garbage collection and memory pressure. Large objects, inefficient schemas, and excessive caching can cause executor pauses or out-of-memory errors. Use compact types, avoid unnecessary object creation, monitor executor memory, and distinguish execution memory from storage memory in configuration analysis.

A Practical Diagnostic Sequence

  1. Confirm the input volume, file count, schema, and partition layout.
  2. Use explain("formatted") to inspect exchanges, joins, scans, and filters.
  3. Open the Spark UI and identify the slowest stage.
  4. Compare task durations and input sizes to detect skew.
  5. Examine shuffle read, shuffle write, spill, executor memory, and garbage collection.
  6. Test one change at a time using a representative dataset.
  7. Verify that improved runtime does not compromise correctness, cost, or reproducibility.

Worked Example: Estimating Task Waves

Suppose a stage contains 120 partitions and the cluster provides 24 concurrent task slots. Under the simplified equal-duration assumption:

R = ⌈120 ÷ 24⌉ = ⌈5⌉ = 5

The stage requires approximately five waves. If one partition contains ten times as many records as the others, the stage may still take substantially longer than five equal waves would suggest. The calculation estimates scheduling capacity; it does not predict wall-clock time.

7. Machine Learning with Spark

Spark ML provides distributed machine-learning algorithms and pipelines through the DataFrame-based pyspark.ml API. A typical workflow contains:

  1. Data ingestion and schema validation.
  2. Cleaning and feature construction.
  3. Training and test splitting.
  4. Estimator fitting.
  5. Model transformation to generate predictions.
  6. Evaluation using task-appropriate metrics.
  7. Cross-validation or train-validation selection.
  8. Persistence and deployment.

A Transformer converts one DataFrame into another. An Estimator learns parameters from data and produces a Transformer after fitting. A Pipeline chains stages so that feature processing and prediction are executed consistently.

Example:

from pyspark.ml import Pipeline
from pyspark.ml.feature import StringIndexer, VectorAssembler
from pyspark.ml.classification import LogisticRegression

indexer = StringIndexer(inputCol="category", outputCol="category_index")
assembler = VectorAssembler(
    inputCols=["age", "income", "category_index"],
    outputCol="features"
)
classifier = LogisticRegression(featuresCol="features", labelCol="label")
pipeline = Pipeline(stages=[indexer, assembler, classifier])
model = pipeline.fit(training_data)
predictions = model.transform(test_data)

Data leakage occurs when information unavailable at prediction time enters training features. Splitting data before fitting data-dependent transformations, using time-aware splits for temporal data, and encapsulating transformations in a pipeline reduce leakage risk.

For binary classification, accuracy is defined as:

Accuracy = (TP + TN) ÷ (TP + TN + FP + FN)

TP and TN are true positives and true negatives; FP and FN are false positives and false negatives. Accuracy can be misleading for imbalanced classes. Precision, recall, area under the precision-recall curve, and cost-sensitive measures may be more appropriate. Metric choice must reflect the operational consequences of errors.

Distributed machine learning does not eliminate statistical responsibilities. Analysts must consider sampling bias, feature validity, fairness, explainability, privacy, reproducibility, and whether a model’s performance generalizes beyond the training data.

8. Illustrative Case: Retail Revenue Analytics

Background

A national retailer receives daily transaction files from stores and online channels. Managers need regional revenue summaries, while an analytics team wants to predict whether an order will be returned. The organization has several years of Parquet data in a cloud object store.

Problem

The initial pipeline reads all columns, converts data to Python objects, performs a groupByKey, and writes thousands of small output files. Runtime is unpredictable. Some jobs fail because the driver receives all predictions through collect().

Analysis

The engineering team inspects the SQL plan and Spark UI. The plan shows a full scan despite queries using only six columns. A large exchange appears before aggregation. Task-duration statistics reveal that a small number of product-category keys account for most records. The output directory contains many tiny files.

Approach and Solution

The team implements the following changes:

  • Converts raw CSV data into typed Parquet.
  • Selects only required columns and filters by date during the scan.
  • Uses built-in DataFrame aggregations rather than Python UDFs.
  • Pre-aggregates transaction data before a regional join.
  • Broadcasts a genuinely small store-dimension table after verifying its size.
  • Investigates skewed product categories and applies a controlled key-salting strategy for the worst cases.
  • Repartitions output by reporting date and compacts small files.
  • Writes predictions to distributed storage rather than collecting them on the driver.
  • Uses a Pipeline containing feature transformations and the return classifier.

Outcome

The revised workflow reduces input scanning, avoids unnecessary object conversion, distributes the largest aggregation more evenly, and produces manageable output files. The model is evaluated using recall and precision as well as accuracy because missed high-risk returns have a different business cost from unnecessary reviews.

Learning Points

The case demonstrates that Spark performance depends on data layout, query structure, partitioning, and operational discipline—not merely on adding executor nodes. It also illustrates why analytical correctness, model governance, and performance engineering must be addressed together.

9. Governance, Security, and Responsible Use

Spark applications often process personal, financial, health, or behavioral data. Access controls should follow least privilege, and sensitive columns should be encrypted in transit and at rest where required. Secrets must not be embedded in notebooks or source code. Audit logs, retention policies, and lineage records support accountability.

Data minimization is especially important because distributed copies may exist in input files, shuffle files, caches, checkpoints, logs, and output tables. Teams should understand where temporary data is stored and how it is deleted. Access to the Spark UI and event logs must also be controlled because query plans and sampled data can reveal sensitive information.

Machine-learning workflows require additional controls: documented data provenance, approval for high-impact use cases, monitoring for drift, bias assessment, reproducible versions of code and data, and human review when automated decisions affect individuals. Compliance obligations vary by jurisdiction and sector; technical teams should work with legal, privacy, and security specialists rather than treating compliance as a configuration option.

10. Contemporary Developments and Best Practices

Current Spark practice increasingly emphasizes adaptive execution, cloud object storage, container orchestration, table formats with transactional features, declarative pipelines, and integration with streaming platforms. Adaptive Query Execution can revise certain decisions during execution using observed statistics, including some join and partition strategies. It improves robustness but does not replace sound data modeling.

Recommended practices include:

  • Prefer DataFrames and built-in functions for structured processing.
  • Use explicit schemas for production data.
  • Store analytical data in columnar, compressed formats.
  • Filter early and select only necessary columns.
  • Inspect physical plans and Spark UI evidence.
  • Treat shuffles, skew, and small files as design concerns.
  • Cache only reused data and release it when finished.
  • Avoid collecting large datasets to the driver.
  • Use pipelines to make feature processing reproducible.
  • Separate development, testing, and production credentials.
  • Test with representative data volumes and distributions.
  • Monitor cost, runtime, data quality, and model quality together.

Challenges and Limitations

Spark requires cluster resources, operational expertise, and careful configuration. It can be inefficient for very small jobs where startup time dominates. It is not a substitute for a transactional database, and its general-purpose flexibility can lead to expensive accidental shuffles. In-memory persistence is bounded by available resources, and fault tolerance through recomputation can be costly for long dependency chains. Python interoperability is convenient but may introduce serialization overhead. Finally, a technically successful job can still be unacceptable if it produces inaccurate, biased, insecure, or noncompliant results.

Chapter Summary

Apache Spark is a distributed execution engine that represents computation as a directed acyclic graph of operations. Its driver coordinates executors, partitions provide parallelism, and actions trigger lazy transformations. RDDs provide low-level immutable distributed collections with lineage-based fault tolerance. DataFrames and Spark SQL provide schema-aware processing and enable query optimization through logical and physical planning.

Narrow dependencies can usually be pipelined, whereas wide dependencies create shuffles and stage boundaries. Performance depends on partitioning, data locality, serialization, network traffic, disk spills, skew, and resource allocation. Spark often outperforms disk-oriented MapReduce for iterative and multi-stage workloads, but it is not automatically faster. The Spark UI and execution plans are essential diagnostic tools.

Spark ML supports reproducible distributed workflows through Transformers, Estimators, and Pipelines. Effective use requires attention to leakage, metrics, fairness, privacy, and deployment governance. Professional Spark development combines algorithmic understanding with data modeling, systems measurement, security, and responsible data stewardship.

Key Terms

RDD: Immutable, partitioned distributed collection with lineage-based fault tolerance.

DataFrame: Distributed, schema-based collection organized into named columns.

SparkSession: Entry point for DataFrame, SQL, and Spark application functionality.

Driver: Process that coordinates a Spark application and builds execution plans.

Executor: Process that runs tasks and may cache data.

Partition: Logical chunk of distributed data processed by a task.

Transformation: Lazy operation that produces a new dataset.

Action: Operation that triggers execution and returns or writes a result.

Lineage: Record of transformations used to derive a distributed dataset.

Shuffle: Redistribution of records across partitions or executors.

Stage: Group of tasks separated from other groups by shuffle boundaries.

Task: Smallest scheduled unit of Spark computation.

Catalyst: Spark SQL framework for logical and physical query optimization.

Predicate pushdown: Applying filters near the data source to reduce reading.

Data skew: Uneven distribution of records or keys across partitions.

Broadcast join: Join strategy that copies a suitably small relation to executors.

Checkpoint: Materialized representation used to truncate lineage.

Transformer: ML component that converts one DataFrame into another.

Estimator: ML component that learns from data and produces a Transformer.

Data leakage: Use of information during training that would not be available at prediction time.

Review Questions

Short Answer Questions

  1. What is the difference between a Spark transformation and an action?
  2. Why can a shuffle create a stage boundary?
  3. What role does the driver play in a Spark application?
  4. Why are DataFrames often preferred to RDDs for structured data?
  5. What is data skew, and how can it affect runtime?

Long Answer Questions

  1. Explain Spark’s architecture from application submission through task completion.
  2. Compare RDDs, DataFrames, and Spark SQL in terms of abstraction, optimization, and appropriate use.
  3. Compare Spark with classic MapReduce for iterative analytics and fault tolerance.
  4. Describe a systematic process for diagnosing a slow Spark job.
  5. Explain how Spark ML Pipelines support reproducible machine-learning workflows.

Analytical / Application Questions

  1. A job has 240 partitions and 30 concurrent task slots. Estimate the number of equal-sized task waves and explain why actual runtime may be longer.
  2. A join has one extremely frequent key and many ordinary keys. Diagnose the likely bottleneck and propose two remedies, including their risks.
  3. Design a Spark workflow for predicting customer churn. Specify data preparation, leakage controls, evaluation metrics, storage format, and governance safeguards.

Multiple Choice Questions with Answers

Question 1

Which Spark operation normally triggers execution of a lazy transformation chain?

A. filter()

B. map()

C. count()

D. select()

Correct Answer: C. count()

Question 2

Which dependency generally requires records to be redistributed across partitions?

A. Narrow dependency

B. Wide dependency

C. Local dependency

D. Static dependency

Correct Answer: B. Wide dependency

Question 3

What is the main danger of calling collect() on a very large DataFrame?

A. It disables schema inference

B. It transfers all results to the driver

C. It automatically causes data skew

D. It removes all partitions

Correct Answer: B. It transfers all results to the driver

Question 4

Which format is generally well suited to analytical column pruning and compression?

A. CSV

B. Plain text

C. Parquet

D. Unstructured log text

Correct Answer: C. Parquet

Question 5

A stage has 120 partitions and 24 available task slots. Under equal-duration assumptions, how many task waves are required?

A. 4

B. 5

C. 24

D. 96

Correct Answer: B. 5

Question 6

Which technique can reduce the amount of data transferred in a join when one relation is genuinely small?

A. Broadcast join

B. Disabling partitioning

C. Calling collect()

D. Replacing columns with strings

Correct Answer: A. Broadcast join

Question 7

What does Spark lineage primarily support?

A. Automatic schema creation

B. Recovery by recomputing lost partitions

C. Encryption of all outputs

D. Elimination of all shuffles

Correct Answer: B. Recovery by recomputing lost partitions

Question 8

Which practice is usually preferable when a built-in Spark SQL expression can replace a Python UDF?

A. Use the Python UDF because it is always faster

B. Use the built-in expression because it is visible to the optimizer

C. Collect the data first

D. Convert the DataFrame to CSV first

Correct Answer: B. Use the built-in expression because it is visible to the optimizer

Question 9

In Spark ML, what does an Estimator produce after fitting?

A. A cluster manager

B. A Transformer

C. A partition directory

D. A shuffle file

Correct Answer: B. A Transformer

Question 10

Which problem occurs when training uses information unavailable at prediction time?

A. Data leakage

B. Column pruning

C. Predicate pushdown

D. Task pipelining

Correct Answer: A. Data leakage

Big Data Technologies
Chapter 7 of 14