Sizing and TCO

ch07 · When adding servers stops helping

Builds on ch06.

The question

How far does a system scale, and how would you find out from the three measurements you have?

ch06 ended with a fleet too close to its margin and an obvious remedy: buy more machines. This chapter is about how much less that buys than the arithmetic promises. It is also about the count past which each new machine takes capacity away.

The material

Two costs, and only one of them is famous

Machines do not add up, and there are two separate reasons.

Contention. Some fraction of the work cannot be done in parallel: a lock, a single writer, a shared queue, a coordinator. That fraction takes a fixed share of every machine you add, so the cost grows with the count of machines, and the curve flattens. This is the famous one: Amdahl’s argument, and the ceiling it implies.

Crosstalk. Machines have to agree with each other. Every new one has to be told about all the others, so the cost grows with the number of pairs, not the number of machines.

Crosstalk does something contention never does. Contention flattens the curve. Crosstalk turns it over. Past some count, the next machine costs more in agreement than it brings in work, and the total goes down.

Both terms together are the universal scalability law [Gunther (2008)]:

model.yamlyours to edit
  achievable_throughput:
    kind: derived
    unit: request/second
    label: throughput the fleet can actually reach
    note: >-
      The universal scalability law. One host's throughput times
      the count, divided by what contention and crosstalk take
      back. With crosstalk at zero it is Amdahl's ceiling; with
      both at zero it is the straight line above.
    formula: linear_throughput / (1 + contention * (host_count - 1) + crosstalk * host_count * (host_count - 1))

The curve, against the straight line

Throughput against host countWhat more machines actually buyThe straight line is what a budget assumes. The curve is what the machines dothe budget's line leaves the page at 96 hostspast here it falls264128256400hosts87k0requests per second

The dashed line is what a budget assumes. The solid one is what the machines do. They separate almost immediately. The dashed line leaves the top of the figure while the real curve is still climbing slowly, and then the real curve stops climbing.

HostsThroughputIf scaling were freeEfficiencyPer host
22,4312,44899%1,215.5
44,7944,89698%1,198.5
89,3209,79295%1,165.0
1617,59819,58490%1,099.9
3231,29739,16880%978.0
6449,41778,33763%772.1
9658,924117,50550%613.8
12863,216156,67340%493.9
160 peak64,468195,84133%402.9
20063,701244,80226%318.5
25660,636313,34619%236.9
32056,246391,68314%175.8
40050,830489,60410%127.1
swept peak160 hosts, against 164.4 predicted from the two coefficients

Source — scaling-curve · sizing.evaluate

Read the last column: what each machine is worth. It falls the whole way down. By the peak, a machine contributes a fraction of what the first one did. Every machine after that contributes less than nothing.

The last row checks the peak twice. One figure comes from sweeping the model, the other from its two coefficients. The calculations are independent, and they agree. That agreement is why the closed form in problem 7.3 is worth having.

What doubling buys

OutputReference scenarioTwice the hosts
hosts the model recommends54
20 to 230
54
20 to 230
hosts in the fleet54108
five-year total cost of ownership$2,002,083
$1,508,230 to $2,923,724
$3,155,104
$2,410,453 to $4,636,586
cost per million requests$1.82
$0.55 to $5.98
$2.87
$0.88 to $9.47
cost per stored TB per month$839.64
$305.33 to $2,108
$1,323
$486.37 to $3,333
capex$421,214
$272,130 to $658,773
$842,429
$544,260 to $1,317,546
annual opex$316,174
$227,660 to $484,257
$462,535
$340,638 to $718,208
annual energy206,269
159,636 to 269,269
412,539
319,271 to 538,539
utilisation at the busy hour0.644
0.156 to 2.48
0.322
0.0780 to 1.24
utilisation with one host down0.656
0.159 to 2.53
0.325
0.0788 to 1.25
working set against memory0.746
0.194 to 2.67
0.373
0.0968 to 1.34
disk fill at horizon0.670
0.213 to 2.12
0.335
0.107 to 1.06
fraction of the fleet doing nothing useful0.321
0.213 to 0.457
0.539
0.399 to 0.692
utilisation, counting coordination0.948
0.233 to 3.81
0.698
0.171 to 2.95
utilisation0.644
0.156 to 2.48
0.322
0.0780 to 1.24
residence time0.0367
0.0128 to 0.857
0.0193
0.0113 to 0.735
time spent queueing0.0236
0.0021 to 0.839
0.0062
0.0010 to 0.720
requests in the system1,562
160 to 107,336
820
146 to 107,336
requests in flight, if none waited556
135 to 2,147
556
135 to 2,147
how much the queueing view understated it1.47
1.27 to 1.84
2.17
1.66 to 3.25
fraction of the peak already built0.328
0.186 to 0.584
0.657
0.371 to 1.17
working set against memory — over its limit35%10%
utilisation, counting coordination — over its limit48%35%
disk fill at horizon — over its limit29%6%
utilisation with one host down — over its limit30%9%
utilisation at the busy hour — over its limit30%8%
fraction of the fleet doing nothing useful — over its limit0%0%

