ЁЯПл The SchoolтА║ЁЯУР System DesignтА║ЁЯЫЯ рдзрдбрд╛ 13 тАФ Design рдордзреВрдирдЪ resilience: рдкреНрд░рддреНрдпреЗрдХ рдмрд╛рдгрд╛рд╕рд╛рдареА рдПрдХ рдпреЛрдЬрдирд╛
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯЫЯ рдзрдбрд╛ 13 тАФ Design рдордзреВрдирдЪ resilience: рдкреНрд░рддреНрдпреЗрдХ рдмрд╛рдгрд╛рд╕рд╛рдареА рдПрдХ рдпреЛрдЬрдирд╛

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 18 рдкреИрдХреА рдзрдбрд╛ 13 ┬╖ рдкреБрдвреАрд▓: lesson-14-security


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ12, рдЖрдгрд┐ рдХрд╛рд░реНрдпрд╛рд▓рдп рдЖрдХреГрддреАрд╡рд░реАрд▓ рдкреНрд░рддреНрдпреЗрдХ box рд▓рд╛ рд╡рд┐рдЪрд╛рд░рддреЗ рддреЛ рдкрд╣рд┐рд▓рд╛ рдкреНрд░рд╢реНрди: рддреЛ рдЕрдкрдпрд╢реА рдЭрд╛рд▓рд╛ рддрд░ рдХрд╛рдп рд╣реЛрддреЗ? рддреБрдореНрд╣реА рд╢рд┐рдХрддрд╛ timeout budgets, backoff рдЖрдгрд┐ jitter рд╕рд╣ retries, circuit breaker, bulkheads, graceful degradation, рдЖрдгрд┐ multi-AZ рдЪреЗ рдЧрдгрд┐рдд. рд╣рд╛ рдзрдбрд╛ рдирд┐рдпреЛрдЬрдирд╛рдЪреНрдпрд╛ рдирдЬрд░реЗрддреВрди рдЖрд╣реЗ. Scaling рд╢рд╛рд│рд╛, рдзрдбрд╛ 13 рдкреНрд░рддреНрдпреЗрдХ рд╕рд╛рдзрди simulations рд╕рд╣ рдЦреЛрд▓рд╛рдд рдмрд╛рдВрдзрддреЛ.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рджреАрдкрд┐рдХрд╛ рд╕реВрдЪрдирд╛-рдлрд▓рдХ app рдЪреА рдЖрдХреГрддреА рднрд┐рдВрддреАрд╡рд░ рд▓рд╛рд╡рддреЗ. рддреНрдпрд╛рдд boxes рдЖрдгрд┐ рдмрд╛рдг рдЖрд╣реЗрдд. рддреА рдХрддрд░рд┐рдирд╛рд▓рд╛ рдПрдХ рд▓рд╛рд▓ рдкреЗрди рджреЗрддреЗ. "рдкреНрд░рддреНрдпреЗрдХ рдмрд╛рдгрд╛рд╕рд╛рдареА рдЪрд╛рд░ рдЧреЛрд╖реНрдЯреА рд▓рд┐рд╣реА."

  1. тП▒я╕П рдЖрдкрдг рдХрд┐рддреА рд╡реЗрд│ рдерд╛рдВрдмрд╛рдпрдЪреЗ? рдкреВрд░реНрдг page 800 ms рдордзреНрдпреЗ рддрдпрд╛рд░ рд╡реНрд╣рд╛рдпрд▓рд╛ рд╣рд╡реЗ. рдореНрд╣рдгреВрди login рддрдкрд╛рд╕рдгреАрдХрдбрдЪреНрдпрд╛ рдмрд╛рдгрд╛рд▓рд╛ 50 ms, рд╕реВрдЪрдирд╛-рдиреЛрдВрджрд╡рд╣реАрдХрдбрдЪреНрдпрд╛ рдмрд╛рдгрд╛рд▓рд╛ 150 ms, photo service рдХрдбрдЪреНрдпрд╛ рдмрд╛рдгрд╛рд▓рд╛ 300 ms рдорд┐рд│рддрд╛рдд. рдпрд╛ рд╕рдЧрд│реНрдпрд╛ рдкреНрд░рддреАрдХреНрд╖рд╛ 800 рдЪреНрдпрд╛ рдЖрдд рдмрд╕рд╛рдпрд▓рд╛ рд╣рд╡реНрдпрд╛рдд.
  2. ЁЯЩП рдЖрдкрдг рдкреБрдиреНрд╣рд╛ рд╡рд┐рдЪрд╛рд░рд╛рдпрдЪреЗ рдХрд╛? рдПрдХ-рджреЛрдирджрд╛, рд╡рд╛рдврддреНрдпрд╛ рд╡рд┐рд░рд╛рдорд╛рдиреЗ рдЖрдгрд┐ рдереЛрдбреНрдпрд╛ random рдЬрд╛рджрд╛ рд╡реЗрд│рд╛рдиреЗ, рдореНрд╣рдгрдЬреЗ рд╕рдЧрд│реЗ callers рдПрдХрд╛рдЪ рдХреНрд╖рдгреА рдкреБрдиреНрд╣рд╛ рд╡рд┐рдЪрд╛рд░рдд рдирд╛рд╣реАрдд.
  3. ЁЯФМ рд╡рд┐рдЪрд╛рд░рдгреЗ рдХрдзреА рдерд╛рдВрдмрд╡рд╛рдпрдЪреЗ? рд╕рд▓рдЧ рдЕрдиреЗрдХ рдЕрдкрдпрд╢рд╛рдВрдирдВрддрд░ рдПрдХ switch рдЙрдШрдбрддреЛ: "10 seconds photo service рд▓рд╛ call рдХрд░реВ рдирдХрд╛." рдордЧ рдПрдХ рдЪрд╛рдЪрдгреА call.
  4. ЁЯкз рддреНрдпрд╛рдРрд╡рдЬреА рдХрд╛рдп рджрд╛рдЦрд╡рд╛рдпрдЪреЗ? Photo service рдмрдВрдж рдЖрд╣реЗ? рд╕реВрдЪрдирд╛ photos рд╢рд┐рд╡рд╛рдп рджрд╛рдЦрд╡рд╛. Page рддрд░реАрд╣реА рдЙрдШрдбрддреЗ.

