FieryWing 的技术博客

CDN 是什么:让内容离用户更近

2025-11-22

CDN(内容分发网络)在各地部署缓存节点,用户访问时从最近的节点取内容,从而降低延迟、减轻源站压力。这篇文章我想从原理讲起,再聊缓存头怎么写、更新后怎么刷新、什么时候真的需要它,以及排查问题时看哪里。

为什么需要 CDN

网络延迟有一部分是物理距离决定的:信号在光纤里再快也有上限,距离越远往返时间越长。如果你的服务器在一个城市,远方的用户每个请求都要跨越长长的链路,还要经过许多个路由器。可以拿连锁超市来类比:与其让所有人都去总仓库取货,不如在各个城市开分店,常用商品提前备在分店里,顾客就近购买;分店没有的货,再从总仓库调。CDN 的节点就是“分店”,你的源站就是“总仓库”。

它带来的好处主要有几点:降低访问延迟,提升加载速度;分担源站的带宽和请求压力,源站只需要应对“缓存未命中”的那部分流量;流量高峰时更稳;并且可以在边缘做一些 HTTPS 终结、压缩、防护之类的工作。

工作方式

  1. 域名解析到 CDN 的节点(通常用 CNAME)。
  2. 节点收到请求,命中缓存就直接返回。
  3. 没有命中就回源到你的服务器取内容,并按规则缓存。

展开说一下第一步。假设你的站点是 www.example.com,在 CDN 服务商那里添加加速域名后,服务商会给你一个专属的 CNAME 目标,你需要在自己的 DNS 里把 www.example.com 指向它。之后用户查询这个域名时,CDN 的调度系统(通常基于 DNS 或任播等机制)会根据用户所在位置、节点负载等因素,返回一个合适的边缘节点的 IP。DNS 的基础可以参考我的 DNS 那篇。可以用下面的命令观察:

dig www.example.com
dig +short CNAME www.example.com

如果返回了一个指向服务商域名的 CNAME,再往下解析到若干 IP,就说明接入已经生效。要注意:根域名(例如 example.com 本身)按 DNS 规范不能直接设置 CNAME,因此常见做法是让 www 子域名走 CDN,或者使用服务商支持的 ALIAS/ANAME 类似功能,具体以服务商文档为准。

缓存命中与回源

“命中”(HIT)是指节点本地已经有一份未过期的副本,直接返回;“未命中”(MISS)则要回源。回源时节点相当于一个客户端,向你的源站发出普通的 HTTP 请求,拿到响应后一边返回给用户,一边按规则存下来,下一个请求就能命中了。衡量 CDN 效果的一个关键指标是缓存命中率:命中率越高,源站越轻松,用户也越快。

还有一种“分层缓存”的做法,边缘节点没命中时先问上一层的中间缓存节点,而不是都直接打到源站,进一步减少回源。具体是否提供、怎么配置,各家产品不同,需要看对应文档,这里不做展开。

适合缓存什么

文件名带哈希,指的是像 app.3f9c1b2.js 这样,内容变了文件名就跟着变。既然每个版本都是不同的 URL,旧版本永远不会被误用,因此可以放心地设置一年的缓存。HTML 页面本身则不适合缓存太久,因为它引用着那些带哈希的资源,页面一旦缓存了旧版,就会一直指向旧文件。常见的搭配是:HTML 短缓存或需要验证,静态资源长缓存。

用 Cache-Control 控制缓存

缓存行为主要由源站响应头里的 Cache-Control 决定。几个常见写法:

Cache-Control: public, max-age=31536000, immutable
Cache-Control: max-age=0, must-revalidate
Cache-Control: no-store
Cache-Control: private, max-age=600
Cache-Control: public, max-age=60, s-maxage=3600

验证用到的是 ETag 与 Last-Modified,配合条件请求,资源没变化时源站回 304,不用重传内容,细节在 HTTP 那篇里有讲。另外要留意,如果响应里带了 Set-Cookie,或者请求带有认证信息,很多 CDN 默认不会缓存,或者需要你显式配置,所以别指望随便什么响应都能被缓存。而且各家 CDN 往往允许在控制台里覆盖源站的缓存头,规则可能和源站的设置叠加,出问题时两边都要检查。

还有一个值得注意的响应头是 Vary。它告诉缓存:同一个 URL 的响应可能随某些请求头不同而不同,比如 Vary: Accept-Encoding 表示压缩与否会产生不同的副本。设置不当会造成缓存碎片,也就是命中率下降。

