更新日期:2026-09-18
缓存命中率是成本与性能上最直接的杠杆,不是可以炫耀的面子指标。按请求口径算,命中率 60% 意味着每 100 个边缘请求有 40 个打到源站;提到 95% 后只剩 5 个,回源请求数下降约 87.5%,源站 CPU、数据库连接、出口带宽和首字节延迟同步下降。相反,命中率长期卡在 60%,通常说明关键响应根本没进缓存,或者进了缓存却因 Cache Key 碎片化而无法复用。
本文面向已在生产环境运行 Nginx 或 WordPress、前面挂着 Cloudflare 或同类 CDN 的站长与运维工程师。方法按「原理 → 检查 → 操作 → 验证 → 回滚」组织,命令通用可执行,不依赖厂商私有开关。自建边缘缓存(OpenResty、Varnish、Nginx proxy_cache)同样适用,可配合 OpenResty 与 Varnish 缓存实践 一起看。
与常见教程的区别在顺序。多数文章直接给一组「调大 TTL、打开 Cache Everything」的开关,但命中率低往往不是开关没开,而是缓存对象被拆成了互不复用的碎片。所以先讲口径——同一个 60%,在请求口径、字节口径和不同层级上可能是三个数字;再讲规则设计;最后讲回源治理与雪崩保护。
适用环境:CDN 支持按路径或扩展名配置缓存规则、源站为 Nginx 或 Nginx 反代、站内存在可公开缓存的内容。不适用场景:整站必须登录且所有响应都带个性化数据、实时行情与即时通讯接口、以 WebSocket 长连接为主的业务、以及视频流切片分发——这些场景命中率不是主要矛盾。
先看结论
- 先固定口径:同时记录请求命中率与字节命中率,写明统计窗口、是否含机器人、是否含非 2xx 响应,否则「提升到 95%」不可复核。
- 用源站日志交叉验证。在 log_format 加入 $upstream_cache_status,用 awk 聚合各路径命中分布,比只看控制台更早发现问题。
- 把 Cache Key 的最小必要维度写成清单,只保留真正改变响应内容的维度,追踪参数一律忽略或归一化。
- 静态资源用内容哈希文件名加长 TTL 与不可变标记,走「发布即新 URL」,正常情况不需要主动刷新。
- HTML 与接口用短 TTL 配合 stale-while-revalidate 与 stale-if-error,压缩必须回源的比例,同时保留故障兜底。
- 打开请求合并与源站锁,避免未命中对象在重建瞬间被并发回源打穿源站,这是命中率提升后最容易新引入的故障。
- 清理优先用版本化 URL,其次标签或前缀刷新;全量刷新只作事故时的最后手段,并配合预热,否则形成回源洪峰。
命中率到底在看什么口径
命中率是比值,分子分母由你选择。请求口径是命中请求数除以总请求数;字节口径是命中响应字节数除以总响应字节数。典型 WordPress 站点里,请求多为小体积 HTML 与接口,字节多为大图片与静态资源:HTML 全未命中而图片全命中时,请求命中率可能只有 45%,字节命中率却高达 92%。两个数字都对,只是回答的问题不同——请求命中率贴近源站应用负载与数据库压力,字节命中率贴近出口带宽与流量成本。优化必须同时看,否则容易把预算花错地方。
第三个口径是缓存状态。命中不是二元状态,Cloudflare 用 cf-cache-status 暴露,Nginx 用 $upstream_cache_status 暴露,语义基本对应,可互相印证。
| 状态值 | 含义 | 判据 | 常见误读 |
|---|---|---|---|
| HIT | 对象在缓存中且未过期 | 带 Age,状态为 HIT | 以为 HIT 就省下全部成本,实际不同边缘机房各自命中 |
| MISS | 对象不在缓存中,本次回源 | 回源后对象通常写入缓存 | 以为 MISS 都是配置错误,首次访问必然 MISS |
| EXPIRED | 对象在缓存但已过期,本次回源重建 | 之前有缓存记录,TTL 到期 | 与 MISS 混为一谈,导致 TTL 调优方向错误 |
| REVALIDATED | 回源校验返回 304,继续用旧副本 | 条件请求命中 | 以为省了回源,实际仍有回源校验往返 |
| STALE | 返回过期副本 | 由 stale-while-revalidate 或 stale-if-error 触发 | 以为是故障,多数情况是设计好的兜底 |
| UPDATING | 返回过期副本并后台异步更新 | 后台更新期间的状态 | 以为缓存失效,实际是可用降级形态 |
| BYPASS | 按规则跳过缓存 | 请求或响应配置明确要求不缓存 | 常被忽略,是命中率被拉低的高频原因 |
| DYNAMIC | 未被缓存规则覆盖,默认不缓存 | 没有匹配任何缓存规则的 HTML 或接口 | 以为「动态内容无法缓存」,实际多数可边缘缓存 |
第四个口径是层级。现代 CDN 常有多级缓存:边缘、区域或上层缓存层、源站。边缘命中率、区域层命中率、回源率是三个数字。开启分层缓存后,边缘未命中不等于回源,排障不能只看边缘一层;评估时应同时看边缘未命中数与源站实际收到的请求数,差值即中间层拦截量。
两个仪表盘不一致的原因有七类:分母不同(源站侧还包含健康检查与运维直连);采样与延迟导致短窗口偏差;机器人流量处理不一致;301、304、404 是否计入不一致;WebSocket 与长连接是否计入不一致;时间窗口时区错位;发布或清理窗口内大量对象过期,两侧采样粒度不同,谷底深度也不同。结论是先用源站日志自算一遍,把它当作唯一仲裁基准。
先量化再优化
量化分两条腿:CDN 侧看总量与对象维度,源站侧看路径维度。控制台通常提供总请求数、命中数、未命中数、带宽、按内容类型或路径的分布、热门 URL 排名。菜单与字段名各厂商不同,以当前控制台实际展示为准,不要照抄别人的字段名。需要记录的字段是统计窗口、请求总量、命中量、未命中量、回源带宽、边缘带宽、热门路径命中率。
源站侧要补齐 Nginx 日志维度。默认日志没有缓存状态,必须显式加进 log_format。下面配置只影响日志写入,不改变缓存行为;重载前先用 nginx -t 校验。
log_format cache_probe '$remote_addr [$time_local] "$request" $status '
'$body_bytes_sent cache=$upstream_cache_status '
'upstream_time=$upstream_response_time host=$host uri=$uri args=$args';
access_log /var/log/nginx/access-cache.log cache_probe;
上线后确认字段生效,再聚合。下面的命令统计各路径命中分布与源站视角的未命中占比;若调整了 log_format 字段顺序,需同步改 awk 列号。
# 1) 确认新字段生效
tail -n 3 /var/log/nginx/access-cache.log
# 2) 按缓存状态统计请求数与字节数(源站视角)
awk '{ for (i=1;i<=NF;i++) if ($i ~ /^cache=/) { st=$i; gsub("cache=","",st) } req[st]++; bytes[st]+=$6 } END { for (s in req) printf "%-10s req=%-7d bytes=%d\n", s, req[s], bytes[s] }' /var/log/nginx/access-cache.log | sort -k2 -r
# 3) 找出发往源站最多的前 30 个路径
grep -E 'cache=(MISS|EXPIRED|BYPASS|DYNAMIC)' /var/log/nginx/access-cache.log \
| awk '{ for (i=1;i<=NF;i++) if ($i ~ /^uri=/) { u=$i; gsub("uri=","",u) } print u }' \
| sort | uniq -c | sort -rn | head -30
# 4) 统计带查询串的未命中占比
awk '{ total++; for (i=1;i<=NF;i++) { if ($i ~ /^cache=/) st=$i; if ($i ~ /^args=/) a=$i } if (a != "args=-" && st != "cache=HIT") q++ } END { printf "non-hit-with-args=%d total=%d ratio=%.1f%%\n", q, total, (total ? q*100/total : 0) }' /var/log/nginx/access-cache.log
正常输出应呈现清晰形状:静态资源路径绝大多数 HIT,HTML 与接口以 MISS 或 DYNAMIC 为主,并有一批带查询串的路径反复出现在未命中前列。异常分支有三种:静态资源也大量 MISS(TTL 过短、Key 被请求头拆散、或源站下了禁止缓存的头);同一 uri 出现成百上千个不同 args(追踪参数制造对象爆炸);BYPASS 占比异常高(绕过条件写得太宽)。
字节口径在源站算不全,因为源站只看得见穿透进来的流量。可行做法是用源站日志算出各路径未命中字节总量,再从控制台取同窗口边缘出口总字节,相减得到节省字节,除以边缘总字节即为字节命中率。该算法依赖两侧窗口对齐,采集时取整小时并留出几分钟聚合延迟。另外要专门统计「每个 URL 的日均回源次数」:一个从未变更的图片每天被回源几百次,说明缓存没活到第二天,问题在 TTL 或 Cache Key,而不是源站性能。
拉低命中率的五类原因
第一类是 Cache Key 碎片化。同一响应因请求头、Cookie、查询串或协议差异被拆成多个对象。判断方法:对同一 URL 分别用不同 User-Agent、不同 Accept-Language、带与不带某个跟踪参数各请求一次,若内容相同却次次 MISS,就是 Key 混入了不该有的维度。代价是对象数乘以维度数,缓存空间被迅速耗尽,淘汰率上升,命中率反而下降。
第二类是查询参数与 Cookie 污染。多数 CDN 默认把整个查询串纳入 Key,?utm_source=、?fbclid=、?spm=、分享令牌、会话 ID 会让同一页面变成无数对象。Cookie 更隐蔽:源站下发 Set-Cookie 时很多 CDN 直接放弃缓存;若把 Cookie 纳入键,命中率会塌到接近零。用下面命令检查请求头与响应头分布。
# 统计带 Cookie 的请求占比(识别会话类流量)
awk 'BEGIN{FS="\""} /Cookie:/ {c++} END {print "requests-with-cookie=" c}' /var/log/nginx/access-cache.log
# 探测响应侧是否下发禁止缓存的头
curl -sI https://www.yoursite.net/ | grep -iE 'cache-control|expires|set-cookie|vary|age|cf-cache-status|x-cache-status'
第三类是 TTL 过短。TTL 短于内容实际访问间隔时,每个访问者都撞上过期重建。WordPress 默认对登录态页面下发 Cache-Control: no-cache, must-revalidate, max-age=0,若规则直接沿用源站头,公开页面也会变得几乎不可缓存。判据是 Age 响应头分布:若 Age 绝大多数是个位数而访问间隔是分钟级,说明缓存几乎没起作用;另一个判据是「每个 URL 日均回源次数」远大于 1。
第四类是内容被标记为私有或不可存储。Cache-Control: private、no-store、no-cache 与 Set-Cookie 都会阻止共享缓存保存副本。有些是安全需要(购物车、结算、账户页),有些是框架或插件默认行为误伤,例如主题给所有页面加 no-cache,或会话插件每次响应刷新 Cookie。对公开页面执行 curl -I,出现 private 或 no-store 时先确认是有意为之还是配置泄漏。
第五类是把动态页面一律当成不可缓存。公开文章页、列表页、分类页在未登录状态下可以短 TTL 缓存,只需用 s-maxage 与 stale-while-revalidate 控制新鲜度,并对登录态、预览态做精确绕过。判据是 HTML 状态为 DYNAMIC 或 BYPASS 的请求占 HTML 总请求的比例,这个比例就是最直接的优化空间。
缓存规则设计
设计目标是「同一份内容只存一份,且存活足够久」。落地顺序按静态资源、HTML、接口三层推进,每层显式写出缓存条件、TTL、Cache Key 维度与绕过条件。
按扩展名和路径前缀划分是最稳定的起点。静态资源走扩展名规则(js、css、woff2、png、jpg、webp、avif、svg、ico),HTML 走路径规则(文章详情、分类归档、标签页),必须动态的路径显式排除(后台、登录、接口、结算、购物车、预览)。扩展名规则简单但脆弱——URL 改写去掉扩展名或加了版本参数就会失配,关键资源应同时补路径前缀规则作为兜底。
查询串策略决定成败。忽略所有查询串最激进,风险是真正改变内容的参数(分页 page、筛选、版本 v)被抹掉导致返回错误内容;白名单最安全,只把确认影响响应的参数纳入键,其余丢弃。下面 Nginx 配置只接受形如哈希的 v 参数进入 Key,其他参数无论怎么变都不产生新对象。注意它同时改变回源地址,必须先验证分页与筛选未被误伤。
map $arg_v $cache_ver {
default "";
"~^[0-9a-f]{8,64}$" $arg_v;
}
proxy_cache_key "$scheme$request_method$host$uri$cache_ver";
请求方法要明确:只有 GET 与 HEAD 适合缓存,POST、PUT、PATCH、DELETE 必须直连源站。多数 CDN 默认只缓存 GET 与 HEAD;自建 Nginx 缓存时用 proxy_cache_methods GET HEAD; 显式收口。HEAD 有时会被内部转换为 GET 处理,不能只看 HEAD 结果就下结论。
规则的顺序与优先级是踩坑重灾区。缓存规则通常按列表顺序求值,第一条匹配往往决定结果,后续规则可能不再叠加;站点上还可能存在其他影响缓存的产品,例如安全产品里的绕过设置或页面级规则,冲突时不会在界面高亮。可靠验证方法是二元的:改一条规则,只测一个 URL,用响应头确认最终状态,不要一次改五条再猜哪条生效。发现冲突后删掉冗余规则,让每条 URL 只被一条规则覆盖,而不是靠调顺序赌优先级。
| 内容类型 | 建议 TTL | Cache Key 维度 | 必须绕过的条件 |
|---|---|---|---|
| 带内容哈希的静态资源 | 一年以上并标记不可变 | 主机名、路径,忽略全部查询串 | 路径不在已发布资源清单内 |
| 不带哈希的图片、图标 | 数天到数十天 | 主机名、路径,忽略追踪参数 | 带鉴权令牌或属于私有图床 |
| 公开文章页与归档页 HTML | 数分钟到数十分钟,配合异步更新 | 主机名、路径,忽略追踪参数 | 登录态 Cookie、预览参数 |
| 公开接口 GET 响应 | 数十秒到数分钟,视敏感度 | 主机名、路径、白名单参数 | 带登录态或用户标识参数、写操作 |
| 后台、登录、结算、购物车 | 不缓存 | 不适用 | 一律绕过且不写入缓存 |
Cache Key 与 Vary 的代价
Cache Key 每增加一个维度,对象数就乘以该维度的取值数。2000 个 HTML 页面按路径缓存是 2000 个对象;加语言维度(3 种)变 6000;再加设备类型(3 种)变 18000;再加币种或地区(4 种)就是 72000。缓存空间有限,对象数上升直接推高淘汰率,把「多存一份更精准」变成「谁都不常驻」,这正是规则精细却命中率走低的原因。
Vary 响应头是另一条碎片化路径。源站用 Vary: Accept-Language、Vary: User-Agent、Vary: Cookie 表达内容协商,共享缓存要么尊重它、每个取值各存一份,要么忽略它、承担返回错误变体的风险。不同 CDN 对 Vary 的处理并不一致,有的只对编码协商特殊处理,有的完全忽略,必须以官方文档和控制台实际行为为准。更稳妥的思路是让维度收敛到 Cache Key 的显式白名单,用路径或主机名表达大的内容分叉,而不是依赖 Vary。
降低维度的成熟手法:语言维度只保留精简标签,把 zh-CN,zh;q=0.9,en;q=0.8 归一化为 zh,且只从 URL 前缀或明确的语言 Cookie 判断;设备维度优先用客户端提示而不是完整 User-Agent,因为完整 UA 取值近乎无限,纳入 Key 就会失控;内容分叉大时拆独立主机名,如静态资源走 static.yoursite.net、图片走 img.yoursite.net,各自拥有缓存空间、规则集与统计口径。
验证对象数是否增长有明确方法:挑一个代表性 URL,构造几组仅一个维度不同的请求,逐次探测并记录状态。第一次 MISS 正常,若每组都稳定 MISS,说明该维度确实在分裂缓存;再改一个本应被忽略的追踪参数,若仍命中,说明忽略规则生效。
URL="https://www.yoursite.net/blog/post-sample"
# 同一 URL 重复请求,第二次应为 HIT
curl -sI "$URL" | grep -iE 'cf-cache-status|x-cache-status|age'
curl -sI "$URL" | grep -iE 'cf-cache-status|x-cache-status|age'
# 仅改追踪参数,忽略规则正确时应命中同一对象
curl -sI "$URL?utm_source=newsletter" | grep -iE 'cf-cache-status|x-cache-status|age'
# 改 Accept-Language 与 UA,观察是否被拆成新对象
curl -sI -H 'Accept-Language: zh-CN,zh;q=0.9' "$URL" | grep -iE 'cf-cache-status|x-cache-status|age'
curl -sI -H 'User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)' "$URL" | grep -iE 'cf-cache-status|x-cache-status|age'
# 带会话 Cookie,预期为 BYPASS 而非 MISS
curl -sI -H 'Cookie: wordpress_logged_in_sample=1' "$URL" | grep -iE 'cf-cache-status|x-cache-status|age'
判据:仅改追踪参数应命中;会话 Cookie 应绕过而不是回源后又被写入缓存。最危险的异常是带 Cookie 仍返回 HIT——登录用户拿到别人的缓存页面,属于必须立即回滚的事故。
动态内容的边缘缓存
动态页面能否边缘缓存,取决于它是否对所有人返回相同内容。公开文章页、列表页、搜索结果首页、不随用户变化的接口响应都可以缓存;响应里出现用户名、购物车数量、余额、推荐位等个性化内容就不能用共享缓存保存。判断标准不是页面「看起来像不像动态页」,而是「是否存在两个用户应看到不同内容的情况」。
常见做法是给公开路径配置缓存资格与边缘 TTL,再由源站用 Cache-Control 表达新鲜度。s-maxage 只约束共享缓存、不影响浏览器缓存,适合「边缘短缓存、浏览器不缓存」;stale-while-revalidate 允许过期后先返回旧内容并后台异步更新,把等待压到接近零并避免过期瞬间同步回源;stale-if-error 允许源站出错时继续提供旧副本,是故障降级通道。这两个扩展在 RFC 5861 中定义,主流 CDN 与 Nginx 缓存都有支持,具体程度需查对应文档。示例 Cache-Control: public, max-age=0, s-maxage=600, stale-while-revalidate=60, stale-if-error=86400 的含义是:浏览器每次校验、边缘缓存十分钟、过期后一分钟内可先用旧副本、源站故障最多可用一天旧副本。数值须按内容更新频率调整,不能照搬。
登录态绕过要做成精确且可验证的规则。维护一份绕过条件清单:出现登录 Cookie、出现预览参数、属于后台与接口路径、属于购物车与结算路径。把这些条件集中在一个 map 里,读缓存与写缓存的判断共用它,避免出现「绕过了缓存但仍写入缓存」的半吊子状态。Cookie 名用正则前缀匹配,因为 WordPress 登录 Cookie 带哈希后缀。
map $http_cookie $skip_cache {
default 0;
"~*wordpress_logged_in" 1;
"~*wp-postpass" 1;
"~*woocommerce_items_in_cart" 1;
}
map $arg_preview $preview_bypass {
default 0;
"~^true$" 1;
"~^1$" 1;
}
map "$skip_cache$preview_bypass" $final_bypass {
default 0;
"~1" 1;
}
避免缓存个性化响应,最可靠的办法是让个性化内容走独立路径并显式关闭共享缓存。不要依赖 Vary: Cookie 区分用户,因为不少 CDN 会忽略该字段,忽略的后果是跨用户串页;也不要靠「用户标识放在查询串」隔离,那会同时制造海量对象并带来缓存欺骗风险。验证标准动作是双 Cookie 对照:准备两个不同登录 Cookie 请求同一路径,确认两者都绕过,且返回内容不含对方用户名或订单信息。
回源治理与缓存雪崩
命中率提升后,剩余低比例未命中会更有破坏力:集中出现时源站会在极短时间内被并发回源打穿。雪崩通常发生在三个时刻——缓存刚被全量清理、热点内容刚过期、发布后大量新 URL 同时首次访问。治理目标是把「同时回源」变成「合并为一次回源」,并准备好故障降级路径。
第一道闸是请求合并。Nginx 的 proxy_cache_lock 让同一未命中对象的并发请求只放一个回源,其余等待,proxy_cache_lock_age 与 proxy_cache_lock_timeout 控制等待上限,避免锁本身成为故障点;CDN 侧通常内置请求合并或去重,边界行为以官方文档为准。第二道闸是后台更新,proxy_cache_background_update 配合 proxy_cache_use_stale updating 在副本过期时先返回旧内容并异步重建。第三道闸是过期降级,把 error、timeout 及 500、502、503、504 加入 use_stale,源站抖动时用旧副本兜底。第四道闸是源站保护:限制回源连接数与并发数、设置超时,并确认 CDN 的回源重试策略不会在源站过载时放大压力。
缓存预热是发布流程里最易被忽略的一步。全量清理或大规模发布后热门 URL 会同时首次访问,预热就是在真实流量到来前主动填好热点对象。预热脚本应从访问日志或分析系统导出热门 URL,按固定速率顺序请求,并检查每次状态是否为命中,避免「请求了但没进缓存」。
# 预热并按响应头核对是否进入缓存
URLS=/tmp/warmup-urls.txt
while read -r u; do
[ -z "$u" ] && continue
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "$u")
state=$(curl -sI --max-time 10 "$u" | awk 'BEGIN{IGNORECASE=1} /^(cf-cache-status|x-cache-status):/ {gsub(/\r/,""); print $2}')
printf '%s\t%s\t%s\n' "$code" "$state" "$u"
sleep 0.2
done < "$URLS" | tee /tmp/warmup-result.tsv
# 统计预热后仍未命中的比例
awk -F'\t' '{ total++; if ($2 !~ /HIT/) miss++ } END { printf "total=%d not-hit=%d ratio=%.1f%%\n", total, miss, (total ? miss*100/total : 0) }' /tmp/warmup-result.tsv
注意该脚本会对生产站点产生真实请求,先在测试域名试跑并控制速率。高峰期间的额外保护:对回源路径做并发限制与排队而非无限放大;把可延迟任务(统计上报、图片缩放)从关键路径剥离;确认源站在缓存全空时也能撑住一个时间窗口,因为再高的命中率也有冷启动时刻。
Purge 策略与发布流程
第一原则是尽量不清理。静态资源文件名含内容哈希时,每次发布产生新 URL,旧 URL 自然淘汰,完全不需要主动刷新,这是成本最低也最安全的方案。需要关注清理的是 HTML,因为它通常保持固定 URL 且需要较快更新。
清理能力按粒度分档:按 URL 精确清理、按标签清理、按前缀或主机名批量清理、全量清理。可用档位与套餐相关,控制台里能看到哪些选项就以哪些为准,不要假设某个粒度一定可用。选择顺序是精确清理优先,其次标签或前缀,最后才是全量。标签清理需要响应带可枚举标签,通常在发布流水线由构建或应用层注入,否则后期无法按内容维度批量刷新;前缀清理适合按目录组织的站点。
清理预算必须提前算清。清理请求本身受接口频率限制,全量清理会让下次流量到来时所有对象变成未命中,等价于自伤式雪崩。判断依据很直接:全量清理后第一分钟的边缘请求量是否超过源站承载能力;若超过,就不能在高峰做全量清理,也不应把它当常规发布步骤。把清理与发布绑定更稳:发布流程产出文件清单,静态资源靠新 URL 免清理,HTML 按清单精确清理,清理后立即预热,最后用脚本核对关键页面的状态与内容版本号。
回滚路径要事先写清。内容发布错误的回滚是「恢复上一版内容 + 精确清理受影响 URL + 预热」;清理策略改错的回滚是「恢复上一版规则 + 重载配置 + 抽样验证」。全量清理不可逆——旧副本一旦删除只能靠预热与源站容量扛住——必须人工确认,并记录执行人、时间与原因。
Nginx 与 WordPress 落地片段
下面是定位为「公开内容短缓存、登录态与敏感路径穿透」的最小可用反向代理缓存配置,阈值偏保守,需按实际流量调整。修改后先 nginx -t,再 nginx -s reload,然后立刻用 curl -I 抽样验证并观察错误日志与缓存目录增长。回滚即恢复变更前的配置备份再重载。
proxy_cache_path /var/cache/nginx/cdn levels=1:2 keys_zone=cdn_cache:256m
max_size=20g inactive=7d use_temp_path=off;
proxy_cache_key "$scheme$request_method$host$uri$cache_ver";
proxy_cache_methods GET HEAD;
proxy_cache_min_uses 1;
proxy_cache_valid 200 301 302 10m;
proxy_cache_valid 404 1m;
proxy_cache_revalidate on;
proxy_cache_lock on;
proxy_cache_lock_age 10s;
proxy_cache_lock_timeout 10s;
proxy_cache_background_update on;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_bypass $final_bypass;
proxy_no_cache $final_bypass;
add_header X-Cache-Status $upstream_cache_status always;
PHP-FPM 直连时可用 fastcgi_cache,少一跳但必须配套绕过规则,否则后台与接口也会被缓存。下面片段用 map 集中定义跳过条件,用 fastcgi_ignore_headers 忽略源站的禁止缓存响应头——这一步威力很大,只在确认公开内容确实需要强制缓存时使用。
fastcgi_cache_path /var/cache/nginx/wp levels=1:2 keys_zone=wp_cache:256m
max_size=10g inactive=7d;
map $request_uri $wp_skip_cache {
default 0;
"~*/wp-admin/" 1;
"~*/wp-login\.php" 1;
"~*/wp-json/" 1;
"~*/wp-cron\.php" 1;
"~*add-to-cart=" 1;
"~*/(cart|checkout|my-account)/" 1;
"~*preview=true" 1;
}
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_methods GET HEAD;
fastcgi_cache_valid 200 301 302 10m;
fastcgi_cache_lock on;
fastcgi_cache_background_update on;
fastcgi_cache_use_stale error timeout updating http_500 http_503;
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
fastcgi_cache_bypass $wp_skip_cache;
fastcgi_no_cache $wp_skip_cache;
add_header X-FastCGI-Cache $upstream_cache_status always;
WordPress 侧三处适配重点。其一,页面缓存与对象缓存分工不同:对象缓存(例如用 Redis 作对象缓存后端,参考 WordPress 接入阿里云 Redis 对象缓存)解决数据库查询重复,页面缓存解决整页渲染重复,两者不冲突,但页面缓存插件与 fastcgi_cache 属同一层,同时启用会互相干扰,应二选一。其二,nonce 与用户身份绑定,整页缓存会把某个用户的 nonce 发给所有人,导致表单提交失败,正确做法是对登录用户绕过缓存,并让页面缓存插件处理或剥离 nonce 片段。其三,可用 DONOTCACHEPAGE 常量表达「此页不可缓存」,但其语义由具体插件实现,以所用插件文档为准。
上线顺序:先在测试域名启用并验证绕过规则,再对生产的一条低风险路径灰度开启,观察一整天后逐步扩大。每次只改一个变量,并保留变更前备份与回滚命令。
验证、监控与风险
目标设定要可复核。建议同时设定请求命中率目标、字节命中率目标、回源请求数绝对上限,以及源站在缓存全空时的承载边界。只写「命中率到 95%」不合格,因为分母、窗口、是否含机器人都未定义。可用表述:「公开内容的边缘请求命中率达 90% 以上,字节命中率达 95% 以上,回源请求数在业务高峰不超过源站实测承载能力的 60%,窗口为整小时且排除已知爬虫与健康检查」。
实验要避免同时改多个变量:先在基线窗口(连续 3 天)记录命中率、回源请求数、源站 CPU、首字节延迟与错误率;再只改一类规则(如查询串忽略策略),保持 TTL 与其他条件不变,观察一个完整业务周期;最后对比同组指标并保留原始日志。分组对比要注意工作日与周末、白天与凌晨的流量形态差异,尽量选形态相近的窗口。
监控与告警至少覆盖:命中率与回源率趋势(按小时)、源站请求量与错误率、缓存目录占用与淘汰情况、清理次数与清理后的回源峰值、关键页面内容版本是否与发布一致。阈值不要贴太近目标值,否则正常波动产生噪声;更实用的是对「突降」告警,例如命中率十分钟内相对前一小时下降超过一定比例,通常意味着规则被误改、源站响应头变化或批量清理。
需要提前想清两类故障。其一是过期内容:用户看到旧价格、旧库存或已修复的错误信息,应对办法是缩短相关 TTL、发布时精确清理受影响 URL 并预热,必要时临时对特定前缀关闭缓存。其二是私有数据泄漏:个性化内容被共享缓存保存后返回给其他用户,回滚必须立即彻底——先用绕过规则把相关路径置为不缓存,再做范围清理,然后复查日志确认没有同类路径;若涉及敏感信息,按事件响应流程处理。其他风险包括缓存欺骗(把动态 URL 伪装成静态资源路径)、把攻击者响应写进缓存、以及基于请求头的 Key 维度被恶意构造导致对象爆炸。共同防线是「Cache Key 只保留必要维度」与「动态路径显式绕过」。
上线前检查清单
- 已明确命中率口径:请求还是字节、时间窗口、是否含机器人与非 2xx 响应。
- 已在源站日志加入 $upstream_cache_status 并完成基线采集,保留原始日志。
- 已列出公开可缓存路径清单,以及必须绕过的后台、接口、结算、预览路径。
- 已确认 Cache Key 不含完整 User-Agent、完整 Accept-Language 原文或无意义追踪参数。
- 带会话 Cookie 的请求返回绕过状态,双 Cookie 对照测试未出现跨用户内容。
- 静态资源已用内容哈希文件名,并配合长 TTL 与不可变标记。
- HTML 已配置短 TTL、异步更新与过期兜底,并有明确更新时限预期。
- 已开启请求合并与源站锁,并验证并发回源被合并为一次。
- 已按热点清单做过预热演练,确认清理后能快速恢复命中率。
- 清理策略以精确清理与版本化 URL 为主,全量清理需人工确认。
- 已配置命中率突降、源站错误率与回源量告警,并明确值班响应方式。
- 已在测试环境验证全部配置,且保留可一键回滚的配置备份与恢复命令。
官方资料与继续阅读
- Cloudflare Cache 文档总览
- Cloudflare 缓存响应概念与状态说明
- Cloudflare Cache Key 配置说明
- Nginx proxy_cache 模块文档
- RFC 9111:HTTP 缓存
站内相关文章:



