Sizing and TCO

Appendix E · The web service model, in full

A web service and the data it keeps, on a fleet of Linux hosts, sized from a stated busy hour and costed over five years. No product is named and none is implied: what is on this page is a structure, and the numbers in it are placeholders for yours.

This is the model the chapters build a few nodes at a time, and it is a sizing model by the rule and by its shape. Most of it is accounting identity and physics: requests times CPU time per request, watts times hours times price, capital plus running cost over a horizon. That is the structure the introduction says sampling the inputs is sufficient for. But it carries one measured constant and six ceilings, so scripts/verify-models.py classifies it a sizing model and holds it to the stricter rules. That is the right call: a compression ratio belongs to a codec, and a fleet asked for more than it can serve does not slow down proportionally. Appendix F is the book’s second model, and shows what one looks like when a whole chain of it has not been measured.

The graph

Try it

The same model, as an interactive page: every slider comes from a range this model file declares, moving one recomputes the whole graph immediately, and clicking any node shows what fed it and how uncertain it is. Resample runs the book’s own sampler in your browser with the inputs you have moved held at their values.

A web service and its data, on a fleet of Linux hosts — dependency graphinputderivedmeasuredceilingyou decidethroughput the fleet canactually reachannual energyannual energy costannual growth factorannual licencesannual opexannual staff costannual supportmean request rate over thehorizonaverage storedcores busy at the busyhourlimit on working setagainst memorycache margincapexrequests in the systemcontentionlimit on utilisation,counting coordinationcores in the fleetcores per hostcost per million requestscost per stored TB permonthcrosstalklimit on disk fill athorizondisk margindisk per hosteffective utilisationelectricity pricefacility powerlimit on utilisation withone host downmemory the service canuse, whole fleetfully loaded salaryfraction of the peakalready builthorizonhorizon periodshost capexhost counthost powerhost pricehosts in the fleethosts left when one dieshosts for memoryhosts for requestshosts for storagehosts the model recommendsshare of records touchedin a busy hourhours per yearrequests in flight, ifnone waitedindex overheadinstalled disklicence per corelicence per hostlifecycle opexthroughput if scaling werefreemean request rate athorizonone-off cost of moving tothis designnetwork capexnetwork price per hostone coreone hostone requestone yearhow much the queueing viewunderstated itos reservewhere adding hosts stopshelpingpeak request rate athorizonpeak request rate, day onepeak-to-mean ratioPUElimit on utilisation atthe busy hourqueueing marginmemory the service canuse, per hostram per hostraw disk needed at horizonraw bytes per stored byterecord compression ratioreplication factorrequests over horizonresidence timescaling efficiencylimit on fraction of thefleet doing nothing usefulseconds per yearCPU time per requestservice timethroughput of one hostaloneengineers, full-timeequivalentrecords held at horizonrecords held, day onesupport ratefive-year total cost ofownershiputilisationutilisation with one hostdownutilisation the model willadmit toutilisation, countingcoordinationtime spent queueingworking set at horizon

Colour is kind: inputs, derived quantities, the one measured constant, and the ceilings. An input’s border says what it is claiming: solid for a fact, dashed for a vendor’s claim, dotted for an assumption. Arrows run from cause to effect, and every node sits immediately to the right of the last thing it depends on.

Read two things off it directly. The graph is wide at the left and narrow at the right: many quantities collapsing into a handful of answers, which is what makes a single wrong input so hard to spot downstream. And three separate chains reach the host count: the request rate, the working set and the data on disk. That is ch10’s whole subject.

Every formula

The graph above, as text: every derived quantity and every ceiling, with the formula the file gives it and the chapter that added it. The table is rendered from the file, so it cannot disagree with it.

