Apache HTTP Server 配合 mod_cache 做反向代理缓存时,缓存 Key 并不是单纯把请求 URL 塞进哈希函数。无论是排查回源次数异常,还是想提高命中率,都需要先弄清楚哪些字段参与了 Key 的计算,以及 Vary 头、会话参数这些隐藏变量在什么时候会把同一个页面拆成多份缓存。

Apache mod_cache 默认缓存Key由哪些元素组成
在默认配置下,mod_cache 会先构造一个规范化的明文串,然后再交给哈希函数生成内部存储键。参与这个明文串的元素至少包括请求方法、完整的绝对 URI 和 Vary 变体标识。很多教程只是笼统地说“缓存 Key 等于 URL”,这会导致开发者在排查时漏掉方法差异和查询参数顺序带来的影响。
以一次反向代理请求为例,客户端访问 http://cache.ipipp.com/docs/index.html?lang=zh-CN,Apache 在计算 Key 时并不是直接使用浏览器的原始字符串。它会把协议、主机名、端口、路径和查询参数拆开,去掉默认可能被代理规范化掉的片段,再重新拼成内部统一形式。下面的伪代码可以表示这个拼接过程:
原始缓存键素材 = 请求方法 + " " + scheme + "://" + host + ":" + port + path + "?" + query 最终缓存键 = 哈希(原始缓存键素材 + 规范化后的 Vary 变体值)
这里最容易被忽略的是查询参数。Apache 默认不会对查询参数做排序,所以 /item?a=1&b=2 和 /item?b=2&a=1 会被当作两个不同的缓存条目,哪怕它们在语义上完全相同。另一方面,Fragment 不会发送到服务器,因此 /page#section 和 /page 不会产生两个 Key。主机名大小写和默认端口是否被省略,也取决于反向代理层是否做了规范化,建议在 ProxyPass 上游保持 Host 一致,避免一套后端出现多个缓存键。
Vary 响应头如何把单个 URL 拆成多个缓存副本
Vary 头是理解缓存 Key 的另一个关键。源站如果在响应中返回 Vary: Accept-Encoding,就表示同一个 URL 可能根据客户端是否支持压缩返回不同内容。mod_cache 不能只存一个压缩版本,否则不支持压缩的客户端会拿到乱码;也不能只存非压缩版本,否则支持压缩的客户端每次都要回源。于是它的做法是在基础 URL Key 后面追加请求头的实际值,形成变体标识。
可以把这个过程理解为:
页面基础键 = 哈希(方法 + 完整URL) 缓存条目键 = 页面基础键 + "|" + Vary头名 + "=" + 对应请求头的值
如果请求带着 Accept-Encoding: gzip, deflate, br,源站返回 Vary: Accept-Encoding,那么 Apache 会记录一个与 gzip 组合相关的变体;下一次请求如果只带 Accept-Encoding: br,就会根据新值匹配另一个变体。这本身是正确的协商机制,但如果源站错误地在 Vary 中加入了 User-Agent,那么不同浏览器、不同版本都会创建独立缓存,命中率会迅速下降。实际运维中,这类过宽的 Vary 比 URL 参数更隐蔽,也更值得优先排查。
如何观察真实缓存Key:打开 X-Cache-Detail
调试缓存问题时,最直接的办法是让 Apache 暴露它实际使用的 Key。mod_cache 提供 CacheDetailHeader 指令,开启后响应头中会出现 X-Cache-Detail,里面包含本次请求是否命中缓存,以及 Apache 采用的缓存键信息。通过对比两次请求的 X-Cache-Detail,很容易确认是不是 Key 不一致。
一个典型配置如下:
<VirtualHost *:80>
ServerName cache.ipipp.com
ProxyPass / http://192.168.1.50/
ProxyPassReverse / http://192.168.1.50/
CacheEnable disk /
CacheRoot /var/cache/apache2/mod_cache_disk
CacheDirLevels 2
CacheDirLength 1
CacheDetailHeader on
</VirtualHost>
保存并重载后,可以用 curl -I http://cache.ipipp.com/docs/index.html 观察响应头。第一次请求通常会看到类似 X-Cache-Detail: cache miss 或带完整 URL 的信息;第二次相同请求则可能变为 cache hit。如果第二次仍未命中,可以对照两次响应中的 X-Cache-Detail 是否完全一致,尤其注意 Host、查询参数顺序和 Vary 值。
除了响应头,磁盘缓存目录结构也能辅助定位。Apache 会根据 CacheDirLevels 和 CacheDirLength 把哈希后的键拆成多级目录,避免单个目录文件过多。虽然这些目录名不是人可读的完整 URL,但它们能证明 Key 确实经过哈希处理,而不是直接拿明文 URL 当文件名。
减少缓存碎片:从Key计算角度优化命中率
如果确认性能瓶颈来自大量“伪不同”的缓存条目,可以先从查询参数入手。很多 Java、PHP 应用会在 URL 上附加 jsessionid、PHPSESSID 这类会话标识。对匿名用户来说,这些参数通常不改变响应内容,但它们会原封不动进入缓存 Key,导致每访问一次都可能产生新条目。mod_cache 的 CacheIgnoreURLSessionIdentifiers 指令可以在计算 Key 前把这些参数剥离。
CacheEnable disk / CacheRoot /var/cache/apache2/mod_cache_disk CacheIgnoreURLSessionIdentifiers jsessionid PHPSESSID
上面的配置会让 /list?jsessionid=123 和 /list?jsessionid=456 共享同一个缓存 Key。需要注意的是,这个指令只是忽略 Key 中的会话参数,不会修改回源请求;如果后端确实依赖这些参数返回不同内容,就需要谨慎处理,不能随便剥离。
另一个优化方向是统一 URL 的书写形式。反代入口可以配置一条 301 规则,把首页请求从 /news/ 重定向到 /news,或统一转成带尾斜杠的版本,避免两个等效地址分别回源。同时,尽量在网关层控制 Accept-Encoding 的取值集合,减少 Vary 变体数量。Apache 的 mod_deflate 通常会正确添加 Vary: Accept-Encoding,只要浏览器发送的 Accept-Encoding 值不要太杂乱,缓存变体数量就是可控的。
总结来说,Apache 代理缓存的 Key 计算并不是一串 URL 那么直观,它由请求方法、规范化后的完整 URI 以及 Vary 变体共同决定。调试时先打开 X-Cache-Detail 观察实际键,再排查参数顺序和 Vary 范围,通常能很快定位到缓存未命中的原因。理清这套拼接逻辑后,优化命中率就不会只停留在延长缓存时间上,而是从 Key 生成的源头减少无效缓存碎片。
Apache代理缓存缓存Key计算mod_cache修改时间:2026-10-02 12:04:52