Apache HTTP Server 作为反向代理时,通常会启用 mod_cache 模块来缓存源站响应。缓存去重并不是一个新概念,但在配置不当时,同一份资源可能因为不同的查询参数、不同的请求头甚至大小写差异而产生多份缓存副本。这不仅浪费磁盘空间,还会降低缓存命中率。去重的核心思路是让缓存键足够规范化,同时利用 HTTP 验证器避免重复拉取相同实体。

要理解 Apache 缓存去重,必须先了解 mod_cache 的存储模型。mod_cache 由 mod_cache_disk 或 mod_cache_socache 提供后端存储,每个缓存对象由一个缓存键唯一标识。缓存键由请求 URL、Vary 头和缓存策略共同决定。如果两个请求生成的缓存键一致,Apache 会认为它们是同一个缓存项,从而命中已有的缓存副本。去重工作的第一步就是控制缓存键的生成规则。
缓存键规范化:从源头上合并重复请求
缓存键的生成看似简单,实际上包含很多细节。默认情况下,Apache 会使用完整的请求 URL 作为缓存键的一部分,包括查询字符串。这意味着 https://ipipp.com/page?id=1 和 https://ipipp.com/page?id=1&sid=abc 会被视为两个完全不同的缓存项。如果 sid 只是会话标识,这种差异就会导致大量重复缓存。
Apache 提供了 CacheKeyBaseURL 指令来修正反向代理场景下的缓存键。该指令允许指定一个基础 URL,Apache 会用它来替换实际请求中的主机名和协议部分。例如后端服务器通过内部 IP 访问,但外部请求使用不同的域名时,CacheKeyBaseURL 可以统一缓存键。另一个重要的指令是 CacheIgnoreURLSessionIdentifiers,它可以忽略 URL 中的会话标识参数,比如 jsessionid、phpsessid、sid 等。配置后,带有不同会话标识的请求会生成相同的缓存键,从而复用同一个缓存条目。
下面是一个典型的反向代理缓存配置,展示了如何规范化缓存键:
<IfModule mod_cache.c>
CacheRoot /var/cache/apache2/
CacheEnable disk /
CacheDirLevels 2
CacheDirLength 1
CacheIgnoreURLSessionIdentifiers jsessionid phpsessid sid
CacheKeyBaseURL http://origin.ipipp.com
CacheDefaultExpire 3600
CacheMaxExpire 86400
CacheIgnoreHeaders Set-Cookie
</IfModule>
这里 CacheIgnoreURLSessionIdentifiers 后面的参数是要忽略的查询参数名称,多个参数用空格分隔。需要注意的是,该指令只忽略指定参数的值,不会删除参数本身,因此缓存键计算时这些参数会被剔除。CacheKeyBaseURL 则确保无论客户端请求使用 HTTP 还是 HTTPS,缓存键都保持一致。如果后端响应包含 Set-Cookie 头,使用 CacheIgnoreHeaders 忽略它可以避免因为 Cookie 值不同而生成多个缓存变体。
内容去重:ETag 与 Last-Modified 的条件请求
缓存键去重解决的是入口层面的重复,而内容去重解决的是缓存过期后如何避免重新下载相同实体。HTTP 协议提供了 ETag 和 Last-Modified 两个验证器。当源站响应包含 ETag 时,Apache 会将其与缓存对象一起存储。缓存过期后,Apache 不会直接拉取完整响应,而是向源站发送条件请求,请求头中带上 If-None-Match 和 If-Modified-Since。如果源站返回 304 Not Modified,Apache 就会把本地缓存标记为新鲜,继续使用已有副本。
这个机制对去重非常重要,因为很多情况下内容本身没有变化,只是缓存时间到了。没有条件请求的话,Apache 每次都会重新下载完整资源,造成带宽浪费。通过 ETag 和 Last-Modified 的协同,即使缓存过期,也能避免重复传输相同内容。要让这个过程正常工作,需要确保后端源站正确生成 ETag 或 Last-Modified 响应头。Apache 自身的 mod_headers 模块可以辅助处理,例如在后端没有生成 ETag 时手动添加一个基于文件修改时间的 ETag。
下面是一个反向代理缓存与 ETag 配合的配置示例:
<VirtualHost *:80>
ServerName proxy.ipipp.com
ProxyPass / http://origin.ipipp.com/
ProxyPassReverse / http://origin.ipipp.com/
CacheEnable disk /
CacheRoot /var/cache/apache2
CacheIgnoreNoLastMod On
CacheHeader on
CacheDetailHeader on
</VirtualHost>
其中 CacheIgnoreNoLastMod 设置为 On 时,如果源站响应没有 Last-Modified 头,Apache 也不会拒绝缓存该响应。CacheHeader 用于在响应中加入 X-Cache 头,方便调试缓存命中情况。CacheDetailHeader 则可以输出更详细的缓存状态。这些指令组合起来,可以让你清楚地观察条件请求是否按预期工作。
去重与 Vary 头的协作:避免误合并与漏合并
Vary 头是 HTTP 缓存中容易忽略但非常关键的机制。当源站响应带有 Vary: Accept-Encoding 时,意味着同一个 URL 针对不同的 Accept-Encoding 请求头会返回不同的内容,例如 gzip 压缩版和未压缩版。Apache 在生成缓存键时会把 Vary 头以及对应的请求头值纳入计算,这样就不会把压缩版与未压缩版合并。但如果 Vary 头使用不当,也会导致缓存条目膨胀。例如源站返回 Vary: User-Agent,那么每种不同的浏览器 UA 都会生成一个独立缓存副本,即使内容完全相同。
去重工作需要权衡:既要保证不同变体不会被错误合并,又要防止 Vary 头导致过多无用副本。Apache 的 CacheIgnoreHeaders 指令可以忽略某些响应头,使其不参与 Vary 的计算。例如后端返回 Vary: Accept-Language 但你的服务只提供单一语言,可以把它加入忽略列表。另一个方式是在源站层面修改 Vary 头,只保留真正影响内容的请求头。实际排查时,可以通过查看缓存目录中的 header 文件来确认每个缓存项对应的 Vary 信息。
下面是一个结合 Vary 处理的缓存配置片段:
<IfModule mod_cache.c>
CacheEnable disk /
CacheRoot /var/cache/apache2
CacheIgnoreHeaders Accept-Language
CacheLock on
CacheLockMaxAge 5
CacheLockPath /tmp/mod_cache-lock
</IfModule>
这里 CacheIgnoreHeaders Accept-Language 表示在计算缓存键时忽略 Accept-Language 响应头的影响,避免因为语言偏好产生多个相同内容的缓存。如果源站仍然返回 Vary: Accept-Language,该指令会覆盖这个行为。CacheLock 用于解决缓存击穿问题,当多个并发请求同时发现缓存过期时,只允许一个请求去源站获取新内容,其他请求等待锁释放后复用同一个缓存结果。这个机制也能间接帮助去重,防止瞬时重复缓存。
实战避坑:会话标识、规范化与缓存锁
在真实项目中,缓存去重最大的坑往往来自 URL 中的会话标识。很多应用框架会在 URL 后面追加 jsessionid 或 phpsessid,这些参数对内容没有影响,但会导致缓存目录中产生大量重复条目。解决方法是使用 CacheIgnoreURLSessionIdentifiers,但要注意参数名称必须完全匹配,并且该指令只支持出现在 URL 查询字符串中的参数。如果应用把会话标识放在路径中,例如 /page/;jsessionid=abc,则需要使用 CacheKeyBaseURL 配合 CacheIgnoreURLSessionIdentifiers 之外的方案,比如在代理层用 mod_rewrite 先规范化 URL。
另一个常见问题是缓存键的大小写敏感性。默认情况下,Apache 对 URL 路径的大小写是敏感的,/Page 和 /page 会生成两个缓存键。如果源站对大小写不敏感,就会产生重复。可以通过 mod_rewrite 将所有请求重写为小写路径,再进入缓存逻辑。此外,还需要注意 CacheQuickHandler 的默认值。该指令默认开启,会尽早处理缓存命中,但如果请求经过重写或认证模块,建议将其关闭,否则可能出现缓存键与实际处理路径不一致的问题。
最后提一下缓存锁的配置细节。CacheLock 默认关闭,开启后需要设置 CacheLockPath 指定锁文件存放目录,并确保 Apache 进程对该目录有写权限。锁的过期时间通过 CacheLockMaxAge 控制,单位是秒。如果源站响应时间较长,可以适当调大该值。但锁时间过长会导致其他请求长时间等待,影响响应速度。合理配置缓存锁可以在缓存过期瞬间合并并发请求,有效减少重复缓存写入。
总结来看,Apache 代理缓存的去重并不是单一的开关,而是由缓存键规范化、验证器协同、Vary 头管理和并发控制共同组成的策略。配置时需要从请求入口到缓存存储全链路检查,才能达到理想的去重效果。