QuantityFormulaUnitIntroduced in
horizon periods (horizon_periods)horizon / one_yearch02
peak request rate at horizon (peak_request_rate)peak_request_rate_t0 * annual_growth ** horizon_periodsrequest/secondch02
records held at horizon (stored_data)stored_data_t0 * annual_growth ** horizon_periodsTBch02
memory the service can use, per host (ram_for_service)ram_per_host * (1 - os_reserve)TB/hostch03
mean request rate at horizon (mean_request_rate)peak_request_rate / peak_to_meanrequest/secondch04
cores in the fleet (cores)hosts * cores_per_hostcorech05
cores busy at the busy hour (busy_cores)peak_request_rate * service_demandcorech05
utilisation (utilisation)busy_cores / coresch05
service time (service_seconds)service_demand * one_request / one_coresecondch05
requests in flight, if none waited (in_flight_unqueued)peak_request_rate * service_secondsrequestch05
effective utilisation (effective_utilisation)min(utilisation, utilisation_cap)ch06
residence time (residence_time)service_seconds / (1 - effective_utilisation)secondch06
time spent queueing (waiting_time)residence_time - service_secondssecondch06
requests in the system (concurrency)peak_request_rate * residence_timerequestch06
utilisation at the busy hour (queueing_headroom)utilisation against a limit of 1, keeping queueing_margin below itch06
host count (host_count)hosts / one_hostch07
throughput of one host alone (single_host_throughput)cores_per_host / service_demandrequest/second/hostch07
throughput if scaling were free (linear_throughput)hosts * single_host_throughputrequest/secondch07
throughput the fleet can actually reach (achievable_throughput)linear_throughput / (1 + contention * (host_count - 1) + crosstalk * host_count * (host_count - 1))request/secondch07
scaling efficiency (scaling_efficiency)achievable_throughput / linear_throughputch07
fraction of the fleet doing nothing useful (scaling_loss)1 - scaling_efficiency against a limit of 1, keeping 0.5 below itch07
where adding hosts stops helping (peak_hosts)one_host * sqrt((1 - contention) / crosstalk)hostch07
fraction of the peak already built (headroom_to_peak)host_count / (peak_hosts / one_host)ch07
utilisation, counting coordination (utilisation_including_coordination)peak_request_rate / achievable_throughputch07
how much the queueing view understated it (optimism)utilisation_including_coordination / utilisationch07
utilisation, counting coordination (coordination_headroom)utilisation_including_coordination against a limit of 1, keeping queueing_margin below itch07
working set at horizon (working_set)stored_data * hot_fractionTBch08
memory the service can use, whole fleet (fleet_ram)hosts * ram_for_serviceTBch08
working set against memory (cache_fill)working_set / fleet_ram against a limit of 1, keeping cache_margin below itch08
raw bytes per stored byte (raw_per_stored)replication_factor * index_overhead / record_compressionch09
raw disk needed at horizon (raw_data)stored_data * raw_per_storedTBch09
hosts for storage (hosts_for_storage)ceil(raw_data / (disk_per_host * (1 - disk_margin)))hostch09
installed disk (installed_disk)hosts * disk_per_hostTBch09
disk fill at horizon (disk_fill)raw_data / installed_disk against a limit of 1, keeping disk_margin below itch09
hosts for requests (hosts_for_requests)ceil(busy_cores / (cores_per_host * (1 - queueing_margin)))hostch10
hosts for memory (hosts_for_memory)ceil(working_set / (ram_for_service * (1 - cache_margin)))hostch10
hosts the model recommends (hosts_recommended)max(hosts_for_requests, hosts_for_memory, hosts_for_storage)hostch10
hosts left when one dies (hosts_after_failure)hosts - one_hosthostch11
utilisation with one host down (utilisation_after_failure)busy_cores / (hosts_after_failure * cores_per_host)ch11
utilisation with one host down (failure_headroom)utilisation_after_failure against a limit of 1, keeping queueing_margin below itch11
host capex (host_capex)hosts * host_priceUSDch18
network capex (network_capex)hosts * network_price_per_hostUSDch18
capex (capex)host_capex + network_capexUSDch18
facility power (facility_power)hosts * host_power * puekWch18
annual energy (annual_energy)facility_power * hours_per_yearkWh / yearch18
annual energy cost (annual_energy_cost)annual_energy * electricity_priceUSD / yearch18
annual licences (annual_licences)cores * licence_per_core + hosts * licence_per_hostUSD / yearch18
annual support (annual_support)capex * support_rateUSD / yearch18
annual staff cost (annual_staff_cost)staff_fte * fully_loaded_salaryUSD / yearch18
annual opex (annual_opex)annual_energy_cost + annual_licences + annual_support + annual_staff_costUSD / yearch18
lifecycle opex (lifecycle_opex)annual_opex * horizonUSDch18
five-year total cost of ownership (tco)capex + lifecycle_opex + migration_costUSDch18
mean request rate over the horizon (average_request_rate)(peak_request_rate_t0 / peak_to_mean + mean_request_rate) / 2request/secondch18
requests over horizon (requests_over_horizon)average_request_rate * seconds_per_year * horizonrequestch18
cost per million requests (cost_per_million_requests)tco / requests_over_horizonUSD/megarequestch18
average stored (average_stored)(stored_data_t0 + stored_data) / 2TBch18
cost per stored TB per month (cost_per_stored_tb_month)tco / average_stored / horizonUSD / TB / monthch18

