做前端和运维这些年,被问得最多的问题之一就是"页面打开慢,加个页面CDN加速是不是就好了"。说实话,这句话对一半。CDN确实能把静态资源的物理距离压下去,但它解决不了所有慢,甚至配置不当的时候会让问题更隐蔽——页面看着是快了,可一到发布就出缓存不一致,一到秒杀就回源打爆源站。我见过好几个团队,域名解析切过去,控制台点了"开启",然后就再没管过,直到某天大促流量上来,源站带宽跑满,才发现缓存命中率只有六成。
所以这篇我想把页面CDN加速这件事从头到尾讲透:它到底加速的是链路上的哪一段,缓存规则该怎么设计,哪些参数是真影响命中率的,接入实操的完整步骤长什么样,以及出问题的时候按什么顺序排查。内容偏实战,写给已经准备接入或者已经接了一半的同行看,也写给需要判断"这活儿值不值得做"的技术负责人看。看完你至少能知道,你花在 CDN 上的钱,有多少是真正买到效果的。
1. 页面CDN加速到底在加速哪一段
1.1 一次页面打开经过的四个环节
先把链路摆出来,不然讨论"快不快"都是玄学。用户从点开链接到页面可交互,大致分四段:DNS 解析、建立连接(TCP 加 TLS 握手)、首字节等待(TTFB,也就是请求发出去到收到第一个字节)、内容传输与渲染。这四段里,CDN 主要能吃下的是第二段和第三段的一部分,外加第四段里的静态资源传输。
DNS 这一段,CDN 一般通过调度系统给用户返回就近节点的地址,解析本身耗时不多,但如果调度不准,用户被指到一个跨省甚至跨洲的节点,后面全白搭。连接建立这一段,边缘节点离用户近,RTT 小,握手自然快;再叠加会话复用和 HTTP/2 多路复用,同一页面几十个请求不用开几十条连接。TTFB 这一段最微妙:如果资源命中缓存,边缘直接返回,那基本就是几十毫秒的事;如果没命中,边缘要回源取,这时的 TTFB 等于"边缘到用户"加"边缘到源站"两段之和,反而可能比直连源站更慢。
这也就解释了一个常见困惑:为什么有些人上了 CDN 感觉没变化,甚至首屏更慢了。原因往往不是 CDN 不行,而是命中率太低,绝大部分请求都在走"边缘转发",只赚了个就近接入的便宜,却多了一跳回源。所以做页面CDN加速,第一优先级永远是让该缓存的东西真正缓存住,而不是纠结节点铺了多少。
1.2 "页面CDN"这个说法里的两种含义
行业里说"页面CDN加速",其实混着两件事。一种是静态资源加速:JS、CSS、图片、字体、视频切片这些内容,特点是内容稳定、可以长时间缓存,是 CDN 最经典的战场。另一种是整页 HTML 的加速,包括 HTML 文档本身、服务端渲染出来的首屏结构,这类内容带用户态、更新频繁,处理起来难度高一个量级。
两者混在一起谈,最容易出的问题就是规则一刀切。我见过把 HTML 也设成一小时缓存的配置,上线当天首页还是旧版,用户点进详情页发现价格是昨天的。也见过相反的,全站no-store,CDN 一点缓存都不做,等于花钱买了个转发器。
比较靠谱的做法是把资源按"是否带用户态"和"更新频率"两个维度分成三类,分别给策略:带指纹的静态资源(app.a1b2c3.js这种)、公共静态资源(logo、通用库)、HTML 与接口。第一类可以放心长缓存,第二类给中等 TTL 加协商缓存,第三类要么不缓存要么极短 TTL。分类这件事想清楚了,后面 80% 的坑都不会踩。
2. 整体方案设计:缓存分层比节点数量更关键
2.1 三层缓存与回源收敛
一个成熟的 CDN 网络不是"一堆平行节点",而是分层的。最贴近用户的是边缘层(L1),只放最热的内容,存储小、命中快;往上一层是中间层或者叫父层(L2),缓存容量大得多,承接边缘未命中的请求;再往上才是源站。这三层设计的目的只有一个词:回源收敛。
打个比方,边缘层像小区门口的便利店,中间层像城区的大仓,源站是工厂。如果每个便利店缺货都直接打电话给工厂,工厂的电话线必然被打爆;正常做法是便利店向大仓要货,大仓没有才找工厂。CDN 里的逻辑完全一样:一个热门文件在 200 个边缘节点都没命中,如果不分层,就是 200 次回源;有了父层,边缘向父层回源,父层只向源站回源一次,源站压力从 200 降到 1。
这个机制对页面CDN加速的意义在哪?在于突发流量的抗性。新版本发布、活动页上线、爆款内容扩散,这些场景的共同点是"同一批资源在极短时间内被大量用户请求"。分层结构能在几分钟内把回源量压到源站能承受的水平,而纯扁平结构在这个窗口期很可能直接把源站打挂。选服务商的时候,问一句"有没有分层回源、父层覆盖率多少",比问"你们有多少节点"有用得多。
2.2 缓存键的设计决定命中率上限
缓存键(Cache Key)是 CDN 判断"这个请求是不是我要的那份缓存"的依据,默认通常是"协议 + 域名 + 路径 + 全部查询参数"。这个默认值在真实业务里经常是灾难,因为页面上大量 URL 带着业务无关的参数。
举个特别典型的:分享链接带?from=timeline&isappinstalled=0,广告投放带?utm_source=xxx&utm_campaign=yyy,前端埋点带?t=1699999999。这些参数一变,缓存键就变,同一份资源在边缘被存了几十上百份副本,命中率自然崩。我接手过一个站点,图片命中率长期在 55% 左右晃,排查半天发现是图片 URL 后面挂了防缓存的随机数,一行代码改掉之后直接干到 94%。
所以缓存键必须主动设计。常见的处理是设"忽略参数",把utm_*、from、spm、t、timestamp一类加进忽略列表;更严格的做法是"参数白名单",只有明确参与内容生成的参数(比如id、page、lang)才进缓存键,其余全部忽略。白名单更安全,代价是每上一个新功能要评估参数,但换来的是可预期的命中率。
除了查询参数,缓存键还有几个可选维度,用不用要看业务:
| 维度 | 什么时候需要 | 代价 |
|---|---|---|
| 设备类型(PC/移动/平板) | 服务端按 UA 输出不同 HTML | 缓存副本翻倍,需边缘识别而非Vary: User-Agent |
| 语言/地区 | 多语言站点按 IP 或参数分流 | 副本数乘以语言数 |
| 编码格式(gzip/Brotli) | 基本都需要,通常由 CDN 自动处理 | 无额外配置成本 |
| Cookie 分组 | 登录态与游客内容差异大 | 分组过多会击穿命中率,慎用 |
注意:不要用
Vary: User-Agent来做设备适配。UA 字符串几乎是无限长的组合,缓存键会碎成渣,命中率接近零。正确做法是在边缘用规则判断出设备类型,再把这个类型作为缓存键的一个受控维度。
2.3 静态与动态资源要分开谈
静态资源加速的思路很直接:给文件加内容指纹,长缓存,发布即换 URL。真正需要花心思的是动态部分,尤其是 HTML。这里有个认知必须先立起来:HTML 本身要不要缓存,是个业务问题,不是技术问题。
如果 HTML 完全个性化(比如首页推荐位千人千面),那就不该在 CDN 缓存,或者只能对游客态做短缓存。如果 HTML 是通用的(内容页、活动页、文档页),那完全可以缓存几十秒到几分钟,用"短 TTL + 快速刷新"来平衡时效与性能。中间还有一类是"骨架通用、局部个性化",这种最考验设计,主流做法是 HTML 缓存,个性化区块用客户端异步请求填充,或者在边缘做片段组装。
把这三类分清楚,规则表才好写。我给一个我常用的分类模板,实际项目里按它填一遍,基本不会有遗漏:内容页 HTML(短缓存 + 发布刷新)、带指纹的 JS/CSS(一年 + immutable)、未带指纹的公共库(一天 + 协商)、图片(按是否可覆盖更新决定,可覆盖的一小时,带指纹的一年)、字体(一年)、接口 JSON(默认不缓存,少数只读列表接口给 5 到 30 秒)。
3. 缓存策略的核心参数与配置细节
3.1 强缓存与协商缓存的组合拳
浏览器和 CDN 判断缓存,靠的是响应头。真正起作用的就那么几个,但组合方式决定了行为差异。
强缓存由Cache-Control: max-age=N控制,在有效期内浏览器根本不会发请求,直接从本地磁盘或内存取。这是最省时间的方式,0 网络开销。协商缓存靠ETag或Last-Modified,浏览器会带着If-None-Match或If-Modified-Since去问,服务端返回 304 就复用本地副本。它省的是传输体积,不省往返时间。
两种方式的选择标准很简单:内容变了 URL 会不会变?会变,用强缓存加长 max-age;不会变,用协商缓存,或者短 max-age 加协商兜底。所谓"内容指纹",就是让 URL 跟着内容变——文件名里塞一段内容哈希,内容一改文件名就改,旧文件永远不被覆盖。这种情况下max-age=31536000, immutable是完全安全的,immutable还能告诉浏览器"别再发协商请求了,我保证不变"。
Cache-Control: public, max-age=31536000, immutable反过来,如果资源是logo.png这种固定文件名、会被覆盖更新的,就绝对不能给一年缓存,否则用户一年内都看不到新图。这种要给短 TTL 加协商:
Cache-Control: public, max-age=3600, must-revalidate ETag: "a1b2c3d4"还有一个特别实用的组合是stale-while-revalidate和stale-if-error。前者意思是"过期之后先拿旧的给用户,同时后台去回源更新",后者是"回源失败了也别报错,先给旧内容顶着"。这两个指令对可用性的提升非常明显——源站抖一下、回源超时几秒,用户完全无感,只是短暂看到稍旧的内容。对新闻、商品详情这类"旧一点没关系"的页面,我基本都会加上:
Cache-Control: public, max-age=60, stale-while-revalidate=30, stale-if-error=6003.2 TTL 到底设多久:按资源指纹倒推
TTL 拍脑袋定是常见毛病。有人一律 3600,有人一律 300,都没什么依据。合理的做法是倒推:这个资源更新后,业务能容忍多久的延迟?
假设一个内容页,运营改完标题希望"一分钟内全网生效"。那 TTL 就不能超过 60 秒,同时要配发布时的主动刷新。如果运营能接受十分钟,TTL 给 300 到 600 秒都行,配合刷新兜底。而带指纹的 JS,业务容忍度是"新版本发布后用户刷新一次就能拿到新的"——因为文件名变了,URL 是新的,根本不存在"旧的被缓存"的问题,所以可以给一年。
这里有个容易被忽略的点:CDN 的 TTL 和浏览器的 max-age 不必相同。实践中经常是边缘缓存时间长(比如 1 小时),浏览器缓存时间短(比如 5 分钟)。这样边缘命中率高、回源少,同时用户端又能较快感知更新。有些 CDN 支持通过单独的响应头指定边缘 TTL,比如CDN-Cache-Control一类,具体字段名各家不同,配置时看文档。
还有一个经验值:对于"内容会被覆盖更新"的资源,TTL 别设太长,因为一旦设长了,出问题时你只能靠刷新来救,而刷新是有配额、有生效延迟的。我一般给这类资源 1 到 6 小时,既保证命中率,又不至于在事故时束手无策。
3.3 容易被忽略的 Vary、Cookie 与查询参数
Vary这个头在 CDN 场景下杀伤力被严重低估。它的语义是"这个响应随哪个请求头变化"。Vary: Accept-Encoding是合理的,因为压缩变体确实不同;Vary: User-Agent是灾难;Vary: Cookie更是灾难中的灾难——几乎每个请求的 Cookie 都不一样,等于每份响应都是独一份,缓存彻底失效。
比Vary更隐蔽的是Set-Cookie。很多 CDN 的默认行为是"响应里带Set-Cookie就不缓存",这本来是防止把用户 A 的会话发给用户 B 的保护机制,但如果源站无脑给所有响应都种 Cookie(比如某些框架默认给每个请求下发 session id),就会导致静态资源也全都不缓存。这种情况的典型症状是:规则怎么调命中率都上不去,抓包一看每个响应都有Set-Cookie。
解决思路是分两类处理。对纯静态资源,直接在边缘把Set-Cookie剥掉再缓存;对 HTML 和接口,要么治理源站只在该种 Cookie 的时候种,要么把带用户态的请求分流到不缓存的规则上。我个人的偏好是后者,宁可少缓存一点,也不要冒串号的风险——把登录用户的页面缓存成游客版本发出去,或者更糟,把 A 用户的页面发给 B,这类事故的代价远大于那点带宽钱。
还有Range请求。视频、大文件用分片下载时,边缘如果对每个 Range 请求都单独回源整文件,回源量会爆炸。现在主流 CDN 都支持"分片回源",即边缘发现未命中时按 Range 分片拉取,多个节点还能并行拉同一文件的不同片段,拉完拼起来存。这块属于服务商能力,接入前确认一下有没有,对多媒体内容多的站点差别很大。
4. 实操:一次完整的页面CDN接入过程
4.1 域名、证书与回源配置
接入第一步是域名规划。加速域名不能和源站主域名相同(会循环),常用做法是static.example.com给静态资源、img.example.com给图片、www.example.com走全站加速。子域划分的意义不只是管理方便,更重要的是可以按域名配不同的缓存策略和证书,规则清晰,出问题影响面可控。
第二步是证书。整站 HTTPS 之后,证书管理成了运维的日常。要注意三点:一是证书要覆盖所有加速域名,包括可能被遗忘的二级子域;二是回源时如果走 HTTPS,边缘到源站也要校验证书,很多"回源 502"就是源站证书过期或者证书链不全导致的;三是证书到期要有监控,别指望服务商的提醒邮件,自己配个提前 30 天的告警。
第三步是回源配置,这块坑最多。核心是三个值:回源地址(源站 IP 或域名)、回源 Host(发什么 Host 头给源站)、回源协议(HTTP 还是 HTTPS)。最常见的错误是回源 Host 和源站期望的不一致。比如源站是虚拟主机,靠 Host 头区分站点,CDN 默认回源 Host 是加速域名,源站收到static.example.com这个 Host 直接返回 404。解决办法是显式配置回源 Host 为源站真实域名。
还有一个细节:回源端口、超时时间和重试次数。超时时间设太短,源站稍微慢一点就判失败,误判率高;设太长,边缘的并发连接被占满,反而拖累其他请求。我通常设连接超时 3 到 5 秒、读超时 15 到 30 秒,重试 1 到 2 次且只对 5xx 和超时重试,4xx 不重试。重试次数一定要控制,源站已经扛不住的时候,每次失败都重试两次等于把压力放大三倍,很容易形成重试风暴把源站彻底打死。
4.2 缓存规则表与配置片段
规则配置是接入的核心工作,我的习惯是先画一张表,把资源类型、匹配规则、边缘 TTL、浏览器 TTL、是否忽略参数全部列清楚,评审通过再往控制台里填。表大概长这样:
| 资源类型 | 匹配规则示例 | 边缘 TTL | 浏览器 max-age | 忽略参数 |
|---|---|---|---|---|
| 带指纹 JS/CSS | 路径含-.[0-9a-f]{8,}. | 30 天 | 1 年 immutable | 全部忽略 |
| 公共库 | /libs/前缀 | 7 天 | 1 天 | 全部忽略 |
| 图片(可覆盖) | img.域名下的 jpg/png/webp | 6 小时 | 1 小时 | 忽略 utm 类 |
| 字体 | .woff2后缀 | 30 天 | 1 年 | 全部忽略 |
| 内容页 HTML | www.域名的 text/html | 60 秒 | 0(协商) | 白名单保留id、lang |
| 只读列表接口 | /api/list前缀 | 10 秒 | 0 | 白名单保留page、type |
| 用户态接口 | /api/user/前缀 | 不缓存 | 不缓存 | 不适用 |
配置片段以常见的反向代理思路写,逻辑通用,字段名按各家控制台对应调整:
# 静态资源:带指纹,长缓存 location ~* \.[0-9a-f]{8,}\.(js|css|woff2)$ { add_header Cache-Control "public, max-age=31536000, immutable"; add_header CDN-Cache-Control "public, max-age=2592000"; } # 内容页 HTML:短缓存 + 后台更新 + 回源容错 location ~* ^/article/ { add_header Cache-Control "public, max-age=0, must-revalidate"; add_header CDN-Cache-Control "public, max-age=60, stale-while-revalidate=30, stale-if-error=600"; proxy_hide_header Set-Cookie; } # 用户态接口:禁止缓存 location ~* ^/api/(user|cart|order)/ { add_header Cache-Control "private, no-store"; proxy_pass http://origin_backend; }配置完之后一定要先验证再全量。验证方法是抓响应头看实际生效的字段,而不是看控制台里填的值——中间可能被源站的响应头覆盖,也可能被更靠前的规则命中。用命令行工具直接看:
curl -sI https://static.example.com/app.a1b2c3d4.js | grep -iE "cache-control|age|x-cache|via"重点看两个:Age表示这份响应在边缘已经存了多久,刷新几次如果Age在递增,说明命中了;X-Cache一类的头会直接告诉你 HIT 还是 MISS(字段名各家不同)。这套验证我每次都做,便宜且直观。
4.3 预热、刷新与发布流程改造
预热就是主动把还没人访问的资源推送到边缘,避免"第一个用户当小白鼠"。为什么需要?因为冷启动时,一个页面的几十个资源会同时回源,如果正好赶上发布后所有用户都在刷新,源头会瞬间被打出尖峰。预热把这次尖峰提前消化掉。
预热要有优先级,不是什么都推。我的顺序是:HTML(如果缓存)→ 首屏 CSS → 首屏 JS → 字体 → 首屏图片。一页推 10 到 20 个关键资源足够,推全站不仅配额扛不住,长尾资源的收益也很低。预热还有一个讲究是"推新 URL",因为带指纹的版本一上线 URL 就变了,边缘自然没有缓存,这时候预热才有意义。
刷新是反过来的操作,用于"内容被覆盖更新后强制失效"。三种方式各有用处:URL 刷新最精准最快,适合少量文件;目录刷新范围大但生效慢,适合整批更新;标签刷新最优雅,发布时给资源打上业务标签(比如product-12345),需要失效时按标签刷,一次能刷掉一个商品相关的所有资源,特别适合商品详情、文章这类"一条数据对应多个资源"的场景。
发布流程要跟着改。以前发布就是"传文件、重启",现在要加两步:先预热、再切流量。完整顺序我一般这么走:构建产物上传到源站(此时新 URL 已可访问)→ 预热关键资源 → 灰度切 1% 到 5% 流量观察错误率和命中率 → 全量。回滚的时候反过来:切回旧版本入口 → 按标签或目录刷新 → 预热旧版本关键资源。这套流程看着啰嗦,但真出过事故的人都知道,多花五分钟比线上被用户骂一晚上划算。
4.4 容量与带宽估算(含计算过程)
做方案要给数字,不然说服不了人。这里演算一遍最常见的估算,你可以直接套。
假设站点日均 PV 500 万,单页平均触发资源请求 60 次(JS、CSS、图片、字体、接口加起来),则日总请求数为 500 万 × 60 = 3 亿次。资源平均大小按压缩后 100KB 估(这个值随站点差异大,图文站高、工具站低),日总流量 3 亿 × 100KB = 30TB。平均带宽 30TB ÷ 86400 秒 ≈ 347MB/s ≈ 2.8Gbps。峰值按平均的 3 倍算,边缘侧峰值带宽约 8.3Gbps。
再看回源。命中率 90% 时,回源请求 3000 万次,回源流量 3TB/天,折算平均约 34.7MB/s,也就是 278Mbps 左右,峰值接近 850Mbps。如果把命中率提到 97%,回源请求降到 900 万次,回源流量 0.9TB/天,平均约 10.4MB/s,约 83Mbps,峰值 250Mbps 上下。
这组数字说明什么?命中率从 90% 提到 97%,源站带宽需求降到原来的三分之一。这就是为什么我一直强调命中率优先于节点数量——它是乘法关系,不是加法关系。
缓存容量也要估。边缘节点不是无限盘,它靠淘汰策略维持热点。粗算:热门资源集合假设 20 万个文件,平均 50KB,总大小 10GB;给每个节点配 200 到 500GB 缓存盘,足以覆盖热点集合并且留足冗余。如果你的站点有大量大文件(视频、安装包),那要单独评估,别和图片混在一个容量模型里算。
5. 动态内容与接口的加速思路
5.1 链路优化的实质
动态内容没法缓存,那 CDN 还能做什么?答案是优化"最后一公里"之外的传输路径。用户到源站之间可能跨越多个网络、多次互联互通,中间任何一段拥塞或丢包,都会体现在 TCP 重传和握手等待上,用户感受到的就是"转圈"。
CDN 在这个环节的价值是把"用户到源站的一条长链路"拆成"用户到边缘的短链路"加"边缘到源站的长链路",后一段由服务商自己维护,通常走更优的路径、更稳的互联、更积极的拥塞控制策略。效果在弱网和跨地域场景下最明显——同样是 200 毫秒 RTT 的内陆城市用户,走优化链路的首字节时间可能只有直连的一半。
不过要提醒一句:动态加速不是万能的。如果源站本身处理一个请求要 800 毫秒(慢查询、串行调用),链路再怎么优化也只能省掉几十毫秒。所以上线动态加速前,先看源站的 TTFB 构成,把自身耗时降下来,加速才有意义。顺序反了,钱花了效果不明显。
5.2 个性化页面能不能缓存
这是个高频问题。我的回答是:能,但要拆。做法叫"页面分片"或者"边缘组装"——把页面拆成"公共骨架"和"个性化区块",骨架部分正常缓存,个性化区块用三种方式之一处理:客户端异步拉取(最简单,代价是多一次请求)、边缘侧发起子请求组装(性能好,配置复杂)、源站用 ESI 一类的标签声明片段(依赖源站改造)。
还有一种轻量做法是"游客态缓存 + 登录态回源"。边缘检查请求有没有登录 Cookie,没有就返回缓存的游客版本,有就放过到源站。这对内容型站点效果极好,因为大部分流量是未登录用户,缓存能覆盖七八成。风险点在于"游客版本"里绝对不能混进任何用户数据,配置时要确保Set-Cookie被剥离、个性化位是占位符。
我见过一个反例:某站点为了提命中率,把带用户昵称的页面也缓存了,结果 A 用户看到了 B 用户的名字。这类问题排查起来极其痛苦,因为复现依赖缓存状态。所以涉及用户态的缓存,宁可保守。
6. 效果验证与监控指标
6.1 该盯哪几个数
CDN 接完不是结束,是开始。要盯的指标不多,但每个都得看对口径。
缓存命中率是第一指标,但注意它有请求数口径和流量数口径两种。请求数口径反映"有多少次请求被边缘接住",流量口径反映"有多少字节没回源"。两者可能差很远——命中率 95%(请求数)但流量口径只有 70%,说明未命中的都是大文件。两个都要看,别只看一个。
回源率和回源 QPS直接关联源站压力,是容量规划的输入。边缘首字节时间和整体下载耗时反映用户体验,要分地区、分运营商、分协议版本看。5xx 率和4xx 率反映健康度,5xx 上涨通常意味着回源出问题,4xx 上涨可能是规则误配或者资源路径变了。
还有一个我必看的:各协议的请求占比。HTTP/2 和 HTTP/3 的占比如果很低,说明要么证书配置有问题,要么部分中间设备在降级,这时优化连接复用的收益还没吃到,值得查。
6.2 命中率突然下跌的排查顺序
命中率掉了是最常见的告警,按这个顺序查,能覆盖九成情况:
- 看是不是发布引起的。新版本是不是给资源加了时间戳参数、是不是改了文件名规则(从带指纹改成不带)、是不是把原本分域的静态资源合到了主域。
- 看源站响应头变了没。是不是新增了
Set-Cookie、Cache-Control从 public 变成了 private、Vary多了维度。这类变化经常是框架升级或者中间件配置改动带来的,改动的人根本不知道会影响缓存。 - 看 URL 结构变了没。新增了筛选参数、分页参数、排序参数,长尾 URL 暴增,每个都是一份新缓存,命中率必掉。这种情况要么做参数白名单,要么对这些页面不缓存。
- 看是不是刷新太频繁。有人为了"保证实时"配了定时批量刷新,等于每隔几分钟就把缓存清一遍,命中率永远上不去。
- 看流量结构变了没。突然来了大量爬虫或者新渠道流量,访问的都是冷门 URL,命中率自然低。这种情况要看绝对值,别被比例骗了。
7. 常见问题与避坑清单
7.1 高频故障速查表
下面这些是我这些年遇到频率最高的,按症状整理成表,方便对照:
| 症状 | 大概率原因 | 处理方式 |
|---|---|---|
| 回源 404,直连源站正常 | 回源 Host 与源站虚拟主机不匹配 | 显式指定回源 Host 为源站域名 |
| 回源 502/526 | 源站证书过期、证书链不全、回源端口不通 | 补全证书链,校验端口与协议 |
| 静态资源命中率长期低于 70% | URL 带随机参数、响应带Set-Cookie、Vary过宽 | 配参数忽略、边缘剥离 Cookie、收窄Vary |
| 发布后用户仍看到旧页面 | HTML TTL 过长且未刷新 | 缩短 TTL,发布流程加目录/标签刷新 |
| 页面串号,看到别人的信息 | 带用户态的页面被公共缓存 | 该类页面设private, no-store,或按 Cookie 分流 |
| 首次访问极慢,之后正常 | 冷启动回源,未预热 | 发布前预热首屏关键资源 |
| 大促期间源站带宽打满 | 命中率不足、重试放大、无分层回源 | 提前压测、限制重试次数、确认分层能力 |
| 301 跳转改不回来 | 永久重定向被长缓存 | 重定向响应给短 TTL 或不缓存 |
| 图片更新后部分用户看到旧图 | 固定文件名 + 长 TTL | 改为带指纹文件名,或缩短 TTL 加刷新 |
7.2 几条用事故换来的经验
第一条:Cache-Control一定要由源站控制,别在边缘硬写。边缘硬写看起来省事,但会让源站的意图失效,日后排查时两处配置打架,谁也不知道最终生效的是哪个。正确姿势是源站输出准确的头,边缘只在必要时做覆盖,并且覆盖规则要写注释、进版本库。
第二条:所有"永久"的东西都要留后路。一年缓存、301 重定向、immutable,这些用起来爽,但一旦配错,恢复周期以小时甚至天计。我的习惯是:给长缓存之前先确认 URL 一定带指纹;给 301 之前先想清楚这个地址未来三年会不会变;immutable只用在真正不会变的资源上。
第三条:压缩和缓存要一起考虑。文本资源开 gzip 或 Brotli,能省六七成体积,但要注意不同压缩变体的ETag必须不同,否则协商缓存会返回错误的副本。图片别再二次压缩,改成按Accept头转 WebP 或 AVIF 更划算,同时记得把格式维度纳入缓存键,不然会把 WebP 发给不支持的客户端。
第四条:给回源加保护。回源 QPS 上限、并发上限、熔断阈值,这三样配了之后,源站至少不会被 CDN 拖死。我见过最惨的一次是源站数据库慢查询,响应从 50 毫秒变成 5 秒,边缘大量超时重试,回源 QPS 翻了三倍,直接把数据库压垮,形成正反馈。有熔断的话,边缘在检测到回源错误率超阈值时直接返回旧内容或错误页,源站还有喘息机会。
第五条:灰度必须按地域或运营商做,不要按百分比随机。随机灰度会让同一个用户的请求时而命中新节点时而命中旧节点,缓存状态混乱,问题难复现。按地域切,一个区域内状态一致,出问题影响可控、回滚干净。
第六条:把 CDN 配置纳入版本管理。控制台点出来的配置也是代码,也应该有变更记录、有评审、有回滚方案。我知道这听起来很重,但比起"某天发现某条规则被人改过但没人记得",这点成本太值了。
我个人在实际操作中的体会是,页面CDN加速这件事,技术门槛不高,难点全在"想清楚"三个字:想清楚哪些内容能缓存、能缓存多久、更新后怎么让用户拿到新的。这三件事定了,配置就是照着填;这三件事没定,配置填得再花哨,也只是把问题延后到流量上涨的那天。所以每次有新同学问我怎么配 CDN,我都先让他把那张资源分类表填一遍——表填明白了,剩下的都是执行。