HTTP 缓存与 CDN 加速:从响应头到命中率验证
HTTP 缓存与 CDN 加速的关键在于用 Cache-Control 写明复用规则、用缓存键与 Vary 控制命中粒度、用 ETag 与版本化 URL 管理更新,避免仅添加一个加速域名、开启回源后就结束;并用脚本把这些语义固化为可断言的回归测试。
- 缓存先保证正确性再谈命中率,顺序反了,省下的带宽会变成排障工时
- 用 Cache-Control 的 max-age、s-maxage、no-cache、SWR 显式表达复用规则
- 缓存键被时间戳参数、参数顺序和 Vary 拆碎,是命中率低的主要原因
- ETag 条件请求返回 304,配合版本化 URL 与精确 purge 可控地更新内容
- 把缓存语义写成脚本断言进 smoke test,再用命中率与回源 QPS 验证
一、缓存要先说清楚,再开起来#
很多团队接 CDN(Content Delivery Network,内容分发网络) 的流程是:加一条加速域名、开个回源、压测一次,看到 TTFB(Time To First Byte,首字节时间) 降了就收工。但缓存真正决定的是三件事:谁可以复用这个响应、能复用多久、过期或回源失败时怎么办。这三件事没有标准答案,HTTP 头就是把你的选择显式写下来。
最近围绕 “Slop-Free AI Programming” 的讨论里有个共识:AI 能很快产出一份看起来合理的 nginx 配置或 CDN(Content Delivery Network,内容分发网络) 规则,但它不会替你验证这条规则在真实流量下产生了什么行为。缓存尤其如此:错的缓存配置不会报错,只会安静地返回过期数据,或者在密钥维度上串了数据。所以本文的路径是:先把语义写对,再用脚本证明它是对的。
缓存的第一原则是正确性,第二原则才是命中率。顺序反了,省下的带宽会以排障工时的形式还回去。
二、用响应头把缓存语义写下来#
先把常用指令的语义对齐,再决定每个接口用哪一组:
| 指令 / 头 | 含义 | 典型用法 |
|---|---|---|
| max-age | 私有缓存(浏览器)的新鲜期 | 静态资源、图片 |
| s-maxage | 共享缓存(CDN(Content Delivery Network,内容分发网络))的新鲜期,优先于 max-age | 公共只读接口 |
| no-cache | 可以存,但每次使用前必须回源验证 | 需要强一致又想省带宽 |
| no-store | 任何缓存都不得存储 | 密钥、账单、个人信息 |
| stale-while-revalidate | 过期后先返回旧值,同时后台刷新 | 高 QPS(Queries Per Second,每秒请求数) 的读接口 |
| stale-if-error | 回源失败时用旧值兜底 | 依赖上游的聚合接口 |
| ETag / Last-Modified | 验证器,配合条件请求返回 304 | 内容会变但不频繁 |
先看一眼线上真实响应头,注意用 HEAD 之外的 GET 请求,避免部分服务端对 HEAD 的处理差异:
示例说明:以下代码仅用于逻辑验证,生产环境需补充异常处理、超时与重试、日志与监控告警及降级策略。
curl -sS -D - -o /dev/null \
"https://www.api.plus/api/get?apikey=YOUR_KEY&ip=8.8.8.8"
重点看三个地方:有没有 Cache-Control;有没有 Set-Cookie 或 private(它们会让公共缓存倾向于跳过这个响应,这既是保护,也可能是命中率低的根因);有没有 Vary。像 IP 查询接口 这类结果依赖查询参数、且对同一 IP 相对稳定的接口,是天然的边缘缓存候选;而带密钥的请求要单独判断,见下一节。
三、缓存键与 Vary:命中率的大头在这里#
缓存键大致等于:请求方法 + 主机 + 规范化后的路径 + 查询串 + Vary 中列出的请求头。命中率上不去,八成是缓存键被拆碎了。
- 别带时间戳参数。 给请求追加
&t=1730000000做“防缓存”,等于把命中率按到 0,还会把边缘存储写满。要绕过缓存,用Cache-Control: no-cache的请求头或单独的调试开关,而不是改 URL。 - 参数顺序要统一。
?ip=8.8.8.8&apikey=K与?apikey=K&ip=8.8.8.8在某些 CDN(Content Delivery Network,内容分发网络) 上就是两个缓存对象。要么在 SDK 里固定顺序,要么在 CDN 上开启查询参数规范化。 - 密钥维度的取舍。 带 apikey 的请求,不同 key 的配额与返回内容可能不同,按 key 隔离缓存是对的;但要确认密钥不会出现在可公开访问的缓存键或日志里。如果无法保证,就给这些响应加
private或在 CDN(Content Delivery Network,内容分发网络) 上配置按路径绕过缓存。 - Vary 只放真正影响响应的头。
Vary: Accept-Encoding基本是必开项(gzip / br 是不同变体);把User-Agent放进 Vary 会让几乎每个请求都变成独立对象,属于典型的自伤。
以 随机动漫壁纸接口 为例,https://www.api.plus/api/dongman?type=json 的 type 不同就是不同资源,缓存键天然区分;如果接口行为是“每次返回不同图”,那就应该给很短的 s-maxage 或干脆不缓存,而不是指望 CDN(Content Delivery Network,内容分发网络) 帮你兜住随机性。
四、条件请求与边缘失效:安全地换内容#
内容会变但变化不频繁时,用验证器比用短 TTL 更划算。服务端返回 ETag,客户端带上 If-None-Match,内容没变就回 304,省掉整个响应体:
URL="https://www.api.plus/api/get?apikey=YOUR_KEY&ip=8.8.8.8"
ETAG=$(curl -sS -D - -o /dev/null "$URL" \
| awk -F': ' 'tolower($1)=="etag"{print $2}' | tr -d '\r')
echo "ETag: $ETAG"
curl -sS -o /dev/null -w "%{http_code}\n" \
-H "If-None-Match: $ETAG" "$URL"
如果第二次打印 304,说明验证链路是通的。ETag 建议基于响应体的摘要或业务版本号生成,不要用进程启动时间这类每次都变的值,那样等于没有验证器。
失效策略按可控性排序:
- 版本化 URL:把版本放进查询串或路径(如
?type=json&v=2),发布即生效,不依赖 purge 的传播速度。 - 精确 purge:按缓存键删除,必须保证线上请求的查询串与 purge 时完全一致,否则清了个寂寞。
- 短 TTL + SWR:放弃“立刻生效”,换取实现简单和回源平滑。
注意 POST 示例接口 这类写操作默认不进共享缓存,别指望 CDN 缓存它;幂等的查询如果要用 POST,只能在应用层做缓存。
五、SWR 与 stale-if-error:把缓存当可用性预算#
一个实用的读接口头可以长这样:
Cache-Control: public, s-maxage=60, stale-while-revalidate=300, stale-if-error=86400
它的意思是:边缘新鲜 60 秒;60 到 360 秒之间返回旧值并在后台刷新;回源出错时,最多可以拿一天内的旧值兜底。对上游偶发抖动的聚合类接口,这比直接 5xx 体面得多。
这类配置在流量突发时价值更高。Cloudflare 2026 Q3 的 DDoS 报告提到攻击正在变成“更多蜂群、更少巨兽”——单次时间更短、频率更高,源站面对的是密集的小突发而不是一次长打。缓存层能吸收掉一部分读流量,把回源 QPS(Queries Per Second,每秒请求数) 压到源站真正能承受的量级,这本身就是一种降级能力。
六、把缓存约束写成回归测试#
这是 Slop-Free 最直接的落地方式:不要只给 Agent 一份配置文件,给它一份可执行的契约。下面这段脚本对线上真实 URL 断言响应头,放进部署流水线的 smoke test 里:
#!/usr/bin/env bash
set -euo pipefail
check() {
local url="$1"; shift
local hdr; hdr=$(mktemp)
curl -sS -D "$hdr" -o /dev/null "$url"
for rule in "$@"; do
if ! grep -qiE "$rule" "$hdr"; then
echo "FAIL $url -> $rule"; rm -f "$hdr"; return 1
fi
done
echo "PASS $url"; rm -f "$hdr"
}
check "https://www.api.plus/api/dongman?type=json" \
'^cache-control:.*(s-maxage|max-age)=' \
'^vary:.*accept-encoding'
check "https://www.api.plus/api/get?apikey=YOUR_KEY&ip=8.8.8.8" \
'^cache-control:'
上面断言的是“期望契约”,请按你站点当前的真实响应调整具体规则。断言的对象应该是语义(有没有新鲜期、有没有 Vary),而不是某个精确的秒数——精确值会随业务调整,语义不应该随便变。
七、观测:判断缓存是否真的在工作#
配置上线后,至少盯住这几项:
- 边缘命中率,按路径分组看,整体数字会被少数热点路径带偏。
- 回源 QPS(Queries Per Second,每秒请求数) 与带宽,这是缓存真正的收益指标。
- TTFB(Time To First Byte,首字节时间) 的 p50 / p95,并区分命中与未命中,否则两组数据混在一起看不出问题。
- 304 比例,验证器是否真的被用上。
- 单缓存键请求量 Top N,用来发现参数碎片化。
- stale 命中次数与错误率,判断降级是否被频繁触发。
快速自检的办法是对同一 URL 连发三次,看响应里的缓存状态头(不同 CDN 命名不同,常见如 X-Cache、CF-Cache-Status)是否从 MISS 变成 HIT。如果一直是 MISS,先回去查第三节的缓存键。
八、总结#
HTTP 缓存和 CDN 加速不是“开个开关”,而是把三件事写成可验证的契约:用 Cache-Control 表达复用规则,用缓存键和 Vary 控制命中粒度,用 ETag 与版本化 URL 控制内容更替,最后用脚本把这些语义钉在流水线里。这套做法对人和对 AI 生成的配置同样适用——能被断言的行为,才值得被信任。
想直接拿真实接口练手,可以在 API.PLUS 注册获取免费 API Key,用 IP 查询接口 和 随机动漫壁纸接口 跑一遍本文的响应头检查与命中率观测;更多接入细节见 API 文章中心,遇到问题可走 技术支持。