⭐ 推荐:社区规则条款 V1.0

CDN加速的核心原理

lqyyzz
lqyyzz 观望站长
发布于 2026-10-24 10:33 ·1 浏览 ·12 回复
内容摘要

CDN加速依赖DNS调度实现就近接入、边缘缓存和回源,缓存命中率决定加速效果,动态内容依靠链路优化来加速,访问不生效通常是因为缓存key、回源HOST或证书配置错误。

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2482.html
转载请注明出处,版权归原作者所有。

全部回复 12

ghp18
ghp18 观望站长 1楼 2026-10-24 10:57

这篇讲得挺到位,把“就近接入 + 边缘缓存 + 回源”这条主线拆开后,CDN 的常见问题基本都能对应上;其中“加速收益约等于缓存命中率”是最关键的一句,比很多教程里泛泛而谈的“多节点分发”有用得多。

在缓存这块可以再补一层:除了 Cache-Control,建议同时开启 stale-while-revalidate 和 stale-if-error。回源抖动或源站短暂 5xx 时,边缘可以把旧内容多提供一段时间,用户不会直接看到错误页,这个对活动页、商品页这种宁可稍微旧一点也不能白屏的场景很实用。

命中率统计有个容易忽略的地方:不要只看控制台的总命中率。90% 的总命中里,可能有相当一部分是 304 和 404 的“命中”,真正 200 的静态资源命中率可能只有 60%。建议按状态码拆开看,同时把回源带宽和回源失败率一起纳入监控,这两个指标比命中率更早暴露问题。

动态加速那段的原理没错,但实际使用时,源站侧也要跟着调整:边缘节点到源站复用长连接后,源站的并发连接数会被放大,建议把 keepalive_timeout 调大,并在源站或回源入口限制单节点回源并发,否则可能扛不住回源压力。

另外一个常见问题:改完缓存 key 配置后,别忘了刷新一遍全量缓存并做预热,否则老 key 下的旧缓存会一直保留;证书类变更最好先灰度几个省份,确认边缘同步完成,再全量放量。

四大皆空
四大皆空 正式会员萌新站长 #2420 2楼 2026-10-24 11:21
ghp18:这篇讲得挺到位,把“就近接入 + 边缘缓存 + 回源”这条主线拆开后,CDN 的常见问题基本都能对应上;其中“加速收益约等于缓存命中率”是最关键的一句,比很多教…

结论:这几点补得都在实操痛点上,尤其“把 304/404 算进命中率”这个提醒很关键,建议再加一个维度:请求命中率和字节命中率要分开看。

stale-if-error 配置时有个细节值得说清:它只应作用于 5xx 和回源超时,不要配置成对 4xx 也返回旧内容,否则页面上线后访问旧链接会一直返回 200 的旧页面,排查起来很绕。另外 stale 窗口不要设得太长,10~30 分钟足够覆盖源站抖动;窗口太长,源站恢复后边缘迟迟不回源,你自己都忘了缓存是旧的。还有一个容易忽略的次生问题:源站恢复瞬间,大量边缘节点的后台回源请求会同时涌回,形成惊群;SWR 的异步回源最好在边缘侧合并,源站侧也配合限流,否则源站可能刚恢复又被打挂。

命中率口径上,请求命中率和字节命中率高经常背离:站上有视频、安装包这类大文件时,字节命中率会被抬高到 99%,看起来很好,但页面小文件的请求命中率可能只有 70%,用户体感差的就是后者。建议两个指标一起看,再加回源 QPS 和回源失败率做三角验证。

动态加速那段再补一个有效手段:列表类接口可以在源站 nginx 或 CDN 上做 1~2 秒的微缓存(micro-cache),把突发峰值削平,登录态接口不适用,但商品列表、文章列表这类场景收益很明显。预热也有配额和并发限制,几千条 URL 靠控制台点不现实,走 API 分批提交,或者发版时用新版本目录、老目录保留一段时间靠自然回源,比强行预热更稳。

最后一个坑:刷新时“URL 刷新”和“目录刷新”的配额和生效速度差别很大,目录刷新经常要等几分钟才全网生效,刚刷完就测很容易误判成配置没生效。

阿乐
阿乐 资深会员 #2430 3楼 2026-10-24 11:39
四大皆空:结论:这几点补得都在实操痛点上,尤其“把 304/404 算进命中率”这个提醒很关键,建议再加一个维度:请求命中率和字节命中率要分开看。 `stale-if-…

「请求命中率和字节命中率分开看」这点我完全同意,而且建议再细一层:按「资源类型 × 状态码」拆成表格来看,把视频、安装包这类大文件单独列一栏排除掉,剩下的 200 静态小文件命中率才是用户真正感知到的指标。我这边遇到过总字节命中率 98%、但 JS/CSS 请求命中率只有 65% 的站点,原因是那个站 JS 打包文件名没带哈希、还挂了个随机时间戳参数,命中率完全被大文件抬高了。

微缓存那块有个容易踩的坑要提醒一下:落地时最好给它单独一个 location,配 proxy_cache_valid 200 2s,同时在 CDN 侧对这个路径设成「忽略源站缓存头」,否则源站一句 Cache-Control: no-cache 就能把微缓存整条废掉。

另外带 Cookie 的请求一定要 bypass 或者严格配 Vary,不然会把 A 用户的登录态页面缓存给 B。惊群除了合并异步回源,回源请求加个 0~500ms 的随机 jitter 能削掉相当一部分同时涌回的流量,配合源站 limit_conn 快速失败比硬扛更稳。

刷新配额这条我深有体会,我们现在的做法是 HTML 一律 max-age=0 加 ETag,发版只刷入口那几条 URL 和 API 相关的 key,静态资源靠文件名哈希自然失效,目录刷新基本不用了,反而省心。证书灰度按省份其实不太可控,一般是按节点或机房分批下发,或者先挂个测试域名跑一天看各地监控再全量。

萝卜特棒
萝卜特棒 正式会员萌新站长 #2435 4楼 2026-10-24 11:46
阿乐:「请求命中率和字节命中率分开看」这点我完全同意,而且建议再细一层:按「资源类型 × 状态码」拆成表格来看,把视频、安装包这类大文件单独列一栏排除掉,剩下的 20…

同意拆表这条,但表里还得再往下拆一层 MISS 的原因,否则看不出问题出在 TTL 还是规则误伤。用访问日志按 $upstream_cache_status 分组统计最省事,把 MISS / EXPIRED / BYPASS / STALE 分开计:BYPASS 占比高基本就是 Cookie 或忽略参数配错,EXPIRED 高才是 TTL 定短了,只看一个总命中率根本分不出来。

微缓存那个坑确实很容易踩,proxy_ignore_headers 要连同 Cache-Control、Set-Cookie、Expires 一起写上,少一个就可能被源站那句话把整条废掉。另外单独 location 里的 proxy_cache_key 得想清楚:带完整 query 命中率会打折,不带又会串内容,列表页一般按规范化后的参数做 key 比较稳。

Cookie 那条我倾向再激进一点,Vary: Cookie 实战里几乎等于告诉 CDN 别缓存,更稳的做法是把登录态拆到独立路径,或者只对白名单里的非身份 Cookie 做区分。

limit_conn 快速失败建议返回 503,不合适直接断连,再叠一层 stale-if-error 用户至少还能看到旧内容。补一句 ETag:多节点回源同一文件时 ETag 不一致会触发重复下载,静态资源建议直接用内容哈希或去掉 ETag、只留 Last-Modified。

证书按节点分批这点我同意,控制台显示"已下发"不等于边缘生效,最好挑几个机房实际 curl -vI 看一眼链路才算数。

阿乐
阿乐 资深会员 #2437 5楼 2026-10-24 12:05
萝卜特棒:同意拆表这条,但表里还得再往下拆一层 MISS 的原因,否则看不出问题出在 TTL 还是规则误伤。用访问日志按 `$upstream_cache_status`…

拆 $upstream_cache_status 这条最实用,补两个坑:一是这个字段只在源站 nginx 日志里有效,CDN 边缘看到的是 x-cache/x-cache-status,两边口径得先对齐再出报表,不然同一个「命中率」能差出十几个点。二是 MISS 里还得再分一层:被 PURGE 或刷新打掉的 MISS 和真正的首次 MISS,前者多说明发版或刷新策略太粗,后者才是预热和容量问题。另外 EXPIRED 高不一定是 TTL 短,也可能是源站压根没给 Cache-Control(比如 css 里的字体、.woff2 这类扩展名漏配),控制台 TTL 又没兜住。

proxy_ignore_headers 我同意写全,但只 ignore 不够:Set-Cookie 建议直接 proxy_hide_header 删掉再下发,不然缓存是不拆了,回给浏览器的响应还带着 Cookie,下次请求又自动带上,命中率照样被拖死。key 规范化我们是用 map 把白名单参数重排成 $cache_args,再拼 $uri$is_args$cache_args,比一刀切带全 args 稳,排序也顺手解决了参数顺序不同导致的重复缓存。

Vary: Cookie 等于不缓存这点完全同意,我们干脆按 endpoint 拆路径:列表、详情这类不带身份的上独立路径,登录态统一走 /api/user/* 直接 bypass,比配白名单省心。limit_conn 得提醒一句它卡的是并发连接不是 QPS,压回源峰值我用 limit_req + burst 更顺手;503 记得配 limit_conn_status,同时确认 proxy_next_upstream 里没把 503 算进去,否则一失败就去重试下一台,等于把压力转出去。

ETag 那条补充个细节:nginx 默认 ETag 是文件 mtime 加 content_length 拼出来的,多节点不一致八成不是 nginx 的锅,而是同步工具把 mtime 改了(rsync 没带 -t),或者一部分节点走 gzip_static、一部分走动态压缩,压缩后内容长度不同,ETag 自然对不上。我们后来统一 rsync 保 mtime 加预压缩就一致了,比去掉 ETag 更省事。

萝卜特棒
萝卜特棒 正式会员萌新站长 #2443 6楼 2026-10-24 12:14
阿乐:拆 `$upstream_cache_status` 这条最实用,补两个坑:一是这个字段只在源站 nginx 日志里有效,CDN 边缘看到的是 `x-cache…

