Apache的mod_cache与mod_proxy组合能够显著降低源站压力,但缓存命中率波动、磁盘缓存目录膨胀、过期内容未及时清理等问题,通常要等到源站负载异常时才会被发现。把这些状态放进一个运维Dashboard,才能持续观察缓存的真实表现。

一、缓存数据采集:命中率与状态头从哪来
Apache的缓存逻辑并不在mod_proxy内部完成,而是由mod_cache及其磁盘后端mod_cache_disk协同处理。请求进入Apache后,mod_proxy负责转发到源站,mod_cache则在转发前后检查缓存键。如果缓存命中,直接返回本地副本;如果没有命中,请求继续发往源站,响应返回后再根据规则写入缓存目录。因此要判断一次请求到底命中还是未命中,必须依赖mod_cache输出的状态信息。
在Apache配置中开启CacheDetailHeader后,响应头中会多出一个X-Cache-Detail字段,常见取值包括hit、miss、stale、updating等。这个字段是日志采集的基础。配置示例中同时开启了CacheHeader和CacheLock,前者输出简洁的X-Cache头,后者用来防止缓存击穿时大量请求同时回源。
LoadModule cache_module modules/mod_cache.so
LoadModule cache_disk_module modules/mod_cache_disk.so
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so
<IfModule mod_cache.c>
CacheRoot "/var/cache/apache2/mod_cache_disk"
CacheEnable disk /
CacheDirLevels 2
CacheDirLength 1
CacheMaxFileSize 1000000
CacheMinFileSize 1
CacheIgnoreCacheControl On
CacheIgnoreNoLastMod On
CacheDetailHeader On
CacheHeader On
CacheLock on
CacheLockMaxAge 5
</IfModule>
接着要把X-Cache-Detail写入访问日志。日志格式中加入%{X-Cache-Detail}o即可,这样每一行日志都会带上缓存状态。同时建议记录%D响应耗时,便于后续分析命中与未命中请求在延迟上的差异。
LogFormat "%h %l %u %t \"%r\" %>s %b %{X-Cache-Detail}o %D" cache_log
CustomLog logs/cache_access.log cache_log
有了日志文件,就可以用脚本做聚合统计。下面这段Python会读取每行日志,按状态字段分类汇总,并分别计算请求命中率和字节命中率。字节命中率更容易反映缓存对带宽的节省效果,因为大文件命中带来的收益远高于小文件。
import re
log_pattern = re.compile(
r'^(\S+) \S+ \S+ \[[^\]]+\] "(?:[^"]*)" (\d{3}) (\S+) (\S+) (\d+)$'
)
stats = {
"hit": 0,
"miss": 0,
"stale": 0,
"updating": 0,
"bytes_hit": 0,
"bytes_total": 0,
}
with open("/var/log/apache2/cache_access.log", "r", encoding="utf-8") as f:
for line in f:
m = log_pattern.match(line)
if not m:
continue
status = m.group(3)
body_bytes = int(m.group(4) or 0)
stats["bytes_total"] += body_bytes
if status in stats:
stats[status] += 1
if status == "hit":
stats["bytes_hit"] += body_bytes
total = stats["hit"] + stats["miss"] + stats["stale"] + stats["updating"]
if total:
hit_ratio = stats["hit"] / total
byte_ratio = stats["bytes_hit"] / stats["bytes_total"] if stats["bytes_total"] else 0
print(f"request_hit_ratio={hit_ratio:.4f}")
print(f"byte_hit_ratio={byte_ratio:.4f}")
二、Dashboard指标模型与轻量可视化方案
一个实用的缓存运维Dashboard至少应该包含五类指标:请求命中率、字节命中率、缓存目录占用、回源请求趋势、Top未命中URL。请求命中率展示缓存效果,字节命中率体现带宽节省,目录占用帮助判断磁盘是否接近水位线,回源趋势能反映缓存穿透风险,Top未命中URL则帮助发现哪些资源长期无法缓存。
采集层如果已经使用Prometheus,可以直接让脚本把指标写入textfile collector目录。Prometheus会定时读取.prom文件,Grafana再通过PromQL绘制趋势图。下面是一个典型的指标文件内容,脚本每5分钟覆盖一次即可。
# HELP apache_cache_request_hit_ratio Apache cache request hit ratio # TYPE apache_cache_request_hit_ratio gauge apache_cache_request_hit_ratio 0.82 apache_cache_byte_hit_ratio 0.67 apache_cache_dir_size_bytes 1288490188 apache_cache_total_requests 294832
如果不想引入Prometheus,也可以让脚本生成一个JSON文件,再由一个极简的HTML页面通过fetch拉取后交给ECharts渲染。JSON结构可以设计为包含时间戳、命中率、字节命中率、目录大小和Top未命中URL数组。这种方式的好处是部署简单,只需要一个静态站点,缺点是历史数据需要自己存留,不利于长期查询。
无论选择哪种展示方案,都要注意命中率的分母。如果统计所有请求,动态接口、带Cookie的请求、明确不可缓存的状态码都会拉低命中率,显得缓存效果很差。更合理的做法是只统计可缓存响应的请求,或者在Dashboard上同时展示全量命中率与可缓存命中率两条曲线。
三、缓存目录维护与命中率调优
Apache的mod_cache_disk会把缓存文件按CacheDirLevels和CacheDirLength生成多级子目录,避免单个目录文件数过多。但长期运行后,过期条目、Vary头产生的多条副本、被中断的写入文件仍会堆积。htcacheclean是Apache自带的清理工具,支持按固定间隔、目录总大小或文件数进行清理。
htcacheclean -d 120 -p /var/cache/apache2/mod_cache_disk -l 2G -n
上面命令表示每120秒检查一次缓存目录,当总大小超过2G时开始清理,并保留目录结构。实际生产环境建议把清理任务交给cron,避免长时间运行后出现inode耗尽。同时Dashboard上要持续观察目录大小变化,如果清理后仍然持续增长,说明缓存写入速度超过清理能力,需要调低CacheMaxFileSize或缩小CacheEnable的作用范围。
命中率长期偏低时,首先要检查是否缓存了不应缓存的内容。例如/api/这类动态接口默认返回Set-Cookie或Cache-Control: no-cache,mod_cache通常不会缓存它们,但某些配置会错误地把这些请求也纳入缓存区。可以通过Location块显式关闭缓存,减少无效写入。
<Location "/api/">
CacheEnable off
</Location>
Vary头也是影响命中率的关键因素。如果源站对同一个静态资源同时返回Vary: User-Agent,那么不同浏览器会生成不同缓存副本,命中率被严重稀释。对不依赖User-Agent的静态资源,可以在反向代理层剥离或统一该请求头,让缓存键更加稳定。另外,CacheIgnoreCacheControl开启后虽然能强制缓存,但也可能缓存了源站明确不希望缓存的内容,需要结合业务谨慎使用。
四、告警与自动化运维脚本
Dashboard的价值不只是事后查看,更在于异常时主动通知。当请求命中率低于60%、缓存目录占用超过80%、回源请求数突然升高时,应该触发告警。如果使用Prometheus,可以直接写alert rules;如果采用脚本方案,可以在采集逻辑中加入阈值判断并调用Webhook。
下面是一个完整的Python采集脚本框架,它读取Apache缓存日志,计算命中率与字节命中率,并将结果写入Prometheus采集目录。脚本可以放入crontab每5分钟执行一次,同时检查缓存目录大小是否超过预设阈值。
import os
import re
import shutil
from datetime import datetime
LOG_PATH = "/var/log/apache2/cache_access.log"
PROM_DIR = "/var/lib/prometheus/textfile_collector"
CACHE_DIR = "/var/cache/apache2/mod_cache_disk"
ALERT_WEBHOOK = "https://ipipp.com/webhook"
SIZE_THRESHOLD = 0.8
MAX_CACHE_DIR_SIZE = 3 * 1024 * 1024 * 1024
log_pattern = re.compile(
r'^(\S+) \S+ \S+ \[[^\]]+\] "(?:[^"]*)" (\d{3}) (\S+) (\S+) (\d+)$'
)
stats = {
"hit": 0,
"miss": 0,
"stale": 0,
"updating": 0,
"bytes_hit": 0,
"bytes_total": 0,
}
with open(LOG_PATH, "r", encoding="utf-8") as f:
for line in f:
m = log_pattern.match(line)
if not m:
continue
status = m.group(3)
body_bytes = int(m.group(4) or 0)
stats["bytes_total"] += body_bytes
if status in stats:
stats[status] += 1
if status == "hit":
stats["bytes_hit"] += body_bytes
total = stats["hit"] + stats["miss"] + stats["stale"] + stats["updating"]
request_hit_ratio = stats["hit"] / total if total else 0.0
byte_hit_ratio = stats["bytes_hit"] / stats["bytes_total"] if stats["bytes_total"] else 0.0
total_disk, used_disk, free_disk = shutil.disk_usage(CACHE_DIR)
cache_usage_ratio = used_disk / total_disk
prom_content = f"""# HELP apache_cache_request_hit_ratio Apache cache request hit ratio
# TYPE apache_cache_request_hit_ratio gauge
apache_cache_request_hit_ratio {request_hit_ratio:.4f}
apache_cache_byte_hit_ratio {byte_hit_ratio:.4f}
apache_cache_dir_usage_ratio {cache_usage_ratio:.4f}
apache_cache_total_requests {total}
"""
with open(os.path.join(PROM_DIR, "apache_cache.prom"), "w", encoding="utf-8") as f:
f.write(prom_content)
if cache_usage_ratio > SIZE_THRESHOLD:
os.system(f"curl -X POST -d 'ratio={cache_usage_ratio}' {ALERT_WEBHOOK}")
在实际运维中,这套脚本可以持续运行,产生历史趋势数据。当缓存命中率出现非预期下降时,可以结合Apache错误日志和源站响应头判断原因。某些情况下,一次后端发布改变了Cache-Control策略,就可能导致先前缓存的内容被大量重新验证。通过Dashboard观察命中率骤降的时间点,能够更快定位发布与缓存策略之间的关系。
Apache代理缓存的运维Dashboard本质上是一套组合方案:配置层输出缓存状态,采集层汇总为量化指标,展示层呈现趋势与告警。只用htcacheclean或偶尔看几行日志,很难发现缓慢的缓存退化。把命中率、字节命中率、目录占用的变化串起来看,才能在源站压力升高之前做出调整。
Apache代理缓存运维Dashboardmod_cache修改时间:2026-09-21 10:02:45