做运维和前端性能优化的朋友大概都遇到过这种场景:源站明明跑得好好的,本地测速一二百毫秒,结果一到异地用户那边,首屏时间直接飙到三四秒,图片一张张往下掉。我第一次被这个问题按在地上摩擦,是给一个内容型站点做异地访问优化,当时天真地以为加个 CDN 域名解析过去就完事了,结果上线第三天发现缓存命中率只有六成出头,回源带宽不降反升,源站 CPU 被打到告警。后来花了两周时间把边缘缓存规则、缓存键、回源配置、预热刷新这一整套东西重新捋了一遍,才算把跨地域访问慢这个毛病按下去。这篇就把我踩过的坑和最后落地的配置方案完整写出来,从边缘缓存怎么设计,到回源怎么配、怎么压回源率、怎么验证效果,适合正在做站点加速的初中级同学照着抄,也适合已经上了 CDN 但效果不理想的同学对照排查。
1. 先搞清楚跨地域访问慢到底慢在哪
1.1 物理距离带来的延迟是绕不开的硬成本
很多人一上来就怀疑服务器性能不行,其实跨地域访问慢,第一位的锅是物理距离。光在光纤里的传播速度大约是每毫秒两百公里,一个来回下来,跨越上千公里的链路,单纯 RTT 就要几十毫秒。而一个网页加载往往不是一次请求,HTML 拉下来之后还要拉 CSS、JS、字体、图片,每一类资源都要重新握手。TCP 三次握手、TLS 握手,在长链路加弱网环境下,握手阶段就能吃掉两三个 RTT,运气不好碰上丢包重传,时间还要翻倍。这就是为什么同一份代码,本地访问两百毫秒,异地用户却要三四秒——不是源站算得慢,而是数据在链路上跑得久。
再往下拆,还有几个容易被忽略的因素。其一是运营商互联质量,跨网访问时中间要经过多次跳转和交换,链路质量参差不齐,高峰期丢包率上升,TCP 拥塞控制一触发,传输速率直接腰斩。其二是 MTU 和分片问题,某些链路上大包会被丢弃,导致大量重传。其三是源站出口带宽本身有限,几十个并发大文件下载就能把出口打满,后面的用户只能排队。所以做加速之前,先明确一件事:你要解决的是延迟问题、带宽问题,还是两者都有。延迟问题靠就近接入和协议优化,带宽问题靠边缘缓存分流,两者的手段不一样,混在一起谈就会像没头苍蝇一样乱改配置。
1.2 边缘缓存加回源,是这个体系里最基本的两条腿
CDN 这套东西说白了就干两件事:把内容提前放到离用户近的地方,以及在没有缓存的时候替用户去源站取一趟。前者叫边缘缓存,后者叫回源。理想状态下,热点内容都在边缘,用户请求在边缘节点直接返回,RTT 从几十上百毫秒降到个位数到十几毫秒;只有冷内容、动态内容、首次访问才会触发回源。所以衡量一个加速方案好不好,核心就看两个数:边缘缓存命中率高不高,回源质量和回源量可不可控。
我习惯把整个链路画成四段来理解:用户到边缘节点这一段,比拼的是调度准不准、节点离得近不近;边缘节点内部的缓存查找,比拼的是缓存键设计合不合理、TTL 配得对不对;边缘回源到中间层或源站这一段,比拼的是回源链路质量、超时重试、并发合并策略;源站自身处理这一段,比拼的是后端能力和限流保护。跨地域访问慢,通常会在这四段里的某一段或者某几段同时出问题,而绝大多数人只盯着"上没上 CDN",不看这四段的实际数据,最后自然改不到点子上。
1.3 什么情况下 CDN 帮不上忙,得先想清楚
有一个认知必须先建立:CDN 不是万能药。纯动态接口、强依赖用户会话的内容、每次请求结果都不同的页面,边缘缓存基本无能为力,只能走动态加速,靠优化链路和协议来减少往返次数,收益通常在百分之二三十,不会像静态资源那样立竿见影。还有一类是内容本身更新极其频繁又要求强一致的场景,比如库存、余额,这类内容硬缓存只会带来数据错乱,宁可让它回源。
另外,如果源站本身的响应时间就很差,比如一个接口要查三秒数据库,那 CDN 再优化也只能把这三秒原封不动地传回来。这种情况先做源站性能治理,再谈加速。我见过太多案例,问题根子在慢 SQL 和没加索引,却一直折腾边缘配置,最后当然没效果。所以在动手之前,先把一个请求的耗时拆开:DNS 多久、建连多久、首字节多久、内容传输多久。哪一段占比最大,就先治哪一段,这个顺序错了,后面全是无用功。
2. 边缘缓存规则怎么设计才算合理
2.1 缓存键的设计直接决定命中率的天花板
缓存键是这个体系里最容易被忽视、又最要命的一环。默认情况下,缓存键通常由协议、域名、路径三部分组成,查询参数是否参与、是否区分大小写、是否带上某些请求头,各家平台的默认策略并不一致。问题就出在这里:很多前端项目习惯在资源地址后面挂时间戳,比如app.js?v=1699999999,或者营销链接带一堆utm_source、from、share_id参数,这些参数一变,缓存键就变,边缘节点会认为是不同的资源,于是同一份文件被缓存了几十份,命中率被硬生生打碎。
我的处理原则是:静态资源要么用文件名带内容哈希的方式做版本管理(比如app.3f9a2c.js),要么在配置里明确忽略掉无意义的查询参数。前者是最干净的做法,文件内容变了文件名就变,天然不需要刷新缓存;后者是补救手段,把utm_*、from、timestamp这类参数加入忽略列表,让它们不参与缓存键计算。这里有个坑要注意:忽略参数不能一刀切。像?page=2、?id=100这种真正决定内容差异的参数,一旦被忽略,用户就会看到别人的内容,这是事故级别的问题。所以忽略列表一定是白名单式的精确列举,不是模糊匹配,更不是"全部忽略"。
还有一个细节是域名。如果同一个站点同时挂了主域名和备用域名,或者带 www 和不带 www 都能访问,缓存键里包含域名就意味着两套缓存,各占一份空间。在 CDN 配置里把这两个域名收敛到同一份缓存,能白捡一截命中率。这类调整看着不起眼,实测下来对中小站点往往能带来五到十个百分点的命中率提升。
2.2 TTL 分层:不同资源用完全不同的时间尺度
TTL 设置最常见的错误是全站一个值,要么全设十分钟导致频繁回源,要么全设一天导致发布后用户看到旧内容。合理的做法是按资源类型分层,我给一个自己一直在用的参考表:
| 资源类型 | 建议 TTL | 版本策略 | 说明 |
|---|---|---|---|
| 带哈希的 JS/CSS | 一年 | 文件名哈希 | 内容变则名变,无需刷新 |
| 图片、字体、图标 | 7 到 30 天 | 目录加版本号 | 更新走新路径 |
| 视频、大文件分片 | 7 天以上 | 路径版本化 | 配合 Range 回源 |
| HTML 入口页 | 10 秒到 5 分钟 | 短 TTL | 保证发布能较快生效 |
| 公共配置 JSON | 1 到 5 分钟 | 接口版本号 | 兼顾新鲜度与命中率 |
| 用户相关接口 | 不缓存 | 无 | 避免串号 |
这张表背后的逻辑很简单:TTL 越长命中率越高、回源越少,但内容新鲜度越差。破解这个矛盾的办法不是找一个折中的中间值,而是把"内容变化"这件事从时间维度转移到路径维度。文件名带哈希、目录带版本号,内容一变更,地址就变,缓存自然失效,此时 TTL 设多长都无所谓,设一年反而最优。入口 HTML 因为地址不能变,只能靠短 TTL 加主动刷新来控制。把这两类资源分开对待,是整套缓存策略的核心分水岭。
2.3 缓存头的优先级关系,别让源站把 CDN 规则覆盖掉
这一条踩坑的人极多。很多平台的缓存规则是有优先级的:源站返回的响应头优先级通常高于控制台上配的默认规则,而控制台上配的针对特定路径的规则又可能高于全局默认。也就是说,你辛辛苦苦在控制台把某个目录设成缓存一天,结果源站返回一个Cache-Control: no-cache,边缘节点照样不缓存,你还在纳闷为什么命中率上不去。
几个关键响应头必须记牢。Cache-Control: max-age是给浏览器和共享缓存用的;s-maxage专门针对共享缓存(CDN 就属于这一类),它的优先级高于max-age,想给 CDN 单独设更长的缓存时间,用这个最合适。private表示只允许浏览器缓存,CDN 必须不缓存,用户态接口一定要带上。no-store是完全不存,连浏览器都不存;no-cache容易被误解,它其实允许存储,只是每次使用前必须回源校验,很多人把这两个搞混,结果配了no-cache还奇怪为什么还有缓存。Expires是老古董,优先级低于Cache-Control,两都存在时以后者为准。
我的经验是:源站只负责声明内容的性质(能不能缓存、是不是私有),具体缓存多久交给 CDN 控制台的规则去配。这样发布新策略时不用改代码重新发版,灵活得多。实操中我一般让后端统一输出Cache-Control: public, max-age=0, s-maxage=xxx,把 CDN 侧的缓存时间完全掌握在运维手里。
2.4 Cookie、Vary 和 Range 这三个隐形杀手
Cookie 对缓存的影响非常直接:绝大多数 CDN 的默认行为是,请求里带 Cookie 就不缓存,或者把 Cookie 纳入缓存键。前者会让所有登录用户的静态资源请求全部回源,后者会让缓存被无限打碎。正确做法是让静态资源域和主站域分离,静态资源请求根本不带 Cookie,这是最干净的方案;如果做不到分离,就在 CDN 上配置"忽略 Cookie 进行缓存",但只对静态资源目录开这个口子,接口路径千万别开。
Vary 头是第二个隐形杀手。Vary: Accept-Encoding会让同一份资源按压缩与非压缩存两份,这还算正常,因为客户端确实需要不同格式。但Vary: User-Agent或Vary: Cookie就是灾难,前者会把缓存按 UA 打碎成几十份,后者直接导致缓存不可用。上线前用curl -I扫一遍响应头,看到这两个基本就得让后端改掉。
Range 请求影响的是大文件和视频场景。如果 CDN 不支持分片回源,用户拖动进度条请求某一段时,边缘节点会把整个文件从源站拉一遍,源站带宽瞬间爆炸。所以视频、安装包这类大文件,一定要确认边缘支持 Range 回源,并且回源时按片拉取。这一条我是在一次视频站点优化中血泪学到的,当时源站出口被打满,查了半天才发现是拖动进度条导致的整文件回源。
3. 回源配置:把回源率压下去,把回源质量提上来
3.1 回源 Host、协议和端口这三个参数必须一次配对
回源配置里最基础也最容易配错的就是回源 Host。它的作用是告诉源站"我这次请求访问的是哪个站点",因为一台源站上可能挂了几十个站点,靠 Host 区分。常见的坑是:CDN 加速域名是static.example.com,源站实际站点绑定的是origin.internal.example.com,如果回源 Host 没改成后者,源站会返回 404 或者跳到默认站点,表现就是"CDN 上全是 404,但源站直接访问正常"。这个问题的排查成本不低,因为很多人第一反应是怀疑缓存,绕一大圈才想到 Host。
回源协议要跟源站实际监听保持一致。源站只开了 443,你配 80 回源,结果就是连接超时或者 301 循环。反过来,源站只开 80,你配 443 回源,握手直接失败。现在比较稳妥的做法是源站同时监听 80 和 443,CDN 侧统一用 HTTPS 回源,并在回源链路上校验证书。如果源站用的是自签证书或者内部 CA 签发的证书,记得把回源"校验证书"的开关按实际情况处理,别一边要求校验一边用自签,那必然是握手失败。
端口这块相对简单,但要注意非标准端口在某些平台上需要在回源配置里显式声明,否则默认走 80/443 会连不上。还有一个细节是回源域名解析:如果回源地址写的是域名,边缘节点需要做 DNS 解析,解析结果的稳定性和 TTL 会影响回源成功率。生产环境我更倾向于配多个回源地址做冗余,避免单个地址故障导致整体不可用。
下面是一段源站侧的参考配置,重点是让源站能正确识别 CDN 回源请求、正确输出缓存头、并支持 Range:
server { listen 443 ssl; server_name origin.internal.example.com; # 回源链路开启长连接,减少握手开销 keepalive_timeout 75s; keepalive_requests 1000; # 静态资源:允许共享缓存,CDN 侧可长缓存 location ~* \.(js|css|png|jpg|jpeg|webp|woff2)$ { add_header Cache-Control "public, max-age=0, s-maxage=2592000"; add_header Access-Control-Allow-Origin "*"; add_header Accept-Ranges bytes; etag on; expires off; } # 入口 HTML:短缓存,允许协商 location = /index.html { add_header Cache-Control "public, max-age=0, s-maxage=60"; } # 用户态接口:禁止 CDN 缓存 location /api/user/ { add_header Cache-Control "private, no-store"; } }3.2 回源超时、重试与并发回源合并
回源超时的三个参数要分开设置:连接超时、首字节超时(等待源站开始响应的时间)、读超时(传输过程中的空闲时间)。连接超时一般设短一些,三到五秒足够,因为跨地域建连如果五秒还没建立,基本可以判定链路有问题;首字节超时才是关键,它直接决定了源站慢的时候用户等多久,通常设十到三十秒;读超时针对大文件传输,可以设长一些,六十秒以上都正常。
这三个值配小了会导致源站稍微抖一下就被判超时,用户看到 5xx;配大了会导致源站真挂的时候,用户一直转圈直到超时,体验更差。我的经验是按业务类型分开配:接口类首字节超时短一些,八到十秒,快速失败让前端走降级;页面和静态资源可以放宽到二十秒以上。
重试策略要特别注意"重试放大"问题。边缘节点回源失败后自动重试,本身是好事,但如果源站已经因为压力过大而变慢,重试会让请求量翻倍甚至翻几倍,直接把源站压死。所以我一般只对连接失败、5xx 这类明确可重试的错误开重试,且重试次数限制在一到两次,重试间隔做退避。4xx 一律不重试,因为它重试一百次结果也一样。
并发回源合并(有的平台叫请求折叠)是提效利器。同一个资源在同一时刻被一百个用户请求,边缘节点只向源站发一次请求,其余请求等这一次的结果。这个机制对热点内容首次回源特别有效,能把回源峰值削掉一大截。启用它的前提是回源请求必须是幂等的 GET,对 POST 一定要关掉,否则会出数据错乱。
3.3 分片回源与大文件场景的额外处理
大文件场景跟小文件完全是两套逻辑。一个几百兆的安装包,如果按整文件回源,源站出口带宽会被单个请求占满,其他用户只能干等。分片回源把文件切成固定大小的块(常见是 1MB 或 4MB),边缘节点按需拉取,用户拖到哪就拉到哪,带宽利用率高得多。
这里有个前提:源站必须支持 Range 请求,返回 206 状态码和Content-Range头。如果源站是个纯静态文件服务器,通常天然支持;如果源站是应用服务器动态输出文件流,就要自己实现 Range 逻辑,否则 CDN 发来的 Range 请求会被当成普通请求,返回整个文件,分片就白配了。验证方法很简单,用 curl 发一个带 Range 的请求看返回码:
# 期望返回 206 Partial Content,并带 Content-Range curl -I -H "Range: bytes=0-1023" https://origin.internal.example.com/bigfile.zip # 检查边缘节点是否正确透传 Range curl -I -H "Range: bytes=0-1023" https://static.example.com/bigfile.zip另外视频场景还有个特殊点:拖动进度条时会产生大量随机 Range 请求,如果每个都回源,源站压力很大。比较稳妥的做法是开预取,让边缘在用户播放时提前拉取相邻片段;同时把分片时长设短一些,减少单次拉取的数据量。
3.4 源站保护:别让加速把源站拖垮
这一条是我认为最应该强调、但讲得最少的。CDN 的一个副作用是:它会把流量放大后打到源站,尤其是缓存没命中的时候。如果源站没有保护,一次缓存集体失效或者一次恶意刷量,就能把源站打挂。
基本的保护手段有几个层次。第一层是回源限速,给单个边缘节点到源站的并发连接数设上限,避免瞬间打满;第二层是回源鉴权,让源站只接受带合法签名的回源请求,防止有人绕过 CDN 直接刷源站;第三层是源站前面的负载均衡和限流,按接口维度限制 QPS;第四层是兜底降级,源站超负荷时返回一个静态兜底页,而不是让请求堆积。
回源鉴权这块,常见做法是 CDN 在回源时带上一个密钥计算的签名头,源站在入口处校验。密钥要定期轮换,且不能写死在前端代码里。还有一种是时间戳签名,把过期时间编进签名,超过时间就拒绝,防止重放。这套东西配起来麻烦一点,但比源站被打挂之后救火要省事得多。
4. 一套完整的落地配置过程
4.1 接入前的准备:域名规划与源站梳理
动手配之前,先花半天时间把资产盘清楚,能省掉后面大量返工。第一步是域名规划,我推荐至少分三个:主站域名承载 HTML 和接口,静态资源域名专门承载 JS、CSS、图片、字体,上传下载域名承载大文件。分开的好处是缓存策略可以完全独立配置,静态资源域可以放心开长缓存、忽略 Cookie,主站域保持短缓存并允许带 Cookie,互不干扰。顺带还能突破浏览器对单域名并发连接数的限制,虽然 HTTP/2 之后这个限制弱化了,但域名分离在缓存管理上的价值依然很大。
第二步是梳理源站。把源站的 IP、端口、监听协议、证书情况、站点绑定关系全部列出来,特别是确认源站是否有多个实例、是否有负载均衡层、回源应该指向负载均衡还是直接指向实例。如果源站有 WAF 或者访问控制白名单,要提前把 CDN 的回源网段加进去,否则回源全部被拦截,表现就是全站 403。
第三步是确认业务里哪些路径是绝对不能缓存的。用户中心、订单、支付回调、后台管理,这些路径要在配置里显式排除。我的做法是先在 CDN 上配一条全局"不缓存"的兜底规则,然后逐条添加允许缓存的路径白名单,这样即使漏配,最坏结果是命中率低,而不是数据串号。这个顺序千万别反过来。
4.2 缓存规则配置实录
下面这张表是我实际项目里用的规则集,按优先级从高到低排列,平台之间的字段名可能不同,但逻辑是通用的:
| 优先级 | 匹配路径 | 缓存行为 | TTL | 缓存键处理 |
|---|---|---|---|---|
| 1 | /api/user/* | 不缓存 | - | - |
| 2 | /api/order/* | 不缓存 | - | - |
| 3 | /api/public/config | 缓存 | 60s | 忽略 utm_* 参数 |
| 4 | /static/.js, /static/.css | 缓存 | 30d | 忽略全部查询参数 |
| 5 | /img/* | 缓存 | 7d | 忽略全部查询参数 |
| 6 | /upload/* | 缓存 | 7d | 保留全部参数 |
| 7 | /*.html | 缓存 | 60s | 保留全部参数 |
| 8 | 默认 | 不缓存 | - | - |
第 4 条之所以敢"忽略全部查询参数",是因为这些文件名本身带内容哈希,查询参数纯属历史遗留,忽略掉只会提升命中率,不会串内容。第 6 条保留全部参数,是因为上传目录的图片地址参数可能带尺寸处理指令,忽略会导致返回错误尺寸。第 7 条 HTML 保留参数,因为分页、筛选都靠参数区分。
配套还要配几条响应头处理:在边缘统一加上Access-Control-Allow-Origin,省得源站每个接口都写一遍;把源站返回的Set-Cookie在静态资源路径上剥掉,避免污染缓存;开启 Brotli 压缩,文本类资源能省三到五成带宽,注意开了压缩之后缓存键会自动按编码区分,这是正常的。
4.3 预热与刷新的正确用法
预热是把内容主动推到边缘,让第一批用户不用等回源。它特别适合两类场景:大促或者活动上线前,把确定会热的关键页面和图片提前推一遍;版本发布后,把新的静态资源推到主要节点。预热的坑在于它有速率限制,一次推几千个 URL 会被限流,而且预热是按 URL 来的,如果你的资源地址带随机参数,预热基本无效。所以预热清单要精确,优先推首页、频道页、核心图片这些真正高热的内容。
刷新是让缓存失效,分 URL 刷新、目录刷新、正则刷新几种。URL 刷新的额度通常有限,发布后动辄几百个文件,靠 URL 刷新根本不够用。我的做法是发布流程里把静态资源文件名带上内容哈希,这样根本不需要刷新,新文件新地址,老文件自然淘汰;只有入口 HTML 需要刷新,数量少,用 URL 刷新完全够。目录刷新要慎用,因为它会把整个目录下的缓存清空,一瞬间全部回源,很容易把源站打崩。如果非要清目录,配合预热一起做,先刷新再预热,把回源压力控制在可控范围。
4.4 验证:怎么确认配置真的生效了
配完不验证等于没配。最基础的是用 curl 看响应头,重点看三个字段:X-Cache类字段(各家命名不同,表示命中还是回源)、Age(表示这份缓存在边缘存了多久)、Cache-Control。
# 第一次请求,观察是否 MISS 并触发回源 curl -sI https://static.example.com/static/app.3f9a2c.js | grep -iE "x-cache|age|cache-control" # 连续请求几次,观察是否变成 HIT 且 Age 递增 for i in 1 2 3; do curl -sI https://static.example.com/static/app.3f9a2c.js | grep -iE "x-cache|age" done除了看头,还要做地域维度的验证。用不同区域的探测节点分别请求同一个地址,比较首字节时间和下载速度。我更关注的是首字节时间的分布,而不是平均值——平均值会被少数好数据拉低,掩盖长尾问题。看 P95 和 P99,如果某些区域的 P95 明显偏高,说明该区域的调度或者节点覆盖有问题,这时候要考虑调整调度策略,而不是继续改缓存。
最后一步是压测。用几十上百并发打同一个热点 URL,观察边缘节点的响应时间和源站的回源量。正常情况下,边缘响应应该稳定在几十毫秒内,源站回源量应该只有一次(并发合并生效)。如果源站回源量随并发线性增长,说明并发合并没开或者没生效,这个必须查清楚。
5. 动态加速和链路层的优化空间
5.1 动态内容加速能做到什么程度
动态内容不能缓存,但可以优化传输。核心思路有三条:一是就近接入,用户的请求先到最近的边缘节点,再由边缘节点通过优化过的骨干链路回源,避免公网绕路;二是连接复用,边缘节点与源站之间保持长连接,省掉每次请求的握手开销;三是协议优化,用更新的传输协议减少往返次数。
这三条加起来的收益,实测下来在跨地域场景通常是百分之二十到四十,具体取决于原始链路的糟糕程度。链路越差,收益越明显;如果用户本来就在源站附近,收益接近于零。所以动态加速不是必需品,要看你的用户分布。如果八成用户集中在源站所在区域,优先做静态资源加速就够了;如果用户分布很散,动态加速的性价比才体现出来。
这里要泼一盆冷水:动态加速解决的是"传输慢",解决不了"处理慢"。一个接口如果后端要算两秒,加速之后还是一片哀嚎。上线动态加速之前,先把源站的慢接口治理一遍,把 P99 压到合理区间,再谈链路优化,顺序不能乱。
5.2 协议层:HTTP/2、HTTP/3 与 TLS 优化的实际收益
HTTP/2 的多路复用解决了队头阻塞问题,同一个连接上可以并行传多个资源,对资源数量多的页面提升明显。开启很简单,边缘节点基本都支持,源站侧如果用的是主流反向代理,加一行配置就能开。但要注意,HTTP/2 要求 HTTPS,而且部分老旧客户端不支持,需要保留 HTTP/1.1 的兼容。
HTTP/3 基于 UDP,在弱网和移动网络下的表现明显更好,丢包时不会像 TCP 那样整体降速。代价是部分网络环境对 UDP 有限制,需要有回退到 HTTP/2 的机制。现在的做法一般是同时开 HTTP/2 和 HTTP/3,让客户端自己协商,通过Alt-Svc头告知客户端可以升级。上线后观察两个协议各自的请求占比和错误率,如果某个区域 HTTP/3 的失败率明显偏高,就在该区域降级。
TLS 这块的优化点也不少。开 TLS 1.3 能把握手从两个 RTT 降到一個,配合会话复用(Session Ticket),回访用户几乎不用重新握手。开启 OCSP 装订,把证书状态校验的结果缓存在边缘,省掉客户端额外去查证书状态的一次请求。证书链要精简,只带必要的中间证书,多带一级就多一次传输。这些细节单个看起来收益不大,叠在一起对首屏时间的改善是实打实的。
5.3 调度与容灾:让用户总能落到合适的节点
调度决定了用户被分配到哪个节点,是整套体系里最上游的一环。常见方式有 DNS 调度和 HTTP DNS 调度,前者依赖本地 DNS 解析,精度受限于 LDNS 的位置,后者由客户端直接请求调度接口,精度高但需要客户端配合。还有一种方式是任播,让同一个 IP 在不同区域都有节点,靠路由协议把用户带到最近的一个,这种方式对运维透明,但故障切换的粒度较粗。
不管用哪种调度,都要配健康检查。节点故障时,调度系统要在几十秒内把它摘掉,否则用户会持续访问到坏节点。健康检查的探测点要覆盖真实链路,不能只在机房内部探测,因为很多故障是局部链路问题,机房内部探测一切正常,外部用户却完全连不上。我一般会要求探测点分布在不同区域和不同运营商,探测间隔控制在十秒以内,连续失败三次才摘除,避免误判。
容灾还要考虑源站故障的情况。边缘缓存里已有的内容,在源站挂掉的时候依然可以正常服务,这是 CDN 的一个隐性价值。所以对于关键内容,可以适当延长 TTL,让它在源站故障时多撑一段时间。同时配置好源站故障时的兜底策略,比如返回缓存中的旧副本(有的平台叫"过期缓存兜底"),宁可给用户看几分钟前的旧数据,也好过直接报错。
6. 监控指标与问题排查实录
6.1 必须盯住的几个核心指标
配完只是开始,真正的工作在后面的持续观察。我每天会看这几个数:缓存命中率、回源率、边缘 5xx 率、首字节时间 P95、下载速度 P95。命中率的计算口径各家略有差异,要搞清楚分母是请求数还是流量,按请求数算的命中率对静态小文件敏感,按流量算的命中率对大文件敏感,两个都要看,否则容易被单一指标误导。
回源率是命中率的另一面,但它更能反映问题。命中率下降时,回源率会上升,同时回源带宽也会上升。如果发现回源率突然抬头,先看是哪些 URL 在回源,通常能立刻定位到问题。首字节时间和下载速度要分区域看,全局平均值参考价值不大。5xx 率是最需要盯的,一旦抬头,往往意味着源站出问题或者回源配置有误,要在几分钟内响应。
还有一个容易被忽略的指标是状态码分布里的 4xx 占比。如果 4xx 突然增多,可能是有人扫描,也可能是缓存键配置错误导致大量请求打到不存在的路径。另外要注意 206 的占比,206 占比高说明大文件分片在正常工作,如果大文件站点 206 占比接近零,那多半是 Range 没生效。
6.2 常见问题速查表
| 现象 | 可能原因 | 排查动作 | 处理方式 |
|---|---|---|---|
| 命中率长期低于 70% | 查询参数打碎缓存键 | 抽样看回源 URL 的参数分布 | 配忽略参数列表或用哈希文件名 |
| 发布后用户看到旧内容 | HTML TTL 过长且未刷新 | 检查入口页 TTL 和刷新记录 | 缩短 TTL,发布流程加刷新步骤 |
| 全站 404 但源站正常 | 回源 Host 配错 | 直接访问源站 IP 加 Host 头验证 | 修正回源 Host 为源站绑定域名 |
| 大面积 502/504 | 回源超时或源站过载 | 看源站负载和回源超时配置 | 放宽超时,加限流和重试退避 |
| 图片偶发串号 | 缓存键忽略了有效参数 | 复现带不同参数的请求 | 恢复该参数的缓存键参与 |
| 视频拖动卡顿 | 不支持 Range 回源 | 检查 206 响应占比 | 开启分片回源,源站支持 Range |
| 某些区域访问特别慢 | 调度不准或节点覆盖不足 | 分区域看 P95 首字节 | 调整调度策略,补充节点 |
| 源站带宽被打满 | 缓存集体失效导致回源洪峰 | 看回源带宽曲线和刷新记录 | 错峰刷新,配合预热,加回源限速 |
6.3 几个反直觉的坑,我替你踩过了
第一个坑:缓存时间设得越长越好吗?不是。有一次我把图片 TTL 设成三十天,结果运营改了一张 banner 图但没改文件名,用户三天后还在看旧图,投诉一堆。后来改成图片走带版本号的目录,新版放新目录,老版本继续缓存,两不相干,问题才彻底解决。所以长 TTL 的前提是地址可变,地址不可变就别贪长。
第二个坑:开了压缩反而变慢了。早期我在边缘开了全量压缩,结果图片也被压了一遍,CPU 上去不说,压缩后的图片体积反而更大。压缩只能对文本类资源开,图片、视频、压缩包这些已经是压缩格式的,再压纯属浪费。而且压缩要在边缘做,不要回源时让源站压,边缘做的效率高得多。
第三个坑:以为开了 CDN 就万事大吉,忽略了 DNS 的 TTL。有次切换加速服务商,改了解析,结果半天内还有用户解析到老地址,访问异常。原因是原来的 DNS 记录 TTL 设了一天。后来所有准备切换的域名,提前两天把 TTL 降到五分钟,切换完再调回来,就再没出过这个问题。
第四个坑:缓存刷新当成万能药。一有内容不一致就刷缓存,刷完源站被打一波洪峰,然后又开始限流,恶性循环。正确的姿势是尽量用版本化地址从根上避免刷新,刷新只作为应急手段,且刷新后立刻预热,把回源压力提前释放掉。这个习惯养成之后,源站告警少了一大半。
第五个坑:只测首页不测长尾。首页往往是最热的,预热也最充分,测出来数据很好看。但真正影响用户体感的是那些长尾页面,它们冷、没预热、每次都要回源。我现在的做法是专门抽样一批长尾 URL 做定时探测,把长尾的首字节时间单独拉一条曲线出来看,这条曲线才真实反映大部分用户的体验。
最后分享一个我在实际项目里固定下来的习惯:每次调整 CDN 配置,都在变更记录里写清楚改了哪条规则、预期影响是什么、怎么回滚。这套体系里的配置项太多,彼此还有优先级关系,隔两个月回头看,没人记得当时为什么把某条规则设成六十秒。把决策理由留下来,比留下配置本身更有价值。