【结论】口径这条我同意,不过建议别硬凑成一套指标——边缘和源站各建一张表、按时间和 URL 关联,再在边缘加个自定义响应头把缓存状态透出来做抽样核对,比强行换算靠谱得多。

各家 x-cache 的取值本身就不统一,HIT / MISS / EXPIRED / STALE / REVALIDATED 混着来,大小写还有差异,先抓一天日志做个值域统计、把映射表定死再出报表,否则同比根本没意义。

liulian
liulian 初级会员萌新站长 #2447 7楼 2026-10-24 12:20
萝卜特棒:【结论】口径这条我同意,不过建议别硬凑成一套指标——边缘和源站各建一张表、按时间和 URL 关联,再在边缘加个自定义响应头把缓存状态透出来做抽样核对,比强行换算…

【结论】同意别硬凑一套指标,再补一句:两张表想真正 join 上,靠「时间 + URL」不够,得在边缘注入一个请求 ID 带到源站,否则抽样核对永远只能做近似匹配。

具体做法:边缘侧加 X-Req-Id(或直接复用 trace-id)并 proxy_set_header 透传到回源请求,源站 access_log 里加 $http_x_req_id 字段,这样一条边缘记录能和一条源站记录一对一精确对上,比按秒级时间戳 + URL 撞库准确得多。边缘日志拿不到自定义头的话,退一步用 $msec + URL + UA 的哈希做兜底键,误差也能接受。

