它是什么
内容分发网络(CDN)从靠近用户的接入点(PoP) 提供内容,尽可能缓存,使大多数请求根本不会到达你的服务器。CDN 的设计围绕四个问题:路由(用户如何到达最近的健康 PoP)、缓存层级(什么保存在哪里、未命中时会发生什么)、失效(变更的内容如何停止被提供),以及对视频来说的分段交付(为什么流媒体内容缓存得这么好)。
Important
核心要点: 产品就是边缘命中率。用 anycast 到达边缘(故障切换快),用请求合并和区域 shield 让未命中不触及源站,用版本化 URL 让部署不触碰任何已缓存的字节,只把 purge 留作应急手段。
何时需要它
“设计一个 CDN”或”设计视频流媒体”会要求这四个问题全部回答。最常见的弱回答是先设计 purge 路径,却从不设计版本化方案。
Note
面试信号: 描述一条在 PoP 和 shield 两处都做合并的未命中路径(“每个对象一次源站取回,而不是每个 PoP 一次”),说明你想过源站的暴露面。说”我会给 URL 加版本,让一次部署不需要 purge”,说明你运营过这种系统。
到达边缘:路由
| 选项 | 做法 | 优点 | 缺陷 |
|---|---|---|---|
| GeoDNS 引导 | CDN 的 DNS 根据解析器的位置和负载,返回某个 PoP 的地址 | 智能、集中、可按客户调整 | DNS 看到的是解析器而不是用户(一个公共解析器会”定位”整个国家的用户);故障切换受限于解析器会忽略的 TTL,所以排空一个 PoP 要几分钟到几小时 |
| BGP anycast | 每个 PoP 宣告同一个 IP 前缀;互联网路由把流量送到拓扑上最近的那个 | 故障切换像撤销路由一样快(几秒);不需要客户端配合;一个 IP 简化了证书 | ”最近”是 BGP 的看法(跳数和对等关系,而不是延迟);负载控制粗糙;连接中途的路由变化可能把 TCP 连接移到没有它状态的 PoP |
| 混合 | 数据面用 anycast,DNS 作为区域和负载的粗调 | 故障切换快,外加负载偏置 | 复杂 |
通常的选择是混合,anycast 优先:故障切换的敏捷性最重要,而 anycast 在负载控制上的缺口可以用区域性的 DNS 映射来弥补,DNS 在敏捷性上的缺口则无法从服务端弥补。现代传输协议(QUIC 连接 id)能熬过路径变化,缓解了 anycast 的连接中途问题。
缓存层级与读路径