рдРрд╢реНрд╡рд░реНрдпрд╛ рдкрд╛рдЪрд╡реА рдХрд▓реНрдкрдирд╛ рдЬреЛрдбрддреЗ: рд╡реЗрдЧрд│реНрдпрд╛ рдЦреЛрд▓реНрдпрд╛. Photo рдорджрддрдиреАрд╕ рддреНрдпрд╛рдВрдЪреНрдпрд╛ рд╕реНрд╡рддрдГрдЪреНрдпрд╛ рдЦреЛрд▓реАрдд рд╕реНрд╡рддрдГрдЪреНрдпрд╛ рдЦреБрд░реНрдЪреНрдпрд╛рдВрд╡рд░ рдХрд╛рдо рдХрд░рддрд╛рдд. рддреЗ рд╕рдЧрд│реЗ рдЕрдбрдХрд▓реЗ рддрд░реА рд╕реВрдЪрдирд╛-рдорджрддрдиреАрд╕рд╛рдВрдЪреНрдпрд╛ рдЦреБрд░реНрдЪреНрдпрд╛ рддреНрдпрд╛рдВрдЪреНрдпрд╛рдХрдбреЗрдЪ рд░рд╛рд╣рддрд╛рдд. рд╣реЗрдЪ bulkhead, рдЬрд╣рд╛рдЬрд╛рдЪреНрдпрд╛ рдЖрддрд▓реНрдпрд╛ рднрд┐рдВрддреАрдВрд╕рд╛рд░рдЦреЗ.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    u["ЁЯСк parent<br/>page budget 800 ms"] --> api["ЁЯзй notice API"]
    api -->|"тП▒я╕П 50 ms"| auth["ЁЯФС auth"]
    api -->|"тП▒я╕П 150 ms"| db["ЁЯЧДя╕П notices DB"]
    api -->|"тП▒я╕П 300 ms ┬╖ ЁЯФМ breaker"| ph["ЁЯЦ╝я╕П photo service"]
    ph -.->|"down"| fb["ЁЯкз degrade:<br/>notices without photos"]
    subgraph bh["ЁЯЪк bulkheads"]
      p1["pool for DB calls"]
      p2["pool for photo calls"]
    end
    az["ЁЯПвЁЯПвЁЯПв 3 AZs<br/>99.5% each тЖТ 99.99999%"]

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрд╡реГрддреНрддреА + рдПрдХ lab: https://school-edh.pages.dev/system-design/lesson-diagrams.html#l13

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рдкреНрд░рддреНрдпреЗрдХ рдмрд╛рдгрд╛рд▓рд╛ рдЕрдкрдпрд╢рд╛рдЪреА рдпреЛрдЬрдирд╛ рдорд┐рд│реЗрдкрд░реНрдпрдВрдд рдЖрдХреГрддреА рдЕрд░реНрдзреАрдЪ рдЕрд╕рддреЗ. Timeout рдирд╕реЗрд▓ рддрд░ рдПрдХ рдЕрдбрдХрд▓реЗрд▓реА service рдкреНрд░рддреНрдпреЗрдХ thread рдзрд░реВрди рдареЗрд╡рддреЗ. Jitter рд╢рд┐рд╡рд╛рдпрдЪреЗ retries рдЫреЛрдЯреНрдпрд╛ рдЕрдбрдерд│реНрдпрд╛рдЪреЗ рд╡рд╛рджрд│ рдХрд░рддрд╛рдд. Fallback рдирд╕рд▓реЗрд▓реЗ design "photos рдирд╛рд╣реАрдд" рдЪреЗ "app рдирд╛рд╣реА" рдХрд░рддреЗ. рд╣реЗ design рдЪреЗ рдирд┐рд░реНрдгрдп рдЖрд╣реЗрдд тАФ outage рдЪреНрдпрд╛ рд╡реЗрд│реА рдЬреЛрдбрдгреНрдпрд╛рдкреЗрдХреНрд╖рд╛ рддреЗ рдпреЛрдЬрдиреЗрд╡рд░ рд▓рд┐рд╣рд┐рдгреЗ рдЦреВрдк рд╕реНрд╡рд╕реНрдд рдЕрд╕рддреЗ.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

