The Recompute Cost

Concepts covered: Spark Execution Model

Each check replays every earlier round

The stopping check ends in first(), an action, and an action runs a plan from its sources. At round k, the check's plan begins at the nodes scan and holds k joins and k groupBys, so Spark recomputes rounds 1 through k before adding up a single label. The job for round k-1 sent its sum to the driver and kept nothing a later job could start from.

Put a price on it. Suppose a round of join and groupBy over the member graph takes 2 minutes of cluster time. The check at round k then costs about 2k minutes, and the run pays for every check along the way:

RoundRounds its check replaysCheck timeRounds of work so far
112 min1
224 min3
101020 min55
202040 min210
303060 min465

After n checks the total is n(n+1)/2 rounds, which grows with the square of n. 30 rounds of progress cost 465 rounds of work, or 930 minutes at 2 minutes a round, where computing each round once would take 60 minutes. Doubling the run to 60 rounds roughly quadruples the bill, to 1,830 rounds.

Narrow loops pay less of this bill on executors. A loop of withColumn rewrites like the one in the previous section's cell replays cheaply, because pipelining fuses its steps into a single pass over each row. Its cost lands on the driver, which still plans every step. Loops whose rounds hold joins and groupBys pay on both sides.

Fewer actions, same plan

Running fewer actions cuts the cost without any new API. Here are 3 loop shapes over 30 rounds, priced in rounds of work.

Loop shapeRounds of workJoins in the last plan
Check after every round46530
Check after every 5th round10530
30 fixed rounds, one action at the end3030

Checking less often cuts executor work, but the final action still plans and runs a 30-join chain, and a fixed round count either stops too early or wastes rounds after the labels settle. Only a checkpoint bounds the depth of the plan itself.

The bill shows up in 3 places

Where repeated rounds cost you

Reading it in the Spark UI

Where to lookLoop with its plan depth cappedLoop replaying every round
Jobs tab, check durationsRise for a few rounds, then drop back to one round's timeA straight climb: 2, 4, 6 ... 60 minutes
Stages per check jobStays under a fixed capMore each round, a join and groupBy's worth
Stages tab, edges inputRead a capped number of times per checkRead k times by round k's check
Event timelineShort, steady gapsWidening gaps with idle executors

A climbing line on the Jobs tab while input size holds still points to lineage before it points to data. To confirm, open 2 check jobs a few rounds apart and compare their DAG visualizations. If the later one holds more joins, the plan is carrying history from round to round.

dims holds 8 DataFrames, and each join takes about 1 minute when run once. Roughly how many join-minutes does this loop spend on executors?

events = spark.table("user_events")
for i in range(8):
    events = events.join(dims[i], "user_id")
    print(i, events.count())

About this lesson

This section is part of the Lazy Evaluation: Advanced lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.