01

Reliable, scalable, maintainable applications

Most applications today are limited by data (how much there is, how complex it is, how fast it changes) rather than by CPU. They are built by combining databases, caches, indexes, stream and batch processors, and they are judged on three properties. Reliability: keep working when hardware, software or people fail. Scalability: have a plan for when load grows, which means describing load and performance in numbers. Maintainability: let the people who operate and change the system keep doing so productively. This chapter turns those words into things you can measure, such as fault rates, fan-out and percentiles.
A fleet of disks, each failing at random with a given mean time to failure. At this scale something breaks every few days. Redundancy turns independent hardware faults into non-events. Correlated software bugs and fleet-wide human mistakes get past it.
copies of each item
Inject a fault

A fleet of 1,200 disks

day 0
uphardware faultchaos-injectedsoftware bughuman errorgroup lost data
  • no incidents yet

Each disk fails independently with probability 1/MTTF per day and is replaced after the repair time. Try: play with 1 copy and watch every hardware fault become data loss; switch to 3 copies and the same faults are masked. Then press "software bug": a correlated fault takes down all replicas at once.

disk faults / day
0.00
expected n ÷ MTTF = 0.33
disks down now
0
hardware faults
0
+ 0 injected
data-loss failures
0
every copy gone at once
availability
100.000%
share of group-days with ≥1 copy up
at 10,000 disks
2.7/day
faults are routine, not exceptional

Describing load: Twitter's home timeline

scalabilityload-parametersfan-out-timeline
Posting a tweet is cheap; building a home timeline is not. You either merge followees' tweets at read time, or push every tweet into each follower's cached timeline at write time. Which one wins depends on a load parameter, the distribution of followers per user.

Toy network: click a left dot to tweet, a right dot to load a home timeline