design/designs.py рдордзреАрд▓ call_chain(hops, ms_each, availability_each) calls рдЪреНрдпрд╛ рдПрдХрд╛ рдУрд│реАрдЪреА latency (hops ├Ч ms_each) рдЖрдгрд┐ availability (availability_each ** hops) рдкрд░рдд рдХрд░рддреЛ. design/demo.py рдордзреАрд▓ resilience() рдПрдХрд╛ page рдЪреЗ timeout budget рдЖрдгрд┐ multi-AZ рдмреЗрд░реАрдЬ 1 тИТ (1 тИТ 0.995)┬│ рдЫрд╛рдкрддреЛ. Snippet budget рдЪреА рдмреЗрд░реАрдЬ рдХрд░рддреЛ, рд╕рд╛рдЦрд│реА рд╡рд╛рдврд╡рддреЛ, рдЖрдгрд┐ рдерд░рд╛рдВрдордзреАрд▓ retries рдореЛрдЬрддреЛ.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 design/demo.py resilience
python3 - <<'EOF'
import sys; sys.path.insert(0, "design"); from designs import call_chain
budget, deps = 800, {"auth": 50, "notices DB": 150, "photo service": 300}
print("timeouts add up to", sum(deps.values()), "ms of an", budget, "ms budget тЖТ left for our own work:", budget - sum(deps.values()), "ms")
for hops in (1, 3, 5):
    c = call_chain(hops, 20, 0.999); print(f"{hops} hard dependencies in a line тЖТ {c['latency_ms']:>3} ms, {c['availability']:.3%}")
