HTTP 缓存与 CDN 加速实践:让命中率可测量
HTTP 缓存与 CDN 加速的关键不是打开开关,而是用明确的 Cache-Control、收敛的缓存键和可复现的头部验证脚本,把命中率变成可测量、可回归的工程指标。
- 缓存失效多因响应头自相矛盾,而非 CDN 配置错误
- 静态、半静态、私有数据必须用三套不同的缓存头
- 用 curl 连续请求观察 Age 与缓存状态头验证命中
- 缓存键要规范化参数,避免随机 query 打散命中率
HTTP 缓存与 CDN(Content Delivery Network,内容分发网络) 加速能否真正提速,取决于响应头是否明确、缓存键是否收敛、命中率是否可测量,而不取决于是否打开了 CDN 开关。
很多团队在接入 CDN(Content Delivery Network,内容分发网络) 后会发现一个尴尬现象:带宽账单没降,源站 QPS(Queries Per Second,每秒请求数) 也没降多少。问题往往不在 CDN 厂商,而在源站返回的响应头自相矛盾——一边写 Cache-Control: public, max-age=300,一边又带 Set-Cookie,边缘节点只能选择不缓存。
为什么 API 的 HTTP 缓存经常“配了等于没配”?#
缓存不生效,九成是响应头冲突或缓存键被打散。以下是接入 API.PLUS 接口时最常见的四类失真点:
- Set-Cookie 污染:响应里出现
Set-Cookie,多数 CDN(Content Delivery Network,内容分发网络) 默认直接跳过缓存,无论Cache-Control写得多激进。 - Vary 头过宽:写了
Vary: *或把User-Agent、Cookie放进 Vary,缓存键瞬间爆炸,命中率趋近于零。 - 参数随机化:前端每次请求都带
?_t=时间戳或未排序的参数,同一份数据被拆成无数个缓存键。 - 状态码不参与缓存:只给 200 配了 TTL,404 与 5xx 也进入缓存或都不进入缓存,导致错误被放大或抖动被放大。
以 随机动漫壁纸接口 为例,它返回的是图片地址与元数据,天然适合边缘缓存;而 GET 示例接口 返回的 IP 归属信息属于半静态数据,缓存策略就应该完全不同。
如何设计一套能真正命中的 API 缓存响应头?#
同一套服务里,静态、半静态、私有数据必须用三套不同的头,混用是命中率杀手。参考下面的分层策略:
| 数据类型 | Cache-Control | CDN(Content Delivery Network,内容分发网络) 侧行为 | 典型接口 |
|---|---|---|---|
| 静态资源 / 不可变结果 | public, max-age=31536000, immutable | 长 TTL,靠文件名或参数版本更新 | 动漫壁纸图片地址 |
| 半静态数据 | public, max-age=60, s-maxage=300, stale-while-revalidate=60 | 边缘缓存 5 分钟,回源时先返回旧值 | IP 归属查询 |
| 个性化 / 鉴权响应 | private, no-store | 不进共享缓存 | 账户余额、密钥信息 |
| 写操作 | no-store | 永不缓存 | POST 示例接口 |
s-maxage 只对共享缓存生效,浏览器会忽略它。这个特性正好用来让 CDN(Content Delivery Network,内容分发网络) 扛住更多流量,同时保证用户端拿到相对新鲜的数据。设计流程建议按四步走:
- 按“数据变更频率”给每个接口打标签,而不是按业务模块打标签。
- 为每类标签定义唯一的
Cache-Control模板,禁止开发者在业务代码里随手写字符串。 - 在网关层统一剥离
Set-Cookie,确需下发的走独立响应头或前端二次请求。 - 为 4xx 与 5xx 单独配置短 TTL,避免错误页被长时间缓存。
CDN(Content Delivery Network,内容分发网络) 缓存键与 Vary:哪些变量必须参与?#
缓存键应只包含真正影响响应内容的变量,多一个字段就少一截命中率。推荐做法是把查询参数按字母序规范化后再参与键计算。
- 必须参与:路径、影响内容的查询参数(如
type=json)、语言或区域标记(若确实影响内容)。 - 不应参与:请求时间戳、随机数、埋点 ID、无关的追踪参数。
- 谨慎参与:
Origin、Accept,除非接口真的做内容协商。
如果要在 Nginx 侧自己做一层缓存,可以这样写:
示例说明:以下代码仅用于逻辑验证,生产环境需补充异常处理、超时与重试、日志与监控告警及降级策略。
proxy_cache_path /var/cache/nginx/api levels=1:2 keys_zone=api_cache:64m max_size=4g inactive=30m;
server {
listen 443 ssl;
server_name www.api.plus;
location = /api/dongman {
proxy_pass http://127.0.0.1:9000;
proxy_cache api_cache;
proxy_cache_key "$scheme$request_method$host$uri$is_args$args";
proxy_cache_valid 200 10m;
proxy_cache_valid 404 1m;
proxy_cache_use_stale error timeout updating http_500 http_502;
proxy_cache_lock on;
add_header X-Cache-Status $upstream_cache_status always;
add_header Cache-Control "public, max-age=300, stale-while-revalidate=60" always;
}
}
注意 add_header 会覆盖上游同名头,所以这类配置要放在网关层统一管理,避免源站与网关各写一套互相打架。proxy_cache_lock 能在缓存失效瞬间把并发回源合并成一次,对短时高频抓取尤其有效。
怎么验证 CDN 命中率是真实提升而不是错觉?#
验证只需要一个循环脚本,观察 Age 是否递增、缓存状态头是否从 MISS 转 HIT:
#!/usr/bin/env bash
URL="https://www.api.plus/api/dongman?type=json"
for i in 1 2 3 4 5; do
echo "--- 第 $i 次请求 ---"
curl -sS -o /dev/null -D - "$URL" -H 'Accept-Encoding: gzip' \
| grep -Ei '^(HTTP/|cache-control:|age:|cf-cache-status:|x-cache-status:|vary:)'
sleep 2
done
预期结果是:第一次 MISS 且 Age 缺失或为 0,后续请求出现 HIT 且 Age 逐步增长。若五次全是 MISS,按下面的顺序排查:
- 检查响应里是否有
Set-Cookie或Vary: *。 - 确认 CDN 是否对该内容类型启用了缓存规则,部分平台默认不缓存 JSON。
- 核对缓存键,把请求 URL 的完整 query 打出来比对。
- 查看 CDN 的缓存命中率面板,与源站访问日志的请求数交叉验证。
缓存层同时是一道廉价的流量缓冲。Cloudflare 在 2026 年第三季度 DDoS 威胁报告 中指出,攻击正转向更短、更高频的脉冲式打击。这类流量如果全部穿透到 PHP-FPM,源站几乎必然被打满;而把可缓存的读接口挡在边缘,能显著降低穿透比例。
如何用 AI Agent 流程守住缓存契约不回归?#
缓存配置最容易在迭代中悄悄退化:某次改动加了一个 Set-Cookie,命中率就从 80% 掉到 5%。可以把缓存契约当成接口契约的一部分,交给 Agent 辅助维护:
- 先定契约:把每个接口应有的
Cache-Control、Vary、状态码 TTL 写成一份 YAML 或表格,作为唯一事实来源。 - 再让 Agent 生成断言:把契约和路由文件一起交给 Agent,要求生成只断言响应头、不断言响应体的测试用例,避免测试脆弱。
- 人工只审 diff:重点看 Agent 是否新增了未在契约中登记的缓存行为,而不是逐行读生成代码。
- 进 CI:把断言挂到每次发布前的集成测试里,失败即阻断,缓存回归就变成可拦截的问题。
这套流程的价值在于把“我知道应该加缓存头”变成“机器每次都会检查缓存头”。Agent 负责体力活,人负责判断哪些数据可以被共享缓存。
总结#
HTTP 缓存与 CDN 加速的落地顺序应该是:先按数据变更频率分类,再统一响应头模板,然后收敛缓存键,最后用脚本和 CI 把命中率固定下来。顺序颠倒,就只能在面板上看到一个好看但无意义的数字。
动手验证时,可以直接用 GET 示例接口 与 随机动漫壁纸接口 跑上面的脚本,观察 Age 与缓存状态头的变化;更多接入细节可在 API 文章中心 查阅,遇到问题可通过 技术支持 反馈。在 API.PLUS(沐小悠API)注册即可获取免费 API Key,用真实请求把缓存策略跑通,比读十篇配置文档更有效。