Source — models/web_service/model.yaml and its build order

The outputs

OutputPoint estimate90% intervalUnit
hosts the model recommends5420 to 230host
hosts in the fleet54fixedhost
five-year total cost of ownership$2,002,083$1,508,230 to $2,923,724USD
cost per million requests$1.82$0.55 to $5.98USD/megarequest
cost per stored TB per month$839.64$305.33 to $2,108USD / TB / month
capex$421,214$272,130 to $658,773USD
annual opex$316,174$227,660 to $484,257USD / year
annual energy206,269159,636 to 269,269kWh / year
utilisation at the busy hour0.6440.156 to 2.48
utilisation with one host down0.6560.159 to 2.53
working set against memory0.7460.194 to 2.67
disk fill at horizon0.6700.213 to 2.12
fraction of the fleet doing nothing useful0.3210.213 to 0.457
utilisation, counting coordination0.9480.233 to 3.81
utilisation0.6440.156 to 2.48
residence time0.03670.0128 to 0.857second
time spent queueing0.02360.0021 to 0.839second
requests in the system1,562160 to 107,336request
requests in flight, if none waited556135 to 2,147request
how much the queueing view understated it1.471.27 to 1.84
fraction of the peak already built0.3280.186 to 0.584

Source — web_service-reference · every input on a slider

Where each ceiling sits

CeilingAt the planHeadroomAllowedLimitVerdictOver allowedOver limit
working set against memory0.7525%0.751.00ok49%35%
utilisation, counting coordination0.9530%0.701.00into the margin65%48%
disk fill at horizon0.6725%0.751.00ok44%29%
utilisation with one host down0.6630%0.701.00ok47%30%
utilisation at the busy hour0.6430%0.701.00ok46%30%
fraction of the fleet doing nothing useful0.3250%0.501.00ok1%0%

Source — web_service-reference · every input on a slider

The verdict column is about the plan; the last two columns are about the world. A design that is under its hard limit and inside the margin it declared has not failed. It has spent the reserve it was keeping for the failure it has not had yet (ch11).

What moves the answer

Tornado for five-year total cost of ownershipfive-year total cost of ownership (USD) — point estimate $2,002,083and 10 that do not move it at allengineers, full-time equivalent$628,081licence per core$518,400fully loaded salary$453,590host price$429,235support rate$129,049electricity price$128,918network price per host$77,059host power$49,271PUE$32,940
InputKindfive-year total cost of ownership at its p10at its p90Swing
engineers, full-time equivalentinput$1,732,449$2,360,530$628,081
licence per coreinput$1,829,283$2,347,683$518,400
fully loaded salaryinput$1,805,056$2,258,646$453,590
host priceinput$1,827,097$2,256,332$429,235
support rateinput$1,945,271$2,074,320$129,049
electricity priceinput$1,951,956$2,080,874$128,918
network price per hostinput$1,969,541$2,046,600$77,059
host powerinput$1,979,689$2,028,960$49,271