for a in (0.99, 0.995):
    print(f"one AZ {a:.1%} тЖТ", " ┬╖ ".join(f"{n} AZs {1 - (1 - a) ** n:.6%}" for n in (1, 2, 3)))
for tries in (1, 2, 3):
    print(f"3 layers each trying {tries}x тЖТ up to {tries ** 3} calls reach the sick service for one click")
EOF

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

resilience рд╣реЗ рдЫрд╛рдкрддреЛ:

тФАтФА the budget for one page is 800 ms; each dependency gets a timeout inside it
   auth           timeout  50 ms
   notices DB     timeout 150 ms
   photo service  timeout 300 ms
   photo service down тЖТ show notices without photos (degrade), breaker open, retries with jitter
   one AZ 99.5% тЖТ three AZs 99.99999% ┬╖ deeper dive: the Scaling school, lesson 13

рддреБрдордЪрд╛ snippet рд╣реЗ рдЫрд╛рдкрддреЛ:

timeouts add up to 500 ms of an 800 ms budget тЖТ left for our own work: 300 ms
1 hard dependencies in a line тЖТ  20 ms, 99.900%
3 hard dependencies in a line тЖТ  60 ms, 99.700%
5 hard dependencies in a line тЖТ 100 ms, 99.501%
one AZ 99.0% тЖТ 1 AZs 99.000000% ┬╖ 2 AZs 99.990000% ┬╖ 3 AZs 99.999900%
one AZ 99.5% тЖТ 1 AZs 99.500000% ┬╖ 2 AZs 99.997500% ┬╖ 3 AZs 99.999988%
3 layers each trying 1x тЖТ up to 1 calls reach the sick service for one click
3 layers each trying 2x тЖТ up to 8 calls reach the sick service for one click
3 layers each trying 3x тЖТ up to 27 calls reach the sick service for one click

Tests рдордзреНрдпреЗ тЬЕ L13 three copies beat one рдЕрд╕рддреЗ.

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

рддреАрди timeouts 800 ms рдЪреНрдпрд╛ page рдкреИрдХреА 500 ms рд╡рд╛рдкрд░рддрд╛рдд, API рдЪреНрдпрд╛ рд╕реНрд╡рддрдГрдЪреНрдпрд╛ рдХрд╛рдорд╛рд╕рд╛рдареА 300 ms рдЙрд░рддрд╛рдд тАФ рдЖрдгрд┐ photo call рдЪреНрдпрд╛ retry рд╕рд╛рдареА рдЬрд╛рдЧрд╛рдЪ рдЙрд░рдд рдирд╛рд╣реА (рдЖрдгрдЦреА 300 ms), рдореНрд╣рдгреВрди рддреНрдпрд╛ call рд▓рд╛ рдХреЛрдгрддрд╛рд╣реА retry рдорд┐рд│рдд рдирд╛рд╣реА. 99.9% рдЪреНрдпрд╛ рдкрд╛рдЪ hard dependencies 99.501% рдкрд░реНрдпрдВрдд рдШрд╕рд░рддрд╛рдд. 99.5% рдЪрд╛ рдПрдХ zone рджреЛрди zones рд╕рд╣ 99.9975% рдЖрдгрд┐ рддреАрди рд╕рд╣ 99.999988% рд╣реЛрддреЛ (demo рддреЗ рдЧреЛрд▓ рдХрд░реВрди 99.99999% рдЫрд╛рдкрддреЛ) тАФ рдлрдХреНрдд zones рд╕реНрд╡рддрдВрддреНрд░рдкрдгреЗ рдЕрдкрдпрд╢реА рд╣реЛрдд рдЕрд╕реЗрдкрд░реНрдпрдВрдд. рдЖрдгрд┐ рддреАрди рдерд░рд╛рдВрд╡рд░ "рдлрдХреНрдд 3 retries" рдореНрд╣рдгрдЬреЗ рдЖрдзреАрдЪ рдЖрдЬрд╛рд░реА рдЕрд╕рд▓реЗрд▓реНрдпрд╛ service рдХрдбреЗ 27 рдкрд░реНрдпрдВрдд calls.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

