20992883ff
---ci--- project: atelier phase: 1 milestone: v0.4 status: complete phase_role: execution phase_tag: v0.3.1 requirements: covered: [ATELIER-92, ATELIER-93, ATELIER-94, ATELIER-95, ATELIER-96] partial: [] ---/ci---
227 lines
12 KiB
Markdown
227 lines
12 KiB
Markdown
# CDN — Derived Rules
|
|
|
|
> Derives from `domains/edge/first-principles.md`. Applies P1
|
|
> (Proximity is the Design Driver) and P6 (Cache Invalidation is
|
|
> Explicit) primarily, with P5 (idempotent cache fill), P8
|
|
> (geographic distribution), and P9 (identity at the edge). For the
|
|
> edge-cache-vs-origin decision, see the decision matrix below.
|
|
> Cross-links `domains/performance/frontend` for generic caching,
|
|
> `domains/security/input-validation` for cache poisoning, and
|
|
> `domains/observability/metrics` for cache-hit ratio.
|
|
|
|
## What a CDN Is (P1 Proximity is the Design Driver)
|
|
|
|
- A content delivery network is a fleet of PoPs (points of presence)
|
|
placed near users. The PoP serves cached content; the origin is
|
|
the authoritative source. The CDN's whole purpose is P1: compute
|
|
(the cache) is placed near the user so the round trip to the origin
|
|
does not bound latency. Latency is a correctness constraint at the
|
|
edge (C1), not a performance preference.
|
|
- The CDN is the canonical edge-cache architecture (Akamai,
|
|
Cloudflare, Fastly): PoPs near users, origin shielding, cache-key
|
|
normalization, purge APIs. Atelier derives the
|
|
placement/invalidation principles, not the vendor config.
|
|
- The boundary with `domains/performance/frontend` is per D-061:
|
|
performance owns *generic* caching and optimization (cache what is
|
|
expensive, stable, read often — `performance/P5 Caching with
|
|
Intent`); edge owns the *geographic, partition-aware* placement and
|
|
invalidation angle. A CDN is an edge concern because its defining
|
|
trait is geographic distribution (P8) and partition-aware
|
|
invalidation (P6), not measurement.
|
|
|
|
## Cache Key Design (P6 Cache Invalidation is Explicit)
|
|
|
|
- The cache key is the contract between the URL and the cached
|
|
representation. A key that varies on the wrong dimensions serves
|
|
the wrong content; a key that varies on too many dimensions
|
|
collapses the hit ratio. Key design *is* the invalidation
|
|
surface: a key that includes a content hash or version segment
|
|
makes invalidation explicit; a key that ignores `Vary` headers
|
|
serves stale variants.
|
|
- Normalize the key: lower-case the host, strip default ports,
|
|
sort query parameters, ignore tracking parameters. A
|
|
non-normalized key is a cache-poisoning vector (see
|
|
`domains/security/input-validation`) and a hit-ratio destroyer
|
|
(see `domains/observability/metrics`).
|
|
- A cache with no explicit key strategy is a TTL-less cache under
|
|
partition (P6 violation): staleness is silent and unbounded.
|
|
|
|
```http
|
|
# Cache key derivation: vary on what changes content, ignore what
|
|
# does not. The key is the tuple (host, normalized-path, sorted-
|
|
# query, Vary-headers); the cache entry is the representation + TTL.
|
|
Cache-Key: example.com /api/v1/products?sort=price®ion=us Vary:Accept-Encoding
|
|
Cache-Control: public, max-age=60, s-maxage=600, stale-while-revalidate=300
|
|
Vary: Accept-Encoding
|
|
```
|
|
|
|
- `max-age` bounds the browser cache; `s-maxage` bounds the CDN
|
|
PoP; `stale-while-revalidate` allows serving stale while
|
|
refetching. Each is an explicit invalidation strategy (P6).
|
|
|
|
## TTL vs Explicit Invalidation (P6, C3 Simplicity)
|
|
|
|
- **TTL-based invalidation** (`max-age`, `s-maxage`): the cache entry
|
|
expires after a duration. Simple, no origin contact required to
|
|
invalidate, but bounded staleness is the contract — the entry may
|
|
be stale up to TTL. Fits content where eventual consistency is
|
|
acceptable (asset fingerprints, lists, derived images).
|
|
- **Explicit invalidation** (purge, surrogate keys): the operator
|
|
signals the cache to drop entries. Tighter staleness bounds, but
|
|
requires the origin or operator to know which entries to purge.
|
|
Fits content where staleness is a correctness defect (price
|
|
updates, availability, breaking news).
|
|
- A TTL-less cache with no explicit invalidation is a P6 violation:
|
|
stale-forever under partition. Every cache must have one or the
|
|
other (or both), and the choice is documented per content type.
|
|
|
|
## Cache-Hit / Miss / Origin-Fetch (P5, P6)
|
|
|
|
- **Hit**: the PoP serves from cache. Latency is PoP-local (P1).
|
|
- **Miss**: the PoP has no entry; it fetches from the origin (or an
|
|
origin-shield PoP). The fetch must be idempotent (P5) — a retried
|
|
miss must not corrupt the cache or double-write side effects.
|
|
- **Revalidate**: the PoP holds a stale entry and asks the origin
|
|
(`If-None-Match`, `If-Modified-Since`); a 304 refreshes the TTL
|
|
without re-fetching the body. Revalidation is the bandwidth-economical
|
|
middle ground (C8).
|
|
|
|
```http
|
|
# Conditional revalidation — the PoP asks the origin "is this still
|
|
# current?" The 304 response refreshes the TTL without a body.
|
|
GET /api/v1/products HTTP/1.1
|
|
Host: example.com
|
|
If-None-Match: "etag-7a3f"
|
|
|
|
HTTP/1.1 304 Not Modified
|
|
ETag: "etag-7a3f"
|
|
Cache-Control: s-maxage=600
|
|
```
|
|
|
|
- A cache-hit ratio that is not measured is a gate on noise — see
|
|
`domains/observability/metrics` for the SLI/SLO discipline that
|
|
makes the hit ratio a meaningful signal. A CDN with no hit-ratio
|
|
metric is operating blind (P10 analog).
|
|
|
|
## Origin Shielding (P1, P8, C8 Economy)
|
|
|
|
- Origin shielding routes all origin fetches through a single
|
|
shield PoP (or shield region). The shield absorbs the
|
|
thundering-herd: 10 000 PoPs missing the same URL fetch the origin
|
|
once, not 10 000 times. This is C8 Economy (origin bandwidth is
|
|
bounded) and P1 (the shield is itself a proximity layer for the
|
|
origin).
|
|
- Shielding is a geographic decision (P8): the shield sits in a
|
|
region close to the origin, not close to the user. The shield is
|
|
the inner ring of the CDN; the user-facing PoPs are the outer ring.
|
|
- A CDN without origin shielding under a stampede will overload the
|
|
origin; a shield that is itself partitioned from the origin must
|
|
degrade gracefully (P7) — serve stale per `stale-while-revalidate`
|
|
rather than 500.
|
|
|
|
## Purge Strategies (P6, C3 Simplicity)
|
|
|
|
| Strategy | Granularity | Latency to Invalidate | Cost | Best for |
|
|
|----------|-------------|-----------------------|------|----------|
|
|
| URL purge | One URL | Seconds | Low (one entry) | Surgical fixes, single-page corrections |
|
|
| Soft purge | One URL (mark stale, serve while refetch) | Seconds | Low | High-traffic URLs where a hard purge causes a stampede |
|
|
| Surrogate-key purge | A tag set (e.g., `product:123`, `category:shoes`) | Seconds | Medium (key indexing) | Related-content invalidation (a product update purges all its category pages) |
|
|
| Wildcard purge | A path prefix or pattern | Seconds to minutes | High (scan) | Site-wide template changes |
|
|
| All-cache purge | Everything | Seconds | Very high (origin stampede) | Disaster recovery only; never the steady-state invalidation path |
|
|
|
|
- Surrogate-key purge (Fastly, Akamai) is the highest-value
|
|
strategy: tag cache entries with content keys, then purge by tag.
|
|
This is explicit invalidation at scale (P6) without the origin
|
|
stampede of an all-cache purge.
|
|
- An all-cache purge as the steady-state invalidation path is a P6
|
|
violation dressed as a feature — it pushes the origin load back to
|
|
100% miss, defeating the CDN's purpose (P1).
|
|
|
|
## Cache Poisoning Prevention (P9, cross-link security/input-validation)
|
|
|
|
- A cache poisoned by a crafted request (a URL with a malicious
|
|
header that gets cached and served to others) is a correctness
|
|
defect (C1) and a security breach (P9 — the edge node is
|
|
exploited). Prevent poisoning by:
|
|
- Normalizing the cache key (strip untrusted query parameters,
|
|
ignore unknown headers, lower-case the host).
|
|
- Validating `Vary` against an allow-list; never `Vary: *` on a
|
|
shared cache (poisonable via header injection).
|
|
- Treating uncacheable responses (`Set-Cookie`,
|
|
`Cache-Control: private`) as never-stored.
|
|
- See `domains/security/input-validation` for the general
|
|
input-validation discipline the cache key must follow. The cache
|
|
key is a validation surface; a non-validated key is an attack
|
|
surface.
|
|
|
|
## Multi-CDN Routing (P8 Geographic Distribution)
|
|
|
|
- A multi-CDN strategy routes each request to the best PoP across
|
|
providers (Akamai + Cloudflare + Fastly). Routing is
|
|
location-aware (P8): latency, cost, and availability vary by
|
|
region and provider. The DNS layer (or a client-side router)
|
|
selects the CDN per request.
|
|
- Multi-CDN is a P8 decision, not a vendor-management decision:
|
|
geographic distribution is the first-class constraint. A
|
|
single-CDN deployment routes everything to one provider's PoPs;
|
|
a multi-CDN deployment routes by region, latency, and cost.
|
|
- Invalidation across multiple CDNs is harder (P6): each provider
|
|
has its own purge API and surrogate-key scheme. A multi-CDN purge
|
|
must fan out to all providers; a purge that reaches only one CDN
|
|
leaves the others stale. Track purge completion per provider —
|
|
see `domains/observability/metrics` for the per-CDN hit-ratio and
|
|
purge-latency signals.
|
|
|
|
```http
|
|
# A CDN config example: cache-control headers + a purge rule.
|
|
# Origin response: declare the cache contract (P6).
|
|
HTTP/1.1 200 OK
|
|
Cache-Control: public, max-age=60, s-maxage=600, stale-while-revalidate=300
|
|
Surrogate-Key: product:123 category:shoes
|
|
ETag: "etag-7a3f"
|
|
Vary: Accept-Encoding
|
|
|
|
# Purge rule (Fastly-style surrogate-key): when product 123
|
|
# updates, purge every cache entry tagged product:123 OR
|
|
# category:shoes. Explicit, bounded, no origin stampede (P6).
|
|
POST /service/svc1/purge
|
|
Surrogate-Key: product:123 category:shoes
|
|
# Returns: {"status": "ok", "id": "purge-abc"} — poll the purge
|
|
# status to confirm completion across all PoPs (P8, P10).
|
|
```
|
|
|
|
## Edge-Cache vs Origin — Decision Matrix (D-069)
|
|
|
|
| Strategy | When | Latency | Origin Load | Correctness Risk |
|
|
|----------|------|---------|-------------|------------------|
|
|
| Serve from PoP (cache hit) | The PoP holds a fresh entry (within TTL or revalidated) | Lowest (PoP-local, P1) | None | Low — bounded by TTL staleness (P6) |
|
|
| Serve stale while revalidate | The PoP holds a stale entry and `stale-while-revalidate` is set | Low (stale served immediately, refetch in background) | Background refetch (1 per entry) | Medium — stale served up to the revalidate window; acceptable for eventually-consistent content |
|
|
| Fetch fresh from origin (miss) | The PoP has no entry, or the content is non-cacheable | High (origin round trip) | Full fetch per miss | Low — fresh by construction; the miss is the correctness floor |
|
|
| Origin-shield fetch | Multiple PoPs miss the same URL; the shield collapses the herd | Medium (PoP → shield → origin) | Bounded to one origin fetch per shield (C8) | Low — shield is the inner ring; staleness bounded by shield TTL |
|
|
| Purge and serve fresh | Explicit invalidation received (surrogate-key or URL purge) | Medium (purge propagates, then fresh fetch) | Full fetch post-purge | Lowest — explicit invalidation is the tightest staleness bound (P6) |
|
|
| Serve from origin directly (bypass cache) | Content is non-cacheable (personalized, real-time) | Highest (every request hits origin) | Full fetch per request | Lowest for correctness, highest for origin load — use sparingly |
|
|
|
|
- The default is **serve from PoP** when fresh, **fetch fresh from
|
|
origin** on miss with **origin-shield** to bound origin load, and
|
|
**purge and serve fresh** when explicit invalidation is required.
|
|
Bypass-the-cache is for non-cacheable content only — bypassing for
|
|
cacheable content is a P1 violation (you have defeated the CDN).
|
|
- The correctness risk column is bounded by the invalidation
|
|
strategy (P6): every row except "bypass" carries staleness risk
|
|
that is bounded by TTL or explicit purge. A row with no
|
|
invalidation strategy is a P6 violation.
|
|
|
|
## What Violates CDN Discipline
|
|
|
|
| Violation | Principle |
|
|
|-----------|-----------|
|
|
| TTL-less edge cache under partition (stale-forever, no explicit invalidation) | P6 Cache Invalidation is Explicit |
|
|
| Cache key that varies on untrusted query parameters (poisonable) | P6, P9 (`domains/security/input-validation`) |
|
|
| All-cache purge as the steady-state invalidation path (origin stampede) | P6, C8 Economy |
|
|
| Non-normalized cache key (case-sensitive host, unsorted query) | P6, `domains/security/input-validation` |
|
|
| Bypass-the-cache for cacheable content | P1 Proximity is the Design Driver (defeats the CDN) |
|
|
| Multi-CDN with no per-CDN purge completion tracking | P8, P10 (stale cache invisible to the operator) |
|
|
| Origin fetch that is not idempotent under retry | P5 Edge Operations are Idempotent |
|
|
| Cache-hit ratio not measured | P10, `domains/observability/metrics` |
|
|
| Single-CDN deployed where geographic distribution requires multi-CDN | P8 Geographic Distribution |
|
|
| Shield PoP that 500s instead of serving stale under partition | P7 Partial Degradation is Engineered | |