CDN 这个词,做前端的、做后端的、做运维的,基本都绕不开。但真被人当面问一句“CDN 到底是个啥,它凭什么快”,能一口气讲清楚的人并不多。我见过不少团队,静态资源往 CDN 上一推,页面打开确实快了一截,可等到缓存不刷新、回源带宽打满、灰度发布串了版本,立刻就抓瞎了——因为他们只知道“推上去”,不知道推上去之后到底发生了什么。这篇就把 CDN(内容分发网络)从里到外拆一遍:它解决的到底是哪几类问题,边缘节点、调度系统、缓存层、回源链路各自在干什么,一次用户请求从按下回车到首字节落地中间经历了哪些环节,TTL 和缓存键这些参数该怎么定,以及最值钱的部分——出问题之后怎么一步步排查。刚接手静态资源托管的新手能照着配,已经在盯命中率曲线的老手也能找到几个能直接抄的细节。
1. CDN 到底在解决什么问题
1.1 单点源站的三个硬伤
先把场景摆出来:一台服务器放在某个机房,带宽 100Mbps,全国用户直接访问它。这个架构在用户量小的时候毫无问题,一旦访问量上来,三个硬伤会同时暴露。
第一个是带宽天花板。100Mbps 换算下来理论峰值也就 12.5MB/s,一个 2MB 的首屏图片,理论上同时只能服务 6 个用户。真实情况更糟,因为 TCP 慢启动、并发连接竞争,实际吞吐往往只有理论值的六七成。业务方一句“首页再加两张 Banner”,带宽就爆了。
第二个是并发连接数。服务器再强,能维持的并发连接也是有上限的,每个连接占内存、占文件描述符、占 CPU 时间片。静态资源的特点是“请求多、单请求小、连接生命周期短”,这种模式对连接数的消耗极其凶残,一台机器扛几万 QPS 的静态请求就已经很吃力了。
第三个,也是最容易被忽略的,是距离。服务器在华东,用户在西北,物理距离一千多公里,这段距离不是靠加 CPU 能解决的。光在光纤里跑,速度大约是每毫秒 200 公里,一千公里单程就是 5ms,加上实际路由绕行、中间设备排队,真实 RTT 往往在 30ms 以上。这 30ms 会跟着每一个 TCP 握手、每一次 TLS 协商、每一个串行请求反复叠加,最后变成用户能明显感知的“卡”。
这三个硬伤叠在一起,就是 CDN 存在的全部理由。它不是为了“技术先进”而生的,它是被带宽、并发和距离这三件事硬生生逼出来的解法。
1.2 物理距离换算成秒:RTT 与丢包的真实代价
很多人对延迟的理解停留在“慢一点点”,实际上 RTT 对吞吐的影响是超线性的,这里面有个公式值得记住。
TCP 的稳态吞吐大致服从这个关系:吞吐 ≈ (MSS / RTT) × (1.22 / √丢包率)。MSS 按常见的 1460 字节算,假设 RTT 是 60ms、丢包率 1%,代进去:1460 / 0.06 ≈ 24333 字节/秒的基础值,乘以 1.22 / 0.1 ≈ 12.2,得到约 29.7 万字节/秒,也就是2.3Mbps 左右。同样条件下把丢包率降到 0.1%,吞吐直接涨到7.4Mbps上下——三倍多。
这说明什么?说明跨运营商、跨地域的长链路,真正的杀手不是“带宽不够”,而是丢包。长链路经过的路由跳数多,任何一跳拥塞都会丢包,而 TCP 一旦检测到丢包就把拥塞窗口砍半,长肥管道(带宽时延积大的链路)下恢复极慢。这也是为什么同一个文件,本地下载 5MB/s,跨国拉取只有 300KB/s,而两边的“带宽”看起来都是够的。
CDN 的核心价值之一,就是把这条长链路切成两段:用户到边缘节点是一段短链路(通常同城或同省,RTT 个位数毫秒、几乎不丢包),边缘节点到源站是一段长链路,但这段链路上跑的是回源,可以慢慢跑、可以复用连接、可以并发预取,不需要让终端用户等。用户感知到的只有前面那一小段,体验自然就上去了。
1.3 CDN 的三重身份:加速器、缓冲垫、流量闸门
把 CDN 只当成“加速器”是低估它了。实际用下来,它至少同时扮演三个角色。
加速器是最直观的:静态资源就近取,首字节时间(TTFB)从几百毫秒压到几十毫秒。这个提升对转化率的影响,做电商的都懂。
缓冲垫是第二重身份,也是稳定性层面的关键。源站不可能永远健康,发布时会有短暂 502,数据库抖动时接口会超时,机房偶尔会有网络割接。CDN 的缓存层在这里起到的作用是:即使源站挂了,只要缓存还在 TTL 内,用户依然能拿到内容。我经历过一次源站数据库主从切换导致的接口雪崩,静态资源因为全部走了 CDN,页面框架和样式依然是完整的,只是数据区加载失败——这个降级体验比白屏好太多。
流量闸门是第三重,也是最容易被低估的。所有请求先到边缘,边缘可以做限频、可以做鉴权、可以拦掉明显的异常流量、可以按 UA 或 Referer 做分流。相当于把一个全局的入口控制点前移到了离用户最近的地方。促销活动、突发热点、爬虫扫站,这些场景下这层闸门的价值远超“加速”本身。
理解了这三重身份,后面所有的配置和排障思路就都能挂上去:加速靠缓存和调度,缓冲靠 TTL 和源站保护,闸门靠边缘规则。
2. CDN 的骨架:四层组件各司其职
2.1 边缘节点:离用户最近的那台机器
边缘节点(Edge Node)是整个体系里唯一和用户直接打交道的部分。它的部署逻辑很朴素:往用户密集的地方堆。国内常见的做法是在省会城市和骨干网互联节点铺点,一个中等规模的商业 CDN 在全国有几百到上千个节点,自建的小型 CDN 可能只有十几个。
单节点的硬件配置通常是这个量级:万兆网卡起步,SSD 缓存盘几十 TB,内存几百 GB,能维持几十万并发连接。为什么强调 SSD?因为缓存命中判断是要读盘的,机械盘在几十万 QPS 下 IOPS 直接打死,缓存层就退化成摆设了。
边缘节点内部一般分几层:最前面是接入层(做 TCP/TLS 卸载、协议解析、限频),中间是缓存层(内存做一级热点缓存,SSD 做二级),最后是回源模块(管理到上级节点或源站的连接池)。这个分层很重要,排查问题的时候,你得知道请求是在哪一层卡的——是 TLS 握手慢,还是缓存查不到,还是回源超时。
还有个细节:边缘节点不是“一台上线就完事”,它需要持续的健康检查和灰度能力。节点故障是常态,硬盘会坏、网卡会抖、上游路由会变。所以调度系统必须能快速把故障节点摘掉,而这个“快速”通常要求在秒级到十秒级完成,否则用户会看到一片超时。
2.2 中间层与源站:把回源压力摊薄
只在边缘和源站之间做两级是不够的。想象一下:某个资源刚发布,全国 500 个边缘节点缓存全是空的,第一个请求到达时,500 个节点几乎同时回源,源站瞬间被打穿。这就是经典的回源风暴。
中间层(有的叫父层、有的叫中层、有的叫源站组)就是为了解决这个问题。它的逻辑是:边缘节点不回源站,而是回中间层;中间层只有少数几个节点,它们去回源站。这样源站看到的回源请求量被压缩了一到两个数量级,而且中间层的缓存命中率通常很高(因为它服务的是全国边缘,热点资源几乎必中)。
中间层的规模不大,通常每个大区一到两个,但单节点的存储和带宽配置要高得多。它的另一个作用是跨区域回源收敛:比如华南的边缘节点需要华北源站的资源,让它直接跨区回源,延迟高、丢包多;走中间层的话,中间层和源站之间可以走专线或优化链路,稳定得多。
源站这一侧要做的事情其实不多,但每件都关键:正确返回缓存相关的响应头、支持条件请求(If-None-Match / If-Modified-Since)、支持 Range 请求(大文件分片)、保证回源 HOST 和协议配置正确。源站侧最常见的坑是响应头带上了不该带的字段,比如 Set-Cookie 或者 Cache-Control: private,这会让 CDN 直接放弃缓存,前面所有的优化全部白做。
2.3 调度系统:一次 DNS 查询背后的大脑
调度是整个 CDN 里最“玄学”的部分,因为它的决策质量直接决定了用户体验,但用户完全感知不到。
主流做法是DNS 调度 + 全局负载均衡(GSLB)。流程大概是:用户的请求域名 CNAME 到 CDN 的调度域名,本地 DNS 递归解析这个调度域名时,请求会打到 CDN 的智能 DNS 上。智能 DNS 拿到本地 DNS 的出口 IP,据此判断用户的大致地域和运营商,再结合各节点的实时负载、健康状态、成本权重,返回一个最优的边缘节点 IP。
这套机制的精度取决于一个东西:本地 DNS 的出口 IP 是否等于用户的真实位置。绝大多数情况下是接近的,但有些公共 DNS 的出口 IP 和用户实际位置差得很远,会导致调度到错误的节点。业界为此引入了 EDNS Client Subnet(ECS)扩展,让递归服务器把用户的网段信息带上来,精度能显著提升。如果你的业务对调度精度敏感(比如直播、大文件下载),ECS 基本是必开的。
另一种调度方式是Anycast:同一个 IP 在全球多个节点同时宣告,靠 BGP 路由把用户带到“路由上最近”的节点。它的优点是故障切换极快(路由收敛就切换了),缺点是大规模部署时路由会抖动,且不好做精细的负载均衡。实际生产里常见的是两者结合:Anycast 做第一层粗调度,DNS 做第二层精调度。
还有一个经常被忽略的点:DNS 缓存。调度决策下发之后,会被各级 DNS 缓存住,TTL 通常设得很短(几十秒到几分钟),但即使这样,节点故障后仍然会有一段时间的“脏流量”打到故障节点上。这就是为什么边缘节点自身必须有兜底能力——比如回源到备用节点,而不是直接报错。
2.4 管控平面:配置下发与日志回流
用户看不到的那一半,是管控平面。它负责配置的存储、校验、下发,以及日志的采集、清洗、聚合。
配置下发这件事听起来简单,实际上很容易出事。CDN 的配置项非常多:缓存规则、回源规则、鉴权规则、Header 改写、限频策略、证书绑定……这些配置要在几百上千个节点上保持一致。如果下发是“全量推”,一次误操作可能瞬间打挂全国;所以成熟的做法是分批灰度:先推一个测试节点,观察几分钟,再推一个小区域,再全量。而且必须有快速回滚能力,配置版本要能一键切回上一版。
日志回流同样重要。边缘节点每秒产生的访问日志量是海量的,全量回传成本极高,通常采用采样 + 聚合的方式:明细日志按比例采样落盘,聚合指标(QPS、命中率、状态码分布、回源带宽)全量上报。这些指标回过头来又喂给调度系统做决策,形成一个闭环。
我在实际排查中养成的一个习惯是:先看聚合指标定位时间段和区域,再按需拉取那个时间段的采样明细。直接一上来就捞全量日志,等半天不说,还容易被海量数据淹没。
3. 一次请求的完整旅程
3.1 解析阶段:CNAME 接力与节点选定
用户在浏览器里输入一个资源地址,第一件事是解析域名。浏览器缓存、系统缓存、hosts 文件依次查过,没有命中就交给本地 DNS 递归查询。
这里的关键是CNAME 接力。业务域名(比如静态资源域名)在权威 DNS 里配置的不是 A 记录,而是一条 CNAME,指向 CDN 服务商分配的调度域名。本地 DNS 拿到这条 CNAME 后,会继续解析调度域名,这次请求最终打到 CDN 的智能 DNS 集群上。
智能 DNS 在这一刻要做完所有决策:判断来源 IP 的归属地、判断运营商、查询各节点的实时负载和健康状态、按权重选出一个节点,然后返回它的 A 记录。整个过程必须在毫秒级完成,因为 DNS 查询本身也是有超时重试的,慢了用户就感知到了。
选完之后,本地 DNS 把这个 IP 返回给用户端,同时按 TTL 缓存住。这里有个实操细节:调度域名的 TTL 不要设太长。设成 300 秒意味着节点故障后,最长要 5 分钟才能把流量切走;设成 30 秒则切换快,但 DNS 查询量会上去。我一般的经验是 60 到 120 秒之间,兼顾切换速度和查询压力。
还有个常见的配置错误:业务域名同时配了 CDN 的 CNAME 和一条老的 A 记录。这种情况下解析结果是不确定的,可能一部分用户根本没走 CDN。上线前一定要把原来的 A 记录清干净,用 dig 命令多查几次确认。
3.2 建连阶段:TLS 握手能省则省
拿到边缘节点 IP 之后,客户端开始建连。这一段是延迟优化的主战场,因为它涉及多个 RTT 的串行等待。
最坏的情况是:TCP 三次握手(1 个 RTT)+ TLS 完整握手(TLS 1.2 需要 2 个 RTT,TLS 1.3 需要 1 个 RTT)+ HTTP 请求响应(1 个 RTT),总共 4 个 RTT。如果 RTT 是 40ms,光建立连接就要 160ms,内容还没开始传。
所以能省的地方必须省:
TLS 1.3是第一个要开的。相比 1.2,它把密钥交换和握手合并,直接省掉一个 RTT。而且它支持 0-RTT 会话恢复,复访用户可以在第一个包里就带上加密的应用数据,理论上零额外延迟。
会话复用(Session Ticket / Session ID)是第二个。开启之后,复访用户握手只需 1 个 RTT,配合 TLS 1.3 的 0-RTT 效果更好。需要注意的是 0-RTT 有重放攻击风险,只应该用在幂等的 GET 请求上,POST 请求不要用。
HTTP/2 或 HTTP/3是第三个。HTTP/1.1 下,同一个域名并发连接数受限(浏览器通常 6 个),大量小资源会排队。HTTP/2 的多路复用让一个连接并发跑几十个流,配合头部压缩,整体提升很明显。HTTP/3 基于 QUIC,把 TCP 和 TLS 合并,还解决了 TCP 层的队头阻塞,在弱网和高丢包环境下优势巨大。
OCSP Stapling也值得开。它把证书吊销状态检查的结果由服务器预先获取并随握手一起下发,省掉了客户端单独去查 OCSP 服务器的那一次请求。这个请求在跨国场景下经常要几百毫秒,不开的话首屏时间会被拖得很惨。
还有一个容易被忽视的优化:证书链要完整且精简。只带必要的中间证书,不要带根证书,链越长握手包越大,在弱网下越容易出问题。
3.3 命中与回源:边缘节点内部发生了什么
连接建好,HTTP 请求到达边缘节点的接入层。接入层解析出请求的 Host、Path、Query,然后拼成缓存键去缓存层查询。
缓存键的默认构成是协议 + Host + Path + Query,但这是可以配置的。查缓存这个动作本身很快(内存查一次,没有就查 SSD),真正决定命运的是“查得到还是查不到”。
命中的话,边缘直接把内容返回,响应头里通常会带上X-Cache: HIT和一个Age字段(表示这份内容在缓存里待了多少秒)。整个过程就是读盘加写 socket,延迟在毫秒级。
未命中的话,请求进入回源模块。回源模块会先检查有没有“正在回源的相同资源”——如果有,它不会重复回源,而是挂在这个进行中的请求上等着,这就是回源收敛(也叫请求合并)。这个机制在高并发下极其重要:一个热点资源刚过期,瞬间来了 1000 个请求,如果没有合并,源站就要挨 1000 次;有了合并,只回源 1 次,其余 999 个等着拿结果。
回源请求会带上条件请求头。如果边缘节点有旧版本的副本,会带If-None-Match(对应 ETag)或If-Modified-Since(对应 Last-Modified)。源站判断内容没变,返回 304 而不是 200,虽然省不了多少带宽,但省了源站的渲染成本,这个在大促场景下很有价值。
源站返回 200 之后,边缘节点做两件事:一边完整接收响应体写入缓存,一边边收边回传给客户端。这个“流式回源”很关键,如果等整个文件收完再回传,大文件的 TTFB 会非常难看。当然,边回边传意味着如果中途回源失败,客户端已经收到了一部分内容,只能靠断开连接让客户端重试,这点在排障时会表现为“下载到一半卡住”。
3.4 回源收敛与分片:高并发下的保护机制
回源收敛前面提了,这里重点说分片(Range)。
大文件下载场景下,如果只支持整文件回源,会带来两个问题:一是回源时延高(要等整个文件传完才能开始回传,虽然流式回源缓解了一部分,但缓存写入还是整块的);二是断点续传没法做。开启Range 回源后,边缘节点可以按分片向源站请求,比如每 4MB 一片,并发拉取,同时支持客户端用Range头做断点续传。
分片大小是有讲究的。太小,请求数量多,源站压力大;太大,首片等待时间长,且缓存管理粒度粗。业界常见的默认值在 1MB 到 4MB 之间,视频和大安装包可以用大一点,小文件用不上分片反而增加开销。
另一个保护机制是回源超时和重试策略。回源超时设太短,源站稍微慢一点就报 504,用户看到错误页;设太长,边缘的连接被长时间占住,并发一上来就撑爆。我的经验值是:连接超时 3 到 5 秒,读取超时 30 到 60 秒(取决于资源大小),重试次数 1 到 2 次且只对幂等的 GET 请求重试。重试还要配合退避,不然源站抖动时会迎来一波重试风暴。
还有主备源站的配置。主源站返回 5xx 或连接失败时,自动切到备用源站。这个在源站做发布、迁移、容灾的时候特别有用。但要注意:主备切换的判定条件要小心,如果备用源站本身数据是旧的,切过去可能会返回错误内容,反而更糟。所以主备源站一般要求数据同源,或者只用于纯静态、可重建的场景。
4. 缓存策略怎么定:TTL、缓存键与刷新
4.1 TTL 分层的经验做法
TTL 这个参数,设长了更新不及时,设短了命中率上不去,没有标准答案,只能按资源类型分层。
我一般按下面这个思路切:
| 资源类型 | 典型 TTL | 缓存位置 | 说明 |
|---|---|---|---|
| 带内容哈希的 JS/CSS/图片 | 30 天以上,配合 immutable | 浏览器 + CDN | 文件名变了 URL 就变,不存在更新问题 |
| 不带哈希的静态资源(如 logo) | 1 到 7 天 | 浏览器 + CDN | 需要更新时走刷新 |
| HTML 入口页 | 0 到 60 秒,或 no-cache | 仅 CDN,浏览器不缓存 | 入口页一变,引用的资源版本就全变了 |
| 接口类 JSON(可缓存) | 30 秒到 5 分钟 | 仅 CDN | 必须确认业务能接受这个延迟 |
| 用户相关的私有数据 | 不缓存 | 无 | 带 Set-Cookie 或 private 的响应 |
核心原则是:入口页短、资源长,带哈希的无限长。现代前端构建工具会给每个产物文件名注入哈希(比如app.a1b2c3d4.js),版本更新时文件名变了,浏览器和 CDN 自然就会去取新文件,根本不需要刷新。这种情况下 TTL 设一年都没问题,而且应该设一年——这才是命中率的极致。
反过来,如果构建产物不带哈希,那所有资源都得靠刷新来更新,命中率和更新及时性就成了跷跷板。这也是为什么我一直建议项目先把构建产物的哈希命名做掉,这是 CDN 优化的地基。
HTTP 头这块还有个细节:Cache-Control和Expires同时存在时,Cache-Control优先。s-maxage又是专门给共享缓存(也就是 CDN)用的,优先级高于max-age。所以你可以让浏览器缓存 10 分钟、CDN 缓存 1 小时,写成Cache-Control: max-age=600, s-maxage=3600。这个在“想让 CDN 多扛一会,但又怕用户看到太旧内容”的场景下很好用。
4.2 缓存键设计:参数、Vary 与碎片化
缓存键设计不好,命中率会莫名其妙地低,而且很难查。
最常见的坑是URL 参数污染。前端或者第三方脚本喜欢往请求后面加随机参数,比如?t=1699999999、?_=abc123,用来破除缓存。这在开发阶段很常见,但如果不小心带到了生产,每个请求的缓存键都不一样,命中率直接归零。解法是在 CDN 配置里忽略指定参数(配置参数白名单或忽略列表),只保留业务真正需要的参数(比如v=、id=)。
第二个坑是Vary 头。如果源站返回了Vary: User-Agent,CDN 会按不同的 UA 分别缓存同一份内容。UA 有多少种组合?成千上万。结果就是每份内容被切成无数碎片,每片都只被访问一两次,缓存等于没做。除非业务真的需要按 UA 返回不同内容(比如移动端和 PC 端返回不同的 HTML),否则不要随便加 Vary。
第三个坑是Host 头不一致。同一个资源通过www.xxx.com和xxx.com两个域名访问,缓存键不同,会各存一份。如果两个域名都接了 CDN,建议在业务层做 301 统一到主域名,或者在 CDN 侧配置“忽略 Host 差异”。当然,忽略 Host 有风险,如果不同域名下路径相同但内容不同,就会串内容,这个要评估清楚。
第四个坑是大小写和编码。/Image/Logo.PNG和/image/logo.png是两个不同的缓存键。如果源站对大小写不敏感,但 CDN 敏感,就会出现同一张图被缓存两份甚至多份的情况。可以在 CDN 配置里开启 URL 归一化(统一转小写、解码百分号编码),但前提是确认源站行为一致。
4.3 刷新、预热与版本化发布
刷新(Purge)和预热(Prefetch)是 CDN 的两个运维动作,用得好能救命,用不好能出事。
刷新是让缓存立即失效。分 URL 刷新、目录刷新、正则刷新几种。URL 刷新最精确,但量大时很慢;目录刷新一次清一批,适合整站更新,但可能误伤;正则刷新最灵活,也最危险,一条写错的正则可能把整个站的缓存清空,瞬间回源风暴。
刷新还有几个必须知道的事实:第一,刷新不是即时的,配置下发到全国节点需要时间,通常几十秒到几分钟,着急的话要提前做。第二,刷新有配额,商业 CDN 一般限制每天多少条,超了要申请。第三,刷新只影响 CDN,不影响浏览器缓存,如果资源在浏览器里还有很长的 max-age,用户那边依然是旧版本,这就是为什么带哈希的资源更省心。
预热是把内容主动推到边缘,避免第一批用户回源。适合大促前、新版本发布前、直播开始前。预热的关键是提前量:太早了内容可能又过期了,太晚了没推完。我一般的做法是提前 30 分钟开始推,推完检查预热成功率和各区域的覆盖情况。
但真正优雅的做法是版本化发布,压根不用刷新。做法是:资源文件带哈希,HTML 里引用新哈希,发布时只更新 HTML。用户拿到新 HTML,自然去请求新资源,旧资源还在缓存里但已经没人引用了,等 TTL 到期自然淘汰。整个过程零刷新、零预热、零抖动,回源量也只是新资源的量。这是我最推荐的方式,前期多花点时间改造构建流程,后面几年都省心。
4.4 动态内容:边缘算力能帮上什么忙
CDN 早就不只是缓存静态文件了。动态请求(API、个性化内容)虽然不能直接缓存,但依然有优化空间。
最基础的是链路优化。动态请求走 CDN 的时候,边缘节点和源站之间可以走专线或优化链路,避开公网拥塞。用户到边缘这一段是短链路,边缘到源站这一段虽然有距离,但在私有链路上丢包率低、RTT 稳定,整体效果比用户直连源站好不少。这个技术在弱网、跨国场景下提升尤其明显。
其次是连接复用。边缘节点维护到源站的连接池,用 HTTP/2 或长连接,避免每个用户请求都重新握手。这个对源站的连接数压力是小一个量级的。
再往上是边缘计算。把一部分逻辑放到边缘执行:A/B 测试的分流、简单的鉴权、请求头改写、响应内容裁剪、图片实时缩放和格式转换(比如自动转 WebP/AVIF)。这些能力用起来之后,很多原本要走源站的请求在边缘就处理掉了。
不过边缘计算要克制。边缘的运行时环境、调试手段、日志能力都比源站弱,复杂的业务逻辑放上去,出问题很难查。我的原则是:只把无状态、纯计算、失败可降级的逻辑放边缘,涉及数据写入、事务、复杂依赖的一律回源。
5. 接入实操:从零配一个能上线的 CDN
5.1 接入前的准备清单
正式配置之前,有几件事必须先确认,不然配到一半会卡住。
域名规划。至少要准备一个专门给静态资源用的域名,跟主站域名分开。分开的好处是:Cookie 不会跟着静态请求发出去(减少请求头体积),缓存策略可以独立设置,出问题也容易隔离。如果主站域名下有 Cookie 且在根域(Domain=.xxx.com),静态资源走子域名依然会带上 Cookie,这时候要考虑用完全不同的域名。
源站信息。源站地址、端口、回源协议(HTTP/HTTPS)、回源 HOST(这个特别重要,源站可能靠 Host 来做虚拟主机路由,配错了会返回 404 或默认站点)。如果有多个源站,要准备好主备关系和权重。
证书。如果走 HTTPS(现在没有不走的理由),边缘节点需要有对应域名的证书。商业 CDN 一般支持上传证书或自动签发。证书链要完整,过期的证书要提前换,这个很容易忘。
备案与合规。这个不用展开,但确实是接入的前置条件,提前确认好归属和材料。
回源 IP 白名单。如果源站有防火墙或安全组,要把 CDN 的回源 IP 段加进去,否则回源全部被拒。反过来,源站也可能需要识别“这个请求是 CDN 回源的还是用户直连的”,通常靠回源时带的特定 Header 来区分。
5.2 关键配置项逐条说明
配置项很多,我只说那些配错了会直接出问题的。
CNAME 接入。把业务域名的 DNS 改成指向 CDN 分配的 CNAME。配完之后用dig多查几次,确认返回的是 CNAME 而不是老的 A 记录,同时确认全国各地的解析结果都正常(可以用多个不同地域的在线拨测工具验证)。
回源 HOST。默认通常是跟随用户请求的 Host,但很多时候需要单独指定,比如源站是对象存储,必须用存储的域名作为回源 HOST。这个配错了,表现是回源全部 404,但缓存里可能还是空的,很容易误判成“源站文件不存在”。
缓存规则。按路径后缀或目录配置。典型的规则集是:*.html不缓存或缓存 10 秒,*.js|*.css|*.png缓存 30 天,/api/*不缓存。规则有优先级,越具体的越优先,配置的时候要理清顺序。
协议跟随。回源用 HTTP 还是 HTTPS,通常建议跟随用户请求(用户走 HTTPS 就回源 HTTPS)。但如果源站只有 HTTP,要强制指定 HTTP,不然回源会连不上。
压缩。开启 Brotli 或 Gzip。注意:如果源站已经压缩过了,CDN 不要再压一次,会造成双重压缩导致内容损坏。判断依据是看响应头有没有Content-Encoding,有就跳过。
防盗链。Referer 白名单是最基础的,配置允许的域名列表,空 Referer 是否放行要按业务定(有些 App 内嵌浏览器不带 Referer)。更可靠的是 URL 鉴权:用密钥 + 过期时间 + 路径算一个签名放在 URL 参数里,CDN 侧校验签名和时间戳。这个能防住直接盗链,但防不住会算签名的爬虫。
限频。按 IP 或按 URL 维度限制 QPS,超过就返回 429。这个在防刷和防爬时很有用,但阈值要调好,设太低会误伤正常用户(尤其是 NAT 后面的一大片用户共享一个出口 IP)。
5.3 容量与带宽的参数估算
这部分是很多人拍脑袋的地方,我给一套能落地的算法。
假设某站点日均请求量 800 万次,平均每个对象 60KB,日峰值是均值的 3 倍。
先算日均流量:800 万 × 60KB = 480GB(按十进制算约 480GB,按二进制约 458GiB)。
再算平均带宽:480GB × 8 bit / 86400 秒 ≈ 44.4Mbps。
峰值带宽:44.4 × 3 ≈133Mbps。这是 CDN 侧需要承载的峰值带宽。
然后算回源带宽。假设字节命中率是 92%,回源带宽 ≈ 133 × 8% ≈10.6Mbps。听起来很小对不对?但要注意两个放大因素:一是回源是突发性的,热点资源过期时可能瞬间全量回源;二是刷新和大版本发布会导致短时间命中率骤降。所以源站出口带宽不能只按 10.6Mbps 准备,我一般按峰值回源的 3 到 5 倍留余量,也就是40 到 55Mbps,同时配置好回源限速和队列,防止把源站打穿。
缓存容量怎么估?这个没有精确公式,用“容量-命中率曲线”来定。做法是:先按估算量买,跑一周,看命中率曲线在哪个容量点之后趋于平缓(边际收益递减的拐点),把容量定在拐点附近。经验上,只缓存热点的话,容量不需要覆盖全站资源,20% 到 30% 的资源量往往能覆盖 90% 以上的请求。
还有一个容易忽略的成本项:HTTPS 请求产生的计算开销。TLS 握手是 CPU 密集的,QPS 高的时候边缘节点的 CPU 会成为瓶颈。如果 QPS 特别高(比如十万级),要确认 CDN 侧的 TLS 卸载能力和是否支持硬件加速。
5.4 上线后的监控指标
配置上线只是开始,真正的工作在监控。
必须盯的指标有这么几个:缓存命中率(分请求命中率和字节命中率,两个都要看,前者反映请求效率,后者反映带宽节省)、回源带宽和回源 QPS、5xx 比例(分 5xx 来源,是 CDN 侧产生的还是源站返回的)、首字节时间 P50/P95/P99(只看均值会被长尾骗)、各区域和运营商的可用性。
这里有个特别容易踩的坑:只看总命中率会被平均值骗。总命中率 95% 看着很漂亮,但可能某个区域是 60%,某个资源类型是 20%。一定要做维度下钻:按域名、按路径前缀、按地域、按状态码分别看。我一般会固定看一张“命中率热力图”,横轴是时间,纵轴是区域,颜色深浅表示命中率,异常一眼就能看出来。
告警阈值也别设得太粗暴。比如 5xx 比例超过 1% 告警,这个阈值在流量低谷时很容易误报(基数小,几个错误就超了)。更好的做法是设置绝对值 + 比例的双条件:5xx 数量超过 100 且比例超过 1% 才告警。
6. 排障实录:那些文档里不写的坑
6.1 缓存不生效的六种典型原因
这是被问得最多的一类问题。按排查顺序列一下:
第一,响应头里带了Cache-Control: no-cache、no-store或private。这是最常见的原因。很多后端框架默认就给动态响应加no-cache,静态资源如果也走了同一套中间件,就会被一起标记。排查方法很简单:用curl -I直接请求源站,看原始响应头。
第二,响应里带了Set-Cookie。CDN 默认不缓存带 Set-Cookie 的响应,因为怕把 A 用户的 Cookie 发给 B 用户。如果静态资源确实需要 Set-Cookie,可以在 CDN 侧配置“忽略 Set-Cookie 后缓存”,但要确认这个 Cookie 不是用户相关的。
第三,URL 参数随机化。前面说过,前端加了时间戳参数。排查方法:看访问日志里同一个资源的 URL 有几种形态,如果每种都不一样,就是这个问题。
第四,Vary 头导致碎片化。同样前面说过。排查方法:看响应头有没有 Vary,以及缓存的 key 数量和请求数量是否接近 1:1。
第五,缓存规则没匹配上。规则是按优先级匹配的,可能一条更宽泛的规则把具体的规则覆盖了。排查方法:在 CDN 控制台用“缓存规则诊断”工具(大部分商业 CDN 都有),输入一个 URL 看命中了哪条规则。
第六,TTL 设得太短。设了 1 秒的 TTL,等于没缓存。这个最隐蔽,因为配置看起来是对的。
6.2 回源异常与 5xx 暴增
5xx 暴增的时候,第一步是分清是谁产生的。CDN 侧产生的 5xx(比如回源超时返回 504、回源连接失败返回 502)和源站返回的 5xx,处理方向完全不同。
判断方法看响应头。大部分 CDN 会在自己产生的错误响应里加一个标识头,比如X-Cache: Error from server或者特定的 Server 标识。也可以用“源站访问日志里有没有这个请求”来反推:如果源站日志里完全没有,那就是 CDN 侧的问题。
回源 502/504 的常见原因:源站进程挂了或端口没监听、源站防火墙拦截了 CDN 回源 IP、回源 HOST 配错导致源站返回了错误站点、源站 TLS 证书过期、源站连接数打满。逐个排查的话,最快的办法是从一台机器上用 curl 模拟回源:带上正确的 Host 头和源站 IP,直接请求,看能不能通。
源站返回 5xx 的常见原因:发布过程中部分实例未就绪、数据库连接池耗尽、下游依赖超时。这种要回到源站侧排查,CDN 这边能做的是配置源站健康检查和自动切备,以及熔断降级——连续失败超过阈值就返回缓存中的旧副本(如果配置了 stale-while-revalidate 或 stale-if-error)。
stale-while-revalidate和stale-if-error这两个指令值得单独说。前者允许在缓存过期后先返回旧内容,同时后台异步回源更新;后者允许在回源失败时返回旧内容。这两个组合起来,能在源站抖动时保住用户体验,代价是可能返回稍旧的内容。对静态资源和可容忍短暂不一致的接口,强烈建议开启。
6.3 命中率曲线异常
命中率突然掉下来,通常有三个方向:
资源变了。比如一次发布把所有资源的 URL 都改了(去掉了哈希但改了路径、或者加了版本号参数),全部变成新 URL,缓存全部失效。这种是“预期内的掉”,看一下发布记录就能确认,通常几小时内会自然恢复。
响应头变了。源站某次上线,中间件版本升级,给所有响应加了Cache-Control: no-cache。这种最阴险,因为配置没动、代码逻辑没动,只是框架默认值变了。排查方法是对比发布前后的响应头,用 curl 抓几个典型资源比对。
流量结构变了。比如被爬虫扫了大量不存在的 URL,或者被攻击者用随机 URL 刷,这些请求全部未命中,会把整体命中率拉低。这种要看请求的 URL 分布,如果长尾 URL 的数量突然暴增,基本就能确认。处理方式是上 WAF 或限频,把异常流量挡在缓存层之前。
还有一种情况是命中率没掉,但回源带宽涨了。这通常是因为命中率算的是请求数,而回源带宽看的是字节数。如果突然有大量大文件请求未命中(比如新上了一个大安装包),请求数命中率可能只掉 1%,但回源带宽能翻好几倍。所以两个指标一定要一起看。
6.4 排障速查表
把上面这些整理成一张表,出问题的时候按图索骥会快很多。
| 现象 | 优先怀疑 | 快速验证方法 | 处理方向 |
|---|---|---|---|
| 资源更新后仍返回旧内容 | 浏览器缓存未过期 / CDN 未刷新 | 用无痕窗口 + 加随机参数访问,看是否还是旧的 | 刷新 CDN + 缩短 HTML 的 TTL + 推动构建产物哈希化 |
| 命中率长期低于 80% | URL 参数污染 / Vary 头 / 响应头禁止缓存 | 抽样看访问日志的 URL 形态与响应头 | 配置参数忽略、去掉多余 Vary、修正 Cache-Control |
| 回源带宽突然翻倍 | 大文件未命中 / 刷新操作 / 攻击流量 | 看回源 URL 的分布和大小分布 | 预热大文件、限制刷新频率、接入防护 |
| 部分地域访问超时 | 调度到故障节点 / 该区域链路问题 | 用多地拨测工具同时请求,对比响应 | 摘除故障节点、调整调度权重、开启 ECS |
| 下载到一半中断 | 回源超时 / 分片未开 / 源站限速 | 复现时看回源日志的耗时分布 | 调大读取超时、开启 Range 回源、源站放开限速 |
| TLS 握手慢 | 证书链过长 / 未开 OCSP Stapling / 未开 TLS 1.3 | 用在线工具测握手各阶段耗时 | 精简证书链、开 OCSP Stapling、升级 TLS 版本 |
| 5xx 集中在某个时间段 | 发布 / 源站抖动 / 依赖超时 | 对比源站监控和发布记录的时间线 | 灰度发布、配置 stale-if-error、加源站熔断 |
最后分享一个我自己用了很多年的排查习惯:先用curl -I打三次。第一次看响应头(缓存策略、Age、X-Cache),第二次看时间(-w打印各阶段耗时,DNS、连接、TLS、首字节分别是多少),第三次加个随机参数强制未命中,对比命中与未命中的差异。这三下打完,八成的问题定位方向就出来了。剩下的两成,才需要去翻日志、查配置、找服务商。
这套方法没什么技术含量,但它能让你在凌晨三点被告警叫醒的时候,用最短的路径确认“到底是 CDN 的问题还是我自己的问题”——这个判断本身,往往比修复更值钱。