Source — web_service-reference · every input on a slider

Each bar swings one input across the middle of its own declared range with everything else held still. The ordering is the useful part: it says which input to go and measure first (ch19). It is also a one-at-a-time analysis, so an input whose effect only appears in combination with another gets a short bar here and can still be the thing that sinks you.

Every bar is a cost input, and the demand side is not here at all. No request rate, no growth rate, no records held. That is not an omission in the chart. The total prices the fleet that was bought, and the fleet is hosts — a number somebody decided, not one the model derived. More data does not buy more hosts by itself; it makes the fleet you have too small, and the model says so through the disk and cache ceilings rather than through the bill. Drag records held, day one on the interactive page and watch the recommendation and those two ceilings move while the total sits still. The panel names, for any input, which outputs it can move and which it cannot.

To see demand move the price you re-decide the fleet, which is what the second scenario below does.

The answer as a distribution

Distribution of cost per million requestscost per million requests — 100,000 samples90% interval $0.55 to $5.98 · median $1.90p5pointp95$0.07$4.88$9.700.9% of samples run on to $25.76

Unit cost rather than total cost, because it is the figure that behaves least like people expect. A fleet bought for growth that then arrives is cheap per request; the same fleet is expensive if the growth never comes. The distribution carries both futures, and a single number carries neither.

Where the inputs came from