Source — web_service-reference and web_service-twice_the_hosts · every input on a slider

Twice the hosts. Read down.

Utilisation halves, exactly as arithmetic says it should. Time spent queueing falls to about a quarter, because ch06’s division is not linear, and in this direction the non-linearity runs in your favour. The share of futures over the knee falls by more still.

Throughput goes up by about a third, for a doubling of the fleet. Efficiency falls by about a third at the same time. That is the same fact, counted from the other end.

So doubling a fleet is an excellent way to fix latency and a poor way to buy capacity. Those are different purchases. They are usually conflated, and the model tells them apart.

Both are in the graph, and so is the peak. Drag crosstalk and watch the peak move while the fleet you have stays where it is.

The graph as ch07 leaves it. Two ceilings arrived with it: one on what the fleet spends on itself, one on the utilisation the queueing view understated.

The utilisation you were quoted was optimistic

ch06 had no scaling term, so its utilisation was the work arriving divided by what the machines could do if each of them worked alone. They do not work alone. Some of their capacity is spent on each other, and the honest utilisation is the arriving work divided by what the fleet can deliver.

The model carries both numbers, side by side, on purpose:

model.yamlyours to edit
  utilisation_including_coordination:
    kind: derived
    unit: dimensionless
    label: utilisation, counting coordination
    note: >-
      The same fleet's utilisation, worked out from what it can
      actually deliver rather than from what one host could deliver
      alone. It is higher than the row above, always, and the
      gap is the work the hosts are doing on each other's behalf.
    formula: peak_request_rate / achievable_throughput

At the reference point the two differ by half again. The queueing view is not wrong. It is optimistic, by a factor nobody notices until they measure the fleet at two sizes and find the second one disappointing.

Measuring the fleet at more than one size is also how the coefficients get fitted.

Change any block above, then press Run. The first press fetches Python.

The same file, running. Double the contention and watch where the peak goes: it is the software’s number, and no host count in the file moves it.

Fitting the coefficients from what you have

Three unknowns, so three measurements determine them exactly. You will usually have three: one machine on a bench, the fleet you are running, and the fleet you were running before you grew it. That is not much data, and it is what exists.

Problem 7.2 is the algebra. Do it by hand once. Rearranging the law into a straight line shows why three points are the minimum, and why they must be at different counts. Two measurements at the same size determine nothing at all.

Then notice what you have done. You have extended a two-parameter curve out to hundreds of machines from three points clustered at the low end, and you are about to spend money on the extrapolation. The coefficients in this book’s model are assumptions, and they say so in their provenance. The shape is the claim. The position of the peak is a guess.

Key takeaways

  • Machines do not add up, for two separate reasons. Contention takes a fixed share of every machine you add and flattens the curve. Crosstalk grows with the number of pairs and turns the curve over.

  • Past the peak, the next machine takes capacity away. Each machine is worth less than the one before it, and after the peak less than nothing.

  • Doubling the fleet fixes latency and buys little capacity. Utilisation halves, queueing time falls to about a quarter, and throughput rises by a fraction. Those are different purchases.

  • The utilisation a queueing view quotes is optimistic. Some of every machine’s capacity is spent on the others, so the honest figure divides by what the fleet can deliver.

  • The shape of the curve is the claim. The position of the peak is a guess. Three measurements at different sizes fit the coefficients exactly, and extrapolating them to hundreds of machines is the bet you are placing.

What this cannot tell you

Where your peak is. The coefficients here are assumptions, and the peak follows from them. The model’s own interval on the peak spans more than a factor of three. Fitting the coefficients from three measurements gives numbers with the same problem and a false air of precision. What transfers is that a peak exists, and that it is a property of the software.

Whether the coefficients are stable. They are fitted from a system doing one kind of work at one size. A different workload mix has a different serial fraction. A version that adds a coordination round has different crosstalk. A curve fitted last year describes last year’s software.

What to do about it. The law says where scaling stops paying. It has nothing to say about which lock to remove, and removing the lock changes the coefficients in a way only another measurement can establish.

Anything about failure. Every figure above is a healthy fleet. Machines coordinating while one of them is unreachable behave differently and worse, and this model has no term for it. ch11 is where that gets a margin rather than a model.

Whether the fleet is even the constraint. The whole chapter assumes throughput is what you are buying. If the system is bounded by something else, a database, a licence or a single-threaded step, the curve above describes a queue in front of the real problem.

Problems

Four, in tests/when_adding_servers_stops_helping/. The first three have tests. The last does not, and says why.

