PySpark Data Streaming

Spark offers two streaming APIs. The original Spark Streaming API is built on DStreams, sequences of RDDs produced every batch interval; it is now a legacy API and receives no new features. Structured Streaming, built on DataFrames and the Spark SQL engine, treats a stream as an unbounded table that grows with every new record and lets you query it with the same DataFrame operations used for batch jobs. New PySpark work, including everything on Databricks, should use Structured Streaming. The concepts listed in section 2 apply to both APIs; the worked example and diagrams use Structured Streaming.

1. The unbounded-table model

Structured Streaming micro-batch execution Source Kafka, files, Kinesis, Delta Input table unbounded; rows added per trigger Incremental plan same DataFrame ops as batch Checkpoint offsets, aggregates write-ahead log Sink Delta, Kafka, foreachBatch Each trigger processes only new rows; the checkpoint lets a restarted query continue exactly where it stopped.

You write a query as if the input were a static table. Spark plans it once, then on each trigger (by default as soon as the previous micro-batch finishes) reads the new offsets from the source, runs the incremental plan over just those rows, updates any aggregation state, and writes results to the sink. Offsets and state are recorded in the checkpoint directory before results are committed, which, with a replayable source and an idempotent or transactional sink such as Delta, gives end-to-end exactly-once results.

2. Key concepts

3. Worked example: Kafka to Delta with windows

from pyspark.sql import SparkSession, functions as F, types as T

spark = SparkSession.builder.appName("clicks-stream").getOrCreate()

schema = T.StructType([
    T.StructField("user_id", T.StringType()),
    T.StructField("page", T.StringType()),
    T.StructField("event_time", T.TimestampType()),
])

raw = (spark.readStream.format("kafka")
       .option("kafka.bootstrap.servers", "broker:9092")
       .option("subscribe", "clicks")
       .option("startingOffsets", "latest")
       .load())

events = (raw.select(F.from_json(F.col("value").cast("string"), schema).alias("e"))
             .select("e.*"))

counts = (events
          .withWatermark("event_time", "10 minutes")              # how late data may arrive
          .groupBy(F.window("event_time", "5 minutes"), "page")
          .count())

query = (counts.writeStream
         .format("delta")
         .outputMode("append")                                     # emit a window once it is final
         .option("checkpointLocation", "/chk/clicks_by_page")
         .trigger(processingTime="1 minute")
         .toTable("analytics.clicks_by_page"))

query.awaitTermination()

The Kafka source delivers bytes, so the value is cast and parsed with an explicit schema. The watermark tells Spark it may finalize a window once the maximum event time seen exceeds the window end by ten minutes; in append mode each window is written exactly once at that point, and its state is dropped. The checkpoint location must be unique per query and kept across restarts.

4. Windows and watermarks step by step

5-minute windows with a 10-minute watermark Window start times 12:00 12:05 12:10 12:15 12:20 Batch 1: max event 12:12, wm 12:02 open open open Batch 2: max event 12:18, wm 12:08 emit open open open Batch 3: max event 12:26, wm 12:16 done emit emit open open Late event 12:03 in batch 4 dropped A window [s, s+5) is emitted when the watermark passes its end; events older than the watermark are ignored.

The watermark is computed as the maximum event time seen so far minus the allowed delay. In batch 2 it reaches 12:08, which is past the end of the 12:00-12:05 window, so that window's count is final and is written. By batch 3 the watermark is 12:16, closing the 12:05 and 12:10 windows. An event stamped 12:03 that arrives afterwards is older than the watermark and is dropped rather than reopening a window that has already been written. Choosing the delay is a trade-off between completeness and how long results (and state) are held back.

5. DStreams vs Structured Streaming

AspectDStreams (legacy)Structured Streaming
AbstractionSequence of RDDsUnbounded DataFrame
Event-time windowsProcessing time onlyEvent time with watermarks
OptimizerNone (RDD operations)Catalyst and Tungsten
Exactly-once sinksApplication responsibilityBuilt in with checkpoints and transactional sinks
StatusMaintenance onlyActively developed; default on Databricks

6. Pitfalls