Skip to main content

参考:CDN 与边缘交付

一份关于 CDN 设计的参考——anycast 与 GeoDNS 路由、带请求合并的 PoP 与区域 shield、版本化 URL 与按标签 purge,以及视频分段为什么缓存得好。

它是什么

内容分发网络(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 的连接中途问题。

缓存层级与读路径

CDN 读路径:请求落在最近的 PoP,按缓存 key 哈希到所属的缓存节点,命中时从 RAM 或 SSD 提供;未命中时进入带合并的区域 shield,再到源站,响应流式返回并在途中被缓存

每一层回答它能回答的;每次未命中在向上之前都先做合并。

  1. 连接落在最近的 PoP;TLS 在那里终止(会话复用可以省一次往返,在 50 ms 的预算下这很重要)。
  2. 前端计算缓存 key:host、path,以及协商参数中被归一化的部分(例如把 Accept-Encoding 折叠成几类),然后对它做**一致性哈希**,映射到 PoP 里拥有它的缓存节点,这样每个对象大约只存在一个节点上,PoP 的磁盘是累加而不是重复。非常热的对象会被复制到多个节点。
  3. RAM 命中(热度分布的头部)以微秒级提供,SSD 命中以毫秒级提供。响应带着正确的 Cache-Control 和 Age header,所以边缘是一个合规的 HTTP 缓存,浏览器缓存可以免费叠加在上面。
  4. 未命中会被合并:每个 PoP 每个对象只发一次向上游的取回,所有并发的请求者都等在它上面。取回发往一个区域 shield,它在触碰源站之前重复同样的逻辑。
  5. 响应流式穿过:用户收到第一批字节时,尾部还在到达,并且对象在途中被写入 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 加抖动、在重新验证期间提供过期内容,并把命中率和源站请求率当作一等指标来盯着。

如何选择

需求选择原因
快速故障切换和一个 IPAnycast 优先的路由,DNS 作粗调撤销一条路由,流量几秒内转移
保护源站免受 PoP 数量的影响带合并的区域 shield每个对象一次取回,而不是每个 PoP 一次
更新资源而不 purge内容哈希的版本化 URL,不可变缓存任何已缓存的东西都不需要改变
立刻移除一个有问题的资源通过 epoch 提升做标签 purge快速且对分区安全
高命中率归一化的缓存 key 与合理的 TTLkey 决定了什么能被共享
大规模视频不可变分段、ABR 阶梯、CDN 交付可缓存的小对象

相关