tweets byhome timeline of@stella · 13@stella follows 3@kai · 7@kai follows 3@mo · 7@mo follows 3@ana · 4@ana follows 3@li · 4@li follows 3@raj · 2@raj follows 3@zoe · 2@zoe follows 3@ben · 0@ben follows 3@ivy · 0@ivy follows 3@omar · 0@omar follows 3@eli · 0@eli follows 3@sam · 0@sam follows 3@noa · 2@noa follows 3@tom · 1@tom follows 3
@stella tweets (fan-out on write): 14 writes (1 tweet + 1 per follower's cache)

Amber dots are celebrities (≥6 followers); in hybrid mode their tweets are not fanned out (dashed) and are merged in at read time. Try: tweet as the biggest account in each mode, then raise the skew and run traffic.

Toy: cumulative operations

fan-out on read
0
0w · 0r
fan-out on write
0
0w · 0r
hybrid
0
0w · 0r
most-followed account
13 of 13
At Twitter scale

Operations per second: 100M users × 200 follows, 4.6k tweets/s, 300k timeline reads/s

fan-out on read
60.0M
5kw · 60.0Mr
fan-out on write
1.2M
925kw · 300kr
hybrid
1.2M
924kw · 318kr
05101k10k100k1.0M10.0M100.0Mcelebrity threshold (followers)hybrid ops/s (M)pure fan-out on write

Fan-out on read costs 300k × 200 = 60M lookups/s. Fan-out on write moves the work to tweet time, which is cheaper on average because reads outnumber tweets, but one celebrity tweet becomes tens of millions of inserts. Try: set skew to 0 (no celebrities), then to 0.75 and watch the worst-case delivery time.

largest account
2.3M followers
celebrity accounts
4
0.0% of all follows point at them
worst tweet delivery, write
2.3 s
at 1M timeline inserts/s
worst tweet delivery, hybrid
1.0 s
lookups per timeline load
1.1 (hybrid)
vs 200 with fan-out on read, 1 on write

Describing performance: response-time percentiles

response-timepercentilessla-slo
Run the same request over and over and you get a distribution of response times, not a single number. Watch a live stream of requests: the mean is pulled up by rare stalls, the median shows the typical case, and the high percentiles show the tail.

Response-time distribution, last 2,000 requests (log scale)

1ms10ms100ms1s10sSLOmeanp50p95p99p999

Each bar is one request (newest on the right)

median

Even with identical requests, response time varies: context switches, packet loss, GC pauses, cache misses. Try: raise the stalls to 3% and watch the mean jump while the median barely moves; the p99 and p999 are what your heaviest users (often your best customers) feel.

mean
54 ms
hides how many users waited
median (p50)
40 ms
half the requests are faster
p95
94 ms
p99
374 ms
SLO violated
p999
1,315 ms
1 in 1,000
slower than 300 ms
1.1%

Tail latency amplification

tail-latency-amplification
One page view often calls many backend services in parallel and has to wait for all of them. Even when only a small share of backend calls are slow, the share of slow user requests grows quickly with fan-out.
hedged requests

One user request fans out; it is as slow as its slowest call

user request30 ms1091230151121161224121716118139111314

Red = slow call (>200 ms), purple = rescued by a hedged backup request. Try: keep 1% slow calls and drag fan-out to 100; most user requests become slow even though 99% of backend calls are fast.

P(user request is slow) = 1 − (1 − p)^N

p = 1.0%hedged (≈p²)
0%50%100%20406080100backend calls N
slow user requests (theory)
18.2%
slow user requests (measured)
0.0%
0 of 1 requests
user p50
30 ms
user p99
30 ms
one backend call slow
1.00%
looks negligible on a dashboard

Queueing delay and head-of-line blocking

head-of-line-blockingthroughputresponse-time
A server handles only a few requests at once. A couple of slow ones at the front hold the workers, and every request queued behind them waits. That wait is invisible to the server and fully visible to the client.
workers
load generator

A FIFO request queue in front of the server

t = 0.0 s
arriving →idle

Last 100 responses: waiting (amber) + processing (blue / red)

800 ms

A few slow requests hold the only worker, and every fast request queued behind them pays the wait. Try: inject a 450 ms request with 1 worker, then with 4. Then switch to a closed-loop load generator: it stops sending while it waits, hiding the queue that real users would build up.

mean processing time
0.0 ms
what the server measures
mean response time
0.0 ms
what the client measures
p99 response
0 ms
fast requests stuck behind others
0.0%
waited > 25 ms
throughput
0/s
offered 40/s

Coping with load: up, out, or elastic

scale-up-vs-outelasticity
A day of traffic with a diurnal cycle and a viral spike. Buy one big machine, run a fixed fleet of small ones, or let an autoscaler add nodes. Compare what each costs, what each drops, and what happens when a machine dies.

One day of load vs capacity: elastic: autoscale

loadcapacityunserved
010200h5h10h15h20hhour of dayk requests/s

One node serves 1k req/s at $0.20/h; a machine k times bigger costs k^1.5 times as much and tops out at 24×. Try: turn on the failure, which takes the single big machine down for 20 minutes but only one node of a fleet; then shorten the autoscaler's boot time to catch the viral spike.

cost per day
$38
requests not served
0.00%
minutes overloaded
0
average utilization
62%
share of paid capacity used
all three, same settings
up$200 · 1.53% lost
out$58 · 1.53% lost
elastic$38 · 0.00% lost
No architecture scales to everything. A system for 100,000 small requests per second looks nothing like one for 3 requests per minute of 2 GB each. Scalable designs start from assumptions about which operations will be common, which is why load parameters matter.

Maintainability: operability, simplicity, evolvability

operabilityaccidental-complexityevolvability
Most of a system's cost comes after it ships. Accidental complexity, the kind that comes from the implementation and not from the problem, shows up as tangled dependencies: change one module and things break far away. Good abstractions contain the ripple.

Click a module to change it: 29 other modules may break

01234567891011121314151617181920212223242526272829

Arrows point from caller to callee. A change ripples to everything that depends on the changed module, directly or transitively. Green rings are interfaces: they hide implementation details, so a change behind one stops there. Try: add modules in the ball of mud and watch the average blast radius grow; then click an interface (a public API change) in the modular design.

this change touches
29 modules
average blast radius
90% of code
worst change
29 modules
dependencies
83
Operability means making routine work easy. Monitor health, automate, avoid depending on individual machines, document, behave predictably, and allow overriding defaults. Simplicity keeps the system understandable, and evolvability follows from it: a system that is easy to understand is easy to change.
  • takeawayFaults happen all the time at scale. Design so they don't become failures: redundancy for random hardware faults, and for correlated software and human faults use testing, isolation, gradual rollouts and fast rollback.
  • takeawayDescribe load with the parameter that dominates cost (for Twitter, fan-out), and describe performance with percentiles of client-side response time. Tails grow with fan-out and with queueing.
  • takeawayScaling is a set of trade-offs between big machines, many machines and elasticity. Maintainability comes from removing accidental complexity with abstractions that keep a change from spreading.

Key concepts · 20

book chapter 1
  1. Reliability

    The system keeps working correctly, at the expected performance, even when things go wrong.

    Things going wrong is the normal case at scale. Reliability is designed in, by expecting faults and keeping them from turning into user-visible failures.

    ↑ see it in the visualization
  2. Fault vs failure

    A fault is one component deviating from its spec; a failure is the system as a whole no longer serving the user.

    You can't drive faults to zero, so fault-tolerant design aims to stop faults from becoming failures. Redundancy masks a dead disk; nothing masks a fault that hits every replica.

    ↑ see it in the visualization
  3. Hardware faults

    Disks crash, RAM goes bad, power and network fail. Mostly random and independent.

    A disk's MTTF is 10 to 50 years, so a cluster of 10,000 disks loses about one a day. Redundancy (RAID, replicas, multi-machine setups) handles them, and software fault tolerance lets you lose whole machines.

    RAIDAWS instance retirement
    ↑ see it in the visualization
  4. Software errors (systematic faults)

    Bugs and runaway processes that are correlated across nodes because every node runs the same code.

    They cause many more simultaneous failures than hardware does (for example the 2012 leap-second bug that hung Linux servers). Replication doesn't help; testing, isolation, monitoring and crash-restart design do.

    Leap second 2012cascading failures
    ↑ see it in the visualization
  5. Human errors

    Operator mistakes, such as bad configuration changes, are a leading cause of outages.

    The fixes are about design: interfaces that make the right thing easy, sandboxes, fast rollback, gradual rollouts to a small canary group, and detailed telemetry.

    ↑ see it in the visualization
  6. Deliberate fault injection

    Killing processes or machines on purpose to make sure fault-tolerance machinery really works.

    Error-handling code that never runs is probably broken. Exercising it continuously builds confidence that real faults will be handled.

    Netflix Chaos Monkey
    ↑ see it in the visualization
  7. Scalability

    Whether there are reasonable ways to deal with growth in load, data volume or complexity.

    It is not a yes/no label. The useful question is: if load grows in this particular way, what are our options for adding resources?

    ↑ see it in the visualization
  8. Load parameters

    The few numbers that describe load for your system: requests/s, read/write ratio, active users, cache hit rate.

    Choosing the right one matters. For Twitter the tweet rate was not the bottleneck; fan-out (followers per user) was.

    ↑ see it in the visualization
  9. Fan-out (home timelines)

    Building home timelines either by merging followees' tweets at read time, or by pushing each tweet into every follower's cached timeline at write time.

    Pushing on write makes reads cheap but turns a celebrity's tweet into millions of writes. Twitter ended up with a hybrid: fan out most users' tweets, and merge celebrities' tweets in at read time.

    Twitter
    ↑ see it in the visualization
  10. Throughput

    Records processed or requests served per second.

    Batch systems care mostly about throughput; online systems care mostly about response time. The two are linked through queueing.

    ↑ see it in the visualization
  11. Response time vs latency

    Response time is what the client sees (network + queueing + service). Latency is the time a request spends waiting to be handled.

    Measure response time at the client. Time measured only inside the server misses the queueing delay.

    ↑ see it in the visualization
  12. Percentiles (p50, p95, p99, p999)

    The response time that a given share of requests is faster than. The median (p50) is the typical case.

    The mean doesn't tell you how many users waited. High percentiles (tail latencies) hit the users with the most data, often the most valuable customers.

    ↑ see it in the visualization
  13. SLOs and SLAs

    Service-level objectives and agreements, often stated in percentiles: e.g. p50 under 200 ms and p99 under 1 s.

    Percentiles make the contract measurable: customers can ask for a refund if it is not met.

    ↑ see it in the visualization
  14. Tail latency amplification

    When one user request makes N backend calls in parallel, it is as slow as the slowest call.

    P(slow) = 1 − (1 − p)^N, so a 1% slow backend makes most user requests slow once N reaches the hundreds.

    Google's "Tail at Scale"hedged requests
    ↑ see it in the visualization
  15. Head-of-line blocking

    A few slow requests hold the server's limited workers, and the fast requests queued behind them wait.

    This is why client-side response time can be far worse than server-side processing time. Load tests must send requests independently of responses (open loop), otherwise they hide the queue.

    ↑ see it in the visualization
  16. Scaling up vs scaling out

    Vertical scaling moves to a more powerful machine; horizontal scaling spreads load over many small machines (shared-nothing).

    Big machines get expensive fast and are a single point of failure; distributing stateless services is easy, but distributing stateful data adds a lot of complexity. Real systems mix the two.

    ↑ see it in the visualization
  17. Elasticity

    Adding computing resources automatically when load increases.

    Useful when load is unpredictable, but autoscalers react with a delay. Manually scaled systems are simpler and have fewer operational surprises.

    AWS Auto ScalingKubernetes HPA
    ↑ see it in the visualization
  18. Operability

    Making it easy for operations teams to keep the system running smoothly.

    Good monitoring, automation, documentation, predictable behaviour and sensible defaults let people focus on the high-value work.

    ↑ see it in the visualization
  19. Simplicity and accidental complexity

    Accidental complexity comes from the implementation, not from the problem the users have.

    Abstraction is the main tool for removing it: a clean interface hides implementation details, so a change behind it doesn't ripple through the codebase.

    ↑ see it in the visualization
  20. Evolvability

    Making it easy to adapt the system to new requirements (also called extensibility or plasticity).

    Requirements always change. How easily you can change a data system depends a lot on how simple its abstractions are.

    ↑ see it in the visualization