InputProvenanceSource
annual growth factorassumptionch04 — lognormal because growth compounds and cannot be negative. The p10/p90 say: surprised below 12% a year, surprised above 60%. One factor for requests and for records, because users drive both.
cache marginassumptionch08 — how much of the fleet’s memory is kept free of the working set, for its own daily swing and for the process heaps. Declared once and used by the chain and by the ceiling
contentionassumptionch07 — fitted from two measurements, where two exist. Triangular because a fit gives a central value and a range rather than a shape, and its maximum is the claim to distrust: a serial fraction can always be worse than the one you measured
cores per hostvendor claimspec sheet: physical cores. A hyperthread is not a core, and a sheet that counts threads doubles this number without doubling the work a host does (ch07)
crosstalkassumptionch07 — fitted, and the harder of the two to fit. Lognormal because it spans an order of magnitude and cannot be negative, and because a shape should not assert a hard upper bound on a quantity this weakly fitted
disk marginassumptionch11 — room to re-replicate a dead host’s records onto the survivors, plus what a filesystem needs to keep allocating well. Declared once and used by the chain and by the ceiling
disk per hostvendor claimspec sheet: one local drive. Decimal TB, not TiB — appendix D, and it is a 10% difference
electricity priceassumptionall-in delivered rate including transmission. Lognormal: it cannot go negative and its history is multiplicative
fully loaded salaryassumptionsalary, employer costs, tooling and overhead. Lognormal because pay is right-skewed and cannot go negative; the p90 is a senior engineer in an expensive city
horizonassumptionthe refresh cycle this fleet is bought against
host powervendor claimtypical draw under load, per host as configured. Triangular, and one of the few inputs here whose bounds are physical rather than editorial: a host cannot draw less than it idles at, or more than its supply will give it
host pricevendor claimchassis, CPU, memory, drives and boot media, as configured. Lognormal like any price, and wide because a host is a configuration rather than a commodity
hosts in the fleetassumptionthe sizing decision, taken the way it is usually taken: hosts_recommended evaluated at every input’s point estimate. Change this number and watch the ceilings move — that is the exercise of ch12
share of records touched in a busy hourassumptionch08 — the working set as a share of everything held. Triangular; nobody measures this and everybody has an opinion, and the maximum is a service whose users all look at the same week’s data
hours per yearfactby definition, 365.25 x 24. The quarter-day is worth about a fifth of a per cent over five years — less than this model’s other errors, and free to get right
index overheadassumptionindexes, the write-ahead log and journals as a multiplier on stored bytes. Triangular, and the bounds are for a service with a few indexes per table — a search-heavy one is off the top of this range
licence per corevendor claimthe platform software’s per-core licence, as quoted. Lognormal: a price, and a wide one, because it is the line item most often negotiated. It is what makes the core count a cost as well as a capacity (ch17)
licence per hostvendor claimthe price list: per core, with no per-host charge. A quote that licenses per host instead puts its figure here and zero in licence_per_core, and the two totals are then compared like for like
one-off cost of moving to this designassumptionnothing to migrate: the platform the plan already runs on, on the hosts already quoted. A challenger’s scenario overrides this with the team’s own estimate of the move, marked as what it is
network price per hostassumptionswitch ports, optics and cabling, amortised per host. Triangular rather than lognormal, although it is a price: it is a bill of materials divided by a host count somebody chose, so the bounds are the plausible designs rather than a market
one corefactdefinition
one hostfactdefinition
one requestfactdefinition
one yearfactdefinition
os reserveassumptionthe share of memory the kernel, the agents and the page cache floor keep before the service sees any. Triangular: a floor, a usual figure, and a host with too many agents on it
peak request rate, day oneassumptionch04 — the busy hour, not the daily mean. Triangular because this is an engineer’s min/likely/max and pretending to more shape than that would be invention.
peak-to-mean ratioassumptionch04 — the busy hour against the daily mean. Triangular, and it is a property of your traffic that belongs to the estate target: nothing here can measure it
PUEassumptionch16 — facility overhead. A multiplier on IT load, and the single number a colocation contract is most likely to disagree with you about. Triangular: the minimum is a good building, the maximum is a poor one, and below one is impossible
queueing marginassumptionch06 — how far under the knee the fleet is sized to run at the busy hour. Declared once, here, and used by the sizing chain and by the ceiling that checks it, so the two cannot drift apart
ram per hostvendor claimspec sheet: the modules fitted. The sheet says 64 GB and means GiB — appendix D — and the operating system will report less than either, which is os_reserve’s job
replication factorassumptionthree copies of every record, so that a host can die and take its disks with it. A different durability scheme substitutes its own factor here and the rest of the model is unchanged, which is the point of it being a node
seconds per yearfactby definition, 365.25 x 86,400
CPU time per requestassumptionheld as an assumption because no reference machine is declared in rig/machine.yml; make measure-rig on a declared machine replaces this node with a measured one. Triangular because it has not been measured: once it is, the shape becomes a normal around the measurement, which is a change of claim and not only of numbers (ch13)
engineers, full-time equivalentassumptionengineers this fleet occupies, full-time equivalent. Triangular, and the shape cannot express what actually happens: people are not divisible, so the real distribution is lumpy in the way ch08 calls a regime change
records held, day oneassumptionstated workload (ch02) — what the service holds today: its database and the objects users have uploaded, before replication, indexes or compression
support ratevendor claimannual support as a fraction of capital cost. Triangular because it is negotiated inside a band the market sets rather than drawn from one: the spread is what different buyers get, not what varies from year to year
utilisation the model will admit toassumptionwhere this model stops being about queues (ch06)
37 inputs6 fact, 8 vendor claim, 23 assumption

Source — web_service-reference · every input on a slider

ConstantValueStandard errorUnitMeasured against
record compression ratio3.58± 0.0009python zlib (DEFLATE level 6)

Source — web_service-reference · every input on a slider

One measured constant, and its conditions are on the row: a compression ratio belongs to a codec and a body of data, and this one was measured over a synthetic corpus of application records that this repository generates. The method transfers; the number does not. Point the runner at a sample of your own records (ch03).