每一层回答它能回答的;每次未命中在向上之前都先做合并。
- 连接落在最近的 PoP;TLS 在那里终止(会话复用可以省一次往返,在 50 ms 的预算下这很重要)。
- 前端计算缓存 key:host、path,以及协商参数中被归一化的部分(例如把
Accept-Encoding折叠成几类),然后对它做**一致性哈希**,映射到 PoP 里拥有它的缓存节点,这样每个对象大约只存在一个节点上,PoP 的磁盘是累加而不是重复。非常热的对象会被复制到多个节点。 - RAM 命中(热度分布的头部)以微秒级提供,SSD 命中以毫秒级提供。响应带着正确的
Cache-Control和Ageheader,所以边缘是一个合规的 HTTP 缓存,浏览器缓存可以免费叠加在上面。 - 未命中会被合并:每个 PoP 每个对象只发一次向上游的取回,所有并发的请求者都等在它上面。取回发往一个区域 shield,它在触碰源站之前重复同样的逻辑。
- 响应流式穿过:用户收到第一批字节时,尾部还在到达,并且对象在途中被写入 SSD(或者由一个准入策略决定不缓存,避免缓存只被访问一次的对象)。
算例:为什么 shield 值得多一跳。 Shield 给未命中增加了一次区域往返,但它去掉了源站对 PoP 数量的暴露。没有它,一个在所有地方同时过期的对象,会引起多达 200 次同时到源站的取回(每个 PoP 一次);每个区域有一个 shield,就只有寥寥几次(加上合并,则是一次)。Shield 还集中了长尾:一个冷到无法在任何单个 PoP 里驻留的对象,可以在共享的区域缓存里保持温热。
源站卸载算术。 在每秒 1,000 个请求、边缘命中率 95% 时,源站看到每秒 50 个请求,减少了 20 倍;在 99% 时,它看到 10 个。命中率是要优化的数字:它取决于缓存 key 的归一化(不要因为无关的 header 或查询参数而让 key 碎片化)、TTL,以及对象的大小和热度(偏斜的、类 Zipf 的热度让一个小的 RAM 层非常有效)。
失效
| 方案 | 做法 | 角色 |
|---|---|---|
| 版本化 URL | 内容哈希写在 URL 里(app.9f3ab2.js);对象不可变,并带 Cache-Control: immutable, max-age=31536000 缓存;更新就是部署指向新哈希的 HTML | 首选。 一致性问题被删除,而不是被解决:任何已缓存的东西都不需要改变 |
| 按标签 purge | 控制面把 purge 扇出到每个 PoP,按代理标签(surrogate tag)(product-123)而不是按 URL | 应急手段:处理失误、下架,以及你无法控制的可变 URL |
按标签 purge 优于按 URL,因为一次逻辑变更会触碰许多缓存 key(变体、编码),而标签索引让它只需一条消息。要让”5 秒内 purge 完成”在网络分区期间依然成立,要把 purge 实现为一次 epoch 提升,而不是删除:条目记录它被缓存时的 epoch,一次 purge 提升该标签的 epoch,查找时把更旧的条目视为过期(在提供前重新验证)。一个在 purge 期间离线的节点,会在接收流量之前随着配置的重新同步拿到新的 epoch,所以正确性不依赖于删除与重连之间的赛跑。
Caution
常见错误: 先设计 purge 路径,从不设计版本化方案。一个客户每次部署都 purge 的 CDN,等于让每次发布都变成跨几百个 PoP 的全局一致性问题。正确的设计是让部署不触碰任何已缓存的字节,把 purge 留给罕见的例外。
视频:为什么分段的内容缓存得好
视频不是一个大文件;它是一个清单,列出许多短的、不可变的分段(每段几秒),每段有多个码率。这种结构适合 CDN:
- 每个分段都是一个小的、不可变的、可单独缓存的对象,所以目录中热门的头部留在边缘,冷的长尾通过 shield 按需取回。
- 自适应码率(ABR): 播放器测量自己的吞吐和缓冲,为每个分段选择它能承受的清晰度(分辨率和码率),并无缝地升降。同一内容有多个画质级别(一个码率阶梯)。
- 直播流随着新分段的出现而变更清单,所以清单的生命周期非常短,而分段仍然不可变。
Warning
生产实践: 一个碎片化的缓存 key(URL 里带了 session token 或追踪参数)会悄悄地毁掉命中率,一个在所有地方同时过期的热门对象会冲击源站。要归一化 key、错开或给 TTL 加抖动、在重新验证期间提供过期内容,并把命中率和源站请求率当作一等指标来盯着。
如何选择
| 需求 | 选择 | 原因 |
|---|---|---|
| 快速故障切换和一个 IP | Anycast 优先的路由,DNS 作粗调 | 撤销一条路由,流量几秒内转移 |
| 保护源站免受 PoP 数量的影响 | 带合并的区域 shield | 每个对象一次取回,而不是每个 PoP 一次 |
| 更新资源而不 purge | 内容哈希的版本化 URL,不可变缓存 | 任何已缓存的东西都不需要改变 |
| 立刻移除一个有问题的资源 | 通过 epoch 提升做标签 purge | 快速且对分区安全 |
| 高命中率 | 归一化的缓存 key 与合理的 TTL | key 决定了什么能被共享 |
| 大规模视频 | 不可变分段、ABR 阶梯、CDN 交付 | 可缓存的小对象 |
相关
- 演练:设计 CDN — 完整的 CDN 设计,包括控制面。
- 演练:设计视频流媒体平台 — 建立在 CDN 之上的分段交付与自适应码率。
- 参考:缓存访问与失效模式 — CDN 在边缘应用的 cache-aside 与踩踏防护模式。