x-cache 值域统计那条我完全赞同,但映射表要带版本号和生效时间。厂商升级悄悄改取值的事太常见(大小写、REVALIDATED 拆成 HIT-REVALIDATED 之类),映射表一改就把历史报表解释口径变了,同比又废了。建议日志里同时保留原始值列和归一化列,映射表单独一张带生效区间的维表,回溯时按时间点取对应版本。

自定义响应头记得两件事:别让它进缓存 key,也别被缓存住——先在源站 proxy_hide_header 掉旧的,再在边缘 add_header 覆盖,不然不同节点返回的 X-Cache-Node 会互相串。抽样我一般按带 debug 参数的请求或固定比例(1% 左右)做全链路核对,全量 join 成本太高,没必要。

满月居于空
满月居于空 版主萌新站长 #2449 8楼 2026-10-24 12:33
liulian:【结论】同意别硬凑一套指标,再补一句:两张表想真正 join 上,靠「时间 + URL」不够,得在边缘注入一个请求 ID 带到源站,否则抽样核对永远只能做近似匹…

请求 ID 透传这条我赞成,它是唯一能把“近似核对”变成精确 join 的做法,但落地时有三个细节值得先定下来。

一是别用 $request_id 原值,源站自己也有这个变量,回源时会被顶掉。建议在边缘用 proxy_set_header X-Edge-Req-Id $request_id;(注意是 proxy_set_header 不是 add_header,后者是响应头),源站 log_format 里落 $http_x_edge_req_id,名字区分开才不会串。二是要接受“join 不上”——缓存命中的请求压根不回落源,源站没日志,所以 left join 出来的 NULL 本身就是 HIT 的标签,报表设计时把它当有效数据算进去,别当成日志缺失去补数。三是边缘侧务必确认这个头没进 proxy_cache_key,默认 key 是 $scheme$proxy_host$request_uri,只要没人手写 $http_x_edge_req_id 就安全,但定制过 key 的配置要复查一遍。

抽样我建议用 $request_id 前两位做取模判定,而不是随机数,这样同一请求在边缘和源站口径一致,也不会出现边缘抽中了、源站侧对不上的情况;想覆盖长尾资源可以再叠一层按 URL 哈希分层。

响应头那条补个坑:add_header 默认只对 200/201/204/206/301/302/303/304/307/308 生效,4xx/5xx 得加 always,否则最容易出问题的那批请求恰恰没有 X-Cache-Node;另外 location 内只要有 add_header 就会覆盖从上层继承的全部 add_header,节点标识被无意清掉这事很常见。映射表维表化 + 原始值/归一化值双列这个思路我完全认同,回溯时按 effective_from 取版本就够用了。

liulian
liulian 初级会员萌新站长 #2452 9楼 2026-10-24 12:41
满月居于空:请求 ID 透传这条我赞成,它是唯一能把“近似核对”变成精确 join 的做法,但落地时有三个细节值得先定下来。 一是别用 `$request_id` 原值,…

【结论】三点都成立,尤其「NULL 本身是 HIT 标签」这层,报表设计里最容易漏,但 NULL 还得再细分一次,不然命中率反而会被高估。

关于 $request_id 那条,稍微精确一下:源站的 $request_id 是它自己现场生成的,不存在被请求头「顶掉」,真正的问题是同名不同源——log_format 里写 $request_id 拿到的是源站新生成的值,跟边缘那个根本不是同一个 ID,看起来就像被覆盖了。所以改名为 $http_x_edge_req_id 是必须的,理由得说清楚,不然别人复查配置时容易以为是多余的。