7.1 — Write the law. Two terms in the denominator, behaving differently. The tests check that contention alone flattens the curve and that crosstalk alone turns it over, so the two cannot stand in for each other.

tests/when_adding_servers_stops_helping/stubs.py · throughputyours to edit
def throughput(
    hosts: np.ndarray | float, one_host: float, contention: float, crosstalk: float
) -> np.ndarray:
    """Problem 7.1 - the scalability law, written out.

    ``one_host`` is what a single machine achieves alone. ``contention`` is the fraction of the
    work that cannot be done in parallel. ``crosstalk`` is the cost of machines having to agree
    with each other. Return the throughput of ``hosts`` machines.

    Two terms in the denominator and they behave differently, which is the whole of the chapter:

    * contention costs a fixed share of every machine you add - it grows with the *count*, and it
      flattens the curve;
    * crosstalk is machines coordinating with each other, so it grows with the number of *pairs* -
      and it is what makes the curve turn over and come back down.

    With crosstalk at zero you have Amdahl's ceiling: the curve flattens and stays flat. With both
    at zero you have the straight line nobody has ever measured.

    The test checks it against the sweep the book publishes, so it has to be the model's formula
    and not one that happens to be close near the middle.
    """
    raise NotImplementedError("problem 7.1")

The same check at a desk: python3 -m pytest tests/when_adding_servers_stops_helping/test_problem_1_law.py -m problem

7.2 — Fit it from three measurements. Rearrange the law until it is linear, then solve. Write the rearrangement down before you code it.

7.3 — Find the peak. Differentiate and set to zero. It comes out as a square root, and it says the peak belongs to the software rather than to the budget. Handle zero crosstalk honestly: there is no peak, and a large number is not the same answer.

Both are checked by one file, because the peak is what the fit is for:

tests/when_adding_servers_stops_helping/stubs.py · fityours to edit
def fit(measurements: list[tuple[float, float]]) -> tuple[float, float, float]:
    """Problem 7.2 - the two coefficients, from measurements somebody could actually take.

    ``measurements`` is a list of ``(hosts, throughput)`` pairs. You will usually have three: one
    machine, the fleet you have, and the fleet you had before you grew it. That is not much
    data and it is what exists.

    Return ``(one_host, contention, crosstalk)``.

    Three unknowns, so three measurements determine them exactly - solve, do not optimise. With
    the one-machine measurement in hand, ``one_host`` falls straight out, and the remaining two
    come from two linear equations once you rearrange the law to put the denominator on the other
    side. Write the rearrangement down before you code it; it is the part worth understanding.

    Then notice what you have done. You have fitted a two-parameter curve, extending to hundreds
    of machines, from three points clustered at the low end. ch07 says what that is worth.
    """
    raise NotImplementedError("problem 7.2")
tests/when_adding_servers_stops_helping/stubs.py · peak_hostsyours to edit
def peak_hosts(contention: float, crosstalk: float) -> float:
    """Problem 7.3 - where adding machines stops helping.

    Return the host count at which throughput is greatest, from the two coefficients alone.

    Differentiate your answer to 7.1 with respect to the host count and set it to zero. It comes
    out as a square root and it is worth the five minutes: it says the peak is a property of the
    *software* - of how much it serialises and how much it coordinates - and that no budget moves
    it.

    Handle zero crosstalk. With no coordination cost the curve never turns over, and the honest
    answer is that there is no peak: raise, or return an infinity. A large number is not the
    same answer.
    """
    raise NotImplementedError("problem 7.3")

The same check at a desk: python3 -m pytest tests/when_adding_servers_stops_helping/test_problem_2_fit.py -m problem

7.4 — The time adding machines did not help. No test: the measurements are your fleet’s, and this repository has none of them.

Somewhere in your organisation there is a tier that got a bigger fleet and did not get proportionally faster. Find its throughput at two fleet sizes, and a single machine’s on a bench if anyone ever ran one. Those are the three measurements the chapter says everyone has. Run problem 7.2 on them and write down where the peak comes out.

Then write down which of this chapter’s two costs the numbers point at: contention for something shared, or the cost of machines agreeing with each other. If the fit says neither, you have found a third mechanism, which is more interesting than the chapter.

A good answer names the tier, the fleet sizes and their throughputs, the peak the fit implies, and why you would not spend money on that peak. If you cannot find throughput at two sizes, that is an answer too. It means nobody measured throughput per host when the fleet grew, and the next growth is the chance to.

Where to go next

Gunther’s paper [Gunther (2008)] derives the law from a queueing argument rather than by fitting a curve to data. Read it if the crosstalk term has so far felt like a free parameter.

ch08 is what this chapter and the last one have in common: a point where the chain of multiplications stops describing the system, and no care over the inputs would have warned you.