рдЦрд▒реНрдпрд╛ account рд╡рд░ тАФ рдкреНрд░рддреНрдпреЗрдХ call рд╕реЛрдмрдд deadline рдЦрд╛рд▓реА рдкрд╛рдард╡рд╛, рдЖрдгрд┐ рдЦреВрдк рдХрдореА рд╡реЗрд│ рдЙрд░рд▓рд╛ рдЕрд╕реЗрд▓ рддрд░ рд▓рд╡рдХрд░ рдерд╛рдВрдмрд╛:

import time, requests

def call(url, deadline):
    left = deadline - time.monotonic()
    if left < 0.05:                              # under 50 ms left: do not start
        raise TimeoutError("budget spent")
    return requests.get(url, timeout=(0.1, min(left, 0.3)),          # connect, read (s)
                        headers={"X-Request-Deadline": str(round(left * 1000))})

deadline = time.monotonic() + 0.8                # the page budget: 800 ms

Code рдордзрд▓рд╛ bulkhead тАФ рдПрдХрд╛ рд╡реЗрд│реА рдЬрд╛рд╕реНрддреАрдд рдЬрд╛рд╕реНрдд 20 photo calls, рдореНрд╣рдгрдЬреЗ photo service рдХрдзреАрдЪ рд╕рдЧрд│реЗ workers рдШреЗрдК рд╢рдХрдд рдирд╛рд╣реА:

import threading
photo_slots = threading.BoundedSemaphore(20)

def photos_for(notice_id, deadline):
    if not photo_slots.acquire(timeout=0.01):
        return []                                # bulkhead full тЖТ degrade: no photos
    try:
        return call(f"https://photos.internal/notices/{notice_id}", deadline).json()
    except Exception:
        return []                                # photo service slow or down тЖТ degrade
    finally:
        photo_slots.release()

Backoff рдЖрдгрд┐ jitter рд╕рд╣ AWS SDK retries, рдЖрдгрд┐ Multi-AZ database:

export AWS_RETRY_MODE=standard AWS_MAX_ATTEMPTS=3
aws rds modify-db-instance --db-instance-identifier notices-db --multi-az --apply-immediately

Service mesh рдордзреАрд▓ circuit breakers, outlier detection, DLQs, health checks рдЖрдгрд┐ AWS FIS рд╕рд╣ chaos tests рд╕рд╛рдареА, рдкрд╛рд╣рд╛ Scaling рд╢рд╛рд│рд╛, рдзрдбрд╛ 13.

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рдкреНрд░рддреНрдпреЗрдХ design рдордзреНрдпреЗ рдПрдХ "failure" table рдЬреЛрдбрд╛: рдкреНрд░рддреНрдпреЗрдХ рдмрд╛рдгрд╛рд╕рд╛рдареА рдПрдХ row, рддреНрдпрд╛рдЪрд╛ timeout, retry рдирд┐рдпрдо, breaker, bulkhead рдЖрдгрд┐ fallback рд╕рд╣. рд░рд┐рдХрд╛рдорд╛ рдХрдкреНрдкрд╛ рдореНрд╣рдгрдЬреЗ рднрд╡рд┐рд╖реНрдпрд╛рддрд▓рд╛ outage.

тПня╕П рдкреБрдвреЗ

рднрд╛рдЧ рдЕрдкрдШрд╛рддрд╛рдиреЗ рдЕрдкрдпрд╢реА рд╣реЛрддрд╛рдд. рдХрд╛рд╣реА рд▓реЛрдХ рддреЗ рдореБрджреНрджрд╛рдо рдореЛрдбрдгреНрдпрд╛рдЪрд╛ рдкреНрд░рдпрддреНрди рдХрд░рддрд╛рдд. Design рдордзреВрдирдЪ security.

