在 Apache HTTP Server 中,mod_cache 负责将后端响应缓存到磁盘或内存。默认情况下,缓存键由完整的请求 URL 生成,包括 scheme、host、port、path 以及 query string。也就是说,/assets/logo.png?t=123 和 /assets/logo.png?t=456 虽然指向同一个物理文件,但在缓存层被视为完全不同的两个资源。这种设计是为了保证不同查询参数可能对应不同内容时的正确性,但如果你只是用查询参数做版本号或广告追踪,就会产生大量内容完全相同的缓存副本。

这些副本不仅占用磁盘空间,还会降低缓存命中率,因为每个新的参数值都需要向后端重新请求一次。对于访问量较大的静态资源站点,这种浪费会非常明显。要改变这一行为,Apache 提供了 CacheIgnoreQueryString 指令,允许缓存键忽略 query string。下面从实际配置和边界条件两个角度展开。
查询字符串如何影响缓存键与命中率
mod_cache 在判断一个请求是否命中缓存时,会先根据请求 URL 计算缓存键。这个缓存键通常包含协议、主机名、端口、路径以及完整的查询字符串。只要查询字符串中任意一个字符发生变化,缓存键就会不同。比如 /images/banner.png?from=home 和 /images/banner.png?from=email 会被当成两个独立请求对待,即使后端返回的图片内容完全一致。
在实际网站运营中,查询字符串经常被用来做前端缓存刷新、广告渠道追踪或者用户会话标识。例如同一个 CSS 文件可能被加上 ?v=1.0.1、?v=1.0.2,或者带上 ?utm_source=wechat、?utm_source=weibo。这些参数本身不改变资源内容,却会让 Apache 为每个不同的参数组合都创建一份缓存副本。如果参数组合数量很大,磁盘空间会被迅速消耗,同时缓存命中率也会明显下降。
默认情况下 Apache 不会忽略查询字符串,这是出于安全考虑。因为服务器无法判断某个查询参数是否会影响响应内容,贸然合并不同请求的缓存可能会返回错误数据。因此,开启忽略查询字符串应当是管理员在明确了解业务特征后的主动配置,而不是全局默认行为。
CacheIgnoreQueryString 的配置与作用范围
CacheIgnoreQueryString 是 mod_cache 模块提供的一个开关指令,语法为 CacheIgnoreQueryString On|Off,默认值为 Off。它可以出现在服务器配置、虚拟主机、目录上下文以及 .htaccess 文件中。通常建议在虚拟主机级别或者针对静态资源目录进行配置,这样既能覆盖主要场景,又不会误伤动态内容。
下面是一个典型的配置示例。在启用 mod_cache 和 mod_cache_disk 之后,将 CacheIgnoreQueryString 设置为 On:
<IfModule mod_cache.c>
CacheEnable disk /
CacheRoot /var/cache/apache2/mod_cache_disk
CacheIgnoreQueryString On
</IfModule>
如果只想对静态资源目录启用,可以使用 Directory 或 Location 指令限定范围。例如只对 /assets/ 路径开启忽略查询字符串:
<Location /assets/>
CacheEnable disk
CacheRoot /var/cache/apache2/mod_cache_disk
CacheIgnoreQueryString On
</Location>
配置生效后,缓存键将只由请求的 scheme、主机、端口和路径构成,查询字符串不再参与。也就是说,/assets/logo.png?t=111 和 /assets/logo.png?t=222 会命中同一个缓存条目。不过需要特别注意的是,如果响应中包含 Vary 头,Apache 仍会根据 Vary 头中的内容进一步细分缓存。也就是说,忽略查询字符串并不会让 Vary 机制失效,两者是并行工作的。
忽略查询字符串的风险与 Vary 交互
开启 CacheIgnoreQueryString On 后,最大的风险在于内容依赖查询参数的场景被错误合并。例如一个搜索接口 /search?q=apple 和 /search?q=banana 通常会返回不同的结果,如果这个路径被纳入忽略查询字符串的缓存范围,那么第一个请求的结果会被缓存,后续所有不同的 q 值都会复用同一份响应,导致用户看到的搜索结果完全错误。这种缓存污染还可能持续影响后续所有访问者,直到缓存过期或被手动清除。
另一个常见误区是认为忽略查询字符串会影响登录状态或个性化内容。实际上,如果应用正确使用 Vary: Cookie 或 Vary: Authorization 响应头,Apache 仍然会为不同的 Cookie 或认证信息维护独立缓存副本。查询字符串被忽略后,缓存键少了 qs 这一维度,但 Vary 头仍然可以区分不同用户状态。因此,在评估是否可以开启该指令时,除了确认内容不依赖查询参数,还要检查源站返回的 Vary 头是否覆盖了所有必要维度。
从实践角度看,CacheIgnoreQueryString On 最适合用于图片、CSS、JavaScript、字体等静态资源,以及那些明确无状态且参数不影响响应体的 API。对于动态页面、搜索、表单回显、支付回调等场景,应当保持默认关闭,或者将缓存策略拆分成不同目录,仅对静态资源开启该功能。
验证方法与替代方案
修改配置后,需要用实际请求验证缓存是否按照预期工作。可以使用 curl 命令发送两个只有查询字符串不同的请求,并观察响应头中的 Age。第一次请求通常由源站生成,Age 可能为 0 或不存在;第二次请求如果命中缓存,Age 会大于 0。示例命令如下:
curl -I "http://www.ippipp.com/assets/logo.png?t=111" curl -I "http://www.ippipp.com/assets/logo.png?t=222"
如果两次请求的 Age 都大于 0,并且第二次请求明显更快,说明查询字符串已经被忽略,缓存命中成功。也可以结合自定义日志格式记录缓存效果。下面的 Apache 日志配置会在访问日志中输出 Age 响应头,方便后续统计命中情况:
LogFormat "%h %l %u %t "%r" %>s %b %{Age}o" cache_common
CustomLog /var/log/apache2/cache-access.log cache_common
如果业务上只需要忽略部分查询参数,例如去掉 utm_source、utm_campaign 等跟踪参数,但又想保留 page、id 等会影响内容的参数,那么单独使用 CacheIgnoreQueryString 就不合适了。这时可以考虑在缓存之前增加一层规范化处理,例如通过 mod_rewrite 将无用的查询参数删除或重定向到规范 URL,再交给 mod_cache 处理。这种方式虽然配置更复杂,但能够在避免错误缓存的同时保持较高的命中率。对于拥有多个服务节点的架构,也可以将查询字符串规范化逻辑放在反向代理或 CDN 层,统一控制缓存键的生成规则。
总之,CacheIgnoreQueryString 是一项简单有效的缓存优化手段,但必须谨慎限定作用范围。只有在确认查询参数不会影响响应内容的场景下开启,才能真正提升命中率而不带来数据错误。