缓存键与查询参数

CDN 用“缓存键”来判断两个请求是不是同一个资源,默认通常包含域名和路径,是否包含查询字符串则取决于配置。如果忽略了不该忽略的参数,用户可能拿到别人的内容;如果把每次都不同的追踪参数(比如时间戳)算进缓存键,又会导致永远不命中。配置时要想清楚:哪些参数真正影响内容,就纳入缓存键,其余的忽略。

如何刷新缓存

更新内容后,旧内容可能还在节点里没过期。办法有这么几种:

在我看来最稳妥的方式就是版本化文件名,因为刷新操作有延迟、有配额,还可能因为操作失误漏掉某个路径。另外,浏览器本地也有缓存,CDN 刷新了不代表用户浏览器里的旧版会立刻消失,这点经常被忽略。

需要注意

关于备案,需要多说两句。如果你的站点面向中国大陆提供服务,一般要先在工信部门的系统里完成 ICP 备案(网站底部常见的那一串“ICP备”编号即是),并且使用在大陆开展业务、具备相应资质的 CDN 服务商。具体的流程、所需材料和要求,请以主管部门以及你所选服务商的官方说明为准,我这里不做保证性的描述。跨境的情况(用户在境外、源站在境内或相反)又是另一回事,同样需要看服务商的条款和相关规定。

关于“隐藏源站”:CDN 确实让外部看到的是节点 IP 而不是源站 IP,但如果源站地址通过其他途径泄漏(比如历史 DNS 记录、邮件头、同一服务器上的其他域名),攻击者仍可能直连源站。更稳妥的做法是在源站防火墙上只放行 CDN 的回源网段,具体网段以服务商公布的为准,防火墙的思路见 防火墙那篇。

HTTPS 与 CDN

当站点启用 HTTPS 时,浏览器与 CDN 节点之间会建立一条 TLS 连接,因此 CDN 上需要配置你域名的证书。CDN 到源站这一段(回源)可以是 HTTP 或 HTTPS,建议用 HTTPS,并校验源站证书,否则节点到源站之间就是明文传输。要避免出现“浏览器到 CDN 用 HTTPS,CDN 到源站用 HTTP,源站又强制跳转 HTTPS”这样的配置,它会造成无限重定向。证书和 TLS 的基础可以看 HTTPS 那篇。

怎么判断有没有命中

最直接的方法是看响应头。各家服务商会添加自己的头部来标示缓存状态(名称不统一,比如有的叫 X-Cache,有的叫 CF-Cache-Status 一类,请以实际服务商为准),此外还有标准的 Age 头,表示这份响应已经在缓存里待了多少秒。

curl -I https://www.example.com/static/app.3f9c1b2.js
curl -s -o /dev/null -D - https://www.example.com/static/app.3f9c1b2.js
curl -I --resolve www.example.com:443:203.0.113.20 https://www.example.com/
dig +short www.example.com
traceroute www.example.com

连续请求同一个资源两次,第二次出现了 Age 或者命中的标识,说明缓存生效了。--resolve 可以强制指定某个节点(这里 203.0.113.20 是示例地址),用于对比不同节点的表现,或者直连源站对比内容是否一致。用 traceroute 则可以看到到达节点的大致路径与时延。

常见问题排查

我自己的建议

个人静态博客流量不大,起步阶段不一定需要 CDN。真正需要的时候通常是:访问者分布很广、延迟明显影响体验;有大体积的静态文件(图片、视频、下载包);源站带宽有限、被流量压得喘不过气;或者需要边缘层提供额外的防护。引入 CDN 同时也引入了新的复杂度:缓存一致性、刷新操作、额外的配置、排查链路多了一环。先把缓存头写对、静态资源压缩好、图片体积控制好,往往已经能获得不小的提升,然后再根据实际情况决定是否上 CDN。

总结

CDN 的本质是把内容缓存到离用户更近的节点:通过 CNAME 与调度把用户引到边缘,命中则直接返回,未命中则回源并缓存。用好它的关键在于 Cache-Control 的正确设置、静态资源的版本化命名、合理的缓存键,以及对个性化内容的谨慎处理。面向中国大陆用户时要留意备案与服务商资质等合规要求,具体以官方规定为准。最后,别为了用而用,先看看自己是否真的需要。