反向代理缓存是减轻后端压力的常用手段,但缓存配好之后,很多运维人员面临的问题是:缓存到底有没有生效?命中了多少?Apache的mod_cache模块在处理每个请求时会给出明确的缓存状态标记,只要把这些标记记录到日志里,统计命中率就是水到渠成的事。本文围绕Apache的代理缓存命中率统计展开,从缓存状态的含义、日志配置、统计方法到常见误区逐一说明。

一、先弄懂mod_cache的缓存状态标记
Apache的mod_cache模块在决定是否从缓存返回内容时,会通过环境变量cache-status记录本次请求的处理结果。这个状态值是统计命中率的基石,不同的值含义不同,直接影响到统计口径的定义。
常见状态有下面几种:HIT表示缓存直接命中,未回源;MISS表示缓存未命中,请求穿透到后端,返回的内容可能被写入缓存;REVALIDATED表示缓存内容过期但通过条件请求(If-None-Match或If-Modified-Since)向后端校验后仍然有效,后端返回304,这种情况其实也算一种广义的命中;STALE表示缓存已过期,但后端不可达或配置了stale-on-error策略,返回了过期内容;INVALID表示缓存条目被标记无效。
统计命中率时要想清楚口径:狭义命中率只算HIT,广义命中率把HIT和REVALIDATED一起计算。REVALIDATED虽然发生了网络交互,但后端没有传输完整响应体,带宽和后端计算开销都省了下来,所以在多数业务场景中建议把两种都算进命中。否则你可能会看到命中率只有30%,加上REVALIDATED后可能达到60%,评估结果完全不同。
二、配置日志记录缓存状态
默认的combined日志格式里没有缓存状态字段,需要自定义日志格式把%{cache-status}e加进去。前提是mod_cache和mod_cache_disk已经启用,配置示例如下:
LoadModule cache_module modules/mod_cache.so
LoadModule cache_disk_module modules/mod_cache_disk.so
CacheEnable disk /
CacheRoot "/var/cache/httpd/proxy"
CacheDirLevels 2
CacheDirLength 1
CacheMaxFileSize 10000000
# 自定义日志格式,加入缓存状态字段
LogFormat "%h %l %u %t \"%r\" %>s %b %{cache-status}e" cachecustom
CustomLog "logs/access_cache.log" cachecustom
配置完成后重启Apache,用curl请求几次接口,再查看access_cache.log,最后一列会出现HIT、MISS或空白。空白通常表示该请求路径没有被CacheEnable规则覆盖,或者响应头不允许缓存(比如带了Cache-Control: no-store),这部分请求建议单独归类分析,而不是简单忽略。
如果还需要知道缓存条目的存储路径和键名,可以再加上%{CACHE_KEY}e字段,排查某些URL为何总是MISS时非常有用。例如后端每次返回不同的Set-Cookie导致缓存键计算不稳定,从日志里就能看出端倪。
三、三种统计方法
1. 命令行快速统计
日志有了,最直接的方式是用awk计算。统计最近一天的命中率:
#!/bin/bash
LOG=/var/log/httpd/access_cache.log
total=$(awk '$NF!="-"' $LOG | wc -l)
hit=$(awk '$NF=="HIT" || $NF=="REVALIDATED"' $LOG | wc -l)
miss=$(awk '$NF=="MISS"' $LOG | wc -l)
echo "可缓存请求总数: $total"
echo "命中次数: $hit"
echo "未命中次数: $miss"
awk "BEGIN{printf \"命中率: %.2f%%\n\", $hit*100/$total}"
这个脚本简单可靠,适合小规模场景或临时排查。分母用可缓存请求总数而不是全部请求数,是因为静态资源以外的动态接口如果不区分开,命中率会被稀释得没有参考价值。可以按URL维度再拆一层,用awk对请求字段分组统计,找出命中率异常低的接口针对性优化。
2. 定时采集输出监控报表
把统计脚本放进crontab,每小时采集一次并写入数据文件,再配合Grafana或简单的HTML表格展示趋势。命中率的变化曲线比单点数值有价值得多,例如某次发版后命中率从70%跌到20%,往往意味着响应头里的缓存策略被改掉了。采集脚本示例如下:
#!/bin/bash
LOG=/var/log/httpd/access_cache.log
DATA=/var/www/stats/cache_rate.csv
ts=$(date +%Y-%m-%d_%H)
total=$(awk '$NF!="-"' $LOG | wc -l)
hit=$(awk '$NF=="HIT" || $NF=="REVALIDATED"' $LOG | wc -l)
rate=$(awk "BEGIN{printf \"%.2f\", $hit*100/$total}")
echo "$ts,$total,$hit,$rate" >> $DATA
3. 日志聚合平台分析
请求量大、有多台代理节点时,单机脚本就不合适了,可以把日志统一送入ELK体系。Logstash解析日志时把cache-status字段提取出来,Kibana里做Terms聚合,一眼就能看到HIT、MISS、REVALIDATED各占多少,还能按域名、URL、节点多维下钻。Grok模式的核心是解析日志末尾的状态字段,例如%{WORD:cache_status}加适当的分隔符匹配即可。
这种方式的优点是统计口径统一、可回溯历史,缺点是部署成本高。中小流量场景下用前两种方法足够,不必为了统计命中率专门上一套日志平台。
四、统计中容易踩的坑
第一个坑是忽略stale命中的语义。开启了CacheStaleOnError后,后端故障期间大量请求会返回STALE状态,这部分请求用户体验上等同于命中,但如果统计时不单列出来,故障期间的报表会出现奇怪的命中率波动。建议在awk统计时把STALE单独作为一个类别输出。
第二个坑是响应头干扰。后端返回Cache-Control: no-cache时,mod_cache每次都会revalidate,日志里全是REVALIDATED而不是HIT,看起来命中了但实际每笔请求都往后端发条件请求。如果后端内容变化不频繁,改用Cache-Control: max-age配合较长的有效期,才能真正减少回源。
第三个坑是缓存键包含Cookie的问题。mod_cache默认对带Authorization头或某些Cookie的请求处理很保守,容易导致重复MISS。排查时可以在日志中记录%{CACHE_KEY}e,确认同一URL的缓存键是否稳定。另外要注意日志轮转(rotatelogs或logrotate)之后的统计脚本读取路径是否正确,避免统计到空文件得出命中率为零的错误结论。
最后,命中率不是越高越好,要结合内容时效性综合评估。静态资源追求95%以上,动态接口能到50%已经能明显减轻后端压力。把统计做起来,持续观察趋势变化,才能让缓存配置有据可依。