Lesson 11 / 11
Cost and Cluster Sizing
Counting that the decision of how many workers a cluster is set up with is made between makespan and total work, and that the two numbers go in opposite directions: the same job of 2200 rows gives a makespan of 2250, 3691, 3179, 2610 and 1398 and a total work of 2200, 5830, 7414, 8074 and 8635 on one, two, four, eight and sixteen workers; as the makespan falls the total work rises, and the sizing decision cannot be made without writing which one is paid. The second worker raising the makespan from 2250 to 3691 comes from the shuffle coming into being, and even with a good key of skew 1.00 a rise from 2250 to 2800 is seen. The product of workers and makespan climbs one way from 2250 to 22368, when the fixed cost is counted per worker the gain of sixteen workers falls from 852 to 102, and when the network cost rises to 12 the setup with the shortest makespan returns to a single worker. The lesson closes the course and gathers the numbers of eleven lessons in one table.
Published Updated
In this lesson
- Sweeps the same job between one and sixteen workers and counts that the makespan and the total work go in separate directions.
- Makes the cluster sizing decision separately by the measures of makespan, total work and the product of workers and makespan.
- Shows the sensitivity of the decision to the network cost, the way the fixed cost is counted and the choice of partition key.
Contents
The previous lesson wrote the fifth name of the cost: the recomputed row. Before it had come the shuffled row, the copy, the skew and the held row. Beneath the five names there stood always the same two numbers: how long a job took and how much work it did in total. What remains is the question the course is asked most often: with how many workers should this cluster be set up.
The question looks innocent and has no answer, because it is incomplete. Growing a cluster changes two numbers at the same time, and they go in opposite directions. This lesson sweeps the number of workers from one to sixteen, writes both at every step, and counts which measure, which choice and which partition key the decision hangs on.
- PF53. The source, the pipeline and the distributed setup are fictional and are generated within the lesson. No distributed processing framework, cluster manager or cloud service is called. The seed is 20260218.
- PF54. The procedure of cost accounting was measured in the Cloud Governance, Security and Cost course of the Cloud Computing curriculum and in the Building Applications with Model APIs course of the AI Engineering curriculum; it is not repeated. Here the cost is written only as makespan and total work; no currency or price is used.
- PF55. Makespan is the work of the busiest worker and is the model’s own unit, not raw milliseconds. Total work is the sum of the work done by all the workers.
- PF56.
NETWORK_COST = 3andTASK_OVERHEAD = 50are choices; the result of this lesson is sensitive to both, and below both are handled together with their values. - PF57.
costcounts the fixed cost once per job; the Resource Managers lesson counts the same cost per worker. The two ways of counting are written here side by side as two columns. - PF58. The baseline is the single worker: no shuffle, skew 1.00, makespan 2250, total work 2200.
The Worker Sweep
The first block sets up the source, the three stages of the pipeline up to consumption, and the
measures of partitioning and cost. The job measured is the job used throughout the course: the rows are
partitioned by the region key and then shuffled to the subscriber key.
# FICTIONAL. Meter readings of the measurement network, the first three stages of the pipeline and # the distributed setup are generated within the lesson; no distributed processing framework, # cluster manager or cloud service is called. Seed 20260218. SEED, M32 = 20260218, 0xFFFFFFFF def rng(t): x = ((t ^ (t >> 16)) * 2246822507) & M32 x = ((x ^ (x >> 13)) * 3266489909) & M32 s = [(x ^ (x >> 16)) & M32] def next_value(): s[0] = (s[0] * 1664525 + 1013904223) & M32 return s[0] / 4294967296 return next_value REGION = ["dogu", "bati", "kuzey", "guney", "merkez"] TYPE = ["residential", "commercial"] PERIODS = [f"2026-{a:02d}" for a in range(1, 13)] SUBSCRIBERS = {} for i in range(200): r = rng(SEED + 37 * i) SUBSCRIBERS[f"A{i + 1:03d}"] = {"region": REGION[int(r() * 5)], "type": TYPE[int(r() * 2)], "start": 10000 + int(r() * 60000)} def generate_source(): records = [] for i, (sub, a) in enumerate(SUBSCRIBERS.items()): e = a["start"] for p in PERIODS: r = rng(SEED + 101 * i + 7 * PERIODS.index(p)) e += 4 + int(r() * 46) records.append({"subscriber": sub, "period": p, "region": a["region"], "type": a["type"], "index": e, "unit": "m3"}) return records def s1_screen(records): REQUIRED = ("subscriber", "period", "region", "index", "unit") keep = [k for k in records if all(a in k for a in REQUIRED)] return keep, len(records) - len(keep) def s2_dedupe(records): seen, keep = set(), [] for k in records: key = (k["subscriber"], k["period"]) if key in seen: continue seen.add(key) keep.append(k) return keep, len(records) - len(keep) def s3_consumption(records): table = {(k["subscriber"], k["period"]): k for k in records} out, dropped = [], 0 for k in records: i = PERIODS.index(k["period"]) if i == 0: continue prev = table.get((k["subscriber"], PERIODS[i - 1])) if prev is None: dropped += 1 continue out.append(dict(k, m3=int(k["index"]) - int(prev["index"]))) return out, dropped WORKERS, NETWORK_COST, TASK_OVERHEAD = 4, 3, 50 def rows(): v, _ = s1_screen(generate_source()) v, _ = s2_dedupe(v) v, _ = s3_consumption(v) return v def trace_id(m): h = 2166136261 for c in str(m): h = ((h ^ ord(c)) * 16777619) & 0xFFFFFFFF return h def distribute(records, key, workers=WORKERS): buckets = {i: [] for i in range(workers)} for k in records: buckets[trace_id(k[key]) % workers].append(k) return buckets def load(buckets): n = [len(v) for v in buckets.values()] return {"total": sum(n), "busiest": max(n), "least": min(n), "skew": round(max(n) / (sum(n) / len(n)), 4)} def shuffle(buckets, new_key, workers=WORKERS): new = {i: [] for i in range(workers)} moved = 0 for old, records in buckets.items(): for k in records: target = trace_id(k[new_key]) % workers if target != old: moved += 1 new[target].append(k) return new, moved def cost(buckets, moved=0, workers=WORKERS, network=NETWORK_COST, overhead=TASK_OVERHEAD): """Makespan = the work of the busiest worker (parallel). Total work is counted separately.""" n = [len(v) for v in buckets.values()] return {"duration": max(n) + moved * network // workers + overhead, "total_work": sum(n) + moved * network, "moved": moved} ALL = rows() COUNTS = (1, 2, 4, 8, 16) print(f"job {len(ALL)} rows , partition key region , shuffle to subscriber") print("workers skew moved makespan total work workers x makespan makespan (overhead per worker)") for i in COUNTS: bk = distribute(ALL, "region", i) _, t = shuffle(bk, "subscriber", i) m = cost(bk, t, i) mp = cost(bk, t, i, overhead=i * TASK_OVERHEAD) print(f"{i:>7} {load(bk)['skew']:>4} {t:>5} {m['duration']:>8} {m['total_work']:>10}" f" {i * m['duration']:>18} {mp['duration']:>30}")
job 2200 rows , partition key region , shuffle to subscriber
workers skew moved makespan total work workers x makespan makespan (overhead per worker)
1 1.0 0 2250 2200 2250 2250
2 1.66 1210 3691 5830 7382 3741
4 3.32 1738 3179 7414 12716 3329
8 6.64 1958 2610 8074 20880 2960
16 6.88 2145 1398 8635 22368 2148
The makespan column does not fall in a straight line: it rises from 2250 to 3691, then falls to 3179, 2610 and 1398. It drops below the single worker only at sixteen workers. The total work column, on the other hand, never turns back: from 2200 to 5830, to 7414, to 8074 and to 8635 it climbs one way. The same set of rows, the same result, four times the total work.
The second row is the clearest example of the second claim of the course. With a single worker there is no shuffle; the moment the second worker is added, 1210 rows have to cross the network, the total work jumps from 2200 to 5830, and because the share of the busiest worker is still large, the makespan also rises. Parallelism is not free, and its first step is negative.
The last two columns show how the decision turns. The product of workers and makespan, which is the
equivalent of renting the cluster for the length of the job, climbs from 2250 to 22368 without ever
falling; by this measure the best setup is the single worker. When the fixed cost is counted per
worker, the makespan of sixteen workers rises from 1398 to 2148, and its advantage over the single
worker falls from 852 units to 102 units. A choice as small as TASK_OVERHEAD = 50 nearly erases
the gain of a setup sixteen times as large.
What the Decision Hangs On
The three measures gave three separate answers. The next question is how much these answers depend on the two choices in the model and on the partition key. The second block sweeps the network cost, and then repeats the same sweep with a key that has no skew.
# This block continues the definitions of ALL, COUNTS, distribute, load, shuffle and cost from the first block. print("network cost " + " ".join(f"{i:>5}" for i in COUNTS) + " shortest makespan") for network in (1, 3, 6, 12): s = [] for i in COUNTS: bk = distribute(ALL, "region", i) _, t = shuffle(bk, "subscriber", i) s.append(cost(bk, t, i, network=network)["duration"]) print(f"{network:>12} " + " ".join(f"{v:>5}" for v in s) + f" {COUNTS[s.index(min(s))]:>17}") print("\npartition key subscriber , shuffle to period") print("workers skew moved makespan total work") for i in COUNTS: bk = distribute(ALL, "subscriber", i) _, t = shuffle(bk, "period", i) m = cost(bk, t, i) print(f"{i:>7} {load(bk)['skew']:>4} {t:>5} {m['duration']:>8} {m['total_work']:>10}")
network cost 1 2 4 8 16 shortest makespan
1 2250 2481 2310 2120 1130 16
3 2250 3691 3179 2610 1398 16
6 2250 5506 4483 3344 1800 16
12 2250 9136 7090 4813 2604 1
partition key subscriber , shuffle to period
workers skew moved makespan total work
1 1.0 0 2250 2200
2 1.0 1100 2800 5500
4 1.02 1651 1849 7153
8 1.04 1927 1058 7981
16 1.2 2066 602 8398
The first table shows what it means that NETWORK_COST is a choice. At a cost of 1, 3 and 6 sixteen
workers give the shortest makespan; at a cost of 12 the winner returns to the single worker, because
the number 2604 is above 2250. Once the ratio of the network to local processing changes, the case for a
cluster sixteen times as large disappears. The sizing decision is not a decision of the measured job
but of the ratio between the network and the computation, and without that ratio written the result
cannot be carried.
The second table separates out the share of skew. With the subscriber key the skew does not exceed
1.20 up to sixteen workers, and the makespan falls to 602, far below the 1398 of the region key. On the
other hand the second worker raising the makespan happens here too: from 2250 to 2800. The shuffle
comes into being even with a good key, and at the first step it is larger than its gain. The second
claim of the course is not a consequence of skew; skew only deepens it.
Summary
- The same job of 2200 rows gives a makespan of 2250, 3691, 3179, 2610, 1398 and a total work of 2200, 5830, 7414, 8074, 8635 on one, two, four, eight and sixteen workers; as the makespan falls the total work rises.
- The second worker raises the makespan from 2250 to 3691: the shuffle comes into being, 1210 rows cross the network and the total work jumps to 5830. The makespan falls below the single worker only at sixteen workers.
- Three measures choose three separate setups: makespan chooses sixteen workers, total work the single worker, and the product of workers and makespan (from 2250 to 22368) again the single worker. The decision cannot be made without writing which measure is paid.
- When
TASK_OVERHEAD = 50is counted per worker, the makespan of sixteen workers becomes 2148 and the gain falls from 852 to 102; whenNETWORK_COSTrises to 12, the setup with the shortest makespan returns to the single worker. - With the
subscriberkey, which has no skew, the makespan falls as far as 602, but the second worker raising the makespan from 2250 to 2800 happens there too; the cost of the shuffle at the first step is independent of skew.
Course Wrap-Up
The rows carry no common unit; by which measure each row is read stands next to the lesson name. The difference in parentheses is the distributed setup minus the single-worker baseline; the direction of the sign is read from the measure name.
| Lesson | Single-worker baseline | Distributed setup | Cost |
|---|---|---|---|
| Cluster Computing Fundamentals — makespan and total work | single worker makespan 2250, total work 2200 | four workers with subscriber makespan 611 (−1639), total work 2200 (0) |
total work is 2200 in all four setups and does not tell them apart |
| Distributed File Systems — locally read blocks and makespan | single copy 5/22 local, makespan 1850 | replication 3 gives 22/22 (+17), makespan 650 (−1200) | the stored blocks rise from 22 to 66, and a fourth copy adds nothing to locality |
| MapReduce Model — shuffled rows and total work | map shuffled 0, total work 2200 | direct reduction shuffled 1738 (+1738), total work 7414 (+5214) | pre-reduction gives the same result with 20 intermediate rows and a total work of 2245 |
| Distributed Datasets — rows read and partitions opened | eager plan 6556 rows, 44 partitions | lazy plan 200 rows (−6356), 1 partition (−43) | a type error in the plan shows up only on the first action, after 200 rows have been read |
| Resource Managers — makespan and fixed cost paid | single slot makespan 3000, fixed cost 800 | sixteen slots makespan 215 (−2785), fixed cost 800 (0) | when every task is given a slot, the gain from eight to sixteen stays at 121 and is beaten by the added 400 |
| Shuffle Cost — shuffled rows and makespan | shuffled 0 in the subscriber layout, makespan 611 |
shuffling to region moves 1738 (+1738), makespan 1914 (+1303) |
the total work rises from 2200 to 7414; 5214 units are pure movement that does no computation |
| Joins at Scale — join makespan, size of 4000 rows | broadcast 5876 | partitioned 5429 (−447) | the crossover point is 2980, and at network costs 6 and 12 partitioned cannot get ahead at any size |
| Data Skew — skew and makespan | single worker skew 1.00, makespan 2250 | two workers with region skew 1.66 (+0.66), makespan 3691 (+1441) |
at sixteen workers skew 6.88, 13 idle workers; the makespan cannot fall below 589 at any number |
| Caching and Persistence — total of two queries | uncached 5500 | cached 3300 (−2200) | if the cache is not held the total is again 5500, and with a single query the gain is 0 |
| State in Stream Engines — recomputed rows | no checkpoint 1600 | interval 3 gives 400 (−1200) | the certain cost rises from 0 to 800, yet interval 6 gives the same 400 |
| Cost and Cluster Sizing — makespan and total work | single worker makespan 2250, total work 2200 | sixteen workers makespan 1398 (−852), total work 8635 (+6435) | the second worker raises the makespan to 3691; when the fixed cost is counted per worker the gain falls to 102 |
The table is a single rule written eleven times. The number of a distributed job is not one number: the makespan and the total work are measured separately, and a claim of speedup that does not say which was measured is incomplete. The places where the two columns go in opposite directions in the same row are the rule itself: in MapReduce Model the makespan falls while the total work rises from 2200 to 7414, and in this lesson the makespan falls by 852 units while the total work climbs by 6435 units. Cluster Computing Fundamentals shows its mirror: the makespan of the four setups varies between 611 and 2250 while the total work is 2200 in all four, so a report that writes only the total work cannot tell any of these setups apart.
The second claim is read from the same table: adding workers makes the job worse. In Data Skew the
second worker raises the makespan from 2250 to 3691 and carries the total work to 5830; in this lesson
the same step raises the makespan to 2800 even with the subscriber key, whose skew is 1.00. In
Resource Managers the 121 units gained in going from eight to sixteen tasks are beaten by the added 400
units of fixed cost. In Distributed File Systems a fourth copy raises the stored block to 88 and adds
nothing to locality. In Caching and Persistence, in a job with a single query the cache holds 2200 rows
and gains 0, and in State in Stream Engines lowering the interval from six to three raises the certain
cost from 400 to 800 and changes the recovery not at all. In six lessons the added resource was paid
for and nothing was received in return.
The third claim is quieter: the result is the same in every setup. In MapReduce Model the five region totals presented by the three setups are exactly the same, although one spends 7414 and another 2245 of total work. In Shuffle Cost the output does not change while the makespan rises from 611 to 1914, in Joins at Scale the two join strategies match the same rows at every size, in Data Skew the three partitionings produce the same region totals, in Caching and Persistence a cache that is not held changes not the result but only the cost, and in State in Stream Engines all four intervals give the same result as long as exactly-once is achieved. No check that tests correctness shows a wrong partitioning; a wrong partitioning becomes visible only when it is counted, and as long as it is not counted the bill silently grows.
In eleven lessons the data was split, moved, balanced, stored and recovered; in twenty-seven lessons it was ingested, transformed, stored and queued. Where this data goes was never asked. The very model it feeds, whether the same model can be reproduced with the same data, and how a trained model goes to production were not measured in any lesson. The next course takes the output of the pipeline as input and counts the reproducibility, the versioning and the deployment of a training run.
To keep your progress and take notes, Log in
My notes
Log in to take notes.