反向代理上线后,第一次访问某个资源往往要穿透到后端,响应慢且增加源站负载。Apache的mod_cache可以把响应缓存到磁盘,但重启或缓存过期后命中率会急剧下降。一个主动的定时任务可以在低峰期把这些热点URL重新拉取一遍,让缓存始终“热”着。

一、缓存预热的基本原理与适用场景
Apache反向代理通常结合mod_proxy和mod_cache工作。mod_cache提供缓存编排,mod_cache_disk负责磁盘存储,缓存键由请求方法、Host头、规范化后的URL以及Vary响应头共同决定。当请求命中缓存时,Apache直接返回本地副本,不再回源。缓存条目过期或被清除后,下一次请求会回源并重新填充。如果这些回源操作集中在业务高峰,用户会感知到明显延迟。
预热的思路是在业务低峰主动触发回源。通过向Apache代理发送与真实用户请求一致的HTTP请求,让mod_cache检查缓存,未命中则回源并把响应保存到磁盘。这样当真实用户到来时,缓存已经准备好。适合缓存预热的主要是静态资源、商品详情页、文章页、公开API响应等不包含用户私有数据的可缓存内容。登录后的页面或包含Cookie的请求需要谨慎处理,否则容易把个性化内容缓存给所有用户。
预热任务的价值取决于缓存有效期和URL数量。如果缓存TTL只有几分钟,定时任务需要足够频繁;如果TTL较长,可以每小时或每天执行一次。预热本身也会消耗源站资源,因此需要限制并发、控制总请求量,避免把低峰期变成回源压力高峰。
二、基于cron和curl的预热脚本实现
最简单的预热方式是准备一个URL列表,然后用curl依次请求。为了让请求经过Apache代理并命中缓存键,必须使用与真实客户端一致的Host头,并保持URL规范化形式。比如真实用户访问的是https://www.ipipp.com/product/123,预热脚本不能只请求http://127.0.0.1/product/123,因为Host不同会导致缓存键不一致,预热写入的缓存条目与用户请求的键对不上。
下面是一个基础脚本示例。它从urls.txt读取相对路径,通过本地Apache的8080端口发起请求,并设置Host头为真实域名。脚本使用curl的-s -o /dev/null -w输出状态码和耗时,便于记录。如果返回非200状态码,可以重试一次。
#!/bin/bash
# Apache缓存预热脚本
HOST_HEADER="www.ipipp.com"
APACHE_PROXY="http://127.0.0.1:8080"
URL_FILE="/opt/warmup/urls.txt"
LOG_FILE="/var/log/apache-cache-warmup.log"
if [ ! -f "$URL_FILE" ]; then
echo "URL list not found: $URL_FILE"
exit 1
fi
while IFS= read -r path; do
[ -z "$path" ] && continue
url="${APACHE_PROXY}${path}"
code=$(curl -s -o /dev/null -w "%{http_code}" -H "Host: ${HOST_HEADER}" "$url")
if [ "$code" != "200" ]; then
echo "$(date '+%F %T') retry $path first status $code" >> "$LOG_FILE"
sleep 1
code=$(curl -s -o /dev/null -w "%{http_code}" -H "Host: ${HOST_HEADER}" "$url")
fi
echo "$(date '+%F %T') $path $code" >> "$LOG_FILE"
done < "$URL_FILE"
注意脚本中的&&和>>在HTML源码中已做转义,实际运行时是正常的逻辑与和重定向符。curl的-H "Host: ..."非常关键,它决定了mod_cache用什么Host键存储缓存。如果Apache配置了基于域名的虚拟主机,Host头还会影响路由到哪个后端。
把脚本加入crontab定时执行。以下示例每小时第5分钟运行一次,并以www-data用户身份执行,因为缓存目录和日志通常属于该用户。
# /etc/cron.d/apache-cache-warmup 5 * * * * www-data /opt/warmup/warmup.sh > /dev/null 2>&1
如果URL数量较多,串行执行会很慢。可以在脚本中加入xargs并发,例如cat urls.txt | xargs -P 8 -I {} curl ...,但并发过高会瞬间打满源站。建议从2到4个并发开始,逐步观察源站QPS和错误率。
三、动态生成热点URL清单
静态的urls.txt需要人工维护,时间长了会遗漏新增内容。更合理的做法是从Apache的访问日志中分析热点URL,自动生成预热列表。比如每天凌晨统计前一天访问量最高的200个GET请求,排除静态资源或带参数的个性化路径,再交给预热脚本执行。
以下是一个简单的awk分析命令。假设access.log使用combined格式,第7个字段是请求行,其中包含方法和路径。先用grep过滤GET请求,再用awk提取路径,去重统计,最后按次数降序取前200条。
#!/bin/bash
ACCESS_LOG="/var/log/apache2/access.log"
OUTPUT_FILE="/opt/warmup/urls.txt"
TMP_FILE="/tmp/warmup-top.tmp"
grep '"GET ' "$ACCESS_LOG" | awk '{print $7}' | cut -d'?' -f1 | sort | uniq -c | sort -rn | head -200 | awk '{print $2}' > "$OUTPUT_FILE"
这里用cut -d'?' -f1去掉查询字符串,因为缓存键通常忽略查询字符串或单独处理,保留路径可以避免重复。如果站点使用查询参数区分内容,比如/article?id=123,则不能粗暴去掉问号后的部分,需要结合业务判断。建议在生成清单后人工抽查,确保没有包含退出登录、订单回调等敏感路径。
对于路径中包含中文或特殊字符,awk和grep处理可能出问题。可以在生成后使用curl --data-urlencode或直接保留原始文本。还可以把清单生成和预热放在同一个脚本中,先分析日志再执行预热,避免中间文件权限问题。
四、避免预热任务带来的缓存污染与安全问题
预热不是简单请求一遍URL就行。如果请求带了额外的Cookie或Authorization头,Apache可能基于这些头产生不同的缓存键,导致之后真实用户不带该头时无法命中。因此预热请求应该尽量模拟最普通的匿名用户:不发送Cookie,不发送Authorization,只携带必要的User-Agent和Accept头。如果后端根据User-Agent返回不同内容,而真实用户的UA五花八门,缓存键可能会分裂,预热收益会打折扣。
另一个常见问题是缓存了错误的状态码。mod_cache默认只缓存200、203、300、301、410等可缓存状态。如果预热请求返回302重定向到登录页,脚本记录的状态码是302,可能没有写入有效缓存。应该在预热脚本中检查最终URL和状态码,确认确实得到200而不是登录跳转。curl的-L选项可以跟随重定向,但要避免把登录页作为最终内容缓存下来。建议对返回内容做简单校验,比如用curl -s抓取HTML,检查是否包含登录表单关键词,或者检查Content-Type。
缓存目录的磁盘空间也需要监控。预热会写入大量缓存文件,如果磁盘满了,Apache会报错并可能停止缓存。脚本可以加入清理逻辑,只预热最近N天内的热点URL,并配合Apache的CacheMaxExpire和CacheDefaultExpire控制缓存时长。对于需要认证或涉及用户隐私的路径,应在Apache配置中用CacheDisable排除,确保预热任务不会碰这些地址。
最后,定时任务的执行时机要避开备份、日志切割和源站发布窗口。如果源站每晚有维护窗口,预热任务最好在维护结束后运行,以免预热到旧内容或大量失败。可以用cron的随机延迟来分散不同服务器的预热开始时间,比如在脚本开头sleep一个随机秒数。
Apache缓存预热反向代理定时任务修改时间:2026-09-25 15:36:14