在LNMPA中实现代理缓存proxy_cache
2015年在linode上了一款VPS至今,而且不知道当时怎么想的,选用的是LNMPA架构,不过确实稳定!服务器环境经过优化,在国内访问也基本在2s以内。相比基本600ms来比,确实相差有点大,但考虑到配置和国内外差异,也因为没有配置CDN,所以基本上还能接受。我所有WordPress都没有安装cache插件,不安装cache插件主要是不喜欢太依赖第三方工具。最近有点时间,将proxy cache配置上,速度直接到达300ms秒开。发现proxy cache对于外贸独立站或是博客来说是非常合适的,而且可以同时应用到所有的外贸独立站上,比单个站配置Cache插件更快,更直接,维护也更方便。先理一下优缺点,记录一下具体配置。
优缺点比较
proxy cache原理就是将所有静态,甚至动态数据直接输出为cache保存在内存或是硬盘上,当被访问时直接从cache中读取,以快速替代数据库查询和资源调配
优点
- 显著提升响应速度:缓存命中时,响应直接从内存或磁盘读取,延迟可降低 90% 以上。
- 大幅降低后端负载:缓存静态或半静态内容(如 HTML、图片、API 响应),避免重复请求后端,假设某接口 QPS 为 1000,缓存命中率 80%,则后端实际负载降至 200 QPS。
- 增强服务可用性:若后端故障,可配置 proxy_cache_use_stale 继续返回旧缓存(如商品详情页),防止页面404。
- 节省带宽成本:CDN 边缘节点使用 Nginx 缓存,减少回源流量,降低带宽消耗。
- 精细灵活的缓存策略:按状态码缓存(proxy_cache_valid 200 302 10m;),按请求方法控制(proxy_cache_methods GET HEAD;)动态条件缓存(通过 proxy_cache_bypass 和 proxy_no_cache 跳过特定请求。)
缺点
- 缓存一致性问题:比如,商品价格更新后,用户可能看到旧价格(缓存未失效)或是,动态内容(如用户会话数据)误缓存导致逻辑错误。解决方案是:设置合理的 Cache-Control 头或手动清除缓存(proxy_cache_purge)。
- 内存与磁盘资源消耗:缓存 1GB 数据约占用 1.2GB 磁盘空间(含元数据),高并发下内存缓存(proxy_cache_path 的 keys_zone)可能成为瓶颈。解决方案是:使用多级缓存(如内存 + SSD),限制 max_size 防止磁盘写满。
- 配置复杂度高:缓存键(proxy_cache_key)设计不当导致冗余或遗漏。未处理 Vary 头,导致不同设备/语言收到错误缓存。解决方案是:擅用分析调试工具或使用add_header X-Cache-Status $upstream_cache_status; 显示缓存命中状态来解决错误。
- 缓存击穿与雪崩:热点 Key 失效瞬间,大量请求穿透到后端。或是大量 Key 同时过期,后端压力骤增。解决方案:复杂点使用互斥锁,通过 Lua 脚本(OpenResty)实现原子化缓存更新。简单点,随机化TTL,合理使用proxy_cache_valid 200 随机时间;
通常对于一般的外贸独立站或是博客来讲,配置并不是太复杂,也很难出现缓存击穿或雪崩的情况。而对于综合性或电商网站,他们有更好的缓存方案,也较少使用proxy_cache. 下面也记录一下在LNMPA上配置proxy_cache的具体方法。
配置环境
LNMPA环境是LNMP.org提供的独立主机生产环境的Shell程序。LNMPA即为Linux+Nginx+MySQL+PHP+Apache, 这是一种特殊于大众常用的LNMP架构之外的以Apche为后端PHP处理php_mod结构的apache2handler。因为LNMP使用的是PHP-FPM (FastCGI Process Manager),所以以下proxy_cache可能并不适用于LNMP结构中。LNMP组合的缓存为fastcgi_cache。
Proxy_Cache配置流程
在nginx.conf中的通用配置
proxy_cache_path /var/cache/nginx_proxy levels=1:2 keys_zone=proxycache:200m inactive=24h max_size=10g use_temp_path=off;
proxy_cache_key "$host$request_uri";
proxy_cache_methods GET HEAD;
proxy_cache_min_uses 1;
proxy_cache_valid 200 12h;
proxy_cache_valid 301 24h;
proxy_cache_valid 302 1h;
proxy_cache_valid any 12h;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_revalidate on;
proxy_cache_background_update on;
proxy_cache_lock on;
proxy_cache_lock_timeout 5s;
proxy_cache_lock_age 50s;
proxy_headers_hash_max_size 1024;
proxy_headers_hash_bucket_size 128;
重点记录下proxy_cache_key,尽量简洁一点比较好,"$host$request_uri";使用主机名+参数结合,即使VPS下有几个不同的网站,主机名也能很好地区分不同的缓存。当然,如果需要将不同网站的缓存分开存放,在需要在各个网站.conf下单独设置proxy_cache_path
设定动态条件缓存
因为我的站基本都是以WordPress为架构的外贸独立站,所以设置条件相同,wp-admin基本不用缓存。
map $sent_http_content_type $skip_cache_content_type {
default 0;
"~application/json" 1;
}
# Skip Configuring
map $request_uri $decoded_uri {
default $request_uri;
~%3F ?;
~%26 &;
~%2B +;
}
map $decoded_uri $skip_cache {
default 0;
~^/wp-admin/ 1;
~^/wp-login\.php 1;
~^/wp-cron\.php 1;
~^/xmlrpc\.php 1;
~^/admin-ajax\.php 1;
~^/sitemap(_index)?\.xml 1;
~^/form/[^/]+\.php 1;
~^/page-(forms|slider|video)\.php 1;
~^/search\.php 1;
~^/wp-comments-post\.php 1;
~*(^|&)(token|password)= 1;
~*wp-postpass 1;
~*wordpress_no_cache 1;
~*woocommerce_items_in_cart 1;
~*wp_woocommerce_session_ 1;
}
# Skip PROXY CACHE
proxy_cache_bypass $arg_nocache $arg_comment $cookie_nocache $cookie_session $skip_cache_content_type $skip_cache;
proxy_no_cache $http_authorization $skip_cache;
解说:
- application/json数据不用缓存,因为JSON通常由API或AJAX产生,缓存会导致数据不一致
- 不缓存~*wp-settings-/~*wordpress_logged_in_/~*wordpress_[a-f0-9]+,因为在WordPress页面中,都会产生此结构cookie,如果直接跳过此cookie不缓存,所有页面都不会缓存,直接BYPASS
- 特殊一点就是要跳过~*(^|&)(token|password)=,这是针对密码或是token验证,必须不能缓存,缓存导致新旧数据不一致。
添加缓存点
上述提到的LNMPA架构与LNMP是不同的,所以它的缓存点就放在以下两个location中。
location @apache
{
internal;
proxy_pass http://127.0.0.1:88;
proxy_cache proxycache;
include proxy.conf;
}
location ~ [^/]\.php(/|$)
{
proxy_pass http://127.0.0.1:88;
proxy_cache proxycache;
include proxy.conf;
}
而以下根location是不需要放置的:
location /
{
try_files $uri @apache;
}
location /{} 只是 try_files,并不会直接代理到 Apache,因此不需要添加 proxy_cache;在 location @apache 里使用了 proxy_cache proxycache;,这样可以缓存静态页面(HTML、JSON等)。
重点:代理PHP请求通常不适合使用proxy_cache,因为PHP代码的执行结果通常是动态的。但我主要是外贸独立站,所有这些数据,内容都是保存在MySQL中,且是不会或很少有更新会变化的。但如果每次访问都要从数据中调用,必然需要更长的访问时间和占据更多的资源,为何不把这些不常变化的数据全部缓存,这就是为什么在location ~ [^/]\.php(/|$)添加proxy_cache,主要目的就是要将整个网站的PHP数据全部缓存。
手动清除缓存
因为我们已经在通用配置中设置proxy_cache_valid缓存时间,我把它设置了12h,也就是说,当你发布了新的内容,12小时内,读者是看不到的,因为你的缓存一直是旧的。所以这里就需要手动清除缓存,将新的内容也立刻缓存起来且能正常访问。
以下配置了两个手动清除缓存的方法,一是在前面直接刷新,二是在WP后台,使用手动清除缓存还必须要用到proxy_cache_purge命令,但这个默认不是在nginx内置模块中的,必然要安装动态模块来实现。
安装ngx_http_cache_purge_module模块
从https://github.com/nginx-modules/ngx_cache_purge/releases下载后 通过编译(--add-dynamic-module=/usr/src/lnmp2.1/src/ngx_cache_purge-2.5.3 --with-compat),make后,生成objs/ngx_http_cache_purge_module.so,make install后会自动拷贝文件cp /usr/src/lnmp2.1/src/nginx-1.26.3/objs/ngx_http_cache_purge_module.so load_module到/usr/local/nginx/modules/ngx_http_cache_purge_module.so;如果只是后期增加不make install,就要手动CP;最后在nginx.conf的event事件之前load_module load_module /usr/local/nginx/modules/ngx_http_cache_purge_module.so; 根据服务器环境不同,有可能在此位置/usr/lib/modules/ngx_http_cache_purge_module.so;
在ngx_http_cache_purge_module模块安装好后,然后就是在proxy-pass-php.conf或是nginx.conf下增加purge处理块
location /purge_cache
{
limit_req zone=purge burst=10 nodelay;
valid_referers none blocked server_names;
if ($invalid_referer) {
return 403 "Forbidden: Invalid Referer";
}
proxy_cache_purge proxycache "$arg_key";
post_action @apache;
}
location /purge
{
limit_req zone=purge burst=10 nodelay;
valid_referers none blocked server_names;
if ($invalid_referer) {
return 403 "Forbidden: Invalid Referer";
}
if ($http_x_purge_secret != 'colinqi') {
return 403;
}
if ($arg_key = "") {
return 400;
}
proxy_set_header If-Match "";
proxy_set_header If-None-Match "";
proxy_set_header If-Modified-Since "";
proxy_set_header If-Unmodified-Since "";
proxy_set_header If-Range "";
access_log /var/log/nginx_purge.log purge_log;
proxy_cache_purge proxycache "$arg_key";
add_header X-ProxyCache-Purge "$arg_key";
error_page 404 @purge_not_found;
error_page 500 @purge_error;
return 200;
}
在上述/purge_cache location中主要是用来处理前端JS刷新强制更新缓存的,JS代码如下:
(function () {
'use strict';
//v1
const perf = window.performance;
const loc = window.location;
const doc = document;
function getOrigin() {
return loc.origin || loc.protocol + '//' + loc.host;
}
const origin = getOrigin();
const isForceRefresh = () => {
// Modern browsers (Chrome 57+, Firefox 50+)
if (perf.getEntriesByType) {
const navEntries = perf.getEntriesByType('navigation');
if (navEntries.length > 0 && navEntries[0].type === 'reload') {
return true;
}
}
// Old browsers (IE 10+, Safari 9+)
if (perf.navigation) {
return perf.navigation.type === 1;
}
// Referer detection with performance timing for fast reloads
try {
//const isSameOrigin = doc.referrer.startsWith(origin); //for better performance
const isSameOrigin = doc.referrer.indexOf(origin) === 0; //for better compatibility
const timing = perf.timing;
const loadTime = (timing.loadEventEnd && timing.navigationStart)
? timing.loadEventEnd - timing.navigationStart
: Infinity;
return isSameOrigin && loadTime < 2000;
} catch (e) {
return false;
}
};
const purgeCache = async () => {
if (isForceRefresh()) {
const loc = window.location;
const currentUrl = loc.href;
const purgeURL = new URL('/purge_cache', origin);
const cacheKey = [loc.host, loc.pathname, loc.search].join('');
const purgeURLWithParams = purgeURL + '?key=' + cacheKey;
console.log("Generated cacheKey:", cacheKey);
console.log(purgeURLWithParams);
try {
const response = await fetch(purgeURLWithParams, {
method: 'GET',
headers: {
'Cache-Control': 'no-cache, no-store',
'Pragma': 'no-cache'
},
});
if (response.ok) {
window.location.href = currentUrl;
} else {
console.error('Proxy Cache Purge Failed:', response.statusText);
}
} catch (error) {
console.error('Proxy Cache Purge Failed:', error);
}
}
};
purgeCache();
})();
在上述/purge location中主要是用来处理WP后面通过PHP刷新强制更新缓存的,PHP代码如下:
// AJAX handlers
array_map(static fn($action) => add_action("wp_ajax_$action", "handle_ajax_$action"), [
'purge_proxycache', 'cq_flush_cache', 'run_gc_collect_cycles', 'set_xdebug_gcstats', 'check_redis_socket'
]);
function handle_ajax_purge_proxycache() {
// AJAX handlers - ProxyCache Purge
if (!check_ajax_referer('cq_theme_colinqi_nonce', 'nonce', false)) {
wp_send_json_error('Security check failed', 403);
}
if (!current_user_can('manage_options')) {
wp_send_json_error('Permission denied', 403);
}
$result = nginx_purge_cache();
if ($result['success']) {
wp_send_json_success([
'message' => $result['message'],
'headers' => $result['headers'],
]);
} else {
wp_send_json_error([
'message' => $result['message'],
'headers' => $result['headers'],
'code' => $result['code'],
], $result['code'] ?? 500);
}
}
function nginx_purge_cache($key = null) {
$key = $key ?? generate_proxy_cache_key();
$purge_url = add_query_arg(['key' => $key], home_url('/purge'));
$response = wp_remote_get($purge_url, [
'headers' => [
'X-Purge-Secret' => 'colinqi',
],
'sslverify' => false,
]);
if (is_wp_error($response)) {
return [
'success' => false,
'message' => 'Connection failed: ' . $response->get_error_message(),
'code' => 500
];
}
//cq_debug("Purge URL: $purge_url");
// Check HTTP status code from Nginx
$status_code = wp_remote_retrieve_response_code($response);
$is_success = in_array($status_code, [200, 204]);
$status_message = wp_remote_retrieve_response_message($response);
$headers = wp_remote_retrieve_headers($response)->getAll();
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
$url = $response['http_response']->get_response_object()->url;
if ($is_success) {
return [
'success' => $is_success,
'message' => $status_code.'('.$status_message.'): '.$url,
'headers' => $headers,
'code' => $status_code
];
} else {
return [
'success' => $is_success,
'message' => $status_code.'('.$status_message.'): '.$url,
'headers' => $headers,
'code' => $status_code
];
}
}
function generate_proxy_cache_key($url = null) {
$url = $url ?? home_url();
$parsed = parse_url($url);
$host = $parsed['host'] ?? '';
$path = $parsed['path'] ?? '/';
$query = isset($parsed['query']) ? '?' . $parsed['query'] : '';
return $host . $path . $query;
}
以上PHP代码执行是通过JS的AJAX来的,所以仍需要调用AJAX代码:
$('#ProxyCachePurge').on('click', function(event) {
event.preventDefault();
$('#ProxyCachePurgeResult').text('Purging Proxy Cache...');
$.ajax({
url: admin_ajax.ajax_url,
type: "POST",
dataType: 'json',
data: {
action: 'purge_proxycache',
nonce: admin_ajax.nonce,
},
success: function(response) {
if (response.success) {
$('#ProxyCachePurgeResult').text(response.data.message);
console.log(response.data);
} else {
$('#ProxyCachePurgeResult').text('Error: ' + response.data);
}
},
error: function(jqXHR, textStatus, errorThrown) {
let errorMsg = '';
// Attempt to parse JSON error (from WordPress)
try {
const response = JSON.parse(jqXHR.responseText);
errorMsg = response.data?.message || response.message || response.responseText;
} catch (e) {
// Fallback to plain text
errorMsg = jqXHR.responseText || errorThrown || textStatus;
}
console.log(jqXHR);
$('#ProxyCachePurgeResult').text('errorThrown: ' + errorMsg);
}
});
});
上述AJAX代码放在theme-extend.js中,同时要调用WP-AJAX处理程序
add_action('admin_enqueue_scripts', static function(string $hook): void {
if ($hook !== 'tools_page_theme_extend') return;
// Enqueue the theme settings script
$src = get_template_directory_uri() . '/includes/module/theme-extend.js';
$ver = file_exists($src) ? filemtime($src) : null ;
wp_enqueue_script('theme-extend-js', $src, ['jquery'], $ver, true);
wp_localize_script('theme-extend-js', 'admin_ajax', [
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('cq_theme_colinqi_nonce')
]);
});
最后是WP后面PHP提交是通过button实现的
<div id="proxycache_status" class="cache-tab-content" style="display:none;">
<h2>Proxy Cache</h2>
<button id="ProxyCachePurge" class="button">ProxyCache Purge</button>
<div id="ProxyCachePurgeResult"></div>
</div>
The End.
Leave a Reply
You must be logged in to post a comment.