| title | Structured Streaming — Part 2 | |||
|---|---|---|---|---|
| type | topic | |||
| tags |
|
|||
| status | published |
This part covers stream-static joins, stateful operations, state store management, query management, checkpoints, use cases, and exam tips for Structured Streaming.
For streaming fundamentals, sources, sinks, triggers, output modes, watermarking, and windowed aggregations, see Part 1.
Join a streaming DataFrame with a static (batch) DataFrame.
# Static dimension table
dim_products = spark.table("catalog.schema.dim_products")
# Stream of events
events_stream = spark.readStream.format("delta").load("/events")
# Join stream with static
enriched = events_stream.join(
dim_products,
events_stream.product_id == dim_products.id,
"left"
)Important: The static DataFrame is read once at query start. Changes to the static table won't be reflected until the streaming query is restarted.
Custom stateful processing with exactly-once semantics.
from pyspark.sql.streaming import GroupState, GroupStateTimeout
def update_session(key, events, state: GroupState):
# Custom state management logic
if state.exists:
current_count = state.get
else:
current_count = 0
new_count = current_count + len(list(events))
state.update(new_count)
return (key, new_count)
result = (df
.groupByKey(lambda x: x.user_id)
.mapGroupsWithState(
update_session,
outputMode="update",
timeoutConf=GroupStateTimeout.ProcessingTimeTimeout
))Similar to mapGroupsWithState but can emit multiple output records.
def emit_alerts(key, events, state: GroupState):
alerts = []
# Process events and potentially emit multiple alerts
for event in events:
if event.value > threshold:
alerts.append(Alert(key, event.value, event.timestamp))
return iter(alerts)Understanding state management is critical for production streaming applications.
# Default: HDFS-based state store
spark.conf.get("spark.sql.streaming.stateStore.providerClass")
# org.apache.spark.sql.execution.streaming.state.HDFSBackedStateStoreProvider
# RocksDB state store (better for large state)
spark.conf.set(
"spark.sql.streaming.stateStore.providerClass",
"org.apache.spark.sql.execution.streaming.state.RocksDBStateStoreProvider"
)| Backend | Best For | Memory Usage |
|---|---|---|
| HDFS (default) | Small to medium state | State in memory |
| RocksDB | Large state (GB+) | Spills to disk |
- State size exceeds executor memory
- High-cardinality groupBy keys
- Long watermark delays
- Complex aggregations with many groups
# RocksDB configuration
spark.conf.set("spark.sql.streaming.stateStore.rocksdb.compactOnCommit", "true")
spark.conf.set("spark.sql.streaming.stateStore.rocksdb.changelogCheckpointing.enabled", "true")# Monitor state via query progress
progress = query.lastProgress
# State metrics are in stateOperators
if progress and "stateOperators" in progress:
for op in progress["stateOperators"]:
print(f"Operator: {op.get('operatorName')}")
print(f" numRowsTotal: {op.get('numRowsTotal')}") # Total rows in state
print(f" numRowsUpdated: {op.get('numRowsUpdated')}") # Rows updated this batch
print(f" memoryUsedBytes: {op.get('memoryUsedBytes')}") # Memory usage
print(f" numRowsDroppedByWatermark: {op.get('numRowsDroppedByWatermark')}")| Metric | Meaning | Alert If |
|---|---|---|
numRowsTotal |
Total state size | Growing unboundedly |
memoryUsedBytes |
Memory for state | Approaching heap limit |
numRowsDroppedByWatermark |
Late events dropped | Too high (adjust watermark) |
customMetrics.rocksdbMemUsage |
RocksDB memory | Exceeds configured limits |
from pyspark.sql.streaming import GroupStateTimeout
# Processing time timeout - based on clock time
result = df.groupByKey(...).mapGroupsWithState(
func,
outputMode="update",
timeoutConf=GroupStateTimeout.ProcessingTimeTimeout
)
# Event time timeout - based on watermark
result = (df.withWatermark("timestamp", "1 hour")
.groupByKey(...).mapGroupsWithState(
func,
outputMode="update",
timeoutConf=GroupStateTimeout.EventTimeTimeout
))
# No timeout - state never expires (dangerous!)
result = df.groupByKey(...).mapGroupsWithState(
func,
outputMode="update",
timeoutConf=GroupStateTimeout.NoTimeout # State grows forever!
)| Timeout Type | Behavior | Use Case |
|---|---|---|
ProcessingTimeTimeout |
Expires after wall clock time | Sessions with idle timeout |
EventTimeTimeout |
Expires when watermark passes | Event-time based expiry |
NoTimeout |
Never expires | Small, bounded state only |
# Use watermarks for automatic cleanup
(df.withWatermark("event_time", "1 hour")
.groupBy(window("event_time", "10 minutes"))
.count()) # State cleaned after watermark passes window
# Set timeout in mapGroupsWithState
def update_with_timeout(key, events, state):
if state.hasTimedOut:
state.remove() # Clean up expired state
return None
# ... process events
state.setTimeoutDuration("30 minutes") # Reset timeout# Check checkpoint directory for state size
dbutils.fs.ls("/checkpoint/state/0/")
# View state schema
spark.read.format("delta").load("/checkpoint/state/0/").printSchema()
# Monitor state growth over time
progress_history = query.recentProgress
for p in progress_history:
if p and "stateOperators" in p:
print(f"Batch {p['batchId']}: {p['stateOperators'][0].get('numRowsTotal')} rows")# Start with path
query = (df.writeStream
.format("delta")
.option("checkpointLocation", "/checkpoint")
.start("/output/path"))
# Start with table
query = (df.writeStream
.format("delta")
.option("checkpointLocation", "/checkpoint")
.toTable("catalog.schema.table"))
# Named query
query = (df.writeStream
.queryName("my_streaming_query")
.format("delta")
.start("/output/path"))# Get active queries
spark.streams.active
# Query status
query.status
# Last progress
query.lastProgress
# Recent progress
query.recentProgress
# Check if running
query.isActive
# Exception (if failed)
query.exception()# Wait for termination
query.awaitTermination()
# Wait with timeout
query.awaitTermination(timeout=3600) # 1 hour
# Stop query
query.stop()
# Stop all queries
for q in spark.streams.active:
q.stop()Checkpoints store query progress for fault tolerance.
query = (df.writeStream
.option("checkpointLocation", "/path/to/checkpoint")
.start())| Directory | Contents |
|---|---|
commits/ |
Completed batch info |
offsets/ |
Source offsets for each batch |
sources/ |
Source-specific state |
state/ |
Aggregation state |
metadata |
Query metadata |
- Use cloud storage (S3, ADLS, GCS) for durability
- One checkpoint location per query
- Don't share checkpoints between different queries
- Keep checkpoints in same region as data
| Scenario | Recommended Trigger/Mode | Why? |
|---|---|---|
| Real-time Dashboard | Continuous / Low processingTime |
Minimizes latency for live viewing. |
| Daily ETL | availableNow=True |
Processes all data efficiently, then shuts down to save cost. |
| Aggregates (Counts/Sums) | complete Output Mode |
Validates total counts, usually requires watermarking for state cleanup. |
| De-duplication | dropDuplicates + Watermark |
Ensures unique records without unbounded state growth. |
Scenario: Trying to use append mode with aggregations without watermarking.
Fix: Add watermark or switch to complete or update mode.
Exam Context: Identifying incompatible source/sink/transformation combinations.
Scenario: Running a stream-stream join or deduplication without watermarks.
Fix: Define watermarks on both sides of join or on the dedup stream to allow state cleanup.
Scenario: Changing stateful operations (like grouping keys) and trying to resume from old checkpoint.
Fix: New query structure requires a new checkpoint location.
Scenario: Using update mode with a file sink (Parquet/ORC).
Fix: File sinks only support append mode. Use Delta sink for delete/update capabilities (via MERGE in foreachBatch).
- Always specify checkpoint location for production queries
- Use watermarks with streaming aggregations
- Choose appropriate trigger based on latency needs
- Monitor query progress with
lastProgress - Use
availableNowfor scheduled batch-style streaming - Test with rate source before connecting production sources
- Triggers:
availableNow=Truereplaces deprecatedonce=True- processes all available data in multiple batches - Output modes:
appendfor inserts,completefor aggregations,updatefor stateful - Watermarks: Required for streaming aggregations to enable state cleanup
- Stream-stream joins: Both sides need watermarks for inner joins
- Checkpoints: Required for exactly-once semantics and failure recovery
- ignoreChanges: Use when source has updates/deletes you want to skip
- foreachBatch: Enables batch operations (like MERGE) in streaming
- RocksDB state store: Use for large state that exceeds memory
- State timeouts:
ProcessingTimeTimeoutvsEventTimeTimeoutvsNoTimeout - Monitor
numRowsTotalin stateOperators to detect unbounded state growth
- Output modes:
appendfor inserts only,completefor full aggregation results,updatefor rows changed in the current batch — not every mode is compatible with every operation or sink - Watermarks are required for streaming aggregations to enable state cleanup; without them, state grows unboundedly and causes OOM
- Stream-static joins read the static side once at query start; changes to the static table are only reflected after the streaming query is restarted
- RocksDB state store is recommended when state exceeds available executor heap memory (GB+ scale or high-cardinality keys); HDFS-backed store is the default and suits smaller state
- State timeouts:
ProcessingTimeTimeoutexpires based on wall clock;EventTimeTimeoutexpires when the watermark advances past the set timestamp;NoTimeoutcauses unbounded state growth foreachBatchenables batch operations (such as MERGE) within a streaming context, providing exactly-once semantics when combined with checkpoints- Checkpoint compatibility: changing groupBy keys, watermark delay, output mode, or stateful operators requires a new checkpoint location
query.lastProgressandstateOperators[].numRowsTotalare the primary metrics for detecting unbounded state growth in production
- Incremental Processing - Checkpoint management
- Auto Loader - File ingestion streaming
- Data Deduplication - Streaming dedup
← Previous: Structured Streaming — Part 1 | ↑ Back to Data Processing | Next: Auto Loader →