git checkout lesson-14-security

ЁЯЫЯ Lesson 13 тАФ Resilience by design: a plan for every arrow

ЁЯУН You are here: Lesson 13 of 18 ┬╖ Next: lesson-14-security


ЁЯУж What's in this branch

Lessons 01тАУ12, plus the first question the office asks of every box on the drawing: what happens when it fails? You learn timeout budgets, retries with backoff and jitter, the circuit breaker, bulkheads, graceful degradation, and the multi-AZ maths. This lesson is the planning view. The Scaling school, lesson 13 builds each tool in depth, with simulations.

ЁЯзТ Explain like I'm 5

Dipika pins the drawing of the notice-board app to the wall. It has boxes and arrows. She gives Katrina a red pen. "For every arrow, write four things."

  1. тП▒я╕П How long do we wait? The whole page must be ready in 800 ms. So the arrow to the login check gets 50 ms, the arrow to the notices book gets 150 ms, the arrow to the photo service gets 300 ms. The waits must fit inside the 800.
  2. ЁЯЩП Do we ask again? Once or twice, with a growing pause and a little random extra, so that all callers do not ask again at the same moment.
  3. ЁЯФМ When do we stop asking? After many failures in a row, a switch opens: "Do not call the photo service for 10 seconds." Then one test call.
  4. ЁЯкз What do we show instead? The photo service is down? Show the notices without photos. The page still opens.

Aishwarya adds a fifth idea: separate rooms. The photo helpers work in their own room with their own chairs. If they all get stuck, the notice helpers still have their chairs. That is a bulkhead, like the walls inside a ship.

ЁЯЧ║я╕П Diagram

flowchart LR
    u["ЁЯСк parent<br/>page budget 800 ms"] --> api["ЁЯзй notice API"]
    api -->|"тП▒я╕П 50 ms"| auth["ЁЯФС auth"]
    api -->|"тП▒я╕П 150 ms"| db["ЁЯЧДя╕П notices DB"]
    api -->|"тП▒я╕П 300 ms ┬╖ ЁЯФМ breaker"| ph["ЁЯЦ╝я╕П photo service"]
    ph -.->|"down"| fb["ЁЯкз degrade:<br/>notices without photos"]
    subgraph bh["ЁЯЪк bulkheads"]
      p1["pool for DB calls"]
      p2["pool for photo calls"]
    end
    az["ЁЯПвЁЯПвЁЯПв 3 AZs<br/>99.5% each тЖТ 99.99999%"]

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/system-design/lesson-diagrams.html#l13

тЭУ What

ЁЯдФ Why

Because the drawing is only half done until every arrow has a failure plan. A missing timeout lets one stuck service hold every thread. Retries without jitter turn a short blip into a storm. A design with no fallback turns "no photos" into "no app". These are design decisions тАФ it is much cheaper to write them on the plan than to add them during an outage.

ЁЯФз How (in this repo)

call_chain(hops, ms_each, availability_each) in design/designs.py returns the latency (hops ├Ч ms_each) and availability (availability_each ** hops) of a line of calls. resilience() in design/demo.py prints the timeout budget for one page and the multi-AZ sum 1 тИТ (1 тИТ 0.995)┬│. The snippet adds up the budget, grows the chain, and counts retries across layers.

ЁЯзк Try it

python3 design/demo.py resilience
python3 - <<'EOF'
import sys; sys.path.insert(0, "design"); from designs import call_chain
budget, deps = 800, {"auth": 50, "notices DB": 150, "photo service": 300}
print("timeouts add up to", sum(deps.values()), "ms of an", budget, "ms budget тЖТ left for our own work:", budget - sum(deps.values()), "ms")
for hops in (1, 3, 5):
    c = call_chain(hops, 20, 0.999); print(f"{hops} hard dependencies in a line тЖТ {c['latency_ms']:>3} ms, {c['availability']:.3%}")
