绕开CDN和边缘缓存这两个词,很多团队其实是在"裸奔"跑业务。不少人以为接了CDN就万事大吉,结果回源率居高不下、动态接口慢成狗、资源被盗刷,最后排查一圈发现全是缓存策略没做对。
我这些年经手过的项目里,真正把静态、动态、签名鉴权三套组合拳打完的团队少之又少。多数情况是:静态资源全量缓存但不设版本管理,动态请求一点不缓存全靠回源硬扛,鉴权要么没有要么全站强制导致CDN形同虚设。这篇就把我这几年调CDN和边缘缓存的经验全部摊开来讲,从设计思路到实操步骤,从参数计算到踩坑记录,一次说透。
1. 整体架构与缓存策略的设计思路
1.1 先想清楚三件事再动手
拿到一个项目要配CDN,不要急着去控制台点按钮。我先问三个问题:静态资源占多大比例?动态接口的实时性要求到底有多高?业务有没有被刷量和盗用的风险?
这三个问题的答案基本决定了整套策略的骨架。我之前碰到过一个电商项目,前端静态资源占了全部流量的60%以上,但动态接口的查询频次极高,而且商品价格接口对实时性要求非常苛刻。如果一刀切全站缓存,价格就乱了;如果全站不缓存,源站机器再多也扛不住。最终落地方案是:静态资源全量缓存加版本号管理,动态接口按业务类型分级处理——强实时的不缓存,弱实时的做几秒到几十秒的短缓存,被刷风险高的接口统一加签名鉴权。
这里面的核心逻辑是"分层"。你可以把CDN理解成一个多级的仓库体系:边缘节点是离用户最近的便利店,中间层是区域仓库,源站是总仓。静态资源是可以放心堆在便利店的标品,动态数据则要分清楚哪些是易腐品必须现做现卖,哪些是保质期长的可以预先封装。想清楚这个类比,再去做缓存策略就不会拍脑袋了。
1.2 缓存策略的决策模型
我习惯用一个简化模型来判断一个请求能不能缓存:先看URL是否带查询参数,再看响应头里有没有Cache-Control和Vary,最后判断请求是否携带了影响响应内容的Header或Cookie。
这个模型看起来简单,但实际落地时坑非常多。比如很多团队会忽略Vary头的存在,导致同一个URL缓存了多种不同版本的响应内容,CDN节点不知道该返回哪一份。又比如有的动态接口其实数据变化不频繁,但URL里带了一个时间戳参数,导致缓存永远命中不了。这些都是我在真实项目中踩过的坑,后面会展开讲。
2. 静态内容的缓存策略详解
2.1 缓存键与版本管理的黄金搭档
静态资源缓存最怕的一件事,就是文件名不变但内容变了,用户还在看旧版本。最常见的问题:部署新版本后,入口HTML引用的还是带缓存指纹的旧文件,但CDN节点上旧文件已经过期被清除,用户拿到的页面就出现了资源加载失败或新旧混杂。
解决这个问题的标准做法是"文件名指纹 + 长缓存"。也就是Webpack或Vite这类构建工具在打包时给每个静态文件生成基于内容哈希的文件名,CDN对这些文件名设置超长时间的缓存,同时入口HTML不缓存或只缓存极短时间。这样每次发布后入口文件都会拿到最新内容,引用的也是带新指纹的资源,CDN节点上不管有没有老文件缓存,都不会影响新版本的正确加载。
实操上我建议的配置是这样的:
| 资源类型 | Cache-Control | CDN缓存时间 |
|---|---|---|
| HTML入口文件 | no-cache 或 max-age=0 | 不缓存或120秒 |
| JS/CSS带哈希文件名 | public, max-age=31536000, immutable | 一年 |
| 图片/font/媒体文件 | public, max-age=31536000 | 一年 |
| SVG/图标小文件 | public, max-age=604800 | 七天 |
这里有个关键点:Cache-Control里如果带了immutable字段,CDN和浏览器都会认为这个资源永远不会变,连重新验证都不会发。这个字段只适合带内容哈希的文件名,绝对不能用在接口上。
2.2 缓存键设计的细节
很多CDN默认的缓存键就是完整URL,这会导致一个很典型的业务问题:同一个资源,PC端和移动端需要不同的尺寸或不同的裁剪参数,如果两个端共用同一个URL,CDN只会缓存第一份响应,第二个端拿到的就是错的内容。
解决方法是分场景定制缓存键。拿阿里云CDN和腾讯云EdgeOne来说,控制台里都有缓存键配置功能,可以指定忽略或保留某些参数。我的习惯做法是:把影响响应内容的关键参数(比如模板ID、客户端类型、分辨率标识)放进缓存键保留范围,把那些统计类、时间戳类、随机数类参数(比如utm_source、_t、callback)全部忽略掉。
还有个容易踩的坑是大小写问题。默认情况下大部分CDN对URL是大小写敏感的,但源站如果是大小写不敏感的应用,同样的资源可能被缓存成两份,白白浪费空间和回源带宽。建议在CDN配置里打开URL大小写归一化功能,或者确保应用层做好重定向统一。
3. 动态内容的缓存策略实操
3.1 动态内容真的能不回源吗
先说结论:动态内容不仅能做边缘缓存,而且是降本增效最明显的一块。关键是搞清楚哪些能缓存、缓存多久、怎么保证一致性。
我看过一个社区论坛的架构,帖子列表接口的QPS常年维持在几千,但数据其实每分钟才更新一次。这个接口回源率原来几乎100%,源站MySQL压力非常大。后来做边缘缓存优化,把TTL从0调到30秒,回源率直接降到不足5%,源站Load Average从5以上降到1以下,响应时间从平均200毫秒降到20毫秒以内。
动态接口的缓存分级策略,我建议按下面的框架执行:
- 完全不缓存:登录态相关、购物车、下单、支付回调、实时价格、库存查询
- 秒级缓存(5-30秒):首页推荐流、排行榜、公告类接口
- 分钟级缓存(60-300秒):文章详情、评论列表、热门话题榜
- 边缘计算参与:对动态内容做个性化拼接,比如用户昵称、头像等由边缘脚本注入
这个分级的前提是你要对业务容忍度有清晰的认知。之前有个项目做资讯类App,资讯详情页的阅读量要实时显示,做了5秒缓存后发现阅读量表现明显卡顿。后来改为:文章详情内容缓存5分钟,阅读量字段单独走后端接口不缓存,两边异步合并,问题就解决了。代价是架构复杂了一些,但用户体验完全不受影响。
3.2 Cookie、Header与Vary的复杂场景
动态内容缓存最头疼的不是TTL设置,而是Vary头和Cookie造成的缓存污染。
最常见的反面案例:某个页面针对不同用户显示不同的促销信息,开发者把用户身份放在Cookie里。如果CDN不知道这个逻辑,直接缓存了第一个用户的页面,后面所有用户拿到的都是同一个人的促销信息。解决办法有两个思路:一是在关键路径上的接口返回Vary: Cookie,当然这会导致缓存碎片化严重,缓存命中率直线下降;二是在边缘节点上用规则判断——如果请求带了某些关键Cookie,就直接回源不缓存,这样既避免了污染又保障了未登录用户的缓存命中。
另一个容易被忽视的Header是Accept-Encoding。如果你的源站开启了Gzip或Brotli压缩,同时CDN也做压缩,需要在缓存键里区分到压缩格式,否则同一个URL下有的用户拿到gzip版本、有的拿到br版本,网络传输效率会受到影响。大部分商业CDN现在都能自动处理这个字段,但自建边缘缓存时就要特别留意。
关于Vary头的处理,我一直强调一个原则:少用Vary兜底,多用缓存键显式区分。显式区分意味着你能预测到哪些维度会导致内容变化,提前在缓存键配置里列出来。Vary是响应侧的被动声明,用多了会直接打碎缓存。曾见过一个项目把Vary: Accept、Vary: Accept-Encoding、Vary: Cookie全都返回上,缓存命中率直接跌到个位数。
4. 签名鉴权方案的实现与关键要点
4.1 为什么边缘鉴权必须与缓存配合
很多团队的鉴权方案只在源站做,CDN边缘节点不校验,这就给了刷量盗用很大的可乘之机。攻击者可以直接绕过CDN回源,或者拿到带签名的URL后重复刷取。边缘鉴权的意义在于把校验从源站前置到离用户最近的地方,无效请求根本到不了源站。
但边缘鉴权有个天然的矛盾点:签名参数通常是加在URL query里的,如果缓存键把整个query都纳入,就完全无法命中缓存,每个请求都要回源,鉴权就形同虚设了。解决办法是"双轨制":鉴权相关参数不参与缓存键计算,业务参数单独提取。CDN厂商在处理上面的做法一般是把timestamp、sign、token这类参数标记为"鉴权参数",在缓存键里自动剔除。
4.2 时间戳与MD5签名的原理与计算流程
最常用的边缘鉴权方式是时间戳签名,也叫URL防盗链。核心流程是:请求方用约定好的Key对URL路径和过期时间计算签名,CDN节点校验签名合法性以及时间戳是否过期。
具体签名串的拼接规则,不同厂商略有区别,但思路是一致的。以常见的经验为例:将URI路径、客户端IP(可选)、过期时间戳拼成一个字符串,加上私钥做MD5或HMAC-SHA256计算,最终签名的URL形如 /video/10001.mp4?auth_key=1699999999-0-0-xxxxxmd5hashxxxxx。
我建议优先使用HMAC而非裸MD5拼接,前者能有效避免长度扩展攻击,虽然防盗链场景并发没那么高,但安全性的成本几乎为零,何乐而不为。实际计算时要注意:拼接顺序必须严格按照约定,前后顺序错了哪怕Key对也会校验失败。
4.3 多重组合:时间戳校验、路径限制与Referer白名单
成熟的边缘鉴权方案通常是组合拳。只用Referer黑名单很容易被伪造,只用时间戳密钥一旦泄露全部玩完。我常用的组合是"Referer限制做第一道拦截 + 时间戳签名做第二道门槛 + 单链接访问频次限制做兜底"。
针对下载类和视频类资源,我还特别留意了单链接限频和客户端IP维度限频。单链接限频的意义在于:即使签名被合法用户分享出去,攻击者拿到链接以后也只能有限使用,不会瞬间刷爆源站。实践中我遇到过签名URL被爬到公开平台,导致CDN流量费用暴涨十几倍的案例,最后就是靠限频规则把损失控制住了。
4.4 鉴权配置的回源透传与容错
最后一个内部要点:鉴权通过后请求回源时,签名参数要如何处理。有的CDN默认会把整个原始URL透传给源站,这有两个问题:一是源站如果也做鉴权,需要再做一次校验;二是签名参数会污染源站日志和缓存键。我一般建议CDN在鉴权通过后把签名参数剥离掉再回源,源站看到的是干净的路径。
容错设计也不能少。CDN鉴权失败时返回403还是302重定向,要按业务场景来定。资源类我建议直接返回403,页面类建议重定向到一个统一的提示页。千万不要在鉴权失败时返回200和一个错误JSON,这样容易被误判为成功请求。
5. 工具选型解析与配置实践
5.1 主流CDN方案的差异与适用场景
现在市面上CDN产品五花八门,我自己的判断依据就三条:对动态内容的支持程度、缓存键的灵活度、边缘脚本能力。
传统CDN(如阿里云CDN、网宿)在静态资源分发上非常成熟,覆盖节点多、稳定性好,但对动态内容的处理要靠大量规则堆叠,灵活性一般。云原生边缘平台(如Cloudflare Workers、腾讯云EdgeOne、又拍云边缘计算)则在动态内容、边缘逻辑、个性化处理上明显更强,适合需要精细控制缓存的业务。自建Nginx/Varnish做边缘缓存则适合预算敏感、技术掌控力强的团队,但运维成本和风险都更高。
| 方案 | 静态分发 | 动态加速 | 缓存键细粒度 | 边缘脚本 | 上手成本 |
|---|---|---|---|---|---|
| 传统CDN | 很强 | 一般 | 中等 | 弱 | 低 |
| 云原生边缘平台 | 强 | 强 | 高 | 强 | 中 |
| 自建Nginx | 中等 | 手动控制 | 极高 | 无 | 高 |
5.2 一套可以直接抄作业的Nginx边缘缓存配置
自建边缘缓存最考验功底,我直接给出一份经过验证的Nginx配置模板,覆盖了静态缓存、动态短缓存和基本鉴权校验。
# 静态资源,长缓存 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { expires 365d; add_header Cache-Control "public, immutable"; # 命中缓存直接返回,不进源站 try_files $uri =404; } # 动态API,短缓存 location /api/ { proxy_cache my_cache; proxy_cache_key "$uri$args"; proxy_cache_valid 200 30s; proxy_pass http://backend; } # 简单鉴权校验,校验失败直接返回403 location /protected/ { if ($arg_sign = "") { return 403; } # 实际项目中应该验证时戳 + 签名,写法略 proxy_pass http://backend; }这份配置能解决60%的场景问题,但真正生产环境还需要增加缓存锁、优雅降级、异常状态码处理等。下面用章节5.3单独讲。
5.3 生产级缓存回源与降级配置
生产环境里,Nginx缓存最怕的是缓存雪崩和惊群效应。缓存雪崩是指大批缓存同时过期,请求全部打到源站;惊群效应是同一个热点key同时过期后成千上万个请求同时触发回源。
对应的处理手段叫"缓存锁"或"回源锁"。原理是:当缓存未命中时,只有一个请求去源站拉取数据并回填缓存,其他请求排队等待或直接拿旧缓存兜底。OpenResty里可以用lua-resty-lock实现,Nginx商业版有专门的指令,社区版可以借助共享字典自己实现。
具体的措施,我通常会组合使用:
- 缓存过期抖动:在TTL基础上加一个随机偏移,避免集中过期
- stale-if-error:允许返回过期的缓存内容,即使源站挂了也能保证服务不停
- 请求合并:同一瞬间未命中的请求共享同一个回源任务
特别是"允许过期缓存兜底"这条,真的建议所有自建边缘缓存的团队都打开。我见过一个案例,源站数据库宕机4个小时,靠这一条规则,前端用户几乎完全无感,只是看到的数据旧了几个小时,对很多阅读类业务来说完全能接受。
6. 常见问题与排查技巧实录
6.1 热门问题的排查思路速查
长期和CDN排查打交道,我整理了一张问题速查表,不管是自建还是商用,排查思路都是通用的:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 更新内容用户看不到 | 缓存未刷新/忽略版本号 | 检查缓存键和刷新API |
| 回源率居高不下 | 缓存时间过短、URL参数过多 | 优化缓存键,加CDN层缓存 |
| 动态接口时好时坏 | 缓存污染或TTL不一致 | 检查Vary、Header、Cookie |
| 静态资源访问403 | 鉴权参数被缓存键吞掉 | 检查鉴权配置与缓存键匹配 |
| 带宽暴涨线路拥塞 | 被刷量导致 | 立即加限频和签名鉴权 |
| 部分用户拿到乱码 | 压缩格式未区分 | 检查Accept-Encoding处理逻辑 |
6.2 我在实际项目中踩过的坑与排查过程
讲一个真实的排障案例。有一次一个客户的动态接口在CDN切换后,部分用户频繁反馈数据错乱。第一反应是回源出了问题,但查了源站日志,发现回源请求都是正常的。后来抓包发现,问题出在CDN节点缓存了用户A的数据,然后用户B带着不同的Cookie访问时,CDN依旧返回了缓存的那份。
当时客户端请求的是同一个URL,但响应内容依赖Cookie里的城市ID。排查过程是:先看响应头,发现没有Vary: Cookie;再看CDN缓存键,没有把Cookie维度纳入;最终调整策略——CDN规则里判断请求带了city_id这个Cookie就回源,不带就缓存。这个问题前后花了快半天才定位,不是技术有多难,而是容易被"同一个URL"这个表象迷惑。
另一个印象深刻的坑是大小写敏感。某项目的静态资源在源站是大小写不敏感存储的,但浏览器请求里同一张图有时全小写、有时首字母大写。CDN把这两种URL当成了两份缓存,一个月的时间里莫名其妙多出30%的流量费用。后来在全站的一次CDN配置审计中才发现,这30%的流量全部来自重复缓存和重复回源。开启了路径大小写归一化后,流量立刻恢复正常。
6.3 边缘缓存健康度的主动观测技巧
除了被动排查,我更推荐日常就建立一套"缓存健康度"观测指标。核心指标就三个:回源率、缓存命中率、平均首字节时间。
回源率是最直观的指标。一个优化良好的静态资源站点,回源率应该在5%以下;动态内容经过分级处理后,整体回源率控制在20%以内是常见的健康状态。缓存命中率则是另一个角度看同一件事,它体现的是CDN内部存储的价值。
三者的关系我用一个简单的公式来理解:用户平均响应时间约等于"命中率乘以边缘响应时间加上回源率乘以回源响应时间"。在边缘响应时间基本恒定的前提下,回源率每降低10个百分点,平均响应时间的改善是非常可观的。日常观测建议小时粒度去盯这三个指标的曲线,一旦发现回源率在半小时内快速爬升,大概率是缓存时间配置被改了,或者是某个热点资源大量过期。
热点的处理办法,我特别推荐"主动预热":在流量高峰来临前,把即将上线的爆款资源通过CDN厂商的预热接口提前推送到主要边缘节点。电商大促前、游戏开服前,这招屡试不爽,能极大降低回源压力。
7. 组合拳的落地建议
从我自己的经验看,CDN和边缘缓存的优化从来不是一个配置就能搞定的,而是一套方法论和工具链的组合。静态资源靠指纹版本和长缓存来降本,动态内容靠分级短缓存和边缘能力来提效,鉴权则靠时间戳、路径限制和频次限流来构筑边界,三者环环相扣。
真正做得好的团队都有一个特点:把缓存当成一个日常运维和持续迭代的子系统来对待,而不是上线时配置一次就再也不管。缓存策略是需要跟着业务形态演进的,营销活动来了要开短时强缓存,新功能上线了要临时绕过缓存验证,恶意刷量来了要立刻收紧鉴权和限频。这些动作如果全靠手动去控制台上敲,一定跟不上节奏。
我个人的习惯是:所有CDN配置都会用基础设施即代码的方式管理起来,比如Terraform配合各云厂商的Provider,或者至少保存一套完整的命令行调用脚本。这样任何线上变更都有迹可循、可回滚,也方便在不同环境间复制同样的策略组合。
核心的一点心得,也是一个很多人不太适应的心态转变:做缓存优化,追求的不是"所有请求都命中缓存",而是"源站的每个请求都是必要的"。这句话我琢磨了很久,包含的意义远超字面:必要回源要尽力保障,不必要的回源要尽力消灭,该牺牲的缓存一致性就去牺牲,该保留的实时性就坚决保留。理解了这一点,前面讲的所有策略、参数、配置,就都变成了一盘能真正下好的棋。