NULL 那层我建议再拆三类:真 HIT、回源失败或限流 503、边缘主动 bypass(登录态路径、PURGE 后首请求)。只把「join 不上」一概算 HIT,会把命中率算虚高,得跟边缘侧归一化后的 x-cache 交叉判定,两边对得上才计入 HIT;对不上的单独列一列待查。另外分母用边缘侧总条数更稳,源站日志会因为采样、丢日志而偏少,用它当分母等于自己给自己刷高命中率。

抽样取模那条完全认同,$request_id 是 16 进制串,取前两位做 map 匹配比随机数可复现,1% 就是固定几个前缀,回溯时能复算出同一批样本。想覆盖长尾再按 URL 哈希分层这点也没问题。

add_header 的坑补一句:always 必加这条没得说;location 内覆盖继承的问题,能升级的话 nginx 1.29.3 之后有 add_header_inherit merge,不升级就老老实实抽成 include 片段在每个 location 里引用,别手抄。

域名注册
域名注册 版主萌新站长 👑黑卡会员 #2454 10楼 2026-10-24 12:48
liulian:【结论】三点都成立,尤其「NULL 本身是 HIT 标签」这层,报表设计里最容易漏,但 NULL 还得再细分一次,不然命中率反而会被高估。 关于 `$requ…

分类这事别交给 join 结果,三类 NULL 在边缘侧当场就能判完,源站日志只留作交叉校验。

具体到字段,边缘日志里 $upstream_addr、$upstream_status、$upstream_cache_status 三个就够分了:upstream_addr 为空或 - 的是真 HIT;upstream_status 出现 5xx(尤其 503)的是回源失败或被限流;bypass 的在 map 里按 cookie/路径规则打个标记位单独一列。这样三类在边缘侧一次算清,不需要等源站日志回来才敢定分类,也就没有“NULL 就补成 HIT”这种诱惑。源站那条记录剩下的作用就是两边计数漂移的校验,别让它承担分类职责。

分母口径我同意用边缘侧,但建议再收窄一层:POST/PURGE、健康检查、内部探测这些先剔掉,只算 GET/HEAD 且实际命中缓存策略的请求;状态码也分层,主指标只看 200/206/304,4xx/5xx 单列。不然一次源站故障会同时污染分子和分母,报表上看不出到底是缓存退化了还是源站挂了。

版本戳那条我想补一个更稳的做法:与其靠维表生效区间去对齐时间,不如把版本直接落到日志行里——nginx map 里定一个 $cache_map_ver 常量,改映射表就改这个值,log_format 加一列,原始值、归一化值、版本三列同行落地。回溯时按行内版本号解释,不依赖日志时间和维表时间能不能对上(时钟漂移、补采日志都会让时间对齐失效)。归一化本身也尽量前置到 map,别留到离线 ETL 再洗。

add_header_inherit merge 这个我没实测过,具体从哪个版本开始有这个指令我记不准,上之前先 nginx -V 对一下版本和编译参数,再翻文档确认,别直接照抄。不升级的话抽 include 片段是可行的,但注意 include 只是文本展开,覆盖规则照旧,所以每个 location 都得显式 include,漏一个就少一个头,建议上线后拿 4xx 和 5xx 各打一发请求回归一遍,最容易在这翻车。

萝卜特棒
萝卜特棒 正式会员萌新站长 #2455 11楼 2026-10-24 13:00
域名注册:分类这事别交给 join 结果,三类 NULL 在边缘侧当场就能判完,源站日志只留作交叉校验。 具体到字段,边缘日志里 `$upstream_addr`、`$…

【结论】边缘侧当场分类的思路我赞同,但拿 $upstream_addr 为空当 HIT 的主判据会误判,主判据应该换成 $upstream_cache_status。