for a in (0.99, 0.995):
    print(f"one AZ {a:.1%} тЖТ", " ┬╖ ".join(f"{n} AZs {1 - (1 - a) ** n:.6%}" for n in (1, 2, 3)))
for tries in (1, 2, 3):
    print(f"3 layers each trying {tries}x тЖТ up to {tries ** 3} calls reach the sick service for one click")
EOF

тЬЕ Verify тАФ what you should see

resilience prints:

тФАтФА the budget for one page is 800 ms; each dependency gets a timeout inside it
   auth           timeout  50 ms
   notices DB     timeout 150 ms
   photo service  timeout 300 ms
   photo service down тЖТ show notices without photos (degrade), breaker open, retries with jitter
   one AZ 99.5% тЖТ three AZs 99.99999% ┬╖ deeper dive: the Scaling school, lesson 13

Your snippet prints:

timeouts add up to 500 ms of an 800 ms budget тЖТ left for our own work: 300 ms
1 hard dependencies in a line тЖТ  20 ms, 99.900%
3 hard dependencies in a line тЖТ  60 ms, 99.700%
5 hard dependencies in a line тЖТ 100 ms, 99.501%
one AZ 99.0% тЖТ 1 AZs 99.000000% ┬╖ 2 AZs 99.990000% ┬╖ 3 AZs 99.999900%
one AZ 99.5% тЖТ 1 AZs 99.500000% ┬╖ 2 AZs 99.997500% ┬╖ 3 AZs 99.999988%
3 layers each trying 1x тЖТ up to 1 calls reach the sick service for one click
3 layers each trying 2x тЖТ up to 8 calls reach the sick service for one click
3 layers each trying 3x тЖТ up to 27 calls reach the sick service for one click

The tests include тЬЕ L13 three copies beat one.

ЁЯПБ What you just proved

The three timeouts use 500 ms of the 800 ms page, leaving 300 ms for the API's own work тАФ and no room for a retry of the photo call (300 ms more), so that call gets no retry. Five hard dependencies at 99.9% fall to 99.501%. One zone at 99.5% becomes 99.9975% with two and 99.999988% with three (the demo prints it rounded to 99.99999%) тАФ only while the zones fail independently. And "just 3 retries" at three layers is up to 27 calls into a service that is already sick.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ pass the deadline down with each call, and stop early when too little time is left:

import time, requests

def call(url, deadline):
    left = deadline - time.monotonic()
    if left < 0.05:                              # under 50 ms left: do not start
        raise TimeoutError("budget spent")
    return requests.get(url, timeout=(0.1, min(left, 0.3)),          # connect, read (s)
                        headers={"X-Request-Deadline": str(round(left * 1000))})

deadline = time.monotonic() + 0.8                # the page budget: 800 ms

A bulkhead in code тАФ at most 20 photo calls at once, so the photo service can never take every worker:

import threading
photo_slots = threading.BoundedSemaphore(20)

def photos_for(notice_id, deadline):
    if not photo_slots.acquire(timeout=0.01):
        return []                                # bulkhead full тЖТ degrade: no photos
    try:
        return call(f"https://photos.internal/notices/{notice_id}", deadline).json()
    except Exception:
        return []                                # photo service slow or down тЖТ degrade
    finally:
        photo_slots.release()

AWS SDK retries with backoff and jitter, and a Multi-AZ database:

export AWS_RETRY_MODE=standard AWS_MAX_ATTEMPTS=3
aws rds modify-db-instance --db-instance-identifier notices-db --multi-az --apply-immediately

For circuit breakers in a service mesh, outlier detection, DLQs, health checks and chaos tests with AWS FIS, see the Scaling school, lesson 13.

ЁЯПн Why this matters in production: add a "failure" table to every design: one row per arrow, with its timeout, retry rule, breaker, bulkhead and fallback. An empty cell is a future outage.

тПня╕П Next

Parts fail by accident. Some people try to break them on purpose. Security by design.

git checkout lesson-14-security
тЖР Previousrate limitingNext тЖТsecurity

This page is the lesson's README from the lesson-13-resilience branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.