问题与适用场景
设计一个多租户拉取式内容分发网络,共有 50 个接入点(PoP)。峰值流量为每秒 200 万个 GET 和 HEAD 请求,平均可缓存 GET 响应大小为 256 KiB。客户源站合计最多安全承受每秒 2 万次 回源。CDN 需要分发带内容哈希的静态资源、图片、视频分片,以及稳定 URL 下的可变文档。
目标是:正常条件下缓存响应的 p99 首字节时间低于 50 毫秒,请求可用性达到 99.99%,并在 60 秒内让 99% 的健康 PoP 应用每次清除。系统还必须持续跟踪完整传播和落后 PoP。这些数字是 面试假设,不代表任何供应商的实际承诺。
当用户分布在不同地区、源站距离主导延迟、对象可以重复复用,而且源站带宽或计算能力有限时, 这套设计适用。单区域、复用很少的内部服务可能只需要区域反向代理。面试范围要求设计 CDN 核心 机制;真实生产仍可在比较需求、安全边界、运维成本和供应商故障模型后选择托管 CDN。
面试官在考察什么
第一,候选人能否把控制面与数据面拆开。租户接入、源站配置、证书、缓存规则和清除命令 需要持久的管理流程;管理路径不可用时,请求服务仍要根据最后一个已知良好配置继续运行。
第二,缓存边界是否正确。缓存键漏掉一个响应差异维度,可能在语言、编码、设备或租户之间返回 错误内容甚至泄露数据;把每个 Cookie 和请求头都加入键,则会把缓存切碎到几乎每次都未命中。 答案必须定义哪些请求可缓存、哪些维度会改变响应,以及哪些私有响应必须绕过共享存储。
第三,能否保护源站。50 个 PoP 面对一个突然变热的新对象时,即使还没发生重试,也可能同时 产生 50 次冷填充。每层请求合并、区域屏蔽层、有限的源站并发和重试预算分别解决不同问题。 任何故障都不能悄悄把每秒 200 万次边缘请求转成回源流量。
第四,失效是否正确。只删除当前字节的清除会与旧填充竞态,让过期内容重新出现。高质量答案会 使用有序 generation 或墓碑,保证事件可幂等重放,并同时测量常见传播目标和长尾。
最后,候选人要量化吞吐,明确一致性与过期内容契约,覆盖故障和安全边界,并用能证明用户可见 行为的测试收尾。供应商产品名清单无法提供这类证明。
回答前要澄清的问题
- CDN 是否拥有原始对象? 不拥有。本题是拉取式 CDN,客户源站仍是权威来源。
- 哪些方法可以缓存? 基线只处理
GET和HEAD;不安全方法直接透传,绝不进入共享缓存。 - 哪些内容属于私有内容? 带授权信息或用户专属 Cookie 的请求默认绕过共享缓存,除非租户提供经过评审、明确的分区策略。
- 可变内容允许多旧? 每条路由定义 TTL 和可选的有限陈旧窗口;紧急变化使用清除,但清除不能代替授权或撤销检查。
- 每个查询参数都有效吗? 只有真正改变响应的参数需要保留;租户还可在规范化后移除已知跟踪参数。
- 是否需要字节范围? 大媒体需要。缓存键和元数据必须区分完整对象与已验证分片,源站还要提供稳定校验器或版本化 URL。
- 如何选择较近的 PoP? DNS 和/或 Anycast 把用户路由到可达 PoP;BGP 选路不保证地理最近,所以仍要依赖健康度和实测延迟。
- 控制面故障怎么办? 现有流量使用带签名的最后一个已知良好配置;无法确认安全性的变更关闭并排队等待恢复。
- 源站故障时能否返回旧内容? 只有路由明确允许
stale-if-error,并且最大年龄有界时才可以。 - 怎样定义清除完成? 60 秒目标覆盖 99% 的健康 PoP;系统还要分别记录每个确认、落后节点、重试与探测结果。
30 秒回答框架
“我会拆分控制面和数据面。DNS 与 Anycast 将请求导向健康 PoP;边缘选择租户签名配置,规范化 白名单缓存键,再查 RAM 与 SSD。未命中先在边缘和区域屏蔽层合并,只有受预算约束的填充能访问 源站;旧内容只在明确窗口内返回。不可变资源使用版本化 URL;可变 URL 使用有序清除 generation 和墓碑,填充发布前比较 generation,防止旧字节复活。最后验证请求与字节命中率、回源量、命中 p99 首字节时间、陈旧年龄、清除延迟和故障行为。”
分步深入设计
第一步:量化流量与源站预算
用平均 GET 大小作为峰值请求组合的规划上界时,不计协议开销的响应带宽为:
2,000,000 请求/秒 × 256 KiB × 8 = 4.19 Tb/s如果峰值持续一整天,边缘响应字节约为 45.3 PB。简单平均到 50 个 PoP 后,每个 PoP 是每秒 4 万个请求和 10.5 GB/s;实际流量存在地域和时间倾斜,因此容量要依据各 PoP 实测峰值、余量、 对象大小分位数和故障转移增量,而不能只看平均值。
源站请求上限等于边缘峰值的 1%:
20,000 / 2,000,000 = 1%这不表示边缘命中率目标简单设成 99% 就够了。屏蔽层命中、不可缓存路由、填充、重新校验和重试 都占用同一个源站预算。源站调度器需要租户级、源站级和全局并发与请求速率上限。
第二步:拆分控制面与数据面
控制面保存租户、域名、源站身份、证书、缓存策略、规范化规则、陈旧上限、签名 URL 密钥和配置 版本。通过校验的变更先持久提交,再编译成带签名快照,通过版本化流分发;PoP 确认已应用版本。 证书私钥进入专门的密钥管理边界,不能放在普通配置存储中。
数据面负责 TLS、租户查找、策略执行、请求规范化、缓存、回源和日志。缓存命中时绝不同步访问 控制面数据库。控制面故障期间,PoP 保留最后一个已知良好的签名快照。旧配置有明确寿命;证书 过期、租户已撤销或安全策略含糊时应关闭,而不能永远运行旧规则。
第三步:完成流量路由与租户隔离
DNS 可以返回区域域名或 Anycast 地址;Anycast 网络可由多个 PoP 宣告同一地址。网络先选择可达 路径,服务健康检查再移除坏 PoP,并把流量排到其他位置。设计要观察路由变化、转移负载和延迟, 因为“最近”需要实测,BGP 不提供地理距离保证。
边缘在任何缓存查找之前,根据 SNI 和规范化 Host 映射租户。租户 ID 是每个缓存命名空间隐含的 第一段。源站只接受经过 mTLS、签名请求、私网连接或轮换密钥认证的 CDN 流量,不应保留可公开绕过 的入口。源站地址和重定向目标需要白名单,避免 SSRF。
第四步:定义缓存资格与缓存键
基线只在路由策略和 HTTP 字段允许共享复用时缓存成功的 GET 与 HEAD 响应。private、 no-store、授权信息、用户专属 Cookie、Set-Cookie 与不支持的 Vary 通常都绕过存储。负面响应 只能按状态使用很短 TTL,避免一次瞬时故障变成长时间故障。
概念上的缓存键为:
tenant_id | canonical_scheme_host | normalized_path | selected_query |
encoding_variant | approved_vary_dimensions | object_generation策略判断、查找、日志、填充和清除都使用同一次规范化结果。只有真正改变字节的查询参数和请求头 进入键。源站按语言返回不同内容时加入 Accept-Language 是正确做法;加入任意 Cookie 或 User-Agent 会让键基数爆炸。响应的 Vary 必须符合该路由允许的维度,否则绕过缓存。
新鲜度遵循租户策略和 HTTP 语义:新鲜条目直接返回;陈旧条目用 ETag 或 Last-Modified 重新校验;stale-while-revalidate 与 stale-if-error 只能在明确边界内使用。no-cache 表示 复用前必须校验,no-store 表示不得存储。CDN 策略不能放宽源站更严格的隐私指令。
第五步:构建 RAM、SSD、屏蔽层与回源路径
每个 PoP 在 RAM 中保存热点元数据和小对象,并使用带准入控制的更大 SSD 缓存。准入与淘汰综合 请求频率、字节大小、最近访问和获取成本,避免一次大型冷对象扫描淘汰有价值工作集。源站仍是 权威来源;边缘缓存丢失只影响性能,原始数据仍由源站保存。
未命中时,singleflight 表按准确缓存键合并调用者。一个调用者请求区域屏蔽层,其他调用者有限 等待或使用政策允许的旧条目。屏蔽层跨多个 PoP 再次查找和合并,只有选出的填充进入源站调度器。 第二道合并边界可以防止一个冷对象从每个 PoP 各触发一次回源。
每次填充都有截止时间、最大大小、内容类型校验、校验和、租户字节预算与重试预算。指数退避和 抖动重试仍然消耗源站预算。对冲只能用于幂等读取,并用硬上限防止双倍回源。大对象可一边向用户 流式传输,一边写临时缓存;只有长度、校验器和校验和完整后才让条目可见。
第六步:消除清除与填充竞态
不可变资源默认使用内容寻址文件名:新字节产生新 URL,旧 URL 自然过期。稳定 URL 需要清除 API, 支持精确对象、经批准的前缀或标签,以及租户范围的紧急清除。广泛清除会造成全球未命中风暴, 所以必须限流并要求更强授权。
清除协调器先把 {tenant, selector, generation, issued_at} 提交到持久有序日志,再返回已接受。 PoP 幂等应用事件,推进选择器最低 generation,删除匹配字节,并保留足以覆盖旧填充和延迟事件的 墓碑,随后报告已应用 generation。分层扇出、重试和区域中继避免一个慢 PoP 阻塞常见路径。
缓存发布填充对象前,要比较获取开始时捕获的 generation 与当前最低 generation。若获取期间清除 已经推进,则丢弃这些字节,或使用新 generation 重新获取。这一比较避免旧响应在删除后到达并复活 过期内容;边缘层和屏蔽层都要执行同一规则。
API 分别报告已接受、99% 已传播,以及完成或超时状态。综合探针从多个区域请求已清除键,并校验 版本头或内容哈希。安全撤销仍应使用权威在线检查或独立受限的凭证寿命;60 秒清除 SLO 不等于 即时撤销。
第七步:明确处理过载与故障
- PoP 故障:撤回或停止宣告路由,把流量排到健康 PoP,并为转移流量预留容量。
- SSD 丢失:通过准入控制逐步重建;不要预热所有对象,也不要绕过屏蔽层。
- 屏蔽层故障:选择备用屏蔽层;若允许直连源站,仍沿用相同回源预算。
- 源站超时或 5xx:只有策略允许时才返回有界旧内容;否则明确报错并避免重试放大。
- 控制面故障:使用最后一个已知良好签名配置继续服务;安全变更无法验证时排队并拒绝执行。
- 清除流延迟:幂等重试并暴露落后 PoP;已知关键 generation 但字节不可信时绕过或重新校验。
- 热点对象突发:合并填充、跨缓存进程复制对象、防止单进程 NIC 或锁饱和,并限制滥用租户。
第八步:保护并验证完整系统
使用租户范围证书终止 TLS,并保护源站身份。限制请求大小、请求头数量、Range 数量和响应大小。 对含糊路径与 HTTP 字段只做一次规范化,防止请求走私与缓存键解释不一致。配额、密钥、日志、 清除权限和缓存命名空间都按租户隔离。签名 URL 或 Cookie 在查找前验证,其策略不能意外把私有响应 变成公开对象。
分别观察请求命中率和字节命中率、命中首字节时间、未命中延迟、屏蔽层命中率、源站 QPS 与带宽、 被合并调用者、缓存键基数、淘汰字节、陈旧年龄、各 PoP 清除延迟、配置版本、错误率与故障转移 负载。日志记录隐私安全的键摘要、租户、PoP、结果类型、年龄、generation、上游层和 trace ID。
验证覆盖:50 个 PoP 同时冷启热点对象、清除与故意放慢的填充竞态、重复和乱序清除事件、恶意 Vary、授权与 Cookie 绕过、部分 Range 填充、SSD 重启、屏蔽层丢失、源站限流、PoP 撤回以及 控制面故障。验收要求包括正常条件下缓存命中 p99 首字节时间低于 50 毫秒、请求可用性 99.99%、 回源不超过每秒 2 万次,以及 99% 健康 PoP 在 60 秒内应用清除且旧内容不复活。
高质量参考回答
“我先固定容量边界。每秒 200 万个 256 KiB 响应约等于 4.19 Tb/s 峰值响应流量;源站只能承受 边缘请求量的 1%,所以源站保护是一条硬不变量。
我把持久控制面与请求数据面分开。租户配置、证书、源站身份、缓存规则和清除都版本化并审计。 PoP 使用带签名的最后一个已知良好配置服务,不会每次请求都查控制面数据库。DNS 与 Anycast 把 用户导向健康 PoP;SNI 与 Host 在带租户命名空间的缓存查找前确定租户。
缓存键包含租户、规范 URL、只包括会改变响应的查询与请求头维度,以及 generation。私有、授权、 no-store 与不安全响应绕过共享缓存。命中来自 RAM 或 SSD;未命中先在边缘合并,再到区域屏蔽层 再次合并,最后通过源站级和全局预算。新鲜、重新校验和有限陈旧路径相互分开。
不可变资源使用版本化 URL。可变 URL 清除先向持久日志提交递增 generation。每层保留最低允许 generation 和墓碑;填充发布前检查 generation,因此清除前发出的请求不能在清除后恢复旧内容。 我会明确暴露 99% 已传播与完整状态,不隐藏落后节点。
最后用请求和字节命中率、回源 QPS、命中 p99 首字节时间、陈旧年龄、清除延迟和配置版本证明 设计,再注入热点冷未命中、清除填充竞态、PoP 与屏蔽层故障、源站限流、乱序清除、缓存键污染 和控制面故障,并把四项既定 SLO 作为验收标准。”
常见错误
- 把地理最近 PoP 说成保证 → BGP 选择网络路径,不按直线距离 → 测量延迟与健康度,并设计路由撤回和故障转移。
- 把所有请求头加入键 → 基数爆炸,绝大多数流量未命中 → 只允许改变响应的维度,并拒绝不支持的
Vary。 - 缓存命名空间漏掉租户 → 相同 URL 可能跨租户复用 → 查找前推导租户,并把它作为隐含键前缀。
- 清除只删除字节,没有 generation → 旧的在途填充可能重新发布 → 推进墓碑,插入前比较 generation。
- 只加边缘锁 → 50 个 PoP 仍可发出 50 次回源 → 屏蔽层再次合并,并保留全局回源预算。
- 每次回源失败都重试 → 重试会放大故障 → 使用截止时间、有限预算、抖动,以及路由专属旧内容或错误策略。
- 用清除实现即时安全撤销 → 传播存在可测长尾 → 安全决策使用权威检查或有限凭证寿命。
- 只报告请求命中率 → 大量小命中会隐藏昂贵的大对象未命中 → 同时跟踪字节命中率、回源带宽、大小分布与获取成本。
追问深入讨论
追问一:怎样支持大型视频对象和 Range 请求?
优先使用不可变分片 URL,并在可行时缓存完整分片。合并范围前校验 Content-Range、对象长度、 校验器和 generation。限制 Range 数量及放大倍数,绝不能让两个响应表示共享同一个部分对象键。 超大对象可按受控网格对齐分块,使重叠请求复用字节,同时避免任意碎片。
追问二:怎样防止缓存键投毒?
对 URL 和 HTTP 字段只规范化一次,拒绝含糊编码,并把所有会改变源站字节的已批准输入加入键。 不要把源站用于选择响应、却未进入键的请求头继续透传。测试冲突请求头、重复字段、路径编码、 查询顺序和 Host 规范化,确保边缘与源站解释一致。
追问三:个性化 HTML 可以进入边缘缓存吗?
只有在产品和安全契约明确时才可以。更安全的方案是缓存公开壳,再单独读取私有数据。如果必须缓存 完整 HTML,要用有界、已验证的身份或群组分区,禁止共享复用,定义退出登录行为,并测试跨用户 隔离。把任意会话 Cookie 放进公开缓存键既危险,也会摧毁命中率。
追问四:怎样在单一全球屏蔽层和区域屏蔽层之间选择?
单一屏蔽层最能合并未命中,却可能增加距离并集中故障;区域屏蔽层降低延迟和爆炸半径,但可能让 多个区域分别回源。根据源站位置、可缓存性、区域需求、可接受延迟和回源预算选择,再实际测试 屏蔽层故障转移,不能假设一种拓扑适合所有场景。
追问五:怎样发布新的缓存键策略?
把它编译成新配置版本,同时影子计算新旧缓存键;在不使用新键返回内容时,比较基数、命中率、 隐私分类和回源量。按租户与 PoP 灰度,保留可回滚版本,只预热已证实的热点对象。缓存键变更会 制造冷缓存事件,因此也必须进入普通填充的源站预算。
追问六:99.99% 可用性一定需要多 CDN 吗?
不一定。若一个供应商的实测故障模型、PoP 冗余、路由撤回、源站设计与运维能支持目标,单 CDN 也可能足够。多 CDN 能降低部分供应商风险,却会增加 DNS 或调度一致性、重复配置、清除协调、 日志归一化、证书管理与共享屏蔽层问题。只有测试证明新增控制面能改善既定可用性目标时才引入。