Day 28 Hard CDN / Edge Anycast Tiered Cache Edge Compute

CDN 与 Edge — 把计算和缓存推到离用户 50 公里处CDN & Edge: Anycast, Tiered Cache, Edge Invalidation, Edge Compute

问题场景 + 需求约束

你要为一个全球性的电商 / 媒体站设计 CDN + Edge 层5000 万 DAU、分布在五大洲,内容是静态资产(图片/JS/视频分片,可强缓存)+ 半动态页面(商品页,秒级可 stale)+ 动态 API(购物车,不可缓存)混合。

核心矛盾:把内容推到离用户越近、命中率越高、延迟越低,但失效越难、一致性越弱。CDN 的设计就是在这条轴上找平衡。

高层架构

graph LR
    U["全球用户
同一个 Anycast IP"] subgraph PoP["最近 PoP (lower tier)"] EDGE["边缘节点
TLS 终止 · WAF · Edge Worker"] L1["边缘缓存 L1"] end UPPER["区域上层缓存
upper tier"] SHIELD["Origin Shield
单点回源收敛"] ORG[("Origin
app + 对象存储")] U -->|BGP 选最近| EDGE --> L1 L1 -.miss.-> UPPER UPPER -.miss.-> SHIELD SHIELD -.miss.-> ORG classDef client fill:#1a2530,stroke:#64c8ff,color:#e8eef5 classDef edge fill:#0e2030,stroke:#5eead4,color:#e8eef5 classDef cache fill:#1a1a30,stroke:#ffb450,color:#e8eef5 classDef origin fill:#2a1530,stroke:#ff7ab6,color:#e8eef5 class U client class EDGE,L1 edge class UPPER,SHIELD cache class ORG origin

Anycast 把用户引到最近 PoP;缓存分层收敛回源,越往右节点越少、命中越贵

组件职责边缘节点用 Anycast 接住流量,做 TLS 握手、WAF 过滤、运行 Edge Worker;边缘缓存命中则直接返回(占绝大多数请求);未命中向区域上层缓存请求,再不行才到 Origin Shield(一个指定 PoP 专门回源,把成千上万边缘的 miss 收敛成对 origin 的少量请求),最后才打 origin。这条「下层 → 上层 → shield → origin」的漏斗是命中率与 origin 保护的关键。

关键技术点

1. Anycast + BGP:一个 IP,全球就近接入

核心 trade-off:用路由层做负载均衡,换来零配置故障转移,但牺牲了对「用户落哪个节点」的精确控制。

原理:同一段 IP 前缀从全球几百个 PoP 用 BGP 同时宣告。用户的数据包由互联网路由器按 BGP 度量(AS 跳数等)送到「网络上最近」的 PoP——通常也是延迟最低的。这带来三个白送的好处:就近接入(无需 GeoDNS 猜测)、DDoS 稀释(攻击流量被分摊到所有 PoP,单点容量 = 全网容量)、自动 failover(PoP 宕机时撤回路由宣告,BGP 在秒级把流量收敛到次近 PoP,无需改 DNS、不等 TTL 过期)。

Trade-off(三种就近方案):
现实案例:

2. 边缘缓存键 + 分层缓存(Tiered Cache)

核心 trade-off:边缘节点越多命中率越分散,分层把 miss 收敛回源、保护 origin,代价是多一跳延迟。

原理:CDN 用 cache key(默认 host + path + 部分 query)索引对象。但全球几百个 PoP 各自独立缓存时,每个 PoP 第一次都要回源——命中率被「摊薄」,origin 仍会被几百倍放大的 miss 打到。Tiered Cache 把 PoP 分成下层(近用户)和上层(少数大节点):下层 miss 先问上层,只有上层 miss 才允许回源。这样 origin 看到的是少数上层节点的请求,而非全部边缘。配合 request coalescing(同一 key 的并发 miss 在节点内合并成一次回源,其余请求挂起等结果),可彻底压住 thundering herd。

Trade-off:
# 伪代码:边缘节点处理请求(含 coalescing)
def handle(req):
    key = cache_key(req.host, req.path, normalize(req.query))
    obj = cache.get(key)
    if obj and not obj.expired():
        return obj                        # 命中,绝大多数走这里
    # miss:同 key 只放一个回源,其余等待(single-flight)
    with coalesce(key):
        obj = cache.get(key)              # 双检:可能别人刚填好
        if obj and not obj.expired():
            return obj
        obj = fetch_from_upper_or_origin(key)   # tiered:先问上层
        cache.set(key, obj, ttl=obj.cache_control_ttl())
        return obj
现实案例:

3. 边缘缓存失效:purge 的传播与 stale 兜底

核心 trade-off:主动 purge 一致性强但传播有延迟、有放大;TTL 简单但要么太旧要么打爆 origin。

原理:边缘缓存有几百个副本,「让全球同时失效」本质是一个分布式广播问题。三种手段:① TTL 过期(被动,最简单,但 TTL 短=回源多、TTL 长=数据旧);② 显式 purge(按 URL / 按 tag / purge everything,主动推送失效消息到所有 PoP,秒级但有传播延迟,purge everything 会瞬间清空缓存→origin 被打爆);③ stale-while-revalidate / soft purge(标记为陈旧但仍先返回旧值,后台异步回源刷新)——用「短暂 stale」换「永不让用户等回源、永不雪崩 origin」。生产里通常是 长 TTL + tag-based purge + SWR 的组合:正常靠 tag 精确失效,兜底靠 TTL,体验靠 SWR。

Trade-off:
现实案例:

4. Edge Compute:在 PoP 上跑代码

核心 trade-off:isolate 启动近零、密度高但每请求受限;container/VM 能力强但冷启动慢、密度低。

原理:把鉴权、A/B 分流、个性化、API 聚合等逻辑从 origin 下沉到边缘,省一整趟回源 RTT。但「在几百个 PoP 跑用户代码」要求极高的密度和极低的冷启动。两条路线:V8 isolate(Cloudflare Workers)——单进程内跑成千上万个轻量沙箱,冷启动近乎为零、内存开销极小,但单请求 CPU/内存有硬上限、不能跑任意原生二进制;容器 / 微 VM(Lambda@Edge 等)——能力接近完整运行时,但冷启动以百毫秒计、单节点密度低。Edge 的另一难点是状态:边缘天然无共享状态,强一致数据要么回源、要么用专门的边缘存储(KV 最终一致、Durable Object 单点串行化)。

Trade-off:
方案冷启动密度/成本限制
V8 Isolate(Workers)~0(无进程启动)极高JS/WASM、CPU/内存硬上限
容器/微 VM(Lambda@Edge)百毫秒级能力全但贵、PoP 覆盖少
纯 CDN 规则(无代码)最高只能做声明式重写/路由
现实案例:

扩展与优化

常见陷阱 + 面试追问

深入资源

深入思考

1. 全网命中率 90%,origin 扛 20 万 RPS。如果命中率掉到 80%,origin 压力涨多少?为什么这是「先行告警」指标?

origin 承接的是 miss 流量 = 总 RPS × (1 − 命中率)。200 万 RPS 下:命中率 90% → miss 20 万;命中率 80% → miss 40 万翻倍。命中率每掉 10 个百分点,origin 压力的相对增幅远大于 10%——这是非线性的:从 99% 掉到 98%,miss 直接翻倍(1% → 2%)。

所以命中率是 origin 雪崩的先行指标:等到 origin CPU 报警时,缓存层往往已经失守一阵。常见诱因:cache key 误带参数、大规模 purge、新版本改了 Cache-Control、热点内容 TTL 集体过期(要加 TTL jitter)。监控应对命中率本身设阈值告警,而非只看 origin。

2. 商品调价后要求「全球数秒内一致」,但 CDN purge 有传播延迟。怎么设计让用户绝不看到错误价格?

关键认识:不要把易变的价格塞进可被边缘强缓存的 HTML。分离策略:

  • 骨架与价格分离:商品页骨架(图、描述)长 TTL 强缓存;价格用单独的不可缓存 API(或极短 TTL),由边缘/前端实时拉取。这样调价无需 purge 页面。
  • 版本化 URL:若价格必须内嵌,URL 带版本号/hash(/p/123?v=789),调价即换 URL——旧 URL 的缓存自然作废,无需等 purge 传播。
  • 对价格字段禁用 SWR:宁可让这一个请求回源等几十毫秒,也不返回 stale 价格。

本质:用「缓存可缓存的、实时取易变的」替代「缓存一切再想办法快速失效」。purge 永远有窗口,架构上消除依赖才是稳的。

3. 为什么 Anycast 能「天然抗 DDoS」,而传统单数据中心不能?容量模型差在哪?

单数据中心的抗攻击容量 = 那一个 数据中心的带宽/清洗能力。攻击者只要打出超过它的流量就能压垮——你被迫按「单点峰值」堆容量,极其昂贵。

Anycast 下,同一 IP 在全球几百个 PoP 宣告,攻击流量按源头地理被 BGP 自动分散到各 PoP:来自欧洲的攻击落欧洲 PoP,来自亚洲的落亚洲 PoP。于是有效抗攻击容量 ≈ 全网总容量,而非单点。攻击者要打垮你,得同时压垮全球所有 PoP。再叠加每 PoP 的清洗能力,单点故障也只丢一个区域(BGP 撤回路由后流量转移)。这就是为什么主流 CDN 把「Anycast + 大量 PoP」当作 DDoS 防护的地基。

代价:你失去对流量落点的精确控制,且需要全球 BGP 与海量 PoP 的运营能力——这正是 CDN 作为「专门生意」存在的原因。

4. Edge Worker 用 V8 isolate 而非容器。把「冷启动」和「多租户安全」放一起看,这个选择牺牲了什么?

isolate 在单进程内跑成千上万个 V8 沙箱,省掉了容器/VM 的进程与内核隔离开销——所以冷启动近零、单机密度极高、成本低。代价:

  • 隔离强度更弱(同进程):容器靠内核 namespace/cgroup、微 VM 靠硬件虚拟化隔离;isolate 同进程内只靠 V8 语言级隔离,一旦 V8 出沙箱逃逸漏洞,爆炸半径更大。需要靠 Spectre 类侧信道缓解、进程级再分组等补强。
  • 能力受限:只能跑 JS/WASM,不能任意原生二进制、不能开本地线程/文件系统,单请求 CPU/内存有硬上限——重计算/长任务不适合。
  • 无持久本地状态:isolate 随时被回收,状态要外置(KV 最终一致 / Durable Object 串行化单点)。

所以选型是「海量轻量短请求 选 isolate;重逻辑/需完整运行时 选容器/微 VM」。Cloudflare 赌的是边缘场景绝大多数属于前者。

5. CDN 的一条配置能同时推全球——这既是优势也是「全局爆炸半径」。如何让边缘发布像后端一样安全?

Cloudflare 2019、Fastly 2021 都是「一个改动瞬时全球生效」造成的全网级事故。把边缘当成最高风险的发布面来治理:

  • 配置也要灰度:规则/Worker/缓存配置按 PoP 或百分比分批推(先 1% PoP→ 监控 → 放量),而不是一把推全球。Cloudflare 复盘后正是加了这一条。
  • kill switch / 快速回滚:每个边缘特性带全局开关,出事一键关,且回滚路径不依赖正在出事的组件。
  • 资源耗尽防护:给规则/Worker 设 CPU/内存/正则回溯上限,单条逻辑不能拖垮整节点(2019 正是正则 CPU 打满)。
  • 变更前静态校验:上线前对正则、配置做性能/安全检查,把「灾难性回溯」挡在部署前。
  • Multi-CDN 兜底:单一 CDN 故障时能切走,避免供应商级单点。

一句话:边缘的便利来自「全局即时生效」,而安全来自给这个全局生效加上灰度、限额和开关