Apache 作为反向代理使用时,mod_cache 配合 mod_cache_disk 可以把后端返回的响应保存到磁盘上,下一次同样的请求直接从缓存命中,不再回源。但缓存磁盘不是无限大的,总有一个时刻需要删掉一部分旧缓存腾地方。有人观察缓存目录里的文件变化,发现有些文件留了很久,有些很快就消失了,于是产生一个疑问:Apache 的缓存淘汰是不是随机挑选的?答案是否定的。Apache 的磁盘缓存清理遵循的是明确的最近最少使用原则,也就是通常所说的 LRU 策略,只不过这个清理动作并不是在请求处理过程中实时完成的,而是由一个独立的清理工具 htcacheclean 来承担,这正是很多人产生误解的根源。

mod_cache 的工作流程与缓存目录结构
要理解淘汰策略,得先弄清楚缓存是怎么写进去的。当请求到达 Apache,mod_cache 会根据请求的 URI 计算出一个缓存键,然后检查磁盘上是否存在对应的缓存对象。如果命中且未过期,直接把缓存内容返回给客户端,整个请求不会到达后端。如果缓存不存在或者已经过期,请求会转发给后端,拿到响应后,mod_cache 根据响应头中的 Cache-Control、Expires 等指令判断这个响应是否可缓存,可缓存的话就交由 mod_cache_disk 写入磁盘。
mod_cache_disk 在磁盘上的存储并不是把所有文件堆在一个目录里,而是采用了三级哈希目录结构。默认配置下,CacheDirLevels 为 3,CacheDirLength 为 2,也就是说每个缓存文件会被放在类似 /cache/a/b/c/xxxx 这样的路径下,其中 a、b、c 是根据缓存键逐段计算出来的哈希字符。这种设计是为了避免单个目录下文件数量过多导致文件系统性能下降,和淘汰策略本身没有关系,但了解这个结构对后面配置清理工具很有帮助。
另外需要注意,一个缓存对象在磁盘上通常对应两个文件:一个是响应头文件,一个是响应体文件。这就是为什么在缓存目录里看到的文件数量往往是缓存对象数的两倍,也正因为如此,htcacheclean 在统计磁盘占用时是按一对文件合并计算的。
htcacheclean 的清理机制:LRU 而非随机
Apache 处理请求的进程本身不会主动删除旧缓存,写入新缓存时也不检查磁盘总量是否超限。清理工作完全交给 htcacheclean 这个独立的可执行程序。它的基本用法如下:
# 限制缓存目录总大小为 512M,前台运行 htcacheclean -d120 -p/usr/local/apache2/cache -l512M # 后台守护进程方式,每 30 分钟检查一次 htcacheclean -n -t -p/usr/local/apache2/cache -l1G -i
htcacheclean 在每次运行时会扫描整个缓存目录,统计所有缓存对象占用的空间。如果总量超过了设定的上限,它并不是随机挑文件删除,而是按照 LRU 原则,优先删除最长时间没有被访问过的缓存对象。判断依据是缓存文件的访问时间戳,每当缓存被命中,mod_cache_disk 都会更新对应文件的 atime,htcacheclean 就根据这个信息把对象按新旧程度排序,从最旧的开始清理,直到总占用降到限制值以内。
这个设计很符合代理缓存的业务特点:经常被请求的内容会被反复访问,时间戳持续更新,自然不会被清理;而那些无人问津的旧缓存会在容量紧张时最先被淘汰。如果换成随机策略,热门内容可能被误删,命中率会明显波动,而 LRU 能让有限的磁盘空间尽量留给高价值内容。
htcacheclean 还提供几个实用参数。-t 参数把删除操作交给一个独立线程执行,减少对扫描的阻塞;-n 参数表示温和模式,尽可能降低对系统资源的占用,适合在生产环境长时间运行;-p 指定缓存根目录,必须与 Apache 配置中的 CacheRoot 保持一致;-l 指定总容量上限,可以用 K、M、G 这样的单位。-i 参数配合 -d 使用时,只有在缓存目录内容发生变化后才会执行清理,避免做无意义的重复扫描。
在 Apache 配置中集成自动清理
从 2.4 版本开始,Apache 支持通过配置指令直接托管 htcacheclean,不需要再手动配置 systemd 服务或 crontab。相关的两个指令是 PidFile 同级的 CacheStaleOn,准确说是下面这几个:
# 在 httpd.conf 或对应虚拟主机配置中启用 CacheRoot "/usr/local/apache2/cache" CacheDirLevels 3 CacheDirLength 2 CacheMaxFileSize 1000000 CacheMinFileSize 1 # 启用内置的缓存清理守护进程 CacheCleaner On CacheCleanerInterval 600
CacheCleaner 设为 On 后,Apache 会在主进程启动时拉起清理线程,CacheCleanerInterval 指定两次清理之间的间隔秒数,上面的例子是每 10 分钟执行一次检查。这个方案的优点是清理进程和 Apache 的生命周期绑定,不会出现缓存目录路径配置不一致的问题,运维上也省去了单独维护清理任务的负担。
如果使用的是较老的版本,没有 CacheCleaner 指令,则建议用系统服务的方式运行 htcacheclean。写一个简单的 systemd 单元文件,ExecStart 指向 htcacheclean 并带上守护参数即可。无论哪种方式,清理频率的设置要权衡两点:间隔太短会增加磁盘扫描开销,间隔太长则可能导致缓存目录在两次清理之间超出预期容量。一般来说,把间隔设置在 10 到 30 分钟,容量上限设置为磁盘实际可用空间的七到八成,是比较稳妥的起点。
LRU 与其他淘汰策略的对比及调优建议
常见的缓存淘汰策略除了 LRU,还有 LFU(最不经常使用)和随机淘汰。LFU 按访问频率排序,理论上更精准,但它需要维护每个对象的访问计数,实现复杂,而且对突发流量不敏感,历史上的高频内容可能长期占据空间。随机淘汰实现最简单,分布式的 Redis 集群在某些场景就用它,但在单机磁盘缓存这种场景下,命中率表现不如 LRU 稳定。Apache 选择 LRU 是在实现成本和效果之间的平衡,对于绝大多数反向代理场景已经足够。
实际调优时还有几个值得关注的点。第一,CacheMaxFileSize 和 CacheMinFileSize 用来控制单个缓存对象的体积范围,过大的文件会挤占空间又难以在过期前命中,建议根据业务内容类型设置合理的上限。第二,如果缓存目录所在磁盘的 atime 挂载选项被关闭(比如使用了 noatime),LRU 判断会失效,htcacheclean 就无法正确识别哪些对象最近被访问过,此时需要确认挂载参数或者改用 relatime。第三,监控缓存命中率可以通过 mod_cache 的日志功能实现,在 LogFormat 中加入 %{cache-status}e 变量,就能区分每个请求是命中缓存还是回源,有了这个数据才能评估清理策略是否合理。
总结一下,Apache 代理缓存的淘汰不是随机行为,而是由 htcacheclean 按照 LRU 原则执行的有序清理。理解了写入结构、清理工具和容量参数三者的配合关系,再结合业务流量特征调整清理间隔和容量上限,就能让磁盘缓存稳定地工作在高命中率状态,同时避免缓存目录无限膨胀的问题。