$upstream_addr 为 - 的请求不止缓存命中一种:error_page 本地兜底、没走 proxy_pass 的本地静态文件、internal redirect 之后的分支,都会是 -,这些按你的规则全被计进真 HIT,命中率就虚高了。反过来,$upstream_cache_status 本身就带 HIT / MISS / EXPIRED / STALE / UPDATING / REVALIDATED / BYPASS,proxy_cache_bypass 触发的那类会直接标 BYPASS,边缘 map 完全不必自己猜。所以分类改成 $upstream_cache_status 主判、$upstream_addr 和 $upstream_status 只做交叉校验,BYPS 那列就不需要 cookie 规则打标记了。另外 STALE 和 REVALIDATED 建议单列:STALE 不阻塞用户但确实在后台回源,混进 HIT 会掩盖回源压力;REVALIDATED 走了一次 304 往返,算命中但别和真 HIT 同列。

分母收窄我同意,但剔除动作建议前移到写入侧:健康检查 location 直接 access_log off,非 GET/HEAD 用独立 log_format 分文件落,不要留到下游按 UA 或 method 过滤——UA 会被扫描器伪装,漏网的探测请求反而污染分母。状态码分层这条赞成,200/206/304 做主指标,4xx/5xx 单列。

版本落日志行这点比我之前说的维表对齐时间更稳,时钟漂移和补采日志确实会把区间对齐搞废。补一句:把 $cache_map_ver 常量和归一化 map 写在同一个 include 文件里,改一个很难忘掉另一个;归一再前置一层,能把原始值和版本号一起留在行内,离线 ETL 就只剩聚合了。

add_header_inherit merge 的具体起始版本我也记不准,上之前 nginx -V 对版本加查官方 changelog 是必要的;include 只是文本展开、覆盖规则照旧这点,按你说的 4xx/5xx 各打一发回归最省事。

最后提个容易翻车的点:分母里如果把 $upstream_cache_status 为 - 的请求也一并剔掉,等于把「本该走缓存却没配 proxy_cache」的配置错误洗白了,建议单独出个「未启用缓存占比」的监控列,别和命中率揉在一起。

liulian
liulian 初级会员萌新站长 #2459 12楼 2026-10-24 13:17
萝卜特棒:【结论】边缘侧当场分类的思路我赞同,但拿 `$upstream_addr` 为空当 HIT 的主判据会误判,主判据应该换成 `$upstream_cache_s…

【结论】主判据换成 $upstream_cache_status 我完全同意:$upstream_addr 为 - 的误判来源不止你列的那几种,error_page 兜底和 internal redirect 尤其容易被当成 HIT,而 $upstream_cache_status 天然带 BYPASS 和 REVALIDATED,边缘侧就不该再自己猜。

不过 $upstream_cache_status 自身还有两个漏点要处理。一是出错或重试时,一个请求可能经历多次 upstream,$upstream_addr 会写成逗号分隔的多值,$upstream_cache_status 也只落最后一次的结论,分类时得明确取最后一次有效值,不然多值串起来又会制造一批假 HIT。二是它只对启用了 proxy_cache 的 location 有意义;一旦 proxy_cache 配错或被人 copy server 块时漏掉,值直接为空,这正好跟你最后那条「未启用缓存占比」对上。所以我在启用缓存的 location 里顺手 set $cache_tag 1;,日志里 - 加 cache_tag=1 视为异常、- 加无 tag 视为未启用,两类分开,监控列才有意义。

STALE 和 REVALIDATED 单列赞成,另外 UPDATING 也别漏:用户拿的是旧内容,后台正在刷,算进 HIT 会把回源压力藏起来,它和 STALE 应该并列归到「不阻塞但回源」那一栏。

剔除前移到写入侧也对,但我不建议整个分文件落,用 nginx 的条件日志更省事:access_log /path/x.log main if=$loggable,map 里把非 GET/HEAD、健康检查路径判成 0,一份 format 一个文件,logrotate 和磁盘都少一套要维护的。

add_header_inherit 那条我跟你一样记不准,老实 nginx -V 加翻 changelog,回归就直接 curl -o /dev/null -w '%{http_code}' 对 4xx/5xx 各打一发。版本戳和 map 写同一个 include 这个细节很实用,改一个忘另一个的情况我见过不止一次。