Two scenarios, side by side

OutputReference scenarioSized for the growth we might get, not the growth we expect
hosts the model recommends54
20 to 230
54
20 to 230
hosts in the fleet54131
five-year total cost of ownership$2,002,083
$1,508,230 to $2,923,724
$3,646,206
$2,781,458 to $5,393,738
cost per million requests$1.82
$0.55 to $5.98
$3.32
$1.02 to $10.96
cost per stored TB per month$839.64
$305.33 to $2,108
$1,529
$561.50 to $3,859
capex$421,214
$272,130 to $658,773
$1,021,835
$660,167 to $1,598,135
annual opex$316,174
$227,660 to $484,257
$524,874
$386,622 to $822,825
annual energy206,269
159,636 to 269,269
500,394
387,264 to 653,228
utilisation at the busy hour0.644
0.156 to 2.48
0.265
0.0643 to 1.02
utilisation with one host down0.656
0.159 to 2.53
0.267
0.0648 to 1.03
working set against memory0.746
0.194 to 2.67
0.307
0.0798 to 1.10
disk fill at horizon0.670
0.213 to 2.12
0.276
0.0879 to 0.874
fraction of the fleet doing nothing useful0.321
0.213 to 0.457
0.604
0.464 to 0.753
utilisation, counting coordination0.948
0.233 to 3.81
0.671
0.163 to 2.90
utilisation0.644
0.156 to 2.48
0.265
0.0643 to 1.02
residence time0.0367
0.0128 to 0.857
0.0178
0.0110 to 0.619
time spent queueing0.0236
0.0021 to 0.839
0.0047
0.0008 to 0.606
requests in the system1,562
160 to 107,336
757
144 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.53
1.86 to 4.05
fraction of the peak already built0.328
0.186 to 0.584
0.797
0.450 to 1.42
working set against memory — over its limit35%6%
utilisation, counting coordination — over its limit48%34%
disk fill at horizon — over its limit29%3%
utilisation with one host down — over its limit30%5%
utilisation at the busy hour — over its limit30%5%
fraction of the fleet doing nothing useful — over its limit0%0%

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

The left column buys what the point estimates recommend. The right buys for the growth case the model thinks is plausible but not expected. The difference in capital is a number, and so is the difference in how often each ceiling breaks. Choosing between them is a judgement somebody has to make and defend, which is ch21 · A TCO for a finance audience.

What this cannot tell you

Whether the fleet is the right one. The model prices hosts, and hosts is a decision. Every figure on this page is conditional on it, and nothing here argues for the number: the reference scenario takes the recommendation at the point estimates, which is how the decision is usually taken and is not the same as it being right. The ceilings say whether that fleet survives the model’s futures; they cannot say whether a different fleet would have been a better buy. That comparison is a second scenario and a judgement, which is ch21.

What the structure leaves out. The chains here are demand, memory and disk, and they meet only at the host count. Nothing connects a request to the record it writes, so a model where traffic and data grow together cannot be told from one where they do not — and this one assumes they do, in a single correlation, rather than deriving it. There is no tier that fails differently from the others, no request that costs more than its neighbour, and no second site. Sampling the inputs harder will not find any of that: an omission is not a wide interval, it is a chain that was never drawn (ch20).

Whether five years is the right horizon. It is a decision too, and it sets how much of the total is capital and how much is running cost. A different horizon does not just scale the answer; it changes which half of the bill the argument is about (ch15).

Running it yourself

python3 -m bench.run_models --model web_service     # evaluate, sample, stamp
python3 scripts/verify-models.py                    # units, provenance, ceilings, shape
python3 scripts/render-figures.py                   # re-render every figure above
python3 -m pytest tests/test_models.py              # the reference outputs, asserted

Every figure on this page came from bench.run_models. verify-models.py refuses a model that does not typecheck, and tests/test_models.py is what stops any of it changing silently.