Skip to Content
Course content

83: Spark Streaming Basics

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I worked with a developer who was tasked with monitoring server health across a massive fleet of machines. His initial approach was a classic "poor man's streaming" setup: a cron job that triggered a Spark batch job every ten minutes to process the last ten minutes of logs. It worked fine during the quiet hours, but during a major outage, the volume of error logs spiked so aggressively that the batch job started taking twelve minutes to run. He was effectively chasing a ghost, falling further and further behind the real-time state of the system until the entire pipeline collapsed under the heap pressure. He didn't need a faster batch job; he needed a way to process data as it arrived.

The Mental Shift to Micro-batches

When you first dive into Spark Streaming, you have to understand that Spark (specifically Structured Streaming) doesn't treat data as a continuous flow of individual events in the way something like Apache Flink does. Instead, it uses "micro-batching." Imagine your data is a river; Spark doesn't drink from the river one drop at a time. Instead, it dips a bucket in every few seconds, pulls out whatever accumulated, and processes that bucket using the same DataFrame API you've already mastered in the batch lessons.

I actually prefer this approach. Why? Because it means you don't have to learn a completely different set of operators for streaming. If you know how to filter a DataFrame or join two tables, you already know 90% of Spark Streaming. The primary difference is that you're no longer working with a static dataset, but an "unbounded table" that keeps growing as new data arrives.

Wiring Up a Stream with DataFrames

To get a stream running, you move from spark.read to spark.readStream. You define your source—which could be a Kafka topic, a socket, or even a directory of files—and Spark creates a streaming DataFrame. Let's look at a scenario where we're monitoring IoT temperature sensors that drop JSON files into a cloud storage folder.

val sensorData = spark.readStream
  .schema(sensorSchema) // Streaming requires an explicit schema
  .json("s3a://iot-bucket/incoming-temps/")

val alerts = sensorData
  .filter("temperature > 100")
  .select("sensor_id", "temperature", "timestamp")

Notice that sensorSchema is mandatory here. In batch mode, Spark can peek at the data to infer the schema. In streaming, it can't afford to wait for the "end" of the data to figure out what the columns are, so you have to be explicit. If you forget this, Spark will throw a fit.

Managing the Output Sink

Defining the transformation is the easy part. The tricky bit is the "Sink"—where the data actually goes. Unlike a batch job where you just call .save(), a stream needs a writeStream configuration. This is where you decide how often to trigger the micro-batch and where to store the result.

You'll typically see two main output modes: append (which only adds new rows) and complete (which rewrites the entire result table, usually used for aggregations). For our sensor alerts, we only care about the new overheating events, so append is the way to go.

val query = alerts.writeStream
  .outputMode("append")
  .format("console") // Great for debugging, avoid in production!
  .option("checkpointLocation", "/mnt/checkpoints/temp-alerts")
  .start()

query.awaitTermination()

That checkpointLocation is non-negotiable. If your cluster crashes, Spark uses that directory to remember exactly which files it has already processed. Without a checkpoint, you're essentially gambling with your data integrity, as you'll likely either skip data or process the same records twice upon restart.




📋 Practical Task

Building a Real-time Error Log Monitor

Your task is to implement a Structured Streaming pipeline that monitors a local directory for incoming JSON log files. The goal is to filter out only the "ERROR" level logs and stream them to the console in real-time.

Requirements:

  • Define a schema for the logs containing: timestamp (TimestampType), level (StringType), and message (StringType).
  • Use spark.readStream to monitor a folder named /tmp/log_stream.
  • Filter the stream to include only rows where the level is exactly "ERROR".
  • Configure the writeStream to use append mode and output to the console.
  • Ensure you include a checkpointLocation in /tmp/log_checkpoints to ensure the stream is fault-tolerant.

Testing your solution: Once your code is running, manually create a JSON file in /tmp/log_stream with the content {"timestamp": "2023-10-27 10:00:00", "level": "ERROR", "message": "Database connection failed"} and verify that it appears in your console output.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.